From 954f8f5a1a5cd1282e471bcd5727dd5bffc455d6 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 15 Apr 2025 22:25:25 +0800 Subject: [PATCH 001/182] Kademlia codes --- .../Kademlia/Hash256XorUtilsTests.cs | 85 ++++ .../Kademlia/KBucketTests.cs | 86 ++++ .../Kademlia/KademliaSimulation.cs | 462 ++++++++++++++++++ .../Kademlia/KademliaTests.cs | 185 +++++++ .../Kademlia/BucketAddResult.cs | 11 + .../Kademlia/BucketListRoutingTable.cs | 161 ++++++ .../Kademlia/Content/IContentHashProvider.cs | 11 + .../Kademlia/Content/IContentMessageSender.cs | 19 + .../Kademlia/Content/IKademliaContent.cs | 20 + .../Kademlia/Content/IKademliaContentStore.cs | 14 + .../Content/IServiceCollectionExtensions.cs | 36 ++ .../Kademlia/Content/KademliaContent.cs | 64 +++ .../Content/KademliaContentMessageReceiver.cs | 28 ++ .../Kademlia/DoubleEndedLru.cs | 116 +++++ .../Kademlia/Hash256XORUtils.cs | 125 +++++ .../Kademlia/IKademlia.cs | 62 +++ .../Kademlia/IKademliaMessageSender.cs | 17 + .../Kademlia/ILookupAlgo.cs | 31 ++ .../Kademlia/INodeHashProvider.cs | 21 + .../Kademlia/IRoutingTable.cs | 18 + .../Kademlia/IServiceCollectionExtensions.cs | 59 +++ .../Kademlia/KBucket.cs | 91 ++++ .../Kademlia/KBucketTree.cs | 405 +++++++++++++++ .../Kademlia/Kademlia.cs | 147 ++++++ .../Kademlia/KademliaConfig.cs | 58 +++ .../Kademlia/KademliaMessageReceiver.cs | 21 + .../Kademlia/NewLookupKNearestNeighbour.cs | 228 +++++++++ .../Kademlia/NodeHealthTracker.cs | 115 +++++ .../OriginalLookupKNearestNeighbour.cs | 169 +++++++ 29 files changed, 2865 insertions(+) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs new file mode 100644 index 000000000000..e8ba34053099 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.Intrinsics; +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class Hash256XorUtilsTests +{ + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", 0)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 256)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0xF000000000000000000000000000000000000000000000000000000000000000", 256)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0xE000000000000000000000000000000000000000000000000000000000000000", 256)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x7000000000000000000000000000000000000000000000000000000000000000", 255)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0F00000000000000000000000000000000000000000000000000000000000000", 252)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0E00000000000000000000000000000000000000000000000000000000000000", 252)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0700000000000000000000000000000000000000000000000000000000000000", 251)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x000E000000000000000000000000000000000000000000000000000000000000", 244)] + public void TestDistance(string hash1, string hash2, int expectedDistance) + { + Hash256XorUtils.CalculateDistance(new ValueHash256(hash1), new ValueHash256(hash2)).Should().Be(expectedDistance); + Hash256XorUtils.CalculateDistance(new ValueHash256(hash2), new ValueHash256(hash1)).Should().Be(expectedDistance); + } + + [Test] + public void TestGetRandomHash() + { + Random rand = new Random(0); + ValueHash256 randomized = new ValueHash256(); + rand.NextBytes(randomized.BytesAsSpan); + + void TestForDistance(int distance) + { + var randHash = Hash256XorUtils.GetRandomHashAtDistance(randomized, distance, rand); + Hash256XorUtils.CalculateDistance(randomized, randHash).Should().Be(distance); + } + + for (int i = 1; i < 256; i++) + { + rand = new Random(0); + for (int j = 0; j < 10; j++) + { + TestForDistance(i); + } + } + + } + + [TestCase] + public void TestDistanceCompare() + { + ValueHash256 h1 = new ValueHash256("0x0010000000000000000000000000000000000000000000000000000000000000"); + ValueHash256 h2 = new ValueHash256("0x0110000000000000000000000000000000000000000000000000000000000000"); + ValueHash256 h3 = new ValueHash256("0x0000000000000000000000000000000000000000000000000000000000000000"); + + Hash256XorUtils.Compare(h1, h2, h3).Should().BeLessThan(0); + } + + [TestCase] + public void Strange() + { + ValueHash256 a = new ValueHash256("0x1a0c466f5d75e4d8ad6765d5f519dbc82b7c343b37f88500ec5e64005393b30d"); + ValueHash256 b = new ValueHash256("0x82bf3eb6be6c2d15511b0dc6c68c97bad52b834b11656c6104af44123e565a3d"); + + Vector aBig = new Vector(a.BytesAsSpan); + Vector bBig = new Vector(b.BytesAsSpan); + + ValueHash256 xored = new ValueHash256(); + (aBig ^ bBig).CopyTo(xored.BytesAsSpan); + + Console.Error.WriteLine($"The three {a} {b} {xored}"); + + // Hash256DistanceCalculator calculator = new Hash256DistanceCalculator(); + // Console.Error.WriteLine($"Distance {calculator.CalculateDistance(a, b)} {calculator.BigIntLogDist(a, b)}"); + // Console.Error.WriteLine($"Distanceb {calculator.BigIntDist(a, b)} "); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs new file mode 100644 index 000000000000..86d600fa824b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class KBucketTests +{ + [Test] + public void TryAddOrRefresh_ShouldLimitToK() + { + KBucket bucket = new(5); + + ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); + + foreach (ValueHash256 valueHash256 in toAdd) + { + bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + } + + // Again + foreach (ValueHash256 valueHash256 in toAdd) + { + bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + } + + bucket.GetAll().ToHashSet().Should().BeEquivalentTo(toAdd[..5].ToHashSet()); + bucket.GetAllWithHash().Select(it => it.Item2).ToHashSet().Should().BeEquivalentTo(toAdd[..5].ToHashSet()); + + foreach (ValueHash256 valueHash256 in toAdd[..5]) + { + bucket.ContainsNode(valueHash256).Should().BeTrue(); + bucket.GetByHash(valueHash256).Should().NotBeNull(); + } + } + + [Test] + public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() + { + KBucket bucket = new(5); + + ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); + + foreach (ValueHash256 valueHash256 in toAdd) + { + bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + } + + ValueHash256[] nodes = bucket.GetAll(); + + foreach (ValueHash256 valueHash256 in toAdd) + { + bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + } + + bucket.GetAll().Should().BeSameAs(nodes); + } + + [Test] + public void RemoteAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() + { + KBucket bucket = new(5); + + ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); + + foreach (ValueHash256 valueHash256 in toAdd) + { + bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + } + + bucket.RemoveAndReplace(toAdd[0]); + + bucket.GetAll().ToHashSet() + .Should() + .BeEquivalentTo((toAdd[1..5].Concat(toAdd[9..10])).ToHashSet()); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs new file mode 100644 index 000000000000..b4282b33f7d2 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -0,0 +1,462 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Kademlia.Content; +using NonBlocking; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +[TestFixture(true, true, 3, 0)] +[TestFixture(false, true, 3, 0)] +[TestFixture(true, false, 3, 0)] +[TestFixture(true, true, 3, 4)] +[TestFixture(true, true, 1, 0)] +[TestFixture(true, true, 1, 4)] +public class KademliaSimulation +{ + private readonly KademliaConfig _config; + + public KademliaSimulation(bool useNewLookup, bool useTreeBasedTable, int alpha, int beta) + { + _config = new KademliaConfig() + { + KSize = 20, + Alpha = alpha, + Beta = beta, + UseNewLookup = useNewLookup, + UseTreeBasedRoutingTable = useTreeBasedTable + }; + } + + private TestFabric CreateFabric() + { + return new TestFabric(_config); + } + + [Test] + public async Task TestBootstrap() + { + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(500); + + TestFabric fabric = CreateFabric(); + Random rand = new Random(0); + + ValueHash256 node1Hash = RandomKeccak(rand); + ValueHash256 node2Hash = RandomKeccak(rand); + ValueHash256 node3Hash = RandomKeccak(rand); + + Kademlia node1 = fabric.CreateNode(node1Hash); + Kademlia node2 = fabric.CreateNode(node2Hash); + Kademlia node3 = fabric.CreateNode(node3Hash); + + node1.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray().Should().BeEquivalentTo([node1Hash]); + + node1.AddOrRefresh(new TestNode(node2Hash)); + node2.AddOrRefresh(new TestNode(node3Hash)); + + node1.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray().Should().BeEquivalentTo([node1Hash, node2Hash]); + node2.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray().Should().BeEquivalentTo([node2Hash, node3Hash]); + node3.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray().Should().BeEquivalentTo([node3Hash]); + + // await node2.Bootstrap(cts.Token); + // node2.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToHashSet().Should().BeEquivalentTo([node2Hash, node3Hash]); + + await node1.Bootstrap(cts.Token); + + node1.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToHashSet().Should().BeEquivalentTo([node1Hash, node2Hash, node3Hash]); + node2.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToHashSet().Should().BeEquivalentTo([node1Hash, node2Hash, node3Hash]); + // node3.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToHashSet().Should().BeEquivalentTo([node1Hash, node2Hash, node3Hash]); + } + + [Test] + public async Task TestLookup() + { + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(500); + + TestFabric fabric = CreateFabric(); + Random rand = new Random(0); + + ValueHash256 node1Hash = RandomKeccak(rand); + ValueHash256 node2Hash = RandomKeccak(rand); + ValueHash256 node3Hash = RandomKeccak(rand); + + Kademlia node1 = fabric.CreateNode(node1Hash); + Kademlia node2 = fabric.CreateNode(node2Hash); + KademliaContent node1Content = fabric.GetKademliaContent(node1Hash); + KademliaContent node2Content = fabric.GetKademliaContent(node2Hash); + fabric.CreateNode(node3Hash); + + node1.AddOrRefresh(new TestNode(node2Hash)); + node2.AddOrRefresh(new TestNode(node3Hash)); + + await fabric.Bootstrap(cts.Token); + + (await node1Content.LookupValue(node2Hash, cts.Token)).Should().BeEquivalentTo(node2Hash); + (await node1Content.LookupValue(node3Hash, cts.Token)).Should().BeEquivalentTo(node3Hash); + } + + [Test] + public async Task TestKNearestNeighbour() + { + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(500); + + TestFabric fabric = CreateFabric(); + Random rand = new Random(0); + + ValueHash256 node1Hash = RandomKeccak(rand); + ValueHash256 node2Hash = RandomKeccak(rand); + ValueHash256 node3Hash = RandomKeccak(rand); + + Kademlia node1 = fabric.CreateNode(node1Hash); + + (await node1.LookupNodesClosest(node1Hash, cts.Token)) + .Select(n => n.Hash) + .ToHashSet() + .Should() + .BeEquivalentTo(new HashSet() {node1Hash }); + + Kademlia node2 = fabric.CreateNode(node2Hash); + fabric.CreateNode(node3Hash); + + node1.AddOrRefresh(new TestNode(node2Hash)); + node2.AddOrRefresh(new TestNode(node3Hash)); + + await fabric.Bootstrap(cts.Token); + + (await node1.LookupNodesClosest(node2Hash, cts.Token)) + .Select(n => n.Hash) + .ToHashSet() + .Should() + .BeEquivalentTo(new HashSet() {node1Hash, node2Hash, node3Hash }); + + (await node1.LookupNodesClosest(node3Hash, cts.Token, 1)) + .First().Hash + .Should() + .Be(node3Hash); + } + + [Test] + public async Task SimulateLargeLookupValue() + { + int nodeCount = 500; + + TestFabric fabric = CreateFabric(); + Random rand = new Random(0); + ValueHash256 mainNodeHash = RandomKeccak(rand); + Kademlia mainNode = fabric.CreateNode(mainNodeHash); + KademliaContent mainNodeContent = fabric.GetKademliaContent(mainNodeHash); + + List nodeIds = new(); + for (int i = 0; i < nodeCount; i++) + { + ValueHash256 nodeHash = RandomKeccak(rand); + Kademlia kad = fabric.CreateNode(nodeHash); + kad.AddOrRefresh(new TestNode(mainNodeHash)); + nodeIds.Add(nodeHash); + } + + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(10)); + + Stopwatch sw = Stopwatch.StartNew(); + fabric.SimulateLatency = false; // Bootstrap is so slow, latency simulation is disable for it. + await fabric.Bootstrap(cts.Token); + TimeSpan bootstrapDuration = sw.Elapsed; + sw.Restart(); + fabric.SimulateLatency = true; + + fabric.FindValueCount = 0; + + foreach (ValueHash256 node in nodeIds) + { + (await mainNodeContent.LookupValue(node, cts.Token)).Should().BeEquivalentTo(node); + } + TimeSpan queryDuration = sw.Elapsed; + + TestContext.Out.WriteLine($"FindValue count per lookup {fabric.FindValueCount / (double)nodeIds.Count}"); + TestContext.Out.WriteLine($"FindNeighbour count {fabric.FindNeighbourCount}"); + TestContext.Out.WriteLine($"Bootstrap duration: {bootstrapDuration}"); + TestContext.Out.WriteLine($"Query duration: {queryDuration}"); + } + + [Test] + public async Task SimulateLargeKNearestNeighbour() + { + int nodeCount = 500; + + TestFabric fabric = CreateFabric(); + Random rand = new Random(0); + ValueHash256 mainNodeHash = RandomKeccak(rand); + Kademlia mainNode = fabric.CreateNode(mainNodeHash); + + List nodeIds = new(); + for (int i = 0; i < nodeCount; i++) + { + ValueHash256 nodeHash = RandomKeccak(rand); + Kademlia kad = fabric.CreateNode(nodeHash); + kad.AddOrRefresh(new TestNode(mainNodeHash)); + nodeIds.Add(nodeHash); + } + + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(10)); + + Stopwatch sw = Stopwatch.StartNew(); + // This test is really slow. Slower than find value which can short circuit once it find the value. + fabric.SimulateLatency = false; + await fabric.Bootstrap(cts.Token); + TimeSpan bootstrapDuration = sw.Elapsed; + sw.Restart(); + fabric.FindNeighbourCount = 0; + + int closestKCount = 0; + int missedCount = 0; + + foreach (ValueHash256 targetNode in nodeIds) + { + var nodesClosest = await mainNode.LookupNodesClosest(targetNode, cts.Token); + var expectedNodeClosestK = nodeIds + .Order(Comparer.Create((n1, n2) => Hash256XorUtils.Compare(n1, n2, targetNode))) + .Take(_config.KSize) + .ToHashSet(); + + nodesClosest.Length.Should().Be(_config.KSize); + + foreach (TestNode node in nodesClosest) + { + if (expectedNodeClosestK.Contains(node.Hash)) + { + closestKCount++; + } + else + { + missedCount++; + } + } + } + TimeSpan queryDuration = sw.Elapsed; + double totalNodesReturned = nodeIds.Count * _config.KSize; + + (closestKCount / totalNodesReturned).Should().BeGreaterThan(0.95); + + TestContext.Out.WriteLine($"Closest K ratio {closestKCount / totalNodesReturned}"); + TestContext.Out.WriteLine($"Missed ratio {missedCount / totalNodesReturned}"); + TestContext.Out.WriteLine($"FindNeighbour count per lookup {fabric.FindNeighbourCount / (double)nodeIds.Count}"); + TestContext.Out.WriteLine($"FindNeighbour count {fabric.FindNeighbourCount}"); + TestContext.Out.WriteLine($"Bootstrap duration: {bootstrapDuration}"); + TestContext.Out.WriteLine($"Query duration: {queryDuration}"); + } + + private static ValueHash256 RandomKeccak(Random rand) + { + ValueHash256 val = new ValueHash256(); + rand.NextBytes(val.BytesAsSpan); + return val; + } + + private class OnlySelfIKademliaContentStore(ValueHash256 self) : IKademliaContentStore + { + public bool TryGetValue(ValueHash256 hash, out ValueHash256 value) + { + if (hash != self) + { + value = null; + return false; + } + + value = self; + return true; + } + } + + private class ValueHashNodeHashProvider: INodeHashProvider, IContentHashProvider + { + public ValueHash256 GetHash(TestNode node) + { + return node.Hash; + } + + public ValueHash256 GetHash(ValueHash256 key) + { + return key; + } + } + + private class TestFabric(KademliaConfig config) + { + internal long PingCount = 0; + internal long FindValueCount = 0; + internal long FindNeighbourCount = 0; + + private int _baseLatency = 5; + private int _randomLatency = 2; + public bool SimulateLatency { get; set; } = false; + + internal ConcurrentDictionary _nodes = new(); + readonly ValueHashNodeHashProvider _nodeHashProvider = new ValueHashNodeHashProvider(); + private readonly Random _random = new Random(0); + + private bool TryGetReceiver(TestNode receiverHash, out IKademliaMessageReceiver contentKademliaMessageReceiver) + { + contentKademliaMessageReceiver = null!; + if (_nodes.TryGetValue(receiverHash.Hash, out var serviceProvider)) + { + contentKademliaMessageReceiver = serviceProvider!.GetRequiredService>(); + return true; + } + + return false; + } + + private bool TryGetContentReceiver(TestNode receiverHash, out IContentMessageReceiver contentMessageReceiver) + { + contentMessageReceiver = null!; + if (_nodes.TryGetValue(receiverHash.Hash, out var serviceProvider)) + { + contentMessageReceiver = serviceProvider!.GetRequiredService>(); + return true; + } + + return false; + } + + public KademliaContent GetKademliaContent(ValueHash256 nodeHash) + { + return _nodes[nodeHash].GetRequiredService>(); + } + + public Kademlia CreateNode(ValueHash256 nodeID) + { + var nodeIDTestNode = new TestNode(nodeID); + + var serviceProvider = new ServiceCollection() + .ConfigureKademliaComponents() + .ConfigureKademliaContentComponents() + .AddSingleton(new TestLogManager(LogLevel.Error)) + .AddSingleton>(_nodeHashProvider) + .AddSingleton>(_nodeHashProvider) + .AddSingleton(new KademliaConfig() + { + CurrentNodeId = nodeIDTestNode, + KSize = config.KSize, + Alpha = config.Alpha, + Beta = config.Beta, + RefreshInterval = TimeSpan.FromHours(1), + UseTreeBasedRoutingTable = config.UseTreeBasedRoutingTable, + UseNewLookup = config.UseNewLookup + }) + .AddSingleton>(new OnlySelfIKademliaContentStore(nodeID)) + .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) + .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) + .AddSingleton>() + .AddSingleton>() + .BuildServiceProvider(); + + _nodes[nodeID] = serviceProvider; + + return serviceProvider.GetRequiredService>(); + } + + private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender, IContentMessageSender + { + public async Task Ping(TestNode node, CancellationToken token) + { + Interlocked.Increment(ref fabric.PingCount); + + await fabric.DoSimulateLatency(token); + fabric.Debug($"ping from {sender} to {node}"); + if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) + { + await receiver.Ping(sender, token); + return; + } + + throw new Exception($"unknown receiver {node}"); + } + + public async Task FindNeighbours(TestNode node, ValueHash256 hash, CancellationToken token) + { + Interlocked.Increment(ref fabric.FindNeighbourCount); + + await fabric.DoSimulateLatency(token); + fabric.Debug($"findn from {sender} to {node}"); + if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) + { + return (await receiver.FindNeighbours(sender, hash, token)).Select((node) => new TestNode(node.Hash)).ToArray(); + } + + throw new Exception($"unknown receiver {node}"); + } + + public async Task> FindValue(TestNode node, ValueHash256 hash, CancellationToken token) + { + Interlocked.Increment(ref fabric.FindValueCount); + + await fabric.DoSimulateLatency(token); + fabric.Debug($"finv from {sender} to {node}"); + if (fabric.TryGetContentReceiver(node, out IContentMessageReceiver receiver)) + { + var resp = await receiver.FindValue(sender, hash, token); + fabric.Debug($"Got {resp.HasValue} {resp.Value} or {resp.Neighbours.Length} next"); + + resp = resp with { Neighbours = resp.Neighbours.Select(node => new TestNode(node.Hash)).ToArray() }; + return resp; + } + + throw new Exception($"unknown receiver {node}"); + } + } + + private Task DoSimulateLatency(CancellationToken token) + { + if (!SimulateLatency) return Task.CompletedTask; + return Task.Delay(_baseLatency + _random.Next(_randomLatency), token); + } + + private void Debug(string debugString) + { + if (!IsDebugging) return; + Console.Error.WriteLine(debugString); + } + + public bool IsDebugging { get; set; } + + public async Task Bootstrap(CancellationToken token) + { + foreach (KeyValuePair kv in _nodes) + { + await kv.Value.GetRequiredService>().Bootstrap(token); + } + } + } + + /// + /// Class representing node in testing. Deliberately used where the hash does not match to make sure that + /// Kademlia code assume so. + /// + /// + internal class TestNode(ValueHash256 hash) + { + public ValueHash256 Hash => hash; + + public override string ToString() + { + return Hash.ToString(); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs new file mode 100644 index 000000000000..8bfea5653f3a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +[TestFixture(true)] +[TestFixture(false)] +public class KademliaTests +{ + private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); + private readonly bool _useTreeBasedBucket; + + public KademliaTests(bool useTreeBasedBucket) + { + _useTreeBasedBucket = useTreeBasedBucket; + } + + private Kademlia CreateKad(KademliaConfig config) + { + config.UseTreeBasedRoutingTable = _useTreeBasedBucket; + + return new ServiceCollection() + .ConfigureKademliaComponents() + .AddSingleton(new TestLogManager(LogLevel.Trace)) + .AddSingleton>(new ValueHashNodeHashProvider()) + .AddSingleton(config) + .AddSingleton(_kademliaMessageSender) + .AddSingleton>() + .BuildServiceProvider() + .GetRequiredService>(); + } + + [Test] + public void TestNewNodeAdded() + { + Kademlia kad = CreateKad(new KademliaConfig() + { + KSize = 5, + Beta = 0, + }); + + int nodeAddedTriggered = 0; + kad.OnNodeAdded += (sender, hash256) => nodeAddedTriggered++; + + ValueHash256 testHash = new ValueHash256("0x1111111111111111111111111111111111111111111111111111111111111111"); + kad.AddOrRefresh(testHash); + kad.AddOrRefresh(testHash); + kad.AddOrRefresh(testHash); + + nodeAddedTriggered.Should().Be(1); + } + + [Test] + public async Task TestTooManyNode() + { + TaskCompletionSource pingSource = new TaskCompletionSource(); + _kademliaMessageSender + .Ping(Arg.Any(), Arg.Any()) + .Returns(pingSource.Task); + + Kademlia kad = CreateKad(new KademliaConfig() + { + KSize = 5, + Beta = 0, + }); + + ValueHash256[] testHashes = Enumerable.Range(0, 10).Select((k) => Hash256XorUtils.GetRandomHashAtDistance( ValueKeccak.Zero, 250) ).ToArray(); + foreach (ValueHash256 valueHash256 in testHashes[..10]) + { + kad.AddOrRefresh(valueHash256); + } + + kad.GetAllAtDistance(250).ToHashSet().Should().BeEquivalentTo(testHashes[..5].ToHashSet()); + + pingSource.SetCanceled(); + + await Task.Delay(100); + + var afterCancelled = (testHashes[1..5].Concat([testHashes[9]])).ToHashSet(); + Assert.That(() => kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(afterCancelled).After(100)); + } + + [Test] + public void TestGetKNeighbours() + { + TaskCompletionSource pingSource = new TaskCompletionSource(); + _kademliaMessageSender + .Ping(Arg.Any(), Arg.Any()) + .Returns(pingSource.Task); + + Kademlia kad = CreateKad(new KademliaConfig() + { + CurrentNodeId = ValueKeccak.Compute("something"), + KSize = 5, + Beta = 0, + }); + + ValueHash256[] testHashes = Enumerable.Range(0, 7).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); + foreach (ValueHash256 valueHash256 in testHashes) + { + kad.AddOrRefresh(valueHash256); + } + + kad.GetKNeighbour(ValueKeccak.Zero).Length.Should().Be(5); + kad.GetKNeighbour(kad.CurrentNode).Should().Contain(kad.CurrentNode); + foreach (ValueHash256 testHash in testHashes) + { + // It must return K items exactly, taking from other bucket if necessary. + kad.GetKNeighbour(testHash).Length.Should().Be(5); + + // It must find the closest one at least. + kad.GetKNeighbour(testHash).Should().Contain(testHash); + + // It must exclude a node when hash is specified + kad.GetKNeighbour(testHash, testHash).Length.Should().Be(5); + kad.GetKNeighbour(testHash, excludeSelf: true).Should().NotContain(kad.CurrentNode); + } + } + + [Test] + public async Task TestTooManyNodeWithAcceleratedLookup() + { + if (!_useTreeBasedBucket) + { + // Accelerated lookup only supported with tree based bucket + return; + } + + _kademliaMessageSender + .Ping(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + Kademlia kad = CreateKad(new KademliaConfig() + { + KSize = 5, + Beta = 1, + }); + + ValueHash256[] testHashes = new IEnumerable[] + { + Enumerable.Range(0, 5).Select((k) => + Hash256XorUtils.GetRandomHashAtDistance(new ValueHash256("0x0000000000000000000000000000000000000000000000000000000000000000"), 248) + ), + Enumerable.Range(0, 5).Select((k) => + Hash256XorUtils.GetRandomHashAtDistance(new ValueHash256("0x0100000000000000000000000000000000000000000000000000000000000000"), 248) + ), + Enumerable.Range(0, 5).Select((k) => + Hash256XorUtils.GetRandomHashAtDistance(new ValueHash256("0x0200000000000000000000000000000000000000000000000000000000000000"), 248) + ), + Enumerable.Range(0, 5).Select((k) => + Hash256XorUtils.GetRandomHashAtDistance(new ValueHash256("0x0300000000000000000000000000000000000000000000000000000000000000"), 248) + ), + }.SelectMany(it => it).ToArray(); + + foreach (ValueHash256 valueHash256 in testHashes[..20]) + { + kad.AddOrRefresh(valueHash256); + } + + await Task.Delay(100); + kad.GetAllAtDistance(248).ToHashSet().Should().BeEquivalentTo(testHashes[..5].ToHashSet()); + kad.GetAllAtDistance(249).ToHashSet().Should().BeEquivalentTo(testHashes[5..10].ToHashSet()); + kad.GetAllAtDistance(250).ToHashSet().Should().BeEquivalentTo(testHashes[10..].ToHashSet()); + } + + private class ValueHashNodeHashProvider: INodeHashProvider + { + public ValueHash256 GetHash(ValueHash256 node) + { + return node; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs new file mode 100644 index 000000000000..b4108865dfa1 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia; + +public enum BucketAddResult +{ + Added, + Refreshed, + Full +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs new file mode 100644 index 000000000000..b4420accc731 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Core.Threading; +using Nethermind.Evm.Tracing.GethStyle.Custom.JavaScript; +using Nethermind.Logging; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class BucketListRoutingTable: IRoutingTable where TNode : notnull +{ + private readonly ILogger _logger; + private readonly KBucket[] _buckets; + private readonly ValueHash256 _currentNodeIdAsHash; + private readonly int _kSize; + + // TODO: Double check and probably make lockless + private readonly McsLock _lock = new McsLock(); + + public BucketListRoutingTable(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + { + _logger = logManager.GetClassLogger>(); + + // Note: It does not have to be this much. In practice, only like 16 of these bucket get populated. + _buckets = new KBucket[Hash256XorUtils.MaxDistance + 1]; + for (int i = 0; i < Hash256XorUtils.MaxDistance + 1; i++) + { + _buckets[i] = new KBucket(config.KSize); + } + + _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); + _kSize = config.KSize; + } + + private KBucket GetBucket(in ValueHash256 hash) + { + int idx = Hash256XorUtils.CalculateDistance(hash, _currentNodeIdAsHash); + return _buckets[idx]; + } + + public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh) + { + using McsLock.Disposable _ = _lock.Acquire(); + BucketAddResult result = GetBucket(hash).TryAddOrRefresh(hash, item, out toRefresh); + if (result == BucketAddResult.Added) + { + OnNodeAdded?.Invoke(this, item); + } + return result; + } + + public bool Remove(in ValueHash256 hash) + { + using McsLock.Disposable _ = _lock.Acquire(); + return GetBucket(hash).RemoveAndReplace(hash); + } + + public TNode[] GetAllAtDistance(int i) + { + using McsLock.Disposable _ = _lock.Acquire(); + return _buckets[i].GetAll(); + } + + public IEnumerable IterateBucketRandomHashes() + { + for (var i = 0; i < _buckets.Length; i++) + { + if (_buckets[i].Count > 0) + { + ValueHash256 nodeToLookup = Hash256XorUtils.GetRandomHashAtDistance(_currentNodeIdAsHash, i); + yield return nodeToLookup; + } + } + } + + public TNode? GetByHash(ValueHash256 hash) + { + return GetBucket(hash).GetByHash(hash); + } + + private IEnumerable<(ValueHash256, TNode)> IterateNeighbour(ValueHash256 hash) + { + int startingDistance = Hash256XorUtils.CalculateDistance(_currentNodeIdAsHash, hash); + foreach (var bucketToGet in EnumerateBucket(startingDistance)) + { + foreach (var entry in bucketToGet.GetAllWithHash()) + { + yield return entry; + } + } + } + + public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bool excludeSelf) + { + using McsLock.Disposable _ = _lock.Acquire(); + + int startingDistance = Hash256XorUtils.CalculateDistance(_currentNodeIdAsHash, hash); + KBucket firstBucket = _buckets[startingDistance]; + bool shouldNotContainExcludedNode = exclude == null || !firstBucket.ContainsNode(exclude.Value); + bool shouldNotContainSelf = excludeSelf == false || !firstBucket.ContainsNode(_currentNodeIdAsHash); + + if (shouldNotContainExcludedNode && shouldNotContainSelf) + { + TNode[] nodes = firstBucket.GetAll(); + if (nodes.Length == _kSize) + { + // Fast path. In theory, most of the time, this would be the taken path, where no array + // concatenation or creation is needed. + return nodes; + } + } + + var iterator = IterateNeighbour(hash); + + if (exclude != null) + iterator = iterator + .Where(kv => kv.Item1 != exclude.Value); + + if (excludeSelf) + iterator = iterator + .Where(kv => kv.Item1 != _currentNodeIdAsHash); + + return iterator.Take(_kSize) + .Select(kv => kv.Item2) + .ToArray(); + } + + private IEnumerable> EnumerateBucket(int startingDistance) + { + // Note, without a tree based routing table, we don't exactly know + // which way (left or right) is the right way to go. So this is all approximate. + // Well, even with a full tree, it would still be approximate, just that it would + // be a bit more accurate. + yield return _buckets[startingDistance]; + int left = startingDistance - 1; + int right = startingDistance + 1; + while (left >= 0 || right <= Hash256XorUtils.MaxDistance) + { + if (left >= 0) + { + yield return _buckets[left]; + } + + if (right <= Hash256XorUtils.MaxDistance) + { + yield return _buckets[right]; + } + + left -= 1; + right += 1; + } + } + + public void LogDebugInfo() + { + _logger.Debug($"Bucket sizes {string.Join(", ", _buckets.Select(b => b.Count))}"); + } + + public event EventHandler? OnNodeAdded; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs new file mode 100644 index 000000000000..a04ccb251262 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +public interface IContentHashProvider +{ + ValueHash256 GetHash(TContentKey key); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs new file mode 100644 index 000000000000..b543eb74e73c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +public interface IContentMessageSender +{ + Task> FindValue(TNode receiver, TContentKey contentKey, CancellationToken token); +} + +public interface IContentMessageReceiver: IContentMessageSender +{ +} + +public record FindValueResponse( + bool HasValue, + TContent? Value, + TNode[] Neighbours +); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs new file mode 100644 index 000000000000..1776ad239660 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +/// +/// This interface extend with the ability to lookup content. +/// +/// +/// +public interface IKademliaContent +{ + /// + /// Initiate a full network traversal for finding the value specified by TContent. + /// + /// + /// + /// + Task LookupValue(TContentKey id, CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs new file mode 100644 index 000000000000..e07f9a7580ae --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +/// +/// Try to get a content for serving. +/// +/// +/// +public interface IKademliaContentStore +{ + bool TryGetValue(TContentKey hash, out TContent? value); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs new file mode 100644 index 000000000000..d34fe3daa93c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Lantern.Discv5.Enr; +using Microsoft.Extensions.DependencyInjection; + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +public static class IServiceCollectionExtensions +{ + /// + /// Configure an extension of kademlia services to look up content. In particular it provide + /// an ` that has a lookup function. + /// Assume the component for was already registered. In addition to that, it assume + /// the following dependencies are also available: + /// + /// - + /// - + /// - + /// + /// Like with main kademlia, the transport is expected to call + /// + /// + /// + /// + /// + /// + /// + public static IServiceCollection ConfigureKademliaContentComponents(this IServiceCollection collection) where TNode : notnull + { + return collection + .AddSingleton, KademliaContent>() + .AddSingleton, KademliaContentMessageReceiver>() + .AddSingleton, KademliaContentMessageReceiver>(); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs new file mode 100644 index 000000000000..41317b8625ff --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Logging; + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +public class KademliaContent( + IContentHashProvider contentHashProvider, + IKademliaContentStore kademliaContentStore, + IContentMessageSender contentMessageSender, + ILookupAlgo lookupAlgo, + KademliaConfig config, + ILogManager logManager + ): IKademliaContent where TNode : notnull +{ + private readonly ILogger _logger = logManager.GetClassLogger>(); + + public async Task LookupValue(TContentKey contentKey, CancellationToken token) + { + TContent? result = default(TContent); + bool resultWasFound = false; + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + token = cts.Token; + // TODO: Timeout? + + if (kademliaContentStore.TryGetValue(contentKey, out TContent? content)) + { + return content; + } + + ValueHash256 targetHash = contentHashProvider.GetHash(contentKey); + + try + { + await lookupAlgo.Lookup( + targetHash, config.KSize, async (nextNode, token) => + { + FindValueResponse valueResponse = await contentMessageSender.FindValue(nextNode, contentKey, token); + + if (valueResponse.HasValue) + { + if (_logger.IsDebug) _logger.Debug($"Value response has value {valueResponse.Value}"); + resultWasFound = true; + result = valueResponse.Value; // Shortcut so that once it find the value, it should stop. + await cts.CancelAsync(); + } + + if (_logger.IsDebug) _logger.Debug($"Value response has no value. Returning {valueResponse.Neighbours.Length} neighbours"); + return valueResponse.Neighbours; + }, + token + ); + } + catch (OperationCanceledException) + { + if (!resultWasFound) throw; + } + + return result; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs new file mode 100644 index 000000000000..01cbdb79c94c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia.Content; + +public class KademliaContentMessageReceiver( + IKademlia kademlia, + NodeHealthTracker nodeHealthTracker, + IContentHashProvider contentHashProvider, + IKademliaContentStore kademliaKademliaContentStore) : IContentMessageReceiver +{ + public Task> FindValue(TNode sender, TContentKey contentKey, CancellationToken token) + { + nodeHealthTracker.OnIncomingMessageFrom(sender); + + if (kademliaKademliaContentStore.TryGetValue(contentKey, out TContent? value)) + { + return Task.FromResult(new FindValueResponse(true, value!, Array.Empty())); + } + + return Task.FromResult( + new FindValueResponse( + false, + default, + kademlia.GetKNeighbour(contentHashProvider.GetHash(contentKey), sender, true) + )); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs new file mode 100644 index 000000000000..bf60a4fc0c77 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Lantern.Discv5.WireProtocol.Table; +using Nethermind.Core.Crypto; +using Nethermind.Core.Threading; +using NonBlocking; + +namespace Nethermind.Network.Discovery.Kademlia; + +// TODO: Combine with LruCace? +public class DoubleEndedLru(int capacity) where TNode : notnull +{ + // Double check if can be done locklesly + private McsLock _lock = new McsLock(); + + private LinkedList<(ValueHash256, TNode)> _queue = new(); + private ConcurrentDictionary> _hashMapping = new(); + public int Count => _queue.Count; + + public BucketAddResult AddOrRefresh(in ValueHash256 hash, TNode node) + { + using McsLock.Disposable _ = _lock.Acquire(); + + if (_hashMapping.TryGetValue(hash, out var listNode)) + { + _queue.Remove(listNode); + _queue.AddFirst(listNode); + return BucketAddResult.Refreshed; + } + + if (_queue.Count >= capacity) + { + return BucketAddResult.Full; + } + + listNode = _queue.AddFirst((hash, node)); + if (_hashMapping.TryAdd(hash, listNode) && _queue.Count <= capacity) return BucketAddResult.Added; + + _queue.Remove((hash, node)); + _hashMapping.TryRemove(hash, out listNode); + + return BucketAddResult.Full; + } + + public bool TryPopHead(out TNode? node) + { + using McsLock.Disposable _ = _lock.Acquire(); + + LinkedListNode<(ValueHash256, TNode)>? front = _queue.First; + if (front == null) + { + node = default; + return false; + } + + _queue.Remove(front); + node = front.Value.Item2; + _hashMapping.TryRemove(front.Value.Item1, out front); + + return true; + } + + public bool TryGetLast(out TNode? last) + { + using McsLock.Disposable _ = _lock.Acquire(); + + LinkedListNode<(ValueHash256, TNode)>? lastNode = _queue.Last; + if (lastNode == null) + { + last = default; + return false; + } + + last = lastNode.Value.Item2; + return true; + } + + public bool Remove(ValueHash256 hash) + { + using McsLock.Disposable _ = _lock.Acquire(); + + if (_hashMapping.TryRemove(hash, out var listNode)) + { + _queue.Remove(listNode); + return true; + } + + return false; + } + + public TNode[] GetAll() + { + return _hashMapping.Select(kv => kv.Value.Value.Item2).ToArray(); + } + + public IEnumerable<(ValueHash256, TNode)> GetAllWithHash() + { + return _queue; + } + + public bool Contains(in ValueHash256 hash) + { + return _hashMapping.ContainsKey(hash); + } + + public TNode? GetByHash(ValueHash256 hash) + { + if (_hashMapping.TryGetValue(hash, out LinkedListNode<(ValueHash256, TNode)>? listNode)) + { + return listNode.Value.Item2; + } + + return default; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs new file mode 100644 index 000000000000..17fcc2670ac6 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.Intrinsics; +using Nethermind.Core.Crypto; +using Nethermind.Int256; + +namespace Nethermind.Network.Discovery.Kademlia; + +public static class Hash256XorUtils +{ + public static int CalculateDistance(ValueHash256 h1, ValueHash256 h2) + { + int zeros = 0; + for (int i = 0; i < 32; i += 1) + { + byte b1 = h1.Bytes[i]; + byte b2 = h2.Bytes[i]; + byte xord = (byte)(b1 ^ b2); + if (xord == 0) + { + zeros += 8; + continue; + } + + int nonZeroPostfix = 1; + while ((xord >>= 1) != 0) + { + nonZeroPostfix++; + } + zeros += 8 - nonZeroPostfix; + + break; + } + + return MaxDistance - zeros; + } + + public static UInt256 CalculateDistanceUInt256(ValueHash256 h1, ValueHash256 h2) + { + ValueHash256 xored = XorDistance(h1, h2); + // TODO: Make this more efficirent/simd it. + for (int i = 0; i < 32; i++) + { + xored.BytesAsSpan[i] = (byte)(h1.BytesAsSpan[i] ^ h2.BytesAsSpan[i]); + } + + UInt256 XORed = new UInt256(xored.BytesAsSpan, true); + return XORed; + } + + public static int MaxDistance => 256; + + public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int distance) + { + return GetRandomHashAtDistance(currentHash, distance, Random.Shared); + } + + public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int distance, Random random) + { + // TODO: Just add a min/max range per bucket and randomized between them. + if (distance == MaxDistance) + { + return currentHash; + } + + ValueHash256 randomized = new ValueHash256(); + random.NextBytes(randomized.BytesAsSpan); + return CopyForRandom(currentHash, randomized, MaxDistance - distance); + } + + public static int Compare(ValueHash256 a, ValueHash256 b, ValueHash256 c) + { + ValueHash256 ac = new ValueHash256(); + (new Vector(a.BytesAsSpan) ^ new Vector(c.BytesAsSpan)).CopyTo(ac.BytesAsSpan); + + ValueHash256 bc = new ValueHash256(); + (new Vector(b.BytesAsSpan) ^ new Vector(c.BytesAsSpan)).CopyTo(bc.BytesAsSpan); + + return ac.CompareTo(bc); + } + + public static ValueHash256 CopyForRandom(ValueHash256 currentHash, ValueHash256 randomizedHash, int distance) + { + if (distance >= 256) return currentHash; + + currentHash.Bytes[0..(distance / 8)].CopyTo(randomizedHash.BytesAsSpan); + + int remainingBit = distance % 8; + int remainingBitByte = distance / 8; + byte mask = (byte)(~((1 << (8 - remainingBit)) - 1)); + byte randomized = randomizedHash.BytesAsSpan[remainingBitByte]; + byte original = currentHash.BytesAsSpan[remainingBitByte]; + randomizedHash.BytesAsSpan[remainingBitByte] = (byte)((original & mask) | (randomized & (~mask))); + + if (distance <= 255) + { + // So it always assume that the next bucket (the closer one) is always populated and therefore, + // the bits here for that distance must not be the same as in currentHash. + int nextBit = distance % 8; + int nextBitByte = distance / 8; + mask = (byte)(1 << (7 - nextBit)); + randomized = randomizedHash.BytesAsSpan[nextBitByte]; + byte opposite = (byte)~(currentHash.BytesAsSpan[nextBitByte]); + + byte final = (byte)((opposite & mask) | (randomized & ~(mask))); + randomizedHash.BytesAsSpan[nextBitByte] = final; + } + + return randomizedHash; + } + + public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) + { + byte[] xorBytes = new byte[hash1.Bytes.Length]; + for (int i = 0; i < xorBytes.Length; i++) + { + xorBytes[i] = (byte)(hash1.Bytes[i] ^ hash2.Bytes[i]); + } + return new ValueHash256(xorBytes); + } + +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs new file mode 100644 index 000000000000..307a4d2ae0cf --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Lantern.Discv5.Enr; +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// Main kademlia interface. High level code is expected to interface with this interface. +/// +/// +public interface IKademlia +{ + + /// + /// Add node to the table. + /// + /// + void AddOrRefresh(TNode node); + + /// + /// Remove from to the table. + /// + /// + void Remove(TNode node); + + /// + /// Lookup k nearest neighbour closest to the target hash. This will traverse the network. + /// + /// + /// + /// + Task LookupNodesClosest(ValueHash256 targetHash, CancellationToken token, int? k = null); + + /// + /// Start timers, refresh and such for maintenance of the table. + /// + /// + Task Run(CancellationToken token); + + /// + /// Just do the bootstrap sequence, which is to initiate a lookup on current node id. + /// Also do a refresh on all bucket which is not part of joining strictly speaking. + /// + /// + Task Bootstrap(CancellationToken token); + + /// + /// Return the K nearest table entry from hash. This does not traverse the network. The returned array is not + /// sorted. The routing table may return the exact same array for optimization purpose. + /// + /// + /// + /// + TNode[] GetKNeighbour(ValueHash256 hash, TNode? excluding = default, bool excludeSelf = false); + + /// + /// Called when a TNode is added to the routing table. + /// + event EventHandler OnNodeAdded; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs new file mode 100644 index 000000000000..a517bfefe023 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +public interface IKademliaMessageSender +{ + Task Ping(TNode receiver, CancellationToken token); + Task FindNeighbours(TNode receiver, ValueHash256 hash, CancellationToken token); +} + +public interface IKademliaMessageReceiver: IKademliaMessageSender +{ +} + diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs new file mode 100644 index 000000000000..7315c29ee3c7 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// Main find closest-k node within the network. See the kademlia paper, 2.3. +/// Since find value is basically the same also just with a shortcut, this allow changing the find neighbour op. +/// Find closest-k is also used to determine which node should store a particular value which is used by +/// store RPC (not implemented). +/// +public interface ILookupAlgo +{ + /// + /// The find neighbour operation here is configurable because the same algorithm is also used for finding + /// value int the network, except that it would short circuit once the value was found. + /// + /// + /// + /// + /// + /// + Task Lookup( + ValueHash256 targetHash, + int k, + Func> findNeighbourOp, + CancellationToken token + ); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs new file mode 100644 index 000000000000..6e0fc60d2f0a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// Translate the TNode key into a which is +/// finally used for implementing the distance calculation. +/// Should this get replaced with an INode.GetHash where TNode need to implement INode? I can't decide. That would make +/// the internal methods cleaner, but it would mean TNode need to be a wrapper or have to implement some interface, +/// which may not be possible. One of the important optimization is to have a cached TNode[], so if TNode is a wrapper, +/// it would need to be unwrapped during serialization. But then again, it could be insignificant or the serialization +/// could be specialized. +/// +/// +public interface INodeHashProvider +{ + ValueHash256 GetHash(TNode node); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs new file mode 100644 index 000000000000..530afd64d85e --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +public interface IRoutingTable +{ + BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh); + bool Remove(in ValueHash256 hash); + TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude = null, bool excludeSelf = false); + TNode[] GetAllAtDistance(int i); + IEnumerable IterateBucketRandomHashes(); + TNode? GetByHash(ValueHash256 nodeId); + void LogDebugInfo(); + event EventHandler? OnNodeAdded; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs new file mode 100644 index 000000000000..dfd403fbb5ec --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Lantern.Discv5.Enr; +using Microsoft.Extensions.DependencyInjection; +using Nethermind.Logging; + +namespace Nethermind.Network.Discovery.Kademlia; + +public static class IServiceCollectionExtensions +{ + /// + /// Configure the service collection with kademlia services. The following + /// dependencies are expected: + /// + /// - + /// - + /// - + /// - + /// + /// Additionally, the transport layer is expected to call the method in + /// when external message is received. + /// + /// + /// + /// The type of node + /// + public static IServiceCollection ConfigureKademliaComponents(this IServiceCollection collection) where TNode : notnull + { + return collection + .AddSingleton, Kademlia>() + .AddSingleton, KademliaKademliaMessageReceiver>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>(provider => + { + KademliaConfig config = provider.GetRequiredService>(); + if (config.UseNewLookup) + { + return provider.GetRequiredService>(); + } + + return provider.GetRequiredService>(); + }) + .AddSingleton>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>(provider => + { + KademliaConfig config = provider.GetRequiredService>(); + if (config.UseTreeBasedRoutingTable) + { + return provider.GetRequiredService>(); + } + + return provider.GetRequiredService>(); + }); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs new file mode 100644 index 000000000000..d57ab98afa79 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections; +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class KBucket where TNode : notnull +{ + private readonly int _k; + private DoubleEndedLru _items; + private DoubleEndedLru _replacement; + + public int Count => _items.Count; + private TNode[] _cachedArray = Array.Empty(); + + public KBucket(int k) + { + _k = k; + _items = new DoubleEndedLru(k); + _replacement = new DoubleEndedLru(k); // Well, the replacement does not have to be k. Could be much lower. + } + + /// + /// Add or refresh a node entry. + /// Used when any traffic is received, or when seeding a node. + /// Return the last entry in a bucket to refresh when bucket is full. + /// + /// + /// + public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh) + { + BucketAddResult addResult = _items.AddOrRefresh(hash, item); + if (addResult == BucketAddResult.Added) + { + _cachedArray = _items.GetAll(); + } + + // Either added or refreshed + if (addResult != BucketAddResult.Full) + { + toRefresh = default; + return addResult; + } + + _replacement.AddOrRefresh(hash, item); + _items.TryGetLast(out toRefresh); + return BucketAddResult.Full; + } + + public TNode[] GetAll() + { + return _cachedArray; + } + + public IEnumerable<(ValueHash256, TNode)> GetAllWithHash() + { + return _items.GetAllWithHash(); + } + + public bool RemoveAndReplace(in ValueHash256 hash) + { + if (!_items.Remove(hash)) return false; + + if (_replacement.TryPopHead(out TNode? replacement)) + { + _items.AddOrRefresh(hash, replacement!); + } + _cachedArray = _items.GetAll(); + + return true; + } + + public void Clear() + { + _items = new DoubleEndedLru(_k); + _replacement = new DoubleEndedLru(_k); + _cachedArray = _items.GetAll(); + } + + public bool ContainsNode(in ValueHash256 hash) + { + return _items.Contains(hash); + } + + public TNode? GetByHash(ValueHash256 hash) + { + return _items.GetByHash(hash); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs new file mode 100644 index 000000000000..9adeeae13180 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -0,0 +1,405 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Text; +using Nethermind.Core.Crypto; +using Nethermind.Core.Threading; +using Nethermind.Logging; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class KBucketTree: IRoutingTable where TNode : notnull +{ + private class TreeNode + { + public KBucket Bucket { get; } + public TreeNode? Left { get; set; } + public TreeNode? Right { get; set; } + public ValueHash256 Prefix { get; } + public bool IsLeaf => Left == null && Right == null; + + public TreeNode(int k, ValueHash256 prefix) + { + Bucket = new KBucket(k); + Prefix = prefix; + } + } + + private readonly TreeNode _root; + private readonly int _b; + private readonly int _k; + private readonly ValueHash256 _currentNodeHash; + private readonly ILogger _logger; + + // TODO: Double check and probably make lockless + private readonly McsLock _lock = new McsLock(); + + public KBucketTree(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + { + _k = config.KSize; + _b = config.Beta; + _currentNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); + _root = new TreeNode(config.KSize, new ValueHash256()); + _logger = logManager.GetClassLogger(); + if (_logger.IsDebug) _logger.Debug($"Initialized KBucketTree with k={_k}, currentNodeId={_currentNodeHash}"); + } + + public BucketAddResult TryAddOrRefresh(in ValueHash256 nodeHash, TNode node, out TNode? toRefresh) + { + using McsLock.Disposable _ = _lock.Acquire(); + + if (_logger.IsDebug) _logger.Debug($"Adding node {node} with XOR distance {Hash256XorUtils.XorDistance(_currentNodeHash, nodeHash)}"); + + TreeNode current = _root; + // As in, what would be the depth of the node assuming all branch on the traversal is populated. + int logDistance = Hash256XorUtils.MaxDistance - Hash256XorUtils.CalculateDistance(_currentNodeHash, nodeHash); + int depth = 0; + while (true) + { + if (current.IsLeaf) + { + if (_logger.IsTrace) _logger.Trace($"Reached leaf node at depth {depth}"); + var resp = current.Bucket.TryAddOrRefresh(nodeHash, node, out toRefresh); + if (resp == BucketAddResult.Added) + { + OnNodeAdded?.Invoke(this, node); + } + if (resp is BucketAddResult.Added or BucketAddResult.Refreshed) + { + if (_logger.IsDebug) _logger.Debug($"Successfully added/refreshed node {node} in bucket at depth {depth}"); + return resp; + } + + if (resp == BucketAddResult.Full && ShouldSplit(depth, logDistance)) + { + if (_logger.IsTrace) _logger.Trace($"Splitting bucket at depth {depth}"); + SplitBucket(depth, current); + continue; + } + + if (_logger.IsDebug) _logger.Debug($"Failed to add node {nodeHash} {node}. Bucket at depth {depth} is full. {_k} {current.Bucket.GetAllWithHash().Count()}"); + return resp; + } + + bool goRight = GetBit(nodeHash, depth); + if (_logger.IsTrace) _logger.Trace($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); + + current = goRight ? current.Right! : current.Left!; + depth++; + } + } + + public TNode? GetByHash(ValueHash256 hash) + { + return GetBucketForHash(hash).GetByHash(hash); + } + + private KBucket GetBucketForHash(ValueHash256 nodeHash) + { + TreeNode current = _root; + int depth = 0; + while (true) + { + if (current.IsLeaf) + { + _logger.Debug($"Reached leaf node at depth {depth}"); + return current.Bucket; + } + + bool goRight = GetBit(nodeHash, depth); + _logger.Debug($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); + + current = goRight ? current.Right! : current.Left!; + depth++; + } + } + + private bool ShouldSplit(int depth, int targetLogDistance) + { + bool shouldSplit = depth < 256 && targetLogDistance + _b >= depth; + _logger.Debug($"ShouldSplit at depth {depth}: {shouldSplit}"); + return shouldSplit; + } + + private void SplitBucket(int depth, TreeNode node) + { + node.Left = new TreeNode(_k, node.Prefix); + var rightPrefixBytes = node.Prefix.Bytes.ToArray(); + rightPrefixBytes[depth / 8] |= (byte)(1 << (7 - (depth % 8))); + node.Right = new TreeNode(_k, new ValueHash256(rightPrefixBytes)); + + _logger.Debug($"Created children at depth {depth + 1}"); + + // The reverse is because the bucket is iterated from the most recent. Without it + // reading would have reversed this order. + foreach (var item in node.Bucket.GetAllWithHash().Reverse()) + { + ValueHash256 itemHash = item.Item1; + TreeNode? targetNode = GetBit(itemHash, depth) ? node.Right : node.Left; + targetNode.Bucket.TryAddOrRefresh(itemHash, item.Item2, out _); + _logger.Debug($"Moved item {item} to {(GetBit(itemHash, depth) ? "right" : "left")} child"); + } + + node.Bucket.Clear(); + _logger.Debug($"Finished splitting bucket. Left count: {node.Left.Bucket.Count}, Right count: {node.Right.Bucket.Count}"); + } + + public bool Remove(in ValueHash256 nodeHash) + { + using McsLock.Disposable _ = _lock.Acquire(); + + _logger.Debug($"Attempting to remove node {nodeHash} with hash {nodeHash}"); + + return GetBucketForHash(nodeHash).RemoveAndReplace(nodeHash); + } + + public TNode[] GetAllAtDistance(int distance) + { + using McsLock.Disposable _ = _lock.Acquire(); + + _logger.Debug($"Getting all nodes at distance {distance}"); + List result = new List(); + GetAllAtDistanceRecursive(_root, 0, distance, result); + _logger.Debug($"Found {result.Count} nodes at distance {distance}"); + return result.ToArray(); + } + + private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, List result) + { + int targetDepth = Hash256XorUtils.MaxDistance - distance; + if (node.IsLeaf) + { + if (depth <= targetDepth) + { + result.AddRange(node.Bucket.GetAllWithHash() + .Where(kv => Hash256XorUtils.CalculateDistance(kv.Item1, _currentNodeHash) == distance) + .Select(kv => kv.Item2)); + } + else + { + result.AddRange(node.Bucket.GetAll()); + } + } + else + { + if (depth < targetDepth) + { + bool goRight = GetBit(_currentNodeHash, depth); + if (goRight) + { + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); + } + else + { + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result); + } + } + else if (depth == targetDepth) + { + bool goRight = GetBit(_currentNodeHash, depth); + // Note: We go the opposite direction here, as the same direction would have a distance + 1 + if (goRight) + { + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result); + } + else + { + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); + } + } + else + { + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); + } + } + } + + public IEnumerable IterateBucketRandomHashes() + { + using McsLock.Disposable _ = _lock.Acquire(); + + // Well, it need to ToArray, otherwise the lock does not really do anything. + return DoIterateBucketRandomHashes(_root, 0).ToArray(); + } + + private IEnumerable DoIterateBucketRandomHashes(TreeNode node, int depth) + { + if (node.IsLeaf) + { + yield return Hash256XorUtils.GetRandomHashAtDistance(_currentNodeHash, depth); + } + else + { + foreach (ValueHash256 bucketHash in DoIterateBucketRandomHashes(node.Left!, depth + 1)) + { + yield return bucketHash; + } + + foreach (ValueHash256 bucketHash in DoIterateBucketRandomHashes(node.Right!, depth + 1)) + { + yield return bucketHash; + } + } + } + + private IEnumerable<(ValueHash256, TNode)> IterateNeighbour(ValueHash256 hash) + { + foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(_root, 0, hash)) + { + foreach ((ValueHash256, TNode) entry in treeNode.Bucket.GetAllWithHash()) + { + yield return entry; + } + } + } + + private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNode, int depth, ValueHash256 target) + { + if (currentNode.IsLeaf) + { + yield return currentNode; + } + else + { + if (GetBit(target, depth)) + { + foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(currentNode.Right!, depth + 1, target)) + { + yield return treeNode; + } + + foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(currentNode.Left!, depth + 1, target)) + { + yield return treeNode; + } + } + else + { + foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(currentNode.Left!, depth + 1, target)) + { + yield return treeNode; + } + + foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(currentNode.Right!, depth + 1, target)) + { + yield return treeNode; + } + } + } + } + + public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bool excludeSelf) + { + using McsLock.Disposable _ = _lock.Acquire(); + + KBucket firstBucket = GetBucketForHash(hash); + bool shouldNotContainExcludedNode = exclude == null || !firstBucket.ContainsNode(exclude.Value); + bool shouldNotContainSelf = excludeSelf == false || !firstBucket.ContainsNode(_currentNodeHash); + + if (shouldNotContainExcludedNode && shouldNotContainSelf) + { + TNode[] nodes = firstBucket.GetAll(); + if (nodes.Length == _k) + { + // Fast path. In theory, most of the time, this would be the taken path, where no array + // concatenation or creation is needed. + return nodes; + } + } + + var iterator = IterateNeighbour(hash); + + if (exclude != null) + iterator = iterator + .Where(kv => kv.Item1 != exclude.Value); + + if (excludeSelf) + iterator = iterator + .Where(kv => kv.Item1 != _currentNodeHash); + + return iterator.Take(_k) + .Select(kv => kv.Item2) + .ToArray(); + } + + private bool GetBit(ValueHash256 hash, int index) + { + int byteIndex = index / 8; + int bitIndex = index % 8; + return (hash.Bytes[byteIndex] & (1 << (7 - bitIndex))) != 0; + } + + private void LogTreeStructureRecursive(TreeNode node, string indent, bool last, int depth, StringBuilder sb) + { + sb.Append(indent); + if (last) + { + sb.Append("└─"); + indent += " "; + } + else + { + sb.Append("├─"); + indent += "│ "; + } + + if (node.Left == null && node.Right == null) + { + sb.AppendLine($"Bucket (Depth: {depth}, Count: {node.Bucket.Count})"); + return; + } + + sb.AppendLine($"Node (Depth: {depth})"); + LogTreeStructureRecursive(node.Left!, indent, false, depth+1, sb); + LogTreeStructureRecursive(node.Right!, indent, true, depth+1, sb); + } + + private void LogTreeStatistics() + { + int totalNodes = 0; + int totalBuckets = 0; + int maxDepth = 0; + int totalItems = 0; + + void TraverseTree(TreeNode node, int depth) + { + totalNodes++; + maxDepth = Math.Max(maxDepth, depth); + + if (node.Left == null && node.Right == null) + { + totalBuckets++; + totalItems += node.Bucket.Count; + } + else + { + TraverseTree(node.Left!, depth + 1); + TraverseTree(node.Right!, depth + 1); + } + } + + TraverseTree(_root, 0); + + _logger.Debug($"Tree Statistics:\n" + + $"Total Nodes: {totalNodes}\n" + + $"Total Buckets: {totalBuckets}\n" + + $"Max Depth: {maxDepth}\n" + + $"Total Items: {totalItems}\n" + + $"Average Items per Bucket: {(double)totalItems / totalBuckets:F2}"); + } + private void LogTreeStructure() + { + StringBuilder sb = new StringBuilder(); + LogTreeStructureRecursive(_root, "", true, 0, sb); + _logger.Info($"Current Tree Structure:\n{sb}"); + } + + public void LogDebugInfo() + { + LogTreeStatistics(); + LogTreeStructure(); + } + + public event EventHandler? OnNodeAdded; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs new file mode 100644 index 000000000000..d5ef892bf830 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Diagnostics; +using Nethermind.Core.Crypto; +using Nethermind.Logging; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class Kademlia : IKademlia where TNode : notnull +{ + private readonly IKademliaMessageSender _kademliaMessageSender; + private readonly INodeHashProvider _nodeHashProvider; + private readonly IRoutingTable _routingTable; + private readonly ILookupAlgo _lookupAlgo; + private readonly NodeHealthTracker _nodeHealthTracker; + private readonly ILogger _logger; + + private readonly TNode _currentNodeId; + private readonly ValueHash256 _currentNodeIdAsHash; + private readonly int _kSize; + private readonly TimeSpan _refreshInterval; + + public Kademlia( + INodeHashProvider nodeHashProvider, + IKademliaMessageSender sender, + IRoutingTable routingTable, + ILookupAlgo lookupAlgo, + ILogManager logManager, + NodeHealthTracker nodeHealthTracker, + KademliaConfig config) + { + _nodeHashProvider = nodeHashProvider; + _kademliaMessageSender = sender; + _routingTable = routingTable; + _lookupAlgo = lookupAlgo; + _nodeHealthTracker = nodeHealthTracker; + _logger = logManager.GetClassLogger>(); + + _currentNodeId = config.CurrentNodeId; + _currentNodeIdAsHash = _nodeHashProvider.GetHash(_currentNodeId); + _kSize = config.KSize; + _refreshInterval = config.RefreshInterval; + + AddOrRefresh(_currentNodeId); + } + + public TNode CurrentNode => _currentNodeId; + + public void AddOrRefresh(TNode node) + { + // It add to routing table and does the whole refresh logid. + _nodeHealthTracker.OnIncomingMessageFrom(node); + } + public void Remove(TNode node) + { + _routingTable.Remove(_nodeHashProvider.GetHash(node)); + } + + public TNode[] GetAllAtDistance(int i) + { + return _routingTable.GetAllAtDistance(i); + } + + private bool SameAsSelf(TNode node) + { + return _nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; + } + + public async Task LookupNodesClosest(ValueHash256 targetHash, CancellationToken token, int? k = null) + { + return await LookupNodesClosest( + targetHash, + k ?? _kSize, + async (nextNode, token) => + { + if (SameAsSelf(nextNode)) + { + return _routingTable.GetKNearestNeighbour(targetHash); + } + return await _kademliaMessageSender.FindNeighbours(nextNode, targetHash, token); + }, + token + ); + } + + private Task LookupNodesClosest( + ValueHash256 targetHash, + int k, + Func> findNeighbourOp, + CancellationToken token + ) + { + return _lookupAlgo.Lookup( + targetHash, + k, + findNeighbourOp, + token); + } + + public async Task Run(CancellationToken token) + { + await LookupNodesClosest(_currentNodeIdAsHash, token); + + while (true) + { + await Bootstrap(token); + // The main loop can potentially be parallelized with multiple concurrent lookups to improve efficiency. + + await Task.Delay(_refreshInterval, token); + } + } + + public async Task Bootstrap(CancellationToken token) + { + Stopwatch sw = Stopwatch.StartNew(); + await LookupNodesClosest(_currentNodeIdAsHash, token); + + token.ThrowIfCancellationRequested(); + + // Refreshes all bucket. one by one. That is not empty. + // A refresh means to do a k-nearest node lookup for a random hash for that particular bucket. + foreach (ValueHash256 nodeToLookup in _routingTable.IterateBucketRandomHashes()) + { + await LookupNodesClosest(nodeToLookup, token); + } + + if (_logger.IsDebug) + { + _logger.Debug($"Bootstrap completed. Took {sw}."); + _routingTable.LogDebugInfo(); + } + } + + public TNode[] GetKNeighbour(ValueHash256 hash, TNode? excluding = default, bool excludeSelf = false) + { + ValueHash256? excludeHash = null; + if (excluding != null) excludeHash = _nodeHashProvider.GetHash(excluding); + return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); + } + + public event EventHandler OnNodeAdded + { + add => _routingTable.OnNodeAdded += value; + remove => _routingTable.OnNodeAdded -= value; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs new file mode 100644 index 000000000000..50a3ac806cc8 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia; + +public class KademliaConfig +{ + /// + /// The current node id + /// + public TNode CurrentNodeId { get; set; } = default!; + + /// + /// K, as in the size of the kbucket. + /// + public int KSize { get; set; } = 16; + + /// + /// Alpha, as in the parallelism of the lookup algorith. + /// + public int Alpha { get; set; } + + /// + /// Beta, as in B in kademlia the kademlia paper, 4.2 Accelerated Lookups + /// Only works with tree based routing table. + /// + public int Beta { get; set; } = 2; + + /// + /// Use tree based routing table. False to use fixed array table. + /// + public bool UseTreeBasedRoutingTable { get; set; } = true; + + /// + /// The interval on which a table refresh is initiated. + /// + public TimeSpan RefreshInterval { get; set; } = TimeSpan.FromMinutes(30); + + /// + /// Use a different algorithm for the neighbour and value lookup. + /// + public bool UseNewLookup { get; set; } + + /// + /// The timeout for each find neighbour call lookup + /// + public TimeSpan LookupFindNeighbourHardTimout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// The timeout for a ping message during a refresh after which the node is considered to be offline. + /// + public TimeSpan RefreshPingTimeout { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// How many time a request for a node failed before we remove it from the routing table. + /// + public int NodeRequestFailureThreshold { get; set; } = 5; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs new file mode 100644 index 000000000000..184f97cc1d6b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class KademliaKademliaMessageReceiver(IKademlia kademlia, NodeHealthTracker healthTracker): IKademliaMessageReceiver +{ + public Task Ping(TNode sender, CancellationToken token) + { + healthTracker.OnIncomingMessageFrom(sender); + return Task.CompletedTask; + } + + public Task FindNeighbours(TNode sender, ValueHash256 hash, CancellationToken token) + { + healthTracker.OnIncomingMessageFrom(sender); + return Task.FromResult(kademlia.GetKNeighbour(hash, sender)); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs new file mode 100644 index 000000000000..1ba8f90df5a5 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Diagnostics.CodeAnalysis; +using Nethermind.Core.Crypto; +using Nethermind.Core.Threading; +using Nethermind.Logging; +using NonBlocking; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// This find nearest k query does not follow the kademlia paper faithfully. Instead of distinct rounds, it has +/// num worker where alpha is the number of worker. Worker does not wait for other worker. Stop condition +/// happens if no more node to query or no new node can be added to the current result set that can improve it +/// for more than alpha*2 request. It is slightly faster than the legacy query on find value where it can be cancelled +/// earlier as it converge to the content faster, but take more query for findnodes due to a more strict stop +/// condition. +/// +public class NewLookupKNearestNeighbour( + IRoutingTable routingTable, + INodeHashProvider nodeHashProvider, + NodeHealthTracker nodeHealthTracker, + KademliaConfig config, + ILogManager logManager): ILookupAlgo +{ + private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; + private readonly ILogger _logger = logManager.GetClassLogger>(); + + public async Task Lookup( + ValueHash256 targetHash, + int k, + Func> findNeighbourOp, + CancellationToken token + ) { + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + token = cts.Token; + + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); + + IComparer comparer = Comparer.Create((h1, h2) => + Hash256XorUtils.Compare(h1, h2, targetHash)); + IComparer comparerReverse = Comparer.Create((h1, h2) => + Hash256XorUtils.Compare(h2, h1, targetHash)); + + McsLock queueLock = new McsLock(); + + // Ordered by lowest distance. Will get popped for next round. + PriorityQueue<(ValueHash256, TNode), ValueHash256> bestSeen = new(comparer); + + // Ordered by highest distance. Added on result. Get popped as result. + PriorityQueue<(ValueHash256, TNode), ValueHash256> finalResult = new(comparerReverse); + + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, default)) + { + ValueHash256 nodeHash = nodeHashProvider.GetHash(node); + seen.TryAdd(nodeHash, node); + bestSeen.Enqueue((nodeHash, node), nodeHash); + } + + TaskCompletionSource roundComplete = new TaskCompletionSource(token); + int closestNodeRound = 0; + int currentRound = 0; + int queryingTask = 0; + bool finished = false; + + Task[] worker = Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => + { + while (!finished) + { + token.ThrowIfCancellationRequested(); + if (!TryGetNodeToQuery(out (ValueHash256 hash, TNode node)? toQuery)) + { + if (queryingTask > 0) + { + // Need to wait for all querying tasks first here. + await Task.WhenAny(roundComplete.Task, Task.Delay(100, token)); + continue; + } + + // No node to query and running query. + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); + break; + } + + try + { + if (ShouldStopDueToNoBetterResult(out var round)) + { + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); + break; + } + + queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); + (TNode, TNode[]? neighbours)? result = await WrappedFindNeighbourOp(toQuery.Value.node); + if (result == null) continue; + + ProcessResult(toQuery.Value.hash, toQuery.Value.node, result, round); + } + finally + { + Interlocked.Decrement(ref queryingTask); + if (roundComplete.TrySetResult()) roundComplete = new TaskCompletionSource(token); + } + } + }, token)).ToArray(); + + // When any of the worker is finished, we consider the whole query as done. + // This prevent this operation from hanging on a timed out request + await Task.WhenAny(worker); + finished = true; + await cts.CancelAsync(); + + return CompileResult(); + + async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourOp(TNode node) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + cts.CancelAfter(_findNeighbourHardTimeout); + + try + { + // targetHash is implied in findNeighbourOp + var ret = await findNeighbourOp(node, cts.Token); + nodeHealthTracker.OnIncomingMessageFrom(node); + + return (node, ret); + } + catch (OperationCanceledException) + { + nodeHealthTracker.OnRequestFailed(node); + return (node, null); + } + catch (Exception e) + { + nodeHealthTracker.OnRequestFailed(node); + if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); + return (node, null); + } + } + + bool TryGetNodeToQuery([NotNullWhen(true)] out (ValueHash256, TNode)? toQuery) + { + using McsLock.Disposable _ = queueLock.Acquire(); + if (bestSeen.Count == 0) + { + toQuery = default; + // No more node to query. + // Note: its possible that there are other worker currently which may add to bestSeen. + return false; + } + + Interlocked.Increment(ref queryingTask); + toQuery = bestSeen.Dequeue(); + return true; + } + + void ProcessResult(ValueHash256 hash, TNode toQuery, (TNode, TNode[]? neighbours)? valueTuple, int round) + { + using var _ = queueLock.Acquire(); + + finalResult.Enqueue((hash, toQuery), hash); + while (finalResult.Count > k) + { + finalResult.Dequeue(); + } + + TNode[]? neighbours = valueTuple?.neighbours; + if (neighbours == null) return; + + foreach (TNode neighbour in neighbours) + { + ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + + // Already queried, we ignore + if (queried.ContainsKey(neighbourHash)) continue; + + // When seen already dont record + if (!seen.TryAdd(neighbourHash, neighbour)) continue; + + bestSeen.Enqueue((neighbourHash, neighbour), neighbourHash); + + if (closestNodeRound < round) + { + if (finalResult.Count < k) + { + closestNodeRound = round; + } + + // If the worst item in final result is worst that this neighbour, update closes node round + if (finalResult.TryPeek(out (ValueHash256 hash, TNode node) worstResult, out ValueHash256 _) && comparer.Compare(neighbourHash, worstResult.hash) < 0) + { + closestNodeRound = round; + } + } + } + } + + TNode[] CompileResult() + { + using var _ = queueLock.Acquire(); + if (finalResult.Count > k) finalResult.Dequeue(); + return finalResult.UnorderedItems.Select((kv) => kv.Element.Item2).ToArray(); + } + + bool ShouldStopDueToNoBetterResult(out int round) + { + using var _ = queueLock.Acquire(); + + round = Interlocked.Increment(ref currentRound); + if (finalResult.Count >= k && round - closestNodeRound >= (config.Alpha*2)) + { + // No closer node for more than or equal to _alpha*2 round. + // Assume exit condition + // Why not just _alpha? + // Because there could be currently running work that may increase closestNodeRound. + // So including this worker, assume no more + if (_logger.IsTrace) _logger.Trace($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); + return true; + } + + return false; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs new file mode 100644 index 000000000000..fe203f73b09a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Caching; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using NonBlocking; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class NodeHealthTracker( + KademliaConfig config, + IRoutingTable routingTable, + INodeHashProvider nodeHashProvider, + IKademliaMessageSender kademliaMessageSender, + ILogManager logManager +) +{ + private readonly ILogger _logger = logManager.GetClassLogger>(); + + private readonly ConcurrentDictionary _isRefreshing = new(); + private readonly LruCache _peerFailures = new(1024, "peer failure"); + private readonly ValueHash256 _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); + private readonly TimeSpan _refreshPingTimeout = config.RefreshPingTimeout; + + private bool SameAsSelf(TNode node) + { + return nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; + } + + private void TryRefresh(TNode toRefresh) + { + ValueHash256 nodeHash = nodeHashProvider.GetHash(toRefresh); + if (_isRefreshing.TryAdd(nodeHash, true)) + { + Task.Run(async () => + { + // First, we delay in case any new message come and clear the refresh task, so we don't need to send any ping. + await Task.Delay(100); + if (!_isRefreshing.ContainsKey(nodeHash)) + { + return; + } + + // OK, fine, we'll ping it. + using CancellationTokenSource cts = new CancellationTokenSource(_refreshPingTimeout); + try + { + await kademliaMessageSender.Ping(toRefresh, cts.Token); + OnIncomingMessageFrom(toRefresh); + } + catch (OperationCanceledException) + { + OnRequestFailed(toRefresh); + } + catch (Exception e) + { + OnRequestFailed(toRefresh); + if (_logger.IsDebug) _logger.Debug($"Error while refreshing node {toRefresh}, {e}"); + } + + if (_isRefreshing.TryRemove(nodeHash, out _)) + { + routingTable.Remove(nodeHash); + } + }); + } + } + + /// + /// Call when an incoming message from a node is received. This is used by other algorithm for health checks. + /// + /// + public void OnIncomingMessageFrom(TNode node) + { + _isRefreshing.TryRemove(nodeHashProvider.GetHash(node), out _); + + var addResult = routingTable.TryAddOrRefresh(nodeHashProvider.GetHash(node), node, out TNode? toRefresh); + if (addResult == BucketAddResult.Full && toRefresh != null) + { + if (SameAsSelf(toRefresh)) + { + // Move the current node entry to the front of its bucket. + routingTable.TryAddOrRefresh(_currentNodeIdAsHash, node, out TNode? _); + } + else + { + TryRefresh(toRefresh); + } + } + _peerFailures.Delete(nodeHashProvider.GetHash(node)); + } + + /// + /// Call when a requset to a node failed. This is used by other algorithm for health checks. + /// + /// + public void OnRequestFailed(TNode node) + { + ValueHash256 hash = nodeHashProvider.GetHash(node); + if (!_peerFailures.TryGet(hash, out var currentFailure)) + { + _peerFailures.Set(hash, 1); + return; + } + + if (currentFailure >= config.NodeRequestFailureThreshold) + { + routingTable.Remove(hash); + _peerFailures.Delete(hash); + } + + _peerFailures.Set(hash, currentFailure + 1); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs new file mode 100644 index 000000000000..4b835f051ad9 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Logging; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// This find nearest k query follows the kademlia paper faithfully, but does not do much parallelism. +/// +public class OriginalLookupKNearestNeighbour( + IRoutingTable routingTable, + INodeHashProvider nodeHashProvider, + NodeHealthTracker nodeHealthTracker, + KademliaConfig config, + ILogManager logManager): ILookupAlgo +{ + private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; + private readonly ILogger _logger = logManager.GetClassLogger>(); + + public async Task Lookup( + ValueHash256 targetHash, + int k, + Func> findNeighbourOp, + CancellationToken token + ) { + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + + Func> wrappedFindNeighbourHop = async (node) => + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + cts.CancelAfter(_findNeighbourHardTimeout); + + try + { + // targetHash is implied in findNeighbourOp + var res = await findNeighbourOp(node, cts.Token); + nodeHealthTracker.OnIncomingMessageFrom(node); + return (node, res); + } + catch (OperationCanceledException) + { + nodeHealthTracker.OnRequestFailed(node); + return (node, null); + } + catch (Exception e) + { + nodeHealthTracker.OnRequestFailed(node); + _logger.Error($"Find neighbour op failed. {e}"); + return (node, null); + } + }; + + Dictionary queried = new(); + Dictionary queriedAndResponded = new(); + Dictionary seen = new(); + + IComparer comparer = Comparer.Create((h1, h2) => + Hash256XorUtils.Compare(h1, h2, targetHash)); + + // Ordered by lowest distance. Will get popped for next round. + PriorityQueue bestSeen = new (comparer); + + // Ordered by lowest distance. Will not get popped for next round, but will at final collection. + PriorityQueue bestSeenAllTime = new (comparer); + + ValueHash256 closestNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); + (ValueHash256 nodeHash, TNode node)[] roundQuery = routingTable.GetKNearestNeighbour(targetHash, default) + .Take(config.Alpha) + .Select((node) => (nodeHashProvider.GetHash(node), node)) + .ToArray(); + + foreach ((ValueHash256 nodeHash, TNode node) entry in roundQuery) + { + (ValueHash256 nodeHash, TNode node) = entry; + seen.Add(nodeHash, node); + bestSeen.Enqueue(node, nodeHash); + bestSeenAllTime.Enqueue(node, nodeHash); + } + + while (roundQuery.Length > 0) + { + // TODO: The paper mentioned that the next round can start immediately while waiting + // for the result of previous round. + token.ThrowIfCancellationRequested(); + + foreach (var kv in roundQuery) + { + queried.TryAdd(kv.nodeHash, kv.node); + } + + (TNode NodeId, TNode[]? Neighbours)[] currentRoundResponse = await Task.WhenAll( + roundQuery.Select((hn) => wrappedFindNeighbourHop(hn.Item2))); + + bool hasCloserThanClosest = false; + foreach ((TNode NodeId, TNode[]? Neighbours) response in currentRoundResponse) + { + if (response.Neighbours == null) continue; // Timeout or failed to get response + if (_logger.IsTrace) _logger.Trace($"Received {response.Neighbours.Length} from {response.NodeId}"); + + queriedAndResponded.TryAdd(nodeHashProvider.GetHash(response.NodeId), response.NodeId); + + foreach (TNode neighbour in response.Neighbours) + { + ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + // Already queried, we ignore + if (queried.ContainsKey(neighbourHash)) continue; + + // When seen already dont record + if (!seen.TryAdd(neighbourHash, neighbour)) continue; + + bestSeen.Enqueue(neighbour, neighbourHash); + bestSeenAllTime.Enqueue(neighbour, neighbourHash); + + if (comparer.Compare(neighbourHash, closestNodeHash) < 0) + { + hasCloserThanClosest = true; + closestNodeHash = neighbourHash; + } + } + } + + if (!hasCloserThanClosest) + { + // end condition it seems + break; + } + + int toTake = Math.Min(config.Alpha, bestSeen.Count); + roundQuery = Enumerable.Range(0, toTake).Select((_) => + { + TNode node = bestSeen.Dequeue(); + return (nodeHashProvider.GetHash(node), node); + }).ToArray(); + } + + // At this point need to query for the maxNode. + List result = []; + while (result.Count < k && bestSeenAllTime.Count > 0) + { + token.ThrowIfCancellationRequested(); + TNode nextLowest = bestSeenAllTime.Dequeue(); + ValueHash256 nextLowestHash = nodeHashProvider.GetHash(nextLowest); + + if (queriedAndResponded.ContainsKey(nextLowestHash)) + { + result.Add(nextLowest); + continue; + } + + if (queried.ContainsKey(nextLowestHash)) + { + // Queried but not responded + continue; + } + + // TODO: In parallel? + // So the paper mentioned that node that it need to query findnode for node that was not queried. + (_, TNode[]? nextCandidate) = await wrappedFindNeighbourHop(nextLowest); + if (nextCandidate != null) + { + result.Add(nextLowest); + } + } + + return result.ToArray(); + } +} From ebbae61b8007e8fbe2e3c7c744c3ac3eb2a38f1f Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 16 Apr 2025 13:49:39 +0800 Subject: [PATCH 002/182] Commit before I do something bad --- .../Kademlia/IKademlia.cs | 1 - .../Kademlia/KademliaModule.cs | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index 307a4d2ae0cf..81b47dba7694 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Lantern.Discv5.Enr; using Nethermind.Core.Crypto; namespace Nethermind.Network.Discovery.Kademlia; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs new file mode 100644 index 000000000000..699d99b174e9 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac; +using Nethermind.Core; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class KademliaModule : Module where TNode : notnull +{ + protected override void Load(ContainerBuilder builder) + { + base.Load(builder); + + builder + .AddSingleton, Kademlia>() + .AddSingleton, KademliaKademliaMessageReceiver>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>(provider => + { + KademliaConfig config = provider.Resolve>(); + if (config.UseNewLookup) + { + return provider.Resolve>(); + } + + return provider.Resolve>(); + }) + .AddSingleton>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>(provider => + { + KademliaConfig config = provider.Resolve>(); + if (config.UseTreeBasedRoutingTable) + { + return provider.Resolve>(); + } + + return provider.Resolve>(); + }); + } +} From 0fbaa511537152c8e05c3ebcfe0808fa84944154 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 16 Apr 2025 18:32:20 +0800 Subject: [PATCH 003/182] Better default --- .../Kademlia/IKademliaMessageSender.cs | 8 ++++++++ .../Kademlia/KademliaConfig.cs | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs index a517bfefe023..3e796b21b0e2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs @@ -5,12 +5,20 @@ namespace Nethermind.Network.Discovery.Kademlia; +/// +/// Should be exposed by application to kademlia so that kademlia can send out message. +/// +/// public interface IKademliaMessageSender { Task Ping(TNode receiver, CancellationToken token); Task FindNeighbours(TNode receiver, ValueHash256 hash, CancellationToken token); } +/// +/// Application should call this class on incoming messages. +/// +/// public interface IKademliaMessageReceiver: IKademliaMessageSender { } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index 50a3ac806cc8..acfe88f17c32 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -18,7 +18,7 @@ public class KademliaConfig /// /// Alpha, as in the parallelism of the lookup algorith. /// - public int Alpha { get; set; } + public int Alpha { get; set; } = 3; /// /// Beta, as in B in kademlia the kademlia paper, 4.2 Accelerated Lookups @@ -39,7 +39,7 @@ public class KademliaConfig /// /// Use a different algorithm for the neighbour and value lookup. /// - public bool UseNewLookup { get; set; } + public bool UseNewLookup { get; set; } = true; /// /// The timeout for each find neighbour call lookup From c6fcc4fb8d2ddccec68343cef768ec46beb154b6 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 16 Apr 2025 18:32:34 +0800 Subject: [PATCH 004/182] It does work, just really slow --- .../Extensions/CancellationTokenExtensions.cs | 10 + .../CompositeDiscoveryApp.cs | 1 + .../DiscoveryApp.cs | 334 ++++++++++++------ .../Kademlia/NewLookupKNearestNeighbour.cs | 1 + .../KademliaDiscv4MessageSender.cs | 289 +++++++++++++++ .../NettyDiscoveryHandler.cs | 8 +- 6 files changed, 531 insertions(+), 112 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs diff --git a/src/Nethermind/Nethermind.Core/Extensions/CancellationTokenExtensions.cs b/src/Nethermind/Nethermind.Core/Extensions/CancellationTokenExtensions.cs index f4828653398b..a7d801fbab4f 100644 --- a/src/Nethermind/Nethermind.Core/Extensions/CancellationTokenExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Extensions/CancellationTokenExtensions.cs @@ -63,5 +63,15 @@ public static AutoCancelTokenSource CreateChildTokenSource(this CancellationToke return new AutoCancelTokenSource(cts); } + + public static CancellationTokenRegistration RegisterToCompletionSource(this CancellationToken cancellationToken, TaskCompletionSource taskCompletionSource) + { + return cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(), false); + } + + public static CancellationTokenRegistration RegisterToCompletionSource(this CancellationToken cancellationToken, TaskCompletionSource taskCompletionSource) + { + return cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(), false); + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index 40a1f168a50d..cdcaca86c9e3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -190,6 +190,7 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator _logManager); _v4 = new DiscoveryApp( + selfNodeRecord, nodesLocator, discoveryManager, nodeTable, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 9734d3364288..57a6a8c58994 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -1,9 +1,11 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics; using System.Net.NetworkInformation; using System.Runtime.CompilerServices; using System.Threading.Channels; +using Autofac; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; using Nethermind.Config; @@ -14,8 +16,9 @@ using Nethermind.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Lifecycle; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Discovery.RoutingTable; +using Nethermind.Network.Enr; using Nethermind.Stats.Model; using LogLevel = DotNetty.Handlers.Logging.LogLevel; @@ -26,19 +29,32 @@ public class DiscoveryApp : IDiscoveryApp private readonly IDiscoveryConfig _discoveryConfig; private readonly ITimestamper _timestamper; private readonly INodesLocator _nodesLocator; - private readonly IDiscoveryManager _discoveryManager; - private readonly INodeTable _nodeTable; + // private readonly IDiscoveryManager _discoveryManager; + // private readonly INodeTable _nodeTable; private readonly ILogManager _logManager; private readonly ILogger _logger; private readonly IMessageSerializationService _messageSerializationService; private readonly ICryptoRandom _cryptoRandom; private readonly INetworkStorage _discoveryStorage; private readonly INetworkConfig _networkConfig; + private IContainer? _kademliaServices; + + private readonly IList _bootNodes; + private PublicKey _masterNode = null!; + private readonly NodeRecord _selfNodeRecorrd; + +#pragma warning disable CS0414 // Field is assigned but its value is never used + private KademliaDiscv4MessageReceiver _discv4MessageReceiver = null!; + private KademliaDiscv4MessageSender _discv4MessageSender = null!; + private IKademlia _kademlia = null!; +#pragma warning restore CS0414 // Field is assigned but its value is never used private NettyDiscoveryHandler? _discoveryHandler; private Task? _storageCommitTask; - public DiscoveryApp(INodesLocator nodesLocator, + public DiscoveryApp( + NodeRecord selfNodeRecord, + INodesLocator nodesLocator, IDiscoveryManager? discoveryManager, INodeTable? nodeTable, IMessageSerializationService? msgSerializationService, @@ -49,24 +65,54 @@ public DiscoveryApp(INodesLocator nodesLocator, ITimestamper? timestamper, ILogManager? logManager) { + _selfNodeRecorrd = selfNodeRecord; _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); _logger = _logManager.GetClassLogger(); _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); _nodesLocator = nodesLocator ?? throw new ArgumentNullException(nameof(nodesLocator)); - _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); - _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); + // _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); + // _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); _messageSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); _cryptoRandom = cryptoRandom ?? throw new ArgumentNullException(nameof(cryptoRandom)); _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); + _bootNodes = new List(); _discoveryStorage.StartBatch(); + + NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); + if (bootnodes.Length == 0) + { + if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); + return; + } + + for (int i = 0; i < bootnodes.Length; i++) + { + NetworkNode bootnode = bootnodes[i]; + if (bootnode.NodeId is null) + { + _logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); + } + + _bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.Port)); + } + } + + private class NodeNodeHashProvider : INodeHashProvider + { + public ValueHash256 GetHash(Node node) + { + return node.Id.Hash; + } } public void Initialize(PublicKey masterPublicKey) { - _discoveryManager.NodeDiscovered += OnNodeDiscovered; + // _discoveryManager.NodeDiscovered += OnNodeDiscovered; + _masterNode = masterPublicKey; + /* _nodeTable.Initialize(masterPublicKey); if (_nodeTable.MasterNode is null) { @@ -76,6 +122,28 @@ public void Initialize(PublicKey masterPublicKey) } _nodesLocator.Initialize(_nodeTable.MasterNode); + */ + + _kademliaServices = new ContainerBuilder() + .AddModule(new KademliaModule()) + .AddSingleton, NodeNodeHashProvider>() + .AddSingleton(_timestamper) + .AddSingleton(_networkConfig) + .AddSingleton(_logManager) + .AddSingleton(_selfNodeRecorrd) + .AddSingleton>(new KademliaConfig() + { + CurrentNodeId = new Node(_masterNode, "127.0.0.1", 9999, true) + }) + .AddSingleton, KademliaDiscv4MessageSender>() + .AddSingleton() + .Build(); + + _kademlia = _kademliaServices.Resolve>(); + _discv4MessageReceiver = _kademliaServices.Resolve(); + _discv4MessageSender = _kademliaServices.Resolve(); + + // TODO: Setup kademlia here } public Task StartAsync() @@ -112,11 +180,13 @@ await _storageCommitTask.ContinueWith(x => Cleanup(); if (_logger.IsInfo) _logger.Info("Discovery shutdown complete.. please wait for all components to close"); + + _kademliaServices?.DisposeAsync(); } public void AddNodeToDiscovery(Node node) { - _discoveryManager.GetNodeLifecycleManager(node); + _kademlia.AddOrRefresh(node); } private void Initialize() @@ -130,6 +200,7 @@ private void Initialize() private void ResetUnreachableStatus(object? sender, NetworkAvailabilityEventArgs e) { + /* if (!e.IsAvailable) { return; @@ -139,13 +210,15 @@ private void ResetUnreachableStatus(object? sender, NetworkAvailabilityEventArgs { unreachable.ResetUnreachableStatus(); } + */ } public void InitializeChannel(IChannel channel) { - _discoveryHandler = new NettyDiscoveryHandler(_discoveryManager, channel, _messageSerializationService, + _discoveryHandler = new NettyDiscoveryHandler(_discv4MessageReceiver, channel, _messageSerializationService, _timestamper, _logManager); - _discoveryManager.MsgSender = _discoveryHandler; + + _discv4MessageSender.MsgSender = _discoveryHandler; _discoveryHandler.OnChannelActivated += OnChannelActivated; channel.Pipeline @@ -165,23 +238,23 @@ private void OnChannelActivated(object? sender, EventArgs e) Task.Factory .StartNew(() => OnChannelActivated(_appShutdownSource.Token), _appShutdownSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) .ContinueWith - ( - t => - { - if (t.IsFaulted) - { - string faultMessage = "Cannot activate channel."; - _logger.Info(faultMessage); - throw t.Exception ?? - (Exception)new NetworkingException(faultMessage, NetworkExceptionType.Discovery); - } - - if (t.IsCompleted && !_appShutdownSource.IsCancellationRequested) + ( + t => { - _logger.Debug("Discovery App initialized."); + if (t.IsFaulted) + { + string faultMessage = "Cannot activate channel."; + _logger.Info(faultMessage); + throw t.Exception ?? + (Exception)new NetworkingException(faultMessage, NetworkExceptionType.Discovery); + } + + if (t.IsCompleted && !_appShutdownSource.IsCancellationRequested) + { + _logger.Debug("Discovery App initialized."); + } } - } - ); + ); } private async Task OnChannelActivated(CancellationToken cancellationToken) @@ -207,10 +280,12 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) //Check if we were able to communicate with any trusted nodes or persisted nodes //if so no need to replay bootstrapping, we can start discovery process + /* if (_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count != 0) { break; } + */ _logger.Warn("Could not communicate with any nodes (bootnodes, trusted nodes, persisted nodes)."); await Task.Delay(1000, cancellationToken); @@ -240,7 +315,7 @@ private void AddPersistedNodes(CancellationToken cancellationToken) break; } - if (!_discoveryManager.NodesFilter.Set(networkNode.HostIp)) + if (!_discv4MessageSender.NodesFilter.Set(networkNode.HostIp)) { // Already seen this node ip recently continue; @@ -259,6 +334,7 @@ private void AddPersistedNodes(CancellationToken cancellationToken) continue; } + /* INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node, true); if (manager is null) { @@ -272,6 +348,7 @@ private void AddPersistedNodes(CancellationToken cancellationToken) } manager.NodeStats.CurrentPersistedNodeReputation = networkNode.Reputation; + */ if (_logger.IsTrace) _logger.Trace($"Adding persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); } @@ -310,94 +387,80 @@ private void Cleanup() private async Task InitializeBootnodes(CancellationToken cancellationToken) { - NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); - if (bootnodes.Length == 0) + foreach (var bootNode in _bootNodes) { - if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); - return true; + _kademlia.AddOrRefresh(bootNode); } - List managers = new(); - for (int i = 0; i < bootnodes.Length; i++) + //Wait for pong message to come back from Boot nodes + /* + int maxWaitTime = _discoveryConfig.BootnodePongTimeout; + int itemTime = maxWaitTime / 100; + for (int i = 0; i < 100; i++) + { + if (cancellationToken.IsCancellationRequested) { - NetworkNode bootnode = bootnodes[i]; - if (bootnode.NodeId is null) - { - _logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); - } - - Node node = new(bootnode.NodeId, bootnode.Host, bootnode.Port); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node); - if (manager is not null) - { - managers.Add(manager); - } - else - { - _logger.Warn($"Bootnode config contains self: {bootnode.NodeId}"); - } + break; } - //Wait for pong message to come back from Boot nodes - int maxWaitTime = _discoveryConfig.BootnodePongTimeout; - int itemTime = maxWaitTime / 100; - for (int i = 0; i < 100; i++) + if (managers.Any(static x => x.State == NodeLifecycleState.Active)) { - if (cancellationToken.IsCancellationRequested) - { - break; - } - - if (managers.Any(static x => x.State == NodeLifecycleState.Active)) - { - break; - } + break; + } - if (_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count != 0) - { - if (_logger.IsTrace) - _logger.Trace( - "Was not able to connect to any of the bootnodes, but successfully connected to at least one persisted node."); - break; - } + if (_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count != 0) + { + if (_logger.IsTrace) + _logger.Trace( + "Was not able to connect to any of the bootnodes, but successfully connected to at least one persisted node."); + break; + } - if (_logger.IsTrace) _logger.Trace($"Waiting {itemTime} ms for bootnodes to respond"); + if (_logger.IsTrace) _logger.Trace($"Waiting {itemTime} ms for bootnodes to respond"); - try - { - await Task.Delay(itemTime, cancellationToken); - } - catch (OperationCanceledException) - { - break; - } + try + { + await Task.Delay(itemTime, cancellationToken); + } + catch (OperationCanceledException) + { + break; } + } - int reachedNodeCounter = 0; - for (int i = 0; i < managers.Count; i++) + int reachedNodeCounter = 0; + for (int i = 0; i < managers.Count; i++) + { + INodeLifecycleManager manager = managers[i]; + if (manager.State != NodeLifecycleState.Active) { - INodeLifecycleManager manager = managers[i]; - if (manager.State != NodeLifecycleState.Active) - { - if (_logger.IsTrace) - _logger.Trace($"Could not reach bootnode: {manager.ManagedNode.Host}:{manager.ManagedNode.Port}"); - } - else - { - if (_logger.IsTrace) - _logger.Trace($"Reached bootnode: {manager.ManagedNode.Host}:{manager.ManagedNode.Port}"); - reachedNodeCounter++; - } + if (_logger.IsTrace) + _logger.Trace($"Could not reach bootnode: {manager.ManagedNode.Host}:{manager.ManagedNode.Port}"); + } + else + { + if (_logger.IsTrace) + _logger.Trace($"Reached bootnode: {manager.ManagedNode.Host}:{manager.ManagedNode.Port}"); + reachedNodeCounter++; } + } + */ + /* if (_logger.IsInfo) _logger.Info( $"Connected to {reachedNodeCounter} bootnodes, {_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count} trusted/persisted nodes"); return reachedNodeCounter > 0; + */ + + await _kademlia.Bootstrap(cancellationToken); + return true; } - private async Task RunDiscoveryProcess() + private Task RunDiscoveryProcess() { + return Task.CompletedTask; + /* byte[] randomId = new byte[64]; CancellationToken cancellationToken = _appShutdownSource.Token; PeriodicTimer timer = new(TimeSpan.FromMilliseconds(10)); @@ -451,6 +514,7 @@ private async Task RunDiscoveryProcess() lastTickMs = Environment.TickCount64; } + */ } [Todo(Improve.Allocations, "Remove ToArray here - address as a part of the network DB rewrite")] @@ -460,10 +524,11 @@ private async Task RunDiscoveryPersistenceCommit() PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_discoveryConfig.DiscoveryPersistenceInterval)); while (!cancellationToken.IsCancellationRequested - && await timer.WaitForNextTickAsync(cancellationToken)) + && await timer.WaitForNextTickAsync(cancellationToken)) { try { + /* IReadOnlyCollection managers = _discoveryManager.GetNodeLifecycleManagers(); DateTime utcNow = DateTime.UtcNow; //we need to update all notes to update reputation @@ -478,6 +543,7 @@ private async Task RunDiscoveryPersistenceCommit() _discoveryStorage.Commit(); _discoveryStorage.StartBatch(); + */ } catch (Exception ex) { @@ -486,44 +552,96 @@ private async Task RunDiscoveryPersistenceCommit() } } + /* private void OnNodeDiscovered(object? sender, NodeEventArgs e) { NodeAdded?.Invoke(this, e); } - public event EventHandler? NodeAdded; + private event EventHandler? NodeAdded; + */ - public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { - // TODO: Rewrote this to properly support throttling. - Channel ch = Channel.CreateBounded(64); // Some reasonably large value - void handler(object? _, NodeEventArgs args) + if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); + Channel ch = Channel.CreateBounded(1); + + async Task DiscoverAsync(ValueHash256 hash) { - if (!ch.Writer.TryWrite(args.Node)) + if (_logger.IsDebug) _logger.Debug($"Looking up {hash}"); + bool anyFound = false; + IList newNodesFound = (await _kademlia.LookupNodesClosest(hash, token)).ToList(); + foreach (var node in newNodesFound) + { + anyFound = true; + await ch.Writer.WriteAsync(node, token); + } + + if (!anyFound) { - // Keep in mind, the channel is already buffered, so forgetting this node is probably fine. - _nodesLocator.ShouldThrottle = true; + if (_logger.IsDebug) _logger.Debug($"No node found for {hash}"); } else { - _nodesLocator.ShouldThrottle = false; + if (_logger.IsDebug) _logger.Debug($"Found {newNodesFound.Count} nodes"); + foreach (var node in newNodesFound) + { + if (_logger.IsDebug) _logger.Debug($" {node}"); + } } } - try + Random random = new(); + + const int RandomNodesToLookupCount = 3; + + Task discoverTask = Task.Run(async () => { - // TODO: Use lookup like kademlia - NodeAdded += handler; + ValueHash256 randomNodeId = new(); + while (!token.IsCancellationRequested) + { + Stopwatch iterationTime = Stopwatch.StartNew(); + foreach (var bootNode in _bootNodes) + { + _kademlia.AddOrRefresh(bootNode); + } + + try + { + List discoverTasks = new List(); + discoverTasks.Add(DiscoverAsync(_masterNode.Hash)); + + for (int i = 0; i < RandomNodesToLookupCount; i++) + { + random.NextBytes(randomNodeId.BytesAsSpan); + discoverTasks.Add(DiscoverAsync(randomNodeId)); + } + + await Task.WhenAll(discoverTasks); + } + catch (Exception ex) + { + if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); + } - await foreach (Node node in ch.Reader.ReadAllAsync(cancellationToken)) + // Prevent high CPU when all node is not reachable due to network connectivity issue. + if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + }); + + try + { + await foreach (Node node in ch.Reader.ReadAllAsync(token)) { yield return node; } } finally { - NodeAdded -= handler; - _nodesLocator.ShouldThrottle = false; + await discoverTask; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs index 1ba8f90df5a5..8709af7854f9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs @@ -137,6 +137,7 @@ CancellationToken token catch (Exception e) { nodeHealthTracker.OnRequestFailed(node); + if (_logger.IsWarn) _logger.Warn($"Find neighbour op failed. {e}"); if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); return (node, null); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs new file mode 100644 index 000000000000..4e10bafdfc15 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -0,0 +1,289 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Lantern.Discv5.WireProtocol.Session; +using Nethermind.Core; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Timers; +using Nethermind.Logging; +using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; +using Nethermind.Stats.Model; +using NonBlocking; + +namespace Nethermind.Network.Discovery; + +public class KademliaDiscv4MessageSender( + INetworkConfig networkConfig, + KademliaConfig kademliaConfig, + ITimestamper timestamper +): IKademliaMessageSender +{ + public IMsgSender? MsgSender { get; set; } + public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); + private TimeSpan _requestTimeout = TimeSpan.FromSeconds(5); + + private ConcurrentDictionary _awaitingPingMsg = new(); + + // TODO: Allow multiple in flight request per node + private ConcurrentDictionary> _awaitingFindNeighbourMsg = new(); + private ConcurrentDictionary> _awaitingEnrRequestMsg = new(); + + public async Task Ping(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); + await SendDiscV4Message(msg); + ValueHash256 mdc = new ValueHash256(msg.Mdc!); // Mdc is populated after serialization + + TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); + try + { + _awaitingPingMsg.TryAdd(mdc, completionSource); + await completionSource.Task; + } + finally + { + unregister.Unregister(); + _awaitingPingMsg.TryRemove(mdc, out _); + } + } + + public async Task FindNeighbours(Node receiver, ValueHash256 hash, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), hash.ToByteArray()); + ValueHash256 requestHash = receiver.IdHash; + + TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); + while (!_awaitingFindNeighbourMsg.TryAdd(requestHash, completionSource)) + { + if (_awaitingFindNeighbourMsg.TryGetValue(requestHash, out TaskCompletionSource? tcs)) + { + try + { + await tcs.Task; + } + finally + { + _awaitingFindNeighbourMsg.TryRemove(requestHash, out _); + } + } + } + + await SendDiscV4Message(msg); + try + { + return await completionSource.Task; + } + finally + { + unregister.Unregister(); + _awaitingFindNeighbourMsg.TryRemove(requestHash, out _); + } + } + + public async Task SendEnrRequest(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); + ValueHash256 requestHash = receiver.IdHash; + + TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); + while (!_awaitingEnrRequestMsg.TryAdd(requestHash, completionSource)) + { + if (_awaitingEnrRequestMsg.TryGetValue(requestHash, out TaskCompletionSource? tcs)) + { + try + { + await tcs.Task; + } + finally + { + _awaitingEnrRequestMsg.TryRemove(requestHash, out _); + } + } + } + + await SendDiscV4Message(msg); + try + { + return await completionSource.Task; + } + finally + { + unregister.Unregister(); + _awaitingEnrRequestMsg.TryRemove(requestHash, out _); + } + } + + internal void OnPong(Node node, PongMsg msg) + { + ValueHash256 mdc = new ValueHash256(msg.PingMdc); + if (_awaitingPingMsg.TryRemove(mdc, out TaskCompletionSource? completionSource)) + { + completionSource.TrySetResult(); + } + } + + public void HandleEnrResponse(Node node, EnrResponseMsg msg) + { + ValueHash256 requestId = node.IdHash; + if (_awaitingEnrRequestMsg.TryRemove(requestId, out TaskCompletionSource? completionSource)) + { + completionSource.TrySetResult(msg); + } + } + + public void OnNeighbour(Node node, NeighborsMsg msg) + { + ValueHash256 requestId = node.IdHash; + if (_awaitingFindNeighbourMsg.TryRemove(requestId, out TaskCompletionSource? completionSource)) + { + completionSource.TrySetResult(msg.Nodes); + } + } + + public async Task SendDiscV4Message(DiscoveryMsg msg) + { + if (MsgSender is { } sender) + { + await sender.SendMsg(msg); + } + } + + /// + /// This is the value set by other clients based on real network tests. + /// + private const int ExpirationTimeInSeconds = 20; + private long CalculateExpirationTime() + { + return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; + } +} + +#pragma warning disable CS9113 // Parameter is unread. +public class KademliaDiscv4MessageReceiver( + IKademliaMessageReceiver receiver, + KademliaDiscv4MessageSender sender, + NodeRecord selfNodeRecord, + ITimestamper timestamper, + ILogManager logManager +) : IDiscoveryMsgListener, IAsyncDisposable +#pragma warning restore CS9113 // Parameter is unread. +{ + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly CancellationTokenSource _cts = new(); + + public void OnIncomingMsg(DiscoveryMsg msg) + { + try + { + if (_logger.IsTrace) _logger.Trace($"Received msg: {msg}"); + MsgType msgType = msg.MsgType; + Node node = new(msg.FarPublicKey, msg.FarAddress); + + switch (msgType) + { + case MsgType.Neighbors: + sender.OnNeighbour(node, (NeighborsMsg)msg); + break; + case MsgType.Pong: + sender.OnPong(node, (PongMsg)msg); + break; + case MsgType.Ping: + PingMsg ping = (PingMsg)msg; + HandlePing(node, ping); + break; + case MsgType.FindNode: + HandleFindNode(node, (FindNodeMsg)msg); + break; + case MsgType.EnrRequest: + HandleEnrRequest(node, (EnrRequestMsg)msg); + break; + case MsgType.EnrResponse: + sender.HandleEnrResponse(node, (EnrResponseMsg)msg); + break; + default: + _logger.Error($"Unsupported msgType: {msgType}"); + return; + } + } + catch (Exception e) + { + _logger.Error("Error during msg handling", e); + } + } + + private void HandleEnrRequest(Node node, EnrRequestMsg msg) + { + if (!IsPeerSafe(node)) return; + + Task.Run(async () => + { + Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); + await sender.SendDiscV4Message(new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + }); + } + + private void HandleFindNode(Node node, FindNodeMsg msg) + { + if (!IsPeerSafe(node)) return; + + Task.Run(async () => + { + ValueHash256 searchId = new ValueHash256(msg.SearchedNodeId); + Node[] nodes = await receiver.FindNeighbours(node, searchId, _cts.Token); + if (nodes.Length > 12) + { + // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. + nodes = nodes.Slice(0, 12).ToArray(); + } + await sender.SendDiscV4Message(new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + }); + } + + private void HandlePing(Node node, PingMsg ping) + { + Task.Run(async () => + { + await receiver.Ping(node, _cts.Token); + PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); + await sender.SendDiscV4Message(msg); + }); + } + + private bool IsPeerSafe(Node node) + { + return true; + } + + /// + /// This is the value set by other clients based on real network tests. + /// + private const int ExpirationTimeInSeconds = 20; + private long CalculateExpirationTime() + { + return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; + } + + public async ValueTask DisposeAsync() + { + await _cts.CancelAsync(); + _cts.Dispose(); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index a0dfb7afedfe..8b05fa305edc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -19,20 +19,20 @@ namespace Nethermind.Network.Discovery; public class NettyDiscoveryHandler : NettyDiscoveryBaseHandler, IMsgSender { private readonly ILogger _logger; - private readonly IDiscoveryManager _discoveryManager; + private readonly IDiscoveryMsgListener _discoveryMsgListener; private readonly IChannel _channel; private readonly IMessageSerializationService _msgSerializationService; private readonly ITimestamper _timestamper; public NettyDiscoveryHandler( - IDiscoveryManager? discoveryManager, + IDiscoveryMsgListener? discoveryManager, IChannel? channel, IMessageSerializationService? msgSerializationService, ITimestamper? timestamper, ILogManager? logManager) : base(logManager) { _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); + _discoveryMsgListener = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); _channel = channel ?? throw new ArgumentNullException(nameof(channel)); _msgSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); @@ -175,7 +175,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket // Explicitly run it on the default scheduler to prevent something down the line hanging netty task scheduler. Task.Factory.StartNew( - () => _discoveryManager.OnIncomingMsg(msg), + () => _discoveryMsgListener.OnIncomingMsg(msg), CancellationToken.None, TaskCreationOptions.RunContinuationsAsynchronously, TaskScheduler.Default From d865921e52c8556b808c7d3a55f1890c3e565651 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 17 Apr 2025 19:48:24 +0800 Subject: [PATCH 005/182] It does work. Just need some metric. --- .../DiscoveryApp.cs | 43 ++-- .../Kademlia/ILookupAlgo2.cs | 29 +++ .../Kademlia/KademliaConfig.cs | 2 +- .../Kademlia/KademliaModule.cs | 1 + .../NewTrackingLookupKNearestNeighbour.cs | 234 ++++++++++++++++++ .../KademliaDiscv4MessageSender.cs | 190 ++++++++++---- .../Nethermind.Network.Enr/NodeRecord.cs | 5 + 7 files changed, 434 insertions(+), 70 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 57a6a8c58994..154f3ab2abae 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -28,13 +28,13 @@ public class DiscoveryApp : IDiscoveryApp { private readonly IDiscoveryConfig _discoveryConfig; private readonly ITimestamper _timestamper; - private readonly INodesLocator _nodesLocator; + // private readonly INodesLocator _nodesLocator; // private readonly IDiscoveryManager _discoveryManager; // private readonly INodeTable _nodeTable; private readonly ILogManager _logManager; private readonly ILogger _logger; private readonly IMessageSerializationService _messageSerializationService; - private readonly ICryptoRandom _cryptoRandom; + // private readonly ICryptoRandom _cryptoRandom; private readonly INetworkStorage _discoveryStorage; private readonly INetworkConfig _networkConfig; private IContainer? _kademliaServices; @@ -43,11 +43,10 @@ public class DiscoveryApp : IDiscoveryApp private PublicKey _masterNode = null!; private readonly NodeRecord _selfNodeRecorrd; -#pragma warning disable CS0414 // Field is assigned but its value is never used private KademliaDiscv4MessageReceiver _discv4MessageReceiver = null!; private KademliaDiscv4MessageSender _discv4MessageSender = null!; private IKademlia _kademlia = null!; -#pragma warning restore CS0414 // Field is assigned but its value is never used + private ILookupAlgo2 _lookup2 = null!; private NettyDiscoveryHandler? _discoveryHandler; private Task? _storageCommitTask; @@ -70,12 +69,12 @@ public DiscoveryApp( _logger = _logManager.GetClassLogger(); _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - _nodesLocator = nodesLocator ?? throw new ArgumentNullException(nameof(nodesLocator)); + // _nodesLocator = nodesLocator ?? throw new ArgumentNullException(nameof(nodesLocator)); // _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); // _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); _messageSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); - _cryptoRandom = cryptoRandom ?? throw new ArgumentNullException(nameof(cryptoRandom)); + // _cryptoRandom = cryptoRandom ?? throw new ArgumentNullException(nameof(cryptoRandom)); _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); _bootNodes = new List(); @@ -140,6 +139,7 @@ public void Initialize(PublicKey masterPublicKey) .Build(); _kademlia = _kademliaServices.Resolve>(); + _lookup2 = _kademliaServices.Resolve>(); _discv4MessageReceiver = _kademliaServices.Resolve(); _discv4MessageSender = _kademliaServices.Resolve(); @@ -387,10 +387,12 @@ private void Cleanup() private async Task InitializeBootnodes(CancellationToken cancellationToken) { + /* foreach (var bootNode in _bootNodes) { _kademlia.AddOrRefresh(bootNode); } + */ //Wait for pong message to come back from Boot nodes /* @@ -570,10 +572,12 @@ async Task DiscoverAsync(ValueHash256 hash) { if (_logger.IsDebug) _logger.Debug($"Looking up {hash}"); bool anyFound = false; - IList newNodesFound = (await _kademlia.LookupNodesClosest(hash, token)).ToList(); - foreach (var node in newNodesFound) + int count = 0; + + await foreach (var node in _lookup2.Lookup(hash, token)) { anyFound = true; + count++; await ch.Writer.WriteAsync(node, token); } @@ -583,17 +587,13 @@ async Task DiscoverAsync(ValueHash256 hash) } else { - if (_logger.IsDebug) _logger.Debug($"Found {newNodesFound.Count} nodes"); - foreach (var node in newNodesFound) - { - if (_logger.IsDebug) _logger.Debug($" {node}"); - } + if (_logger.IsDebug) _logger.Debug($"Found {count} nodes"); } } Random random = new(); - const int RandomNodesToLookupCount = 3; + const int RandomNodesToLookupCount = 1; Task discoverTask = Task.Run(async () => { @@ -603,13 +603,24 @@ async Task DiscoverAsync(ValueHash256 hash) Stopwatch iterationTime = Stopwatch.StartNew(); foreach (var bootNode in _bootNodes) { - _kademlia.AddOrRefresh(bootNode); + _ = Task.Factory.StartNew(async (obj) => + { + try + { + Node node = (Node)obj!; + await _discv4MessageSender.Ping(node, token); + _kademlia.AddOrRefresh(node); + } + catch (OperationCanceledException) + { + } + }, bootNode); } try { List discoverTasks = new List(); - discoverTasks.Add(DiscoverAsync(_masterNode.Hash)); + // discoverTasks.Add(DiscoverAsync(_masterNode.Hash)); for (int i = 0; i < RandomNodesToLookupCount; i++) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs new file mode 100644 index 000000000000..6096481dfd75 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// Main find closest-k node within the network. See the kademlia paper, 2.3. +/// Since find value is basically the same also just with a shortcut, this allow changing the find neighbour op. +/// Find closest-k is also used to determine which node should store a particular value which is used by +/// store RPC (not implemented). +/// +public interface ILookupAlgo2 +{ + /// + /// The find neighbour operation here is configurable because the same algorithm is also used for finding + /// value int the network, except that it would short circuit once the value was found. + /// + /// + /// + /// + /// + /// + IAsyncEnumerable Lookup( + ValueHash256 targetHash, + CancellationToken token + ); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index acfe88f17c32..a0e3b90634e6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -44,7 +44,7 @@ public class KademliaConfig /// /// The timeout for each find neighbour call lookup /// - public TimeSpan LookupFindNeighbourHardTimout { get; set; } = TimeSpan.FromSeconds(5); + public TimeSpan LookupFindNeighbourHardTimout { get; set; } = TimeSpan.FromSeconds(10); /// /// The timeout for a ping message during a refresh after which the node is considered to be offline. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 699d99b174e9..f50a17a0e2c6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -27,6 +27,7 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) + .AddSingleton, NewaTrackingLookupKNearestNeighbour>() .AddSingleton>() .AddSingleton>() .AddSingleton>() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs new file mode 100644 index 000000000000..2f4bd5818d44 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Nethermind.Core.Caching; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Threading; +using Nethermind.Logging; +using NonBlocking; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class NewaTrackingLookupKNearestNeighbour( + IRoutingTable routingTable, + INodeHashProvider nodeHashProvider, + KademliaConfig kademliaConfig, + IKademliaMessageSender kademliaMessageSender, + NodeHealthTracker nodeHealthTracker, + KademliaConfig config, + ILogManager logManager) : ILookupAlgo2 +{ + private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; + private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ValueHash256 _currentNodeIdAsHash = nodeHashProvider.GetHash(kademliaConfig.CurrentNodeId); + + public async Task LookupFunc(TNode nextNode, ValueHash256 targetHash, CancellationToken token) + { + if (SameAsSelf(nextNode)) + { + return routingTable.GetKNearestNeighbour(targetHash); + } + return await kademliaMessageSender.FindNeighbours(nextNode, targetHash, token); + } + + private bool SameAsSelf(TNode node) + { + return nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; + } + public async IAsyncEnumerable Lookup( + ValueHash256 targetHash, + [EnumeratorCancellation] CancellationToken token + ) { + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + + using var cts = token.CreateChildTokenSource(); + token = cts.Token; + + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); + + IComparer comparer = Comparer.Create((h1, h2) => + Hash256XorUtils.Compare(h1, h2, targetHash)); + + McsLock queueLock = new McsLock(); + + // Ordered by lowest distance. Will get popped for next round. + PriorityQueue<(ValueHash256, TNode), ValueHash256> queryQueue = new(comparer); + ValueHash256 bestNodeId = ValueKeccak.Zero; + + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, default)) + { + ValueHash256 nodeHash = nodeHashProvider.GetHash(node); + seen.TryAdd(nodeHash, node); + queryQueue.Enqueue((nodeHash, node), nodeHash); + + if (bestNodeId == ValueKeccak.Zero || comparer.Compare(nodeHash, bestNodeId) < 0) + { + bestNodeId = nodeHash; + } + } + + Channel outChan = Channel.CreateBounded(1); + + TaskCompletionSource roundComplete = new TaskCompletionSource(token); + int closestNodeRound = 0; + int currentRound = 0; + int queryingTask = 0; + bool finished = false; + + Task[] worker = Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => + { + while (!finished) + { + token.ThrowIfCancellationRequested(); + if (!TryGetNodeToQuery(out (ValueHash256 hash, TNode node)? toQuery)) + { + if (queryingTask > 0) + { + // Need to wait for all querying tasks first here. + await Task.WhenAny(roundComplete.Task, Task.Delay(100, token)); + continue; + } + + // No node to query and running query. + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); + _logger.Warn("Stopping lookup. No node to query."); + break; + } + + Interlocked.Increment(ref queryingTask); + try + { + queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); + _logger.Warn($"Query {toQuery.Value.node} at round {currentRound}, isself {SameAsSelf(toQuery.Value.node)}"); + (TNode, TNode[]? neighbours) result = await WrappedFindNeighbourOp(toQuery.Value.node); + if (result.neighbours == null || result.neighbours?.Length == 0) + { + if (_logger.IsTrace) _logger.Trace("Empty result"); + continue; + } + + await ProcessResult(toQuery.Value.node, result, currentRound); + if (ShouldStopDueToNoBetterResult(out var round)) + { + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); + break; + } + } + finally + { + Interlocked.Decrement(ref queryingTask); + if (roundComplete.TrySetResult()) roundComplete = new TaskCompletionSource(token); + } + } + + outChan.Writer.TryComplete(); + }, token)).ToArray(); + + await foreach (var node in outChan.Reader.ReadAllAsync(token)) + { + yield return node; + } + + // When any of the worker is finished, we consider the whole query as done. + // This prevent this operation from hanging on a timed out request + await Task.WhenAny(worker); + finished = true; + + async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourOp(TNode node) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + cts.CancelAfter(_findNeighbourHardTimeout); + + try + { + // targetHash is implied in findNeighbourOp + var ret = await LookupFunc(node, targetHash, cts.Token); + nodeHealthTracker.OnIncomingMessageFrom(node); + + return (node, ret); + } + catch (OperationCanceledException) + { + if (_logger.IsWarn) _logger.Warn($"Find neighbour op timout."); + nodeHealthTracker.OnRequestFailed(node); + return (node, null); + } + catch (Exception e) + { + if (_logger.IsWarn) _logger.Warn($"Find neighbour op failed. {e}"); + nodeHealthTracker.OnRequestFailed(node); + if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); + return (node, null); + } + } + + bool TryGetNodeToQuery([NotNullWhen(true)] out (ValueHash256, TNode)? toQuery) + { + using McsLock.Disposable _ = queueLock.Acquire(); + if (queryQueue.Count == 0) + { + toQuery = default; + // No more node to query. + // Note: its possible that there are other worker currently which may add to bestSeen. + return false; + } + + toQuery = queryQueue.Dequeue(); + return true; + } + + async Task ProcessResult(TNode thisNode, (TNode, TNode[]? neighbours)? valueTuple, int round) + { + TNode[]? neighbours = valueTuple?.neighbours; + if (neighbours == null) return; + + var writer = outChan.Writer; + foreach (TNode neighbour in neighbours) + { + ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + + // Already queried, we ignore + if (queried.ContainsKey(neighbourHash)) continue; + + // When seen already dont record + if (!seen.TryAdd(neighbourHash, neighbour)) continue; + await writer.WriteAsync(neighbour, cts.Token); + + using var _ = queueLock.Acquire(); + bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; + queryQueue.Enqueue((neighbourHash, neighbour), neighbourHash); + + // If found a better node, reset closes node round and continue + if (closestNodeRound < round && foundBetter) + { + bestNodeId = neighbourHash; + closestNodeRound = round; + } + } + } + + bool ShouldStopDueToNoBetterResult(out int round) + { + round = Interlocked.Increment(ref currentRound); + if (round - closestNodeRound >= (config.Alpha*2)) + { + // No closer node for more than or equal to _alpha*2 round. + // Assume exit condition + // Why not just _alpha? + // Because there could be currently running work that may increase closestNodeRound. + // So including this worker, assume no more + if (_logger.IsTrace) _logger.Trace($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); + _logger.Warn($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); + return true; + } + + return false; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs index 4e10bafdfc15..238706fabaf0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -1,12 +1,12 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Lantern.Discv5.WireProtocol.Session; +using System.Diagnostics; using Nethermind.Core; +using Nethermind.Core.Caching; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; -using Nethermind.Core.Timers; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Kademlia; @@ -21,92 +21,124 @@ namespace Nethermind.Network.Discovery; public class KademliaDiscv4MessageSender( INetworkConfig networkConfig, KademliaConfig kademliaConfig, + ILogManager logManager, ITimestamper timestamper ): IKademliaMessageSender { + private ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); - private TimeSpan _requestTimeout = TimeSpan.FromSeconds(5); + private TimeSpan _unauthenticatedRequestTimeout = TimeSpan.FromSeconds(2.5); + private TimeSpan _requestTimeout = TimeSpan.FromSeconds(8); + private TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(1); + private TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(100); - private ConcurrentDictionary _awaitingPingMsg = new(); + private ConcurrentDictionary> _awaitingPingMsg = new(); + private ConcurrentDictionary _awaitingPongToNode = new(); // TODO: Allow multiple in flight request per node private ConcurrentDictionary> _awaitingFindNeighbourMsg = new(); private ConcurrentDictionary> _awaitingEnrRequestMsg = new(); - public async Task Ping(Node receiver, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; + private LruCache _lastPong = new(1024, ""); - PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); - await SendDiscV4Message(msg); - ValueHash256 mdc = new ValueHash256(msg.Mdc!); // Mdc is populated after serialization + private async Task EnsureSession(Node node, CancellationToken token) + { + if (_lastPong.TryGet(node.IdHash, out DateTimeOffset lastPong) && lastPong > DateTimeOffset.Now - TimeSpan.FromHours(12)) + { + if (_logger.IsTrace) _logger.Trace($"Node already had pong within deadline {node}. Pong duration: {DateTimeOffset.Now - lastPong}"); + return; + } - TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); + if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); + using var cts = token.CreateChildTokenSource(_tryAuthenticatedTimeout); + token = cts.Token; + TaskCompletionSource pongCts = new(TaskCreationOptions.RunContinuationsAsynchronously); + CancellationTokenRegistration unregister = token.RegisterToCompletionSource(pongCts); try { - _awaitingPingMsg.TryAdd(mdc, completionSource); - await completionSource.Task; + _awaitingPongToNode.TryAdd(node.IdHash, pongCts); + await Ping(node, token); + await pongCts.Task; + await Task.Delay(_waitAfterPongTimeout, token); // Give some time for peer to process pong. + + if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); + } + catch (OperationCanceledException) + { + if (_logger.IsTrace) _logger.Trace($"Node {node} timeout trying to trigger pong."); } finally { unregister.Unregister(); - _awaitingPingMsg.TryRemove(mdc, out _); + _awaitingPongToNode.TryRemove(node.IdHash, out _); } } - public async Task FindNeighbours(Node receiver, ValueHash256 hash, CancellationToken token) + private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) { + Stopwatch sw = Stopwatch.StartNew(); using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), hash.ToByteArray()); - ValueHash256 requestHash = receiver.IdHash; - - TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); - while (!_awaitingFindNeighbourMsg.TryAdd(requestHash, completionSource)) { - if (_awaitingFindNeighbourMsg.TryGetValue(requestHash, out TaskCompletionSource? tcs)) + using var firstTryCts = cts.Token.CreateChildTokenSource(_unauthenticatedRequestTimeout); + try { - try - { - await tcs.Task; - } - finally - { - _awaitingFindNeighbourMsg.TryRemove(requestHash, out _); - } + return await callRequest(firstTryCts.Token); + } + catch (OperationCanceledException) + { + // If we don't get a response in time, it could just be that we are not authenticated there } } - await SendDiscV4Message(msg); + await EnsureSession(node, token); + + // Then we just try a final time. try { - return await completionSource.Task; + return await callRequest(token); } - finally + catch (OperationCanceledException) { - unregister.Unregister(); - _awaitingFindNeighbourMsg.TryRemove(requestHash, out _); + if (_lastPong.TryGet(node.IdHash, out DateTimeOffset lastPong)) + { + _logger.Info($"Still cancelled after {sw.Elapsed} with pong time {DateTimeOffset.Now - lastPong}"); + } + + _lastPong.Delete(node.IdHash); + throw; } } - public async Task SendEnrRequest(Node receiver, CancellationToken token) + public async Task Ping(Node receiver, CancellationToken token) { using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); - ValueHash256 requestHash = receiver.IdHash; + PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); + + PongMsg pongMsg = await CallAndWaitForResponse(_awaitingPingMsg, receiver, msg, token); + if (!Bytes.AreEqual(msg.Mdc, pongMsg.PingMdc)) + { + _logger.Error($"Invalid pong mdc. Send {msg.Mdc?.ToHexString()}, Received {pongMsg.PingMdc?.ToHexString()}"); + throw new OperationCanceledException(); // Expose as timeout + } + } - TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + private async Task CallAndWaitForResponse( + ConcurrentDictionary> requestDictionary, + Node receiver, + DiscoveryMsg msg, + CancellationToken token + ) { + TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); - while (!_awaitingEnrRequestMsg.TryAdd(requestHash, completionSource)) + ValueHash256 requestHash = receiver.IdHash; + while (!requestDictionary.TryAdd(requestHash, completionSource)) { - if (_awaitingEnrRequestMsg.TryGetValue(requestHash, out TaskCompletionSource? tcs)) + if (requestDictionary.TryGetValue(requestHash, out TaskCompletionSource? tcs)) { try { @@ -114,12 +146,12 @@ public async Task SendEnrRequest(Node receiver, CancellationToke } finally { - _awaitingEnrRequestMsg.TryRemove(requestHash, out _); + requestDictionary.TryRemove(requestHash, out _); } } } - await SendDiscV4Message(msg); + await SendDiscV4Message(receiver, msg); try { return await completionSource.Task; @@ -127,16 +159,54 @@ public async Task SendEnrRequest(Node receiver, CancellationToke finally { unregister.Unregister(); - _awaitingEnrRequestMsg.TryRemove(requestHash, out _); + requestDictionary.TryRemove(requestHash, out _); } } + public async Task FindNeighbours(Node receiver, ValueHash256 hash, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + return await RunAuthenticatedRequest(receiver, async token => + { + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), hash.ToByteArray()); + + return await CallAndWaitForResponse(_awaitingFindNeighbourMsg, receiver, msg, token); + }, token); + } + + public async Task SendEnrRequest(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + return await RunAuthenticatedRequest(receiver, async token => + { + EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); + + return await CallAndWaitForResponse(_awaitingEnrRequestMsg, receiver, msg, token); + }, token); + } + + public async Task SendUnauthEnrRequest(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); + return await CallAndWaitForResponse(_awaitingEnrRequestMsg, receiver, msg, token); + } + internal void OnPong(Node node, PongMsg msg) { - ValueHash256 mdc = new ValueHash256(msg.PingMdc); - if (_awaitingPingMsg.TryRemove(mdc, out TaskCompletionSource? completionSource)) + if (_awaitingPingMsg.TryRemove(node.IdHash, out TaskCompletionSource? completionSource)) { - completionSource.TrySetResult(); + completionSource.TrySetResult(msg); + } + else + { + _logger.Error($"No ping for pong {node} {msg.PingMdc.ToHexString()}"); } } @@ -156,12 +226,25 @@ public void OnNeighbour(Node node, NeighborsMsg msg) { completionSource.TrySetResult(msg.Nodes); } + else + { + _logger.Error($"No FindNeighbour for Neighbour {node} {msg}"); + } } - public async Task SendDiscV4Message(DiscoveryMsg msg) + public async Task SendDiscV4Message(Node node, DiscoveryMsg msg) { if (MsgSender is { } sender) { + if (msg is PongMsg pong) + { + _lastPong.Set(node.IdHash, DateTimeOffset.Now); + if (_awaitingPongToNode.TryGetValue(node.IdHash, out TaskCompletionSource? completionSource)) + { + completionSource.TrySetResult(); + } + } + await sender.SendMsg(msg); } } @@ -236,7 +319,7 @@ private void HandleEnrRequest(Node node, EnrRequestMsg msg) Task.Run(async () => { Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await sender.SendDiscV4Message(new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + await sender.SendDiscV4Message(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); }); } @@ -253,17 +336,18 @@ private void HandleFindNode(Node node, FindNodeMsg msg) // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. nodes = nodes.Slice(0, 12).ToArray(); } - await sender.SendDiscV4Message(new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + await sender.SendDiscV4Message(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); }); } private void HandlePing(Node node, PingMsg ping) { + if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); Task.Run(async () => { await receiver.Ping(node, _cts.Token); PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); - await sender.SendDiscV4Message(msg); + await sender.SendDiscV4Message(node, msg); }); } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 0390c4424688..74079c0ccd2b 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -258,4 +258,9 @@ private void RequireSignature() throw new Exception("Cannot encode a node record with an empty signature."); } } + + public string NodeRecordString() + { + return string.Join(",", Entries.Select((e) => $"{e.Key}:{e.Value}")); + } } From b0aac86a20370f53a0b5733073049ad2580a78f0 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 17 Apr 2025 20:31:40 +0800 Subject: [PATCH 006/182] More discovery message --- .../Collections/DictionaryExtensions.cs | 14 +++++++ .../LockableConcurrentDictionary.cs | 15 +++++++ .../Nethermind.Core/Nethermind.Core.csproj | 1 + .../NettyDiscoveryHandler.cs | 3 ++ .../Discovery}/Messages/MsgType.cs | 0 src/Nethermind/Nethermind.Network/Metrics.cs | 25 ++++++++++++ .../Nethermind.Network/PeerManager.cs | 39 +++++++------------ 7 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs rename src/Nethermind/{Nethermind.Network.Discovery => Nethermind.Network/Discovery}/Messages/MsgType.cs (100%) diff --git a/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs b/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs new file mode 100644 index 000000000000..30ef7dcf8fa0 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; + +namespace Nethermind.Core.Collections; + +public static class DictionaryExtensions +{ + public static void Increment(this IDictionary dictionary, TKey key) + { + if (!dictionary.TryAdd(key, 1)) dictionary[key]++; + } +} diff --git a/src/Nethermind/Nethermind.Core/Collections/LockableConcurrentDictionary.cs b/src/Nethermind/Nethermind.Core/Collections/LockableConcurrentDictionary.cs index 3d577028aad7..2a78c1747693 100644 --- a/src/Nethermind/Nethermind.Core/Collections/LockableConcurrentDictionary.cs +++ b/src/Nethermind/Nethermind.Core/Collections/LockableConcurrentDictionary.cs @@ -110,6 +110,21 @@ public static void Increment(this ConcurrentDictionary diction { dictionary.AddOrUpdate(key, 1, static (_, value) => value + 1); } + + public static void Increment(this NonBlocking.ConcurrentDictionary dictionary, TKey key) where TKey : notnull + { + dictionary.AddOrUpdate(key, 1, static (_, value) => value + 1); + } + + public static void AddBy(this NonBlocking.ConcurrentDictionary dictionary, TKey key, long amount) where TKey : notnull + { + dictionary.AddOrUpdate( + key, + (_, amount) => amount, + (_, startValue, amount) => startValue + amount, + amount + ); + } } diff --git a/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj b/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj index 930e96abe487..54af4e25febe 100644 --- a/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj +++ b/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index a0dfb7afedfe..4d668e74866b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -9,6 +9,7 @@ using FastEnumUtility; using Nethermind.Core; using Nethermind.Core.Buffers; +using Nethermind.Core.Collections; using Nethermind.Core.Extensions; using Nethermind.Logging; using Nethermind.Network.Discovery.Messages; @@ -107,6 +108,7 @@ public async Task SendMsg(DiscoveryMsg discoveryMsg) } Interlocked.Add(ref Metrics.DiscoveryBytesSent, size); + Metrics.DiscoveryMessagesSent.Increment(discoveryMsg.MsgType); } private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg) @@ -259,6 +261,7 @@ private static void ReportMsgByType(DiscoveryMsg msg, int size) { if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", msg.MsgType.ToString(), size); } + Metrics.DiscoveryMessagesReceived.Increment(msg.MsgType); } public event EventHandler? OnChannelActivated; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/MsgType.cs b/src/Nethermind/Nethermind.Network/Discovery/Messages/MsgType.cs similarity index 100% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/MsgType.cs rename to src/Nethermind/Nethermind.Network/Discovery/Messages/MsgType.cs diff --git a/src/Nethermind/Nethermind.Network/Metrics.cs b/src/Nethermind/Nethermind.Network/Metrics.cs index 01b41f57889f..e6ea3a23d1e9 100644 --- a/src/Nethermind/Nethermind.Network/Metrics.cs +++ b/src/Nethermind/Nethermind.Network/Metrics.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Runtime.Serialization; using Nethermind.Core.Attributes; +using Nethermind.Network.Discovery.Messages; using Nethermind.Network.P2P; using Nethermind.Stats.Model; @@ -53,6 +54,19 @@ public static class Metrics [Description("Number of bytes received through Discovery (UDP).")] public static long DiscoveryBytesReceived; + [CounterMetric] + [Description("Number of sent discovery message")] + [DetailedMetric] + [KeyIsLabel("message_type")] + public static NonBlocking.ConcurrentDictionary DiscoveryMessagesSent { get; } = new(); + + [CounterMetric] + [Description("Number of sent discovery message")] + [DetailedMetric] + [KeyIsLabel("message_type")] + public static NonBlocking.ConcurrentDictionary DiscoveryMessagesReceived { get; } = new(); + + [GaugeMetric] //EIP-2159: Common Prometheus Metrics Names for Clients [Description("The current number of peers connected.")] @@ -89,5 +103,16 @@ public static class Metrics [Description("Bytes of incoming p2p packets.")] [KeyIsLabel("protocol", "message")] public static NonBlocking.ConcurrentDictionary IncomingP2PMessageBytes { get; } = new(); + + [CounterMetric] + [Description("Number of candidate peers in peer manager")] + [DetailedMetric] + public static int PeerCandidateCount { get; set; } + + [CounterMetric] + [Description("Number of filter reason per peer candidate")] + [DetailedMetric] + [KeyIsLabel("filter")] + public static NonBlocking.ConcurrentDictionary PeerCandidateFilter { get; } = new(); } } diff --git a/src/Nethermind/Nethermind.Network/PeerManager.cs b/src/Nethermind/Nethermind.Network/PeerManager.cs index 8f88fa24c564..45becde8d336 100644 --- a/src/Nethermind/Nethermind.Network/PeerManager.cs +++ b/src/Nethermind/Nethermind.Network/PeerManager.cs @@ -201,7 +201,7 @@ private class CandidateSelection public List PreCandidates { get; } = new(); public List Candidates { get; } = new(); public List Incompatible { get; } = new(); - public Dictionary Counters { get; } = new(); + public Dictionary Counters { get; } = new(); } private readonly CandidateSelection _currentSelection = new(); @@ -252,6 +252,8 @@ private async Task RunPeerUpdateLoop() if (_logger.IsDebug) _logger.Error("DEBUG/ERROR Candidate peers cleanup failed", e); } + Metrics.PeerCandidateCount = _peerPool.PeerCount; + _peerUpdateRequested.Wait(_cancellationTokenSource.Token); _peerUpdateRequested.Reset(); @@ -387,9 +389,6 @@ private bool EnsureAvailableActivePeerSlot() return AvailableActivePeersCount - _pending > 0; } - - private static readonly IReadOnlyList _enumValues = FastEnum.GetValues(); - private void SelectAndRankCandidates() { if (AvailableActivePeersCount <= 0) @@ -400,11 +399,7 @@ private void SelectAndRankCandidates() _currentSelection.PreCandidates.Clear(); _currentSelection.Candidates.Clear(); _currentSelection.Incompatible.Clear(); - - for (int i = 0; i < _enumValues.Count; i++) - { - _currentSelection.Counters[_enumValues[i]] = 0; - } + _currentSelection.Counters.Clear(); foreach ((_, Peer peer) in _peerPool.Peers) { @@ -438,35 +433,26 @@ private void SelectAndRankCandidates() return; } - _currentSelection.Counters[ActivePeerSelectionCounter.AllNonActiveCandidates] = - _currentSelection.PreCandidates.Count; - DateTime nowUTC = DateTime.UtcNow; foreach (Peer preCandidate in _currentSelection.PreCandidates) { if (preCandidate.Node.Port == 0) { - _currentSelection.Counters[ActivePeerSelectionCounter.FilteredByZeroPort]++; + _currentSelection.Counters.Increment(ActivePeerSelectionCounter.FilteredByZeroPort.ToString()); continue; } (bool Result, NodeStatsEventType? DelayReason) delayResult = preCandidate.Stats.IsConnectionDelayed(nowUTC); if (delayResult.Result) { - if (delayResult.DelayReason == NodeStatsEventType.Disconnect) - { - _currentSelection.Counters[ActivePeerSelectionCounter.FilteredByDisconnect]++; - } - else if (delayResult.DelayReason == NodeStatsEventType.ConnectionFailed) - { - _currentSelection.Counters[ActivePeerSelectionCounter.FilteredByFailedConnection]++; - } + _currentSelection.Counters.Increment(delayResult.DelayReason.ToString()); continue; } if (preCandidate.Stats.FailedCompatibilityValidation.HasValue) { + _currentSelection.Counters.Increment(ActivePeerSelectionCounter.Incompatible.ToString()); _currentSelection.Incompatible.Add(preCandidate); continue; } @@ -495,6 +481,13 @@ private void SelectAndRankCandidates() } _currentSelection.Candidates.Sort(_peerComparer); + + foreach (var currentSelectionCounter in _currentSelection.Counters) + { + Metrics.PeerCandidateFilter.AddBy( + currentSelectionCounter.Key, + currentSelectionCounter.Value); + } } private void StartPeerUpdateLoop() @@ -585,10 +578,8 @@ private void CleanupCandidatePeers() private enum ActivePeerSelectionCounter { - AllNonActiveCandidates, FilteredByZeroPort, - FilteredByDisconnect, - FilteredByFailedConnection + Incompatible } private readonly struct PeerStats From 74c25cde81e66a9792f6c5672783dda3a1ae23d5 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 17 Apr 2025 20:33:09 +0800 Subject: [PATCH 007/182] Allow disabling static labels --- .../Nethermind.Monitoring/Config/IMetricsConfig.cs | 5 ++++- src/Nethermind/Nethermind.Monitoring/Config/MetricsConfig.cs | 1 + .../Nethermind.Monitoring/Metrics/MetricsController.cs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Monitoring/Config/IMetricsConfig.cs b/src/Nethermind/Nethermind.Monitoring/Config/IMetricsConfig.cs index 55d8c6eecae7..83ac5bcd7e50 100644 --- a/src/Nethermind/Nethermind.Monitoring/Config/IMetricsConfig.cs +++ b/src/Nethermind/Nethermind.Monitoring/Config/IMetricsConfig.cs @@ -38,6 +38,9 @@ public interface IMetricsConfig : IConfig [ConfigItem(Description = "The Prometheus metrics job name.", DefaultValue = "nethermind")] string MonitoringJob { get; } - [ConfigItem(Description = "Enable detailed metric", DefaultValue = "nethermind")] + [ConfigItem(Description = "Enable detailed metric", DefaultValue = "false")] bool EnableDetailedMetric { get; } + + [ConfigItem(Description = "Enable static label initialization", DefaultValue = "true", HiddenFromDocs = true)] + bool InitializeStaticLabels { get; set; } } diff --git a/src/Nethermind/Nethermind.Monitoring/Config/MetricsConfig.cs b/src/Nethermind/Nethermind.Monitoring/Config/MetricsConfig.cs index 6e49bfdf67df..77e194cc7f85 100644 --- a/src/Nethermind/Nethermind.Monitoring/Config/MetricsConfig.cs +++ b/src/Nethermind/Nethermind.Monitoring/Config/MetricsConfig.cs @@ -16,4 +16,5 @@ public class MetricsConfig : IMetricsConfig public string MonitoringGroup { get; set; } = "nethermind"; public string MonitoringJob { get; set; } = "nethermind"; public bool EnableDetailedMetric { get; set; } = false; + public bool InitializeStaticLabels { get; set; } = true; } diff --git a/src/Nethermind/Nethermind.Monitoring/Metrics/MetricsController.cs b/src/Nethermind/Nethermind.Monitoring/Metrics/MetricsController.cs index e53636057ba5..ee181bd9e20e 100644 --- a/src/Nethermind/Nethermind.Monitoring/Metrics/MetricsController.cs +++ b/src/Nethermind/Nethermind.Monitoring/Metrics/MetricsController.cs @@ -316,7 +316,7 @@ private static Gauge CreateGauge(string name, string? help = null, IDictionary Date: Thu, 17 Apr 2025 20:41:14 +0800 Subject: [PATCH 008/182] Allow disabling static labels --- src/Nethermind/Nethermind.Network/Metrics.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network/Metrics.cs b/src/Nethermind/Nethermind.Network/Metrics.cs index e6ea3a23d1e9..abbfa0c15fdd 100644 --- a/src/Nethermind/Nethermind.Network/Metrics.cs +++ b/src/Nethermind/Nethermind.Network/Metrics.cs @@ -66,7 +66,6 @@ public static class Metrics [KeyIsLabel("message_type")] public static NonBlocking.ConcurrentDictionary DiscoveryMessagesReceived { get; } = new(); - [GaugeMetric] //EIP-2159: Common Prometheus Metrics Names for Clients [Description("The current number of peers connected.")] From 3440fbb33491ef649acb77e73a5956086e6eee2b Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Fri, 18 Apr 2025 18:25:12 +0800 Subject: [PATCH 009/182] Address comment --- .../Nethermind.Core/Collections/DictionaryExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs b/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs index 30ef7dcf8fa0..e036bc568d54 100644 --- a/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs @@ -2,13 +2,15 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Collections.Generic; +using System.Runtime.InteropServices; namespace Nethermind.Core.Collections; public static class DictionaryExtensions { - public static void Increment(this IDictionary dictionary, TKey key) + public static void Increment(this Dictionary dictionary, TKey key) where TKey : notnull { - if (!dictionary.TryAdd(key, 1)) dictionary[key]++; + ref int res = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool _); + res++; } } From a6cd97117aa221939d89d58aad72a7b956c98954 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 22 Apr 2025 08:48:19 +0800 Subject: [PATCH 010/182] This is confusing AF --- .../Steps/InitializeNetwork.cs | 7 +- .../DiscoveryApp.cs | 64 ++++++++++++------- .../NewTrackingLookupKNearestNeighbour.cs | 28 ++++++-- .../KademliaDiscv4MessageSender.cs | 21 ++++-- src/Nethermind/Nethermind.Runner/NLog.config | 3 + 5 files changed, 86 insertions(+), 37 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index 81b64387e639..ec409050c65e 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -389,17 +389,18 @@ private async Task InitPeer() _networkConfig.DiscoveryDns = $"all.{chainName}.ethdisco.net"; } - EnrDiscovery enrDiscovery = new(enrRecordParser, _networkConfig, _api.LogManager); // initialize with a proper network + // EnrDiscovery enrDiscovery = new(enrRecordParser, _networkConfig, _api.LogManager); // initialize with a proper network if (!_networkConfig.DisableDiscV4DnsFeeder) { // Feed some nodes into discoveryApp in case all bootnodes is faulty. - _ = new NodeSourceToDiscV4Feeder(enrDiscovery, _api.DiscoveryApp, 50).Run(_api.ProcessExit!.Token); + // _ = new NodeSourceToDiscV4Feeder(enrDiscovery, _api.DiscoveryApp, 50).Run(_api.ProcessExit!.Token); } CompositeNodeSource nodeSources = _networkConfig.OnlyStaticPeers ? new(_api.StaticNodesManager, _api.TrustedNodesManager, nodesLoader) - : new(_api.StaticNodesManager, _api.TrustedNodesManager, nodesLoader, enrDiscovery, _api.DiscoveryApp); + // : new(_api.StaticNodesManager, _api.TrustedNodesManager, nodesLoader, enrDiscovery, _api.DiscoveryApp); + : new(_api.StaticNodesManager, _api.TrustedNodesManager, nodesLoader, _api.DiscoveryApp); _api.PeerPool = new PeerPool(nodeSources, _nodeStatsManager, peerStorage, _networkConfig, _api.LogManager, _api.TrustedNodesManager); _api.PeerManager = new PeerManager( _api.RlpxPeer, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 154f3ab2abae..d3d82f8f05fa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Collections.Concurrent; using System.Diagnostics; using System.Net.NetworkInformation; using System.Runtime.CompilerServices; @@ -11,6 +12,7 @@ using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Attributes; +using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Crypto; @@ -567,6 +569,16 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); Channel ch = Channel.CreateBounded(1); + ConcurrentDictionary _writtenNodes = new(); + int duplicated = 0; + int total = 0; + int fromNewNode = 0; + + void handler(object? _, Node addedNode) + { + _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); + ch.Writer.TryWrite(addedNode); + } async Task DiscoverAsync(ValueHash256 hash) { @@ -578,6 +590,8 @@ async Task DiscoverAsync(ValueHash256 hash) { anyFound = true; count++; + total++; + if (!_writtenNodes.TryAdd(node.IdHash, node.IdHash)) duplicated++; await ch.Writer.WriteAsync(node, token); } @@ -589,39 +603,22 @@ async Task DiscoverAsync(ValueHash256 hash) { if (_logger.IsDebug) _logger.Debug($"Found {count} nodes"); } + Console.Error.WriteLine($"Total is {total}, duplicated {duplicated}, fromNewNode {fromNewNode}"); } - Random random = new(); - - const int RandomNodesToLookupCount = 1; - Task discoverTask = Task.Run(async () => { + Random random = new(); ValueHash256 randomNodeId = new(); while (!token.IsCancellationRequested) { Stopwatch iterationTime = Stopwatch.StartNew(); - foreach (var bootNode in _bootNodes) - { - _ = Task.Factory.StartNew(async (obj) => - { - try - { - Node node = (Node)obj!; - await _discv4MessageSender.Ping(node, token); - _kademlia.AddOrRefresh(node); - } - catch (OperationCanceledException) - { - } - }, bootNode); - } + await EnsureBootNodes(token); try { - List discoverTasks = new List(); - // discoverTasks.Add(DiscoverAsync(_masterNode.Hash)); - + const int RandomNodesToLookupCount = 3; + using ArrayPoolList discoverTasks = new ArrayPoolList(RandomNodesToLookupCount); for (int i = 0; i < RandomNodesToLookupCount; i++) { random.NextBytes(randomNodeId.BytesAsSpan); @@ -645,6 +642,8 @@ async Task DiscoverAsync(ValueHash256 hash) try { + _kademlia.OnNodeAdded += handler; + await foreach (Node node in ch.Reader.ReadAllAsync(token)) { yield return node; @@ -653,8 +652,29 @@ async Task DiscoverAsync(ValueHash256 hash) finally { await discoverTask; + _kademlia.OnNodeAdded -= handler; } } + private async Task EnsureBootNodes(CancellationToken token) + { + Stopwatch sw = Stopwatch.StartNew(); + int onlineBootnodes = 0; + await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => + { + try + { + await _discv4MessageSender.Ping(node, token); + onlineBootnodes++; + _kademlia.AddOrRefresh(node); + } + catch (OperationCanceledException) + { + } + }); + + _logger.Info($"Ensure bootnodes took {sw.Elapsed}. {onlineBootnodes} out of {_bootNodes.Count} online"); + } + public event EventHandler? NodeRemoved { add { } remove { } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 2f4bd5818d44..61ded6c6e65a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -79,6 +79,8 @@ [EnumeratorCancellation] CancellationToken token int closestNodeRound = 0; int currentRound = 0; int queryingTask = 0; + int minResult = 128; + int totalResult = 0; bool finished = false; Task[] worker = Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => @@ -105,7 +107,7 @@ [EnumeratorCancellation] CancellationToken token try { queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); - _logger.Warn($"Query {toQuery.Value.node} at round {currentRound}, isself {SameAsSelf(toQuery.Value.node)}"); + if (_logger.IsTrace) _logger.Trace($"Query {toQuery.Value.node} at round {currentRound}, isself {SameAsSelf(toQuery.Value.node)}"); (TNode, TNode[]? neighbours) result = await WrappedFindNeighbourOp(toQuery.Value.node); if (result.neighbours == null || result.neighbours?.Length == 0) { @@ -140,6 +142,9 @@ [EnumeratorCancellation] CancellationToken token await Task.WhenAny(worker); finished = true; + _logger.Warn("Lookup operation finished."); + yield break; + async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourOp(TNode node) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -155,7 +160,6 @@ [EnumeratorCancellation] CancellationToken token } catch (OperationCanceledException) { - if (_logger.IsWarn) _logger.Warn($"Find neighbour op timout."); nodeHealthTracker.OnRequestFailed(node); return (node, null); } @@ -189,15 +193,27 @@ async Task ProcessResult(TNode thisNode, (TNode, TNode[]? neighbours)? valueTupl if (neighbours == null) return; var writer = outChan.Writer; + int queryIgnored = 0; + int seenIgnored = 0; foreach (TNode neighbour in neighbours) { ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); // Already queried, we ignore - if (queried.ContainsKey(neighbourHash)) continue; + if (queried.ContainsKey(neighbourHash)) + { + queryIgnored++; + continue; + } // When seen already dont record - if (!seen.TryAdd(neighbourHash, neighbour)) continue; + if (!seen.TryAdd(neighbourHash, neighbour)) + { + seenIgnored++; + continue; + } + + Interlocked.Increment(ref minResult); await writer.WriteAsync(neighbour, cts.Token); using var _ = queueLock.Acquire(); @@ -207,16 +223,18 @@ async Task ProcessResult(TNode thisNode, (TNode, TNode[]? neighbours)? valueTupl // If found a better node, reset closes node round and continue if (closestNodeRound < round && foundBetter) { + _logger.Warn($"Found better neighbour {neighbour} at round {round}."); bestNodeId = neighbourHash; closestNodeRound = round; } } + _logger.Warn($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); } bool ShouldStopDueToNoBetterResult(out int round) { round = Interlocked.Increment(ref currentRound); - if (round - closestNodeRound >= (config.Alpha*2)) + if (totalResult >= minResult && round - closestNodeRound >= (config.Alpha*2)) { // No closer node for more than or equal to _alpha*2 round. // Assume exit condition diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs index 238706fabaf0..cdee600f7819 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -15,6 +15,7 @@ using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; using NonBlocking; +using Prometheus; namespace Nethermind.Network.Discovery; @@ -42,11 +43,15 @@ ITimestamper timestamper private LruCache _lastPong = new(1024, ""); + private Counter EnsureSessionResult = + Prometheus.Metrics.CreateCounter("kademlia_ensure_session_result", "result", "result"); + private async Task EnsureSession(Node node, CancellationToken token) { if (_lastPong.TryGet(node.IdHash, out DateTimeOffset lastPong) && lastPong > DateTimeOffset.Now - TimeSpan.FromHours(12)) { if (_logger.IsTrace) _logger.Trace($"Node already had pong within deadline {node}. Pong duration: {DateTimeOffset.Now - lastPong}"); + EnsureSessionResult.WithLabels("pong_not_expired"); return; } @@ -63,10 +68,18 @@ private async Task EnsureSession(Node node, CancellationToken token) await Task.Delay(_waitAfterPongTimeout, token); // Give some time for peer to process pong. if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); + EnsureSessionResult.WithLabels("pong_success"); } catch (OperationCanceledException) { if (_logger.IsTrace) _logger.Trace($"Node {node} timeout trying to trigger pong."); + _logger.Warn($"Node {node} timeout trying to trigger pong."); + EnsureSessionResult.WithLabels("pong_timeout"); + } + catch (Exception) + { + EnsureSessionResult.WithLabels("error"); + throw; } finally { @@ -77,12 +90,11 @@ private async Task EnsureSession(Node node, CancellationToken token) private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) { - Stopwatch sw = Stopwatch.StartNew(); using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; { - using var firstTryCts = cts.Token.CreateChildTokenSource(_unauthenticatedRequestTimeout); + using var firstTryCts = token.CreateChildTokenSource(_unauthenticatedRequestTimeout); try { return await callRequest(firstTryCts.Token); @@ -102,11 +114,6 @@ private async Task RunAuthenticatedRequest(Node node, Func + + + From 18dcfd51cd199673ac652db352a21417118602d9 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 28 Apr 2025 13:33:37 +0800 Subject: [PATCH 011/182] Ok, I see where the problem is --- .../DiscoveryApp.cs | 46 ++++--- .../KademliaDiscv4MessageSender.cs | 117 +++++++++++++----- 2 files changed, 115 insertions(+), 48 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index d3d82f8f05fa..fe0bb3074e3e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -9,6 +9,7 @@ using Autofac; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; +using Microsoft.ClearScript; using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Attributes; @@ -22,6 +23,7 @@ using Nethermind.Network.Discovery.RoutingTable; using Nethermind.Network.Enr; using Nethermind.Stats.Model; +using Prometheus; using LogLevel = DotNetty.Handlers.Logging.LogLevel; namespace Nethermind.Network.Discovery; @@ -82,7 +84,8 @@ public DiscoveryApp( _bootNodes = new List(); _discoveryStorage.StartBatch(); - NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); + // NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); + NetworkNode[] bootnodes = NetworkNode.ParseNodes("enode://8cd847302089d4906c5eb3125770b067fbcb7dc6bd62dfd3517483cc2e6acae6141a5fb4061f76825ea9f585d157b625f84f976fb6aa1582dc87b0d0b652f51f@127.0.0.1:40404", _logger); if (bootnodes.Length == 0) { if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); @@ -99,6 +102,7 @@ public DiscoveryApp( _bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.Port)); } + // enr:-Iq4QH5BqYiMrk3JM9PjbWQywalXCmIJEls45OVyd-DOZ662I-h9Te3B3l_DUYb69qODpUJXqROXZ-bsl0KTEsXl4JyGAZZ6r115gmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQNC_Da2PwnTdLvnKpys14XtqIGoZqdngnIMh6cPhVwK3oN1ZHCCo9c } private class NodeNodeHashProvider : INodeHashProvider @@ -565,10 +569,13 @@ private void OnNodeDiscovered(object? sender, NodeEventArgs e) private event EventHandler? NodeAdded; */ + private Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_size", "kad size"); + private Counter _kademliaDiscoveredNodes = Prometheus.Metrics.CreateCounter("kademlia_discovered_nodes", "Discovered"); + public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); - Channel ch = Channel.CreateBounded(1); + Channel ch = Channel.CreateBounded(64); ConcurrentDictionary _writtenNodes = new(); int duplicated = 0; int total = 0; @@ -576,8 +583,8 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance void handler(object? _, Node addedNode) { - _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); - ch.Writer.TryWrite(addedNode); + // _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); + // ch.Writer.TryWrite(addedNode); } async Task DiscoverAsync(ValueHash256 hash) @@ -591,7 +598,12 @@ async Task DiscoverAsync(ValueHash256 hash) anyFound = true; count++; total++; - if (!_writtenNodes.TryAdd(node.IdHash, node.IdHash)) duplicated++; + if (!_writtenNodes.TryAdd(node.IdHash, node.IdHash)) + { + duplicated++; + continue; + } + _kademliaDiscoveredNodes.Inc(); await ch.Writer.WriteAsync(node, token); } @@ -606,26 +618,24 @@ async Task DiscoverAsync(ValueHash256 hash) Console.Error.WriteLine($"Total is {total}, duplicated {duplicated}, fromNewNode {fromNewNode}"); } - Task discoverTask = Task.Run(async () => + Task discoverTask = Task.WhenAll(Enumerable.Range(0, 6).Select((_) => Task.Run(async () => { Random random = new(); ValueHash256 randomNodeId = new(); + int iterationCount = 0; while (!token.IsCancellationRequested) { Stopwatch iterationTime = Stopwatch.StartNew(); - await EnsureBootNodes(token); + if (iterationCount % 10 == 0) + { + // Probably shnould be done once or in a few interval + await EnsureBootNodes(token); + } try { - const int RandomNodesToLookupCount = 3; - using ArrayPoolList discoverTasks = new ArrayPoolList(RandomNodesToLookupCount); - for (int i = 0; i < RandomNodesToLookupCount; i++) - { - random.NextBytes(randomNodeId.BytesAsSpan); - discoverTasks.Add(DiscoverAsync(randomNodeId)); - } - - await Task.WhenAll(discoverTasks); + random.NextBytes(randomNodeId.BytesAsSpan); + await DiscoverAsync(randomNodeId); } catch (Exception ex) { @@ -635,10 +645,10 @@ async Task DiscoverAsync(ValueHash256 hash) // Prevent high CPU when all node is not reachable due to network connectivity issue. if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) { - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(1), token); } } - }); + }))); try { diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs index cdee600f7819..0a2895b60ef2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -29,10 +29,10 @@ ITimestamper timestamper private ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); - private TimeSpan _unauthenticatedRequestTimeout = TimeSpan.FromSeconds(2.5); - private TimeSpan _requestTimeout = TimeSpan.FromSeconds(8); + private TimeSpan _unauthenticatedRequestTimeout = TimeSpan.FromSeconds(4); + private TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); private TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(1); - private TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(100); + private TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); private ConcurrentDictionary> _awaitingPingMsg = new(); private ConcurrentDictionary _awaitingPongToNode = new(); @@ -41,16 +41,26 @@ ITimestamper timestamper private ConcurrentDictionary> _awaitingFindNeighbourMsg = new(); private ConcurrentDictionary> _awaitingEnrRequestMsg = new(); - private LruCache _lastPong = new(1024, ""); + private LruCache _lastPong = new(1024 * 10, ""); + private LruCache _knownFailedFindNeighbour = new(1024 * 10, ""); + + private int findNodeFailureLimit = 5; + private ConcurrentDictionary _findNodesFailure = new(); private Counter EnsureSessionResult = Prometheus.Metrics.CreateCounter("kademlia_ensure_session_result", "result", "result"); + private bool IsShouldBeAuthenticated(Node node) + { + return _lastPong.TryGet(node.IdHash, out DateTimeOffset lastPong) + && lastPong > DateTimeOffset.Now - TimeSpan.FromHours(12) + && (!_findNodesFailure.TryGetValue(node.IdHash, out int failedFinedNodes) || failedFinedNodes <= findNodeFailureLimit); + } + private async Task EnsureSession(Node node, CancellationToken token) { - if (_lastPong.TryGet(node.IdHash, out DateTimeOffset lastPong) && lastPong > DateTimeOffset.Now - TimeSpan.FromHours(12)) + if (IsShouldBeAuthenticated(node)) { - if (_logger.IsTrace) _logger.Trace($"Node already had pong within deadline {node}. Pong duration: {DateTimeOffset.Now - lastPong}"); EnsureSessionResult.WithLabels("pong_not_expired"); return; } @@ -73,7 +83,6 @@ private async Task EnsureSession(Node node, CancellationToken token) catch (OperationCanceledException) { if (_logger.IsTrace) _logger.Trace($"Node {node} timeout trying to trigger pong."); - _logger.Warn($"Node {node} timeout trying to trigger pong."); EnsureSessionResult.WithLabels("pong_timeout"); } catch (Exception) @@ -88,33 +97,33 @@ private async Task EnsureSession(Node node, CancellationToken token) } } + Counter AuthRequestCounter = Prometheus.Metrics.CreateCounter("kademlia_auth_request", "request", "status"); + private ConcurrentDictionary _previouslyOk = new(); + + private ConcurrentDictionary _maybePingOnRequest = new(); private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) { + AuthRequestCounter.WithLabels("auth_request").Inc(); + TaskCompletionSource pingCts = new(TaskCreationOptions.RunContinuationsAsynchronously); + _maybePingOnRequest.TryAdd(node.IdHash, pingCts); using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - { - using var firstTryCts = token.CreateChildTokenSource(_unauthenticatedRequestTimeout); - try - { - return await callRequest(firstTryCts.Token); - } - catch (OperationCanceledException) - { - // If we don't get a response in time, it could just be that we are not authenticated there - } - } - await EnsureSession(node, token); - - // Then we just try a final time. try { - return await callRequest(token); + var resp = await callRequest(token); + AuthRequestCounter.WithLabels("ok_attempt").Inc(); + return resp; } catch (OperationCanceledException) { - _lastPong.Delete(node.IdHash); + AuthRequestCounter.WithLabels("failed_attempt").Inc(); + if (IsShouldBeAuthenticated(node)) + { + AuthRequestCounter.WithLabels("failed_attempt_should_be_authenticated").Inc(); + } + // _lastPong.Delete(node.IdHash); throw; } } @@ -141,7 +150,7 @@ private async Task CallAndWaitForResponse( CancellationToken token ) { TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); + await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); ValueHash256 requestHash = receiver.IdHash; while (!requestDictionary.TryAdd(requestHash, completionSource)) { @@ -165,22 +174,60 @@ CancellationToken token } finally { - unregister.Unregister(); requestDictionary.TryRemove(requestHash, out _); } } + private Counter FindNeighbourStatus = + Prometheus.Metrics.CreateCounter("find_neighbour_status", "find neighbour", "status"); + public async Task FindNeighbours(Node receiver, ValueHash256 hash, CancellationToken token) { using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - return await RunAuthenticatedRequest(receiver, async token => + if (_knownFailedFindNeighbour.TryGet(receiver.IdHash, out DateTimeOffset lastFailure) && + lastFailure > DateTimeOffset.Now - TimeSpan.FromMinutes(5)) { - FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), hash.ToByteArray()); + FindNeighbourStatus.WithLabels("Known fail").Inc(); + return []; + } - return await CallAndWaitForResponse(_awaitingFindNeighbourMsg, receiver, msg, token); - }, token); + try + { + var result = await RunAuthenticatedRequest(receiver, async token => + { + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), hash.ToByteArray()); + + return await CallAndWaitForResponse(_awaitingFindNeighbourMsg, receiver, msg, token); + }, token); + + _findNodesFailure[receiver.IdHash] = 0; + FindNeighbourStatus.WithLabels("ok").Inc(); + return result; + } + catch (OperationCanceledException) + { + if (_findNodesFailure.TryGetValue(receiver.IdHash, out int failureCount)) + { + _findNodesFailure[receiver.IdHash] = failureCount + 1; + } + else + { + _findNodesFailure[receiver.IdHash] = 1; + } + + _knownFailedFindNeighbour.Set(receiver.IdHash, DateTimeOffset.Now); + if (IsShouldBeAuthenticated(receiver)) + { + FindNeighbourStatus.WithLabels("timeout_should_be_authenticated").Inc(); + } + else + { + FindNeighbourStatus.WithLabels("timeout").Inc(); + } + throw; + } } public async Task SendEnrRequest(Node receiver, CancellationToken token) @@ -213,7 +260,16 @@ internal void OnPong(Node node, PongMsg msg) } else { - _logger.Error($"No ping for pong {node} {msg.PingMdc.ToHexString()}"); + // Pong timeout? + // _logger.Error($"No ping for pong {node} {msg.PingMdc.ToHexString()}"); + } + } + + public void OnPing(Node node, PingMsg ping) + { + if (_maybePingOnRequest.TryRemove(node.IdHash, out var cts)) + { + cts.TrySetResult(); } } @@ -349,6 +405,7 @@ private void HandleFindNode(Node node, FindNodeMsg msg) private void HandlePing(Node node, PingMsg ping) { + sender.OnPing(node, ping); if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); Task.Run(async () => { From 56cbe78e5ca45b44785b13c78d6fd96709e2c72c Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 28 Apr 2025 14:43:43 +0800 Subject: [PATCH 012/182] Fix the neighbour issue --- .../Kademlia/KademliaSimulation.cs | 63 ++++++++++++------- .../Kademlia/KademliaTests.cs | 37 +++++++---- .../DiscoveryApp.cs | 54 ++++++++++------ .../Kademlia/BucketListRoutingTable.cs | 15 ++--- .../Kademlia/Content/KademliaContent.cs | 7 +-- .../Content/KademliaContentMessageReceiver.cs | 10 +-- .../Kademlia/IKademlia.cs | 8 +-- .../Kademlia/IKademliaMessageSender.cs | 6 +- .../Kademlia/ILookupAlgo.cs | 4 +- .../Kademlia/ILookupAlgo2.cs | 4 +- .../Kademlia/INodeHashProvider.cs | 5 +- .../Kademlia/IRoutingTable.cs | 4 +- .../Kademlia/IServiceCollectionExtensions.cs | 26 ++++---- .../Kademlia/KBucketTree.cs | 21 ++++--- .../Kademlia/Kademlia.cs | 47 +++++++------- .../Kademlia/KademliaMessageReceiver.cs | 9 ++- .../Kademlia/KademliaModule.cs | 28 ++++----- .../Kademlia/NewLookupKNearestNeighbour.cs | 15 ++--- .../NewTrackingLookupKNearestNeighbour.cs | 28 ++++----- .../Kademlia/NodeHealthTracker.cs | 16 +++-- .../OriginalLookupKNearestNeighbour.cs | 15 ++--- .../KademliaDiscv4MessageSender.cs | 12 ++-- 22 files changed, 252 insertions(+), 182 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index b4282b33f7d2..2cf5b39006ee 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -58,9 +58,9 @@ public async Task TestBootstrap() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Kademlia node1 = fabric.CreateNode(node1Hash); - Kademlia node2 = fabric.CreateNode(node2Hash); - Kademlia node3 = fabric.CreateNode(node3Hash); + Kademlia node1 = fabric.CreateNode(node1Hash); + Kademlia node2 = fabric.CreateNode(node2Hash); + Kademlia node3 = fabric.CreateNode(node3Hash); node1.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray().Should().BeEquivalentTo([node1Hash]); @@ -94,8 +94,8 @@ public async Task TestLookup() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Kademlia node1 = fabric.CreateNode(node1Hash); - Kademlia node2 = fabric.CreateNode(node2Hash); + Kademlia node1 = fabric.CreateNode(node1Hash); + Kademlia node2 = fabric.CreateNode(node2Hash); KademliaContent node1Content = fabric.GetKademliaContent(node1Hash); KademliaContent node2Content = fabric.GetKademliaContent(node2Hash); fabric.CreateNode(node3Hash); @@ -122,7 +122,7 @@ public async Task TestKNearestNeighbour() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Kademlia node1 = fabric.CreateNode(node1Hash); + Kademlia node1 = fabric.CreateNode(node1Hash); (await node1.LookupNodesClosest(node1Hash, cts.Token)) .Select(n => n.Hash) @@ -130,7 +130,7 @@ public async Task TestKNearestNeighbour() .Should() .BeEquivalentTo(new HashSet() {node1Hash }); - Kademlia node2 = fabric.CreateNode(node2Hash); + Kademlia node2 = fabric.CreateNode(node2Hash); fabric.CreateNode(node3Hash); node1.AddOrRefresh(new TestNode(node2Hash)); @@ -158,14 +158,14 @@ public async Task SimulateLargeLookupValue() TestFabric fabric = CreateFabric(); Random rand = new Random(0); ValueHash256 mainNodeHash = RandomKeccak(rand); - Kademlia mainNode = fabric.CreateNode(mainNodeHash); + Kademlia mainNode = fabric.CreateNode(mainNodeHash); KademliaContent mainNodeContent = fabric.GetKademliaContent(mainNodeHash); List nodeIds = new(); for (int i = 0; i < nodeCount; i++) { ValueHash256 nodeHash = RandomKeccak(rand); - Kademlia kad = fabric.CreateNode(nodeHash); + Kademlia kad = fabric.CreateNode(nodeHash); kad.AddOrRefresh(new TestNode(mainNodeHash)); nodeIds.Add(nodeHash); } @@ -202,13 +202,13 @@ public async Task SimulateLargeKNearestNeighbour() TestFabric fabric = CreateFabric(); Random rand = new Random(0); ValueHash256 mainNodeHash = RandomKeccak(rand); - Kademlia mainNode = fabric.CreateNode(mainNodeHash); + Kademlia mainNode = fabric.CreateNode(mainNodeHash); List nodeIds = new(); for (int i = 0; i < nodeCount; i++) { ValueHash256 nodeHash = RandomKeccak(rand); - Kademlia kad = fabric.CreateNode(nodeHash); + Kademlia kad = fabric.CreateNode(nodeHash); kad.AddOrRefresh(new TestNode(mainNodeHash)); nodeIds.Add(nodeHash); } @@ -284,13 +284,28 @@ public bool TryGetValue(ValueHash256 hash, out ValueHash256 value) } } - private class ValueHashNodeHashProvider: INodeHashProvider, IContentHashProvider + private class ValueHashNodeHashProvider: INodeHashProvider, IContentHashProvider { public ValueHash256 GetHash(TestNode node) { return node.Hash; } + public ValueHash256 GetKey(TestNode node) + { + return node.Hash; + } + + public ValueHash256 GetKeyHash(ValueHash256 key) + { + return key; + } + + public ValueHash256 CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) + { + return Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth); + } + public ValueHash256 GetHash(ValueHash256 key) { return key; @@ -311,12 +326,12 @@ private class TestFabric(KademliaConfig config) readonly ValueHashNodeHashProvider _nodeHashProvider = new ValueHashNodeHashProvider(); private readonly Random _random = new Random(0); - private bool TryGetReceiver(TestNode receiverHash, out IKademliaMessageReceiver contentKademliaMessageReceiver) + private bool TryGetReceiver(TestNode receiverHash, out IKademliaMessageReceiver contentKademliaMessageReceiver) { contentKademliaMessageReceiver = null!; if (_nodes.TryGetValue(receiverHash.Hash, out var serviceProvider)) { - contentKademliaMessageReceiver = serviceProvider!.GetRequiredService>(); + contentKademliaMessageReceiver = serviceProvider!.GetRequiredService>(); return true; } @@ -340,15 +355,15 @@ public KademliaContent GetKademliaContent( return _nodes[nodeHash].GetRequiredService>(); } - public Kademlia CreateNode(ValueHash256 nodeID) + public Kademlia CreateNode(ValueHash256 nodeID) { var nodeIDTestNode = new TestNode(nodeID); var serviceProvider = new ServiceCollection() - .ConfigureKademliaComponents() + .ConfigureKademliaComponents() .ConfigureKademliaContentComponents() .AddSingleton(new TestLogManager(LogLevel.Error)) - .AddSingleton>(_nodeHashProvider) + .AddSingleton>(_nodeHashProvider) .AddSingleton>(_nodeHashProvider) .AddSingleton(new KademliaConfig() { @@ -362,17 +377,17 @@ public Kademlia CreateNode(ValueHash256 nodeID) }) .AddSingleton>(new OnlySelfIKademliaContentStore(nodeID)) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) - .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) - .AddSingleton>() + .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) + .AddSingleton>() .AddSingleton>() .BuildServiceProvider(); _nodes[nodeID] = serviceProvider; - return serviceProvider.GetRequiredService>(); + return serviceProvider.GetRequiredService>(); } - private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender, IContentMessageSender + private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender, IContentMessageSender { public async Task Ping(TestNode node, CancellationToken token) { @@ -380,7 +395,7 @@ public async Task Ping(TestNode node, CancellationToken token) await fabric.DoSimulateLatency(token); fabric.Debug($"ping from {sender} to {node}"); - if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) + if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) { await receiver.Ping(sender, token); return; @@ -395,7 +410,7 @@ public async Task FindNeighbours(TestNode node, ValueHash256 hash, C await fabric.DoSimulateLatency(token); fabric.Debug($"findn from {sender} to {node}"); - if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) + if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) { return (await receiver.FindNeighbours(sender, hash, token)).Select((node) => new TestNode(node.Hash)).ToArray(); } @@ -440,7 +455,7 @@ public async Task Bootstrap(CancellationToken token) { foreach (KeyValuePair kv in _nodes) { - await kv.Value.GetRequiredService>().Bootstrap(token); + await kv.Value.GetRequiredService>().Bootstrap(token); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 8bfea5653f3a..17de5fe9dffe 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -19,7 +19,7 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; [TestFixture(false)] public class KademliaTests { - private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); + private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); private readonly bool _useTreeBasedBucket; public KademliaTests(bool useTreeBasedBucket) @@ -27,25 +27,25 @@ public KademliaTests(bool useTreeBasedBucket) _useTreeBasedBucket = useTreeBasedBucket; } - private Kademlia CreateKad(KademliaConfig config) + private Kademlia CreateKad(KademliaConfig config) { config.UseTreeBasedRoutingTable = _useTreeBasedBucket; return new ServiceCollection() - .ConfigureKademliaComponents() + .ConfigureKademliaComponents() .AddSingleton(new TestLogManager(LogLevel.Trace)) - .AddSingleton>(new ValueHashNodeHashProvider()) + .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) - .AddSingleton>() + .AddSingleton>() .BuildServiceProvider() - .GetRequiredService>(); + .GetRequiredService>(); } [Test] public void TestNewNodeAdded() { - Kademlia kad = CreateKad(new KademliaConfig() + Kademlia kad = CreateKad(new KademliaConfig() { KSize = 5, Beta = 0, @@ -70,7 +70,7 @@ public async Task TestTooManyNode() .Ping(Arg.Any(), Arg.Any()) .Returns(pingSource.Task); - Kademlia kad = CreateKad(new KademliaConfig() + Kademlia kad = CreateKad(new KademliaConfig() { KSize = 5, Beta = 0, @@ -100,7 +100,7 @@ public void TestGetKNeighbours() .Ping(Arg.Any(), Arg.Any()) .Returns(pingSource.Task); - Kademlia kad = CreateKad(new KademliaConfig() + Kademlia kad = CreateKad(new KademliaConfig() { CurrentNodeId = ValueKeccak.Compute("something"), KSize = 5, @@ -142,7 +142,7 @@ public async Task TestTooManyNodeWithAcceleratedLookup() .Ping(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); - Kademlia kad = CreateKad(new KademliaConfig() + Kademlia kad = CreateKad(new KademliaConfig() { KSize = 5, Beta = 1, @@ -175,11 +175,26 @@ public async Task TestTooManyNodeWithAcceleratedLookup() kad.GetAllAtDistance(250).ToHashSet().Should().BeEquivalentTo(testHashes[10..].ToHashSet()); } - private class ValueHashNodeHashProvider: INodeHashProvider + private class ValueHashNodeHashProvider: INodeHashProvider { public ValueHash256 GetHash(ValueHash256 node) { return node; } + + public ValueHash256 GetKey(ValueHash256 node) + { + return node; + } + + public ValueHash256 GetKeyHash(ValueHash256 key) + { + return key; + } + + public ValueHash256 CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) + { + return Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth); + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index fe0bb3074e3e..4ccc8cd1091a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -49,8 +49,8 @@ public class DiscoveryApp : IDiscoveryApp private KademliaDiscv4MessageReceiver _discv4MessageReceiver = null!; private KademliaDiscv4MessageSender _discv4MessageSender = null!; - private IKademlia _kademlia = null!; - private ILookupAlgo2 _lookup2 = null!; + private IKademlia _kademlia = null!; + private ILookupAlgo2 _lookup2 = null!; private NettyDiscoveryHandler? _discoveryHandler; private Task? _storageCommitTask; @@ -84,8 +84,8 @@ public DiscoveryApp( _bootNodes = new List(); _discoveryStorage.StartBatch(); - // NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); - NetworkNode[] bootnodes = NetworkNode.ParseNodes("enode://8cd847302089d4906c5eb3125770b067fbcb7dc6bd62dfd3517483cc2e6acae6141a5fb4061f76825ea9f585d157b625f84f976fb6aa1582dc87b0d0b652f51f@127.0.0.1:40404", _logger); + NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); + // NetworkNode[] bootnodes = NetworkNode.ParseNodes("enode://8cd847302089d4906c5eb3125770b067fbcb7dc6bd62dfd3517483cc2e6acae6141a5fb4061f76825ea9f585d157b625f84f976fb6aa1582dc87b0d0b652f51f@127.0.0.1:40404", _logger); if (bootnodes.Length == 0) { if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); @@ -102,15 +102,33 @@ public DiscoveryApp( _bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.Port)); } - // enr:-Iq4QH5BqYiMrk3JM9PjbWQywalXCmIJEls45OVyd-DOZ662I-h9Te3B3l_DUYb69qODpUJXqROXZ-bsl0KTEsXl4JyGAZZ6r115gmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQNC_Da2PwnTdLvnKpys14XtqIGoZqdngnIMh6cPhVwK3oN1ZHCCo9c } - private class NodeNodeHashProvider : INodeHashProvider + private class NodeNodeHashProvider : INodeHashProvider { public ValueHash256 GetHash(Node node) { return node.Id.Hash; } + + public PublicKey GetKey(Node node) + { + return node.Id; + } + + public ValueHash256 GetKeyHash(PublicKey key) + { + return key.Hash; + } + + public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) + { + // Obviously, we can't generate this. So we just randomly pick something. + // I guess we can brute force it if needed. + Span randomBytes = new byte[64]; + Random.Shared.NextBytes(randomBytes); + return new PublicKey(randomBytes); + } } public void Initialize(PublicKey masterPublicKey) @@ -130,8 +148,8 @@ public void Initialize(PublicKey masterPublicKey) */ _kademliaServices = new ContainerBuilder() - .AddModule(new KademliaModule()) - .AddSingleton, NodeNodeHashProvider>() + .AddModule(new KademliaModule()) + .AddSingleton, NodeNodeHashProvider>() .AddSingleton(_timestamper) .AddSingleton(_networkConfig) .AddSingleton(_logManager) @@ -140,12 +158,12 @@ public void Initialize(PublicKey masterPublicKey) { CurrentNodeId = new Node(_masterNode, "127.0.0.1", 9999, true) }) - .AddSingleton, KademliaDiscv4MessageSender>() + .AddSingleton, KademliaDiscv4MessageSender>() .AddSingleton() .Build(); - _kademlia = _kademliaServices.Resolve>(); - _lookup2 = _kademliaServices.Resolve>(); + _kademlia = _kademliaServices.Resolve>(); + _lookup2 = _kademliaServices.Resolve>(); _discv4MessageReceiver = _kademliaServices.Resolve(); _discv4MessageSender = _kademliaServices.Resolve(); @@ -587,13 +605,13 @@ void handler(object? _, Node addedNode) // ch.Writer.TryWrite(addedNode); } - async Task DiscoverAsync(ValueHash256 hash) + async Task DiscoverAsync(PublicKey target) { - if (_logger.IsDebug) _logger.Debug($"Looking up {hash}"); + if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); bool anyFound = false; int count = 0; - await foreach (var node in _lookup2.Lookup(hash, token)) + await foreach (var node in _lookup2.Lookup(target, token)) { anyFound = true; count++; @@ -609,7 +627,7 @@ async Task DiscoverAsync(ValueHash256 hash) if (!anyFound) { - if (_logger.IsDebug) _logger.Debug($"No node found for {hash}"); + if (_logger.IsDebug) _logger.Debug($"No node found for {target}"); } else { @@ -621,7 +639,7 @@ async Task DiscoverAsync(ValueHash256 hash) Task discoverTask = Task.WhenAll(Enumerable.Range(0, 6).Select((_) => Task.Run(async () => { Random random = new(); - ValueHash256 randomNodeId = new(); + byte[] randomBytes = new byte[64]; int iterationCount = 0; while (!token.IsCancellationRequested) { @@ -634,8 +652,8 @@ async Task DiscoverAsync(ValueHash256 hash) try { - random.NextBytes(randomNodeId.BytesAsSpan); - await DiscoverAsync(randomNodeId); + random.NextBytes(randomBytes); + await DiscoverAsync(new PublicKey(randomBytes)); } catch (Exception ex) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs index b4420accc731..fcaeb9cb1a23 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs @@ -3,24 +3,24 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Threading; -using Nethermind.Evm.Tracing.GethStyle.Custom.JavaScript; using Nethermind.Logging; namespace Nethermind.Network.Discovery.Kademlia; -public class BucketListRoutingTable: IRoutingTable where TNode : notnull +public class BucketListRoutingTable: IRoutingTable where TNode : notnull { private readonly ILogger _logger; private readonly KBucket[] _buckets; private readonly ValueHash256 _currentNodeIdAsHash; + private readonly INodeHashProvider _nodeHashProvider; private readonly int _kSize; // TODO: Double check and probably make lockless private readonly McsLock _lock = new McsLock(); - public BucketListRoutingTable(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + public BucketListRoutingTable(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) { - _logger = logManager.GetClassLogger>(); + _logger = logManager.GetClassLogger>(); // Note: It does not have to be this much. In practice, only like 16 of these bucket get populated. _buckets = new KBucket[Hash256XorUtils.MaxDistance + 1]; @@ -29,6 +29,7 @@ public BucketListRoutingTable(KademliaConfig config, INodeHashProvider(config.KSize); } + _nodeHashProvider = nodeHashProvider; _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); _kSize = config.KSize; } @@ -62,14 +63,14 @@ public TNode[] GetAllAtDistance(int i) return _buckets[i].GetAll(); } - public IEnumerable IterateBucketRandomHashes() + public IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() { for (var i = 0; i < _buckets.Length; i++) { if (_buckets[i].Count > 0) { - ValueHash256 nodeToLookup = Hash256XorUtils.GetRandomHashAtDistance(_currentNodeIdAsHash, i); - yield return nodeToLookup; + // TODO: Prefix incorrect + yield return (_currentNodeIdAsHash, i, _buckets[i]); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs index 41317b8625ff..ae841933a144 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs @@ -7,10 +7,9 @@ namespace Nethermind.Network.Discovery.Kademlia.Content; public class KademliaContent( - IContentHashProvider contentHashProvider, IKademliaContentStore kademliaContentStore, IContentMessageSender contentMessageSender, - ILookupAlgo lookupAlgo, + ILookupAlgo lookupAlgo, KademliaConfig config, ILogManager logManager ): IKademliaContent where TNode : notnull @@ -31,12 +30,10 @@ ILogManager logManager return content; } - ValueHash256 targetHash = contentHashProvider.GetHash(contentKey); - try { await lookupAlgo.Lookup( - targetHash, config.KSize, async (nextNode, token) => + contentKey, config.KSize, async (nextNode, token) => { FindValueResponse valueResponse = await contentMessageSender.FindValue(nextNode, contentKey, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs index 01cbdb79c94c..33ed03116f24 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs @@ -4,10 +4,10 @@ namespace Nethermind.Network.Discovery.Kademlia.Content; public class KademliaContentMessageReceiver( - IKademlia kademlia, - NodeHealthTracker nodeHealthTracker, + IRoutingTable kademlia, + INodeHealthTracker nodeHealthTracker, IContentHashProvider contentHashProvider, - IKademliaContentStore kademliaKademliaContentStore) : IContentMessageReceiver + IKademliaContentStore kademliaKademliaContentStore) : IContentMessageReceiver where TNode : notnull { public Task> FindValue(TNode sender, TContentKey contentKey, CancellationToken token) { @@ -18,11 +18,13 @@ public Task> FindValue(TNode sender, TContent return Task.FromResult(new FindValueResponse(true, value!, Array.Empty())); } + // TODO: Exclude sender. + return Task.FromResult( new FindValueResponse( false, default, - kademlia.GetKNeighbour(contentHashProvider.GetHash(contentKey), sender, true) + kademlia.GetKNearestNeighbour(contentHashProvider.GetHash(contentKey), null, true) )); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index 81b47dba7694..fc0fce003b61 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Main kademlia interface. High level code is expected to interface with this interface. /// /// -public interface IKademlia +public interface IKademlia { /// @@ -30,7 +30,7 @@ public interface IKademlia /// /// /// - Task LookupNodesClosest(ValueHash256 targetHash, CancellationToken token, int? k = null); + Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null); /// /// Start timers, refresh and such for maintenance of the table. @@ -49,10 +49,10 @@ public interface IKademlia /// Return the K nearest table entry from hash. This does not traverse the network. The returned array is not /// sorted. The routing table may return the exact same array for optimization purpose. /// - /// + /// /// /// - TNode[] GetKNeighbour(ValueHash256 hash, TNode? excluding = default, bool excludeSelf = false); + TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false); /// /// Called when a TNode is added to the routing table. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs index 3e796b21b0e2..e27ca6400ceb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs @@ -9,17 +9,17 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Should be exposed by application to kademlia so that kademlia can send out message. /// /// -public interface IKademliaMessageSender +public interface IKademliaMessageSender { Task Ping(TNode receiver, CancellationToken token); - Task FindNeighbours(TNode receiver, ValueHash256 hash, CancellationToken token); + Task FindNeighbours(TNode receiver, TKey target, CancellationToken token); } /// /// Application should call this class on incoming messages. /// /// -public interface IKademliaMessageReceiver: IKademliaMessageSender +public interface IKademliaMessageReceiver: IKademliaMessageSender { } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs index 7315c29ee3c7..e5d7dcbcf272 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Find closest-k is also used to determine which node should store a particular value which is used by /// store RPC (not implemented). /// -public interface ILookupAlgo +public interface ILookupAlgo { /// /// The find neighbour operation here is configurable because the same algorithm is also used for finding @@ -23,7 +23,7 @@ public interface ILookupAlgo /// /// Task Lookup( - ValueHash256 targetHash, + TKey target, int k, Func> findNeighbourOp, CancellationToken token diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs index 6096481dfd75..85f1dd033f01 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Find closest-k is also used to determine which node should store a particular value which is used by /// store RPC (not implemented). /// -public interface ILookupAlgo2 +public interface ILookupAlgo2 { /// /// The find neighbour operation here is configurable because the same algorithm is also used for finding @@ -23,7 +23,7 @@ public interface ILookupAlgo2 /// /// IAsyncEnumerable Lookup( - ValueHash256 targetHash, + TKey target, CancellationToken token ); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs index 6e0fc60d2f0a..3dbca0099fb1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs @@ -15,7 +15,10 @@ namespace Nethermind.Network.Discovery.Kademlia; /// could be specialized. /// /// -public interface INodeHashProvider +public interface INodeHashProvider { ValueHash256 GetHash(TNode node); + TKey GetKey(TNode node); + ValueHash256 GetKeyHash(TKey key); + TKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs index 530afd64d85e..621025d536b6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs @@ -5,13 +5,13 @@ namespace Nethermind.Network.Discovery.Kademlia; -public interface IRoutingTable +public interface IRoutingTable where TNode : notnull { BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh); bool Remove(in ValueHash256 hash); TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude = null, bool excludeSelf = false); TNode[] GetAllAtDistance(int i); - IEnumerable IterateBucketRandomHashes(); + IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets(); TNode? GetByHash(ValueHash256 nodeId); void LogDebugInfo(); event EventHandler? OnNodeAdded; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs index dfd403fbb5ec..fdefd224428a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs @@ -25,35 +25,35 @@ public static class IServiceCollectionExtensions /// /// The type of node /// - public static IServiceCollection ConfigureKademliaComponents(this IServiceCollection collection) where TNode : notnull + public static IServiceCollection ConfigureKademliaComponents(this IServiceCollection collection) where TNode : notnull { return collection - .AddSingleton, Kademlia>() - .AddSingleton, KademliaKademliaMessageReceiver>() - .AddSingleton>() - .AddSingleton>() - .AddSingleton>(provider => + .AddSingleton, Kademlia>() + .AddSingleton, KademliaKademliaMessageReceiver>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>(provider => { KademliaConfig config = provider.GetRequiredService>(); if (config.UseNewLookup) { - return provider.GetRequiredService>(); + return provider.GetRequiredService>(); } - return provider.GetRequiredService>(); + return provider.GetRequiredService>(); }) - .AddSingleton>() - .AddSingleton>() - .AddSingleton>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton, NodeHealthTracker>() .AddSingleton>(provider => { KademliaConfig config = provider.GetRequiredService>(); if (config.UseTreeBasedRoutingTable) { - return provider.GetRequiredService>(); + return provider.GetRequiredService>(); } - return provider.GetRequiredService>(); + return provider.GetRequiredService>(); }); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 9adeeae13180..1aae7afe5845 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -8,8 +8,10 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class KBucketTree: IRoutingTable where TNode : notnull +public class KBucketTree: IRoutingTable where TNode : notnull { + private readonly INodeHashProvider _nodeHashProvider; + private class TreeNode { public KBucket Bucket { get; } @@ -34,10 +36,11 @@ public TreeNode(int k, ValueHash256 prefix) // TODO: Double check and probably make lockless private readonly McsLock _lock = new McsLock(); - public KBucketTree(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + public KBucketTree(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) { _k = config.KSize; _b = config.Beta; + _nodeHashProvider = nodeHashProvider; _currentNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); _root = new TreeNode(config.KSize, new ValueHash256()); _logger = logManager.GetClassLogger(); @@ -215,7 +218,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L } } - public IEnumerable IterateBucketRandomHashes() + public IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() { using McsLock.Disposable _ = _lock.Acquire(); @@ -223,22 +226,22 @@ public IEnumerable IterateBucketRandomHashes() return DoIterateBucketRandomHashes(_root, 0).ToArray(); } - private IEnumerable DoIterateBucketRandomHashes(TreeNode node, int depth) + private IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> DoIterateBucketRandomHashes(TreeNode node, int depth) { if (node.IsLeaf) { - yield return Hash256XorUtils.GetRandomHashAtDistance(_currentNodeHash, depth); + yield return (node.Prefix, depth, node.Bucket); } else { - foreach (ValueHash256 bucketHash in DoIterateBucketRandomHashes(node.Left!, depth + 1)) + foreach (var bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) { - yield return bucketHash; + yield return bucketInfo; } - foreach (ValueHash256 bucketHash in DoIterateBucketRandomHashes(node.Right!, depth + 1)) + foreach (var bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) { - yield return bucketHash; + yield return bucketInfo; } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index d5ef892bf830..84a117e28a5f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -7,27 +7,28 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class Kademlia : IKademlia where TNode : notnull +public class Kademlia : IKademlia where TNode : notnull { - private readonly IKademliaMessageSender _kademliaMessageSender; - private readonly INodeHashProvider _nodeHashProvider; + private readonly IKademliaMessageSender _kademliaMessageSender; + private readonly INodeHashProvider _nodeHashProvider; private readonly IRoutingTable _routingTable; - private readonly ILookupAlgo _lookupAlgo; - private readonly NodeHealthTracker _nodeHealthTracker; + private readonly ILookupAlgo _lookupAlgo; + private readonly INodeHealthTracker _nodeHealthTracker; private readonly ILogger _logger; private readonly TNode _currentNodeId; + private readonly TKey _currentNodeIdAsKey; private readonly ValueHash256 _currentNodeIdAsHash; private readonly int _kSize; private readonly TimeSpan _refreshInterval; public Kademlia( - INodeHashProvider nodeHashProvider, - IKademliaMessageSender sender, + INodeHashProvider nodeHashProvider, + IKademliaMessageSender sender, IRoutingTable routingTable, - ILookupAlgo lookupAlgo, + ILookupAlgo lookupAlgo, ILogManager logManager, - NodeHealthTracker nodeHealthTracker, + INodeHealthTracker nodeHealthTracker, KademliaConfig config) { _nodeHashProvider = nodeHashProvider; @@ -35,9 +36,10 @@ public Kademlia( _routingTable = routingTable; _lookupAlgo = lookupAlgo; _nodeHealthTracker = nodeHealthTracker; - _logger = logManager.GetClassLogger>(); + _logger = logManager.GetClassLogger>(); _currentNodeId = config.CurrentNodeId; + _currentNodeIdAsKey = _nodeHashProvider.GetKey(_currentNodeId); _currentNodeIdAsHash = _nodeHashProvider.GetHash(_currentNodeId); _kSize = config.KSize; _refreshInterval = config.RefreshInterval; @@ -67,32 +69,33 @@ private bool SameAsSelf(TNode node) return _nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; } - public async Task LookupNodesClosest(ValueHash256 targetHash, CancellationToken token, int? k = null) + public async Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) { return await LookupNodesClosest( - targetHash, + key, k ?? _kSize, async (nextNode, token) => { if (SameAsSelf(nextNode)) { - return _routingTable.GetKNearestNeighbour(targetHash); + ValueHash256 keyHash = _nodeHashProvider.GetKeyHash(key); + return _routingTable.GetKNearestNeighbour(keyHash); } - return await _kademliaMessageSender.FindNeighbours(nextNode, targetHash, token); + return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); }, token ); } private Task LookupNodesClosest( - ValueHash256 targetHash, + TKey target, int k, Func> findNeighbourOp, CancellationToken token ) { return _lookupAlgo.Lookup( - targetHash, + target, k, findNeighbourOp, token); @@ -100,7 +103,7 @@ CancellationToken token public async Task Run(CancellationToken token) { - await LookupNodesClosest(_currentNodeIdAsHash, token); + await LookupNodesClosest(_currentNodeIdAsKey, token); while (true) { @@ -114,15 +117,16 @@ public async Task Run(CancellationToken token) public async Task Bootstrap(CancellationToken token) { Stopwatch sw = Stopwatch.StartNew(); - await LookupNodesClosest(_currentNodeIdAsHash, token); + await LookupNodesClosest(_currentNodeIdAsKey, token); token.ThrowIfCancellationRequested(); // Refreshes all bucket. one by one. That is not empty. // A refresh means to do a k-nearest node lookup for a random hash for that particular bucket. - foreach (ValueHash256 nodeToLookup in _routingTable.IterateBucketRandomHashes()) + foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { - await LookupNodesClosest(nodeToLookup, token); + var keyToLookup = _nodeHashProvider.CreateRandomKeyAtDistance(Prefix, Distance); + await LookupNodesClosest(keyToLookup, token); } if (_logger.IsDebug) @@ -132,10 +136,11 @@ public async Task Bootstrap(CancellationToken token) } } - public TNode[] GetKNeighbour(ValueHash256 hash, TNode? excluding = default, bool excludeSelf = false) + public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false) { ValueHash256? excludeHash = null; if (excluding != null) excludeHash = _nodeHashProvider.GetHash(excluding); + ValueHash256 hash = _nodeHashProvider.GetKeyHash(target); return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs index 184f97cc1d6b..a5164b623e29 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs @@ -5,7 +5,10 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class KademliaKademliaMessageReceiver(IKademlia kademlia, NodeHealthTracker healthTracker): IKademliaMessageReceiver +public class KademliaKademliaMessageReceiver( + IKademlia kademlia, + INodeHealthTracker healthTracker +): IKademliaMessageReceiver where TNode : notnull { public Task Ping(TNode sender, CancellationToken token) { @@ -13,9 +16,9 @@ public Task Ping(TNode sender, CancellationToken token) return Task.CompletedTask; } - public Task FindNeighbours(TNode sender, ValueHash256 hash, CancellationToken token) + public Task FindNeighbours(TNode sender, TKey target, CancellationToken token) { healthTracker.OnIncomingMessageFrom(sender); - return Task.FromResult(kademlia.GetKNeighbour(hash, sender)); + return Task.FromResult(kademlia.GetKNeighbour(target, sender)); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index f50a17a0e2c6..eb09f2c2b22d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -6,40 +6,40 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class KademliaModule : Module where TNode : notnull +public class KademliaModule : Module where TNode : notnull { protected override void Load(ContainerBuilder builder) { base.Load(builder); builder - .AddSingleton, Kademlia>() - .AddSingleton, KademliaKademliaMessageReceiver>() - .AddSingleton>() - .AddSingleton>() - .AddSingleton>(provider => + .AddSingleton, Kademlia>() + .AddSingleton, KademliaKademliaMessageReceiver>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>(provider => { KademliaConfig config = provider.Resolve>(); if (config.UseNewLookup) { - return provider.Resolve>(); + return provider.Resolve>(); } - return provider.Resolve>(); + return provider.Resolve>(); }) - .AddSingleton, NewaTrackingLookupKNearestNeighbour>() - .AddSingleton>() - .AddSingleton>() - .AddSingleton>() + .AddSingleton, NewaTrackingLookupKNearestNeighbour>() + .AddSingleton>() + .AddSingleton>() + .AddSingleton>() .AddSingleton>(provider => { KademliaConfig config = provider.Resolve>(); if (config.UseTreeBasedRoutingTable) { - return provider.Resolve>(); + return provider.Resolve>(); } - return provider.Resolve>(); + return provider.Resolve>(); }); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs index 8709af7854f9..e9c8635a6ac4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs @@ -17,23 +17,23 @@ namespace Nethermind.Network.Discovery.Kademlia; /// earlier as it converge to the content faster, but take more query for findnodes due to a more strict stop /// condition. /// -public class NewLookupKNearestNeighbour( +public class NewLookupKNearestNeighbour( IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, - NodeHealthTracker nodeHealthTracker, + INodeHashProvider nodeHashProvider, + INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager): ILookupAlgo + ILogManager logManager): ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( - ValueHash256 targetHash, + TKey target, int k, Func> findNeighbourOp, CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); token = cts.Token; @@ -41,6 +41,7 @@ CancellationToken token ConcurrentDictionary queried = new(); ConcurrentDictionary seen = new(); + ValueHash256 targetHash = nodeHashProvider.GetKeyHash(target); IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); IComparer comparerReverse = Comparer.Create((h1, h2) => diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 61ded6c6e65a..30a17fd1896b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -1,11 +1,9 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Channels; -using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Core.Threading; @@ -14,41 +12,43 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class NewaTrackingLookupKNearestNeighbour( +public class NewaTrackingLookupKNearestNeighbour( IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, + INodeHashProvider nodeHashProvider, KademliaConfig kademliaConfig, - IKademliaMessageSender kademliaMessageSender, - NodeHealthTracker nodeHealthTracker, + IKademliaMessageSender kademliaMessageSender, + INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager) : ILookupAlgo2 + ILogManager logManager) : ILookupAlgo2 where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); private readonly ValueHash256 _currentNodeIdAsHash = nodeHashProvider.GetHash(kademliaConfig.CurrentNodeId); - public async Task LookupFunc(TNode nextNode, ValueHash256 targetHash, CancellationToken token) + private async Task LookupFunc(TNode nextNode, TKey target, CancellationToken token) { if (SameAsSelf(nextNode)) { - return routingTable.GetKNearestNeighbour(targetHash); + return routingTable.GetKNearestNeighbour(nodeHashProvider.GetKeyHash(target)); } - return await kademliaMessageSender.FindNeighbours(nextNode, targetHash, token); + return await kademliaMessageSender.FindNeighbours(nextNode, target, token); } private bool SameAsSelf(TNode node) { return nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; } + public async IAsyncEnumerable Lookup( - ValueHash256 targetHash, + TKey target, [EnumeratorCancellation] CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); using var cts = token.CreateChildTokenSource(); token = cts.Token; + var targetHash = nodeHashProvider.GetKeyHash(target); ConcurrentDictionary queried = new(); ConcurrentDictionary seen = new(); @@ -153,7 +153,7 @@ [EnumeratorCancellation] CancellationToken token try { // targetHash is implied in findNeighbourOp - var ret = await LookupFunc(node, targetHash, cts.Token); + var ret = await LookupFunc(node, target, cts.Token); nodeHealthTracker.OnIncomingMessageFrom(node); return (node, ret); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs index fe203f73b09a..4c1b895b2747 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs @@ -8,15 +8,21 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class NodeHealthTracker( +public interface INodeHealthTracker +{ + void OnIncomingMessageFrom(TNode sender); + void OnRequestFailed(TNode node); +} + +public class NodeHealthTracker( KademliaConfig config, IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, - IKademliaMessageSender kademliaMessageSender, + INodeHashProvider nodeHashProvider, + IKademliaMessageSender kademliaMessageSender, ILogManager logManager -) +) : INodeHealthTracker where TNode : notnull { - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); private readonly ConcurrentDictionary _isRefreshing = new(); private readonly LruCache _peerFailures = new(1024, "peer failure"); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs index 4b835f051ad9..95a6270b9cf9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -9,24 +9,25 @@ namespace Nethermind.Network.Discovery.Kademlia; /// /// This find nearest k query follows the kademlia paper faithfully, but does not do much parallelism. /// -public class OriginalLookupKNearestNeighbour( +public class OriginalLookupKNearestNeighbour( IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, - NodeHealthTracker nodeHealthTracker, + INodeHashProvider nodeHashProvider, + INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager): ILookupAlgo + ILogManager logManager): ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( - ValueHash256 targetHash, + TKey target, int k, Func> findNeighbourOp, CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); + ValueHash256 targetHash = nodeHashProvider.GetKeyHash(target); Func> wrappedFindNeighbourHop = async (node) => { using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs index 0a2895b60ef2..40572191f5e3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -24,7 +24,7 @@ public class KademliaDiscv4MessageSender( KademliaConfig kademliaConfig, ILogManager logManager, ITimestamper timestamper -): IKademliaMessageSender +): IKademliaMessageSender { private ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } @@ -181,7 +181,7 @@ CancellationToken token private Counter FindNeighbourStatus = Prometheus.Metrics.CreateCounter("find_neighbour_status", "find neighbour", "status"); - public async Task FindNeighbours(Node receiver, ValueHash256 hash, CancellationToken token) + public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) { using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; @@ -197,7 +197,7 @@ public async Task FindNeighbours(Node receiver, ValueHash256 hash, Cance { var result = await RunAuthenticatedRequest(receiver, async token => { - FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), hash.ToByteArray()); + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); return await CallAndWaitForResponse(_awaitingFindNeighbourMsg, receiver, msg, token); }, token); @@ -324,7 +324,7 @@ private long CalculateExpirationTime() #pragma warning disable CS9113 // Parameter is unread. public class KademliaDiscv4MessageReceiver( - IKademliaMessageReceiver receiver, + IKademliaMessageReceiver receiver, KademliaDiscv4MessageSender sender, NodeRecord selfNodeRecord, ITimestamper timestamper, @@ -392,8 +392,8 @@ private void HandleFindNode(Node node, FindNodeMsg msg) Task.Run(async () => { - ValueHash256 searchId = new ValueHash256(msg.SearchedNodeId); - Node[] nodes = await receiver.FindNeighbours(node, searchId, _cts.Token); + PublicKey publicKey = new PublicKey(msg.SearchedNodeId); + Node[] nodes = await receiver.FindNeighbours(node, publicKey, _cts.Token); if (nodes.Length > 12) { // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. From b37793e08d474e0dd53f74b8116fe6774d7d8c31 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 12:24:58 +0800 Subject: [PATCH 013/182] Cleaner --- .../DiscoveryApp.cs | 2 - .../Kademlia/KademliaModule.cs | 2 +- .../KademliaDiscv4MessageSender.cs | 217 ++++++++++-------- 3 files changed, 118 insertions(+), 103 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 4ccc8cd1091a..44fe331d8785 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -597,7 +597,6 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance ConcurrentDictionary _writtenNodes = new(); int duplicated = 0; int total = 0; - int fromNewNode = 0; void handler(object? _, Node addedNode) { @@ -633,7 +632,6 @@ async Task DiscoverAsync(PublicKey target) { if (_logger.IsDebug) _logger.Debug($"Found {count} nodes"); } - Console.Error.WriteLine($"Total is {total}, duplicated {duplicated}, fromNewNode {fromNewNode}"); } Task discoverTask = Task.WhenAll(Enumerable.Range(0, 6).Select((_) => Task.Run(async () => diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index eb09f2c2b22d..c4cc8cd6b6bd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -30,7 +30,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton, NewaTrackingLookupKNearestNeighbour>() .AddSingleton>() .AddSingleton>() - .AddSingleton>() + .AddSingleton, NodeHealthTracker>() .AddSingleton>(provider => { KademliaConfig config = provider.Resolve>(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs index 40572191f5e3..8333e53020a0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics; +using MathNet.Numerics.Distributions; using Nethermind.Core; using Nethermind.Core.Caching; using Nethermind.Core.Collections; @@ -29,22 +30,84 @@ ITimestamper timestamper private ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); - private TimeSpan _unauthenticatedRequestTimeout = TimeSpan.FromSeconds(4); private TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); private TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(1); private TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); - private ConcurrentDictionary> _awaitingPingMsg = new(); - private ConcurrentDictionary _awaitingPongToNode = new(); + private interface IMessageHandler + { + bool Handle(DiscoveryMsg msg); + } - // TODO: Allow multiple in flight request per node - private ConcurrentDictionary> _awaitingFindNeighbourMsg = new(); - private ConcurrentDictionary> _awaitingEnrRequestMsg = new(); + private interface ITaskCompleter: IMessageHandler + { + TaskCompletionSource TaskCompletionSource { get; } + } - private LruCache _lastPong = new(1024 * 10, ""); - private LruCache _knownFailedFindNeighbour = new(1024 * 10, ""); + private class PongMsgHandler(PingMsg ping) : ITaskCompleter + { + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public bool Handle(DiscoveryMsg msg) + { + if (msg is PongMsg pong && Bytes.AreEqual(pong.PingMdc, ping.Mdc) && TaskCompletionSource.TrySetResult(pong)) + { + return true; + } + return false; + } + } + + private class NeighbourMsgHandler(int k) : ITaskCompleter + { + private Node[] _current = Array.Empty(); + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + private static readonly TimeSpan _secondRequestTimeout = TimeSpan.FromSeconds(1); + private bool _timeoutInitiated = false; + + public bool Handle(DiscoveryMsg msg) + { + NeighborsMsg neighborsMsg = (NeighborsMsg)msg; + if (_current.Length >= k || _current.Length + neighborsMsg.Nodes.Length > k) return false; + + _current = _current.Concat(neighborsMsg.Nodes).ToArray(); + if (_current.Length == k) + { + TaskCompletionSource.TrySetResult(_current); + } + else + { + // Some client (nethermind) only respond with one request. + Task.Run(async () => + { + if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false) == false) return; + await Task.Delay(_secondRequestTimeout); + TaskCompletionSource.TrySetResult(_current); + }); + } + return true; + } + } - private int findNodeFailureLimit = 5; + private class EnrResponseHandler : ITaskCompleter { + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public bool Handle(DiscoveryMsg msg) + { + if (msg is EnrResponseMsg resp && TaskCompletionSource.TrySetResult(resp)) + { + return true; + } + return false; + } + } + + private ConcurrentDictionary _awaitingPongToNode = new(); + private ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); + + private LruCache _lastPong = new(1024 * 10, ""); + private const int findNodeFailureLimit = 5; private ConcurrentDictionary _findNodesFailure = new(); private Counter EnsureSessionResult = @@ -98,14 +161,10 @@ private async Task EnsureSession(Node node, CancellationToken token) } Counter AuthRequestCounter = Prometheus.Metrics.CreateCounter("kademlia_auth_request", "request", "status"); - private ConcurrentDictionary _previouslyOk = new(); - private ConcurrentDictionary _maybePingOnRequest = new(); private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) { AuthRequestCounter.WithLabels("auth_request").Inc(); - TaskCompletionSource pingCts = new(TaskCreationOptions.RunContinuationsAsynchronously); - _maybePingOnRequest.TryAdd(node.IdHash, pingCts); using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; @@ -123,7 +182,6 @@ private async Task RunAuthenticatedRequest(Node node, Func [handler], + (_, currentHandler) => currentHandler.Concat([handler]).ToArray() + ); + } + + private void RemoveMessageHandler( + MsgType msgType, ValueHash256 nodeId, IMessageHandler handler) + { + var key = (nodeId, msgType); + if (_incomingMessageHandlers.TryRemove(new KeyValuePair<(ValueHash256, MsgType), IMessageHandler[]>(key, [handler]))) return; + + while (true) { - _logger.Error($"Invalid pong mdc. Send {msg.Mdc?.ToHexString()}, Received {pongMsg.PingMdc?.ToHexString()}"); - throw new OperationCanceledException(); // Expose as timeout + if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? current)) return; + var newValue = current.Where((it) => it != handler).ToArray(); + if (_incomingMessageHandlers.TryUpdate(key, newValue, current)) return; } } + private async Task CallAndWaitForResponse( - ConcurrentDictionary> requestDictionary, + MsgType msgType, + ITaskCompleter messageHandler, Node receiver, DiscoveryMsg msg, CancellationToken token ) { - TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(completionSource); - ValueHash256 requestHash = receiver.IdHash; - while (!requestDictionary.TryAdd(requestHash, completionSource)) - { - if (requestDictionary.TryGetValue(requestHash, out TaskCompletionSource? tcs)) - { - try - { - await tcs.Task; - } - finally - { - requestDictionary.TryRemove(requestHash, out _); - } - } - } + await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(messageHandler.TaskCompletionSource); + AddMessageHandler(msgType, receiver.IdHash, messageHandler); await SendDiscV4Message(receiver, msg); try { - return await completionSource.Task; + return await messageHandler.TaskCompletionSource.Task; } finally { - requestDictionary.TryRemove(requestHash, out _); + RemoveMessageHandler(msgType, receiver.IdHash, messageHandler); } } @@ -186,20 +250,14 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - if (_knownFailedFindNeighbour.TryGet(receiver.IdHash, out DateTimeOffset lastFailure) && - lastFailure > DateTimeOffset.Now - TimeSpan.FromMinutes(5)) - { - FindNeighbourStatus.WithLabels("Known fail").Inc(); - return []; - } - try { - var result = await RunAuthenticatedRequest(receiver, async token => + Node[] result = await RunAuthenticatedRequest(receiver, async token => { FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); - return await CallAndWaitForResponse(_awaitingFindNeighbourMsg, receiver, msg, token); + // TODO: 16 is configurable + return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(16), receiver, msg, token); }, token); _findNodesFailure[receiver.IdHash] = 0; @@ -217,7 +275,6 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel _findNodesFailure[receiver.IdHash] = 1; } - _knownFailedFindNeighbour.Set(receiver.IdHash, DateTimeOffset.Now); if (IsShouldBeAuthenticated(receiver)) { FindNeighbourStatus.WithLabels("timeout_should_be_authenticated").Inc(); @@ -239,59 +296,21 @@ public async Task SendEnrRequest(Node receiver, CancellationToke { EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); - return await CallAndWaitForResponse(_awaitingEnrRequestMsg, receiver, msg, token); + return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, msg, token); }, token); } - public async Task SendUnauthEnrRequest(Node receiver, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); - return await CallAndWaitForResponse(_awaitingEnrRequestMsg, receiver, msg, token); - } - - internal void OnPong(Node node, PongMsg msg) - { - if (_awaitingPingMsg.TryRemove(node.IdHash, out TaskCompletionSource? completionSource)) - { - completionSource.TrySetResult(msg); - } - else - { - // Pong timeout? - // _logger.Error($"No ping for pong {node} {msg.PingMdc.ToHexString()}"); - } - } - - public void OnPing(Node node, PingMsg ping) - { - if (_maybePingOnRequest.TryRemove(node.IdHash, out var cts)) - { - cts.TrySetResult(); - } - } - - public void HandleEnrResponse(Node node, EnrResponseMsg msg) + public void OnDiscv4Message(Node node, DiscoveryMsg msg) { - ValueHash256 requestId = node.IdHash; - if (_awaitingEnrRequestMsg.TryRemove(requestId, out TaskCompletionSource? completionSource)) + var key = (node.IdHash, msg.MsgType); + if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? handlers)) return; + foreach (var messageHandler in handlers!) { - completionSource.TrySetResult(msg); - } - } - - public void OnNeighbour(Node node, NeighborsMsg msg) - { - ValueHash256 requestId = node.IdHash; - if (_awaitingFindNeighbourMsg.TryRemove(requestId, out TaskCompletionSource? completionSource)) - { - completionSource.TrySetResult(msg.Nodes); - } - else - { - _logger.Error($"No FindNeighbour for Neighbour {node} {msg}"); + if (messageHandler.Handle(msg)) + { + // Note: We dont remove the handler as in case of neighbour, a handler may need multiple message. + return; + } } } @@ -343,13 +362,13 @@ public void OnIncomingMsg(DiscoveryMsg msg) MsgType msgType = msg.MsgType; Node node = new(msg.FarPublicKey, msg.FarAddress); + sender.OnDiscv4Message(node, msg); + switch (msgType) { case MsgType.Neighbors: - sender.OnNeighbour(node, (NeighborsMsg)msg); break; case MsgType.Pong: - sender.OnPong(node, (PongMsg)msg); break; case MsgType.Ping: PingMsg ping = (PingMsg)msg; @@ -362,7 +381,6 @@ public void OnIncomingMsg(DiscoveryMsg msg) HandleEnrRequest(node, (EnrRequestMsg)msg); break; case MsgType.EnrResponse: - sender.HandleEnrResponse(node, (EnrResponseMsg)msg); break; default: _logger.Error($"Unsupported msgType: {msgType}"); @@ -405,7 +423,6 @@ private void HandleFindNode(Node node, FindNodeMsg msg) private void HandlePing(Node node, PingMsg ping) { - sender.OnPing(node, ping); if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); Task.Run(async () => { From 3b1919e2856f68fd3f4b02a1559abe11135c8115 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 12:47:03 +0800 Subject: [PATCH 014/182] Slight cleanup --- .../DiscoveryApp.cs | 5 +- .../KademliaDiscv4MessageSender.cs | 305 +++++++----------- 2 files changed, 123 insertions(+), 187 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 44fe331d8785..1864a161ae82 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -47,7 +47,6 @@ public class DiscoveryApp : IDiscoveryApp private PublicKey _masterNode = null!; private readonly NodeRecord _selfNodeRecorrd; - private KademliaDiscv4MessageReceiver _discv4MessageReceiver = null!; private KademliaDiscv4MessageSender _discv4MessageSender = null!; private IKademlia _kademlia = null!; private ILookupAlgo2 _lookup2 = null!; @@ -159,12 +158,10 @@ public void Initialize(PublicKey masterPublicKey) CurrentNodeId = new Node(_masterNode, "127.0.0.1", 9999, true) }) .AddSingleton, KademliaDiscv4MessageSender>() - .AddSingleton() .Build(); _kademlia = _kademliaServices.Resolve>(); _lookup2 = _kademliaServices.Resolve>(); - _discv4MessageReceiver = _kademliaServices.Resolve(); _discv4MessageSender = _kademliaServices.Resolve(); // TODO: Setup kademlia here @@ -239,7 +236,7 @@ private void ResetUnreachableStatus(object? sender, NetworkAvailabilityEventArgs public void InitializeChannel(IChannel channel) { - _discoveryHandler = new NettyDiscoveryHandler(_discv4MessageReceiver, channel, _messageSerializationService, + _discoveryHandler = new NettyDiscoveryHandler(_discv4MessageSender, channel, _messageSerializationService, _timestamper, _logManager); _discv4MessageSender.MsgSender = _discoveryHandler; diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs index 8333e53020a0..2520fb0be2ec 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Diagnostics; -using MathNet.Numerics.Distributions; using Nethermind.Core; using Nethermind.Core.Caching; using Nethermind.Core.Collections; @@ -16,22 +14,25 @@ using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; using NonBlocking; -using Prometheus; namespace Nethermind.Network.Discovery; public class KademliaDiscv4MessageSender( + Lazy> kademlia, // Cyclic dependency INetworkConfig networkConfig, KademliaConfig kademliaConfig, + NodeRecord selfNodeRecord, ILogManager logManager, ITimestamper timestamper -): IKademliaMessageSender +): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { private ILogger _logger = logManager.GetClassLogger(); + + private readonly CancellationTokenSource _cts = new(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); private TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); - private TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(1); + private TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(2); private TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); private interface IMessageHandler @@ -103,30 +104,100 @@ public bool Handle(DiscoveryMsg msg) } } - private ConcurrentDictionary _awaitingPongToNode = new(); - private ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); + private readonly ConcurrentDictionary _awaitingPongToNode = new(); + private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); - private LruCache _lastPong = new(1024 * 10, ""); - private const int findNodeFailureLimit = 5; - private ConcurrentDictionary _findNodesFailure = new(); + private readonly LruCache _bondDeadline = new(1024 * 10, ""); + private readonly ConcurrentDictionary _authenticatedRequestFailure = new(); + private static TimeSpan _bondTimeout = TimeSpan.FromHours(12); + private const int AuthenticatedRequestFailureLimit = 5; - private Counter EnsureSessionResult = - Prometheus.Metrics.CreateCounter("kademlia_ensure_session_result", "result", "result"); + public async Task Ping(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; - private bool IsShouldBeAuthenticated(Node node) + PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); + + _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, msg, token); + } + + public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) { - return _lastPong.TryGet(node.IdHash, out DateTimeOffset lastPong) - && lastPong > DateTimeOffset.Now - TimeSpan.FromHours(12) - && (!_findNodesFailure.TryGetValue(node.IdHash, out int failedFinedNodes) || failedFinedNodes <= findNodeFailureLimit); + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + return await RunAuthenticatedRequest(receiver, async token => + { + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); + + // TODO: 16 is configurable + return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(16), receiver, msg, token); + }, token); } - private async Task EnsureSession(Node node, CancellationToken token) + public async Task SendEnrRequest(Node receiver, CancellationToken token) { - if (IsShouldBeAuthenticated(node)) + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + return await RunAuthenticatedRequest(receiver, async token => { - EnsureSessionResult.WithLabels("pong_not_expired"); - return; - } + EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); + + return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, msg, token); + }, token); + } + + private void HandleEnrRequest(Node node, EnrRequestMsg msg) + { + if (!IsPeerSafe(node)) return; + + Task.Run(async () => + { + Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); + await SendMessage(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + }); + } + + private void HandleFindNode(Node node, FindNodeMsg msg) + { + if (!IsPeerSafe(node)) return; + + Task.Run(async () => + { + PublicKey publicKey = new PublicKey(msg.SearchedNodeId); + Node[] nodes = await kademlia.Value.FindNeighbours(node, publicKey, _cts.Token); + if (nodes.Length > 12) + { + // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. + nodes = nodes.Slice(0, 12).ToArray(); + } + await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + }); + } + + private void HandlePing(Node node, PingMsg ping) + { + if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); + Task.Run(async () => + { + await kademlia.Value.Ping(node, _cts.Token); + PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); + await SendMessage(node, msg); + }); + } + + private bool IsShouldBeBonded(Node node) + { + return _bondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) + && bondDeadline > DateTimeOffset.Now + && (!_authenticatedRequestFailure.TryGetValue(node.IdHash, out long failedFinedNodes) || failedFinedNodes <= AuthenticatedRequestFailureLimit); + } + + private async Task EnsureBonded(Node node, CancellationToken token) + { + if (IsShouldBeBonded(node)) return true; if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); using var cts = token.CreateChildTokenSource(_tryAuthenticatedTimeout); @@ -141,17 +212,14 @@ private async Task EnsureSession(Node node, CancellationToken token) await Task.Delay(_waitAfterPongTimeout, token); // Give some time for peer to process pong. if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); - EnsureSessionResult.WithLabels("pong_success"); + + return true; } catch (OperationCanceledException) { if (_logger.IsTrace) _logger.Trace($"Node {node} timeout trying to trigger pong."); - EnsureSessionResult.WithLabels("pong_timeout"); - } - catch (Exception) - { - EnsureSessionResult.WithLabels("error"); - throw; + + return false; } finally { @@ -160,42 +228,28 @@ private async Task EnsureSession(Node node, CancellationToken token) } } - Counter AuthRequestCounter = Prometheus.Metrics.CreateCounter("kademlia_auth_request", "request", "status"); - private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) { - AuthRequestCounter.WithLabels("auth_request").Inc(); - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - await EnsureSession(node, token); + bool shouldBeBonded = await EnsureBonded(node, token); try { - var resp = await callRequest(token); - AuthRequestCounter.WithLabels("ok_attempt").Inc(); + T resp = await callRequest(token); + if (!shouldBeBonded) + { + // Well.... maybe we already bonded, we just forgot about it.... + _bondDeadline.Set(node.IdHash, DateTimeOffset.Now + _bondTimeout); + } + _authenticatedRequestFailure[node.IdHash] = 0; return resp; } catch (OperationCanceledException) { - AuthRequestCounter.WithLabels("failed_attempt").Inc(); - if (IsShouldBeAuthenticated(node)) - { - AuthRequestCounter.WithLabels("failed_attempt_should_be_authenticated").Inc(); - } + _authenticatedRequestFailure.Increment(node.IdHash); + throw; } } - public async Task Ping(Node receiver, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); - - _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, msg, token); - } - private void AddMessageHandler( MsgType msgType, ValueHash256 nodeId, IMessageHandler handler) { @@ -220,7 +274,6 @@ private void RemoveMessageHandler( } } - private async Task CallAndWaitForResponse( MsgType msgType, ITaskCompleter messageHandler, @@ -231,7 +284,7 @@ CancellationToken token await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(messageHandler.TaskCompletionSource); AddMessageHandler(msgType, receiver.IdHash, messageHandler); - await SendDiscV4Message(receiver, msg); + await SendMessage(receiver, msg); try { return await messageHandler.TaskCompletionSource.Task; @@ -242,85 +295,14 @@ CancellationToken token } } - private Counter FindNeighbourStatus = - Prometheus.Metrics.CreateCounter("find_neighbour_status", "find neighbour", "status"); - public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - try - { - Node[] result = await RunAuthenticatedRequest(receiver, async token => - { - FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); - - // TODO: 16 is configurable - return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(16), receiver, msg, token); - }, token); - - _findNodesFailure[receiver.IdHash] = 0; - FindNeighbourStatus.WithLabels("ok").Inc(); - return result; - } - catch (OperationCanceledException) - { - if (_findNodesFailure.TryGetValue(receiver.IdHash, out int failureCount)) - { - _findNodesFailure[receiver.IdHash] = failureCount + 1; - } - else - { - _findNodesFailure[receiver.IdHash] = 1; - } - - if (IsShouldBeAuthenticated(receiver)) - { - FindNeighbourStatus.WithLabels("timeout_should_be_authenticated").Inc(); - } - else - { - FindNeighbourStatus.WithLabels("timeout").Inc(); - } - throw; - } - } - - public async Task SendEnrRequest(Node receiver, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - return await RunAuthenticatedRequest(receiver, async token => - { - EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); - - return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, msg, token); - }, token); - } - - public void OnDiscv4Message(Node node, DiscoveryMsg msg) - { - var key = (node.IdHash, msg.MsgType); - if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? handlers)) return; - foreach (var messageHandler in handlers!) - { - if (messageHandler.Handle(msg)) - { - // Note: We dont remove the handler as in case of neighbour, a handler may need multiple message. - return; - } - } - } - - public async Task SendDiscV4Message(Node node, DiscoveryMsg msg) + private async Task SendMessage(Node node, DiscoveryMsg msg) { if (MsgSender is { } sender) { if (msg is PongMsg pong) { - _lastPong.Set(node.IdHash, DateTimeOffset.Now); + _bondDeadline.Set(node.IdHash, DateTimeOffset.Now + _bondTimeout); if (_awaitingPongToNode.TryGetValue(node.IdHash, out TaskCompletionSource? completionSource)) { completionSource.TrySetResult(); @@ -339,20 +321,6 @@ private long CalculateExpirationTime() { return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; } -} - -#pragma warning disable CS9113 // Parameter is unread. -public class KademliaDiscv4MessageReceiver( - IKademliaMessageReceiver receiver, - KademliaDiscv4MessageSender sender, - NodeRecord selfNodeRecord, - ITimestamper timestamper, - ILogManager logManager -) : IDiscoveryMsgListener, IAsyncDisposable -#pragma warning restore CS9113 // Parameter is unread. -{ - private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly CancellationTokenSource _cts = new(); public void OnIncomingMsg(DiscoveryMsg msg) { @@ -362,7 +330,10 @@ public void OnIncomingMsg(DiscoveryMsg msg) MsgType msgType = msg.MsgType; Node node = new(msg.FarPublicKey, msg.FarAddress); - sender.OnDiscv4Message(node, msg); + if (HandleViaMessageHandlers(node, msg)) + { + return; + } switch (msgType) { @@ -393,43 +364,20 @@ public void OnIncomingMsg(DiscoveryMsg msg) } } - private void HandleEnrRequest(Node node, EnrRequestMsg msg) + private bool HandleViaMessageHandlers(Node node, DiscoveryMsg msg) { - if (!IsPeerSafe(node)) return; - - Task.Run(async () => - { - Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await sender.SendDiscV4Message(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); - }); - } - - private void HandleFindNode(Node node, FindNodeMsg msg) - { - if (!IsPeerSafe(node)) return; - - Task.Run(async () => + var key = (node.IdHash, msg.MsgType); + if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? handlers)) return false; + foreach (var messageHandler in handlers!) { - PublicKey publicKey = new PublicKey(msg.SearchedNodeId); - Node[] nodes = await receiver.FindNeighbours(node, publicKey, _cts.Token); - if (nodes.Length > 12) + if (messageHandler.Handle(msg)) { - // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. - nodes = nodes.Slice(0, 12).ToArray(); + // Note: We dont remove the handler as in case of neighbour, a handler may need multiple message. + return true; } - await sender.SendDiscV4Message(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); - }); - } + } - private void HandlePing(Node node, PingMsg ping) - { - if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); - Task.Run(async () => - { - await receiver.Ping(node, _cts.Token); - PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); - await sender.SendDiscV4Message(node, msg); - }); + return true; } private bool IsPeerSafe(Node node) @@ -437,15 +385,6 @@ private bool IsPeerSafe(Node node) return true; } - /// - /// This is the value set by other clients based on real network tests. - /// - private const int ExpirationTimeInSeconds = 20; - private long CalculateExpirationTime() - { - return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; - } - public async ValueTask DisposeAsync() { await _cts.CancelAsync(); From 5cb856851efbff79beb0ac1c20abde42d6bc6fd9 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 13:30:52 +0800 Subject: [PATCH 015/182] Poor simplification attempt --- .../Nethermind.Network.Discovery/DiscoveryApp.cs | 5 +++-- .../Kademlia/BucketListRoutingTable.cs | 4 ++-- .../Kademlia/INodeHashProvider.cs | 5 ++++- .../Kademlia/KBucketTree.cs | 5 +---- .../Kademlia/Kademlia.cs | 15 +++++++++------ .../Kademlia/NewLookupKNearestNeighbour.cs | 5 +++-- .../NewTrackingLookupKNearestNeighbour.cs | 7 ++++--- .../Kademlia/NodeHealthTracker.cs | 2 +- .../Kademlia/OriginalLookupKNearestNeighbour.cs | 5 +++-- 9 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 1864a161ae82..688e077a27ca 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -103,7 +103,7 @@ public DiscoveryApp( } } - private class NodeNodeHashProvider : INodeHashProvider + private class NodeNodeHashProvider : INodeHashProvider, IKeyOperator { public ValueHash256 GetHash(Node node) { @@ -148,7 +148,8 @@ public void Initialize(PublicKey masterPublicKey) _kademliaServices = new ContainerBuilder() .AddModule(new KademliaModule()) - .AddSingleton, NodeNodeHashProvider>() + .AddSingleton, NodeNodeHashProvider>() + .AddSingleton, NodeNodeHashProvider>() .AddSingleton(_timestamper) .AddSingleton(_networkConfig) .AddSingleton(_logManager) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs index fcaeb9cb1a23..0e68aebd9ac0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs @@ -12,13 +12,13 @@ public class BucketListRoutingTable: IRoutingTable where TNo private readonly ILogger _logger; private readonly KBucket[] _buckets; private readonly ValueHash256 _currentNodeIdAsHash; - private readonly INodeHashProvider _nodeHashProvider; + private readonly INodeHashProvider _nodeHashProvider; private readonly int _kSize; // TODO: Double check and probably make lockless private readonly McsLock _lock = new McsLock(); - public BucketListRoutingTable(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + public BucketListRoutingTable(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) { _logger = logManager.GetClassLogger>(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs index 3dbca0099fb1..4106a6a3eb7f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs @@ -15,9 +15,12 @@ namespace Nethermind.Network.Discovery.Kademlia; /// could be specialized. /// /// -public interface INodeHashProvider +public interface INodeHashProvider { ValueHash256 GetHash(TNode node); +} + +public interface IKeyOperator { TKey GetKey(TNode node); ValueHash256 GetKeyHash(TKey key); TKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 1aae7afe5845..85ca8a03a00b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -10,8 +10,6 @@ namespace Nethermind.Network.Discovery.Kademlia; public class KBucketTree: IRoutingTable where TNode : notnull { - private readonly INodeHashProvider _nodeHashProvider; - private class TreeNode { public KBucket Bucket { get; } @@ -36,11 +34,10 @@ public TreeNode(int k, ValueHash256 prefix) // TODO: Double check and probably make lockless private readonly McsLock _lock = new McsLock(); - public KBucketTree(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + public KBucketTree(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) { _k = config.KSize; _b = config.Beta; - _nodeHashProvider = nodeHashProvider; _currentNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); _root = new TreeNode(config.KSize, new ValueHash256()); _logger = logManager.GetClassLogger(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index 84a117e28a5f..55976886f1b9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -10,7 +10,8 @@ namespace Nethermind.Network.Discovery.Kademlia; public class Kademlia : IKademlia where TNode : notnull { private readonly IKademliaMessageSender _kademliaMessageSender; - private readonly INodeHashProvider _nodeHashProvider; + private readonly INodeHashProvider _nodeHashProvider; + private readonly IKeyOperator _keyOperator; private readonly IRoutingTable _routingTable; private readonly ILookupAlgo _lookupAlgo; private readonly INodeHealthTracker _nodeHealthTracker; @@ -23,7 +24,8 @@ public class Kademlia : IKademlia where TNode : notnul private readonly TimeSpan _refreshInterval; public Kademlia( - INodeHashProvider nodeHashProvider, + INodeHashProvider nodeHashProvider, + IKeyOperator keyOperator, IKademliaMessageSender sender, IRoutingTable routingTable, ILookupAlgo lookupAlgo, @@ -32,6 +34,7 @@ public Kademlia( KademliaConfig config) { _nodeHashProvider = nodeHashProvider; + _keyOperator = keyOperator; _kademliaMessageSender = sender; _routingTable = routingTable; _lookupAlgo = lookupAlgo; @@ -39,7 +42,7 @@ public Kademlia( _logger = logManager.GetClassLogger>(); _currentNodeId = config.CurrentNodeId; - _currentNodeIdAsKey = _nodeHashProvider.GetKey(_currentNodeId); + _currentNodeIdAsKey = _keyOperator.GetKey(_currentNodeId); _currentNodeIdAsHash = _nodeHashProvider.GetHash(_currentNodeId); _kSize = config.KSize; _refreshInterval = config.RefreshInterval; @@ -78,7 +81,7 @@ public async Task LookupNodesClosest(TKey key, CancellationToken token, { if (SameAsSelf(nextNode)) { - ValueHash256 keyHash = _nodeHashProvider.GetKeyHash(key); + ValueHash256 keyHash = _keyOperator.GetKeyHash(key); return _routingTable.GetKNearestNeighbour(keyHash); } return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); @@ -125,7 +128,7 @@ public async Task Bootstrap(CancellationToken token) // A refresh means to do a k-nearest node lookup for a random hash for that particular bucket. foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { - var keyToLookup = _nodeHashProvider.CreateRandomKeyAtDistance(Prefix, Distance); + var keyToLookup = _keyOperator.CreateRandomKeyAtDistance(Prefix, Distance); await LookupNodesClosest(keyToLookup, token); } @@ -140,7 +143,7 @@ public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool exclu { ValueHash256? excludeHash = null; if (excluding != null) excludeHash = _nodeHashProvider.GetHash(excluding); - ValueHash256 hash = _nodeHashProvider.GetKeyHash(target); + ValueHash256 hash = _keyOperator.GetKeyHash(target); return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs index e9c8635a6ac4..e91e9e64d138 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs @@ -19,7 +19,8 @@ namespace Nethermind.Network.Discovery.Kademlia; /// public class NewLookupKNearestNeighbour( IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, + INodeHashProvider nodeHashProvider, + IKeyOperator keyOperator, INodeHealthTracker nodeHealthTracker, KademliaConfig config, ILogManager logManager): ILookupAlgo where TNode : notnull @@ -41,7 +42,7 @@ CancellationToken token ConcurrentDictionary queried = new(); ConcurrentDictionary seen = new(); - ValueHash256 targetHash = nodeHashProvider.GetKeyHash(target); + ValueHash256 targetHash = keyOperator.GetKeyHash(target); IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); IComparer comparerReverse = Comparer.Create((h1, h2) => diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 30a17fd1896b..7672e88aff26 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -14,7 +14,8 @@ namespace Nethermind.Network.Discovery.Kademlia; public class NewaTrackingLookupKNearestNeighbour( IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, + INodeHashProvider nodeHashProvider, + IKeyOperator keyOperator, KademliaConfig kademliaConfig, IKademliaMessageSender kademliaMessageSender, INodeHealthTracker nodeHealthTracker, @@ -29,7 +30,7 @@ private async Task LookupFunc(TNode nextNode, TKey target, Cancellation { if (SameAsSelf(nextNode)) { - return routingTable.GetKNearestNeighbour(nodeHashProvider.GetKeyHash(target)); + return routingTable.GetKNearestNeighbour(keyOperator.GetKeyHash(target)); } return await kademliaMessageSender.FindNeighbours(nextNode, target, token); } @@ -48,7 +49,7 @@ [EnumeratorCancellation] CancellationToken token using var cts = token.CreateChildTokenSource(); token = cts.Token; - var targetHash = nodeHashProvider.GetKeyHash(target); + var targetHash = keyOperator.GetKeyHash(target); ConcurrentDictionary queried = new(); ConcurrentDictionary seen = new(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs index 4c1b895b2747..bf32db077ad3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs @@ -17,7 +17,7 @@ public interface INodeHealthTracker public class NodeHealthTracker( KademliaConfig config, IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, + INodeHashProvider nodeHashProvider, IKademliaMessageSender kademliaMessageSender, ILogManager logManager ) : INodeHealthTracker where TNode : notnull diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs index 95a6270b9cf9..f33794a4b2a1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -11,7 +11,8 @@ namespace Nethermind.Network.Discovery.Kademlia; /// public class OriginalLookupKNearestNeighbour( IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, + INodeHashProvider nodeHashProvider, + IKeyOperator keyOperator, INodeHealthTracker nodeHealthTracker, KademliaConfig config, ILogManager logManager): ILookupAlgo where TNode : notnull @@ -27,7 +28,7 @@ CancellationToken token ) { if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); - ValueHash256 targetHash = nodeHashProvider.GetKeyHash(target); + ValueHash256 targetHash = keyOperator.GetKeyHash(target); Func> wrappedFindNeighbourHop = async (node) => { using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); From 90b50c6d499d842272d26c50563387da7b99c905 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 13:32:24 +0800 Subject: [PATCH 016/182] Remove bucket list --- .../Kademlia/BucketListRoutingTable.cs | 162 ------------------ .../Kademlia/IServiceCollectionExtensions.cs | 12 +- .../Kademlia/KademliaConfig.cs | 5 - .../Kademlia/KademliaModule.cs | 15 +- 4 files changed, 3 insertions(+), 191 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs deleted file mode 100644 index 0e68aebd9ac0..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketListRoutingTable.cs +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Core.Threading; -using Nethermind.Logging; - -namespace Nethermind.Network.Discovery.Kademlia; - -public class BucketListRoutingTable: IRoutingTable where TNode : notnull -{ - private readonly ILogger _logger; - private readonly KBucket[] _buckets; - private readonly ValueHash256 _currentNodeIdAsHash; - private readonly INodeHashProvider _nodeHashProvider; - private readonly int _kSize; - - // TODO: Double check and probably make lockless - private readonly McsLock _lock = new McsLock(); - - public BucketListRoutingTable(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) - { - _logger = logManager.GetClassLogger>(); - - // Note: It does not have to be this much. In practice, only like 16 of these bucket get populated. - _buckets = new KBucket[Hash256XorUtils.MaxDistance + 1]; - for (int i = 0; i < Hash256XorUtils.MaxDistance + 1; i++) - { - _buckets[i] = new KBucket(config.KSize); - } - - _nodeHashProvider = nodeHashProvider; - _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); - _kSize = config.KSize; - } - - private KBucket GetBucket(in ValueHash256 hash) - { - int idx = Hash256XorUtils.CalculateDistance(hash, _currentNodeIdAsHash); - return _buckets[idx]; - } - - public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh) - { - using McsLock.Disposable _ = _lock.Acquire(); - BucketAddResult result = GetBucket(hash).TryAddOrRefresh(hash, item, out toRefresh); - if (result == BucketAddResult.Added) - { - OnNodeAdded?.Invoke(this, item); - } - return result; - } - - public bool Remove(in ValueHash256 hash) - { - using McsLock.Disposable _ = _lock.Acquire(); - return GetBucket(hash).RemoveAndReplace(hash); - } - - public TNode[] GetAllAtDistance(int i) - { - using McsLock.Disposable _ = _lock.Acquire(); - return _buckets[i].GetAll(); - } - - public IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() - { - for (var i = 0; i < _buckets.Length; i++) - { - if (_buckets[i].Count > 0) - { - // TODO: Prefix incorrect - yield return (_currentNodeIdAsHash, i, _buckets[i]); - } - } - } - - public TNode? GetByHash(ValueHash256 hash) - { - return GetBucket(hash).GetByHash(hash); - } - - private IEnumerable<(ValueHash256, TNode)> IterateNeighbour(ValueHash256 hash) - { - int startingDistance = Hash256XorUtils.CalculateDistance(_currentNodeIdAsHash, hash); - foreach (var bucketToGet in EnumerateBucket(startingDistance)) - { - foreach (var entry in bucketToGet.GetAllWithHash()) - { - yield return entry; - } - } - } - - public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bool excludeSelf) - { - using McsLock.Disposable _ = _lock.Acquire(); - - int startingDistance = Hash256XorUtils.CalculateDistance(_currentNodeIdAsHash, hash); - KBucket firstBucket = _buckets[startingDistance]; - bool shouldNotContainExcludedNode = exclude == null || !firstBucket.ContainsNode(exclude.Value); - bool shouldNotContainSelf = excludeSelf == false || !firstBucket.ContainsNode(_currentNodeIdAsHash); - - if (shouldNotContainExcludedNode && shouldNotContainSelf) - { - TNode[] nodes = firstBucket.GetAll(); - if (nodes.Length == _kSize) - { - // Fast path. In theory, most of the time, this would be the taken path, where no array - // concatenation or creation is needed. - return nodes; - } - } - - var iterator = IterateNeighbour(hash); - - if (exclude != null) - iterator = iterator - .Where(kv => kv.Item1 != exclude.Value); - - if (excludeSelf) - iterator = iterator - .Where(kv => kv.Item1 != _currentNodeIdAsHash); - - return iterator.Take(_kSize) - .Select(kv => kv.Item2) - .ToArray(); - } - - private IEnumerable> EnumerateBucket(int startingDistance) - { - // Note, without a tree based routing table, we don't exactly know - // which way (left or right) is the right way to go. So this is all approximate. - // Well, even with a full tree, it would still be approximate, just that it would - // be a bit more accurate. - yield return _buckets[startingDistance]; - int left = startingDistance - 1; - int right = startingDistance + 1; - while (left >= 0 || right <= Hash256XorUtils.MaxDistance) - { - if (left >= 0) - { - yield return _buckets[left]; - } - - if (right <= Hash256XorUtils.MaxDistance) - { - yield return _buckets[right]; - } - - left -= 1; - right += 1; - } - } - - public void LogDebugInfo() - { - _logger.Debug($"Bucket sizes {string.Join(", ", _buckets.Select(b => b.Count))}"); - } - - public event EventHandler? OnNodeAdded; -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs index fdefd224428a..408b761bd831 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs @@ -43,17 +43,7 @@ public static IServiceCollection ConfigureKademliaComponents(this I return provider.GetRequiredService>(); }) .AddSingleton>() - .AddSingleton>() .AddSingleton, NodeHealthTracker>() - .AddSingleton>(provider => - { - KademliaConfig config = provider.GetRequiredService>(); - if (config.UseTreeBasedRoutingTable) - { - return provider.GetRequiredService>(); - } - - return provider.GetRequiredService>(); - }); + .AddSingleton, KBucketTree>(); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index a0e3b90634e6..6914209643a9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -26,11 +26,6 @@ public class KademliaConfig /// public int Beta { get; set; } = 2; - /// - /// Use tree based routing table. False to use fixed array table. - /// - public bool UseTreeBasedRoutingTable { get; set; } = true; - /// /// The interval on which a table refresh is initiated. /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index c4cc8cd6b6bd..e680ca76546d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -28,18 +28,7 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) .AddSingleton, NewaTrackingLookupKNearestNeighbour>() - .AddSingleton>() - .AddSingleton>() - .AddSingleton, NodeHealthTracker>() - .AddSingleton>(provider => - { - KademliaConfig config = provider.Resolve>(); - if (config.UseTreeBasedRoutingTable) - { - return provider.Resolve>(); - } - - return provider.Resolve>(); - }); + .AddSingleton, KBucketTree>() + .AddSingleton, NodeHealthTracker>(); } } From c8240357659ad0fd7db320ec801ac63d00b66ca3 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 14:42:43 +0800 Subject: [PATCH 017/182] Reducing change --- .../DiscoveryApp.cs | 25 +-- .../KademliaDiscv4Adapter.cs} | 10 +- .../Kademlia/Content/KademliaContent.cs | 6 +- .../Kademlia/IKademlia.cs | 18 +- .../Kademlia/ILookupAlgo.cs | 4 +- .../Kademlia/ILookupAlgo2.cs | 8 +- .../Kademlia/IServiceCollectionExtensions.cs | 2 +- .../Kademlia/Kademlia.cs | 24 +-- .../Kademlia/KademliaModule.cs | 4 +- .../Kademlia/NewLookupKNearestNeighbour.cs | 8 +- .../NewTrackingLookupKNearestNeighbour.cs | 160 ++++++++---------- .../OriginalLookupKNearestNeighbour.cs | 8 +- 12 files changed, 125 insertions(+), 152 deletions(-) rename src/Nethermind/Nethermind.Network.Discovery/{KademliaDiscv4MessageSender.cs => Discv4/KademliaDiscv4Adapter.cs} (97%) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 688e077a27ca..b22f68ba47e6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -9,11 +9,9 @@ using Autofac; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; -using Microsoft.ClearScript; using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Attributes; -using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Crypto; @@ -47,9 +45,9 @@ public class DiscoveryApp : IDiscoveryApp private PublicKey _masterNode = null!; private readonly NodeRecord _selfNodeRecorrd; - private KademliaDiscv4MessageSender _discv4MessageSender = null!; + private KademliaDiscv4Adapter _discv4Adapter = null!; private IKademlia _kademlia = null!; - private ILookupAlgo2 _lookup2 = null!; + private ILookupAlgo2 _lookup2 = null!; private NettyDiscoveryHandler? _discoveryHandler; private Task? _storageCommitTask; @@ -158,12 +156,12 @@ public void Initialize(PublicKey masterPublicKey) { CurrentNodeId = new Node(_masterNode, "127.0.0.1", 9999, true) }) - .AddSingleton, KademliaDiscv4MessageSender>() + .AddSingleton, KademliaDiscv4Adapter>() .Build(); _kademlia = _kademliaServices.Resolve>(); - _lookup2 = _kademliaServices.Resolve>(); - _discv4MessageSender = _kademliaServices.Resolve(); + _lookup2 = _kademliaServices.Resolve>(); + _discv4Adapter = _kademliaServices.Resolve(); // TODO: Setup kademlia here } @@ -237,10 +235,10 @@ private void ResetUnreachableStatus(object? sender, NetworkAvailabilityEventArgs public void InitializeChannel(IChannel channel) { - _discoveryHandler = new NettyDiscoveryHandler(_discv4MessageSender, channel, _messageSerializationService, + _discoveryHandler = new NettyDiscoveryHandler(_discv4Adapter, channel, _messageSerializationService, _timestamper, _logManager); - _discv4MessageSender.MsgSender = _discoveryHandler; + _discv4Adapter.MsgSender = _discoveryHandler; _discoveryHandler.OnChannelActivated += OnChannelActivated; channel.Pipeline @@ -337,7 +335,7 @@ private void AddPersistedNodes(CancellationToken cancellationToken) break; } - if (!_discv4MessageSender.NodesFilter.Set(networkNode.HostIp)) + if (!_discv4Adapter.NodesFilter.Set(networkNode.HostIp)) { // Already seen this node ip recently continue; @@ -608,7 +606,10 @@ async Task DiscoverAsync(PublicKey target) bool anyFound = false; int count = 0; - await foreach (var node in _lookup2.Lookup(target, token)) + ValueHash256 targetHash = target.Hash; + Func> lookupOp = (nextNode, token) => + _discv4Adapter.FindNeighbours(nextNode, target, token); + await foreach (var node in _lookup2.Lookup(targetHash, lookupOp!, token)) { anyFound = true; count++; @@ -688,7 +689,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => { try { - await _discv4MessageSender.Ping(node, token); + await _discv4Adapter.Ping(node, token); onlineBootnodes++; _kademlia.AddOrRefresh(node); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 2520fb0be2ec..362d5a941267 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscv4MessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -17,8 +17,8 @@ namespace Nethermind.Network.Discovery; -public class KademliaDiscv4MessageSender( - Lazy> kademlia, // Cyclic dependency +public class KademliaDiscv4Adapter( + Lazy> kademliaMessageReceiver, // Cyclic dependency INetworkConfig networkConfig, KademliaConfig kademliaConfig, NodeRecord selfNodeRecord, @@ -26,7 +26,7 @@ public class KademliaDiscv4MessageSender( ITimestamper timestamper ): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { - private ILogger _logger = logManager.GetClassLogger(); + private ILogger _logger = logManager.GetClassLogger(); private readonly CancellationTokenSource _cts = new(); public IMsgSender? MsgSender { get; set; } @@ -167,7 +167,7 @@ private void HandleFindNode(Node node, FindNodeMsg msg) Task.Run(async () => { PublicKey publicKey = new PublicKey(msg.SearchedNodeId); - Node[] nodes = await kademlia.Value.FindNeighbours(node, publicKey, _cts.Token); + Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _cts.Token); if (nodes.Length > 12) { // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. @@ -182,7 +182,7 @@ private void HandlePing(Node node, PingMsg ping) if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); Task.Run(async () => { - await kademlia.Value.Ping(node, _cts.Token); + await kademliaMessageReceiver.Value.Ping(node, _cts.Token); PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); await SendMessage(node, msg); }); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs index ae841933a144..88234806d10c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs @@ -9,7 +9,8 @@ namespace Nethermind.Network.Discovery.Kademlia.Content; public class KademliaContent( IKademliaContentStore kademliaContentStore, IContentMessageSender contentMessageSender, - ILookupAlgo lookupAlgo, + IContentHashProvider contentHashProvider, + ILookupAlgo lookupAlgo, KademliaConfig config, ILogManager logManager ): IKademliaContent where TNode : notnull @@ -32,8 +33,9 @@ ILogManager logManager try { + ValueHash256 contentHash = contentHashProvider.GetHash(contentKey); await lookupAlgo.Lookup( - contentKey, config.KSize, async (nextNode, token) => + contentHash, config.KSize, async (nextNode, token) => { FindValueResponse valueResponse = await contentMessageSender.FindValue(nextNode, contentKey, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index fc0fce003b61..15522abbd047 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -24,14 +24,6 @@ public interface IKademlia /// void Remove(TNode node); - /// - /// Lookup k nearest neighbour closest to the target hash. This will traverse the network. - /// - /// - /// - /// - Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null); - /// /// Start timers, refresh and such for maintenance of the table. /// @@ -46,7 +38,15 @@ public interface IKademlia Task Bootstrap(CancellationToken token); /// - /// Return the K nearest table entry from hash. This does not traverse the network. The returned array is not + /// Lookup k nearest neighbour closest to the target hash. This will traverse the network. + /// + /// + /// + /// + Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null); + + /// + /// Return the K nearest table entry from target. This does not traverse the network. The returned array is not /// sorted. The routing table may return the exact same array for optimization purpose. /// /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs index e5d7dcbcf272..7315c29ee3c7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Find closest-k is also used to determine which node should store a particular value which is used by /// store RPC (not implemented). /// -public interface ILookupAlgo +public interface ILookupAlgo { /// /// The find neighbour operation here is configurable because the same algorithm is also used for finding @@ -23,7 +23,7 @@ public interface ILookupAlgo /// /// Task Lookup( - TKey target, + ValueHash256 targetHash, int k, Func> findNeighbourOp, CancellationToken token diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs index 85f1dd033f01..37b9e5b3ba0c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs @@ -11,19 +11,21 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Find closest-k is also used to determine which node should store a particular value which is used by /// store RPC (not implemented). /// -public interface ILookupAlgo2 +public interface ILookupAlgo2 { /// /// The find neighbour operation here is configurable because the same algorithm is also used for finding /// value int the network, except that it would short circuit once the value was found. /// /// - /// + /// /// /// /// IAsyncEnumerable Lookup( - TKey target, + ValueHash256 target, + int minResult, + Func> findNeighbourOp, CancellationToken token ); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs index 408b761bd831..80ce515de2fb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ public static IServiceCollection ConfigureKademliaComponents(this I .AddSingleton, KademliaKademliaMessageReceiver>() .AddSingleton>() .AddSingleton>() - .AddSingleton>(provider => + .AddSingleton>(provider => { KademliaConfig config = provider.GetRequiredService>(); if (config.UseNewLookup) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index 55976886f1b9..a45812850c25 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -13,7 +13,7 @@ public class Kademlia : IKademlia where TNode : notnul private readonly INodeHashProvider _nodeHashProvider; private readonly IKeyOperator _keyOperator; private readonly IRoutingTable _routingTable; - private readonly ILookupAlgo _lookupAlgo; + private readonly ILookupAlgo _lookupAlgo; private readonly INodeHealthTracker _nodeHealthTracker; private readonly ILogger _logger; @@ -28,7 +28,7 @@ public Kademlia( IKeyOperator keyOperator, IKademliaMessageSender sender, IRoutingTable routingTable, - ILookupAlgo lookupAlgo, + ILookupAlgo lookupAlgo, ILogManager logManager, INodeHealthTracker nodeHealthTracker, KademliaConfig config) @@ -72,10 +72,10 @@ private bool SameAsSelf(TNode node) return _nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; } - public async Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) + public Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) { - return await LookupNodesClosest( - key, + return _lookupAlgo.Lookup( + _keyOperator.GetKeyHash(key), k ?? _kSize, async (nextNode, token) => { @@ -90,20 +90,6 @@ public async Task LookupNodesClosest(TKey key, CancellationToken token, ); } - private Task LookupNodesClosest( - TKey target, - int k, - Func> findNeighbourOp, - CancellationToken token - ) - { - return _lookupAlgo.Lookup( - target, - k, - findNeighbourOp, - token); - } - public async Task Run(CancellationToken token) { await LookupNodesClosest(_currentNodeIdAsKey, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index e680ca76546d..c4f544c6068f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -17,7 +17,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton, KademliaKademliaMessageReceiver>() .AddSingleton>() .AddSingleton>() - .AddSingleton>(provider => + .AddSingleton>(provider => { KademliaConfig config = provider.Resolve>(); if (config.UseNewLookup) @@ -27,7 +27,7 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) - .AddSingleton, NewaTrackingLookupKNearestNeighbour>() + .AddSingleton, NewaTrackingLookupKNearestNeighbour>() .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs index e91e9e64d138..93814e2f2b1a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs @@ -20,21 +20,20 @@ namespace Nethermind.Network.Discovery.Kademlia; public class NewLookupKNearestNeighbour( IRoutingTable routingTable, INodeHashProvider nodeHashProvider, - IKeyOperator keyOperator, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager): ILookupAlgo where TNode : notnull + ILogManager logManager): ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( - TKey target, + ValueHash256 targetHash, int k, Func> findNeighbourOp, CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); token = cts.Token; @@ -42,7 +41,6 @@ CancellationToken token ConcurrentDictionary queried = new(); ConcurrentDictionary seen = new(); - ValueHash256 targetHash = keyOperator.GetKeyHash(target); IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); IComparer comparerReverse = Comparer.Create((h1, h2) => diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 7672e88aff26..e3ca1785fff2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -12,80 +12,75 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class NewaTrackingLookupKNearestNeighbour( +public class NewaTrackingLookupKNearestNeighbour( IRoutingTable routingTable, INodeHashProvider nodeHashProvider, - IKeyOperator keyOperator, KademliaConfig kademliaConfig, - IKademliaMessageSender kademliaMessageSender, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager) : ILookupAlgo2 where TNode : notnull + ILogManager logManager) : ILookupAlgo2 where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); private readonly ValueHash256 _currentNodeIdAsHash = nodeHashProvider.GetHash(kademliaConfig.CurrentNodeId); - private async Task LookupFunc(TNode nextNode, TKey target, CancellationToken token) - { - if (SameAsSelf(nextNode)) - { - return routingTable.GetKNearestNeighbour(keyOperator.GetKeyHash(target)); - } - return await kademliaMessageSender.FindNeighbours(nextNode, target, token); - } - private bool SameAsSelf(TNode node) { return nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; } public async IAsyncEnumerable Lookup( - TKey target, + ValueHash256 targetHash, + int minResult, + Func> findNeighbourOp, [EnumeratorCancellation] CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using var cts = token.CreateChildTokenSource(); token = cts.Token; - var targetHash = keyOperator.GetKeyHash(target); ConcurrentDictionary queried = new(); ConcurrentDictionary seen = new(); IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); - McsLock queueLock = new McsLock(); - // Ordered by lowest distance. Will get popped for next round. + McsLock queueLock = new McsLock(); PriorityQueue<(ValueHash256, TNode), ValueHash256> queryQueue = new(comparer); + + Channel outChan = Channel.CreateBounded(1); + + // Used for fast worker wake up when queue is empty + TaskCompletionSource roundComplete = new TaskCompletionSource(token); + int queryingTask = 0; + + // Used to determine if the worker should stop ValueHash256 bestNodeId = ValueKeccak.Zero; + int closestNodeRound = 0; + int currentRound = 0; + int totalResult = 0; + bool finished = false; - foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, default)) + // Check internal table first + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, null)) { ValueHash256 nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); queryQueue.Enqueue((nodeHash, node), nodeHash); + yield return node; + if (bestNodeId == ValueKeccak.Zero || comparer.Compare(nodeHash, bestNodeId) < 0) { bestNodeId = nodeHash; } } - Channel outChan = Channel.CreateBounded(1); - - TaskCompletionSource roundComplete = new TaskCompletionSource(token); - int closestNodeRound = 0; - int currentRound = 0; - int queryingTask = 0; - int minResult = 128; - int totalResult = 0; - bool finished = false; - Task[] worker = Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => { + var writer = outChan.Writer; while (!finished) { token.ThrowIfCancellationRequested(); @@ -109,15 +104,52 @@ [EnumeratorCancellation] CancellationToken token { queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); if (_logger.IsTrace) _logger.Trace($"Query {toQuery.Value.node} at round {currentRound}, isself {SameAsSelf(toQuery.Value.node)}"); - (TNode, TNode[]? neighbours) result = await WrappedFindNeighbourOp(toQuery.Value.node); - if (result.neighbours == null || result.neighbours?.Length == 0) + TNode[]? neighbours = await WrappedFindNeighbourOp(toQuery.Value.node); + if (neighbours == null || neighbours?.Length == 0) { if (_logger.IsTrace) _logger.Trace("Empty result"); continue; } - await ProcessResult(toQuery.Value.node, result, currentRound); - if (ShouldStopDueToNoBetterResult(out var round)) + int queryIgnored = 0; + int seenIgnored = 0; + foreach (TNode neighbour in neighbours!) + { + ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + + // Already queried, we ignore + if (queried.ContainsKey(neighbourHash)) + { + queryIgnored++; + continue; + } + + // When seen already dont record + if (!seen.TryAdd(neighbourHash, neighbour)) + { + seenIgnored++; + continue; + } + + Interlocked.Increment(ref minResult); + await writer.WriteAsync(neighbour, cts.Token); + + using McsLock.Disposable _ = queueLock.Acquire(); + bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; + queryQueue.Enqueue((neighbourHash, neighbour), neighbourHash); + + // If found a better node, reset closes node round. + // This causes `ShouldStopDueToNoBetterResult` to return false. + if (closestNodeRound < currentRound && foundBetter) + { + _logger.Warn($"Found better neighbour {neighbour} at round {currentRound}."); + bestNodeId = neighbourHash; + closestNodeRound = currentRound; + } + } + _logger.Warn($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); + + if (ShouldStopDueToNoBetterResult()) { if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); break; @@ -146,7 +178,7 @@ [EnumeratorCancellation] CancellationToken token _logger.Warn("Lookup operation finished."); yield break; - async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourOp(TNode node) + async Task WrappedFindNeighbourOp(TNode node) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); cts.CancelAfter(_findNeighbourHardTimeout); @@ -154,22 +186,21 @@ [EnumeratorCancellation] CancellationToken token try { // targetHash is implied in findNeighbourOp - var ret = await LookupFunc(node, target, cts.Token); + TNode[]? ret = await findNeighbourOp(node, cts.Token); nodeHealthTracker.OnIncomingMessageFrom(node); - return (node, ret); + return ret; } catch (OperationCanceledException) { nodeHealthTracker.OnRequestFailed(node); - return (node, null); + return null; } catch (Exception e) { - if (_logger.IsWarn) _logger.Warn($"Find neighbour op failed. {e}"); nodeHealthTracker.OnRequestFailed(node); if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); - return (node, null); + return null; } } @@ -188,53 +219,9 @@ bool TryGetNodeToQuery([NotNullWhen(true)] out (ValueHash256, TNode)? toQuery) return true; } - async Task ProcessResult(TNode thisNode, (TNode, TNode[]? neighbours)? valueTuple, int round) - { - TNode[]? neighbours = valueTuple?.neighbours; - if (neighbours == null) return; - - var writer = outChan.Writer; - int queryIgnored = 0; - int seenIgnored = 0; - foreach (TNode neighbour in neighbours) - { - ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); - - // Already queried, we ignore - if (queried.ContainsKey(neighbourHash)) - { - queryIgnored++; - continue; - } - - // When seen already dont record - if (!seen.TryAdd(neighbourHash, neighbour)) - { - seenIgnored++; - continue; - } - - Interlocked.Increment(ref minResult); - await writer.WriteAsync(neighbour, cts.Token); - - using var _ = queueLock.Acquire(); - bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; - queryQueue.Enqueue((neighbourHash, neighbour), neighbourHash); - - // If found a better node, reset closes node round and continue - if (closestNodeRound < round && foundBetter) - { - _logger.Warn($"Found better neighbour {neighbour} at round {round}."); - bestNodeId = neighbourHash; - closestNodeRound = round; - } - } - _logger.Warn($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); - } - - bool ShouldStopDueToNoBetterResult(out int round) + bool ShouldStopDueToNoBetterResult() { - round = Interlocked.Increment(ref currentRound); + int round = Interlocked.Increment(ref currentRound); if (totalResult >= minResult && round - closestNodeRound >= (config.Alpha*2)) { // No closer node for more than or equal to _alpha*2 round. @@ -243,7 +230,6 @@ bool ShouldStopDueToNoBetterResult(out int round) // Because there could be currently running work that may increase closestNodeRound. // So including this worker, assume no more if (_logger.IsTrace) _logger.Trace($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); - _logger.Warn($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); return true; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs index f33794a4b2a1..df9a9545cf06 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -12,23 +12,21 @@ namespace Nethermind.Network.Discovery.Kademlia; public class OriginalLookupKNearestNeighbour( IRoutingTable routingTable, INodeHashProvider nodeHashProvider, - IKeyOperator keyOperator, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager): ILookupAlgo where TNode : notnull + ILogManager logManager): ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( - TKey target, + ValueHash256 targetHash, int k, Func> findNeighbourOp, CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {target}"); + if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); - ValueHash256 targetHash = keyOperator.GetKeyHash(target); Func> wrappedFindNeighbourHop = async (node) => { using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); From 8440f68cd50c386d61ac95dd849d0ea7ae3d66c4 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 14:51:56 +0800 Subject: [PATCH 018/182] Fix tests --- .../Kademlia/KademliaSimulation.cs | 20 ++++++++---------- .../Kademlia/KademliaTests.cs | 21 +++---------------- .../DiscoveryApp.cs | 2 +- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 2cf5b39006ee..4306ecce3f0f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -18,17 +18,16 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; -[TestFixture(true, true, 3, 0)] -[TestFixture(false, true, 3, 0)] -[TestFixture(true, false, 3, 0)] -[TestFixture(true, true, 3, 4)] -[TestFixture(true, true, 1, 0)] -[TestFixture(true, true, 1, 4)] +[TestFixture(false, 3, 0)] +[TestFixture(true, 1, 0)] +[TestFixture(true, 1, 4)] +[TestFixture(true, 3, 0)] +[TestFixture(true, 3, 4)] public class KademliaSimulation { private readonly KademliaConfig _config; - public KademliaSimulation(bool useNewLookup, bool useTreeBasedTable, int alpha, int beta) + public KademliaSimulation(bool useNewLookup, int alpha, int beta) { _config = new KademliaConfig() { @@ -36,7 +35,6 @@ public KademliaSimulation(bool useNewLookup, bool useTreeBasedTable, int alpha, Alpha = alpha, Beta = beta, UseNewLookup = useNewLookup, - UseTreeBasedRoutingTable = useTreeBasedTable }; } @@ -284,7 +282,7 @@ public bool TryGetValue(ValueHash256 hash, out ValueHash256 value) } } - private class ValueHashNodeHashProvider: INodeHashProvider, IContentHashProvider + private class ValueHashNodeHashProvider: INodeHashProvider, IContentHashProvider, IKeyOperator { public ValueHash256 GetHash(TestNode node) { @@ -363,7 +361,8 @@ public Kademlia CreateNode(ValueHash256 nodeID) .ConfigureKademliaComponents() .ConfigureKademliaContentComponents() .AddSingleton(new TestLogManager(LogLevel.Error)) - .AddSingleton>(_nodeHashProvider) + .AddSingleton>(_nodeHashProvider) + .AddSingleton>(_nodeHashProvider) .AddSingleton>(_nodeHashProvider) .AddSingleton(new KademliaConfig() { @@ -372,7 +371,6 @@ public Kademlia CreateNode(ValueHash256 nodeID) Alpha = config.Alpha, Beta = config.Beta, RefreshInterval = TimeSpan.FromHours(1), - UseTreeBasedRoutingTable = config.UseTreeBasedRoutingTable, UseNewLookup = config.UseNewLookup }) .AddSingleton>(new OnlySelfIKademliaContentStore(nodeID)) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 17de5fe9dffe..e26cfb02b693 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -15,26 +15,17 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; -[TestFixture(true)] -[TestFixture(false)] public class KademliaTests { private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); - private readonly bool _useTreeBasedBucket; - - public KademliaTests(bool useTreeBasedBucket) - { - _useTreeBasedBucket = useTreeBasedBucket; - } private Kademlia CreateKad(KademliaConfig config) { - config.UseTreeBasedRoutingTable = _useTreeBasedBucket; - return new ServiceCollection() .ConfigureKademliaComponents() .AddSingleton(new TestLogManager(LogLevel.Trace)) - .AddSingleton>(new ValueHashNodeHashProvider()) + .AddSingleton>(new ValueHashNodeHashProvider()) + .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) .AddSingleton>() @@ -132,12 +123,6 @@ public void TestGetKNeighbours() [Test] public async Task TestTooManyNodeWithAcceleratedLookup() { - if (!_useTreeBasedBucket) - { - // Accelerated lookup only supported with tree based bucket - return; - } - _kademliaMessageSender .Ping(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); @@ -175,7 +160,7 @@ public async Task TestTooManyNodeWithAcceleratedLookup() kad.GetAllAtDistance(250).ToHashSet().Should().BeEquivalentTo(testHashes[10..].ToHashSet()); } - private class ValueHashNodeHashProvider: INodeHashProvider + private class ValueHashNodeHashProvider: INodeHashProvider, IKeyOperator { public ValueHash256 GetHash(ValueHash256 node) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index b22f68ba47e6..60c2553729a2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -609,7 +609,7 @@ async Task DiscoverAsync(PublicKey target) ValueHash256 targetHash = target.Hash; Func> lookupOp = (nextNode, token) => _discv4Adapter.FindNeighbours(nextNode, target, token); - await foreach (var node in _lookup2.Lookup(targetHash, lookupOp!, token)) + await foreach (var node in _lookup2.Lookup(targetHash, 128, lookupOp!, token)) { anyFound = true; count++; From 4dfdcd2be1fdeab7ad857b34bdf463fd77588c45 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 15:22:26 +0800 Subject: [PATCH 019/182] Fix tests --- .../Kademlia/KademliaSimulation.cs | 2 +- .../Kademlia/Hash256XORUtils.cs | 6 ++ .../OriginalLookupKNearestNeighbour.cs | 61 ++++++++++--------- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 4306ecce3f0f..b4e31109e5e2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -169,7 +169,7 @@ public async Task SimulateLargeLookupValue() } using CancellationTokenSource cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(10)); + cts.CancelAfter(TimeSpan.FromSeconds(20)); Stopwatch sw = Stopwatch.StartNew(); fabric.SimulateLatency = false; // Bootstrap is so slow, latency simulation is disable for it. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs index 17fcc2670ac6..96f61a54b5cb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs @@ -122,4 +122,10 @@ public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) return new ValueHash256(xorBytes); } + public static ValueHash256 GetOppositeHash(ValueHash256 hash) + { + ValueHash256 opposite = new ValueHash256(); + (~(new Vector(hash.BytesAsSpan))).CopyTo(opposite.BytesAsSpan); + return opposite; + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs index df9a9545cf06..2a71df0ec3d9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics; using Nethermind.Core.Crypto; using Nethermind.Logging; @@ -27,31 +28,6 @@ CancellationToken token ) { if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); - Func> wrappedFindNeighbourHop = async (node) => - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); - cts.CancelAfter(_findNeighbourHardTimeout); - - try - { - // targetHash is implied in findNeighbourOp - var res = await findNeighbourOp(node, cts.Token); - nodeHealthTracker.OnIncomingMessageFrom(node); - return (node, res); - } - catch (OperationCanceledException) - { - nodeHealthTracker.OnRequestFailed(node); - return (node, null); - } - catch (Exception e) - { - nodeHealthTracker.OnRequestFailed(node); - _logger.Error($"Find neighbour op failed. {e}"); - return (node, null); - } - }; - Dictionary queried = new(); Dictionary queriedAndResponded = new(); Dictionary seen = new(); @@ -65,7 +41,7 @@ CancellationToken token // Ordered by lowest distance. Will not get popped for next round, but will at final collection. PriorityQueue bestSeenAllTime = new (comparer); - ValueHash256 closestNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); + ValueHash256 closestNodeHash = Hash256XorUtils.GetOppositeHash(targetHash); (ValueHash256 nodeHash, TNode node)[] roundQuery = routingTable.GetKNearestNeighbour(targetHash, default) .Take(config.Alpha) .Select((node) => (nodeHashProvider.GetHash(node), node)) @@ -79,19 +55,22 @@ CancellationToken token bestSeenAllTime.Enqueue(node, nodeHash); } + int roundNumber = 0; while (roundQuery.Length > 0) { // TODO: The paper mentioned that the next round can start immediately while waiting // for the result of previous round. token.ThrowIfCancellationRequested(); + if (_logger.IsTrace) _logger.Trace($"Round {++roundNumber}"); + foreach (var kv in roundQuery) { queried.TryAdd(kv.nodeHash, kv.node); } (TNode NodeId, TNode[]? Neighbours)[] currentRoundResponse = await Task.WhenAll( - roundQuery.Select((hn) => wrappedFindNeighbourHop(hn.Item2))); + roundQuery.Select((hn) => WrappedFindNeighbourHop(hn.Item2))); bool hasCloserThanClosest = false; foreach ((TNode NodeId, TNode[]? Neighbours) response in currentRoundResponse) @@ -157,7 +136,8 @@ CancellationToken token // TODO: In parallel? // So the paper mentioned that node that it need to query findnode for node that was not queried. - (_, TNode[]? nextCandidate) = await wrappedFindNeighbourHop(nextLowest); + Stopwatch sw = Stopwatch.StartNew(); + (_, TNode[]? nextCandidate) = await WrappedFindNeighbourHop(nextLowest); if (nextCandidate != null) { result.Add(nextLowest); @@ -165,5 +145,30 @@ CancellationToken token } return result.ToArray(); + + async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourHop(TNode node) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + cts.CancelAfter(_findNeighbourHardTimeout); + + try + { + // targetHash is implied in findNeighbourOp + var res = await findNeighbourOp(node, cts.Token); + nodeHealthTracker.OnIncomingMessageFrom(node); + return (node, res); + } + catch (OperationCanceledException) + { + nodeHealthTracker.OnRequestFailed(node); + return (node, null); + } + catch (Exception e) + { + nodeHealthTracker.OnRequestFailed(node); + _logger.Error($"Find neighbour op failed. {e}"); + return (node, null); + } + } } } From 7929753bb667d1ed1c3bd92e05079ddef38afe87 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 15:30:01 +0800 Subject: [PATCH 020/182] Slight cleanup --- .../Nethermind.Network.Discovery/DiscoveryApp.cs | 4 ++-- .../Kademlia/{ILookupAlgo2.cs => IITeratorAlgo.cs} | 10 +++++----- .../Kademlia/IServiceCollectionExtensions.cs | 4 ++-- .../Kademlia/KademliaModule.cs | 6 +++--- ...KNearestNeighbour.cs => LookupKNearestNeighbour.cs} | 4 ++-- .../Kademlia/NewTrackingLookupKNearestNeighbour.cs | 2 +- .../Kademlia/OriginalLookupKNearestNeighbour.cs | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) rename src/Nethermind/Nethermind.Network.Discovery/Kademlia/{ILookupAlgo2.cs => IITeratorAlgo.cs} (68%) rename src/Nethermind/Nethermind.Network.Discovery/Kademlia/{NewLookupKNearestNeighbour.cs => LookupKNearestNeighbour.cs} (98%) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 60c2553729a2..320e64b13b73 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -47,7 +47,7 @@ public class DiscoveryApp : IDiscoveryApp private KademliaDiscv4Adapter _discv4Adapter = null!; private IKademlia _kademlia = null!; - private ILookupAlgo2 _lookup2 = null!; + private IITeratorAlgo _lookup2 = null!; private NettyDiscoveryHandler? _discoveryHandler; private Task? _storageCommitTask; @@ -160,7 +160,7 @@ public void Initialize(PublicKey masterPublicKey) .Build(); _kademlia = _kademliaServices.Resolve>(); - _lookup2 = _kademliaServices.Resolve>(); + _lookup2 = _kademliaServices.Resolve>(); _discv4Adapter = _kademliaServices.Resolve(); // TODO: Setup kademlia here diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs similarity index 68% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs index 37b9e5b3ba0c..6efecc180274 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo2.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs @@ -6,12 +6,12 @@ namespace Nethermind.Network.Discovery.Kademlia; /// -/// Main find closest-k node within the network. See the kademlia paper, 2.3. -/// Since find value is basically the same also just with a shortcut, this allow changing the find neighbour op. -/// Find closest-k is also used to determine which node should store a particular value which is used by -/// store RPC (not implemented). +/// Iterate nodes round a target. Returns `IAsyncEnumerable` of nodes that it encounters along the way. +/// This mean that the returned value is not in order and may not be the closest to the target, but it try to +/// eventually get there. Additionally, the returned `TNode` may not be online unlike the standard algo which +/// requires it to be online. /// -public interface ILookupAlgo2 +public interface IITeratorAlgo { /// /// The find neighbour operation here is configurable because the same algorithm is also used for finding diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs index 80ce515de2fb..d081a5f46d30 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs @@ -30,14 +30,14 @@ public static IServiceCollection ConfigureKademliaComponents(this I return collection .AddSingleton, Kademlia>() .AddSingleton, KademliaKademliaMessageReceiver>() - .AddSingleton>() + .AddSingleton>() .AddSingleton>() .AddSingleton>(provider => { KademliaConfig config = provider.GetRequiredService>(); if (config.UseNewLookup) { - return provider.GetRequiredService>(); + return provider.GetRequiredService>(); } return provider.GetRequiredService>(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index c4f544c6068f..d192379771b6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -15,19 +15,19 @@ protected override void Load(ContainerBuilder builder) builder .AddSingleton, Kademlia>() .AddSingleton, KademliaKademliaMessageReceiver>() - .AddSingleton>() + .AddSingleton>() .AddSingleton>() .AddSingleton>(provider => { KademliaConfig config = provider.Resolve>(); if (config.UseNewLookup) { - return provider.Resolve>(); + return provider.Resolve>(); } return provider.Resolve>(); }) - .AddSingleton, NewaTrackingLookupKNearestNeighbour>() + .AddSingleton, NewaTrackingLookupKNearestNeighbour>() .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs index 93814e2f2b1a..fe2ac0949100 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs @@ -17,7 +17,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// earlier as it converge to the content faster, but take more query for findnodes due to a more strict stop /// condition. /// -public class NewLookupKNearestNeighbour( +public class LookupKNearestNeighbour( IRoutingTable routingTable, INodeHashProvider nodeHashProvider, INodeHealthTracker nodeHealthTracker, @@ -25,7 +25,7 @@ public class NewLookupKNearestNeighbour( ILogManager logManager): ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( ValueHash256 targetHash, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index e3ca1785fff2..1450ba5398a6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -18,7 +18,7 @@ public class NewaTrackingLookupKNearestNeighbour( KademliaConfig kademliaConfig, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager) : ILookupAlgo2 where TNode : notnull + ILogManager logManager) : IITeratorAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; private readonly ILogger _logger = logManager.GetClassLogger>(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs index 2a71df0ec3d9..f3267f0c7e54 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -18,7 +18,7 @@ public class OriginalLookupKNearestNeighbour( ILogManager logManager): ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( ValueHash256 targetHash, From 26e48ef31db50cde0712bd59f58eb40be4629499 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 19:40:30 +0800 Subject: [PATCH 021/182] Simplification --- .../Nethermind.Init/Modules/NetworkModule.cs | 4 + .../CompositeDiscoveryApp.cs | 40 +- .../DiscoveryApp.cs | 536 +++--------------- .../Discv4/DiscV4KademliaModule.cs | 58 ++ .../Discv4/KademliaNodeSource.cs | 134 +++++ .../Kademlia/IKademlia.cs | 6 + .../Kademlia/Kademlia.cs | 35 +- .../Kademlia/KademliaConfig.cs | 5 + .../NettyDiscoveryHandler.cs | 3 +- .../Nethermind.Network.Enr/EnrContentKey.cs | 2 + .../Nethermind.Network.Enr/NodeRecord.cs | 2 +- 11 files changed, 328 insertions(+), 497 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs diff --git a/src/Nethermind/Nethermind.Init/Modules/NetworkModule.cs b/src/Nethermind/Nethermind.Init/Modules/NetworkModule.cs index 92001d714c7e..0cd861c35d52 100644 --- a/src/Nethermind/Nethermind.Init/Modules/NetworkModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/NetworkModule.cs @@ -7,7 +7,9 @@ using Nethermind.Core; using Nethermind.Core.Timers; using Nethermind.Logging; +using Nethermind.Network; using Nethermind.Network.Config; +using Nethermind.Network.Enr; using Nethermind.Stats; namespace Nethermind.Init.Modules; @@ -26,6 +28,8 @@ protected override void Load(ContainerBuilder builder) ctx.Resolve() .MaxCandidatePeerCount)) // The INetworkConfig is not referable in NodeStatsManager. + .AddSingleton() + ; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index cdcaca86c9e3..eacdae57afad 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -3,11 +3,13 @@ using System.Net.Sockets; using System.Runtime.InteropServices; +using Autofac; using Autofac.Features.AttributeFilters; using DotNetty.Transport.Bootstrapping; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; using Nethermind.Api; +using Nethermind.Blockchain; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Crypto; @@ -49,16 +51,19 @@ public class CompositeDiscoveryApp : IDiscoveryApp private IDiscoveryApp? _v4; private IDiscoveryApp? _v5; private INodeSource _compositeNodeSource = null!; + private readonly ILifetimeScope _rootLiffetimeScope; public CompositeDiscoveryApp( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey? nodeKey, + ILifetimeScope rootLifetimeScope, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IInitConfig initConfig, IEthereumEcdsa? ethereumEcdsa, IMessageSerializationService? serializationService, ILogManager? logManager, ITimestamper? timestamper, ICryptoRandom? cryptoRandom, INodeStatsManager? nodeStatsManager, IIPResolver? ipResolver, IChannelFactory? channelFactory = null ) { + _rootLiffetimeScope = rootLifetimeScope; _nodeKey = nodeKey ?? throw new ArgumentNullException(nameof(nodeKey)); _networkConfig = networkConfig; _discoveryConfig = discoveryConfig; @@ -150,20 +155,6 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator msgSerializersProvider.RegisterDiscoverySerializers(); - NodeDistanceCalculator nodeDistanceCalculator = new(discoveryConfig); - - NodeTable nodeTable = new(nodeDistanceCalculator, discoveryConfig, _networkConfig, _logManager); - EvictionManager evictionManager = new(nodeTable, _logManager); - - NodeLifecycleManagerFactory nodeLifeCycleFactory = new( - nodeTable, - evictionManager, - _nodeStatsManager, - selfNodeRecord, - discoveryConfig, - _timestamper, - _logManager); - // ToDo: DiscoveryDB is registered outside dbProvider - bad SimpleFilePublicKeyDb discoveryDb = new( "DiscoveryDB", @@ -174,28 +165,11 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator discoveryDb, _logManager); - DiscoveryManager discoveryManager = new( - nodeLifeCycleFactory, - nodeTable, - discoveryStorage, - discoveryConfig, - _networkConfig, - _logManager - ); - - NodesLocator nodesLocator = new( - nodeTable, - discoveryManager, - discoveryConfig, - _logManager); - _v4 = new DiscoveryApp( selfNodeRecord, - nodesLocator, - discoveryManager, - nodeTable, + _rootLiffetimeScope, + _nodeStatsManager, _serializationService, - _cryptoRandom, discoveryStorage, _networkConfig, discoveryConfig, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 320e64b13b73..fd2a74aa0512 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -1,11 +1,6 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Net.NetworkInformation; -using System.Runtime.CompilerServices; -using System.Threading.Channels; using Autofac; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; @@ -13,15 +8,12 @@ using Nethermind.Core; using Nethermind.Core.Attributes; using Nethermind.Core.Crypto; -using Nethermind.Core.Extensions; -using Nethermind.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Kademlia; -using Nethermind.Network.Discovery.RoutingTable; using Nethermind.Network.Enr; +using Nethermind.Stats; using Nethermind.Stats.Model; -using Prometheus; using LogLevel = DotNetty.Handlers.Logging.LogLevel; namespace Nethermind.Network.Discovery; @@ -30,35 +22,32 @@ public class DiscoveryApp : IDiscoveryApp { private readonly IDiscoveryConfig _discoveryConfig; private readonly ITimestamper _timestamper; - // private readonly INodesLocator _nodesLocator; - // private readonly IDiscoveryManager _discoveryManager; - // private readonly INodeTable _nodeTable; private readonly ILogManager _logManager; private readonly ILogger _logger; private readonly IMessageSerializationService _messageSerializationService; - // private readonly ICryptoRandom _cryptoRandom; private readonly INetworkStorage _discoveryStorage; private readonly INetworkConfig _networkConfig; - private IContainer? _kademliaServices; + private readonly INodeStatsManager _nodeStatsManager; + private ILifetimeScope? _kademliaServices; - private readonly IList _bootNodes; + private readonly List _bootNodes; private PublicKey _masterNode = null!; private readonly NodeRecord _selfNodeRecorrd; private KademliaDiscv4Adapter _discv4Adapter = null!; private IKademlia _kademlia = null!; - private IITeratorAlgo _lookup2 = null!; private NettyDiscoveryHandler? _discoveryHandler; - private Task? _storageCommitTask; + + private readonly ILifetimeScope _rootLifetimeScope; + private KademliaNodeSource _kademliaNodeSource = null!; + private Task? _runningTask; public DiscoveryApp( NodeRecord selfNodeRecord, - INodesLocator nodesLocator, - IDiscoveryManager? discoveryManager, - INodeTable? nodeTable, + ILifetimeScope lifetimeScope, + INodeStatsManager nodeStatsManager, IMessageSerializationService? msgSerializationService, - ICryptoRandom? cryptoRandom, INetworkStorage? discoveryStorage, INetworkConfig? networkConfig, IDiscoveryConfig? discoveryConfig, @@ -66,21 +55,18 @@ public DiscoveryApp( ILogManager? logManager) { _selfNodeRecorrd = selfNodeRecord; + _rootLifetimeScope = lifetimeScope; + _nodeStatsManager = nodeStatsManager; _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); _logger = _logManager.GetClassLogger(); _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - // _nodesLocator = nodesLocator ?? throw new ArgumentNullException(nameof(nodesLocator)); - // _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); - // _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); _messageSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); - // _cryptoRandom = cryptoRandom ?? throw new ArgumentNullException(nameof(cryptoRandom)); _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); - _bootNodes = new List(); - _discoveryStorage.StartBatch(); + _bootNodes = new List(); NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); // NetworkNode[] bootnodes = NetworkNode.ParseNodes("enode://8cd847302089d4906c5eb3125770b067fbcb7dc6bd62dfd3517483cc2e6acae6141a5fb4061f76825ea9f585d157b625f84f976fb6aa1582dc87b0d0b652f51f@127.0.0.1:40404", _logger); if (bootnodes.Length == 0) @@ -101,67 +87,16 @@ public DiscoveryApp( } } - private class NodeNodeHashProvider : INodeHashProvider, IKeyOperator - { - public ValueHash256 GetHash(Node node) - { - return node.Id.Hash; - } - - public PublicKey GetKey(Node node) - { - return node.Id; - } - - public ValueHash256 GetKeyHash(PublicKey key) - { - return key.Hash; - } - - public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) - { - // Obviously, we can't generate this. So we just randomly pick something. - // I guess we can brute force it if needed. - Span randomBytes = new byte[64]; - Random.Shared.NextBytes(randomBytes); - return new PublicKey(randomBytes); - } - } - public void Initialize(PublicKey masterPublicKey) { - // _discoveryManager.NodeDiscovered += OnNodeDiscovered; _masterNode = masterPublicKey; - /* - _nodeTable.Initialize(masterPublicKey); - if (_nodeTable.MasterNode is null) - { - throw new NetworkingException( - "Discovery node table initialization failed - master node is null", - NetworkExceptionType.Discovery); - } - - _nodesLocator.Initialize(_nodeTable.MasterNode); - */ - - _kademliaServices = new ContainerBuilder() - .AddModule(new KademliaModule()) - .AddSingleton, NodeNodeHashProvider>() - .AddSingleton, NodeNodeHashProvider>() - .AddSingleton(_timestamper) - .AddSingleton(_networkConfig) - .AddSingleton(_logManager) - .AddSingleton(_selfNodeRecorrd) - .AddSingleton>(new KademliaConfig() - { - CurrentNodeId = new Node(_masterNode, "127.0.0.1", 9999, true) - }) - .AddSingleton, KademliaDiscv4Adapter>() - .Build(); + _kademliaServices = _rootLifetimeScope + .BeginLifetimeScope((builder) => builder.AddModule( + new DiscV4KademliaModule(_selfNodeRecorrd, _masterNode, _bootNodes))); _kademlia = _kademliaServices.Resolve>(); - _lookup2 = _kademliaServices.Resolve>(); _discv4Adapter = _kademliaServices.Resolve(); + _kademliaNodeSource = _kademliaServices.Resolve(); // TODO: Setup kademlia here } @@ -182,25 +117,35 @@ public Task StartAsync() public async Task StopAsync() { - if (_logger.IsDebug) _logger.Debug("Stopping discovery timer"); - if (_logger.IsDebug) _logger.Debug("Stopping discovery persistence timer"); - - _appShutdownSource.Cancel(); + try + { + if (_runningTask is not null) + { + await _runningTask; + } + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error("Error in discovery task", e); + } - if (_storageCommitTask is not null) + try { - await _storageCommitTask.ContinueWith(x => + if (_discoveryHandler is not null) { - if (x.IsFailedButNotCanceled()) - { - if (_logger.IsError) _logger.Error("Error during discovery persistence stop.", x.Exception); - } - }); + _discoveryHandler.OnChannelActivated -= OnChannelActivated; + } + } + catch (Exception e) + { + _logger.Error("Error during discovery cleanup", e); } - Cleanup(); + _appShutdownSource.Cancel(); if (_logger.IsInfo) _logger.Info("Discovery shutdown complete.. please wait for all components to close"); - _kademliaServices?.DisposeAsync(); } @@ -214,31 +159,14 @@ private void Initialize() if (_logger.IsDebug) _logger.Debug($"Discovery : udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); ThisNodeInfo.AddInfo("Discovery :", $"udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); - - NetworkChange.NetworkAvailabilityChanged += ResetUnreachableStatus; - } - - private void ResetUnreachableStatus(object? sender, NetworkAvailabilityEventArgs e) - { - /* - if (!e.IsAvailable) - { - return; - } - - foreach (INodeLifecycleManager unreachable in _discoveryManager.GetNodeLifecycleManagers().Where(static x => x.State == NodeLifecycleState.Unreachable)) - { - unreachable.ResetUnreachableStatus(); - } - */ } public void InitializeChannel(IChannel channel) { _discoveryHandler = new NettyDiscoveryHandler(_discv4Adapter, channel, _messageSerializationService, _timestamper, _logManager); - _discv4Adapter.MsgSender = _discoveryHandler; + _discoveryHandler.OnChannelActivated += OnChannelActivated; channel.Pipeline @@ -255,7 +183,7 @@ private void OnChannelActivated(object? sender, EventArgs e) // Make sure this is non blocking code, otherwise netty will not process messages // Explicitly use TaskScheduler.Default, otherwise it will use dotnetty's task scheduler which have a habit of // not working sometimes. - Task.Factory + _runningTask = Task.Factory .StartNew(() => OnChannelActivated(_appShutdownSource.Token), _appShutdownSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) .ContinueWith ( @@ -281,43 +209,24 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) { try { - //Step 1 - read nodes and stats from db - AddPersistedNodes(cancellationToken); - - //Step 2 - initialize bootnodes - if (_logger.IsDebug) _logger.Debug("Initializing bootnodes."); - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } + // Step 1 - read nodes and stats from db + await AddPersistedNodes(cancellationToken); - if (await InitializeBootnodes(cancellationToken)) - { - break; - } + Task persistenceTask = RunDiscoveryPersistenceCommit(cancellationToken); - //Check if we were able to communicate with any trusted nodes or persisted nodes - //if so no need to replay bootstrapping, we can start discovery process - /* - if (_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count != 0) - { - break; - } - */ - - _logger.Warn("Could not communicate with any nodes (bootnodes, trusted nodes, persisted nodes)."); - await Task.Delay(1000, cancellationToken); + try + { + // Step 2 - run the standard kademlia routine + await _kademlia.Run(cancellationToken); } - - if (cancellationToken.IsCancellationRequested) + finally { - return; + // Block until persistence is finished + await persistenceTask; } - - InitializeDiscoveryPersistenceTimer(); - InitializeDiscoveryTimer(); + } + catch (OperationCanceledException) + { } catch (Exception e) { @@ -325,7 +234,7 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) } } - private void AddPersistedNodes(CancellationToken cancellationToken) + private async Task AddPersistedNodes(CancellationToken cancellationToken) { NetworkNode[] nodes = _discoveryStorage.GetPersistedNodes(); foreach (NetworkNode networkNode in nodes) @@ -354,21 +263,22 @@ private void AddPersistedNodes(CancellationToken cancellationToken) continue; } - /* - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node, true); - if (manager is null) + try + { + await _discv4Adapter.Ping(node, cancellationToken); + } + catch (OperationCanceledException) + { + continue; + } + catch (Exception) { if (_logger.IsDebug) - { - _logger.Debug( - $"Skipping persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}, manager couldn't be created"); - } - + _logger.Error( + $"ERROR/DEBUG error when pinging persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); continue; } - manager.NodeStats.CurrentPersistedNodeReputation = networkNode.Reputation; - */ if (_logger.IsTrace) _logger.Trace($"Adding persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); } @@ -376,173 +286,10 @@ private void AddPersistedNodes(CancellationToken cancellationToken) if (_logger.IsDebug) _logger.Debug($"Added persisted discovery nodes: {nodes.Length}"); } - private void InitializeDiscoveryTimer() - { - if (_logger.IsDebug) _logger.Debug("Starting discovery timer"); - _ = RunDiscoveryProcess(); - } - - private void InitializeDiscoveryPersistenceTimer() - { - if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); - _storageCommitTask = RunDiscoveryPersistenceCommit(); - } - - private void Cleanup() - { - try - { - if (_discoveryHandler is not null) - { - _discoveryHandler.OnChannelActivated -= OnChannelActivated; - } - - NetworkChange.NetworkAvailabilityChanged -= ResetUnreachableStatus; - } - catch (Exception e) - { - _logger.Error("Error during discovery cleanup", e); - } - } - - private async Task InitializeBootnodes(CancellationToken cancellationToken) - { - /* - foreach (var bootNode in _bootNodes) - { - _kademlia.AddOrRefresh(bootNode); - } - */ - - //Wait for pong message to come back from Boot nodes - /* - int maxWaitTime = _discoveryConfig.BootnodePongTimeout; - int itemTime = maxWaitTime / 100; - for (int i = 0; i < 100; i++) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } - - if (managers.Any(static x => x.State == NodeLifecycleState.Active)) - { - break; - } - - if (_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count != 0) - { - if (_logger.IsTrace) - _logger.Trace( - "Was not able to connect to any of the bootnodes, but successfully connected to at least one persisted node."); - break; - } - - if (_logger.IsTrace) _logger.Trace($"Waiting {itemTime} ms for bootnodes to respond"); - - try - { - await Task.Delay(itemTime, cancellationToken); - } - catch (OperationCanceledException) - { - break; - } - } - - int reachedNodeCounter = 0; - for (int i = 0; i < managers.Count; i++) - { - INodeLifecycleManager manager = managers[i]; - if (manager.State != NodeLifecycleState.Active) - { - if (_logger.IsTrace) - _logger.Trace($"Could not reach bootnode: {manager.ManagedNode.Host}:{manager.ManagedNode.Port}"); - } - else - { - if (_logger.IsTrace) - _logger.Trace($"Reached bootnode: {manager.ManagedNode.Host}:{manager.ManagedNode.Port}"); - reachedNodeCounter++; - } - } - */ - - /* - if (_logger.IsInfo) - _logger.Info( - $"Connected to {reachedNodeCounter} bootnodes, {_discoveryManager.GetOrAddNodeLifecycleManagers(static x => x.State == NodeLifecycleState.Active).Count} trusted/persisted nodes"); - return reachedNodeCounter > 0; - */ - - await _kademlia.Bootstrap(cancellationToken); - return true; - } - - private Task RunDiscoveryProcess() - { - return Task.CompletedTask; - /* - byte[] randomId = new byte[64]; - CancellationToken cancellationToken = _appShutdownSource.Token; - PeriodicTimer timer = new(TimeSpan.FromMilliseconds(10)); - - long lastTickMs = Environment.TickCount64; - long waitTimeTimeMs = 10; - while (!cancellationToken.IsCancellationRequested - && await timer.WaitForNextTickAsync(cancellationToken)) - { - long currentTickMs = Environment.TickCount64; - long elapsedMs = currentTickMs - lastTickMs; - if (elapsedMs < waitTimeTimeMs) - { - // TODO: Change timer time in .NET 8.0 to avoid this https://github.com/dotnet/runtime/pull/82560 - // Wait for the remaining time - await Task.Delay((int)(waitTimeTimeMs - elapsedMs), cancellationToken); - } - - try - { - if (_logger.IsTrace) _logger.Trace("Running discovery process."); - - await _nodesLocator.LocateNodesAsync(cancellationToken); - } - catch (Exception e) - { - _logger.Error($"Error during discovery process: {e}"); - } - - try - { - if (_logger.IsTrace) _logger.Trace("Running refresh process."); - - _cryptoRandom.GenerateRandomBytes(randomId); - await _nodesLocator.LocateNodesAsync(randomId, cancellationToken); - } - catch (Exception e) - { - _logger.Error($"Error during discovery refresh process: {e}"); - } - - int nodesCountAfterDiscovery = _nodeTable.Buckets.Sum(static x => x.BondedItemsCount); - waitTimeTimeMs = - nodesCountAfterDiscovery < 16 - ? 10 - : nodesCountAfterDiscovery < 128 - ? 100 - : nodesCountAfterDiscovery < 256 - ? 1000 - : _discoveryConfig.DiscoveryInterval; - - lastTickMs = Environment.TickCount64; - } - */ - } - [Todo(Improve.Allocations, "Remove ToArray here - address as a part of the network DB rewrite")] - private async Task RunDiscoveryPersistenceCommit() + private async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationToken) { - CancellationToken cancellationToken = _appShutdownSource.Token; + if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_discoveryConfig.DiscoveryPersistenceInterval)); while (!cancellationToken.IsCancellationRequested @@ -550,22 +297,13 @@ private async Task RunDiscoveryPersistenceCommit() { try { - /* - IReadOnlyCollection managers = _discoveryManager.GetNodeLifecycleManagers(); - DateTime utcNow = DateTime.UtcNow; - //we need to update all notes to update reputation - _discoveryStorage.UpdateNodes(managers.Select(x => new NetworkNode(x.ManagedNode.Id, x.ManagedNode.Host, - x.ManagedNode.Port, x.NodeStats.NewPersistedNodeReputation(utcNow))).ToArray()); - - if (!_discoveryStorage.AnyPendingChange()) - { - if (_logger.IsTrace) _logger.Trace("No changes in discovery storage, skipping commit."); - continue; - } + _discoveryStorage.StartBatch(); + + var nodes = _kademlia.IterateNodes().ToArray(); + _discoveryStorage.UpdateNodes(nodes.Select(x => new NetworkNode(x.Id, x.Host, + x.Port, _nodeStatsManager.GetNewPersistedReputation(x))).ToArray()); _discoveryStorage.Commit(); - _discoveryStorage.StartBatch(); - */ } catch (Exception ex) { @@ -574,131 +312,9 @@ private async Task RunDiscoveryPersistenceCommit() } } - /* - private void OnNodeDiscovered(object? sender, NodeEventArgs e) - { - NodeAdded?.Invoke(this, e); - } - - private event EventHandler? NodeAdded; - */ - - private Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_size", "kad size"); - private Counter _kademliaDiscoveredNodes = Prometheus.Metrics.CreateCounter("kademlia_discovered_nodes", "Discovered"); - - public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) + public IAsyncEnumerable DiscoverNodes(CancellationToken token) { - if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); - Channel ch = Channel.CreateBounded(64); - ConcurrentDictionary _writtenNodes = new(); - int duplicated = 0; - int total = 0; - - void handler(object? _, Node addedNode) - { - // _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); - // ch.Writer.TryWrite(addedNode); - } - - async Task DiscoverAsync(PublicKey target) - { - if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); - bool anyFound = false; - int count = 0; - - ValueHash256 targetHash = target.Hash; - Func> lookupOp = (nextNode, token) => - _discv4Adapter.FindNeighbours(nextNode, target, token); - await foreach (var node in _lookup2.Lookup(targetHash, 128, lookupOp!, token)) - { - anyFound = true; - count++; - total++; - if (!_writtenNodes.TryAdd(node.IdHash, node.IdHash)) - { - duplicated++; - continue; - } - _kademliaDiscoveredNodes.Inc(); - await ch.Writer.WriteAsync(node, token); - } - - if (!anyFound) - { - if (_logger.IsDebug) _logger.Debug($"No node found for {target}"); - } - else - { - if (_logger.IsDebug) _logger.Debug($"Found {count} nodes"); - } - } - - Task discoverTask = Task.WhenAll(Enumerable.Range(0, 6).Select((_) => Task.Run(async () => - { - Random random = new(); - byte[] randomBytes = new byte[64]; - int iterationCount = 0; - while (!token.IsCancellationRequested) - { - Stopwatch iterationTime = Stopwatch.StartNew(); - if (iterationCount % 10 == 0) - { - // Probably shnould be done once or in a few interval - await EnsureBootNodes(token); - } - - try - { - random.NextBytes(randomBytes); - await DiscoverAsync(new PublicKey(randomBytes)); - } - catch (Exception ex) - { - if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); - } - - // Prevent high CPU when all node is not reachable due to network connectivity issue. - if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) - { - await Task.Delay(TimeSpan.FromSeconds(1), token); - } - } - }))); - - try - { - _kademlia.OnNodeAdded += handler; - - await foreach (Node node in ch.Reader.ReadAllAsync(token)) - { - yield return node; - } - } - finally - { - await discoverTask; - _kademlia.OnNodeAdded -= handler; - } - } - - private async Task EnsureBootNodes(CancellationToken token) - { - Stopwatch sw = Stopwatch.StartNew(); - int onlineBootnodes = 0; - await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => - { - try - { - await _discv4Adapter.Ping(node, token); - onlineBootnodes++; - _kademlia.AddOrRefresh(node); - } - catch (OperationCanceledException) - { - } - }); - - _logger.Info($"Ensure bootnodes took {sw.Elapsed}. {onlineBootnodes} out of {_bootNodes.Count} online"); + return _kademliaNodeSource.DiscoverNodes(token); } public event EventHandler? NodeRemoved { add { } remove { } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs new file mode 100644 index 000000000000..4585ef045648 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Enr; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery; + +public class DiscV4KademliaModule(NodeRecord selfNodeRecord, PublicKey masterNode, IReadOnlyList bootNodes): Module +{ + protected override void Load(ContainerBuilder builder) + { + builder + .AddModule(new KademliaModule()) + .AddSingleton, NodeNodeHashProvider>() + .AddSingleton, NodeNodeHashProvider>() + .AddSingleton(selfNodeRecord) + .AddSingleton() + .AddSingleton(new KademliaConfig() + { + CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), + BootNodes = bootNodes + }) + .AddSingleton, KademliaDiscv4Adapter>(); + } +} + +public class NodeNodeHashProvider : INodeHashProvider, IKeyOperator +{ + public ValueHash256 GetHash(Node node) + { + return node.Id.Hash; + } + + public PublicKey GetKey(Node node) + { + return node.Id; + } + + public ValueHash256 GetKeyHash(PublicKey key) + { + return key.Hash; + } + + public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) + { + // Obviously, we can't generate this. So we just randomly pick something. + // I guess we can brute force it if needed. + Span randomBytes = new byte[64]; + Random.Shared.NextBytes(randomBytes); + return new PublicKey(randomBytes); + } +} + diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs new file mode 100644 index 000000000000..a25eb8f8a05c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Stats.Model; +using Prometheus; + +namespace Nethermind.Network.Discovery; + +public class KademliaNodeSource( + IKademlia kademlia, + IITeratorAlgo _lookup2, + KademliaDiscv4Adapter discv4Adapter, + ILogManager logManager +) +{ + ILogger _logger = logManager.GetClassLogger(); + + private Counter _kademliaDiscoveredNodes = Prometheus.Metrics.CreateCounter("kademlia_discovered_nodes", "Discovered"); + private Counter _kademliaDiscoveredNodeStatus = Prometheus.Metrics.CreateCounter("kademlia_discovered_nodes_status", "Discovered", "status"); + + public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) + { + if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); + Channel ch = Channel.CreateBounded(64); + ConcurrentDictionary _writtenNodes = new(); + int duplicated = 0; + int total = 0; + + void handler(object? _, Node addedNode) + { + _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); + ch.Writer.TryWrite(addedNode); + } + + async Task DiscoverAsync(PublicKey target) + { + if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); + bool anyFound = false; + int count = 0; + + ValueHash256 targetHash = target.Hash; + Func> lookupOp = (nextNode, token) => + discv4Adapter.FindNeighbours(nextNode, target, token); + await foreach (var node in _lookup2.Lookup(targetHash, 128, lookupOp!, token)) + { + try + { + await discv4Adapter.Ping(node, token); + } + catch (OperationCanceledException) + { + _kademliaDiscoveredNodeStatus.WithLabels("ping_timeout").Inc(); + continue; + } + + _kademliaDiscoveredNodeStatus.WithLabels("ok").Inc(); + anyFound = true; + count++; + total++; + if (!_writtenNodes.TryAdd(node.IdHash, node.IdHash)) + { + duplicated++; + continue; + } + _kademliaDiscoveredNodes.Inc(); + await ch.Writer.WriteAsync(node, token); + } + + _logger.Warn($"Round found {count} nodes. Total is {total}"); + if (!anyFound) + { + if (_logger.IsDebug) _logger.Debug($"No node found for {target}"); + } + else + { + if (_logger.IsDebug) _logger.Debug($"Found {count} nodes"); + } + } + + Task discoverTask = Task.WhenAll(Enumerable.Range(0, 6).Select((_) => Task.Run(async () => + { + Random random = new(); + byte[] randomBytes = new byte[64]; + int iterationCount = 0; + while (!token.IsCancellationRequested) + { + Stopwatch iterationTime = Stopwatch.StartNew(); + if (iterationCount % 10 == 0) + { + // Probably shnould be done once or in a few interval + // await EnsureBootNodes(token); + } + + try + { + random.NextBytes(randomBytes); + await DiscoverAsync(new PublicKey(randomBytes)); + } + catch (Exception ex) + { + if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); + } + + // Prevent high CPU when all node is not reachable due to network connectivity issue. + if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) + { + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + } + }))); + + try + { + kademlia.OnNodeAdded += handler; + + await foreach (Node node in ch.Reader.ReadAllAsync(token)) + { + yield return node; + } + } + finally + { + await discoverTask; + kademlia.OnNodeAdded -= handler; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index 15522abbd047..1e817bb8462e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -58,4 +58,10 @@ public interface IKademlia /// Called when a TNode is added to the routing table. /// event EventHandler OnNodeAdded; + + /// + /// Iterate all nodes with no ordering + /// + /// + IEnumerable IterateNodes(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index a45812850c25..144b3828b9c6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -22,6 +22,7 @@ public class Kademlia : IKademlia where TNode : notnul private readonly ValueHash256 _currentNodeIdAsHash; private readonly int _kSize; private readonly TimeSpan _refreshInterval; + private readonly IReadOnlyList _bootNodes; public Kademlia( INodeHashProvider nodeHashProvider, @@ -46,6 +47,7 @@ public Kademlia( _currentNodeIdAsHash = _nodeHashProvider.GetHash(_currentNodeId); _kSize = config.KSize; _refreshInterval = config.RefreshInterval; + _bootNodes = config.BootNodes; AddOrRefresh(_currentNodeId); } @@ -92,8 +94,6 @@ public Task LookupNodesClosest(TKey key, CancellationToken token, int? public async Task Run(CancellationToken token) { - await LookupNodesClosest(_currentNodeIdAsKey, token); - while (true) { await Bootstrap(token); @@ -106,6 +106,26 @@ public async Task Run(CancellationToken token) public async Task Bootstrap(CancellationToken token) { Stopwatch sw = Stopwatch.StartNew(); + + int onlineBootNodes = 0; + + // Check bootnodes is online + await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => + { + try + { + // Should be added on Pong. + await _kademliaMessageSender.Ping(node, token); + onlineBootNodes++; + } + catch (OperationCanceledException) + { + // Unreachable + } + }); + + if (_logger.IsInfo) _logger.Info($"Online bootnodes: {onlineBootNodes}"); + await LookupNodesClosest(_currentNodeIdAsKey, token); token.ThrowIfCancellationRequested(); @@ -138,4 +158,15 @@ public event EventHandler OnNodeAdded add => _routingTable.OnNodeAdded += value; remove => _routingTable.OnNodeAdded -= value; } + + public IEnumerable IterateNodes() + { + foreach ((ValueHash256 _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) + { + foreach (var node in Bucket.GetAll()) + { + yield return node; + } + } + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index 6914209643a9..c2034efce7e0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -50,4 +50,9 @@ public class KademliaConfig /// How many time a request for a node failed before we remove it from the routing table. /// public int NodeRequestFailureThreshold { get; set; } = 5; + + /// + /// Starting boot nodes. + /// + public IReadOnlyList BootNodes { get; set; } = []; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index 579944b565f2..208fcf50c039 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -144,7 +144,8 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg) try { msg = Deserialize(type, new ArraySegment(msgBytes, 0, size)); - msg.FarAddress = (IPEndPoint)address; + IPEndPoint endPoint = (IPEndPoint)address; + msg.FarAddress = endPoint; } catch (Exception e) { diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs index 91b1e614cb96..30ca51718dc6 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs @@ -61,5 +61,7 @@ public static class EnrContentKey /// public const string Udp6 = "udp6"; public static ReadOnlySpan Udp6U8 => "udp6"u8; + + public static HashSet KnownKeys = [Id, Eth, Ip, Ip6, Secp256K1, Tcp, Tcp6, Udp, Udp6]; } } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 74079c0ccd2b..6d0ae071847e 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -22,7 +22,7 @@ public class NodeRecord private Hash256? _contentHash; - private SortedDictionary Entries { get; } = new(); + public SortedDictionary Entries { get; } = new(); /// /// This field is used when this is deserialized and an unknown entry is encountered. From 37a3502d770f32d8d6bafd0ccce6a83307cc287a Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 19:48:41 +0800 Subject: [PATCH 022/182] Exit logic --- .../CompositeDiscoveryApp.cs | 9 +++++---- .../Nethermind.Network.Discovery/DiscoveryApp.cs | 15 ++++++--------- .../Discv4/KademliaNodeSource.cs | 4 ++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index eacdae57afad..3164963394cf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -9,7 +9,7 @@ using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; using Nethermind.Api; -using Nethermind.Blockchain; +using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Crypto; @@ -17,9 +17,7 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; -using Nethermind.Network.Discovery.Lifecycle; using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; using Nethermind.Network.Discovery.Serializers; using Nethermind.Network.Enr; using Nethermind.Stats; @@ -52,6 +50,7 @@ public class CompositeDiscoveryApp : IDiscoveryApp private IDiscoveryApp? _v5; private INodeSource _compositeNodeSource = null!; private readonly ILifetimeScope _rootLiffetimeScope; + private IProcessExitSource _processExitSource; public CompositeDiscoveryApp( [KeyFilter(IProtectedPrivateKey.NodeKey)] @@ -60,7 +59,7 @@ public CompositeDiscoveryApp( INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IInitConfig initConfig, IEthereumEcdsa? ethereumEcdsa, IMessageSerializationService? serializationService, ILogManager? logManager, ITimestamper? timestamper, ICryptoRandom? cryptoRandom, - INodeStatsManager? nodeStatsManager, IIPResolver? ipResolver, IChannelFactory? channelFactory = null + INodeStatsManager? nodeStatsManager, IIPResolver? ipResolver, IProcessExitSource processExitSource, IChannelFactory? channelFactory = null ) { _rootLiffetimeScope = rootLifetimeScope; @@ -76,6 +75,7 @@ public CompositeDiscoveryApp( _nodeStatsManager = nodeStatsManager ?? throw new ArgumentNullException(nameof(nodeStatsManager)); _ipResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); _connections = new DiscoveryConnectionsPool(logManager.GetClassLogger(), _networkConfig, _discoveryConfig); + _processExitSource = processExitSource; _channelFactory = channelFactory; Initialize(nodeKey.PublicKey); @@ -174,6 +174,7 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator _networkConfig, discoveryConfig, _timestamper, + _processExitSource, _logManager); _v4.Initialize(_nodeKey.PublicKey); diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index fd2a74aa0512..3c7213472381 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -42,6 +42,7 @@ public class DiscoveryApp : IDiscoveryApp private readonly ILifetimeScope _rootLifetimeScope; private KademliaNodeSource _kademliaNodeSource = null!; private Task? _runningTask; + private readonly IProcessExitSource _processExitSouce; public DiscoveryApp( NodeRecord selfNodeRecord, @@ -52,6 +53,7 @@ public DiscoveryApp( INetworkConfig? networkConfig, IDiscoveryConfig? discoveryConfig, ITimestamper? timestamper, + IProcessExitSource processExitSource, ILogManager? logManager) { _selfNodeRecorrd = selfNodeRecord; @@ -60,15 +62,15 @@ public DiscoveryApp( _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); _logger = _logManager.GetClassLogger(); _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); _messageSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); + _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); + _processExitSouce = processExitSource ?? throw new ArgumentNullException(nameof(processExitSource)); _bootNodes = new List(); NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); - // NetworkNode[] bootnodes = NetworkNode.ParseNodes("enode://8cd847302089d4906c5eb3125770b067fbcb7dc6bd62dfd3517483cc2e6acae6141a5fb4061f76825ea9f585d157b625f84f976fb6aa1582dc87b0d0b652f51f@127.0.0.1:40404", _logger); if (bootnodes.Length == 0) { if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); @@ -97,8 +99,6 @@ public void Initialize(PublicKey masterPublicKey) _kademlia = _kademliaServices.Resolve>(); _discv4Adapter = _kademliaServices.Resolve(); _kademliaNodeSource = _kademliaServices.Resolve(); - - // TODO: Setup kademlia here } public Task StartAsync() @@ -144,7 +144,6 @@ public async Task StopAsync() _logger.Error("Error during discovery cleanup", e); } - _appShutdownSource.Cancel(); if (_logger.IsInfo) _logger.Info("Discovery shutdown complete.. please wait for all components to close"); _kademliaServices?.DisposeAsync(); } @@ -174,8 +173,6 @@ public void InitializeChannel(IChannel channel) .AddLast(_discoveryHandler); } - private readonly CancellationTokenSource _appShutdownSource = new(); - private void OnChannelActivated(object? sender, EventArgs e) { if (_logger.IsDebug) _logger.Debug("Activated discovery channel."); @@ -184,7 +181,7 @@ private void OnChannelActivated(object? sender, EventArgs e) // Explicitly use TaskScheduler.Default, otherwise it will use dotnetty's task scheduler which have a habit of // not working sometimes. _runningTask = Task.Factory - .StartNew(() => OnChannelActivated(_appShutdownSource.Token), _appShutdownSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) + .StartNew(() => OnChannelActivated(_processExitSouce.Token), _processExitSouce.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) .ContinueWith ( t => @@ -197,7 +194,7 @@ private void OnChannelActivated(object? sender, EventArgs e) (Exception)new NetworkingException(faultMessage, NetworkExceptionType.Discovery); } - if (t.IsCompleted && !_appShutdownSource.IsCancellationRequested) + if (t.IsCompleted && !_processExitSouce.Token.IsCancellationRequested) { _logger.Debug("Discovery App initialized."); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index a25eb8f8a05c..eba43ce4b70b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -103,6 +103,10 @@ async Task DiscoverAsync(PublicKey target) random.NextBytes(randomBytes); await DiscoverAsync(new PublicKey(randomBytes)); } + catch (OperationCanceledException) + { + break; + } catch (Exception ex) { if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); From 744d792f98f388b25a53a2d22ed168d947523881 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 30 Apr 2025 20:39:00 +0800 Subject: [PATCH 023/182] Configurable limit --- .../Nethermind.Network.Discovery/DiscoveryConfig.cs | 1 + .../Discv4/KademliaNodeSource.cs | 10 ++-------- .../Nethermind.Network.Discovery/IDiscoveryConfig.cs | 3 +++ .../Kademlia/NewTrackingLookupKNearestNeighbour.cs | 7 +++---- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs index b18b2d156bdd..4014bdc0990b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs @@ -44,4 +44,5 @@ public class DiscoveryConfig : IDiscoveryConfig public string Bootnodes { get; set; } = string.Empty; public DiscoveryVersion DiscoveryVersion { get; set; } = DiscoveryVersion.V4; + public int ConcurrentDiscoveryJob { get; set; } = 10; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index eba43ce4b70b..fe8a58b706c1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -17,6 +17,7 @@ public class KademliaNodeSource( IKademlia kademlia, IITeratorAlgo _lookup2, KademliaDiscv4Adapter discv4Adapter, + IDiscoveryConfig discoveryConfig, ILogManager logManager ) { @@ -73,7 +74,6 @@ async Task DiscoverAsync(PublicKey target) await ch.Writer.WriteAsync(node, token); } - _logger.Warn($"Round found {count} nodes. Total is {total}"); if (!anyFound) { if (_logger.IsDebug) _logger.Debug($"No node found for {target}"); @@ -84,19 +84,13 @@ async Task DiscoverAsync(PublicKey target) } } - Task discoverTask = Task.WhenAll(Enumerable.Range(0, 6).Select((_) => Task.Run(async () => + Task discoverTask = Task.WhenAll(Enumerable.Range(0, discoveryConfig.ConcurrentDiscoveryJob).Select((_) => Task.Run(async () => { Random random = new(); byte[] randomBytes = new byte[64]; - int iterationCount = 0; while (!token.IsCancellationRequested) { Stopwatch iterationTime = Stopwatch.StartNew(); - if (iterationCount % 10 == 0) - { - // Probably shnould be done once or in a few interval - // await EnsureBootNodes(token); - } try { diff --git a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs index ea4654d64daa..ef7910abe023 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs @@ -117,4 +117,7 @@ public interface IDiscoveryConfig : IConfig [ConfigItem(Description = "Discovery version(s) to enable", DefaultValue = "All", HiddenFromDocs = true)] DiscoveryVersion DiscoveryVersion { get; set; } + + [ConfigItem(Description = "Concurrent discovery job", DefaultValue = "10", HiddenFromDocs = true)] + int ConcurrentDiscoveryJob { get; set; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 1450ba5398a6..54542e47863b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -95,7 +95,6 @@ [EnumeratorCancellation] CancellationToken token // No node to query and running query. if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); - _logger.Warn("Stopping lookup. No node to query."); break; } @@ -142,12 +141,12 @@ [EnumeratorCancellation] CancellationToken token // This causes `ShouldStopDueToNoBetterResult` to return false. if (closestNodeRound < currentRound && foundBetter) { - _logger.Warn($"Found better neighbour {neighbour} at round {currentRound}."); + if (_logger.IsTrace) _logger.Trace($"Found better neighbour {neighbour} at round {currentRound}."); bestNodeId = neighbourHash; closestNodeRound = currentRound; } } - _logger.Warn($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); + if (_logger.IsTrace) _logger.Trace($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); if (ShouldStopDueToNoBetterResult()) { @@ -175,7 +174,7 @@ [EnumeratorCancellation] CancellationToken token await Task.WhenAny(worker); finished = true; - _logger.Warn("Lookup operation finished."); + if (_logger.IsTrace) _logger.Trace("Lookup operation finished."); yield break; async Task WrappedFindNeighbourOp(TNode node) From d040d5dad5250b2696dce467238611a29503c11a Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 7 May 2025 20:28:09 +0800 Subject: [PATCH 024/182] Some cleanup --- .../Discv4/KademliaDiscv4AdapterTests.cs | 324 ++++++++++++++++++ .../DiscoveryApp.cs | 1 + .../Discv4/DiscV4KademliaModule.cs | 1 + .../Discv4/EnrResponseHandler.cs | 19 + .../Discv4/IMessageHandler.cs | 17 + .../Discv4/KademliaDiscv4Adapter.cs | 168 ++++----- .../Discv4/KademliaNodeSource.cs | 1 + .../Discv4/NeighbourMsgHandler.cs | 39 +++ .../Discv4/PongMsgHandler.cs | 21 ++ 9 files changed, 489 insertions(+), 102 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs new file mode 100644 index 000000000000..efa03e61a18b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -0,0 +1,324 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Enr; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv4 +{ + [Parallelizable(ParallelScope.Self)] + [TestFixture] + public class KademliaDiscv4AdapterTests + { + private KademliaDiscv4Adapter _adapter = null!; + + [TearDown] + public async Task TearDown() + { + await _adapter.DisposeAsync(); + } + private Lazy> _kademliaMessageReceiver = null!; + private INetworkConfig _networkConfig = null!; + private KademliaConfig _kademliaConfig = null!; + private NodeRecord _selfNodeRecord = null!; + private ILogManager _logManager = null!; + private ITimestamper _timestamper = null!; + private IMsgSender _msgSender = null!; + private Node _testNode = null!; + private PublicKey _testPublicKey = null!; + + [SetUp] + public void Setup() + { + _testPublicKey = TestItem.PublicKeyA; + _testNode = new Node(_testPublicKey, "192.168.1.1", 30303); + + _kademliaMessageReceiver = new Lazy>(() => + Substitute.For>()); + + _networkConfig = Substitute.For(); + _networkConfig.MaxActivePeers.Returns(25); + + _kademliaConfig = new KademliaConfig(); + _kademliaConfig.CurrentNodeId = _testNode; + + _selfNodeRecord = Substitute.For(); + + _logManager = LimboLogs.Instance; + + _timestamper = Substitute.For(); + _timestamper.UnixTime.Returns(new UnixTime(new DateTime(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); + + _msgSender = Substitute.For(); + + _adapter = new KademliaDiscv4Adapter( + _kademliaMessageReceiver, + _networkConfig, + _kademliaConfig, + _selfNodeRecord, + _logManager, + _timestamper); + + _adapter.MsgSender = _msgSender; + } + + [Test] + public async Task Ping_should_send_ping_message() + { + // Arrange + Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + // Act + await _adapter.Ping(receiver, CancellationToken.None); + + // Assert + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(receiver.Address) && + m.SourceAddress!.Equals(_kademliaConfig.CurrentNodeId.Address))); + } + + [Test] + public async Task FindNeighbours_should_send_find_node_message_and_return_nodes() + { + // Arrange + Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + PublicKey target = TestItem.PublicKeyC; + Node[] expectedNodes = { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; + + // Setup the message sender to respond with a pong when a ping is sent + _msgSender.When(x => x.SendMsg(Arg.Any())) + .Do(x => + { + PingMsg pingMsg = (PingMsg)x[0]; + PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); + _adapter.OnIncomingMsg(pongMsg); + }); + + // Setup the message sender to respond with neighbors when a find node is sent + _msgSender.When(x => x.SendMsg(Arg.Any())) + .Do(x => + { + FindNodeMsg findNodeMsg = (FindNodeMsg)x[0]; + NeighborsMsg neighborsMsg = new NeighborsMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, expectedNodes); + _adapter.OnIncomingMsg(neighborsMsg); + }); + + // Act + Node[] result = await _adapter.FindNeighbours(receiver, target, CancellationToken.None); + + // Assert + await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(receiver.Address) && + m.SearchedNodeId!.SequenceEqual(target.Bytes))); + + result.Should().BeEquivalentTo(expectedNodes); + } + + [Test] + public async Task SendEnrRequest_should_send_enr_request_message_and_return_response() + { + // Arrange + Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + EnrResponseMsg expectedResponse = new EnrResponseMsg(receiver.Id, _selfNodeRecord, new Hash256(new byte[32])); + + // Setup the message sender to respond with a pong when a ping is sent + _msgSender.When(x => x.SendMsg(Arg.Any())) + .Do(x => + { + PingMsg pingMsg = (PingMsg)x[0]; + PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); + _adapter.OnIncomingMsg(pongMsg); + }); + + // Setup the message sender to respond with ENR response when an ENR request is sent + _msgSender.When(x => x.SendMsg(Arg.Any())) + .Do(x => + { + _adapter.OnIncomingMsg(expectedResponse); + }); + + // Act + EnrResponseMsg result = await _adapter.SendEnrRequest(receiver, CancellationToken.None); + + // Assert + await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); + await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); + + result.Should().Be(expectedResponse); + } + + [Test] + public void OnIncomingMsg_ping_should_respond_with_pong() + { + // Arrange + PingMsg pingMsg = new PingMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address, _testNode.Address, new byte[32]); + + // Act + _adapter.OnIncomingMsg(pingMsg); + + // Assert - Allow some time for the async operation to complete + Task.Delay(100).Wait(); + + _kademliaMessageReceiver.Value.Received(1).Ping(Arg.Is(n => n.Id == _testNode.Id), Arg.Any()); + _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_testNode.Address) && + m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); + } + + [Test] + public void OnIncomingMsg_find_node_should_respond_with_neighbors() + { + // Arrange + FindNodeMsg findNodeMsg = new FindNodeMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); + + Node[] expectedNodes = { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; + _kademliaMessageReceiver.Value.FindNeighbours( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expectedNodes); + + // Act + _adapter.OnIncomingMsg(findNodeMsg); + + // Assert - Allow some time for the async operation to complete + Task.Delay(100).Wait(); + + _kademliaMessageReceiver.Value.Received(1).FindNeighbours( + Arg.Is(n => n.Id == _testNode.Id), + Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), + Arg.Any()); + + _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_testNode.Address) && + m.Nodes.Length == expectedNodes.Length)); + } + + [Test] + public void OnIncomingMsg_enr_request_should_respond_with_enr_response() + { + // Arrange + EnrRequestMsg enrRequestMsg = new EnrRequestMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20); + + // Act + _adapter.OnIncomingMsg(enrRequestMsg); + + // Assert - Allow some time for the async operation to complete + Task.Delay(100).Wait(); + + _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_testNode.Address) && + m.NodeRecord.Equals(_selfNodeRecord))); + } + + [Test] + public async Task DisposeAsync_should_cancel_token_and_dispose_cancellation_token_source() + { + // Act + await _adapter.DisposeAsync(); + + // Assert + // This test is mostly to ensure the method doesn't throw exceptions + // The actual cancellation and disposal is hard to test directly + Assert.Pass("DisposeAsync completed without exceptions"); + } + + [Test] + public async Task EnsureIncomingBondedPeer_should_set_incoming_bond_deadline() + { + // Arrange + Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + // Setup the message sender to respond with a pong when a ping is sent + _msgSender.When(x => x.SendMsg(Arg.Any())) + .Do(x => + { + PingMsg pingMsg = (PingMsg)x[0]; + PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); + _adapter.OnIncomingMsg(pongMsg); + }); + + // Act - Call a method that uses EnsureIncomingBondedPeer internally + EnrRequestMsg enrRequestMsg = new EnrRequestMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20); + + _adapter.OnIncomingMsg(enrRequestMsg); + + // Wait for async operations to complete + await Task.Delay(100); + + // Assert - First call should send a ping + await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); + + // Reset the received calls + _msgSender.ClearReceivedCalls(); + + // Act again - This should use the cached bond deadline + _adapter.OnIncomingMsg(enrRequestMsg); + + // Wait for async operations to complete + await Task.Delay(100); + + // Assert - Second call should not send a ping because the node is already bonded + await _msgSender.DidNotReceive().SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); + } + + [Test] + public void IsPeerSafe_should_return_false_for_unbonded_peer() + { + // Arrange + Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + // Act + bool isSafe = _adapter.IsPeerSafe(receiver); + + // Assert + Assert.That(isSafe, Is.False, "Unbonded peer should not be considered safe"); + } + + [Test] + public async Task IsPeerSafe_should_return_true_after_ping_pong_exchange() + { + // Arrange + Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + // Setup the message sender to respond with a pong when a ping is sent + _msgSender.When(x => x.SendMsg(Arg.Any())) + .Do(x => + { + PingMsg pingMsg = (PingMsg)x[0]; + PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); + _adapter.OnIncomingMsg(pongMsg); + }); + + // Act - Call a method that uses EnsureIncomingBondedPeer internally + EnrRequestMsg enrRequestMsg = new EnrRequestMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20); + + _adapter.OnIncomingMsg(enrRequestMsg); + + // Wait for async operations to complete + await Task.Delay(100); + + // Act - Check if the peer is now safe + bool isSafe = _adapter.IsPeerSafe(receiver); + + // Assert + Assert.That(isSafe, Is.True, "Peer should be considered safe after ping/pong exchange"); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 3c7213472381..a10e155f55e4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -10,6 +10,7 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 4585ef045648..2ba1a4bbb58a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -4,6 +4,7 @@ using Autofac; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs new file mode 100644 index 000000000000..531d1e5859bf --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Messages; + +namespace Nethermind.Network.Discovery.Discv4; + +public class EnrResponseHandler : ITaskCompleter { + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public bool Handle(DiscoveryMsg msg) + { + if (msg is EnrResponseMsg resp && TaskCompletionSource.TrySetResult(resp)) + { + return true; + } + return false; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs new file mode 100644 index 000000000000..4a00ab38e58b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Messages; + +namespace Nethermind.Network.Discovery.Discv4; + +internal interface IMessageHandler +{ + bool Handle(DiscoveryMsg msg); +} + + +internal interface ITaskCompleter: IMessageHandler +{ + TaskCompletionSource TaskCompletionSource { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 362d5a941267..6d4eb127e57b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -15,7 +15,7 @@ using Nethermind.Stats.Model; using NonBlocking; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public class KademliaDiscv4Adapter( Lazy> kademliaMessageReceiver, // Cyclic dependency @@ -26,89 +26,22 @@ public class KademliaDiscv4Adapter( ITimestamper timestamper ): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { - private ILogger _logger = logManager.GetClassLogger(); + private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); + private readonly TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(2); + private readonly TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); - private readonly CancellationTokenSource _cts = new(); + private readonly CancellationTokenSource _messageHandlerCancellation = new(); + private readonly ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); - private TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); - private TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(2); - private TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); - private interface IMessageHandler - { - bool Handle(DiscoveryMsg msg); - } - - private interface ITaskCompleter: IMessageHandler - { - TaskCompletionSource TaskCompletionSource { get; } - } - - private class PongMsgHandler(PingMsg ping) : ITaskCompleter - { - public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - - public bool Handle(DiscoveryMsg msg) - { - if (msg is PongMsg pong && Bytes.AreEqual(pong.PingMdc, ping.Mdc) && TaskCompletionSource.TrySetResult(pong)) - { - return true; - } - return false; - } - } - - private class NeighbourMsgHandler(int k) : ITaskCompleter - { - private Node[] _current = Array.Empty(); - public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - - private static readonly TimeSpan _secondRequestTimeout = TimeSpan.FromSeconds(1); - private bool _timeoutInitiated = false; - - public bool Handle(DiscoveryMsg msg) - { - NeighborsMsg neighborsMsg = (NeighborsMsg)msg; - if (_current.Length >= k || _current.Length + neighborsMsg.Nodes.Length > k) return false; - - _current = _current.Concat(neighborsMsg.Nodes).ToArray(); - if (_current.Length == k) - { - TaskCompletionSource.TrySetResult(_current); - } - else - { - // Some client (nethermind) only respond with one request. - Task.Run(async () => - { - if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false) == false) return; - await Task.Delay(_secondRequestTimeout); - TaskCompletionSource.TrySetResult(_current); - }); - } - return true; - } - } - - private class EnrResponseHandler : ITaskCompleter { - public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - - public bool Handle(DiscoveryMsg msg) - { - if (msg is EnrResponseMsg resp && TaskCompletionSource.TrySetResult(resp)) - { - return true; - } - return false; - } - } - - private readonly ConcurrentDictionary _awaitingPongToNode = new(); private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); - private readonly LruCache _bondDeadline = new(1024 * 10, ""); - private readonly ConcurrentDictionary _authenticatedRequestFailure = new(); + private readonly ConcurrentDictionary> _awaitingPongToNode = new(); // This is for waiting to send pong in attempt to authenticate. + private readonly LruCache _outgoingBondDeadline = new(1024 * 10, "outgoing_bond_deadline"); + + private readonly LruCache _incomingBondDeadline = new(1024 * 10, "incoming_bond_deadline"); + private readonly ConcurrentDictionary _authenticatedRequestFailure = new(); // TODO: To lru cache private static TimeSpan _bondTimeout = TimeSpan.FromHours(12); private const int AuthenticatedRequestFailureLimit = 5; @@ -151,29 +84,45 @@ public async Task SendEnrRequest(Node receiver, CancellationToke private void HandleEnrRequest(Node node, EnrRequestMsg msg) { - if (!IsPeerSafe(node)) return; - Task.Run(async () => { - Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await SendMessage(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + try + { + await EnsureIncomingBondedPeer(node, _messageHandlerCancellation.Token); + + Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); + await SendMessage(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + } + catch (Exception) + { + // If ping fails, we don't respond + if (_logger.IsTrace) _logger.Trace($"Failed to ensure bonded peer {node} for ENR request"); + } }); } private void HandleFindNode(Node node, FindNodeMsg msg) { - if (!IsPeerSafe(node)) return; - Task.Run(async () => { - PublicKey publicKey = new PublicKey(msg.SearchedNodeId); - Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _cts.Token); - if (nodes.Length > 12) + try + { + await EnsureIncomingBondedPeer(node, _messageHandlerCancellation.Token); + + PublicKey publicKey = new PublicKey(msg.SearchedNodeId); + Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _messageHandlerCancellation.Token); + if (nodes.Length > 12) + { + // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. + nodes = nodes.Slice(0, 12).ToArray(); + } + await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + } + catch (Exception) { - // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. - nodes = nodes.Slice(0, 12).ToArray(); + // If ping fails, we don't respond + if (_logger.IsTrace) _logger.Trace($"Failed to ensure bonded peer {node} for FindNode request"); } - await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); }); } @@ -182,15 +131,18 @@ private void HandlePing(Node node, PingMsg ping) if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); Task.Run(async () => { - await kademliaMessageReceiver.Value.Ping(node, _cts.Token); - PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); + await kademliaMessageReceiver.Value.Ping(node, _messageHandlerCancellation.Token); + // Generate MDC hash from the ping message + Rlp requestRlp = Rlp.Encode(Rlp.Encode(ping.ExpirationTime)); + byte[] mdc = Keccak.Compute(requestRlp.Bytes).Bytes.ToArray(); + PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), mdc); await SendMessage(node, msg); }); } private bool IsShouldBeBonded(Node node) { - return _bondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) + return _outgoingBondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) && bondDeadline > DateTimeOffset.Now && (!_authenticatedRequestFailure.TryGetValue(node.IdHash, out long failedFinedNodes) || failedFinedNodes <= AuthenticatedRequestFailureLimit); } @@ -202,7 +154,7 @@ private async Task EnsureBonded(Node node, CancellationToken token) if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); using var cts = token.CreateChildTokenSource(_tryAuthenticatedTimeout); token = cts.Token; - TaskCompletionSource pongCts = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource pongCts = new(TaskCreationOptions.RunContinuationsAsynchronously); CancellationTokenRegistration unregister = token.RegisterToCompletionSource(pongCts); try { @@ -237,7 +189,7 @@ private async Task RunAuthenticatedRequest(Node node, Func? completionSource)) { - completionSource.TrySetResult(); + completionSource.TrySetResult(new object()); } } @@ -380,14 +332,26 @@ private bool HandleViaMessageHandlers(Node node, DiscoveryMsg msg) return true; } - private bool IsPeerSafe(Node node) + public bool IsPeerSafe(Node node) { - return true; + return _incomingBondDeadline.TryGet(node.IdHash, out DateTimeOffset safeUntil) && safeUntil > DateTimeOffset.Now; + } + + private async Task EnsureIncomingBondedPeer(Node node, CancellationToken token) + { + if (IsPeerSafe(node)) + { + return; + } + + // If we're here, the node is not safe, so we'll send a ping to verify + await Ping(node, token); + _incomingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + _bondTimeout); } public async ValueTask DisposeAsync() { - await _cts.CancelAsync(); - _cts.Dispose(); + await _messageHandlerCancellation.CancelAsync(); + _messageHandlerCancellation.Dispose(); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index fe8a58b706c1..f1edb4b2aa08 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -7,6 +7,7 @@ using System.Threading.Channels; using Nethermind.Core.Crypto; using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; using Prometheus; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs new file mode 100644 index 000000000000..2826d0fc44b3 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Messages; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv4; + +public class NeighbourMsgHandler(int k) : ITaskCompleter +{ + private Node[] _current = Array.Empty(); + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + private static readonly TimeSpan _secondRequestTimeout = TimeSpan.FromSeconds(1); + private bool _timeoutInitiated = false; + + public bool Handle(DiscoveryMsg msg) + { + NeighborsMsg neighborsMsg = (NeighborsMsg)msg; + if (_current.Length >= k || _current.Length + neighborsMsg.Nodes.Length > k) return false; + + _current = _current.Concat(neighborsMsg.Nodes).ToArray(); + if (_current.Length == k) + { + TaskCompletionSource.TrySetResult(_current); + } + else + { + // Some client (nethermind) only respond with one request. + Task.Run(async () => + { + if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false) == false) return; + await Task.Delay(_secondRequestTimeout); + TaskCompletionSource.TrySetResult(_current); + }); + } + return true; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs new file mode 100644 index 000000000000..25e72e886c85 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Extensions; +using Nethermind.Network.Discovery.Messages; + +namespace Nethermind.Network.Discovery.Discv4; + +public class PongMsgHandler(PingMsg ping) : ITaskCompleter +{ + public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public bool Handle(DiscoveryMsg msg) + { + if (msg is PongMsg pong && Bytes.AreEqual(pong.PingMdc, ping.Mdc) && TaskCompletionSource.TrySetResult(pong)) + { + return true; + } + return false; + } +} From 90b7934e7e55a27addb5adb2d8d2ce6adebde83f Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 7 May 2025 20:51:09 +0800 Subject: [PATCH 025/182] Refinement --- .../DiscoveryManagerTests.cs | 4 +- .../Discv4/KademliaDiscv4AdapterTests.cs | 209 +++++++------- .../NettyDiscoveryHandlerTests.cs | 16 +- .../DiscoveryManager.cs | 8 +- .../Discv4/KademliaDiscv4Adapter.cs | 260 ++++++++---------- .../IDiscoveryMsgListener.cs | 2 +- 6 files changed, 233 insertions(+), 266 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs index 02634ee53c36..b7993f796cc8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs @@ -84,7 +84,7 @@ public async Task OnPingMessageTest() { //receiving ping IPEndPoint address = new(IPAddress.Parse(Host), Port); - _discoveryManager.OnIncomingMsg(new PingMsg(_publicKey, GetExpirationTime(), address, _nodeTable.MasterNode!.Address, new byte[32]) { FarAddress = address }); + await _discoveryManager.OnIncomingMsg(new PingMsg(_publicKey, GetExpirationTime(), address, _nodeTable.MasterNode!.Address, new byte[32]) { FarAddress = address }); await Task.Delay(500); // expecting to send pong @@ -176,7 +176,7 @@ public async Task OnNeighborsMessageTest() //receiving findNode NeighborsMsg msg = new(_publicKey, GetExpirationTime(), _nodes); msg.FarAddress = new IPEndPoint(IPAddress.Parse(Host), Port); - _discoveryManager.OnIncomingMsg(msg); + await _discoveryManager.OnIncomingMsg(msg); //expecting to send 3 pings to both nodes await Task.Delay(600); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index efa03e61a18b..d600a5137800 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; @@ -26,13 +27,13 @@ namespace Nethermind.Network.Discovery.Test.Discv4 public class KademliaDiscv4AdapterTests { private KademliaDiscv4Adapter _adapter = null!; - + [TearDown] public async Task TearDown() { await _adapter.DisposeAsync(); } - private Lazy> _kademliaMessageReceiver = null!; + private IKademliaMessageReceiver _kademliaMessageReceiver = null!; private INetworkConfig _networkConfig = null!; private KademliaConfig _kademliaConfig = null!; private NodeRecord _selfNodeRecord = null!; @@ -47,33 +48,34 @@ public void Setup() { _testPublicKey = TestItem.PublicKeyA; _testNode = new Node(_testPublicKey, "192.168.1.1", 30303); - - _kademliaMessageReceiver = new Lazy>(() => - Substitute.For>()); - + + _kademliaMessageReceiver = Substitute.For>(); + _networkConfig = Substitute.For(); _networkConfig.MaxActivePeers.Returns(25); - + _kademliaConfig = new KademliaConfig(); _kademliaConfig.CurrentNodeId = _testNode; - + _selfNodeRecord = Substitute.For(); - + _logManager = LimboLogs.Instance; - + _timestamper = Substitute.For(); _timestamper.UnixTime.Returns(new UnixTime(new DateTime(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); - + _msgSender = Substitute.For(); - + _adapter = new KademliaDiscv4Adapter( - _kademliaMessageReceiver, + new Lazy>(() => _kademliaMessageReceiver), _networkConfig, _kademliaConfig, _selfNodeRecord, - _logManager, - _timestamper); - + _timestamper, + Substitute.For(), + _logManager + ); + _adapter.MsgSender = _msgSender; } @@ -82,13 +84,13 @@ public async Task Ping_should_send_ping_message() { // Arrange Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - + // Act await _adapter.Ping(receiver, CancellationToken.None); - + // Assert - await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address) && + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(receiver.Address) && m.SourceAddress!.Equals(_kademliaConfig.CurrentNodeId.Address))); } @@ -99,34 +101,34 @@ public async Task FindNeighbours_should_send_find_node_message_and_return_nodes( Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); PublicKey target = TestItem.PublicKeyC; Node[] expectedNodes = { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; - + // Setup the message sender to respond with a pong when a ping is sent _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + .Do(x => { PingMsg pingMsg = (PingMsg)x[0]; PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - _adapter.OnIncomingMsg(pongMsg); + Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); }); - + // Setup the message sender to respond with neighbors when a find node is sent _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + .Do(x => { FindNodeMsg findNodeMsg = (FindNodeMsg)x[0]; NeighborsMsg neighborsMsg = new NeighborsMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, expectedNodes); - _adapter.OnIncomingMsg(neighborsMsg); + Task.Run(() => _adapter.OnIncomingMsg(neighborsMsg)); }); - + // Act Node[] result = await _adapter.FindNeighbours(receiver, target, CancellationToken.None); - + // Assert await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address) && + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(receiver.Address) && m.SearchedNodeId!.SequenceEqual(target.Bytes))); - + result.Should().BeEquivalentTo(expectedNodes); } @@ -136,94 +138,94 @@ public async Task SendEnrRequest_should_send_enr_request_message_and_return_resp // Arrange Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); EnrResponseMsg expectedResponse = new EnrResponseMsg(receiver.Id, _selfNodeRecord, new Hash256(new byte[32])); - + // Setup the message sender to respond with a pong when a ping is sent _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + .Do(x => { PingMsg pingMsg = (PingMsg)x[0]; PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - _adapter.OnIncomingMsg(pongMsg); + Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); }); - + // Setup the message sender to respond with ENR response when an ENR request is sent _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + .Do(x => { - _adapter.OnIncomingMsg(expectedResponse); + Task.Run(() => _adapter.OnIncomingMsg(expectedResponse)); }); - + // Act EnrResponseMsg result = await _adapter.SendEnrRequest(receiver, CancellationToken.None); - + // Assert await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - + result.Should().Be(expectedResponse); } [Test] - public void OnIncomingMsg_ping_should_respond_with_pong() + public async Task OnIncomingMsg_ping_should_respond_with_pong() { // Arrange PingMsg pingMsg = new PingMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address, _testNode.Address, new byte[32]); - + // Act - _adapter.OnIncomingMsg(pingMsg); - + await _adapter.OnIncomingMsg(pingMsg); + // Assert - Allow some time for the async operation to complete Task.Delay(100).Wait(); - - _kademliaMessageReceiver.Value.Received(1).Ping(Arg.Is(n => n.Id == _testNode.Id), Arg.Any()); - _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(_testNode.Address) && + + await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _testNode.Id), Arg.Any()); + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_testNode.Address) && m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); } [Test] - public void OnIncomingMsg_find_node_should_respond_with_neighbors() + public async Task OnIncomingMsg_find_node_should_respond_with_neighbors() { // Arrange FindNodeMsg findNodeMsg = new FindNodeMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); - + Node[] expectedNodes = { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; - _kademliaMessageReceiver.Value.FindNeighbours( - Arg.Any(), - Arg.Any(), + _kademliaMessageReceiver.FindNeighbours( + Arg.Any(), + Arg.Any(), Arg.Any()) .Returns(expectedNodes); - + // Act - _adapter.OnIncomingMsg(findNodeMsg); - + await _adapter.OnIncomingMsg(findNodeMsg); + // Assert - Allow some time for the async operation to complete Task.Delay(100).Wait(); - - _kademliaMessageReceiver.Value.Received(1).FindNeighbours( - Arg.Is(n => n.Id == _testNode.Id), - Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), + + await _kademliaMessageReceiver.Received(1).FindNeighbours( + Arg.Is(n => n.Id == _testNode.Id), + Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), Arg.Any()); - - _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(_testNode.Address) && + + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_testNode.Address) && m.Nodes.Length == expectedNodes.Length)); } [Test] - public void OnIncomingMsg_enr_request_should_respond_with_enr_response() + public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response() { // Arrange EnrRequestMsg enrRequestMsg = new EnrRequestMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20); - + // Act - _adapter.OnIncomingMsg(enrRequestMsg); - + await _adapter.OnIncomingMsg(enrRequestMsg); + // Assert - Allow some time for the async operation to complete Task.Delay(100).Wait(); - - _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(_testNode.Address) && + + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_testNode.Address) && m.NodeRecord.Equals(_selfNodeRecord))); } @@ -232,93 +234,80 @@ public async Task DisposeAsync_should_cancel_token_and_dispose_cancellation_toke { // Act await _adapter.DisposeAsync(); - + // Assert // This test is mostly to ensure the method doesn't throw exceptions // The actual cancellation and disposal is hard to test directly Assert.Pass("DisposeAsync completed without exceptions"); } - + [Test] public async Task EnsureIncomingBondedPeer_should_set_incoming_bond_deadline() { // Arrange Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - + // Setup the message sender to respond with a pong when a ping is sent _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + .Do(x => { PingMsg pingMsg = (PingMsg)x[0]; PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - _adapter.OnIncomingMsg(pongMsg); + Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); }); - + // Act - Call a method that uses EnsureIncomingBondedPeer internally EnrRequestMsg enrRequestMsg = new EnrRequestMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20); - - _adapter.OnIncomingMsg(enrRequestMsg); - + + await _adapter.OnIncomingMsg(enrRequestMsg); + // Wait for async operations to complete await Task.Delay(100); - + // Assert - First call should send a ping await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - + // Reset the received calls _msgSender.ClearReceivedCalls(); - + // Act again - This should use the cached bond deadline - _adapter.OnIncomingMsg(enrRequestMsg); - + await _adapter.OnIncomingMsg(enrRequestMsg); + // Wait for async operations to complete await Task.Delay(100); - + // Assert - Second call should not send a ping because the node is already bonded await _msgSender.DidNotReceive().SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); } - - [Test] - public void IsPeerSafe_should_return_false_for_unbonded_peer() - { - // Arrange - Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - - // Act - bool isSafe = _adapter.IsPeerSafe(receiver); - - // Assert - Assert.That(isSafe, Is.False, "Unbonded peer should not be considered safe"); - } - + [Test] public async Task IsPeerSafe_should_return_true_after_ping_pong_exchange() { // Arrange Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - + // Setup the message sender to respond with a pong when a ping is sent _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + .Do(x => { PingMsg pingMsg = (PingMsg)x[0]; PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - _adapter.OnIncomingMsg(pongMsg); + Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); }); - + // Act - Call a method that uses EnsureIncomingBondedPeer internally EnrRequestMsg enrRequestMsg = new EnrRequestMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20); - - _adapter.OnIncomingMsg(enrRequestMsg); - + + await _adapter.OnIncomingMsg(enrRequestMsg); + // Wait for async operations to complete await Task.Delay(100); - + // Act - Check if the peer is now safe - bool isSafe = _adapter.IsPeerSafe(receiver); - + // bool isSafe = _adapter.IsPeerSafe(receiver); + // Assert - Assert.That(isSafe, Is.True, "Peer should be considered safe after ping/pong exchange"); + // Assert.That(isSafe, Is.True, "Peer should be considered safe after ping/pong exchange"); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs index bff5e125994a..f30fb41ed12f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs @@ -80,7 +80,7 @@ public async Task PingSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); + await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); PingMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, _address2, _address, new byte[32]) { @@ -89,7 +89,7 @@ public async Task PingSentReceivedTest() await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); + await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); AssertMetrics(258); } @@ -107,7 +107,7 @@ public async Task PongSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); + await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); PongMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new byte[] { 1, 2, 3 }) { @@ -115,7 +115,7 @@ public async Task PongSentReceivedTest() }; await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); + await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); AssertMetrics(240); } @@ -133,7 +133,7 @@ public async Task FindNodeSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); + await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); FindNodeMsg msg2 = new(_privateKey2.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new byte[] { 1, 2, 3 }) { @@ -142,7 +142,7 @@ public async Task FindNodeSentReceivedTest() await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); + await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); AssertMetrics(216); } @@ -160,7 +160,7 @@ public async Task NeighborsSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); + await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); NeighborsMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new List().ToArray()) { @@ -169,7 +169,7 @@ public async Task NeighborsSentReceivedTest() await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); + await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); AssertMetrics(210); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs index 48f0edb4b711..d9b92daf048e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs @@ -76,7 +76,7 @@ public IMsgSender MsgSender set => _msgSender = value; } - public void OnIncomingMsg(DiscoveryMsg msg) + public Task OnIncomingMsg(DiscoveryMsg msg) { try { @@ -87,7 +87,7 @@ public void OnIncomingMsg(DiscoveryMsg msg) INodeLifecycleManager? nodeManager = GetNodeLifecycleManager(node); if (nodeManager is null) { - return; + return Task.CompletedTask; } switch (msgType) @@ -116,7 +116,7 @@ public void OnIncomingMsg(DiscoveryMsg msg) break; default: _logger.Error($"Unsupported msgType: {msgType}"); - return; + return Task.CompletedTask; } NotifySubscribersOnMsgReceived(msgType, nodeManager.ManagedNode, msg); @@ -126,6 +126,8 @@ public void OnIncomingMsg(DiscoveryMsg msg) { _logger.Error("Error during msg handling", e); } + + return Task.CompletedTask; } private int _managersCreated; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 6d4eb127e57b..f6e9cdce4df6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Caching; using Nethermind.Core.Collections; @@ -22,15 +23,21 @@ public class KademliaDiscv4Adapter( INetworkConfig networkConfig, KademliaConfig kademliaConfig, NodeRecord selfNodeRecord, - ILogManager logManager, - ITimestamper timestamper + ITimestamper timestamper, + IProcessExitSource processExitSource, + ILogManager logManager ): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { + private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(2); private readonly TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); + private const int AuthenticatedRequestFailureLimit = 5; + /// + /// This is the value set by other clients based on real network tests. + /// + private const int ExpirationTimeInSeconds = 20; - private readonly CancellationTokenSource _messageHandlerCancellation = new(); private readonly ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); @@ -42,120 +49,21 @@ ITimestamper timestamper private readonly LruCache _incomingBondDeadline = new(1024 * 10, "incoming_bond_deadline"); private readonly ConcurrentDictionary _authenticatedRequestFailure = new(); // TODO: To lru cache - private static TimeSpan _bondTimeout = TimeSpan.FromHours(12); - private const int AuthenticatedRequestFailureLimit = 5; - - public async Task Ping(Node receiver, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); - - _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, msg, token); - } - - public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - return await RunAuthenticatedRequest(receiver, async token => - { - FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); - - // TODO: 16 is configurable - return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(16), receiver, msg, token); - }, token); - } - - public async Task SendEnrRequest(Node receiver, CancellationToken token) - { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - - return await RunAuthenticatedRequest(receiver, async token => - { - EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); - - return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, msg, token); - }, token); - } - - private void HandleEnrRequest(Node node, EnrRequestMsg msg) - { - Task.Run(async () => - { - try - { - await EnsureIncomingBondedPeer(node, _messageHandlerCancellation.Token); - - Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await SendMessage(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); - } - catch (Exception) - { - // If ping fails, we don't respond - if (_logger.IsTrace) _logger.Trace($"Failed to ensure bonded peer {node} for ENR request"); - } - }); - } - - private void HandleFindNode(Node node, FindNodeMsg msg) - { - Task.Run(async () => - { - try - { - await EnsureIncomingBondedPeer(node, _messageHandlerCancellation.Token); - - PublicKey publicKey = new PublicKey(msg.SearchedNodeId); - Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _messageHandlerCancellation.Token); - if (nodes.Length > 12) - { - // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. - nodes = nodes.Slice(0, 12).ToArray(); - } - await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); - } - catch (Exception) - { - // If ping fails, we don't respond - if (_logger.IsTrace) _logger.Trace($"Failed to ensure bonded peer {node} for FindNode request"); - } - }); - } + private readonly CancellationToken _processCancellationToken = processExitSource.Token; - private void HandlePing(Node node, PingMsg ping) - { - if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); - Task.Run(async () => - { - await kademliaMessageReceiver.Value.Ping(node, _messageHandlerCancellation.Token); - // Generate MDC hash from the ping message - Rlp requestRlp = Rlp.Encode(Rlp.Encode(ping.ExpirationTime)); - byte[] mdc = Keccak.Compute(requestRlp.Bytes).Bytes.ToArray(); - PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), mdc); - await SendMessage(node, msg); - }); - } + #region Authentication and utils - private bool IsShouldBeBonded(Node node) + private async Task EnsureOutgoingMessageBondedPeer(Node node, CancellationToken token) { - return _outgoingBondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) - && bondDeadline > DateTimeOffset.Now - && (!_authenticatedRequestFailure.TryGetValue(node.IdHash, out long failedFinedNodes) || failedFinedNodes <= AuthenticatedRequestFailureLimit); - } - - private async Task EnsureBonded(Node node, CancellationToken token) - { - if (IsShouldBeBonded(node)) return true; + if (_outgoingBondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) + && bondDeadline > DateTimeOffset.Now + && !TooManyFailure()) return true; if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); using var cts = token.CreateChildTokenSource(_tryAuthenticatedTimeout); token = cts.Token; TaskCompletionSource pongCts = new(TaskCreationOptions.RunContinuationsAsynchronously); - CancellationTokenRegistration unregister = token.RegisterToCompletionSource(pongCts); + await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(pongCts); try { _awaitingPongToNode.TryAdd(node.IdHash, pongCts); @@ -175,21 +83,37 @@ private async Task EnsureBonded(Node node, CancellationToken token) } finally { - unregister.Unregister(); _awaitingPongToNode.TryRemove(node.IdHash, out _); } + + bool TooManyFailure() + { + return _authenticatedRequestFailure.TryGetValue(node.IdHash, out long failedFinedNodes) && failedFinedNodes > AuthenticatedRequestFailureLimit; + } + } + + private async Task EnsureIncomingMessageBondedPeer(Node node, CancellationToken token) + { + if (_incomingBondDeadline.TryGet(node.IdHash, out DateTimeOffset safeUntil) && safeUntil > DateTimeOffset.Now) + { + return; + } + + // If we're here, the node is not safe, so we'll send a ping to verify + await Ping(node, token); + _incomingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); } private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) { - bool shouldBeBonded = await EnsureBonded(node, token); + bool shouldBeBonded = await EnsureOutgoingMessageBondedPeer(node, token); try { T resp = await callRequest(token); if (!shouldBeBonded) { // Well.... maybe we already bonded, we just forgot about it.... - _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + _bondTimeout); + _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); } _authenticatedRequestFailure[node.IdHash] = 0; return resp; @@ -254,7 +178,7 @@ private async Task SendMessage(Node node, DiscoveryMsg msg) { if (msg is PongMsg pong) { - _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + _bondTimeout); + _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); if (_awaitingPongToNode.TryGetValue(node.IdHash, out TaskCompletionSource? completionSource)) { completionSource.TrySetResult(new object()); @@ -265,16 +189,86 @@ private async Task SendMessage(Node node, DiscoveryMsg msg) } } - /// - /// This is the value set by other clients based on real network tests. - /// - private const int ExpirationTimeInSeconds = 20; private long CalculateExpirationTime() { return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; } - public void OnIncomingMsg(DiscoveryMsg msg) + + + #endregion + + public async Task Ping(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); + + _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, msg, token); + } + + public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + return await RunAuthenticatedRequest(receiver, async token => + { + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); + + // TODO: 16 is configurable + return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(16), receiver, msg, token); + }, token); + } + + public async Task SendEnrRequest(Node receiver, CancellationToken token) + { + using var cts = token.CreateChildTokenSource(_requestTimeout); + token = cts.Token; + + return await RunAuthenticatedRequest(receiver, async token => + { + EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); + + return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, msg, token); + }, token); + } + + private async Task HandleEnrRequest(Node node, EnrRequestMsg msg) + { + await EnsureIncomingMessageBondedPeer(node, _processCancellationToken); + + Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); + await SendMessage(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + } + + private async Task HandleFindNode(Node node, FindNodeMsg msg) + { + await EnsureIncomingMessageBondedPeer(node, _processCancellationToken); + + PublicKey publicKey = new PublicKey(msg.SearchedNodeId); + Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _processCancellationToken); + if (nodes.Length > 12) + { + // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. + nodes = nodes.Slice(0, 12).ToArray(); + } + await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + } + + private async Task HandlePing(Node node, PingMsg ping) + { + if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); + await kademliaMessageReceiver.Value.Ping(node, _processCancellationToken); + // Generate MDC hash from the ping message + Rlp requestRlp = Rlp.Encode(Rlp.Encode(ping.ExpirationTime)); + byte[] mdc = Keccak.Compute(requestRlp.Bytes).Bytes.ToArray(); + PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), mdc); + await SendMessage(node, msg); + } + + public async Task OnIncomingMsg(DiscoveryMsg msg) { try { @@ -295,13 +289,13 @@ public void OnIncomingMsg(DiscoveryMsg msg) break; case MsgType.Ping: PingMsg ping = (PingMsg)msg; - HandlePing(node, ping); + await HandlePing(node, ping); break; case MsgType.FindNode: - HandleFindNode(node, (FindNodeMsg)msg); + await HandleFindNode(node, (FindNodeMsg)msg); break; case MsgType.EnrRequest: - HandleEnrRequest(node, (EnrRequestMsg)msg); + await HandleEnrRequest(node, (EnrRequestMsg)msg); break; case MsgType.EnrResponse: break; @@ -318,7 +312,7 @@ public void OnIncomingMsg(DiscoveryMsg msg) private bool HandleViaMessageHandlers(Node node, DiscoveryMsg msg) { - var key = (node.IdHash, msg.MsgType); + (Hash256 IdHash, MsgType MsgType) key = (node.IdHash, msg.MsgType); if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? handlers)) return false; foreach (var messageHandler in handlers!) { @@ -332,26 +326,8 @@ private bool HandleViaMessageHandlers(Node node, DiscoveryMsg msg) return true; } - public bool IsPeerSafe(Node node) - { - return _incomingBondDeadline.TryGet(node.IdHash, out DateTimeOffset safeUntil) && safeUntil > DateTimeOffset.Now; - } - - private async Task EnsureIncomingBondedPeer(Node node, CancellationToken token) - { - if (IsPeerSafe(node)) - { - return; - } - - // If we're here, the node is not safe, so we'll send a ping to verify - await Ping(node, token); - _incomingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + _bondTimeout); - } - - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { - await _messageHandlerCancellation.CancelAsync(); - _messageHandlerCancellation.Dispose(); + return ValueTask.CompletedTask; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs index 8bf045c7b961..70db39491f26 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs @@ -7,5 +7,5 @@ namespace Nethermind.Network.Discovery; public interface IDiscoveryMsgListener { - void OnIncomingMsg(DiscoveryMsg msg); + Task OnIncomingMsg(DiscoveryMsg msg); } From 51524bc6d8c11c141a88cbf65d7ae991aedb3617 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 8 May 2025 13:13:13 +0800 Subject: [PATCH 026/182] Refinement --- .../Discv4/KademliaDiscv4AdapterTests.cs | 143 ++++++++++-------- 1 file changed, 83 insertions(+), 60 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index d600a5137800..3ef80e9c47a3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -28,11 +28,6 @@ public class KademliaDiscv4AdapterTests { private KademliaDiscv4Adapter _adapter = null!; - [TearDown] - public async Task TearDown() - { - await _adapter.DisposeAsync(); - } private IKademliaMessageReceiver _kademliaMessageReceiver = null!; private INetworkConfig _networkConfig = null!; private KademliaConfig _kademliaConfig = null!; @@ -46,24 +41,18 @@ public async Task TearDown() [SetUp] public void Setup() { + // test node & dependencies _testPublicKey = TestItem.PublicKeyA; _testNode = new Node(_testPublicKey, "192.168.1.1", 30303); _kademliaMessageReceiver = Substitute.For>(); - _networkConfig = Substitute.For(); _networkConfig.MaxActivePeers.Returns(25); - - _kademliaConfig = new KademliaConfig(); - _kademliaConfig.CurrentNodeId = _testNode; - + _kademliaConfig = new KademliaConfig { CurrentNodeId = _testNode }; _selfNodeRecord = Substitute.For(); - _logManager = LimboLogs.Instance; - _timestamper = Substitute.For(); _timestamper.UnixTime.Returns(new UnixTime(new DateTime(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); - _msgSender = Substitute.For(); _adapter = new KademliaDiscv4Adapter( @@ -74,94 +63,128 @@ public void Setup() _timestamper, Substitute.For(), _logManager - ); - + ); _adapter.MsgSender = _msgSender; } + [TearDown] + public async Task TearDown() + { + await _adapter.DisposeAsync(); + } + [Test] - public async Task Ping_should_send_ping_message() + [CancelAfter(5000)] + public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token) { - // Arrange - Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + var receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - // Act - await _adapter.Ping(receiver, CancellationToken.None); + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => + { + var sent = (PingMsg)ci[0]!; + sent.FarPublicKey = TestItem.PublicKeyA; + sent.Mdc = new byte[32]; // Normally set by serializer + var pong = new PongMsg( + receiver.Address, + _timestamper.UnixTime.SecondsLong + 1, + sent.Mdc!); + Task.Run(() => _adapter.OnIncomingMsg(pong)); + }); + + await _adapter.Ping(receiver, token); - // Assert await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address) && - m.SourceAddress!.Equals(_kademliaConfig.CurrentNodeId.Address))); + m.FarAddress!.Equals(receiver.Address))); } [Test] - public async Task FindNeighbours_should_send_find_node_message_and_return_nodes() + [CancelAfter(5000)] + public async Task FindNeighbours_should_ping_then_findnode_and_return_nodes(CancellationToken token) { // Arrange - Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - PublicKey target = TestItem.PublicKeyC; - Node[] expectedNodes = { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; - - // Setup the message sender to respond with a pong when a ping is sent - _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + var receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + var target = TestItem.PublicKeyC; + var expected = new[] { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; + + // Stub: replying to PingMsg with PongMsg + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => { - PingMsg pingMsg = (PingMsg)x[0]; - PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); + var sent = (PingMsg)ci[0]!; + var pong = new PongMsg( + receiver.Address, + _timestamper.UnixTime.SecondsLong + 1, + sent.Mdc!); + Task.Run(() => _adapter.OnIncomingMsg(pong)); }); - // Setup the message sender to respond with neighbors when a find node is sent - _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + // Stub: replying to FindNodeMsg with NeighborsMsg + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => { - FindNodeMsg findNodeMsg = (FindNodeMsg)x[0]; - NeighborsMsg neighborsMsg = new NeighborsMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, expectedNodes); - Task.Run(() => _adapter.OnIncomingMsg(neighborsMsg)); + var sent = (FindNodeMsg)ci[0]!; + var neighbors = new NeighborsMsg( + receiver.Address, + _timestamper.UnixTime.SecondsLong + 1, + expected); + Task.Run(() => _adapter.OnIncomingMsg(neighbors)); }); // Act - Node[] result = await _adapter.FindNeighbours(receiver, target, CancellationToken.None); + Node[] result = await _adapter.FindNeighbours(receiver, target, token); // Assert - await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(receiver.Address))); await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address) && m.SearchedNodeId!.SequenceEqual(target.Bytes))); - - result.Should().BeEquivalentTo(expectedNodes); + result.Should().BeEquivalentTo(expected); } [Test] - public async Task SendEnrRequest_should_send_enr_request_message_and_return_response() + [CancelAfter(5000)] + public async Task SendEnrRequest_should_ping_then_enr_request_and_return_response(CancellationToken token) { // Arrange - Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - EnrResponseMsg expectedResponse = new EnrResponseMsg(receiver.Id, _selfNodeRecord, new Hash256(new byte[32])); + var receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + var expectedResponse = new EnrResponseMsg( + receiver.Address, + _selfNodeRecord, + new Hash256(new byte[32])); - // Setup the message sender to respond with a pong when a ping is sent - _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + // Stub: replying to PingMsg with PongMsg + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => { - PingMsg pingMsg = (PingMsg)x[0]; - PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); + var sent = (PingMsg)ci[0]!; + var pong = new PongMsg( + receiver.Address, + _timestamper.UnixTime.SecondsLong + 1, + sent.Mdc!); + Task.Run(() => _adapter.OnIncomingMsg(pong)); }); - // Setup the message sender to respond with ENR response when an ENR request is sent - _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => + // Stub: replying to EnrRequestMsg with EnrResponseMsg + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => { Task.Run(() => _adapter.OnIncomingMsg(expectedResponse)); }); // Act - EnrResponseMsg result = await _adapter.SendEnrRequest(receiver, CancellationToken.None); + EnrResponseMsg result = await _adapter.SendEnrRequest(receiver, token); // Assert - await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - + await _msgSender.Received(1).SendMsg(Arg.Any()); + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(receiver.Address))); result.Should().Be(expectedResponse); } From 894a9ef8d2b2677ebabdf607a8fbd0579733bab0 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 09:39:36 +0800 Subject: [PATCH 027/182] Respond with two nodes message. --- .../Discv4/KademliaDiscv4Adapter.cs | 12 ++++++++---- .../Discv4/NeighbourMsgHandler.cs | 2 +- .../Lifecycle/NodeLifecycleManager.cs | 4 ++-- .../Messages/NeighborsMsg.cs | 12 ++++++------ .../Serializers/NeighborsMsgSerializer.cs | 10 +++++----- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index f6e9cdce4df6..ad2045f05a89 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -249,12 +249,16 @@ private async Task HandleFindNode(Node node, FindNodeMsg msg) PublicKey publicKey = new PublicKey(msg.SearchedNodeId); Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _processCancellationToken); - if (nodes.Length > 12) + if (nodes.Length <= 12) { - // some issue with large neighbour message. Too large, and its larger than the default mtu 1280. - nodes = nodes.Slice(0, 12).ToArray(); + await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + } + else + { + // Split into two because the size of message when nodes is > 12 is larger than mtu size. + await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12])); + await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..])); } - await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); } private async Task HandlePing(Node node, PingMsg ping) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs index 2826d0fc44b3..ef5bf4963e42 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs @@ -17,7 +17,7 @@ public class NeighbourMsgHandler(int k) : ITaskCompleter public bool Handle(DiscoveryMsg msg) { NeighborsMsg neighborsMsg = (NeighborsMsg)msg; - if (_current.Length >= k || _current.Length + neighborsMsg.Nodes.Length > k) return false; + if (_current.Length >= k || _current.Length + neighborsMsg.Nodes.Count > k) return false; _current = _current.Concat(neighborsMsg.Nodes).ToArray(); if (_current.Length == k) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs index 44159f144884..af13f43a036d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs @@ -170,7 +170,7 @@ public void ProcessNeighborsMsg(NeighborsMsg? msg) return; } - if (_lastNeighbourSize + msg.Nodes.Length == 16) + if (_lastNeighbourSize + msg.Nodes.Count == 16) { // Turns out, other client will split the neighbour msg to two msg, whose size sum up to 16. // Happens about 70% of the time. @@ -181,7 +181,7 @@ public void ProcessNeighborsMsg(NeighborsMsg? msg) ProcessNodes(msg); } - _lastNeighbourSize = msg.Nodes.Length; + _lastNeighbourSize = msg.Nodes.Count; _isNeighborsExpected = false; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs index c1ced0592f96..aaa2c77c2dda 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs @@ -9,21 +9,21 @@ namespace Nethermind.Network.Discovery.Messages; public class NeighborsMsg : DiscoveryMsg { - public Node[] Nodes { get; init; } + public ArraySegment Nodes { get; init; } - public NeighborsMsg(IPEndPoint farAddress, long expirationTime, Node[] nodes) : base(farAddress, expirationTime) + public NeighborsMsg(IPEndPoint farAddress, long expirationTime, ArraySegment nodes) : base(farAddress, expirationTime) { - Nodes = nodes ?? throw new ArgumentNullException(nameof(nodes)); + Nodes = nodes; } - public NeighborsMsg(PublicKey farPublicKey, long expirationTime, Node[] nodes) : base(farPublicKey, expirationTime) + public NeighborsMsg(PublicKey farPublicKey, long expirationTime, ArraySegment nodes) : base(farPublicKey, expirationTime) { - Nodes = nodes ?? throw new ArgumentNullException(nameof(nodes)); + Nodes = nodes; } public override string ToString() { - return base.ToString() + $", Nodes: {(Nodes.Length != 0 ? string.Join(",", Nodes.Select(static x => x.ToString())) : "empty")}"; + return base.ToString() + $", Nodes: {(Nodes.Count != 0 ? string.Join(",", Nodes.Select(static x => x.ToString())) : "empty")}"; } public override MsgType MsgType => MsgType.Neighbors; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs index ea3f8820f684..2e7fc92ac352 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs @@ -43,10 +43,10 @@ public void Serialize(IByteBuffer byteBuffer, NeighborsMsg msg) PrepareBufferForSerialization(byteBuffer, totalLength, (byte)msg.MsgType); NettyRlpStream stream = new(byteBuffer); stream.StartSequence(contentLength); - if (msg.Nodes.Length != 0) + if (msg.Nodes.Count != 0) { stream.StartSequence(nodesContentLength); - for (int i = 0; i < msg.Nodes.Length; i++) + for (int i = 0; i < msg.Nodes.Count; i++) { Node node = msg.Nodes[i]; SerializeNode(stream, node.Address, node.Id.Bytes); @@ -81,10 +81,10 @@ private static Node[] DeserializeNodes(RlpStream rlpStream) return rlpStream.DecodeArray(_decodeItem); } - private static int GetNodesLength(Node[] nodes, out int contentLength) + private static int GetNodesLength(ArraySegment nodes, out int contentLength) { contentLength = 0; - for (int i = 0; i < nodes.Length; i++) + for (int i = 0; i < nodes.Count; i++) { Node node = nodes[i]; contentLength += Rlp.LengthOfSequence(GetLengthSerializeNode(node.Address, node.Id.Bytes)); @@ -102,7 +102,7 @@ private static (int totalLength, int contentLength, int nodesContentLength) GetL { int nodesContentLength = 0; int contentLength = 0; - if (msg.Nodes.Length != 0) + if (msg.Nodes.Count != 0) { contentLength += GetNodesLength(msg.Nodes, out nodesContentLength); } From 169bccac743e819a45e1cf2248093bca34679374 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 09:41:51 +0800 Subject: [PATCH 028/182] Fix build --- .../DiscoveryMessageSerializerTests.cs | 2 +- .../NodeLifecycleManagerTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs index ef9e96510bc4..ed8a2cdc9df6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs @@ -189,7 +189,7 @@ public void NeighborsMessageTest() Assert.That(deserializedMessage.FarPublicKey, Is.EqualTo(message.FarPublicKey)); Assert.That(deserializedMessage.ExpirationTime, Is.EqualTo(message.ExpirationTime)); - for (int i = 0; i < message.Nodes.Length; i++) + for (int i = 0; i < message.Nodes.Count; i++) { Assert.That(deserializedMessage.Nodes[i].Host, Is.EqualTo(message.Nodes[i].Host)); Assert.That(deserializedMessage.Nodes[i].Port, Is.EqualTo(message.Nodes[i].Port)); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs index 58731acc2972..4554065f49e8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs @@ -145,7 +145,7 @@ public async Task handling_findnode_msg_will_limit_result_to_12() Assert.That(sentMsg, Is.Not.Null); _nodeTable.Buckets[0].BondedItemsCount.Should().Be(32); - sentMsg!.Nodes.Length.Should().Be(12); + sentMsg!.Nodes.Count.Should().Be(12); } [Test] From dee49275babc41d2d574ec46a6d46d8d95a5b9f1 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 09:53:36 +0800 Subject: [PATCH 029/182] Fix some test --- .../Discv4/KademliaDiscv4AdapterTests.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 3ef80e9c47a3..cb018064b286 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -16,6 +17,7 @@ using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Enr; +using Nethermind.Network.Test.Builders; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -38,6 +40,10 @@ public class KademliaDiscv4AdapterTests private Node _testNode = null!; private PublicKey _testPublicKey = null!; + private IMessageSerializationService _receiverSerializationManager; + private IPEndPoint _receiverHost; + private Node _receiver; + [SetUp] public void Setup() { @@ -55,6 +61,13 @@ public void Setup() _timestamper.UnixTime.Returns(new UnixTime(new DateTime(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); _msgSender = Substitute.For(); + _receiverHost = IPEndPoint.Parse("192.168.1.2"); + _receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + SerializationBuilder builder = new SerializationBuilder(); + builder.WithDiscovery(TestItem.PrivateKeyB); + _receiverSerializationManager = builder.TestObject; + + _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), _networkConfig, @@ -77,26 +90,26 @@ public async Task TearDown() [CancelAfter(5000)] public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token) { - var receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - _msgSender .When(x => x.SendMsg(Arg.Any())) .Do(ci => { var sent = (PingMsg)ci[0]!; - sent.FarPublicKey = TestItem.PublicKeyA; - sent.Mdc = new byte[32]; // Normally set by serializer + var buffer = _receiverSerializationManager.ZeroSerialize(sent); + PingMsg msg = _receiverSerializationManager.Deserialize(buffer); + var pong = new PongMsg( - receiver.Address, + msg.FarPublicKey!, _timestamper.UnixTime.SecondsLong + 1, sent.Mdc!); + pong.FarAddress = _receiverHost; Task.Run(() => _adapter.OnIncomingMsg(pong)); }); - await _adapter.Ping(receiver, token); + await _adapter.Ping(_receiver, token); await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address))); + m.FarAddress!.Equals(_receiver.Address))); } [Test] @@ -232,7 +245,7 @@ await _kademliaMessageReceiver.Received(1).FindNeighbours( await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_testNode.Address) && - m.Nodes.Length == expectedNodes.Length)); + m.Nodes.Count == expectedNodes.Length)); } [Test] From 497c1c8aad6a913bc9dc929988b9140ebb6fd5f2 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 12:50:36 +0800 Subject: [PATCH 030/182] Fix unit tests --- .../Discv4/KademliaDiscv4AdapterTests.cs | 258 +++++++----------- .../Discv4/KademliaDiscv4Adapter.cs | 5 +- .../Discv4/NeighbourMsgHandler.cs | 9 +- 3 files changed, 104 insertions(+), 168 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index cb018064b286..1ec4581c129d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -11,6 +11,7 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; @@ -18,6 +19,7 @@ using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; +using Nethermind.Specs; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -41,9 +43,26 @@ public class KademliaDiscv4AdapterTests private PublicKey _testPublicKey = null!; private IMessageSerializationService _receiverSerializationManager; - private IPEndPoint _receiverHost; private Node _receiver; + private void ConfigureBondCallback() + { + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => + { + var sent = (PingMsg)ci[0]!; + var buffer = _receiverSerializationManager.ZeroSerialize(sent); + PingMsg msg = _receiverSerializationManager.Deserialize(buffer); + var pong = new PongMsg( + msg.FarPublicKey!, + _timestamper.UnixTime.SecondsLong + 1, + sent.Mdc!); + pong.FarAddress = _receiver.Address; + Task.Run(() => _adapter.OnIncomingMsg(pong)); + }); + } + [SetUp] public void Setup() { @@ -55,13 +74,14 @@ public void Setup() _networkConfig = Substitute.For(); _networkConfig.MaxActivePeers.Returns(25); _kademliaConfig = new KademliaConfig { CurrentNodeId = _testNode }; - _selfNodeRecord = Substitute.For(); + + _selfNodeRecord = CreateNodeRecord();; + _logManager = LimboLogs.Instance; _timestamper = Substitute.For(); _timestamper.UnixTime.Returns(new UnixTime(new DateTime(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); _msgSender = Substitute.For(); - _receiverHost = IPEndPoint.Parse("192.168.1.2"); _receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); SerializationBuilder builder = new SerializationBuilder(); builder.WithDiscovery(TestItem.PrivateKeyB); @@ -80,12 +100,40 @@ public void Setup() _adapter.MsgSender = _msgSender; } + private NodeRecord CreateNodeRecord() + { + NodeRecord selfNodeRecord = new(); + selfNodeRecord.SetEntry(IdEntry.Instance); + selfNodeRecord.SetEntry(new IpEntry(IPAddress.Parse("192.168.1.1"))); + selfNodeRecord.SetEntry(new TcpEntry(_networkConfig.P2PPort)); + selfNodeRecord.SetEntry(new UdpEntry(_networkConfig.DiscoveryPort)); + selfNodeRecord.SetEntry(new Secp256K1Entry(TestItem.PrivateKeyA.CompressedPublicKey)); + selfNodeRecord.EnrSequence = 1; + NodeRecordSigner enrSigner = new(new EthereumEcdsa(new MainnetSpecProvider()), TestItem.PrivateKeyA); + enrSigner.Sign(selfNodeRecord); + if (!enrSigner.Verify(selfNodeRecord)) + { + throw new NetworkingException("Self ENR initialization failed", NetworkExceptionType.Discovery); + } + + return selfNodeRecord; + } + [TearDown] public async Task TearDown() { await _adapter.DisposeAsync(); } + private T AddReceiverFarAddress(T msg) where T : DiscoveryMsg + { + var buffer = _receiverSerializationManager.ZeroSerialize(msg); + var farAddress = msg.FarAddress; + msg = _receiverSerializationManager.Deserialize(buffer); + msg.FarAddress = farAddress; + return msg; + } + [Test] [CancelAfter(5000)] public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token) @@ -102,7 +150,7 @@ public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token msg.FarPublicKey!, _timestamper.UnixTime.SecondsLong + 1, sent.Mdc!); - pong.FarAddress = _receiverHost; + pong.FarAddress = _receiver.Address; Task.Run(() => _adapter.OnIncomingMsg(pong)); }); @@ -114,48 +162,29 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => [Test] [CancelAfter(5000)] - public async Task FindNeighbours_should_ping_then_findnode_and_return_nodes(CancellationToken token) + public async Task FindNeighbours_should_return_nodes(CancellationToken token) { - // Arrange - var receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - var target = TestItem.PublicKeyC; - var expected = new[] { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; + var expected = Enumerable.Repeat(new Node(TestItem.PublicKeyD, "192.168.1.3", 30303), 16).ToArray(); - // Stub: replying to PingMsg with PongMsg - _msgSender - .When(x => x.SendMsg(Arg.Any())) - .Do(ci => - { - var sent = (PingMsg)ci[0]!; - var pong = new PongMsg( - receiver.Address, - _timestamper.UnixTime.SecondsLong + 1, - sent.Mdc!); - Task.Run(() => _adapter.OnIncomingMsg(pong)); - }); + ConfigureBondCallback(); - // Stub: replying to FindNodeMsg with NeighborsMsg _msgSender .When(x => x.SendMsg(Arg.Any())) .Do(ci => { - var sent = (FindNodeMsg)ci[0]!; - var neighbors = new NeighborsMsg( - receiver.Address, - _timestamper.UnixTime.SecondsLong + 1, - expected); + ArraySegment neighbours1 = expected[..12]; + + var neighbors = new NeighborsMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 1, neighbours1); + neighbors = AddReceiverFarAddress(neighbors); Task.Run(() => _adapter.OnIncomingMsg(neighbors)); - }); - // Act - Node[] result = await _adapter.FindNeighbours(receiver, target, token); + ArraySegment neighbours2 = expected[12..]; + var neighbors2 = new NeighborsMsg( _receiver.Address, _timestamper.UnixTime.SecondsLong + 1, neighbours2); + neighbors2 = AddReceiverFarAddress(neighbors2); + Task.Run(() => _adapter.OnIncomingMsg(neighbors2)); + }); - // Assert - await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address))); - await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address) && - m.SearchedNodeId!.SequenceEqual(target.Bytes))); + Node[] result = await _adapter.FindNeighbours(_receiver, TestItem.PublicKeyC, token); result.Should().BeEquivalentTo(expected); } @@ -163,187 +192,92 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => [CancelAfter(5000)] public async Task SendEnrRequest_should_ping_then_enr_request_and_return_response(CancellationToken token) { - // Arrange - var receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); var expectedResponse = new EnrResponseMsg( - receiver.Address, + _receiver.Address, _selfNodeRecord, new Hash256(new byte[32])); - // Stub: replying to PingMsg with PongMsg - _msgSender - .When(x => x.SendMsg(Arg.Any())) - .Do(ci => - { - var sent = (PingMsg)ci[0]!; - var pong = new PongMsg( - receiver.Address, - _timestamper.UnixTime.SecondsLong + 1, - sent.Mdc!); - Task.Run(() => _adapter.OnIncomingMsg(pong)); - }); + ConfigureBondCallback(); - // Stub: replying to EnrRequestMsg with EnrResponseMsg _msgSender .When(x => x.SendMsg(Arg.Any())) .Do(ci => { - Task.Run(() => _adapter.OnIncomingMsg(expectedResponse)); + var response = AddReceiverFarAddress(expectedResponse); + Task.Run(() => _adapter.OnIncomingMsg(response)); }); - // Act - EnrResponseMsg result = await _adapter.SendEnrRequest(receiver, token); + EnrResponseMsg result = await _adapter.SendEnrRequest(_receiver, token); - // Assert - await _msgSender.Received(1).SendMsg(Arg.Any()); - await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(receiver.Address))); - result.Should().Be(expectedResponse); + await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address))); + result.NodeRecord.Should().BeEquivalentTo(_selfNodeRecord); } [Test] public async Task OnIncomingMsg_ping_should_respond_with_pong() { - // Arrange - PingMsg pingMsg = new PingMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address, _testNode.Address, new byte[32]); + PingMsg pingMsg = new PingMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address); + pingMsg.FarAddress = _receiver.Address; + pingMsg = AddReceiverFarAddress(pingMsg); - // Act await _adapter.OnIncomingMsg(pingMsg); - // Assert - Allow some time for the async operation to complete - Task.Delay(100).Wait(); + await Task.Delay(100); - await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _testNode.Id), Arg.Any()); + await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _receiver.Id), Arg.Any()); await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(_testNode.Address) && + m.FarAddress!.Equals(_receiver.Address) && m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); } [Test] public async Task OnIncomingMsg_find_node_should_respond_with_neighbors() { - // Arrange - FindNodeMsg findNodeMsg = new FindNodeMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); + ConfigureBondCallback(); - Node[] expectedNodes = { new Node(TestItem.PublicKeyD, "192.168.1.3", 30303) }; + FindNodeMsg findNodeMsg = new FindNodeMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); + findNodeMsg = AddReceiverFarAddress(findNodeMsg); + + Node[] expectedNodes = Enumerable.Repeat(new Node(TestItem.PublicKeyD, "192.168.1.3", 30303), 16).ToArray(); _kademliaMessageReceiver.FindNeighbours( Arg.Any(), Arg.Any(), Arg.Any()) .Returns(expectedNodes); - // Act await _adapter.OnIncomingMsg(findNodeMsg); - // Assert - Allow some time for the async operation to complete - Task.Delay(100).Wait(); + await Task.Delay(100); await _kademliaMessageReceiver.Received(1).FindNeighbours( - Arg.Is(n => n.Id == _testNode.Id), + Arg.Is(n => n.Id == _receiver.Id), Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), Arg.Any()); + // Send out two message instead of one because of MTU limit. await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(_testNode.Address) && - m.Nodes.Count == expectedNodes.Length)); + m.FarAddress!.Equals(_receiver.Address) && + m.Nodes.Count == 12)); + await _msgSender.Received(1).SendMsg(Arg.Is(m => + m.FarAddress!.Equals(_receiver.Address) && + m.Nodes.Count == 4)); } [Test] public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response() { - // Arrange - EnrRequestMsg enrRequestMsg = new EnrRequestMsg(_testNode.Id, _timestamper.UnixTime.SecondsLong + 20); + ConfigureBondCallback(); + + EnrRequestMsg enrRequestMsg = new EnrRequestMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20); + enrRequestMsg = AddReceiverFarAddress(enrRequestMsg); - // Act await _adapter.OnIncomingMsg(enrRequestMsg); - // Assert - Allow some time for the async operation to complete Task.Delay(100).Wait(); await _msgSender.Received(1).SendMsg(Arg.Is(m => - m.FarAddress!.Equals(_testNode.Address) && + m.FarAddress!.Equals(_receiver.Address) && m.NodeRecord.Equals(_selfNodeRecord))); } - - [Test] - public async Task DisposeAsync_should_cancel_token_and_dispose_cancellation_token_source() - { - // Act - await _adapter.DisposeAsync(); - - // Assert - // This test is mostly to ensure the method doesn't throw exceptions - // The actual cancellation and disposal is hard to test directly - Assert.Pass("DisposeAsync completed without exceptions"); - } - - [Test] - public async Task EnsureIncomingBondedPeer_should_set_incoming_bond_deadline() - { - // Arrange - Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - - // Setup the message sender to respond with a pong when a ping is sent - _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => - { - PingMsg pingMsg = (PingMsg)x[0]; - PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); - }); - - // Act - Call a method that uses EnsureIncomingBondedPeer internally - EnrRequestMsg enrRequestMsg = new EnrRequestMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20); - - await _adapter.OnIncomingMsg(enrRequestMsg); - - // Wait for async operations to complete - await Task.Delay(100); - - // Assert - First call should send a ping - await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - - // Reset the received calls - _msgSender.ClearReceivedCalls(); - - // Act again - This should use the cached bond deadline - await _adapter.OnIncomingMsg(enrRequestMsg); - - // Wait for async operations to complete - await Task.Delay(100); - - // Assert - Second call should not send a ping because the node is already bonded - await _msgSender.DidNotReceive().SendMsg(Arg.Is(m => m.FarAddress!.Equals(receiver.Address))); - } - - [Test] - public async Task IsPeerSafe_should_return_true_after_ping_pong_exchange() - { - // Arrange - Node receiver = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); - - // Setup the message sender to respond with a pong when a ping is sent - _msgSender.When(x => x.SendMsg(Arg.Any())) - .Do(x => - { - PingMsg pingMsg = (PingMsg)x[0]; - PongMsg pongMsg = new PongMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20, pingMsg.Mdc!); - Task.Run(() => _adapter.OnIncomingMsg(pongMsg)); - }); - - // Act - Call a method that uses EnsureIncomingBondedPeer internally - EnrRequestMsg enrRequestMsg = new EnrRequestMsg(receiver.Id, _timestamper.UnixTime.SecondsLong + 20); - - await _adapter.OnIncomingMsg(enrRequestMsg); - - // Wait for async operations to complete - await Task.Delay(100); - - // Act - Check if the peer is now safe - // bool isSafe = _adapter.IsPeerSafe(receiver); - - // Assert - // Assert.That(isSafe, Is.True, "Peer should be considered safe after ping/pong exchange"); - } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index ad2045f05a89..6d8a9108021f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -265,10 +265,7 @@ private async Task HandlePing(Node node, PingMsg ping) { if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); await kademliaMessageReceiver.Value.Ping(node, _processCancellationToken); - // Generate MDC hash from the ping message - Rlp requestRlp = Rlp.Encode(Rlp.Encode(ping.ExpirationTime)); - byte[] mdc = Keccak.Compute(requestRlp.Bytes).Bytes.ToArray(); - PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), mdc); + PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); await SendMessage(node, msg); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs index ef5bf4963e42..2a3ece8b01aa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs @@ -17,9 +17,14 @@ public class NeighbourMsgHandler(int k) : ITaskCompleter public bool Handle(DiscoveryMsg msg) { NeighborsMsg neighborsMsg = (NeighborsMsg)msg; - if (_current.Length >= k || _current.Length + neighborsMsg.Nodes.Count > k) return false; - _current = _current.Concat(neighborsMsg.Nodes).ToArray(); + while (true) + { + Node[] current = _current; + if (current.Length >= k || current.Length + neighborsMsg.Nodes.Count > k) return false; + if (Interlocked.CompareExchange(ref _current, _current.Concat(neighborsMsg.Nodes).ToArray(), current) == current) break; + } + if (_current.Length == k) { TaskCompletionSource.TrySetResult(_current); From d1bba3a41264b3e065ed240599770840a832e990 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 13:16:18 +0800 Subject: [PATCH 031/182] Fix E2E --- .../Discv4/KademliaNodeSource.cs | 18 ++++++++++-------- .../NewTrackingLookupKNearestNeighbour.cs | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index f1edb4b2aa08..861411cba576 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -35,12 +35,6 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance int duplicated = 0; int total = 0; - void handler(object? _, Node addedNode) - { - _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); - ch.Writer.TryWrite(addedNode); - } - async Task DiscoverAsync(PublicKey target) { if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); @@ -117,7 +111,7 @@ async Task DiscoverAsync(PublicKey target) try { - kademlia.OnNodeAdded += handler; + kademlia.OnNodeAdded += Handler; await foreach (Node node in ch.Reader.ReadAllAsync(token)) { @@ -127,7 +121,15 @@ async Task DiscoverAsync(PublicKey target) finally { await discoverTask; - kademlia.OnNodeAdded -= handler; + kademlia.OnNodeAdded -= Handler; + } + + yield break; + + void Handler(object? _, Node addedNode) + { + _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); + ch.Writer.TryWrite(addedNode); // Ignore if channel full } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 54542e47863b..d2fbb4a95520 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -68,6 +68,8 @@ [EnumeratorCancellation] CancellationToken token { ValueHash256 nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); + + if (nodeHash == _currentNodeIdAsHash) continue; queryQueue.Enqueue((nodeHash, node), nodeHash); yield return node; From ce1541fc460b34b979864be02d0703d488305d9f Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 13:19:34 +0800 Subject: [PATCH 032/182] Slight cleanup --- .../Discv4/KademliaNodeSource.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 861411cba576..a3301388bd26 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -7,16 +7,15 @@ using System.Threading.Channels; using Nethermind.Core.Crypto; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -using Prometheus; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; +// TODO: Unit test, remove metric public class KademliaNodeSource( IKademlia kademlia, - IITeratorAlgo _lookup2, + IITeratorAlgo lookup2, KademliaDiscv4Adapter discv4Adapter, IDiscoveryConfig discoveryConfig, ILogManager logManager @@ -24,9 +23,6 @@ ILogManager logManager { ILogger _logger = logManager.GetClassLogger(); - private Counter _kademliaDiscoveredNodes = Prometheus.Metrics.CreateCounter("kademlia_discovered_nodes", "Discovered"); - private Counter _kademliaDiscoveredNodeStatus = Prometheus.Metrics.CreateCounter("kademlia_discovered_nodes_status", "Discovered", "status"); - public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); @@ -44,7 +40,7 @@ async Task DiscoverAsync(PublicKey target) ValueHash256 targetHash = target.Hash; Func> lookupOp = (nextNode, token) => discv4Adapter.FindNeighbours(nextNode, target, token); - await foreach (var node in _lookup2.Lookup(targetHash, 128, lookupOp!, token)) + await foreach (var node in lookup2.Lookup(targetHash, 128, lookupOp!, token)) { try { @@ -52,11 +48,9 @@ async Task DiscoverAsync(PublicKey target) } catch (OperationCanceledException) { - _kademliaDiscoveredNodeStatus.WithLabels("ping_timeout").Inc(); continue; } - _kademliaDiscoveredNodeStatus.WithLabels("ok").Inc(); anyFound = true; count++; total++; @@ -65,7 +59,6 @@ async Task DiscoverAsync(PublicKey target) duplicated++; continue; } - _kademliaDiscoveredNodes.Inc(); await ch.Writer.WriteAsync(node, token); } From 5d7712359771e5b1d19c6391f16dbe87a4226523 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 14:30:18 +0800 Subject: [PATCH 033/182] Hide some error --- .../Discv4/KademliaDiscv4Adapter.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 6d8a9108021f..617a7a5faa1c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -301,13 +301,17 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) case MsgType.EnrResponse: break; default: - _logger.Error($"Unsupported msgType: {msgType}"); + if (_logger.IsError) _logger.Error($"Unsupported msgType: {msgType}"); return; } } + catch (TaskCanceledException e) + { + if (_logger.IsDebug) _logger.Debug($"Error during msg handling. {e}"); + } catch (Exception e) { - _logger.Error("Error during msg handling", e); + if (_logger.IsError) _logger.Error("Error during msg handling", e); } } From c510776bb3f7139e4a4eebe47da45228fe5b8a05 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 14:37:28 +0800 Subject: [PATCH 034/182] Improved candidate count metric --- src/Nethermind/Nethermind.Network/PeerManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Nethermind/Nethermind.Network/PeerManager.cs b/src/Nethermind/Nethermind.Network/PeerManager.cs index 45becde8d336..e4deae044583 100644 --- a/src/Nethermind/Nethermind.Network/PeerManager.cs +++ b/src/Nethermind/Nethermind.Network/PeerManager.cs @@ -105,6 +105,7 @@ private void PeerPoolOnPeerAdded(object sender, PeerEventArgs nodeEventArgs) lock (_lock) { int newPeerPoolLength = _peerPool.PeerCount; + Metrics.PeerCandidateCount = newPeerPoolLength; _lastPeerPoolLength = newPeerPoolLength; if (_lastPeerPoolLength > _maxPeerPoolLength + 100) From 9b81905540ae300cb4ea042ba1192d3bd5314bbf Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 15:30:41 +0800 Subject: [PATCH 035/182] Experiment --- .../Discv4/DiscV4KademliaModule.cs | 8 +++- .../Discv4/KademliaDiscv4Adapter.cs | 44 ++++++++++++++----- .../Discv4/KademliaNodeSource.cs | 9 +++- .../Kademlia/IITeratorAlgo.cs | 1 + .../Kademlia/Kademlia.cs | 1 + .../NewTrackingLookupKNearestNeighbour.cs | 5 ++- 6 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 2ba1a4bbb58a..12b7f287677e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -21,9 +21,15 @@ protected override void Load(ContainerBuilder builder) .AddSingleton, NodeNodeHashProvider>() .AddSingleton(selfNodeRecord) .AddSingleton() - .AddSingleton(new KademliaConfig() + .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), + KSize = discoveryConfig.BucketSize, + Alpha = discoveryConfig.Concurrency, + Beta = discoveryConfig.BitsPerHop, + + LookupFindNeighbourHardTimout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), // TODO: This seems very low. + RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PongTimeout), BootNodes = bootNodes }) .AddSingleton, KademliaDiscv4Adapter>(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 617a7a5faa1c..95ebd2954e26 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -15,12 +15,14 @@ using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; using NonBlocking; +using Prometheus; namespace Nethermind.Network.Discovery.Discv4; public class KademliaDiscv4Adapter( Lazy> kademliaMessageReceiver, // Cyclic dependency INetworkConfig networkConfig, + IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, NodeRecord selfNodeRecord, ITimestamper timestamper, @@ -43,12 +45,11 @@ ILogManager logManager public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); - private readonly ConcurrentDictionary> _awaitingPongToNode = new(); // This is for waiting to send pong in attempt to authenticate. - private readonly LruCache _outgoingBondDeadline = new(1024 * 10, "outgoing_bond_deadline"); - private readonly LruCache _incomingBondDeadline = new(1024 * 10, "incoming_bond_deadline"); - private readonly ConcurrentDictionary _authenticatedRequestFailure = new(); // TODO: To lru cache + private readonly LruCache _outgoingBondDeadline = new(discoveryConfig.MaxNodeLifecycleManagersCount, "outgoing_bond_deadline"); + private readonly LruCache _incomingBondDeadline = new(discoveryConfig.MaxNodeLifecycleManagersCount, "incoming_bond_deadline"); + private readonly LruCache _authenticatedRequestFailure = new(discoveryConfig.MaxNodeLifecycleManagersCount, "authenticated_request_failure"); private readonly CancellationToken _processCancellationToken = processExitSource.Token; #region Authentication and utils @@ -88,7 +89,7 @@ private async Task EnsureOutgoingMessageBondedPeer(Node node, Cancellation bool TooManyFailure() { - return _authenticatedRequestFailure.TryGetValue(node.IdHash, out long failedFinedNodes) && failedFinedNodes > AuthenticatedRequestFailureLimit; + return _authenticatedRequestFailure.TryGet(node.IdHash, out long failedFinedNodes) && failedFinedNodes > AuthenticatedRequestFailureLimit; } } @@ -115,12 +116,16 @@ private async Task RunAuthenticatedRequest(Node node, Func DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); @@ -33,6 +37,7 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance async Task DiscoverAsync(PublicKey target) { + _discoverRound.Inc(); if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); bool anyFound = false; int count = 0; @@ -40,14 +45,16 @@ async Task DiscoverAsync(PublicKey target) ValueHash256 targetHash = target.Hash; Func> lookupOp = (nextNode, token) => discv4Adapter.FindNeighbours(nextNode, target, token); - await foreach (var node in lookup2.Lookup(targetHash, 128, lookupOp!, token)) + await foreach (var node in lookup2.Lookup(targetHash, 128, 1, lookupOp!, token)) { try { await discv4Adapter.Ping(node, token); + _discoverPingResult.WithLabels("ok").Inc(); } catch (OperationCanceledException) { + _discoverPingResult.WithLabels("timeout").Inc(); continue; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs index 6efecc180274..b79edc818c2f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs @@ -25,6 +25,7 @@ public interface IITeratorAlgo IAsyncEnumerable Lookup( ValueHash256 target, int minResult, + int alpha, Func> findNeighbourOp, CancellationToken token ); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index 144b3828b9c6..1d3b5930b221 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -124,6 +124,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } }); + // TODO: Gonna need to decide a better log for this. if (_logger.IsInfo) _logger.Info($"Online bootnodes: {onlineBootNodes}"); await LookupNodesClosest(_currentNodeIdAsKey, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index d2fbb4a95520..135187fb825d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -32,6 +32,7 @@ private bool SameAsSelf(TNode node) public async IAsyncEnumerable Lookup( ValueHash256 targetHash, int minResult, + int alpha, Func> findNeighbourOp, [EnumeratorCancellation] CancellationToken token ) { @@ -80,7 +81,7 @@ [EnumeratorCancellation] CancellationToken token } } - Task[] worker = Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => + Task[] worker = Enumerable.Range(0, alpha).Select((i) => Task.Run(async () => { var writer = outChan.Writer; while (!finished) @@ -132,7 +133,7 @@ [EnumeratorCancellation] CancellationToken token continue; } - Interlocked.Increment(ref minResult); + Interlocked.Increment(ref totalResult); await writer.WriteAsync(neighbour, cts.Token); using McsLock.Disposable _ = queueLock.Acquire(); From e9a90ecf553fada2cbbeae52fecafe477d64dfea Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 16:17:02 +0800 Subject: [PATCH 036/182] Remove unnecessary code --- .../Discv4/KademliaNodeSource.cs | 2 +- .../Kademlia/IITeratorAlgo.cs | 1 - .../NewTrackingLookupKNearestNeighbour.cs | 162 ++++++------------ 3 files changed, 56 insertions(+), 109 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 8145d26692fc..4c7ebcf52d36 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -45,7 +45,7 @@ async Task DiscoverAsync(PublicKey target) ValueHash256 targetHash = target.Hash; Func> lookupOp = (nextNode, token) => discv4Adapter.FindNeighbours(nextNode, target, token); - await foreach (var node in lookup2.Lookup(targetHash, 128, 1, lookupOp!, token)) + await foreach (var node in lookup2.Lookup(targetHash, 128, lookupOp!, token)) { try { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs index b79edc818c2f..6efecc180274 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs @@ -25,7 +25,6 @@ public interface IITeratorAlgo IAsyncEnumerable Lookup( ValueHash256 target, int minResult, - int alpha, Func> findNeighbourOp, CancellationToken token ); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 135187fb825d..4feae4db8656 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -32,7 +32,6 @@ private bool SameAsSelf(TNode node) public async IAsyncEnumerable Lookup( ValueHash256 targetHash, int minResult, - int alpha, Func> findNeighbourOp, [EnumeratorCancellation] CancellationToken token ) { @@ -48,21 +47,13 @@ [EnumeratorCancellation] CancellationToken token Hash256XorUtils.Compare(h1, h2, targetHash)); // Ordered by lowest distance. Will get popped for next round. - McsLock queueLock = new McsLock(); PriorityQueue<(ValueHash256, TNode), ValueHash256> queryQueue = new(comparer); - Channel outChan = Channel.CreateBounded(1); - - // Used for fast worker wake up when queue is empty - TaskCompletionSource roundComplete = new TaskCompletionSource(token); - int queryingTask = 0; - // Used to determine if the worker should stop ValueHash256 bestNodeId = ValueKeccak.Zero; int closestNodeRound = 0; int currentRound = 0; int totalResult = 0; - bool finished = false; // Check internal table first foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, null)) @@ -70,7 +61,6 @@ [EnumeratorCancellation] CancellationToken token ValueHash256 nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); - if (nodeHash == _currentNodeIdAsHash) continue; queryQueue.Enqueue((nodeHash, node), nodeHash); yield return node; @@ -81,102 +71,75 @@ [EnumeratorCancellation] CancellationToken token } } - Task[] worker = Enumerable.Range(0, alpha).Select((i) => Task.Run(async () => + while (true) { - var writer = outChan.Writer; - while (!finished) + token.ThrowIfCancellationRequested(); + if (!queryQueue.TryDequeue(out (ValueHash256 hash, TNode node) toQuery, out ValueHash256 hash256)) + { + // No node to query and running query. + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); + yield break; + } + + if (SameAsSelf(toQuery.node)) continue; + + queried.TryAdd(toQuery.hash, toQuery.node); + if (_logger.IsTrace) _logger.Trace($"Query {toQuery.node} at round {currentRound}"); + + TNode[]? neighbours = await WrappedFindNeighbourOp(toQuery.node); + if (neighbours == null || neighbours?.Length == 0) { - token.ThrowIfCancellationRequested(); - if (!TryGetNodeToQuery(out (ValueHash256 hash, TNode node)? toQuery)) + if (_logger.IsTrace) _logger.Trace("Empty result"); + continue; + } + + int queryIgnored = 0; + int seenIgnored = 0; + foreach (TNode neighbour in neighbours!) + { + ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + + // Already queried, we ignore + if (queried.ContainsKey(neighbourHash)) { - if (queryingTask > 0) - { - // Need to wait for all querying tasks first here. - await Task.WhenAny(roundComplete.Task, Task.Delay(100, token)); - continue; - } - - // No node to query and running query. - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); - break; + queryIgnored++; + continue; } - Interlocked.Increment(ref queryingTask); - try + // When seen already dont record + if (!seen.TryAdd(neighbourHash, neighbour)) { - queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); - if (_logger.IsTrace) _logger.Trace($"Query {toQuery.Value.node} at round {currentRound}, isself {SameAsSelf(toQuery.Value.node)}"); - TNode[]? neighbours = await WrappedFindNeighbourOp(toQuery.Value.node); - if (neighbours == null || neighbours?.Length == 0) - { - if (_logger.IsTrace) _logger.Trace("Empty result"); - continue; - } - - int queryIgnored = 0; - int seenIgnored = 0; - foreach (TNode neighbour in neighbours!) - { - ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); - - // Already queried, we ignore - if (queried.ContainsKey(neighbourHash)) - { - queryIgnored++; - continue; - } - - // When seen already dont record - if (!seen.TryAdd(neighbourHash, neighbour)) - { - seenIgnored++; - continue; - } - - Interlocked.Increment(ref totalResult); - await writer.WriteAsync(neighbour, cts.Token); - - using McsLock.Disposable _ = queueLock.Acquire(); - bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; - queryQueue.Enqueue((neighbourHash, neighbour), neighbourHash); - - // If found a better node, reset closes node round. - // This causes `ShouldStopDueToNoBetterResult` to return false. - if (closestNodeRound < currentRound && foundBetter) - { - if (_logger.IsTrace) _logger.Trace($"Found better neighbour {neighbour} at round {currentRound}."); - bestNodeId = neighbourHash; - closestNodeRound = currentRound; - } - } - if (_logger.IsTrace) _logger.Trace($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); - - if (ShouldStopDueToNoBetterResult()) - { - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); - break; - } + seenIgnored++; + continue; } - finally + + Interlocked.Increment(ref totalResult); + yield return neighbour; + + bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; + queryQueue.Enqueue((neighbourHash, neighbour), neighbourHash); + + // If found a better node, reset closes node round. + // This causes `ShouldStopDueToNoBetterResult` to return false. + if (closestNodeRound < currentRound && foundBetter) { - Interlocked.Decrement(ref queryingTask); - if (roundComplete.TrySetResult()) roundComplete = new TaskCompletionSource(token); + if (_logger.IsTrace) + _logger.Trace($"Found better neighbour {neighbour} at round {currentRound}."); + bestNodeId = neighbourHash; + closestNodeRound = currentRound; } } - outChan.Writer.TryComplete(); - }, token)).ToArray(); + if (_logger.IsTrace) + _logger.Trace($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); - await foreach (var node in outChan.Reader.ReadAllAsync(token)) - { - yield return node; + if (ShouldStopDueToNoBetterResult()) + { + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); + break; + } } - // When any of the worker is finished, we consider the whole query as done. - // This prevent this operation from hanging on a timed out request - await Task.WhenAny(worker); - finished = true; - if (_logger.IsTrace) _logger.Trace("Lookup operation finished."); yield break; @@ -206,21 +169,6 @@ [EnumeratorCancellation] CancellationToken token } } - bool TryGetNodeToQuery([NotNullWhen(true)] out (ValueHash256, TNode)? toQuery) - { - using McsLock.Disposable _ = queueLock.Acquire(); - if (queryQueue.Count == 0) - { - toQuery = default; - // No more node to query. - // Note: its possible that there are other worker currently which may add to bestSeen. - return false; - } - - toQuery = queryQueue.Dequeue(); - return true; - } - bool ShouldStopDueToNoBetterResult() { int round = Interlocked.Increment(ref currentRound); From f2325e282865297a849110fbc359f5e16a51e286 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:01:39 +0800 Subject: [PATCH 037/182] Send optional enr --- .../Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 95ebd2954e26..967fbb8c57df 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -209,6 +209,7 @@ public async Task Ping(Node receiver, CancellationToken token) token = cts.Token; PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); + msg.EnrSequence = selfNodeRecord.EnrSequence; // optional and does not seems to be used anywhere. _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, msg, token); } From 1a738605d6535518efc98ff358f12918aee56900 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:04:00 +0800 Subject: [PATCH 038/182] Fix test build --- .../Discv4/KademliaDiscv4AdapterTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 1ec4581c129d..f47abc9ab839 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -91,6 +91,7 @@ public void Setup() _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), _networkConfig, + new DiscoveryConfig(), _kademliaConfig, _selfNodeRecord, _timestamper, From 2302278dd548b85a60f42c7584c1e20413c39b63 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:08:52 +0800 Subject: [PATCH 039/182] Additional todo --- .../Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 967fbb8c57df..b232aa92a7ab 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -19,6 +19,7 @@ namespace Nethermind.Network.Discovery.Discv4; +// TODO: Hard rate limit. public class KademliaDiscv4Adapter( Lazy> kademliaMessageReceiver, // Cyclic dependency INetworkConfig networkConfig, From 18ac3f2a7ea0a20671ffd244cdd4be39b6a7cb20 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:09:05 +0800 Subject: [PATCH 040/182] Remove nodes locator --- .../NodesLocatorTests.cs | 94 ------- .../INodesLocator.cs | 23 -- .../NodesLocator.cs | 251 ------------------ 3 files changed, 368 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/NodesLocatorTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/INodesLocator.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/NodesLocator.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NodesLocatorTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NodesLocatorTests.cs deleted file mode 100644 index e2b957299d1d..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NodesLocatorTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Nethermind.Core; -using Nethermind.Core.Test.Builders; -using Nethermind.Core.Timers; -using Nethermind.Db; -using Nethermind.Logging; -using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Stats; -using Nethermind.Stats.Model; -using NSubstitute; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test -{ - [TestFixture] - public class NodesLocatorTests - { - private NodesLocator? _nodesLocator; - private NodeTable? _nodeTable; - private Node? _masterNode; - - [SetUp] - public void Setup() - { - NetworkConfig networkConfig = new(); - networkConfig.ExternalIp = IPAddress.Broadcast.ToString(); - - _masterNode = new Node(TestItem.PublicKeyA, IPAddress.Broadcast.ToString(), 30000); - DiscoveryConfig config = new() { DiscoveryNewCycleWaitTime = 1 }; - NodeDistanceCalculator distanceCalculator = new(config); - _nodeTable = new NodeTable(distanceCalculator, config, networkConfig, LimboLogs.Instance); - EvictionManager evictionManager = new(_nodeTable, LimboLogs.Instance); - ITimerFactory timerFactory = Substitute.For(); - NodeStatsManager nodeStatsManager = new(timerFactory, LimboLogs.Instance); - NodeLifecycleManagerFactory managerFactory = - new( - _nodeTable, - evictionManager, - nodeStatsManager, - new NodeRecord(), - config, - Timestamper.Default, - LimboLogs.Instance); - DiscoveryManager manager = new( - managerFactory, - _nodeTable, - new NetworkStorage(new MemDb(), LimboLogs.Instance), - config, - null, - LimboLogs.Instance); - _nodesLocator = new NodesLocator(_nodeTable, manager, config, LimboLogs.Instance); - } - - [Test] - public async Task Can_locate_nodes_when_no_nodes() - { - _nodesLocator!.Initialize(_masterNode!); - _nodeTable!.Initialize(_masterNode!.Id); - await _nodesLocator.LocateNodesAsync(CancellationToken.None); - } - - [TestCase(1)] - [TestCase(256)] - [TestCase(1024)] - public async Task Can_locate_nodes_when_some_nodes(int nodesCount) - { - _nodesLocator!.Initialize(_masterNode!); - _nodeTable!.Initialize(_masterNode!.Id); - - for (int i = 0; i < nodesCount; i++) - { - Node node = new(TestItem.PublicKeyA, IPAddress.Broadcast.ToString(), 30000 + i); - _nodeTable.AddNode(node); - } - - await _nodesLocator.LocateNodesAsync(CancellationToken.None); - } - - [Test] - public void Throws_when_uninitialized() - { - Assert.ThrowsAsync(() => _nodesLocator!.LocateNodesAsync(CancellationToken.None)); - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/INodesLocator.cs b/src/Nethermind/Nethermind.Network.Discovery/INodesLocator.cs deleted file mode 100644 index e4c2f14809e3..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/INodesLocator.cs +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery; - -public interface INodesLocator -{ - /// - /// locate nodes for master node - /// - Task LocateNodesAsync(CancellationToken cancellationToken); - - /// - /// locate nodes for specified node id - /// - Task LocateNodesAsync(byte[] searchedNodeId, CancellationToken cancellationToken); - - void Initialize(Node masterNode); - - bool ShouldThrottle { get; set; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodesLocator.cs b/src/Nethermind/Nethermind.Network.Discovery/NodesLocator.cs deleted file mode 100644 index 10d28e9d5454..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/NodesLocator.cs +++ /dev/null @@ -1,251 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Text; -using Nethermind.Core; -using Nethermind.Core.Collections; -using Nethermind.Core.Crypto; -using Nethermind.Core.Extensions; -using Nethermind.Logging; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery; - -public class NodesLocator : INodesLocator -{ - private readonly ILogger _logger; - private readonly INodeTable _nodeTable; - private readonly IDiscoveryManager _discoveryManager; - private readonly IDiscoveryConfig _discoveryConfig; - private Node? _masterNode; - - public bool ShouldThrottle { get; set; } - - public NodesLocator(INodeTable? nodeTable, IDiscoveryManager? discoveryManager, IDiscoveryConfig? discoveryConfig, ILogManager? logManager) - { - _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); - _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); - } - - public void Initialize(Node masterNode) - { - _masterNode = masterNode; - } - - public Task LocateNodesAsync(CancellationToken cancellationToken) - { - return LocateNodesAsync(null, cancellationToken); - } - - public async Task LocateNodesAsync(byte[]? searchedNodeId, CancellationToken cancellationToken) - { - if (_masterNode is null) - { - throw new InvalidOperationException("Master node has not been initialized"); - } - - ISet alreadyTriedNodes = new HashSet(); - - if (_logger.IsDebug) _logger.Debug($"Starting discovery process for node: {(searchedNodeId is not null ? $"randomNode: {new PublicKey(searchedNodeId).ToShortString()}" : $"masterNode: {_masterNode.Id}")}"); - int nodesCountBeforeDiscovery = NodesCountBeforeDiscovery; - - Node[] tryCandidates = new Node[_discoveryConfig.BucketSize]; // max bucket size here - for (int i = 0; i < _discoveryConfig.MaxDiscoveryRounds; i++) - { - if (ShouldThrottle) await Task.Delay(TimeSpan.FromSeconds(1)); - Array.Clear(tryCandidates, 0, tryCandidates.Length); - int candidatesCount; - - int attemptsCount = 0; - while (true) - { - candidatesCount = 0; - if (searchedNodeId is not null) - { - foreach (Node closestNode in _nodeTable.GetClosestNodes(searchedNodeId)) - { - if (alreadyTriedNodes.Contains(closestNode.IdHash)) - { - continue; - } - - tryCandidates[candidatesCount++] = closestNode; - if (candidatesCount > tryCandidates.Length - 1) - { - break; - } - } - } - else - { - foreach (Node closestNode in _nodeTable.GetClosestNodes()) - { - if (alreadyTriedNodes.Contains(closestNode.IdHash)) - { - continue; - } - - tryCandidates[candidatesCount++] = closestNode; - if (candidatesCount > tryCandidates.Length - 1) - { - break; - } - } - } - - if (attemptsCount++ > 20 || candidatesCount > 0) - { - break; - } - - if (_logger.IsTrace) _logger.Trace($"Waiting {_discoveryConfig.DiscoveryNewCycleWaitTime} for new nodes"); - - //we need to wait some time for pong messages received from new nodes we reached out to - try - { - await Task.Delay(_discoveryConfig.DiscoveryNewCycleWaitTime, cancellationToken); - } - catch (OperationCanceledException) - { - return; - } - } - - if (candidatesCount == 0) - { - if (_logger.IsTrace) _logger.Trace("No more closer candidates"); - break; - } - - int successRequestsCount = 0; - int failRequestCount = 0; - int nodesTriedCount = 0; - while (true) - { - int count = failRequestCount > 0 ? failRequestCount : _discoveryConfig.Concurrency; - IEnumerable nodesToSend = tryCandidates.Skip(nodesTriedCount).Take(count); - - using ArrayPoolList> sendFindNodeTasks = SendFindNodes(searchedNodeId, nodesToSend, alreadyTriedNodes).ToPooledList(count); - Result[] results = await Task.WhenAll(sendFindNodeTasks.AsSpan()); - - if (results.Length == 0) - { - if (_logger.IsDebug) _logger.Debug($"No more nodes to send, sent {successRequestsCount} successful requests, failedRequestCounter: {failRequestCount}, nodesTriedCounter: {nodesTriedCount}"); - break; - } - - nodesTriedCount += results.Length; - - foreach (Result? result in results) - { - if ((result?.ResultType ?? ResultType.Failure) == ResultType.Failure) - { - failRequestCount++; - } - else - { - successRequestsCount++; - } - } - - if (successRequestsCount >= _discoveryConfig.Concurrency) - { - if (_logger.IsTrace) _logger.Trace($"Sent {successRequestsCount} successful requests, failedRequestCounter: {failRequestCount}, nodesTriedCounter: {nodesTriedCount}"); - break; - } - } - } - - int nodesCountAfterDiscovery = 0; - var buckets = _nodeTable.Buckets; - for (int i = 0; i < buckets.Length; i++) - { - nodesCountAfterDiscovery += buckets[i].BondedItemsCount; - } - - if (_logger.IsDebug) _logger.Debug($"Finished discovery cycle, tried contacting {alreadyTriedNodes.Count} nodes. All nodes count before the process: {nodesCountBeforeDiscovery}, after the process: {nodesCountAfterDiscovery}"); - - if (_logger.IsTrace) - { - LogNodeTable(); - } - } - - private IEnumerable> SendFindNodes( - byte[]? searchedNodeId, - IEnumerable nodesToSend, - ISet alreadyTriedNodes) - { - foreach (Node? node in nodesToSend.Where(static n => n is not null)) - { - alreadyTriedNodes.Add(node!.IdHash); - yield return SendFindNode(node, searchedNodeId); - } - } - - private int NodesCountBeforeDiscovery - { - get - { - int nodesCountBeforeDiscovery = 0; - for (int index = 0; index < _nodeTable.Buckets.Length; index++) - { - NodeBucket x = _nodeTable.Buckets[index]; - nodesCountBeforeDiscovery += x.BondedItemsCount; - } - - return nodesCountBeforeDiscovery; - } - } - - private void LogNodeTable() - { - IEnumerable nonEmptyBuckets = _nodeTable.Buckets.Where(static x => x.AnyBondedItems()); - StringBuilder sb = new(); - - int length = 0; - int bondedItemsCount = 0; - - foreach (NodeBucket nodeBucket in nonEmptyBuckets) - { - length++; - int itemsCount = nodeBucket.BondedItemsCount; - bondedItemsCount += itemsCount; - sb.AppendLine($"Bucket: {nodeBucket.Distance}, count: {itemsCount}"); - foreach (NodeBucketItem bucketItem in nodeBucket.BondedItems) - { - sb.AppendLine($"{bucketItem.Node}, LastContactTime: {bucketItem.LastContactTime:yyyy-MM-dd HH:mm:ss:000}"); - } - } - - sb.Insert(0, $"------------------------------------------------------{Environment.NewLine}NodeTable, non-empty bucket count: {length}, total items count: {bondedItemsCount}"); - sb.AppendLine("------------------------------------------------------"); - _logger.Trace(sb.ToString()); - } - - private async Task SendFindNode(Node destinationNode, byte[]? searchedNodeId) - { - try - { - INodeLifecycleManager? nodeManager = _discoveryManager.GetNodeLifecycleManager(destinationNode); - - if (nodeManager is not null) - { - await nodeManager.SendFindNode(searchedNodeId ?? _masterNode!.Id.Bytes); - } - - return await _discoveryManager.WasMessageReceived(destinationNode.IdHash, MsgType.Neighbors, _discoveryConfig.SendNodeTimeout) - ? Result.Success - : Result.Fail($"Did not receive Neighbors response in time from: {destinationNode.Host}"); - } - catch (OperationCanceledException) - { - return Result.Fail("Cancelled"); - } - } -} From b85ce7eb5a9ab6d329ab22f8bda7a24bdeba51b3 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:22:38 +0800 Subject: [PATCH 041/182] Nodestats --- .../Discv4/KademliaDiscv4AdapterTests.cs | 2 + .../Discv4/KademliaDiscv4Adapter.cs | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index f47abc9ab839..3819e477f2fc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -20,6 +20,7 @@ using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; using Nethermind.Specs; +using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -94,6 +95,7 @@ public void Setup() new DiscoveryConfig(), _kademliaConfig, _selfNodeRecord, + Substitute.For(), _timestamper, Substitute.For(), _logManager diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index b232aa92a7ab..64364db93dd1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -13,6 +13,7 @@ using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; +using Nethermind.Stats; using Nethermind.Stats.Model; using NonBlocking; using Prometheus; @@ -26,6 +27,7 @@ public class KademliaDiscv4Adapter( IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, NodeRecord selfNodeRecord, + INodeStatsManager nodeStatsManager, ITimestamper timestamper, IProcessExitSource processExitSource, ILogManager logManager @@ -191,6 +193,7 @@ private async Task SendMessage(Node node, DiscoveryMsg msg) } } + RecordStatsForOutgoingMsg(node, msg); await sender.SendMsg(msg); } } @@ -279,6 +282,56 @@ private async Task HandlePing(Node node, PingMsg ping) private Counter _unhandledDiscoveryMesssage = Prometheus.Metrics.CreateCounter("unhandled_disc_message", "Unhaandled", "type"); + private void RecordStatsForIncomingMsg(Node node, DiscoveryMsg msg) + { + switch (msg.MsgType) + { + case MsgType.Ping: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingIn); + break; + case MsgType.FindNode: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeIn); + break; + case MsgType.EnrRequest: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestIn); + break; + case MsgType.Neighbors: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursIn); + break; + case MsgType.Pong: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongIn); + break; + case MsgType.EnrResponse: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseIn); + break; + } + } + + private void RecordStatsForOutgoingMsg(Node node, DiscoveryMsg msg) + { + switch (msg.MsgType) + { + case MsgType.Ping: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingOut); + break; + case MsgType.FindNode: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeOut); + break; + case MsgType.EnrRequest: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestOut); + break; + case MsgType.Neighbors: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursOut); + break; + case MsgType.Pong: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongOut); + break; + case MsgType.EnrResponse: + nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseOut); + break; + } + } + public async Task OnIncomingMsg(DiscoveryMsg msg) { try @@ -286,6 +339,7 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) if (_logger.IsTrace) _logger.Trace($"Received msg: {msg}"); MsgType msgType = msg.MsgType; Node node = new(msg.FarPublicKey, msg.FarAddress); + RecordStatsForIncomingMsg(node, msg); if (HandleViaMessageHandlers(node, msg)) { From 4f2d413ce2c31cdadd510f9ae6dbae276ccc12b7 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:31:40 +0800 Subject: [PATCH 042/182] Remove discovery manager --- .../DiscoveryManagerTests.cs | 214 --------- .../NettyDiscoveryHandlerTests.cs | 4 +- .../NodeLifecycleManagerTests.cs | 397 ----------------- .../DiscoveryManager.cs | 374 ---------------- .../IDiscoveryManager.cs | 25 -- .../Lifecycle/EvictionManager.cs | 79 ---- .../Lifecycle/EvictionPair.cs | 16 - .../Lifecycle/IEvictionManager.cs | 9 - .../Lifecycle/INodeLifecycleManager.cs | 29 -- .../Lifecycle/INodeLifecycleManagerFactory.cs | 14 - .../Lifecycle/NodeLifecycleManager.cs | 417 ------------------ .../Lifecycle/NodeLifecycleManagerFactory.cs | 61 --- .../Lifecycle/NodeLifecycleState.cs | 15 - .../NetworkStorageTests.cs | 44 +- 14 files changed, 19 insertions(+), 1679 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/IDiscoveryManager.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionManager.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionPair.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/IEvictionManager.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManager.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManagerFactory.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleState.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs deleted file mode 100644 index b7993f796cc8..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryManagerTests.cs +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using Nethermind.Core; -using Nethermind.Core.Crypto; -using Nethermind.Core.Test.Builders; -using Nethermind.Core.Timers; -using Nethermind.Crypto; -using Nethermind.Db; -using Nethermind.Logging; -using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Stats; -using Nethermind.Stats.Model; -using NSubstitute; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test -{ - [Parallelizable(ParallelScope.Self)] - [TestFixture] - public class DiscoveryManagerTests - { - private const string TestPrivateKeyHex = "0x3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"; - - private readonly INetworkConfig _networkConfig = new NetworkConfig(); - private IDiscoveryManager _discoveryManager = null!; - private IMsgSender _msgSender = null!; - private INodeTable _nodeTable = null!; - private const int Port = 1; - private const string Host = "192.168.1.17"; - private Node[] _nodes = null!; - private PublicKey _publicKey = null!; - - [SetUp] - public void Initialize() - { - SetupDiscoveryManager(); - } - - private void SetupDiscoveryManager(IDiscoveryConfig? config = null) - { - NetworkNodeDecoder.Init(); - PrivateKey privateKey = new(TestPrivateKeyHex); - _publicKey = privateKey.PublicKey; - LimboLogs? logManager = LimboLogs.Instance; - - IDiscoveryConfig discoveryConfig = config ?? new DiscoveryConfig(); - discoveryConfig.PongTimeout = 100; - - _msgSender = Substitute.For(); - NodeDistanceCalculator calculator = new(discoveryConfig); - - _networkConfig.ExternalIp = "99.10.10.66"; - _networkConfig.LocalIp = "10.0.0.5"; - - _nodeTable = new NodeTable(calculator, discoveryConfig, _networkConfig, logManager); - _nodeTable.Initialize(TestItem.PublicKeyA); - - EvictionManager evictionManager = new(_nodeTable, logManager); - ITimerFactory timerFactory = Substitute.For(); - NodeLifecycleManagerFactory lifecycleFactory = new(_nodeTable, evictionManager, - new NodeStatsManager(timerFactory, logManager), new NodeRecord(), discoveryConfig, Timestamper.Default, logManager); - - _nodes = new[] { new Node(TestItem.PublicKeyA, "192.168.1.18", 1), new Node(TestItem.PublicKeyB, "192.168.1.19", 2) }; - - IFullDb nodeDb = new SimpleFilePublicKeyDb("Test", "test_db", logManager); - _discoveryManager = new DiscoveryManager(lifecycleFactory, _nodeTable, new NetworkStorage(nodeDb, logManager), discoveryConfig, null, logManager); - _discoveryManager.MsgSender = _msgSender; - } - - [Test, Retry(3)] - public async Task OnPingMessageTest() - { - //receiving ping - IPEndPoint address = new(IPAddress.Parse(Host), Port); - await _discoveryManager.OnIncomingMsg(new PingMsg(_publicKey, GetExpirationTime(), address, _nodeTable.MasterNode!.Address, new byte[32]) { FarAddress = address }); - await Task.Delay(500); - - // expecting to send pong - await _msgSender.Received(1).SendMsg(Arg.Is(static m => m.FarAddress!.Address.ToString() == Host && m.FarAddress.Port == Port)); - - // send pings to new node - await _msgSender.Received().SendMsg(Arg.Is(static m => m.FarAddress!.Address.ToString() == Host && m.FarAddress.Port == Port)); - } - - [Test, Ignore("Add bonding"), Retry(3)] - public void OnPongMessageTest() - { - //receiving pong - ReceiveSomePong(); - - //expecting to activate node as valid peer - IEnumerable nodes = _nodeTable.GetClosestNodes().ToArray(); - Assert.That(nodes.Count(), Is.EqualTo(1)); - Node node = nodes.First(); - Assert.That(node.Host, Is.EqualTo(Host)); - Assert.That(node.Port, Is.EqualTo(Port)); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node); - Assert.That(manager?.State, Is.EqualTo(NodeLifecycleState.Active)); - } - - [Test, Ignore("Add bonding"), Retry(3)] - public void OnFindNodeMessageTest() - { - //receiving pong to have a node in the system - ReceiveSomePong(); - - //expecting to activate node as valid peer - IEnumerable nodes = _nodeTable.GetClosestNodes().ToArray(); - Assert.That(nodes.Count(), Is.EqualTo(1)); - Node node = nodes.First(); - Assert.That(node.Host, Is.EqualTo(Host)); - Assert.That(node.Port, Is.EqualTo(Port)); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node); - Assert.That(manager?.State, Is.EqualTo(NodeLifecycleState.Active)); - - //receiving findNode - FindNodeMsg msg = new(_publicKey, GetExpirationTime(), Build.A.PrivateKey.TestObject.PublicKey.Bytes); - msg.FarAddress = new IPEndPoint(IPAddress.Parse(Host), Port); - _discoveryManager.OnIncomingMsg(msg); - - //expecting to respond with sending Neighbors - _msgSender.Received(1).SendMsg(Arg.Is(static m => m.FarAddress!.Address.ToString() == Host && m.FarAddress.Port == Port)); - } - - [Test, Retry(3)] - public void MemoryTest() - { - //receiving pong to have a node in the system - for (int a = 0; a < 255; a++) - { - for (int b = 0; b < 255; b++) - { - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(new Node(TestItem.PublicKeyA, $"{a}.{b}.1.1", 8000)); - manager?.SendPingAsync(); - - PongMsg pongMsg = new(_publicKey, GetExpirationTime(), []); - pongMsg.FarAddress = new IPEndPoint(IPAddress.Parse($"{a}.{b}.1.1"), Port); - _discoveryManager.OnIncomingMsg(pongMsg); - } - } - } - - private static long GetExpirationTime() => Timestamper.Default.UnixTime.SecondsLong + 20; - - [Test, Ignore("Add bonding"), Retry(3)] - public async Task OnNeighborsMessageTest() - { - //receiving pong to have a node in the system - ReceiveSomePong(); - - //expecting to activate node as valid peer - IEnumerable nodes = _nodeTable.GetClosestNodes().ToArray(); - Assert.That(nodes.Count(), Is.EqualTo(1)); - Node node = nodes.First(); - Assert.That(node.Host, Is.EqualTo(Host)); - Assert.That(node.Port, Is.EqualTo(Port)); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node); - Assert.That(manager?.State, Is.EqualTo(NodeLifecycleState.Active)); - - //sending FindNode to expect Neighbors - await manager!.SendFindNode(_nodeTable.MasterNode!.Id.Bytes); - await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Address.ToString() == Host && m.FarAddress.Port == Port)); - - //receiving findNode - NeighborsMsg msg = new(_publicKey, GetExpirationTime(), _nodes); - msg.FarAddress = new IPEndPoint(IPAddress.Parse(Host), Port); - await _discoveryManager.OnIncomingMsg(msg); - - //expecting to send 3 pings to both nodes - await Task.Delay(600); - await _msgSender.Received(3).SendMsg(Arg.Is(m => m.FarAddress!.Address.ToString() == _nodes[0].Host && m.FarAddress.Port == _nodes[0].Port)); - await _msgSender.Received(3).SendMsg(Arg.Is(m => m.FarAddress!.Address.ToString() == _nodes[1].Host && m.FarAddress.Port == _nodes[1].Port)); - } - - private void ReceiveSomePong() - { - PongMsg pongMsg = new(_publicKey, GetExpirationTime(), []); - pongMsg.FarAddress = new IPEndPoint(IPAddress.Parse(Host), Port); - _discoveryManager.OnIncomingMsg(pongMsg); - } - - [Test] - [Repeat(10)] - public async Task RateLimitOutgoingMessage() - { - SetupDiscoveryManager(new DiscoveryConfig() - { - MaxOutgoingMessagePerSecond = 5 - }); - - long startTime = Stopwatch.GetTimestamp(); - FindNodeMsg msg = new(_publicKey, 0, []); - await _discoveryManager.SendMessageAsync(msg); - await _discoveryManager.SendMessageAsync(msg); - await _discoveryManager.SendMessageAsync(msg); - await _discoveryManager.SendMessageAsync(msg); - await _discoveryManager.SendMessageAsync(msg); - await _discoveryManager.SendMessageAsync(msg); - Stopwatch.GetElapsedTime(startTime).Should().BeGreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9)); - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs index f30fb41ed12f..980539a79ad9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs @@ -29,6 +29,7 @@ namespace Nethermind.Network.Discovery.Test [TestFixture] public class NettyDiscoveryHandlerTests { + /* private readonly PrivateKey _privateKey = new("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee"); private readonly PrivateKey _privateKey2 = new("3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"); private List _channels = new(); @@ -214,7 +215,7 @@ private async Task StartUdpChannel(string address, int port, IDiscoveryManager d _channels.Add(await bootstrap.BindAsync(IPAddress.Parse(address), port)); } - private void InitializeChannel(IDatagramChannel channel, IDiscoveryManager discoveryManager, IMessageSerializationService service) + private void InitializeChannel(IDatagramChannel channel, IDiscoveryMsgListener discoveryManager, IMessageSerializationService service) { NettyDiscoveryHandler handler = new(discoveryManager, channel, service, new Timestamper(), LimboLogs.Instance); handler.OnChannelActivated += (_, _) => @@ -232,5 +233,6 @@ private static async Task SleepWhileWaiting() { await Task.Delay((TestContext.CurrentContext.CurrentRepeatCount + 1) * 300); } + */ } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs deleted file mode 100644 index 4554065f49e8..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs +++ /dev/null @@ -1,397 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using MathNet.Numerics.Random; -using Nethermind.Config; -using Nethermind.Core; -using Nethermind.Core.Crypto; -using Nethermind.Core.Test.Builders; -using Nethermind.Core.Timers; -using Nethermind.Db; -using Nethermind.Logging; -using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Stats; -using Nethermind.Stats.Model; -using NSubstitute; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test; - -[Parallelizable(ParallelScope.Self)] -public class NodeLifecycleManagerTests -{ - private Signature[] _signatureMocks = []; - private PublicKey[] _nodeIds = []; - private INodeStats _nodeStatsMock = null!; - - private readonly INetworkConfig _networkConfig = new NetworkConfig(); - private IDiscoveryManager _discoveryManager = null!; - private IDiscoveryManager _discoveryManagerMock = null!; - private IDiscoveryConfig _discoveryConfigMock = null!; - private INodeTable _nodeTable = null!; - private IEvictionManager _evictionManagerMock = null!; - private ILogger _loggerMock = default; - private readonly int _port = 1; - private readonly string _host = "192.168.1.27"; - - [SetUp] - public void Setup() - { - _discoveryManagerMock = Substitute.For(); - _discoveryConfigMock = Substitute.For(); - - NetworkNodeDecoder.Init(); - SetupNodeIds(); - - LimboLogs? logManager = LimboLogs.Instance; - _loggerMock = new(Substitute.For()); - //setting config to store 3 nodes in a bucket and for table to have one bucket//setting config to store 3 nodes in a bucket and for table to have one bucket - - IConfigProvider configurationProvider = new ConfigProvider(); - _networkConfig.ExternalIp = "99.10.10.66"; - _networkConfig.LocalIp = "10.0.0.5"; - - IDiscoveryConfig discoveryConfig = configurationProvider.GetConfig(); - discoveryConfig.PongTimeout = 50; - discoveryConfig.BucketSize = 3; - discoveryConfig.BucketsCount = 1; - - NodeDistanceCalculator calculator = new(discoveryConfig); - - _nodeTable = new NodeTable(calculator, discoveryConfig, _networkConfig, logManager); - _nodeTable.Initialize(TestItem.PublicKeyA); - _nodeStatsMock = Substitute.For(); - - EvictionManager evictionManager = new(_nodeTable, logManager); - _evictionManagerMock = Substitute.For(); - ITimerFactory timerFactory = Substitute.For(); - NodeLifecycleManagerFactory lifecycleFactory = new(_nodeTable, evictionManager, - new NodeStatsManager(timerFactory, logManager), new NodeRecord(), discoveryConfig, Timestamper.Default, logManager); - - IMsgSender udpClient = Substitute.For(); - - SimpleFilePublicKeyDb discoveryDb = new("Test", "test", logManager); - _discoveryManager = new DiscoveryManager(lifecycleFactory, _nodeTable, new NetworkStorage(discoveryDb, logManager), discoveryConfig, null, logManager); - _discoveryManager.MsgSender = udpClient; - - _discoveryManagerMock = Substitute.For(); - _discoveryManagerMock.NodesFilter.Returns(new NodeFilter(16)); - } - - [Test] - public async Task sending_ping_receiving_proper_pong_sets_bounded() - { - Node node = new(TestItem.PublicKeyB, _host, _port); - NodeLifecycleManager nodeManager = new(node, _discoveryManagerMock - , _nodeTable, _evictionManagerMock, _nodeStatsMock, new NodeRecord(), _discoveryConfigMock, Timestamper.Default, _loggerMock); - - byte[] mdc = new byte[32]; - PingMsg? sentPing = null; - await _discoveryManagerMock.SendMessageAsync(Arg.Do(msg => - { - msg.Mdc = mdc; - sentPing = msg; - })); - - await nodeManager.SendPingAsync(); - nodeManager.ProcessPongMsg(new PongMsg(node.Address, GetExpirationTime(), sentPing!.Mdc!)); - - Assert.That(nodeManager.IsBonded, Is.True); - } - - [Test] - public async Task handling_findnode_msg_will_limit_result_to_12() - { - IDiscoveryConfig discoveryConfig = new DiscoveryConfig(); - discoveryConfig.PongTimeout = 50; - discoveryConfig.BucketSize = 32; - discoveryConfig.BucketsCount = 1; - - _nodeTable = new NodeTable(new NodeDistanceCalculator(discoveryConfig), discoveryConfig, _networkConfig, LimboLogs.Instance); - _nodeTable.Initialize(TestItem.PublicKeyA); - - Node node = new(TestItem.PublicKeyB, _host, _port); - NodeLifecycleManager nodeManager = new(node, _discoveryManagerMock, _nodeTable, _evictionManagerMock, _nodeStatsMock, new NodeRecord(), _discoveryConfigMock, Timestamper.Default, _loggerMock); - - await BondWithSelf(nodeManager, node); - - for (int i = 0; i < 32; i++) - { - _nodeTable.AddNode( - new Node( - new PublicKey(Random.Shared.NextBytes(64)), - "127.0.0.1", - i - )); - } - - NeighborsMsg? sentMsg = null; - _discoveryManagerMock.SendMessage(Arg.Do(msg => - { - sentMsg = msg; - })); - - nodeManager.ProcessFindNodeMsg(new FindNodeMsg(TestItem.PublicKeyA, 1, new byte[] { 0 })); - - Assert.That(sentMsg, Is.Not.Null); - _nodeTable.Buckets[0].BondedItemsCount.Should().Be(32); - sentMsg!.Nodes.Count.Should().Be(12); - } - - [Test] - public Task processNeighboursMessage_willCombineTwoSubsequentMessage() - => processNeighboursMessage_Test((pubkey, i) => new Node(pubkey, $"127.0.0.{i + 1}", 0), 16); - - [Test] - public Task processNeighboursMessage_willCombineDeduplicateMultipleIps() - => processNeighboursMessage_Test((pubkey, i) => new Node(pubkey, $"127.0.0.100", 0), 1); - - public async Task processNeighboursMessage_Test(Func createNode, int expectedCount) - { - IDiscoveryConfig discoveryConfig = new DiscoveryConfig(); - discoveryConfig.PongTimeout = 50; - discoveryConfig.BucketSize = 32; - discoveryConfig.BucketsCount = 1; - - _nodeTable = new NodeTable(new NodeDistanceCalculator(discoveryConfig), discoveryConfig, _networkConfig, LimboLogs.Instance); - _nodeTable.Initialize(TestItem.PublicKeyA); - - Node node = new(TestItem.PublicKeyB, _host, _port); - NodeLifecycleManager nodeManager = new(node, _discoveryManagerMock, _nodeTable, _evictionManagerMock, _nodeStatsMock, new NodeRecord(), _discoveryConfigMock, Timestamper.Default, _loggerMock); - - await BondWithSelf(nodeManager, node); - - _discoveryManagerMock - .Received(0) - .GetNodeLifecycleManager(Arg.Any(), Arg.Any()); - - await nodeManager.SendFindNode([]); - - Node[] firstNodes = TestItem.PublicKeys - .Take(12) - .Select(createNode) - .ToArray(); - NeighborsMsg firstNodeMsg = new NeighborsMsg(TestItem.PublicKeyA, 1, firstNodes); - Node[] secondNodes = TestItem.PublicKeys - .Skip(12) - .Take(4) - .Select((pubkey, i) => createNode(pubkey, i + 14)) - .ToArray(); - NeighborsMsg secondNodeMsg = new NeighborsMsg(TestItem.PublicKeyA, 1, secondNodes); - - nodeManager.ProcessNeighborsMsg(firstNodeMsg); - nodeManager.ProcessNeighborsMsg(secondNodeMsg); - - _discoveryManagerMock - .Received(expectedCount) - .GetNodeLifecycleManager(Arg.Any(), Arg.Any()); - } - - [Test] - public async Task sending_ping_receiving_incorrect_pong_does_not_bond() - { - Node node = new(TestItem.PublicKeyB, _host, _port); - NodeLifecycleManager nodeManager = new(node, _discoveryManagerMock - , _nodeTable, _evictionManagerMock, _nodeStatsMock, new NodeRecord(), _discoveryConfigMock, Timestamper.Default, _loggerMock); - - await nodeManager.SendPingAsync(); - nodeManager.ProcessPongMsg(new PongMsg(TestItem.PublicKeyB, GetExpirationTime(), new byte[] { 1, 1, 1 })); - - Assert.That(nodeManager.IsBonded, Is.False); - } - - [Test] - public void Wrong_pong_will_get_ignored() - { - Node node = new(TestItem.PublicKeyB, _host, _port); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node); - Assert.That(manager?.State, Is.EqualTo(NodeLifecycleState.New)); - - PongMsg msgI = new(_nodeIds[0], GetExpirationTime(), new byte[32]); - msgI.FarAddress = new IPEndPoint(IPAddress.Parse(_host), _port); - _discoveryManager.OnIncomingMsg(msgI); - - Assert.That(manager?.State, Is.EqualTo(NodeLifecycleState.New)); - } - - [Test] - [Retry(3)] - public async Task UnreachableStateTest() - { - Node node = new(TestItem.PublicKeyB, _host, _port); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node); - Assert.That(manager?.State, Is.EqualTo(NodeLifecycleState.New)); - - await Task.Delay(500); - - Assert.That(() => manager?.State, Is.EqualTo(NodeLifecycleState.Unreachable).After(500, 50)); - //Assert.AreEqual(NodeLifecycleState.Unreachable, manager.State); - } - - [Test, Retry(3), Ignore("Eviction changes were introduced and we would need to expose some internals to test bonding")] - public void EvictCandidateStateWonEvictionTest() - { - //adding 3 active nodes - List managers = new(); - for (int i = 0; i < 3; i++) - { - string host = "192.168.1." + i; - Node node = new(_nodeIds[i], host, _port); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node) ?? throw new Exception("Manager is null"); - managers.Add(manager); - Assert.That(manager.State, Is.EqualTo(NodeLifecycleState.New)); - - PongMsg msgI = new(_nodeIds[i], GetExpirationTime(), new byte[32]); - msgI.FarAddress = new IPEndPoint(IPAddress.Parse(_host), _port); - _discoveryManager.OnIncomingMsg(msgI); - Assert.That(manager.State, Is.EqualTo(NodeLifecycleState.New)); - } - - //table should contain 3 active nodes - IEnumerable closestNodes = _nodeTable.GetClosestNodes().ToArray(); - Assert.That(closestNodes.Count(x => x.Host == managers[0].ManagedNode.Host) == 0, Is.True); - Assert.That(closestNodes.Count(x => x.Host == managers[1].ManagedNode.Host) == 0, Is.True); - Assert.That(closestNodes.Count(x => x.Host == managers[2].ManagedNode.Host) == 0, Is.True); - - //adding 4th node - table can store only 3, eviction process should start - Node candidateNode = new(_nodeIds[3], _host, _port); - INodeLifecycleManager? candidateManager = _discoveryManager.GetNodeLifecycleManager(candidateNode); - - Assert.That(candidateManager?.State, Is.EqualTo(NodeLifecycleState.New)); - - PongMsg pongMsg = new(_nodeIds[3], GetExpirationTime(), new byte[32]); - pongMsg.FarAddress = new IPEndPoint(IPAddress.Parse(_host), _port); - _discoveryManager.OnIncomingMsg(pongMsg); - - Assert.That(candidateManager?.State, Is.EqualTo(NodeLifecycleState.New)); - INodeLifecycleManager evictionCandidate = managers.First(x => x.State == NodeLifecycleState.EvictCandidate); - - //receiving pong for eviction candidate - should survive - PongMsg msg = new(evictionCandidate.ManagedNode.Id, GetExpirationTime(), new byte[32]); - msg.FarAddress = new IPEndPoint(IPAddress.Parse(evictionCandidate.ManagedNode.Host), _port); - _discoveryManager.OnIncomingMsg(msg); - - //await Task.Delay(100); - - //3th node should survive, 4th node should be active but not in the table - Assert.That(() => candidateManager?.State, Is.EqualTo(NodeLifecycleState.ActiveExcluded).After(100, 50)); - Assert.That(() => evictionCandidate.State, Is.EqualTo(NodeLifecycleState.Active).After(100, 50)); - - //Assert.AreEqual(NodeLifecycleState.ActiveExcluded, candidateManager.State); - //Assert.AreEqual(NodeLifecycleState.Active, evictionCandidate.State); - closestNodes = _nodeTable.GetClosestNodes(); - Assert.That(() => closestNodes.Count(x => x.Host == managers[0].ManagedNode.Host) == 1, Is.True.After(100, 50)); - Assert.That(() => closestNodes.Count(x => x.Host == managers[1].ManagedNode.Host) == 1, Is.True.After(100, 50)); - Assert.That(() => closestNodes.Count(x => x.Host == managers[2].ManagedNode.Host) == 1, Is.True.After(100, 50)); - Assert.That(() => closestNodes.Count(x => x.Host == candidateNode.Host) == 0, Is.True.After(100, 50)); - - //Assert.IsTrue(closestNodes.Count(x => x.Host == managers[0].ManagedNode.Host) == 1); - //Assert.IsTrue(closestNodes.Count(x => x.Host == managers[1].ManagedNode.Host) == 1); - //Assert.IsTrue(closestNodes.Count(x => x.Host == managers[2].ManagedNode.Host) == 1); - //Assert.IsTrue(closestNodes.Count(x => x.Host == candidateNode.Host) == 0); - } - - private static long GetExpirationTime() => Timestamper.Default.UnixTime.SecondsLong + 20; - - [Test] - [Ignore("This test keeps failing and should be only manually enabled / understood when we review the discovery code")] - public void EvictCandidateStateLostEvictionTest() - { - //adding 3 active nodes - List managers = new(); - for (int i = 0; i < 3; i++) - { - string host = "192.168.1." + i; - Node node = new(_nodeIds[i], host, _port); - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node) ?? throw new Exception("Manager is null"); - managers.Add(manager); - Assert.That(manager.State, Is.EqualTo(NodeLifecycleState.New)); - - PongMsg msg = new(_nodeIds[i], GetExpirationTime(), new byte[32]); - msg.FarAddress = new IPEndPoint(IPAddress.Parse(_host), _port); - _discoveryManager.OnIncomingMsg(msg); - - Assert.That(manager.State, Is.EqualTo(NodeLifecycleState.Active)); - } - - //table should contain 3 active nodes - IEnumerable closestNodes = _nodeTable.GetClosestNodes().ToArray(); - for (int i = 0; i < 3; i++) - { - Assert.That(closestNodes.Count(x => x.Host == managers[0].ManagedNode.Host) == 1, Is.True); - } - - //adding 4th node - table can store only 3, eviction process should start - Node candidateNode = new(_nodeIds[3], _host, _port); - - INodeLifecycleManager? candidateManager = _discoveryManager.GetNodeLifecycleManager(candidateNode); - Assert.That(candidateManager?.State, Is.EqualTo(NodeLifecycleState.New)); - - PongMsg pongMsg = new(_nodeIds[3], GetExpirationTime(), new byte[32]); - pongMsg.FarAddress = new IPEndPoint(IPAddress.Parse(_host), _port); - _discoveryManager.OnIncomingMsg(pongMsg); - - //await Task.Delay(10); - Assert.That(() => candidateManager?.State, Is.EqualTo(NodeLifecycleState.Active).After(10, 5)); - //Assert.AreEqual(NodeLifecycleState.Active, candidateManager.State); - - INodeLifecycleManager evictionCandidate = managers.First(x => x.State == NodeLifecycleState.EvictCandidate); - //await Task.Delay(300); - - //3th node should be evicted, 4th node should be added to the table - //Assert.AreEqual(NodeLifecycleState.Active, candidateManager.State); - Assert.That(() => candidateManager?.State, Is.EqualTo(NodeLifecycleState.Active).After(300, 50)); - //Assert.AreEqual(NodeLifecycleState.Unreachable, evictionCandidate.State); - Assert.That(() => evictionCandidate.State, Is.EqualTo(NodeLifecycleState.Unreachable).After(300, 50)); - - closestNodes = _nodeTable.GetClosestNodes(); - Assert.That(() => managers.Where(x => x.State == NodeLifecycleState.Active).All(x => closestNodes.Any(y => y.Host == x.ManagedNode.Host)), Is.True.After(300, 50)); - Assert.That(() => closestNodes.Count(x => x.Host == evictionCandidate.ManagedNode.Host) == 0, Is.True.After(300, 50)); - Assert.That(() => closestNodes.Count(x => x.Host == candidateNode.Host) == 1, Is.True.After(300, 50)); - - //Assert.IsTrue(managers.Where(x => x.State == NodeLifecycleState.Active).All(x => closestNodes.Any(y => y.Host == x.ManagedNode.Host))); - //Assert.IsTrue(closestNodes.Count(x => x.Host == evictionCandidate.ManagedNode.Host) == 0); - //Assert.IsTrue(closestNodes.Count(x => x.Host == candidateNode.Host) == 1); - } - - private void SetupNodeIds() - { - _signatureMocks = new Signature[4]; - _nodeIds = new PublicKey[4]; - - for (int i = 0; i < 4; i++) - { - byte[] signatureBytes = new byte[65]; - signatureBytes[64] = (byte)i; - _signatureMocks[i] = new Signature(signatureBytes); - - byte[] nodeIdBytes = new byte[64]; - nodeIdBytes[63] = (byte)i; - _nodeIds[i] = new PublicKey(nodeIdBytes); - } - } - - private async Task BondWithSelf(NodeLifecycleManager nodeManager, Node node) - { - byte[] mdc = new byte[32]; - PingMsg? sentPing = null; - await _discoveryManagerMock.SendMessageAsync(Arg.Do(msg => - { - msg.Mdc = mdc; - sentPing = msg; - })); - await nodeManager.SendPingAsync(); - nodeManager.ProcessPongMsg(new PongMsg(node.Address, GetExpirationTime(), sentPing!.Mdc!)); - } - -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs deleted file mode 100644 index d9b92daf048e..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs +++ /dev/null @@ -1,374 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Nethermind.Config; -using Nethermind.Core; -using Nethermind.Core.Crypto; -using Nethermind.Logging; -using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery; - -public class DiscoveryManager : IDiscoveryManager -{ - private readonly IDiscoveryConfig _discoveryConfig; - private readonly RateLimiter _outgoingMessageRateLimiter; - private readonly ILogger _logger; - private readonly INodeLifecycleManagerFactory _nodeLifecycleManagerFactory; - private readonly ConcurrentDictionary _nodeLifecycleManagers = new(); - private readonly INodeTable _nodeTable; - private readonly INetworkStorage _discoveryStorage; - public NodeFilter NodesFilter { get; } - - private readonly ConcurrentDictionary> _waitingEvents = new(); - private readonly Func _createNodeLifecycleManager; - private readonly Func _createNodeLifecycleManagerPersisted; - private IMsgSender? _msgSender; - - public DiscoveryManager( - INodeLifecycleManagerFactory? nodeLifecycleManagerFactory, - INodeTable? nodeTable, - INetworkStorage? discoveryStorage, - IDiscoveryConfig? discoveryConfig, - INetworkConfig? networkConfig, - ILogManager? logManager) - { - _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _nodeLifecycleManagerFactory = nodeLifecycleManagerFactory ?? throw new ArgumentNullException(nameof(nodeLifecycleManagerFactory)); - _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); - _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); - _nodeLifecycleManagerFactory.DiscoveryManager = this; - _outgoingMessageRateLimiter = new RateLimiter(discoveryConfig.MaxOutgoingMessagePerSecond); - _createNodeLifecycleManager = GetLifecycleManagerFunc(isPersisted: false); - _createNodeLifecycleManagerPersisted = GetLifecycleManagerFunc(isPersisted: true); - - NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); - } - - public NodeRecord SelfNodeRecord => _nodeLifecycleManagerFactory.SelfNodeRecord; - private Func GetLifecycleManagerFunc(bool isPersisted) - { - return (_, node) => - { - Interlocked.Increment(ref _managersCreated); - INodeLifecycleManager manager = _nodeLifecycleManagerFactory.CreateNodeLifecycleManager(node); - manager.OnStateChanged += ManagerOnOnStateChanged; - if (!isPersisted) - { - _discoveryStorage.UpdateNodes(new[] { new NetworkNode(manager.ManagedNode.Id, manager.ManagedNode.Host, manager.ManagedNode.Port, manager.NodeStats.NewPersistedNodeReputation(DateTime.UtcNow)) }); - } - - return manager; - }; - } - - public IMsgSender MsgSender - { - set => _msgSender = value; - } - - public Task OnIncomingMsg(DiscoveryMsg msg) - { - try - { - if (_logger.IsTrace) _logger.Trace($"Received msg: {msg}"); - MsgType msgType = msg.MsgType; - - Node node = new(msg.FarPublicKey, msg.FarAddress); - INodeLifecycleManager? nodeManager = GetNodeLifecycleManager(node); - if (nodeManager is null) - { - return Task.CompletedTask; - } - - switch (msgType) - { - case MsgType.Neighbors: - nodeManager.ProcessNeighborsMsg((NeighborsMsg)msg); - break; - case MsgType.Pong: - nodeManager.ProcessPongMsg((PongMsg)msg); - break; - case MsgType.Ping: - PingMsg ping = (PingMsg)msg; - if (ValidatePingAddress(ping)) - { - nodeManager.ProcessPingMsg(ping); - } - break; - case MsgType.FindNode: - nodeManager.ProcessFindNodeMsg((FindNodeMsg)msg); - break; - case MsgType.EnrRequest: - nodeManager.ProcessEnrRequestMsg((EnrRequestMsg)msg); - break; - case MsgType.EnrResponse: - nodeManager.ProcessEnrResponseMsg((EnrResponseMsg)msg); - break; - default: - _logger.Error($"Unsupported msgType: {msgType}"); - return Task.CompletedTask; - } - - NotifySubscribersOnMsgReceived(msgType, nodeManager.ManagedNode, msg); - CleanUpLifecycleManagers(); - } - catch (Exception e) - { - _logger.Error("Error during msg handling", e); - } - - return Task.CompletedTask; - } - - private int _managersCreated; - - public INodeLifecycleManager? GetNodeLifecycleManager(Node node, bool isPersisted = false) - { - if (_nodeTable.MasterNode is null) - { - return null; - } - - if (_nodeTable.MasterNode.Equals(node)) - { - return null; - } - - if (node.Port == 0) - { - if (_logger.IsDebug) _logger.Debug($"Node is not listening - Port 0, blocking add to discovery, id: {node.Id}"); - return null; - } - - return _nodeLifecycleManagers.GetOrAdd(node.IdHash, isPersisted ? _createNodeLifecycleManagerPersisted : _createNodeLifecycleManager, node); - } - - private void ManagerOnOnStateChanged(object? sender, NodeLifecycleState e) - { - if (e == NodeLifecycleState.Active) - { - if (sender is INodeLifecycleManager manager) - { - manager.OnStateChanged -= ManagerOnOnStateChanged; - NodeDiscovered?.Invoke(this, new NodeEventArgs(manager.ManagedNode)); - } - } - } - - public void SendMessage(DiscoveryMsg discoveryMsg) - { -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - SendMessageAsync(discoveryMsg); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - - public async Task SendMessageAsync(DiscoveryMsg discoveryMsg) - { - if (_logger.IsTrace) _logger.Trace($"Sending msg: {discoveryMsg}"); - try - { - if (_msgSender is null) return; - await _outgoingMessageRateLimiter.WaitAsync(CancellationToken.None); - await _msgSender.SendMsg(discoveryMsg); - } - catch (Exception e) - { - _logger.Error($"Error during sending message: {discoveryMsg}", e); - } - } - - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] - public async ValueTask WasMessageReceived(Hash256 senderIdHash, MsgType msgType, int timeout) - { - TaskCompletionSource completionSource = GetCompletionSource(senderIdHash, (int)msgType); - CancellationTokenSource delayCancellation = new(); - Task firstTask = await Task.WhenAny(completionSource.Task, Task.Delay(timeout, delayCancellation.Token)); - - bool result = firstTask == completionSource.Task; - if (result) - { - delayCancellation.Cancel(); - } - else - { - RemoveCompletionSource(senderIdHash, (int)msgType); - } - - return result; - } - - public event EventHandler? NodeDiscovered; - - public IReadOnlyCollection GetNodeLifecycleManagers() - { - return _nodeLifecycleManagers.Values.ToArray(); - } - - public IReadOnlyCollection GetOrAddNodeLifecycleManagers(Func query) - { - return _nodeLifecycleManagers.Values.Where(query.Invoke).ToArray(); - } - - private bool ValidatePingAddress(PingMsg msg) - { - if (msg.DestinationAddress is null || msg.FarAddress is null) - { - if (_logger.IsDebug) _logger.Debug($"Received a ping message with empty address, message: {msg}"); - return false; - } - - #region - // port will be different as we dynamically open ports for each socket connection - // if (_nodeTable.MasterNode.Port != message.DestinationAddress?.Port) - // { - // throw new NetworkingException($"Received message with incorrect destination port, message: {message}", NetworkExceptionType.Discovery); - // } - - // either an old Nethermind or other nodes that make the same mistake - // if (!Bytes.AreEqual(message.FarAddress?.Address.MapToIPv6().GetAddressBytes(), message.SourceAddress?.Address.MapToIPv6().GetAddressBytes())) - // { - // // there is no sense to complain here as nodes sent a lot of garbage as their source addresses - // // if (_logger.IsWarn) _logger.Warn($"Received message with incorrect source address {message.SourceAddress}, message: {message}"); - // } - - // if (message.FarAddress?.Port != message.SourceAddress?.Port) - // { - // // there is no sense to complain here as nodes sent a lot of garbage as their source addresses - // // if (_logger.IsWarn) _logger.Warn($"TRACE/WARN Received a message with incorrect source port, message: {message}"); - // } - #endregion - - return true; - } - - private void NotifySubscribersOnMsgReceived(MsgType msgType, Node node, DiscoveryMsg msg) - { - TaskCompletionSource? completionSource = RemoveCompletionSource(node.IdHash, (int)msgType); - completionSource?.TrySetResult(msg); - } - - private TaskCompletionSource GetCompletionSource(Hash256 senderAddressHash, int messageType) - { - MessageTypeKey key = new(senderAddressHash, messageType); - TaskCompletionSource completionSource = _waitingEvents.GetOrAdd(key, new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); - return completionSource; - } - - private TaskCompletionSource? RemoveCompletionSource(Hash256 senderAddressHash, int messageType) - { - MessageTypeKey key = new(senderAddressHash, messageType); - return _waitingEvents.TryRemove(key, out TaskCompletionSource? completionSource) ? completionSource : null; - } - - private void CleanUpLifecycleManagers() - { - int toRemove = (_nodeLifecycleManagers.Count - _discoveryConfig.MaxNodeLifecycleManagersCount) + _discoveryConfig.NodeLifecycleManagersCleanupCount; - if (toRemove <= _discoveryConfig.NodeLifecycleManagersCleanupCount / 2) - { - return; - } - - int remainingToRemove = toRemove; - foreach ((Hash256 key, INodeLifecycleManager value) in _nodeLifecycleManagers) - { - if (value.State == NodeLifecycleState.ActiveExcluded) - { - if (RemoveManager((key, value.ManagedNode.Id))) - { - remainingToRemove--; - if (remainingToRemove <= 0) - { - if (_logger.IsDebug) _logger.Debug($"Cleaned up {toRemove} discovery lifecycle managers."); - return; - } - } - } - } - - foreach ((Hash256 key, INodeLifecycleManager value) in _nodeLifecycleManagers) - { - if (value.State == NodeLifecycleState.Unreachable) - { - if (RemoveManager((key, value.ManagedNode.Id))) - { - remainingToRemove--; - if (remainingToRemove <= 0) - { - if (_logger.IsDebug) _logger.Debug($"Cleaned up {toRemove} discovery lifecycle managers."); - return; - } - } - } - } - DateTime utcNow = DateTime.UtcNow; - foreach ((Hash256 key, INodeLifecycleManager value) in _nodeLifecycleManagers.ToArray() - .OrderBy(x => x.Value.NodeStats.CurrentNodeReputation(utcNow))) - { - if (RemoveManager((key, value.ManagedNode.Id))) - { - remainingToRemove--; - if (remainingToRemove <= 0) - { - if (_logger.IsDebug) _logger.Debug($"Cleaned up {toRemove} discovery lifecycle managers."); - return; - } - } - } - - if (_logger.IsDebug) _logger.Debug($"Cleaned up {toRemove - remainingToRemove} discovery lifecycle managers."); - } - - private bool RemoveManager((Hash256 Hash, PublicKey Key) item) - { - if (_nodeLifecycleManagers.TryRemove(item.Hash, out _)) - { - _discoveryStorage.RemoveNode(item.Key); - return true; - } - - return false; - } - - private readonly struct MessageTypeKey : IEquatable - { - public Hash256 SenderAddressHash { get; } - - public int MessageType { get; } - - public MessageTypeKey(Hash256 senderAddressHash, int messageType) - { - SenderAddressHash = senderAddressHash; - MessageType = messageType; - } - - public bool Equals(MessageTypeKey other) - { - return SenderAddressHash.Equals(other.SenderAddressHash) && MessageType == other.MessageType; - } - - public override bool Equals(object? obj) - { - if (obj is null) return false; - return obj is MessageTypeKey key && Equals(key); - } - - [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] - public override int GetHashCode() - { - unchecked - { - return ((SenderAddressHash is not null ? SenderAddressHash.GetHashCode() : 0) * 397) ^ MessageType; - } - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryManager.cs b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryManager.cs deleted file mode 100644 index 0fc852bde08b..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryManager.cs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Enr; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery; - -public interface IDiscoveryManager : IDiscoveryMsgListener -{ - IMsgSender MsgSender { set; } - INodeLifecycleManager? GetNodeLifecycleManager(Node node, bool isPersisted = false); - void SendMessage(DiscoveryMsg discoveryMsg); - Task SendMessageAsync(DiscoveryMsg discoveryMsg); - ValueTask WasMessageReceived(Hash256 senderIdHash, MsgType msgType, int timeout); - event EventHandler NodeDiscovered; - - IReadOnlyCollection GetNodeLifecycleManagers(); - IReadOnlyCollection GetOrAddNodeLifecycleManagers(Func query); - NodeFilter NodesFilter { get; } - NodeRecord SelfNodeRecord { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionManager.cs deleted file mode 100644 index 1d9bba2e5ecb..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionManager.cs +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Collections.Concurrent; -using Nethermind.Core.Crypto; -using Nethermind.Logging; -using Nethermind.Network.Discovery.RoutingTable; - -namespace Nethermind.Network.Discovery.Lifecycle; - -public class EvictionManager : IEvictionManager -{ - private readonly ConcurrentDictionary _evictionPairs = new(); - private readonly INodeTable _nodeTable; - private readonly ILogger _logger; - - public EvictionManager(INodeTable nodeTable, ILogManager logManager) - { - _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _nodeTable = nodeTable; - } - - public void StartEvictionProcess(INodeLifecycleManager evictionCandidate, INodeLifecycleManager replacementCandidate) - { - if (_logger.IsTrace) _logger.Trace($"Starting eviction process, evictionCandidate: {evictionCandidate.ManagedNode}, replacementCandidate: {replacementCandidate.ManagedNode}"); - - EvictionPair newPair = new(evictionCandidate, replacementCandidate); - EvictionPair? pair = _evictionPairs.GetOrAdd(evictionCandidate.ManagedNode.IdHash, newPair); - if (pair != newPair) - { - //existing eviction in process - //TODO add queue for further evictions - if (_logger.IsTrace) _logger.Trace($"Existing eviction in process, evictionCandidate: {evictionCandidate.ManagedNode}, replacementCandidate: {replacementCandidate.ManagedNode}"); - return; - } - - evictionCandidate.StartEvictionProcess(); - evictionCandidate.OnStateChanged += OnStateChange; - } - - private void OnStateChange(object? sender, NodeLifecycleState state) - { - if (sender is not INodeLifecycleManager evictionCandidate) - { - return; - } - - if (!_evictionPairs.TryGetValue(evictionCandidate.ManagedNode.IdHash, out EvictionPair? evictionPair)) - { - return; - } - - if (evictionPair is null) - { - return; - } - - if (state == NodeLifecycleState.Active) - { - //survived eviction - if (_logger.IsTrace) _logger.Trace($"Survived eviction process, evictionCandidate: {evictionCandidate.ManagedNode}, replacementCandidate: {evictionPair.ReplacementCandidate.ManagedNode}"); - evictionPair.ReplacementCandidate.LostEvictionProcess(); - CloseEvictionProcess(evictionCandidate); - } - else if (state == NodeLifecycleState.Unreachable) - { - //lost eviction, being replaced in nodeTable - _nodeTable.ReplaceNode(evictionCandidate.ManagedNode, evictionPair.ReplacementCandidate.ManagedNode); - if (_logger.IsTrace) _logger.Trace($"Lost eviction process, evictionCandidate: {evictionCandidate.ManagedNode}, replacementCandidate: {evictionPair.ReplacementCandidate.ManagedNode}"); - CloseEvictionProcess(evictionCandidate); - } - } - - private void CloseEvictionProcess(INodeLifecycleManager evictionCandidate) - { - evictionCandidate.OnStateChanged -= OnStateChange; - _evictionPairs.TryRemove(evictionCandidate.ManagedNode.IdHash, out EvictionPair? _); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionPair.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionPair.cs deleted file mode 100644 index 614d0e1049a3..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/EvictionPair.cs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Lifecycle; - -public class EvictionPair -{ - public EvictionPair(INodeLifecycleManager evictionCandidate, INodeLifecycleManager replacementCandidate) - { - EvictionCandidate = evictionCandidate; - ReplacementCandidate = replacementCandidate; - } - - public INodeLifecycleManager EvictionCandidate { get; init; } - public INodeLifecycleManager ReplacementCandidate { get; init; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/IEvictionManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/IEvictionManager.cs deleted file mode 100644 index 0b4eff2d7a56..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/IEvictionManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Lifecycle; - -public interface IEvictionManager -{ - void StartEvictionProcess(INodeLifecycleManager evictionCandidate, INodeLifecycleManager replacementCandidate); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManager.cs deleted file mode 100644 index 235a5c767045..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Network.Discovery.Messages; -using Nethermind.Stats; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Lifecycle; - -public interface INodeLifecycleManager -{ - Node ManagedNode { get; } - INodeStats NodeStats { get; } - NodeLifecycleState State { get; } - void ProcessPingMsg(PingMsg pingMsg); - void ProcessPongMsg(PongMsg pongMsg); - void ProcessNeighborsMsg(NeighborsMsg msg); - void ProcessFindNodeMsg(FindNodeMsg msg); - void ProcessEnrRequestMsg(EnrRequestMsg enrRequestMessage); - void ProcessEnrResponseMsg(EnrResponseMsg msg); - Task SendFindNode(byte[] searchedNodeId); - Task SendPingAsync(); - - void StartEvictionProcess(); - void LostEvictionProcess(); - void ResetUnreachableStatus(); - - event EventHandler OnStateChanged; -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManagerFactory.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManagerFactory.cs deleted file mode 100644 index d7aa53b34b8a..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/INodeLifecycleManagerFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Network.Enr; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Lifecycle; - -public interface INodeLifecycleManagerFactory -{ - INodeLifecycleManager CreateNodeLifecycleManager(Node node); - IDiscoveryManager DiscoveryManager { set; } - NodeRecord SelfNodeRecord { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs deleted file mode 100644 index af13f43a036d..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs +++ /dev/null @@ -1,417 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Net; -using Nethermind.Core; -using Nethermind.Core.Crypto; -using Nethermind.Core.Extensions; -using Nethermind.Logging; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Serialization.Rlp; -using Nethermind.Stats; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Lifecycle; - -public class NodeLifecycleManager : INodeLifecycleManager -{ - private readonly static IPAddress _localhost = IPAddress.Parse("127.0.0.1"); - private readonly IDiscoveryManager _discoveryManager; - private readonly INodeTable _nodeTable; - private readonly ILogger _logger; - private readonly IDiscoveryConfig _discoveryConfig; - private readonly ITimestamper _timestamper; - private readonly IEvictionManager _evictionManager; - private readonly NodeRecord _nodeRecord; - - /// - /// This is the value set by other clients based on real network tests. - /// - private const int ExpirationTimeInSeconds = 20; - - private PingMsg? _lastSentPing; - private bool _isNeighborsExpected; - - // private bool _receivedPing; - private bool _sentPing; - // private bool _sentPong; - private bool _receivedPong; - - private int _lastNeighbourSize = 0; - - public NodeLifecycleManager(Node node, - IDiscoveryManager discoveryManager, - INodeTable nodeTable, - IEvictionManager evictionManager, - INodeStats nodeStats, - NodeRecord nodeRecord, - IDiscoveryConfig discoveryConfig, - ITimestamper timestamper, - ILogger logger) - { - _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); - _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); - _logger = logger; - _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - _evictionManager = evictionManager ?? throw new ArgumentNullException(nameof(evictionManager)); - _nodeRecord = nodeRecord ?? throw new ArgumentNullException(nameof(nodeRecord)); - NodeStats = nodeStats ?? throw new ArgumentNullException(nameof(nodeStats)); - ManagedNode = node; - UpdateState(NodeLifecycleState.New); - } - - public Node ManagedNode { get; } - public NodeLifecycleState State { get; private set; } - public INodeStats NodeStats { get; } - public bool IsBonded => _sentPing && _receivedPong; - - public event EventHandler? OnStateChanged; - - public void ProcessPingMsg(PingMsg pingMsg) - { - // _receivedPing = true; - SendPong(pingMsg); - if (pingMsg.EnrSequence is not null && pingMsg.EnrSequence > _lastEnrSequence) - { - SendEnrRequest(); - } - - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingIn); - RefreshNodeContactTime(); - } - - private void SendEnrRequest() - { - EnrRequestMsg msg = new(ManagedNode.Address, CalculateExpirationTime()); - _discoveryManager.SendMessage(msg); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestOut); - } - - public void ProcessEnrResponseMsg(EnrResponseMsg enrResponseMsg) - { - if (!IsBonded) - { - return; - } - _lastEnrSequence = enrResponseMsg.NodeRecord.EnrSequence; - - // TODO: 6) use the fork ID knowledge to mark each node with info on the forkhash - - // Enr.ForkId? forkId = enrResponseMsg.NodeRecord.GetValue(EnrContentKey.Eth); - // if (forkId is not null) - // { - // _logger.Warn($"Discovered new node with forkId {forkId.Value.ForkHash.ToHexString()}"); - // } - - OnStateChanged?.Invoke(this, NodeLifecycleState.ActiveWithEnr); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseIn); - } - - public void ProcessEnrRequestMsg(EnrRequestMsg enrRequestMessage) - { - if (IsBonded) - { - Rlp requestRlp = Rlp.Encode(Rlp.Encode(enrRequestMessage.ExpirationTime)); - EnrResponseMsg msg = new(ManagedNode.Address, _nodeRecord, Keccak.Compute(requestRlp.Bytes)); - _discoveryManager.SendMessage(msg); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestIn); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseOut); - } - else - { - if (_logger.IsDebug) _logger.Debug("Attempt to request ENR before bonding"); - } - } - - public void ProcessPongMsg(PongMsg pongMsg) - { - PingMsg? sentPingMsg = Interlocked.Exchange(ref _lastSentPing, null); - if (sentPingMsg is null) - { - return; - } - - if (Bytes.AreEqual(sentPingMsg.Mdc, pongMsg.PingMdc)) - { - _receivedPong = true; - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongIn); - if (IsBonded) - { - UpdateState(NodeLifecycleState.Active); - if (_logger.IsTrace) _logger.Trace($"Bonded with {ManagedNode.Host}"); - } - else - { - if (_logger.IsTrace) _logger.Trace($"Bonding with {ManagedNode} failed."); - } - - RefreshNodeContactTime(); - } - else - { - if (_logger.IsTrace) _logger.Trace($"Unmatched MDC when bonding with {ManagedNode}"); - // ignore spoofed message - _receivedPong = false; - } - } - - public void ProcessNeighborsMsg(NeighborsMsg? msg) - { - if (msg is null) - { - return; - } - - if (!IsBonded) - { - return; - } - - if (_lastNeighbourSize + msg.Nodes.Count == 16) - { - // Turns out, other client will split the neighbour msg to two msg, whose size sum up to 16. - // Happens about 70% of the time. - ProcessNodes(msg); - } - else if (_isNeighborsExpected) - { - ProcessNodes(msg); - } - - _lastNeighbourSize = msg.Nodes.Count; - _isNeighborsExpected = false; - } - - private void ProcessNodes(NeighborsMsg msg) - { - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursIn); - RefreshNodeContactTime(); - - IPAddress? externalIp = _discoveryManager.SelfNodeRecord?.GetObj(EnrContentKey.Ip); - foreach (Node node in msg.Nodes) - { - if (node.Address.Address == _localhost) - { - if (_logger.IsTrace) - _logger.Trace($"Received localhost as node address from: {msg.FarPublicKey}, node: {node}"); - continue; - } - if (node.Address.Address == externalIp) - { - if (_logger.IsTrace) - _logger.Trace($"Received self as node address from: {msg.FarPublicKey}, node: {node}"); - // Ignore self - continue; - } - else if (!_discoveryManager.NodesFilter.Set(node.Address.Address)) - { - // Already seen this node ip recently - continue; - } - //If node is new it will create a new nodeLifecycleManager and will update state to New, which will trigger Ping - _discoveryManager.GetNodeLifecycleManager(node); - } - } - - public void ProcessFindNodeMsg(FindNodeMsg msg) - { - if (!IsBonded) - { - return; - } - - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeIn); - RefreshNodeContactTime(); - - // 12 otherwise the payload may become too big, which is out of spec. - var closestNodes = _nodeTable.GetClosestNodes(msg.SearchedNodeId, bucketSize: 12); - Node[] nodes = new Node[closestNodes.Count]; - int count = 0; - foreach (Node node in closestNodes) - { - nodes[count] = node; - count++; - } - - SendNeighbors(nodes); - } - - private readonly DateTime _lastTimeSendFindNode = DateTime.MinValue; - - private long _lastEnrSequence; - - public async Task SendFindNode(byte[] searchedNodeId) - { - if (!IsBonded) - { - if (_logger.IsDebug) _logger.Debug($"Sending FIND NODE on {ManagedNode} before bonding"); - } - - if (DateTime.UtcNow - _lastTimeSendFindNode < TimeSpan.FromSeconds(60)) - { - return; - } - - FindNodeMsg msg = new(ManagedNode.Address, CalculateExpirationTime(), searchedNodeId); - _isNeighborsExpected = true; - await _discoveryManager.SendMessageAsync(msg); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeOut); - } - - private DateTime _lastPingSent = DateTime.MinValue; - - public async Task SendPingAsync() - { - _lastPingSent = DateTime.UtcNow; - _sentPing = true; - await CreateAndSendPingAsync(_discoveryConfig.PingRetryCount); - } - - private long CalculateExpirationTime() - { - return ExpirationTimeInSeconds + _timestamper.UnixTime.SecondsLong; - } - - public void SendPong(PingMsg discoveryMsg) - { - PongMsg msg = new(ManagedNode.Address, CalculateExpirationTime(), discoveryMsg.Mdc!); - _discoveryManager.SendMessage(msg); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongOut); - // _sentPong = true; - if (IsBonded) - { - UpdateState(NodeLifecycleState.Active); - } - } - - public void SendNeighbors(Node[] nodes) - { - if (!IsBonded) - { - if (_logger.IsWarn) _logger.Warn("Attempt to send NEIGHBOURS before bonding"); - return; - } - - NeighborsMsg msg = new(ManagedNode.Address, CalculateExpirationTime(), nodes); - _discoveryManager.SendMessage(msg); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursOut); - } - - public void StartEvictionProcess() - { - UpdateState(NodeLifecycleState.EvictCandidate); - } - - public void ResetUnreachableStatus() - { - if (State == NodeLifecycleState.Unreachable) - { - UpdateState(NodeLifecycleState.New); - } - } - - public void LostEvictionProcess() - { - if (State == NodeLifecycleState.Active) - { - UpdateState(NodeLifecycleState.ActiveExcluded); - } - } - - private void UpdateState(NodeLifecycleState newState) - { - if (newState == NodeLifecycleState.New) - { - //if node is just discovered we send ping to confirm it is active -#pragma warning disable 4014 - SendPingAsync(); -#pragma warning restore 4014 - } - else if (newState == NodeLifecycleState.Active) - { - //TODO && !ManagedNode.IsDiscoveryNode - should we exclude discovery nodes - //received pong first time - if (State == NodeLifecycleState.New) - { - NodeAddResult result = _nodeTable.AddNode(ManagedNode); - if (result.ResultType == NodeAddResultType.Full && result.EvictionCandidate?.Node is not null) - { - INodeLifecycleManager? evictionCandidate = _discoveryManager.GetNodeLifecycleManager(result.EvictionCandidate.Node); - if (evictionCandidate is not null) - { - _evictionManager.StartEvictionProcess(evictionCandidate, this); - } - } - } - } - else if (newState == NodeLifecycleState.EvictCandidate) - { - if (State == NodeLifecycleState.EvictCandidate) - { - throw new InvalidOperationException("Cannot start more than one eviction process on same node."); - } - - if (DateTime.UtcNow - _lastPingSent > TimeSpan.FromSeconds(5)) - { -#pragma warning disable 4014 - SendPingAsync(); -#pragma warning restore 4014 - } - else - { - // TODO: this is very strange...? - // seems like we quickly send two state updates here since we do not return after invocation? - OnStateChanged?.Invoke(this, NodeLifecycleState.Active); - } - } - - State = newState; - OnStateChanged?.Invoke(this, State); - } - - private void RefreshNodeContactTime() - { - if (State == NodeLifecycleState.Active) - { - _nodeTable.RefreshNode(ManagedNode); - } - } - - private async Task CreateAndSendPingAsync(int counter = 1) - { - if (_nodeTable.MasterNode is null) - { - return; - } - - PingMsg msg = new(ManagedNode.Address, CalculateExpirationTime(), _nodeTable.MasterNode.Address); - msg.EnrSequence = _nodeRecord.EnrSequence; - - try - { - _lastSentPing = msg; - await _discoveryManager.SendMessageAsync(msg); - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingOut); - - bool result = await _discoveryManager.WasMessageReceived(ManagedNode.IdHash, MsgType.Pong, _discoveryConfig.PongTimeout); - if (!result) - { - if (counter > 1) - { - await CreateAndSendPingAsync(counter - 1); - } - else - { - UpdateState(NodeLifecycleState.Unreachable); - } - } - } - catch (Exception e) - { - _logger.Error("Error during sending ping message", e); - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs deleted file mode 100644 index b3a107b3d53b..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core; -using Nethermind.Logging; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Stats; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Lifecycle; - -public class NodeLifecycleManagerFactory : INodeLifecycleManagerFactory -{ - private readonly INodeTable _nodeTable; - private readonly ILogger _logger; - private readonly IDiscoveryConfig _discoveryConfig; - private readonly ITimestamper _timestamper; - private readonly IEvictionManager _evictionManager; - private readonly INodeStatsManager _nodeStatsManager; - private readonly NodeRecord _selfNodeRecord; - - public NodeLifecycleManagerFactory(INodeTable nodeTable, - IEvictionManager evictionManager, - INodeStatsManager nodeStatsManager, - NodeRecord self, - IDiscoveryConfig discoveryConfig, - ITimestamper timestamper, - ILogManager? logManager) - { - _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _nodeTable = nodeTable ?? throw new ArgumentNullException(nameof(nodeTable)); - _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - _evictionManager = evictionManager ?? throw new ArgumentNullException(nameof(evictionManager)); - _nodeStatsManager = nodeStatsManager ?? throw new ArgumentNullException(nameof(nodeStatsManager)); - _selfNodeRecord = self ?? throw new ArgumentNullException(nameof(self)); - } - - public IDiscoveryManager? DiscoveryManager { private get; set; } - public NodeRecord SelfNodeRecord => _selfNodeRecord; - - public INodeLifecycleManager CreateNodeLifecycleManager(Node node) - { - if (DiscoveryManager is null) - { - throw new Exception($"{nameof(DiscoveryManager)} has to be set"); - } - - return new NodeLifecycleManager( - node, - DiscoveryManager, - _nodeTable, - _evictionManager, - _nodeStatsManager.GetOrAdd(node), - _selfNodeRecord, - _discoveryConfig, - _timestamper, - _logger); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleState.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleState.cs deleted file mode 100644 index 79626b289541..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleState.cs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Lifecycle; - -public enum NodeLifecycleState -{ - New, - Active, - ActiveWithEnr, - EvictCandidate, - Unreachable, - //Active, but not included in NodeTable - ActiveExcluded -} diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs index 39b6da0e7780..1544e553c8a3 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs @@ -6,12 +6,11 @@ using Nethermind.Config; using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.IO; +using Nethermind.Core.Timers; using Nethermind.Db; using Nethermind.Logging; -using Nethermind.Network.Discovery.Lifecycle; using Nethermind.Stats; using Nethermind.Stats.Model; -using NSubstitute; using NUnit.Framework; namespace Nethermind.Network.Test; @@ -40,18 +39,6 @@ public void TearDown() private TempPath _tempDir; private INetworkStorage _storage; - private INodeLifecycleManager CreateLifecycleManager(Node node) - { - INodeLifecycleManager manager = Substitute.For(); - manager.ManagedNode.Returns(node); - manager.NodeStats.Returns(new NodeStatsLight(node) - { - CurrentPersistedNodeReputation = node.Port - }); - - return manager; - } - [Test] public void Can_store_discovery_nodes() { @@ -67,9 +54,10 @@ public void Can_store_discovery_nodes() new Node(TestItem.PublicKeyE, "192.1.1.5", 3445) }; - var managers = nodes.Select(CreateLifecycleManager).ToArray(); + INodeStatsManager nodeStatsManager = new NodeStatsManager(new TimerFactory(), LimboLogs.Instance); + DateTime utcNow = DateTime.UtcNow; - var networkNodes = managers.Select(x => new NetworkNode(x.ManagedNode.Id, x.ManagedNode.Host, x.ManagedNode.Port, x.NodeStats.NewPersistedNodeReputation(utcNow))).ToArray(); + var networkNodes = nodes.Select(x => new NetworkNode(x.Id, x.Host, x.Port, nodeStatsManager.GetOrAdd(x).NewPersistedNodeReputation(utcNow))).ToArray(); _storage.StartBatch(); @@ -77,13 +65,13 @@ public void Can_store_discovery_nodes() _storage.Commit(); persistedNodes = _storage.GetPersistedNodes(); - foreach (INodeLifecycleManager manager in managers) + foreach (Node manager in nodes) { - NetworkNode persistedNode = persistedNodes.FirstOrDefault(x => x.NodeId.Equals(manager.ManagedNode.Id)); + NetworkNode persistedNode = persistedNodes.FirstOrDefault(x => x.NodeId.Equals(manager.Id)); Assert.That(persistedNode, Is.Not.Null); - Assert.That(persistedNode.Port, Is.EqualTo(manager.ManagedNode.Port)); - Assert.That(persistedNode.Host, Is.EqualTo(manager.ManagedNode.Host)); - Assert.That(persistedNode.Reputation, Is.EqualTo(manager.NodeStats.CurrentNodeReputation())); + Assert.That(persistedNode.Port, Is.EqualTo(manager.Port)); + Assert.That(persistedNode.Host, Is.EqualTo(manager.Host)); + Assert.That(persistedNode.Reputation, Is.EqualTo(nodeStatsManager.GetOrAdd(manager).CurrentNodeReputation())); } _storage.StartBatch(); @@ -91,20 +79,20 @@ public void Can_store_discovery_nodes() _storage.Commit(); persistedNodes = _storage.GetPersistedNodes(); - foreach (INodeLifecycleManager manager in managers.Take(1)) + foreach (Node manager in nodes.Take(1)) { - NetworkNode persistedNode = persistedNodes.FirstOrDefault(x => x.NodeId.Equals(manager.ManagedNode.Id)); + NetworkNode persistedNode = persistedNodes.FirstOrDefault(x => x.NodeId.Equals(manager.Id)); Assert.That(persistedNode, Is.Null); } utcNow = DateTime.UtcNow; - foreach (INodeLifecycleManager manager in managers.Skip(1)) + foreach (Node manager in nodes.Skip(1)) { - NetworkNode persistedNode = persistedNodes.FirstOrDefault(x => x.NodeId.Equals(manager.ManagedNode.Id)); + NetworkNode persistedNode = persistedNodes.FirstOrDefault(x => x.NodeId.Equals(manager.Id)); Assert.That(persistedNode, Is.Not.Null); - Assert.That(persistedNode.Port, Is.EqualTo(manager.ManagedNode.Port)); - Assert.That(persistedNode.Host, Is.EqualTo(manager.ManagedNode.Host)); - Assert.That(persistedNode.Reputation, Is.EqualTo(manager.NodeStats.CurrentNodeReputation(utcNow))); + Assert.That(persistedNode.Port, Is.EqualTo(manager.Port)); + Assert.That(persistedNode.Host, Is.EqualTo(manager.Host)); + Assert.That(persistedNode.Reputation, Is.EqualTo(nodeStatsManager.GetOrAdd(manager).CurrentNodeReputation(utcNow))); } } From 917c9d4a00bd145388e44207942582be8fa25093 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:35:21 +0800 Subject: [PATCH 043/182] Remove content extension --- .../Kademlia/KademliaSimulation.cs | 133 +----------------- .../Kademlia/Content/IContentHashProvider.cs | 11 -- .../Kademlia/Content/IContentMessageSender.cs | 19 --- .../Kademlia/Content/IKademliaContent.cs | 20 --- .../Kademlia/Content/IKademliaContentStore.cs | 14 -- .../Content/IServiceCollectionExtensions.cs | 36 ----- .../Kademlia/Content/KademliaContent.cs | 63 --------- .../Content/KademliaContentMessageReceiver.cs | 30 ---- 8 files changed, 2 insertions(+), 324 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index b4e31109e5e2..4cd8fb13bb01 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -12,7 +12,6 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; -using Nethermind.Network.Discovery.Kademlia.Content; using NonBlocking; using NUnit.Framework; @@ -79,34 +78,6 @@ public async Task TestBootstrap() // node3.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToHashSet().Should().BeEquivalentTo([node1Hash, node2Hash, node3Hash]); } - [Test] - public async Task TestLookup() - { - using CancellationTokenSource cts = new CancellationTokenSource(); - cts.CancelAfter(500); - - TestFabric fabric = CreateFabric(); - Random rand = new Random(0); - - ValueHash256 node1Hash = RandomKeccak(rand); - ValueHash256 node2Hash = RandomKeccak(rand); - ValueHash256 node3Hash = RandomKeccak(rand); - - Kademlia node1 = fabric.CreateNode(node1Hash); - Kademlia node2 = fabric.CreateNode(node2Hash); - KademliaContent node1Content = fabric.GetKademliaContent(node1Hash); - KademliaContent node2Content = fabric.GetKademliaContent(node2Hash); - fabric.CreateNode(node3Hash); - - node1.AddOrRefresh(new TestNode(node2Hash)); - node2.AddOrRefresh(new TestNode(node3Hash)); - - await fabric.Bootstrap(cts.Token); - - (await node1Content.LookupValue(node2Hash, cts.Token)).Should().BeEquivalentTo(node2Hash); - (await node1Content.LookupValue(node3Hash, cts.Token)).Should().BeEquivalentTo(node3Hash); - } - [Test] public async Task TestKNearestNeighbour() { @@ -148,50 +119,6 @@ public async Task TestKNearestNeighbour() .Be(node3Hash); } - [Test] - public async Task SimulateLargeLookupValue() - { - int nodeCount = 500; - - TestFabric fabric = CreateFabric(); - Random rand = new Random(0); - ValueHash256 mainNodeHash = RandomKeccak(rand); - Kademlia mainNode = fabric.CreateNode(mainNodeHash); - KademliaContent mainNodeContent = fabric.GetKademliaContent(mainNodeHash); - - List nodeIds = new(); - for (int i = 0; i < nodeCount; i++) - { - ValueHash256 nodeHash = RandomKeccak(rand); - Kademlia kad = fabric.CreateNode(nodeHash); - kad.AddOrRefresh(new TestNode(mainNodeHash)); - nodeIds.Add(nodeHash); - } - - using CancellationTokenSource cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(20)); - - Stopwatch sw = Stopwatch.StartNew(); - fabric.SimulateLatency = false; // Bootstrap is so slow, latency simulation is disable for it. - await fabric.Bootstrap(cts.Token); - TimeSpan bootstrapDuration = sw.Elapsed; - sw.Restart(); - fabric.SimulateLatency = true; - - fabric.FindValueCount = 0; - - foreach (ValueHash256 node in nodeIds) - { - (await mainNodeContent.LookupValue(node, cts.Token)).Should().BeEquivalentTo(node); - } - TimeSpan queryDuration = sw.Elapsed; - - TestContext.Out.WriteLine($"FindValue count per lookup {fabric.FindValueCount / (double)nodeIds.Count}"); - TestContext.Out.WriteLine($"FindNeighbour count {fabric.FindNeighbourCount}"); - TestContext.Out.WriteLine($"Bootstrap duration: {bootstrapDuration}"); - TestContext.Out.WriteLine($"Query duration: {queryDuration}"); - } - [Test] public async Task SimulateLargeKNearestNeighbour() { @@ -267,22 +194,7 @@ private static ValueHash256 RandomKeccak(Random rand) return val; } - private class OnlySelfIKademliaContentStore(ValueHash256 self) : IKademliaContentStore - { - public bool TryGetValue(ValueHash256 hash, out ValueHash256 value) - { - if (hash != self) - { - value = null; - return false; - } - - value = self; - return true; - } - } - - private class ValueHashNodeHashProvider: INodeHashProvider, IContentHashProvider, IKeyOperator + private class ValueHashNodeHashProvider: INodeHashProvider, IKeyOperator { public ValueHash256 GetHash(TestNode node) { @@ -313,7 +225,6 @@ public ValueHash256 GetHash(ValueHash256 key) private class TestFabric(KademliaConfig config) { internal long PingCount = 0; - internal long FindValueCount = 0; internal long FindNeighbourCount = 0; private int _baseLatency = 5; @@ -336,34 +247,15 @@ private bool TryGetReceiver(TestNode receiverHash, out IKademliaMessageReceiver< return false; } - private bool TryGetContentReceiver(TestNode receiverHash, out IContentMessageReceiver contentMessageReceiver) - { - contentMessageReceiver = null!; - if (_nodes.TryGetValue(receiverHash.Hash, out var serviceProvider)) - { - contentMessageReceiver = serviceProvider!.GetRequiredService>(); - return true; - } - - return false; - } - - public KademliaContent GetKademliaContent(ValueHash256 nodeHash) - { - return _nodes[nodeHash].GetRequiredService>(); - } - public Kademlia CreateNode(ValueHash256 nodeID) { var nodeIDTestNode = new TestNode(nodeID); var serviceProvider = new ServiceCollection() .ConfigureKademliaComponents() - .ConfigureKademliaContentComponents() .AddSingleton(new TestLogManager(LogLevel.Error)) .AddSingleton>(_nodeHashProvider) .AddSingleton>(_nodeHashProvider) - .AddSingleton>(_nodeHashProvider) .AddSingleton(new KademliaConfig() { CurrentNodeId = nodeIDTestNode, @@ -373,11 +265,8 @@ public Kademlia CreateNode(ValueHash256 nodeID) RefreshInterval = TimeSpan.FromHours(1), UseNewLookup = config.UseNewLookup }) - .AddSingleton>(new OnlySelfIKademliaContentStore(nodeID)) - .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton>() - .AddSingleton>() .BuildServiceProvider(); _nodes[nodeID] = serviceProvider; @@ -385,7 +274,7 @@ public Kademlia CreateNode(ValueHash256 nodeID) return serviceProvider.GetRequiredService>(); } - private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender, IContentMessageSender + private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender { public async Task Ping(TestNode node, CancellationToken token) { @@ -415,24 +304,6 @@ public async Task FindNeighbours(TestNode node, ValueHash256 hash, C throw new Exception($"unknown receiver {node}"); } - - public async Task> FindValue(TestNode node, ValueHash256 hash, CancellationToken token) - { - Interlocked.Increment(ref fabric.FindValueCount); - - await fabric.DoSimulateLatency(token); - fabric.Debug($"finv from {sender} to {node}"); - if (fabric.TryGetContentReceiver(node, out IContentMessageReceiver receiver)) - { - var resp = await receiver.FindValue(sender, hash, token); - fabric.Debug($"Got {resp.HasValue} {resp.Value} or {resp.Neighbours.Length} next"); - - resp = resp with { Neighbours = resp.Neighbours.Select(node => new TestNode(node.Hash)).ToArray() }; - return resp; - } - - throw new Exception($"unknown receiver {node}"); - } } private Task DoSimulateLatency(CancellationToken token) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs deleted file mode 100644 index a04ccb251262..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentHashProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -public interface IContentHashProvider -{ - ValueHash256 GetHash(TContentKey key); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs deleted file mode 100644 index b543eb74e73c..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IContentMessageSender.cs +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -public interface IContentMessageSender -{ - Task> FindValue(TNode receiver, TContentKey contentKey, CancellationToken token); -} - -public interface IContentMessageReceiver: IContentMessageSender -{ -} - -public record FindValueResponse( - bool HasValue, - TContent? Value, - TNode[] Neighbours -); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs deleted file mode 100644 index 1776ad239660..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContent.cs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -/// -/// This interface extend with the ability to lookup content. -/// -/// -/// -public interface IKademliaContent -{ - /// - /// Initiate a full network traversal for finding the value specified by TContent. - /// - /// - /// - /// - Task LookupValue(TContentKey id, CancellationToken token); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs deleted file mode 100644 index e07f9a7580ae..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IKademliaContentStore.cs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -/// -/// Try to get a content for serving. -/// -/// -/// -public interface IKademliaContentStore -{ - bool TryGetValue(TContentKey hash, out TContent? value); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs deleted file mode 100644 index d34fe3daa93c..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/IServiceCollectionExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Lantern.Discv5.Enr; -using Microsoft.Extensions.DependencyInjection; - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -public static class IServiceCollectionExtensions -{ - /// - /// Configure an extension of kademlia services to look up content. In particular it provide - /// an ` that has a lookup function. - /// Assume the component for was already registered. In addition to that, it assume - /// the following dependencies are also available: - /// - /// - - /// - - /// - - /// - /// Like with main kademlia, the transport is expected to call - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection ConfigureKademliaContentComponents(this IServiceCollection collection) where TNode : notnull - { - return collection - .AddSingleton, KademliaContent>() - .AddSingleton, KademliaContentMessageReceiver>() - .AddSingleton, KademliaContentMessageReceiver>(); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs deleted file mode 100644 index 88234806d10c..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContent.cs +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Logging; - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -public class KademliaContent( - IKademliaContentStore kademliaContentStore, - IContentMessageSender contentMessageSender, - IContentHashProvider contentHashProvider, - ILookupAlgo lookupAlgo, - KademliaConfig config, - ILogManager logManager - ): IKademliaContent where TNode : notnull -{ - private readonly ILogger _logger = logManager.GetClassLogger>(); - - public async Task LookupValue(TContentKey contentKey, CancellationToken token) - { - TContent? result = default(TContent); - bool resultWasFound = false; - - using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); - token = cts.Token; - // TODO: Timeout? - - if (kademliaContentStore.TryGetValue(contentKey, out TContent? content)) - { - return content; - } - - try - { - ValueHash256 contentHash = contentHashProvider.GetHash(contentKey); - await lookupAlgo.Lookup( - contentHash, config.KSize, async (nextNode, token) => - { - FindValueResponse valueResponse = await contentMessageSender.FindValue(nextNode, contentKey, token); - - if (valueResponse.HasValue) - { - if (_logger.IsDebug) _logger.Debug($"Value response has value {valueResponse.Value}"); - resultWasFound = true; - result = valueResponse.Value; // Shortcut so that once it find the value, it should stop. - await cts.CancelAsync(); - } - - if (_logger.IsDebug) _logger.Debug($"Value response has no value. Returning {valueResponse.Neighbours.Length} neighbours"); - return valueResponse.Neighbours; - }, - token - ); - } - catch (OperationCanceledException) - { - if (!resultWasFound) throw; - } - - return result; - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs deleted file mode 100644 index 33ed03116f24..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Content/KademliaContentMessageReceiver.cs +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Kademlia.Content; - -public class KademliaContentMessageReceiver( - IRoutingTable kademlia, - INodeHealthTracker nodeHealthTracker, - IContentHashProvider contentHashProvider, - IKademliaContentStore kademliaKademliaContentStore) : IContentMessageReceiver where TNode : notnull -{ - public Task> FindValue(TNode sender, TContentKey contentKey, CancellationToken token) - { - nodeHealthTracker.OnIncomingMessageFrom(sender); - - if (kademliaKademliaContentStore.TryGetValue(contentKey, out TContent? value)) - { - return Task.FromResult(new FindValueResponse(true, value!, Array.Empty())); - } - - // TODO: Exclude sender. - - return Task.FromResult( - new FindValueResponse( - false, - default, - kademlia.GetKNearestNeighbour(contentHashProvider.GetHash(contentKey), null, true) - )); - } -} From ee8fe721768e312136e80bb41ffab62471ec8e84 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Mon, 12 May 2025 19:37:01 +0800 Subject: [PATCH 044/182] Reducing change --- src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs | 5 ----- src/Nethermind/Nethermind.Runner/NLog.config | 3 --- 2 files changed, 8 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 6d0ae071847e..610a7e193557 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -258,9 +258,4 @@ private void RequireSignature() throw new Exception("Cannot encode a node record with an empty signature."); } } - - public string NodeRecordString() - { - return string.Join(",", Entries.Select((e) => $"{e.Key}:{e.Value}")); - } } diff --git a/src/Nethermind/Nethermind.Runner/NLog.config b/src/Nethermind/Nethermind.Runner/NLog.config index 67af6e92c047..c5c9bc5d7471 100644 --- a/src/Nethermind/Nethermind.Runner/NLog.config +++ b/src/Nethermind/Nethermind.Runner/NLog.config @@ -173,9 +173,6 @@ - - - From e623c44aa3d7186377ca7c2712204bb12499e659 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 08:16:42 +0800 Subject: [PATCH 045/182] Lets just save for now --- src/Nethermind/Directory.Packages.props | 3 +- .../Nethermind.Core/Nethermind.Core.csproj | 1 + .../DiscoveryConfig.cs | 6 +-- .../Discv4/KademliaDiscv4Adapter.cs | 45 +++++-------------- .../Discv4/KademliaNodeSource.cs | 39 +++++++++++++--- .../Kademlia/IITeratorAlgo.cs | 2 + .../Kademlia/IRoutingTable.cs | 1 + .../Kademlia/KBucket.cs | 1 + .../Kademlia/KBucketTree.cs | 13 ++++++ .../NewTrackingLookupKNearestNeighbour.cs | 29 +++++++++--- src/Nethermind/Nethermind.Network/PeerPool.cs | 12 +++++ 11 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/Nethermind/Directory.Packages.props b/src/Nethermind/Directory.Packages.props index b3c252920c80..6b882ff3d83e 100644 --- a/src/Nethermind/Directory.Packages.props +++ b/src/Nethermind/Directory.Packages.props @@ -68,6 +68,7 @@ + @@ -85,4 +86,4 @@ - + \ No newline at end of file diff --git a/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj b/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj index 54af4e25febe..a62645e96c8d 100644 --- a/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj +++ b/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs index 4014bdc0990b..513102290aa7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs @@ -11,15 +11,15 @@ public class DiscoveryConfig : IDiscoveryConfig public int Concurrency { get; set; } = 3; - public int BitsPerHop { get; set; } = 8; + public int BitsPerHop { get; set; } = 2; public int MaxDiscoveryRounds { get; set; } = 8; public int EvictionCheckInterval { get; set; } = 75; - public int SendNodeTimeout { get; set; } = 500; + public int SendNodeTimeout { get; set; } = 5000; - public int PongTimeout { get; set; } = 1000 * 15; + public int PongTimeout { get; set; } = 1000 * 5; public int BootnodePongTimeout { get; set; } = 1000 * 100; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 64364db93dd1..ee07dcb7bdcb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -4,7 +4,6 @@ using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Caching; -using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Logging; @@ -35,7 +34,6 @@ ILogManager logManager { private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); - private readonly TimeSpan _tryAuthenticatedTimeout = TimeSpan.FromSeconds(2); private readonly TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); private const int AuthenticatedRequestFailureLimit = 5; /// @@ -48,7 +46,6 @@ ILogManager logManager public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); - private readonly ConcurrentDictionary> _awaitingPongToNode = new(); // This is for waiting to send pong in attempt to authenticate. private readonly LruCache _outgoingBondDeadline = new(discoveryConfig.MaxNodeLifecycleManagersCount, "outgoing_bond_deadline"); private readonly LruCache _incomingBondDeadline = new(discoveryConfig.MaxNodeLifecycleManagersCount, "incoming_bond_deadline"); @@ -59,36 +56,19 @@ ILogManager logManager private async Task EnsureOutgoingMessageBondedPeer(Node node, CancellationToken token) { - if (_outgoingBondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) - && bondDeadline > DateTimeOffset.Now - && !TooManyFailure()) return true; + var hasBonded = _outgoingBondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) + && bondDeadline > DateTimeOffset.Now; + if (hasBonded && !TooManyFailure()) return true; if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); - using var cts = token.CreateChildTokenSource(_tryAuthenticatedTimeout); - token = cts.Token; - TaskCompletionSource pongCts = new(TaskCreationOptions.RunContinuationsAsynchronously); - await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(pongCts); - try - { - _awaitingPongToNode.TryAdd(node.IdHash, pongCts); - await Ping(node, token); - await pongCts.Task; - await Task.Delay(_waitAfterPongTimeout, token); // Give some time for peer to process pong. - - if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); + await Ping(node, token); + // We send them ping. But expect that eventually they send back another a ping so that we can pong. + // Give some time for peer to process pong. Such is the logic from geth codebase. + await Task.Delay(_waitAfterPongTimeout, token); - return true; - } - catch (OperationCanceledException) - { - if (_logger.IsTrace) _logger.Trace($"Node {node} timeout trying to trigger pong."); + if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); - return false; - } - finally - { - _awaitingPongToNode.TryRemove(node.IdHash, out _); - } + return true; bool TooManyFailure() { @@ -187,10 +167,6 @@ private async Task SendMessage(Node node, DiscoveryMsg msg) if (msg is PongMsg pong) { _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); - if (_awaitingPongToNode.TryGetValue(node.IdHash, out TaskCompletionSource? completionSource)) - { - completionSource.TrySetResult(new object()); - } } RecordStatsForOutgoingMsg(node, msg); @@ -227,8 +203,7 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel { FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); - // TODO: 16 is configurable - return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(16), receiver, msg, token); + return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(discoveryConfig.BucketSize), receiver, msg, token); }, token); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 4c7ebcf52d36..2d9b8e98b332 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; +using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; @@ -16,6 +17,7 @@ namespace Nethermind.Network.Discovery.Discv4; // TODO: Unit test, remove metric public class KademliaNodeSource( IKademlia kademlia, + IRoutingTable routingTable, IITeratorAlgo lookup2, KademliaDiscv4Adapter discv4Adapter, IDiscoveryConfig discoveryConfig, @@ -26,6 +28,9 @@ ILogManager logManager private Counter _discoverRound = Prometheus.Metrics.CreateCounter("kademlia_discover_rounds", "discovery rounds"); private Counter _discoverPingResult = Prometheus.Metrics.CreateCounter("kademlia_discover_ping", "discovery rounds", "result"); + private Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_routing_table_size", "discovery rounds", "result"); + + private LruCache _unreacheableNodes = new(10000, ""); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { @@ -43,21 +48,41 @@ async Task DiscoverAsync(PublicKey target) int count = 0; ValueHash256 targetHash = target.Hash; - Func> lookupOp = (nextNode, token) => - discv4Adapter.FindNeighbours(nextNode, target, token); - await foreach (var node in lookup2.Lookup(targetHash, 128, lookupOp!, token)) + Func> lookupOp = async (nextNode, token) => { + if (_unreacheableNodes.TryGet(nextNode.IdHash, out var lastAttempt) && + lastAttempt + TimeSpan.FromMinutes(5) > DateTimeOffset.Now) + { + return []; + } + try { - await discv4Adapter.Ping(node, token); - _discoverPingResult.WithLabels("ok").Inc(); + return await discv4Adapter.FindNeighbours(nextNode, target, token); } catch (OperationCanceledException) { - _discoverPingResult.WithLabels("timeout").Inc(); - continue; + _unreacheableNodes.Set(nextNode.IdHash, DateTimeOffset.Now); + throw; + } + }; + await foreach (var node in lookup2.Lookup(targetHash, 128, 3, 5, lookupOp!, token)) + { + if (routingTable.GetByHash(node.IdHash) is null) + { + try + { + await discv4Adapter.Ping(node, token); + _discoverPingResult.WithLabels("ok").Inc(); + } + catch (OperationCanceledException) + { + _discoverPingResult.WithLabels("timeout").Inc(); + continue; + } } + _kademliaSize.Set(routingTable.Size); anyFound = true; count++; total++; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs index 6efecc180274..19a6741badaf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs @@ -25,6 +25,8 @@ public interface IITeratorAlgo IAsyncEnumerable Lookup( ValueHash256 target, int minResult, + int maxNonProgressingRound, + int maxRounds, Func> findNeighbourOp, CancellationToken token ); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs index 621025d536b6..f24f75bd8ccc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs @@ -15,4 +15,5 @@ public interface IRoutingTable where TNode : notnull TNode? GetByHash(ValueHash256 nodeId); void LogDebugInfo(); event EventHandler? OnNodeAdded; + int Size { get; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs index d57ab98afa79..e54ad1e8140e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs @@ -13,6 +13,7 @@ public class KBucket where TNode : notnull private DoubleEndedLru _replacement; public int Count => _items.Count; + private TNode[] _cachedArray = Array.Empty(); public KBucket(int k) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 85ca8a03a00b..92d8baa82497 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -402,4 +402,17 @@ public void LogDebugInfo() } public event EventHandler? OnNodeAdded; + + public int Size + { + get + { + int total = 0; + foreach (var iterateBucket in IterateBuckets()) + { + total += iterateBucket.Bucket.Count; + } + return total; + } + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs index 4feae4db8656..6e069255334e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Channels; @@ -8,7 +9,9 @@ using Nethermind.Core.Extensions; using Nethermind.Core.Threading; using Nethermind.Logging; +using Nethermind.Stats.Model; using NonBlocking; +using Prometheus; namespace Nethermind.Network.Discovery.Kademlia; @@ -32,9 +35,12 @@ private bool SameAsSelf(TNode node) public async IAsyncEnumerable Lookup( ValueHash256 targetHash, int minResult, + int maxNonProgressingRound, + int maxRounds, Func> findNeighbourOp, [EnumeratorCancellation] CancellationToken token - ) { + ) + { if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using var cts = token.CreateChildTokenSource(); @@ -113,7 +119,7 @@ [EnumeratorCancellation] CancellationToken token continue; } - Interlocked.Increment(ref totalResult); + totalResult++; yield return neighbour; bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; @@ -133,7 +139,7 @@ [EnumeratorCancellation] CancellationToken token if (_logger.IsTrace) _logger.Trace($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); - if (ShouldStopDueToNoBetterResult()) + if (ShouldStop()) { if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); break; @@ -148,31 +154,35 @@ [EnumeratorCancellation] CancellationToken token using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); cts.CancelAfter(_findNeighbourHardTimeout); + Stopwatch sw = Stopwatch.StartNew(); try { // targetHash is implied in findNeighbourOp TNode[]? ret = await findNeighbourOp(node, cts.Token); + _findNeighbourRate.WithLabels("ok").Inc(); nodeHealthTracker.OnIncomingMessageFrom(node); return ret; } catch (OperationCanceledException) { + _findNeighbourRate.WithLabels("timout").Inc(); nodeHealthTracker.OnRequestFailed(node); return null; } catch (Exception e) { + _findNeighbourRate.WithLabels("failed").Inc(); nodeHealthTracker.OnRequestFailed(node); if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); return null; } } - bool ShouldStopDueToNoBetterResult() + bool ShouldStop() { - int round = Interlocked.Increment(ref currentRound); - if (totalResult >= minResult && round - closestNodeRound >= (config.Alpha*2)) + int round = ++currentRound; + if (totalResult >= minResult && round - closestNodeRound >= maxNonProgressingRound) { // No closer node for more than or equal to _alpha*2 round. // Assume exit condition @@ -183,7 +193,14 @@ bool ShouldStopDueToNoBetterResult() return true; } + if (round >= maxRounds) + { + return true; + } + return false; } } + + private Counter _findNeighbourRate = Prometheus.Metrics.CreateCounter("lookup_find_neighbour_status", "", "status"); } diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index 439598d4957f..5965a4899e1d 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -16,6 +16,7 @@ using Nethermind.Network.P2P; using Nethermind.Stats; using Nethermind.Stats.Model; +using Prometheus; namespace Nethermind.Network { @@ -283,10 +284,21 @@ private async Task FeedFromNodeSource() await Task.Delay(1000, token); } + int prevCount = PeerCount; GetOrAdd(node); + if (PeerCount == prevCount) + { + _addStates.WithLabels("duplicate").Inc(); + } + else if (PeerCount > prevCount) + { + _addStates.WithLabels("add").Inc(); + } } } + private Counter _addStates = Prometheus.Metrics.CreateCounter("peer_pool_add", "add", "status"); + public async Task StopAsync() { _cancellationTokenSource.Cancel(); From 2279c7f1d29d5d4994f8af0ddfa48934fcb602b2 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 08:19:12 +0800 Subject: [PATCH 046/182] Remove routing table --- .../NodeDistanceCalculatorTests.cs | 37 --- .../RoutingTable/NodeBucketItemTests.cs | 86 ------- .../RoutingTable/NodeBucketTests.cs | 157 ------------- .../RoutingTable/INodeDistanceCalculator.cs | 9 - .../RoutingTable/INodeTable.cs | 29 --- .../RoutingTable/NodeAddResult.cs | 29 --- .../RoutingTable/NodeAddResultType.cs | 11 - .../RoutingTable/NodeBucket.cs | 187 --------------- .../RoutingTable/NodeBucketItem.cs | 46 ---- .../RoutingTable/NodeDistanceCalculator.cs | 51 ----- .../RoutingTable/NodeTable.cs | 216 ------------------ 11 files changed, 858 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/NodeDistanceCalculatorTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketItemTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeDistanceCalculator.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeTable.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResult.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResultType.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucket.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucketItem.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeDistanceCalculator.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeTable.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeDistanceCalculatorTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NodeDistanceCalculatorTests.cs deleted file mode 100644 index c46b3af41352..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeDistanceCalculatorTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Network.Discovery.RoutingTable; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test -{ - [Parallelizable(ParallelScope.Self)] - [TestFixture] - public class NodeDistanceCalculatorTests - { - [Test] - public void Same_length_distance() - { - NodeDistanceCalculator nodeDistanceCalculator = new(new DiscoveryConfig()); - int distance = nodeDistanceCalculator.CalculateDistance(new byte[] { 1, 2, 3 }, new byte[] { 1, 2, 3 }); - Assert.That(distance, Is.EqualTo(232)); - } - - [Test] - public void Left_shorter_distance() - { - NodeDistanceCalculator nodeDistanceCalculator = new(new DiscoveryConfig()); - int distance = nodeDistanceCalculator.CalculateDistance(new byte[] { 1, 2 }, new byte[] { 1, 2, 3 }); - Assert.That(distance, Is.EqualTo(240)); - } - - [Test] - public void Right_shorter_distance() - { - NodeDistanceCalculator nodeDistanceCalculator = new(new DiscoveryConfig()); - int distance = nodeDistanceCalculator.CalculateDistance(new byte[] { 1, 2, 3 }, new byte[] { 1, 2 }); - Assert.That(distance, Is.EqualTo(240)); - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketItemTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketItemTests.cs deleted file mode 100644 index b3b7b0b700a2..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketItemTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using Nethermind.Core.Test.Builders; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Stats.Model; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test.RoutingTable -{ - [TestFixture, Parallelizable(ParallelScope.All)] - public class NodeBucketItemTests - { - [Test] - public void Last_contacted_time_is_set_to_now_at_the_beginning() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - nodeBucketItem.LastContactTime.Should().BeAfter(DateTime.UtcNow.AddDays(-1)); - } - - [Test] - public async Task On_contact_received_we_update_last_contacted_date() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - - DateTime dateTime = nodeBucketItem.LastContactTime; - await Task.Delay(10); - nodeBucketItem.OnContactReceived(); - DateTime dateTime2 = nodeBucketItem.LastContactTime; - dateTime2.Should().BeAfter(dateTime); - } - - [Test] - public void Is_bonded_at_start() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - nodeBucketItem.IsBonded(DateTime.UtcNow).Should().BeTrue(); - } - - [Test] - public void Two_with_same_node_are_equal() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - NodeBucketItem nodeBucketItem2 = new(node, DateTime.UtcNow); - nodeBucketItem.Should().Be(nodeBucketItem2); - } - - [Test] - public void Different_should_not_be_equal() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - Node node2 = new(TestItem.PublicKeyB, IPAddress.Loopback.ToString(), 30000); - - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - NodeBucketItem nodeBucketItem2 = new(node2, DateTime.UtcNow); - nodeBucketItem.Should().NotBe(nodeBucketItem2); - } - - [Test] - public void Two_with_same_node_have_same_hash_code() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - NodeBucketItem nodeBucketItem2 = new(node, DateTime.UtcNow); - nodeBucketItem.GetHashCode().Should().Be(nodeBucketItem2.GetHashCode()); - } - - [Test] - public void Same_are_equal() - { - Node node = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30000); - NodeBucketItem nodeBucketItem = new(node, DateTime.UtcNow); - nodeBucketItem.Should().Be(nodeBucketItem); - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketTests.cs deleted file mode 100644 index d5038f6374e1..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/RoutingTable/NodeBucketTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; -using System.Linq; -using System.Net; -using FluentAssertions; -using Nethermind.Core.Test.Builders; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Stats.Model; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test.RoutingTable -{ - [TestFixture] - public class NodeBucketTests - { - private readonly Node _node = new(TestItem.PublicKeyA, IPAddress.Broadcast.ToString(), 30000); - private readonly Node _node2 = new(TestItem.PublicKeyB, IPAddress.Broadcast.ToString(), 3000); - private readonly Node _node3 = new(TestItem.PublicKeyC, IPAddress.Broadcast.ToString(), 3000); - - [Test] - public void Bonded_count_is_tracked() - { - NodeBucket nodeBucket = new(1, 16); - nodeBucket.AddNode(_node); - nodeBucket.AddNode(_node2); - nodeBucket.AddNode(_node3); - nodeBucket.BondedItemsCount.Should().Be(3); - } - - [Test] - public void Newly_added_can_be_retrieved_as_bonded() - { - NodeBucket nodeBucket = new(1, 16); - nodeBucket.AddNode(_node); - nodeBucket.AddNode(_node2); - nodeBucket.AddNode(_node3); - nodeBucket.BondedItems.Should().HaveCount(3); - } - - [Test] - public void Distance_is_set_properly() - { - NodeBucket nodeBucket = new(1, 16); - nodeBucket.Distance.Should().Be(1); - } - - [Test] - public void Limits_the_bucket_size() - { - NodeBucket nodeBucket = new(1, 16); - AddNodes(nodeBucket, 32); - - nodeBucket.BondedItemsCount.Should().Be(16); - nodeBucket.BondedItems.Should().HaveCount(16); - } - - [Test] - public void Can_replace_existing_when_full() - { - NodeBucket nodeBucket = new(1, 16); - AddNodes(nodeBucket, 32); - - Node node = new( - TestItem.PublicKeyA, - IPAddress.Broadcast.ToString(), - 30001); - - Node existing = nodeBucket.BondedItems.First().Node!; - nodeBucket.ReplaceNode(existing, node); - nodeBucket.BondedItemsCount.Should().Be(16); - nodeBucket.BondedItems.Should().HaveCount(16); - nodeBucket.BondedItems.Should().Contain(bi => bi.Node == node); - nodeBucket.BondedItems.Should().NotContain(bi => bi.Node == existing); - } - - [TestCase(2)] - [TestCase(5)] - [TestCase(32)] - public void Can_refresh(int nodesInTheBucket) - { - NodeBucket nodeBucket = new(1, 16); - AddNodes(nodeBucket, nodesInTheBucket); - - Node existing1 = nodeBucket.BondedItems.First().Node!; - nodeBucket.RefreshNode(existing1); - - nodeBucket.BondedItems.Should().HaveCount(Math.Min(nodeBucket.BucketSize, nodesInTheBucket)); - } - - [TestCase(0)] - [TestCase(5)] - [TestCase(32)] - public void Throws_when_replacing_non_existing(int nodesInTheBucket) - { - NodeBucket nodeBucket = new(1, 16); - AddNodes(nodeBucket, nodesInTheBucket); - - Node node = new( - TestItem.PublicKeyA, - IPAddress.Broadcast.ToString(), - 30001); - - Node nonExisting = new( - TestItem.PublicKeyA, - IPAddress.Broadcast.ToString(), - 30002); - - Assert.DoesNotThrow(() => nodeBucket.ReplaceNode(nonExisting, node)); - } - - [Test] - public void When_addingToFullBucket_then_randomlyDropEntry() - { - NodeBucket nodeBucket = new(1, 16, dropFullBucketProbability: .5f); - for (int i = 0; i < 16; i++) - { - Node node = new( - TestItem.PublicKeys[i], - IPAddress.Broadcast.ToString(), - 30000); - - nodeBucket.AddNode(node); - } - - int dropCount = 0; - for (int i = 0; i < 100; i++) - { - Node node = new( - TestItem.PublicKeys[i + 16], - IPAddress.Broadcast.ToString(), - 30000); - - if (nodeBucket.AddNode(node).ResultType == NodeAddResultType.Dropped) - { - dropCount++; - } - } - - dropCount.Should().BeInRange(25, 75); - } - - private static void AddNodes(NodeBucket nodeBucket, int count) - { - for (int i = 0; i < count; i++) - { - Node node = new( - TestItem.PublicKeys[i], - IPAddress.Broadcast.ToString(), - 30000); - - nodeBucket.AddNode(node); - } - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeDistanceCalculator.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeDistanceCalculator.cs deleted file mode 100644 index 767aebd1845c..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeDistanceCalculator.cs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.RoutingTable; - -public interface INodeDistanceCalculator -{ - int CalculateDistance(ReadOnlySpan sourceId, ReadOnlySpan destinationId); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeTable.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeTable.cs deleted file mode 100644 index da0749b8b33f..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/INodeTable.cs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Stats.Model; -using static Nethermind.Network.Discovery.RoutingTable.NodeTable; - -namespace Nethermind.Network.Discovery.RoutingTable; - -public interface INodeTable -{ - void Initialize(PublicKey masterNodeKey); - Node? MasterNode { get; } - NodeBucket[] Buckets { get; } - NodeAddResult AddNode(Node node); - void ReplaceNode(Node nodeToRemove, Node nodeToAdd); - void RefreshNode(Node node); - - /// - /// GetClosestNodes to MasterNode - /// - ClosestNodesEnumerator GetClosestNodes(); - - /// - /// GetClosestNodes to provided Node - /// - ClosestNodesFromNodeEnumerator GetClosestNodes(byte[] nodeId); - ClosestNodesFromNodeEnumerator GetClosestNodes(byte[] nodeId, int bucketSize); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResult.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResult.cs deleted file mode 100644 index 3ebdff63a7b5..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.RoutingTable; - -public class NodeAddResult -{ - public NodeAddResultType ResultType { get; private init; } - - public NodeBucketItem? EvictionCandidate { get; private init; } - - private static readonly NodeAddResult? _added = null; - private static readonly NodeAddResult? _dropped = null; - - public static NodeAddResult Added() - { - return _added ?? new NodeAddResult { ResultType = NodeAddResultType.Added }; - } - - public static NodeAddResult Full(NodeBucketItem evictionCandidate) - { - return new NodeAddResult { ResultType = NodeAddResultType.Full, EvictionCandidate = evictionCandidate }; - } - - public static NodeAddResult Dropped() - { - return _dropped ?? new NodeAddResult { ResultType = NodeAddResultType.Dropped }; - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResultType.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResultType.cs deleted file mode 100644 index ac7dd808e4c4..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeAddResultType.cs +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.RoutingTable; - -public enum NodeAddResultType -{ - Added, - Full, - Dropped -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucket.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucket.cs deleted file mode 100644 index 781ca9409dcd..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucket.cs +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Collections; -using System.Diagnostics; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.RoutingTable; - -[DebuggerDisplay("{BondedItemsCount} bonded item(s)")] -public class NodeBucket -{ - private readonly Lock _nodeBucketLock = new(); - private readonly LinkedList _items; - private readonly float _dropFullBucketProbability; - - public NodeBucket(int distance, int bucketSize, float dropFullBucketProbability = 0.0f) - { - _items = new LinkedList(); - Distance = distance; - BucketSize = bucketSize; - _dropFullBucketProbability = dropFullBucketProbability; - } - - /// - /// Distance from Master Node - /// - public int Distance { get; } - - public int BucketSize { get; } - - public bool AnyBondedItems() - { - foreach (NodeBucketItem _ in BondedItems) - { - return true; - } - - return false; - } - - public BondedItemsEnumerator BondedItems - => new(this); - - public struct BondedItemsEnumerator : IEnumerator, IEnumerable - { - private readonly NodeBucket _nodeBucket; - private LinkedListNode? _currentNode; - private readonly DateTime _referenceTime; - - public BondedItemsEnumerator(NodeBucket nodeBucket) - { - _nodeBucket = nodeBucket; - lock (_nodeBucket._nodeBucketLock) - { - _currentNode = nodeBucket._items.Last; - } - _referenceTime = DateTime.UtcNow; - Current = null!; - } - - public NodeBucketItem Current { get; private set; } - - readonly object IEnumerator.Current => Current; - - public bool MoveNext() - { - lock (_nodeBucket._nodeBucketLock) - { - while (_currentNode is not null) - { - Current = _currentNode.Value; - _currentNode = _currentNode.Previous; - if (Current.IsBonded(_referenceTime)) - { - return true; - } - } - } - - Current = null!; - return false; - } - - void IEnumerator.Reset() => throw new NotSupportedException(); - - public readonly void Dispose() - { - } - public readonly BondedItemsEnumerator GetEnumerator() => this; - - readonly IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - readonly IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - } - - public int BondedItemsCount - { - get - { - lock (_nodeBucketLock) - { - int result = _items.Count; - LinkedListNode? node = _items.Last; - DateTime utcNow = DateTime.UtcNow; - while (node is not null) - { - if (node.Value.IsBonded(utcNow)) - { - break; - } - - node = node.Previous; - result--; - } - - return result; - } - } - } - - public NodeAddResult AddNode(Node node) - { - lock (_nodeBucketLock) - { - if (_items.Count < BucketSize) - { - NodeBucketItem item = new(node, DateTime.UtcNow); - if (!_items.Contains(item)) - { - _items.AddFirst(item); - } - - return NodeAddResult.Added(); - } - - if (Random.Shared.NextSingle() < _dropFullBucketProbability) - { - NodeBucketItem item = new(node, DateTime.UtcNow); - if (!_items.Contains(item)) - { - _items.AddFirst(item); - _items.RemoveLast(); - } - - return NodeAddResult.Dropped(); - } - - NodeBucketItem evictionCandidate = GetEvictionCandidate(); - return NodeAddResult.Full(evictionCandidate); - } - } - - public void ReplaceNode(Node nodeToRemove, Node nodeToAdd) - { - lock (_nodeBucketLock) - { - NodeBucketItem item = new(nodeToRemove, DateTime.UtcNow); - if (_items.Remove(item)) - { - AddNode(nodeToAdd); - } - } - } - - public void RefreshNode(Node node) - { - lock (_nodeBucketLock) - { - NodeBucketItem item = new(node, DateTime.UtcNow); - LinkedListNode? bucketItem = _items.Find(item); - if (bucketItem is not null) - { - bucketItem.Value.OnContactReceived(); - _items.Remove(item); - _items.AddFirst(item); - } - } - } - - private NodeBucketItem GetEvictionCandidate() - { - return _items.Last(); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucketItem.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucketItem.cs deleted file mode 100644 index 7592ac272ea2..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeBucketItem.cs +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.RoutingTable; - -public class NodeBucketItem -{ - public NodeBucketItem(Node? node, DateTime lastContactTime) - { - Node = node; - LastContactTime = lastContactTime; - } - - public Node? Node { get; } - - public DateTime LastContactTime { get; private set; } - - public bool IsBonded(DateTime utcNow) => LastContactTime > utcNow - TimeSpan.FromDays(2); - - public void OnContactReceived() - { - LastContactTime = DateTime.UtcNow; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj is NodeBucketItem item && Node is not null) - { - return Node.IdHash.Equals(item.Node?.IdHash); - } - - return false; - } - - public override int GetHashCode() - { - return Node?.GetHashCode() ?? 0; - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeDistanceCalculator.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeDistanceCalculator.cs deleted file mode 100644 index b342365c188d..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeDistanceCalculator.cs +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.RoutingTable; - -public class NodeDistanceCalculator : INodeDistanceCalculator -{ - private readonly int _maxDistance; - private readonly int _bitsPerHoop; - - public NodeDistanceCalculator(IDiscoveryConfig discoveryConfig) - { - _maxDistance = discoveryConfig.BucketsCount; - _bitsPerHoop = discoveryConfig.BitsPerHop; - } - - public int CalculateDistance(ReadOnlySpan sourceId, ReadOnlySpan destinationId) - { - int lowerLength = Math.Min(sourceId.Length, destinationId.Length); - int distance = _maxDistance; - - for (int i = 0; i < lowerLength; i++) - { - byte b = (byte)(destinationId[i] ^ sourceId[i]); - if (b == 0) - { - distance -= _bitsPerHoop; - } - else - { - int count = 0; - for (int j = _bitsPerHoop - 1; j >= 0; j--) - { - //why not b[j] == 0 - if ((b & (1 << j)) == 0) - { - count++; - } - else - { - break; - } - } - distance -= count; - break; - } - } - - return distance; - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeTable.cs b/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeTable.cs deleted file mode 100644 index 096d79837f37..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/RoutingTable/NodeTable.cs +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Collections; -using Nethermind.Core.Collections; -using Nethermind.Core.Crypto; -using Nethermind.Logging; -using Nethermind.Network.Config; -using Nethermind.Stats.Model; -using static Nethermind.Network.Discovery.RoutingTable.NodeBucket; - -namespace Nethermind.Network.Discovery.RoutingTable; - -public class NodeTable : INodeTable -{ - private readonly ILogger _logger; - private readonly INetworkConfig _networkConfig; - private readonly IDiscoveryConfig _discoveryConfig; - private readonly INodeDistanceCalculator _nodeDistanceCalculator; - - public NodeTable( - INodeDistanceCalculator? nodeDistanceCalculator, - IDiscoveryConfig? discoveryConfig, - INetworkConfig? networkConfig, - ILogManager? logManager) - { - _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); - _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _nodeDistanceCalculator = nodeDistanceCalculator ?? throw new ArgumentNullException(nameof(nodeDistanceCalculator)); - - Buckets = new NodeBucket[_discoveryConfig.BucketsCount]; - for (int i = 0; i < Buckets.Length; i++) - { - Buckets[i] = new NodeBucket(i, _discoveryConfig.BucketSize, _discoveryConfig.DropFullBucketNodeProbability); - } - } - - public Node? MasterNode { get; private set; } - - public NodeBucket[] Buckets { get; } - - public NodeAddResult AddNode(Node node) - { - CheckInitialization(); - - if (_logger.IsTrace) _logger.Trace($"Adding node to NodeTable: {node}"); - int distanceFromMaster = _nodeDistanceCalculator.CalculateDistance(MasterNode!.IdHash.Bytes, node.IdHash.Bytes); - NodeBucket bucket = Buckets[distanceFromMaster > 0 ? distanceFromMaster - 1 : 0]; - return bucket.AddNode(node); - } - - public void ReplaceNode(Node nodeToRemove, Node nodeToAdd) - { - CheckInitialization(); - - int distanceFromMaster = _nodeDistanceCalculator.CalculateDistance(MasterNode!.IdHash.Bytes, nodeToAdd.IdHash.Bytes); - NodeBucket bucket = Buckets[distanceFromMaster > 0 ? distanceFromMaster - 1 : 0]; - bucket.ReplaceNode(nodeToRemove, nodeToAdd); - } - - private void CheckInitialization() - { - if (MasterNode is null) - { - throw new InvalidOperationException("Master not has not been initialized"); - } - } - - public void RefreshNode(Node node) - { - CheckInitialization(); - - int distanceFromMaster = _nodeDistanceCalculator.CalculateDistance(MasterNode!.IdHash.Bytes, node.IdHash.Bytes); - NodeBucket bucket = Buckets[distanceFromMaster > 0 ? distanceFromMaster - 1 : 0]; - bucket.RefreshNode(node); - } - - public ClosestNodesEnumerator GetClosestNodes() - { - return new ClosestNodesEnumerator(Buckets, _discoveryConfig.BucketSize); - } - - public struct ClosestNodesEnumerator : IEnumerator, IEnumerable - { - private readonly NodeBucket[] _buckets; - private readonly int _bucketSize; - private BondedItemsEnumerator _itemEnumerator; - private bool _enumeratorSet; - private int _bucketIndex; - private int _count; - - public ClosestNodesEnumerator(NodeBucket[] buckets, int bucketSize) - { - _buckets = buckets; - _bucketSize = bucketSize; - Current = null!; - _bucketIndex = -1; - _count = 0; - } - - public Node Current { get; private set; } - - readonly object IEnumerator.Current => Current; - - public bool MoveNext() - { - while (_count < _bucketSize) - { - if (!_enumeratorSet || !_itemEnumerator.MoveNext()) - { - _itemEnumerator.Dispose(); - _bucketIndex++; - if (_bucketIndex >= _buckets.Length) - { - return false; - } - - _itemEnumerator = _buckets[_bucketIndex].BondedItems.GetEnumerator(); - _enumeratorSet = true; - continue; - } - - Current = _itemEnumerator.Current.Node!; - _count++; - return true; - } - - return false; - } - - void IEnumerator.Reset() => throw new NotSupportedException(); - - public readonly void Dispose() => _itemEnumerator.Dispose(); - - public readonly ClosestNodesEnumerator GetEnumerator() => this; - - readonly IEnumerator IEnumerable.GetEnumerator() => this; - - readonly IEnumerator IEnumerable.GetEnumerator() => this; - } - - public ClosestNodesFromNodeEnumerator GetClosestNodes(byte[] nodeId) - { - return GetClosestNodes(nodeId, _discoveryConfig.BucketSize); - } - - public ClosestNodesFromNodeEnumerator GetClosestNodes(byte[] nodeId, int bucketSize) - { - CheckInitialization(); - return new ClosestNodesFromNodeEnumerator(Buckets, nodeId, _nodeDistanceCalculator, Math.Min(bucketSize, _discoveryConfig.BucketSize)); - } - - public struct ClosestNodesFromNodeEnumerator : IEnumerator, IEnumerable - { - private readonly ArrayPoolList _sortedNodes; - private int _currentIndex; - - public ClosestNodesFromNodeEnumerator(NodeBucket[] buckets, byte[] targetNodeId, INodeDistanceCalculator calculator, int bucketSize) - { - _sortedNodes = new ArrayPoolList(capacity: bucketSize); - Hash256 idHash = Keccak.Compute(targetNodeId); - foreach (var bucket in buckets) - { - foreach (var item in bucket.BondedItems) - { - if (item.Node is not null && item.Node.IdHash != idHash) - { - _sortedNodes.Add(item.Node); - } - } - } - - _sortedNodes.Sort((a, b) => calculator.CalculateDistance(a.Id.Bytes, targetNodeId).CompareTo(calculator.CalculateDistance(b.Id.Bytes, targetNodeId))); - if (_sortedNodes.Count > bucketSize) - { - _sortedNodes.ReduceCount(bucketSize); - } - - _currentIndex = -1; - } - - public readonly int Count => _sortedNodes.Count; - - public readonly Node Current => _sortedNodes[_currentIndex]; - - readonly object IEnumerator.Current => Current; - - public bool MoveNext() - { - if (_currentIndex + 1 < _sortedNodes.Count) - { - _currentIndex++; - return true; - } - return false; - } - - void IEnumerator.Reset() => throw new NotSupportedException(); - public readonly void Dispose() - { - _sortedNodes.Dispose(); - } - - public readonly ClosestNodesFromNodeEnumerator GetEnumerator() => this; - readonly IEnumerator IEnumerable.GetEnumerator() => this; - - readonly IEnumerator IEnumerable.GetEnumerator() => this; - } - - public void Initialize(PublicKey masterNodeKey) - { - MasterNode = new Node(masterNodeKey, _networkConfig.ExternalIp, _networkConfig.DiscoveryPort); - if (_logger.IsTrace) _logger.Trace($"Created MasterNode: {MasterNode}"); - } -} From cea08d8052b0c96466f4f7a3222bc4a8729f0302 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 09:01:09 +0800 Subject: [PATCH 047/182] Move session into one class --- .../Discv4/KademliaDiscv4Adapter.cs | 182 ++++++------------ .../Discv4/NodeSession.cs | 77 ++++++++ 2 files changed, 141 insertions(+), 118 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index ee07dcb7bdcb..1eb6f9fbab1f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -32,10 +32,8 @@ public class KademliaDiscv4Adapter( ILogManager logManager ): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { - private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); - private const int AuthenticatedRequestFailureLimit = 5; /// /// This is the value set by other clients based on real network tests. /// @@ -44,21 +42,25 @@ ILogManager logManager private readonly ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); + private readonly CancellationToken _processCancellationToken = processExitSource.Token; private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); - private readonly LruCache _outgoingBondDeadline = new(discoveryConfig.MaxNodeLifecycleManagersCount, "outgoing_bond_deadline"); - private readonly LruCache _incomingBondDeadline = new(discoveryConfig.MaxNodeLifecycleManagersCount, "incoming_bond_deadline"); - private readonly LruCache _authenticatedRequestFailure = new(discoveryConfig.MaxNodeLifecycleManagersCount, "authenticated_request_failure"); - private readonly CancellationToken _processCancellationToken = processExitSource.Token; + private readonly LruCache _sessions = new(discoveryConfig.MaxNodeLifecycleManagersCount, "node_sessions"); #region Authentication and utils - private async Task EnsureOutgoingMessageBondedPeer(Node node, CancellationToken token) + private NodeSession GetSession(Node node) + { + if (_sessions.TryGet(node.IdHash, out var session)) return session; + session = new NodeSession(nodeStatsManager.GetOrAdd(node)); + _sessions.Set(node.IdHash, session); + return session; + } + + private async Task EnsureOutgoingMessageBondedPeer(Node node, NodeSession nodeSession, CancellationToken token) { - var hasBonded = _outgoingBondDeadline.TryGet(node.IdHash, out DateTimeOffset bondDeadline) - && bondDeadline > DateTimeOffset.Now; - if (hasBonded && !TooManyFailure()) return true; + if (nodeSession is { HasReceivedPing: true, NotTooManyFailure: true }) return; if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); await Ping(node, token); @@ -67,48 +69,20 @@ private async Task EnsureOutgoingMessageBondedPeer(Node node, Cancellation await Task.Delay(_waitAfterPongTimeout, token); if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); - - return true; - - bool TooManyFailure() - { - return _authenticatedRequestFailure.TryGet(node.IdHash, out long failedFinedNodes) && failedFinedNodes > AuthenticatedRequestFailureLimit; - } - } - - private async Task EnsureIncomingMessageBondedPeer(Node node, CancellationToken token) - { - if (_incomingBondDeadline.TryGet(node.IdHash, out DateTimeOffset safeUntil) && safeUntil > DateTimeOffset.Now) - { - return; - } - - // If we're here, the node is not safe, so we'll send a ping to verify - await Ping(node, token); - _incomingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); } - private async Task RunAuthenticatedRequest(Node node, Func> callRequest, CancellationToken token) + private async Task RunAuthenticatedRequest(Node node, NodeSession session, Func> callRequest, CancellationToken token) { - bool shouldBeBonded = await EnsureOutgoingMessageBondedPeer(node, token); + await EnsureOutgoingMessageBondedPeer(node, session, token); try { T resp = await callRequest(token); - if (!shouldBeBonded) - { - // Well.... maybe we already bonded, we just forgot about it.... - _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); - } - - _authenticatedRequestFailure.Set(node.IdHash, 0); + session.ResetAuthenticatedRequestFailure(); return resp; } catch (OperationCanceledException) { - _authenticatedRequestFailure.Set(node.IdHash, - _authenticatedRequestFailure.TryGet(node.IdHash, out long current) - ? current + 1 - : 1); + session.OnAuthenticatedRequestFailure(); throw; } @@ -142,13 +116,14 @@ private async Task CallAndWaitForResponse( MsgType msgType, ITaskCompleter messageHandler, Node receiver, + NodeSession session, DiscoveryMsg msg, CancellationToken token ) { await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(messageHandler.TaskCompletionSource); AddMessageHandler(msgType, receiver.IdHash, messageHandler); - await SendMessage(receiver, msg); + await SendMessage(session, msg); try { return await messageHandler.TaskCompletionSource.Task; @@ -160,16 +135,11 @@ CancellationToken token } - private async Task SendMessage(Node node, DiscoveryMsg msg) + private async Task SendMessage(NodeSession session, DiscoveryMsg msg) { if (MsgSender is { } sender) { - if (msg is PongMsg pong) - { - _outgoingBondDeadline.Set(node.IdHash, DateTimeOffset.Now + BondTimeout); - } - - RecordStatsForOutgoingMsg(node, msg); + session.RecordStatsForOutgoingMsg(msg); await sender.SendMsg(msg); } } @@ -191,7 +161,11 @@ public async Task Ping(Node receiver, CancellationToken token) PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); msg.EnrSequence = selfNodeRecord.EnrSequence; // optional and does not seems to be used anywhere. - _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, msg, token); + NodeSession session = GetSession(receiver); + + _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, session, msg, token); + + session.OnPongReceived(); } public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) @@ -199,11 +173,12 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - return await RunAuthenticatedRequest(receiver, async token => + NodeSession session = GetSession(receiver); + return await RunAuthenticatedRequest(receiver, session, async token => { FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); - return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(discoveryConfig.BucketSize), receiver, msg, token); + return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(discoveryConfig.BucketSize), receiver, session, msg, token); }, token); } @@ -212,100 +187,70 @@ public async Task SendEnrRequest(Node receiver, CancellationToke using var cts = token.CreateChildTokenSource(_requestTimeout); token = cts.Token; - return await RunAuthenticatedRequest(receiver, async token => + NodeSession session = GetSession(receiver); + return await RunAuthenticatedRequest(receiver, session, async token => { EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); - return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, msg, token); + return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, session, msg, token); }, token); } - private async Task HandleEnrRequest(Node node, EnrRequestMsg msg) + private Counter _rejectedIncomingMessage = + Prometheus.Metrics.CreateCounter("rejected_incoming_message", "Unhaandled", "type"); + + private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg) { - await EnsureIncomingMessageBondedPeer(node, _processCancellationToken); + if (!session.HasReceivedPong) + { + _rejectedIncomingMessage.WithLabels(msg.MsgType.ToString()).Inc(); + if (_logger.IsDebug) _logger.Debug($"Rejecting enr request from unbonded peer {node}"); + return; + } Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await SendMessage(node, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + await SendMessage(session, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); } - private async Task HandleFindNode(Node node, FindNodeMsg msg) + private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg) { - await EnsureIncomingMessageBondedPeer(node, _processCancellationToken); + if (!session.HasReceivedPong) + { + _rejectedIncomingMessage.WithLabels(msg.MsgType.ToString()).Inc(); + if (_logger.IsDebug) _logger.Debug($"Rejecting findNode request from unbonded peer {node}"); + return; + } PublicKey publicKey = new PublicKey(msg.SearchedNodeId); Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _processCancellationToken); if (nodes.Length <= 12) { - await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); } else { // Split into two because the size of message when nodes is > 12 is larger than mtu size. - await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12])); - await SendMessage(node, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..])); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12])); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..])); } } - private async Task HandlePing(Node node, PingMsg ping) + private async Task HandlePing(Node node, NodeSession session, PingMsg ping) { if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); await kademliaMessageReceiver.Value.Ping(node, _processCancellationToken); PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); - await SendMessage(node, msg); - } - - private Counter _unhandledDiscoveryMesssage = - Prometheus.Metrics.CreateCounter("unhandled_disc_message", "Unhaandled", "type"); + session.OnPingReceived(); + await SendMessage(session, msg); - private void RecordStatsForIncomingMsg(Node node, DiscoveryMsg msg) - { - switch (msg.MsgType) + if (session.HasReceivedPong) { - case MsgType.Ping: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingIn); - break; - case MsgType.FindNode: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeIn); - break; - case MsgType.EnrRequest: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestIn); - break; - case MsgType.Neighbors: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursIn); - break; - case MsgType.Pong: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongIn); - break; - case MsgType.EnrResponse: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseIn); - break; + await Ping(node, _processCancellationToken); } } - private void RecordStatsForOutgoingMsg(Node node, DiscoveryMsg msg) - { - switch (msg.MsgType) - { - case MsgType.Ping: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingOut); - break; - case MsgType.FindNode: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeOut); - break; - case MsgType.EnrRequest: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestOut); - break; - case MsgType.Neighbors: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursOut); - break; - case MsgType.Pong: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongOut); - break; - case MsgType.EnrResponse: - nodeStatsManager.GetOrAdd(node).AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseOut); - break; - } - } + private Counter _unhandledDiscoveryMesssage = + Prometheus.Metrics.CreateCounter("unhandled_disc_message", "Unhaandled", "type"); public async Task OnIncomingMsg(DiscoveryMsg msg) { @@ -314,7 +259,8 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) if (_logger.IsTrace) _logger.Trace($"Received msg: {msg}"); MsgType msgType = msg.MsgType; Node node = new(msg.FarPublicKey, msg.FarAddress); - RecordStatsForIncomingMsg(node, msg); + NodeSession session = GetSession(node); + session.RecordStatsForIncomingMsg(msg); if (HandleViaMessageHandlers(node, msg)) { @@ -329,13 +275,13 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) { return; } - await HandlePing(node, ping); + await HandlePing(node, session, ping); break; case MsgType.FindNode: - await HandleFindNode(node, (FindNodeMsg)msg); + await HandleFindNode(node, session, (FindNodeMsg)msg); break; case MsgType.EnrRequest: - await HandleEnrRequest(node, (EnrRequestMsg)msg); + await HandleEnrRequest(node, session, (EnrRequestMsg)msg); break; case MsgType.Neighbors: case MsgType.Pong: diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs new file mode 100644 index 000000000000..3fa7bb31ebe9 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Messages; +using Nethermind.Stats; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv4; + +internal record NodeSession(INodeStats NodeStats) +{ + private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); + private const int AuthenticatedRequestFailureLimit = 5; + private long AuthenticatedRequestFailureCount { get; set; } + private DateTimeOffset LastPongReceived { get; set; } = DateTimeOffset.MinValue; + private DateTimeOffset LastPingReceived { get; set; } = DateTimeOffset.MinValue; + + public bool HasReceivedPing => LastPingReceived + BondTimeout > DateTimeOffset.UtcNow; + public bool NotTooManyFailure => AuthenticatedRequestFailureCount <= AuthenticatedRequestFailureLimit; + public bool HasReceivedPong => LastPongReceived + BondTimeout > DateTimeOffset.UtcNow; + + public void ResetAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount = 0; + public void OnAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount++; + + public void OnPongReceived() => LastPongReceived = DateTimeOffset.UtcNow; + public void OnPingReceived() => LastPingReceived = DateTimeOffset.UtcNow; + + public void RecordStatsForOutgoingMsg(DiscoveryMsg msg) + { + switch (msg.MsgType) + { + case MsgType.Ping: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingOut); + break; + case MsgType.FindNode: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeOut); + break; + case MsgType.EnrRequest: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestOut); + break; + case MsgType.Neighbors: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursOut); + break; + case MsgType.Pong: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongOut); + break; + case MsgType.EnrResponse: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseOut); + break; + } + } + + public void RecordStatsForIncomingMsg(DiscoveryMsg msg) + { + switch (msg.MsgType) + { + case MsgType.Ping: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingIn); + break; + case MsgType.FindNode: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeIn); + break; + case MsgType.EnrRequest: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestIn); + break; + case MsgType.Neighbors: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursIn); + break; + case MsgType.Pong: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongIn); + break; + case MsgType.EnrResponse: + NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseIn); + break; + } + } +} From 90a5fe128661816a9fa12f4a6f656f9cae0026c4 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 09:06:11 +0800 Subject: [PATCH 048/182] Remove NodeFilter --- .../Discv4/KademliaDiscv4AdapterTests.cs | 1 - .../DiscoveryApp.cs | 7 +- .../Discv4/KademliaDiscv4Adapter.cs | 4 -- .../NodeFilter.cs | 71 ------------------- 4 files changed, 1 insertion(+), 82 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/NodeFilter.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 3819e477f2fc..4264c8786833 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -91,7 +91,6 @@ public void Setup() _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), - _networkConfig, new DiscoveryConfig(), _kademliaConfig, _selfNodeRecord, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index a10e155f55e4..d5215af1fe33 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -242,12 +242,6 @@ private async Task AddPersistedNodes(CancellationToken cancellationToken) break; } - if (!_discv4Adapter.NodesFilter.Set(networkNode.HostIp)) - { - // Already seen this node ip recently - continue; - } - Node node; try { @@ -263,6 +257,7 @@ private async Task AddPersistedNodes(CancellationToken cancellationToken) try { + // If when it receive Pong, it should automatically add to routing table if not full. await _discv4Adapter.Ping(node, cancellationToken); } catch (OperationCanceledException) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 1eb6f9fbab1f..1952d957de1e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -22,7 +22,6 @@ namespace Nethermind.Network.Discovery.Discv4; // TODO: Hard rate limit. public class KademliaDiscv4Adapter( Lazy> kademliaMessageReceiver, // Cyclic dependency - INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, NodeRecord selfNodeRecord, @@ -41,7 +40,6 @@ ILogManager logManager private readonly ILogger _logger = logManager.GetClassLogger(); public IMsgSender? MsgSender { get; set; } - public NodeFilter NodesFilter = new((networkConfig?.MaxActivePeers * 4) ?? 200); private readonly CancellationToken _processCancellationToken = processExitSource.Token; private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); @@ -149,8 +147,6 @@ private long CalculateExpirationTime() return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; } - - #endregion public async Task Ping(Node receiver, CancellationToken token) diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodeFilter.cs b/src/Nethermind/Nethermind.Network.Discovery/NodeFilter.cs deleted file mode 100644 index b6861698790d..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/NodeFilter.cs +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Net; -using Nethermind.Core.Caching; - -namespace Nethermind.Network.Discovery; - -/// -/// Represents a filter that temporarily rejects repeated IP addresses within a specified time window. -/// -/// The maximum capacity of the underlying cache storing IP addresses and their timestamps. -public class NodeFilter(int size) -{ - /// - /// Defines the duration within which an IP address is considered "recent" - /// and will cause the filter to reject new attempts from that same IP. - /// - private static readonly TimeSpan _timeOut = TimeSpan.FromMinutes(5); - - /// - /// A clock-based cache that stores the timestamps for IP addresses. - /// It is initialized with the specified size limit. - /// - private readonly ClockCache _nodesFilter = new(size); - - /// - /// Attempts to set (or update) the specified IP address in the filter. If the IP address has been seen - /// within the timeout window, this method returns false. Otherwise, it updates the cache - /// with the current time and returns true. - /// - /// The IP address to check and insert/update in the filter. - /// - /// true if the IP address was not found (or was outside the timeout window) - /// and was successfully inserted. false if the IP address was found - /// and is still within the timeout window. - /// - public bool Set(IPAddress ipAddress) - { - // Get the current UTC timestamp. - DateTime now = DateTime.UtcNow; - - // Non-atomic branching; so under lock in case two requests come in at same time - lock (_nodesFilter) - { - // Try to retrieve a previously recorded timestamp for the IP address. - if (_nodesFilter.TryGet(ipAddress, out DateTime lastSeen) && - // Check if the last seen time is still within the timeout window. - now - lastSeen < _timeOut) - { - // If yes, reject by returning false. - return false; - } - else - { - // Otherwise, update (or add) the IP address timestamp to the current time. - _nodesFilter.Set(ipAddress, now); - return true; - } - } - } - - private readonly struct IpAddressAsKey(IPAddress ipAddress) : IEquatable - { - private readonly IPAddress _ipAddress = ipAddress; - public static implicit operator IpAddressAsKey(IPAddress ip) => new(ip); - public bool Equals(IpAddressAsKey other) => _ipAddress.Equals(other._ipAddress); - public override bool Equals(object? obj) => obj is IpAddressAsKey ip && _ipAddress.Equals(ip._ipAddress); - public override int GetHashCode() => _ipAddress.GetHashCode(); - } -} From 291d6ddd8c7faae7ea5080e38be60ccd785933fe Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 09:12:33 +0800 Subject: [PATCH 049/182] Pass cancellation token --- .../Discv4/KademliaDiscv4Adapter.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 1952d957de1e..52ec79ef3432 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -7,7 +7,6 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Logging; -using Nethermind.Network.Config; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Enr; @@ -33,17 +32,17 @@ ILogManager logManager { private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); + /// /// This is the value set by other clients based on real network tests. /// private const int ExpirationTimeInSeconds = 20; private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly RateLimiter _outboundRateLimiter = new(discoveryConfig.MaxOutgoingMessagePerSecond); public IMsgSender? MsgSender { get; set; } - private readonly CancellationToken _processCancellationToken = processExitSource.Token; private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); - private readonly LruCache _sessions = new(discoveryConfig.MaxNodeLifecycleManagersCount, "node_sessions"); #region Authentication and utils @@ -121,7 +120,7 @@ CancellationToken token await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(messageHandler.TaskCompletionSource); AddMessageHandler(msgType, receiver.IdHash, messageHandler); - await SendMessage(session, msg); + await SendMessage(session, msg, token); try { return await messageHandler.TaskCompletionSource.Task; @@ -133,10 +132,11 @@ CancellationToken token } - private async Task SendMessage(NodeSession session, DiscoveryMsg msg) + private async Task SendMessage(NodeSession session, DiscoveryMsg msg, CancellationToken token) { if (MsgSender is { } sender) { + await _outboundRateLimiter.WaitAsync(token); session.RecordStatsForOutgoingMsg(msg); await sender.SendMsg(msg); } @@ -195,7 +195,7 @@ public async Task SendEnrRequest(Node receiver, CancellationToke private Counter _rejectedIncomingMessage = Prometheus.Metrics.CreateCounter("rejected_incoming_message", "Unhaandled", "type"); - private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg) + private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg, CancellationToken token) { if (!session.HasReceivedPong) { @@ -205,10 +205,10 @@ private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMs } Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await SendMessage(session, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes))); + await SendMessage(session, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes)), token); } - private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg) + private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg, CancellationToken token) { if (!session.HasReceivedPong) { @@ -218,30 +218,30 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms } PublicKey publicKey = new PublicKey(msg.SearchedNodeId); - Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, _processCancellationToken); + Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, token); if (nodes.Length <= 12) { - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes)); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes), token); } else { // Split into two because the size of message when nodes is > 12 is larger than mtu size. - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12])); - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..])); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12]), token); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..]), token); } } - private async Task HandlePing(Node node, NodeSession session, PingMsg ping) + private async Task HandlePing(Node node, NodeSession session, PingMsg ping, CancellationToken token) { if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); - await kademliaMessageReceiver.Value.Ping(node, _processCancellationToken); + await kademliaMessageReceiver.Value.Ping(node, token); PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); session.OnPingReceived(); - await SendMessage(session, msg); + await SendMessage(session, msg, token); if (session.HasReceivedPong) { - await Ping(node, _processCancellationToken); + await Ping(node, token); } } @@ -263,6 +263,7 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) return; } + CancellationToken token = processExitSource.Token; switch (msgType) { case MsgType.Ping: @@ -271,13 +272,13 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) { return; } - await HandlePing(node, session, ping); + await HandlePing(node, session, ping, token); break; case MsgType.FindNode: - await HandleFindNode(node, session, (FindNodeMsg)msg); + await HandleFindNode(node, session, (FindNodeMsg)msg, token); break; case MsgType.EnrRequest: - await HandleEnrRequest(node, session, (EnrRequestMsg)msg); + await HandleEnrRequest(node, session, (EnrRequestMsg)msg, token); break; case MsgType.Neighbors: case MsgType.Pong: From 033c7f7cfddc02ed0cf9d91da7f41f9206ab90ae Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 13:13:05 +0800 Subject: [PATCH 050/182] Fix --- .../Discv4/KademliaDiscv4Adapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 52ec79ef3432..1f61662d2d3f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -239,7 +239,7 @@ private async Task HandlePing(Node node, NodeSession session, PingMsg ping, Canc session.OnPingReceived(); await SendMessage(session, msg, token); - if (session.HasReceivedPong) + if (!session.HasReceivedPong) { await Ping(node, token); } From 121d9b042ec439041c2d8eeecb6dcee4b9ac2325 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 13:40:18 +0800 Subject: [PATCH 051/182] Fix throttle logic --- src/Nethermind/Nethermind.Network/PeerPool.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index 5965a4899e1d..40f2d591691e 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -278,7 +278,8 @@ private async Task FeedFromNodeSource() await foreach (Node node in _nodeSource.DiscoverNodes(token)) { - while (PeerCount >= _networkConfig.MaxCandidatePeerCount && ActivePeerCount >= _networkConfig.MaxActivePeers) + while (PeerCount >= _networkConfig.CandidatePeerCountCleanupThreshold || + (PeerCount >= _networkConfig.MaxCandidatePeerCount && ActivePeerCount >= _networkConfig.MaxActivePeers)) { if (_logger.IsDebug) _logger.Debug("Peer cleanup threshold reached. Throttling discovery."); await Task.Delay(1000, token); From 7aa73f1fa377ff670762671cf5f80163dc40b520 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 20:25:50 +0800 Subject: [PATCH 052/182] ISolating --- .../Discv4/KademliaDiscv4Adapter.cs | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 1f61662d2d3f..a29c5079a2f1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -21,6 +21,7 @@ namespace Nethermind.Network.Discovery.Discv4; // TODO: Hard rate limit. public class KademliaDiscv4Adapter( Lazy> kademliaMessageReceiver, // Cyclic dependency + Lazy> nodeHealthTracker, IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, NodeRecord selfNodeRecord, @@ -30,8 +31,10 @@ public class KademliaDiscv4Adapter( ILogManager logManager ): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { - private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10); - private readonly TimeSpan _waitAfterPongTimeout = TimeSpan.FromMilliseconds(500); + private readonly TimeSpan _requestEnrTimeout = TimeSpan.FromSeconds(10); + private readonly TimeSpan _findNeighbourTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); + private readonly TimeSpan _pingTimeout = TimeSpan.FromSeconds(1); + private readonly TimeSpan _waitAfterPongDelay = TimeSpan.FromMilliseconds(500); /// /// This is the value set by other clients based on real network tests. @@ -47,7 +50,7 @@ ILogManager logManager #region Authentication and utils - private NodeSession GetSession(Node node) + public NodeSession GetSession(Node node) { if (_sessions.TryGet(node.IdHash, out var session)) return session; session = new NodeSession(nodeStatsManager.GetOrAdd(node)); @@ -63,7 +66,7 @@ private async Task EnsureOutgoingMessageBondedPeer(Node node, NodeSession nodeSe await Ping(node, token); // We send them ping. But expect that eventually they send back another a ping so that we can pong. // Give some time for peer to process pong. Such is the logic from geth codebase. - await Task.Delay(_waitAfterPongTimeout, token); + await Task.Delay(_waitAfterPongDelay, token); if (_logger.IsTrace) _logger.Trace($"Node {node} pong sent."); } @@ -125,6 +128,11 @@ CancellationToken token { return await messageHandler.TaskCompletionSource.Task; } + catch (OperationCanceledException) + { + nodeHealthTracker.Value.OnRequestFailed(receiver); + throw; + } finally { RemoveMessageHandler(msgType, receiver.IdHash, messageHandler); @@ -151,7 +159,7 @@ private long CalculateExpirationTime() public async Task Ping(Node receiver, CancellationToken token) { - using var cts = token.CreateChildTokenSource(_requestTimeout); + using var cts = token.CreateChildTokenSource(_pingTimeout); token = cts.Token; PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); @@ -159,6 +167,7 @@ public async Task Ping(Node receiver, CancellationToken token) NodeSession session = GetSession(receiver); + session.OnPingSent(); _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, session, msg, token); session.OnPongReceived(); @@ -166,12 +175,12 @@ public async Task Ping(Node receiver, CancellationToken token) public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - NodeSession session = GetSession(receiver); return await RunAuthenticatedRequest(receiver, session, async token => { + using var cts = token.CreateChildTokenSource(_findNeighbourTimeout); + token = cts.Token; + FindNodeMsg msg = new FindNodeMsg(receiver.Address, CalculateExpirationTime(), target.Bytes); return await CallAndWaitForResponse(MsgType.Neighbors, new NeighbourMsgHandler(discoveryConfig.BucketSize), receiver, session, msg, token); @@ -180,12 +189,12 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel public async Task SendEnrRequest(Node receiver, CancellationToken token) { - using var cts = token.CreateChildTokenSource(_requestTimeout); - token = cts.Token; - NodeSession session = GetSession(receiver); return await RunAuthenticatedRequest(receiver, session, async token => { + using var cts = token.CreateChildTokenSource(_requestEnrTimeout); + token = cts.Token; + EnrRequestMsg msg = new EnrRequestMsg(receiver.Address, CalculateExpirationTime()); return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, session, msg, token); @@ -260,6 +269,7 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) if (HandleViaMessageHandlers(node, msg)) { + nodeHealthTracker.Value.OnIncomingMessageFrom(node); return; } From a7e8f03669551104b151cbed0bb8fa57d3174d66 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 13 May 2025 20:53:38 +0800 Subject: [PATCH 053/182] Cleanup attempt --- .../DiscoveryConfig.cs | 2 +- .../Discv4/IIteratorNodeLookup.cs | 12 ++ .../Discv4/KademliaNodeSource.cs | 33 +---- .../NewTrackingLookupKNearestNeighbour.cs | 140 +++++++++--------- .../Discv4/NodeSession.cs | 10 +- .../Kademlia/IITeratorAlgo.cs | 33 ----- .../Kademlia/KademliaModule.cs | 3 +- 7 files changed, 105 insertions(+), 128 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs rename src/Nethermind/Nethermind.Network.Discovery/{Kademlia => Discv4}/NewTrackingLookupKNearestNeighbour.cs (56%) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs index 513102290aa7..9961734c6ea4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs @@ -17,7 +17,7 @@ public class DiscoveryConfig : IDiscoveryConfig public int EvictionCheckInterval { get; set; } = 75; - public int SendNodeTimeout { get; set; } = 5000; + public int SendNodeTimeout { get; set; } = 500; public int PongTimeout { get; set; } = 1000 * 5; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs new file mode 100644 index 000000000000..925edd8c4430 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv4; + +public interface IIteratorNodeLookup +{ + IAsyncEnumerable Lookup(PublicKey target, CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 2d9b8e98b332..12eee2e3e528 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; -using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; @@ -18,7 +17,7 @@ namespace Nethermind.Network.Discovery.Discv4; public class KademliaNodeSource( IKademlia kademlia, IRoutingTable routingTable, - IITeratorAlgo lookup2, + IIteratorNodeLookup lookup2, KademliaDiscv4Adapter discv4Adapter, IDiscoveryConfig discoveryConfig, ILogManager logManager @@ -30,8 +29,6 @@ ILogManager logManager private Counter _discoverPingResult = Prometheus.Metrics.CreateCounter("kademlia_discover_ping", "discovery rounds", "result"); private Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_routing_table_size", "discovery rounds", "result"); - private LruCache _unreacheableNodes = new(10000, ""); - public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); @@ -47,29 +44,15 @@ async Task DiscoverAsync(PublicKey target) bool anyFound = false; int count = 0; - ValueHash256 targetHash = target.Hash; - Func> lookupOp = async (nextNode, token) => - { - if (_unreacheableNodes.TryGet(nextNode.IdHash, out var lastAttempt) && - lastAttempt + TimeSpan.FromMinutes(5) > DateTimeOffset.Now) - { - return []; - } - - try - { - return await discv4Adapter.FindNeighbours(nextNode, target, token); - } - catch (OperationCanceledException) - { - _unreacheableNodes.Set(nextNode.IdHash, DateTimeOffset.Now); - throw; - } - }; - await foreach (var node in lookup2.Lookup(targetHash, 128, 3, 5, lookupOp!, token)) + await foreach (var node in lookup2.Lookup(target, token)) { - if (routingTable.GetByHash(node.IdHash) is null) + if (!discv4Adapter.GetSession(node).HasReceivedPong) { + if (discv4Adapter.GetSession(node).HasTriedPingRecently) + { + // Tried ping before and did not receive a response + continue; + } try { await discv4Adapter.Ping(node, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NewTrackingLookupKNearestNeighbour.cs similarity index 56% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/NewTrackingLookupKNearestNeighbour.cs index 6e069255334e..fd6355570787 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NewTrackingLookupKNearestNeighbour.cs @@ -2,58 +2,66 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System.Threading.Channels; +using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; -using Nethermind.Core.Threading; using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; using NonBlocking; using Prometheus; -namespace Nethermind.Network.Discovery.Kademlia; - -public class NewaTrackingLookupKNearestNeighbour( - IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, - KademliaConfig kademliaConfig, - INodeHealthTracker nodeHealthTracker, - KademliaConfig config, - ILogManager logManager) : IITeratorAlgo where TNode : notnull +namespace Nethermind.Network.Discovery.Discv4; + +/// +/// Special lookup made specially for DiscV4 as the standard lookup is too slow or unnecessarily parallelized. +/// Instead of returning k closest node, it just returns the nodes that it found along the way and stopped early. +/// This is useful for node discovery as trying to get the k closest node is not completely necessary, as the main goal +/// is to reach all node. The lookup is not parallelized as it is expected to be parallelized at a higher level with +/// each worker having different target to look into. +/// +public class IteratorNodeLookup( + IRoutingTable routingTable, + KademliaConfig kademliaConfig, + KademliaDiscv4Adapter discv4Adapter, + ILogManager logManager) : IIteratorNodeLookup { - private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); - private readonly ValueHash256 _currentNodeIdAsHash = nodeHashProvider.GetHash(kademliaConfig.CurrentNodeId); + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly ValueHash256 _currentNodeIdAsHash = kademliaConfig.CurrentNodeId.IdHash; + + // Small lru of unreachable nodes, prevent retrying. Pretty effective, although does not improve discovery overall. + private readonly LruCache _unreacheableNodes = new(256, ""); + + // The maximum round per lookup. Higher means that it will 'see' deeper into the network, but come at a latency + // cost of trying many node for increasingly lower new node. + private const int MaxRounds = 3; + + // These two dont come into effect as MaxRounds is low. + private const int MaxNonProgressingRound = 3; + private const int MinResult = 128; - private bool SameAsSelf(TNode node) + private bool SameAsSelf(Node node) { - return nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; + return node.IdHash == _currentNodeIdAsHash; } - public async IAsyncEnumerable Lookup( - ValueHash256 targetHash, - int minResult, - int maxNonProgressingRound, - int maxRounds, - Func> findNeighbourOp, - [EnumeratorCancellation] CancellationToken token - ) + public async IAsyncEnumerable Lookup(PublicKey target, [EnumeratorCancellation] CancellationToken token) { + ValueHash256 targetHash = target.Hash; if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using var cts = token.CreateChildTokenSource(); token = cts.Token; - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(ValueHash256, TNode), ValueHash256> queryQueue = new(comparer); + PriorityQueue<(ValueHash256, Node), ValueHash256> queryQueue = new(comparer); // Used to determine if the worker should stop ValueHash256 bestNodeId = ValueKeccak.Zero; @@ -62,9 +70,9 @@ [EnumeratorCancellation] CancellationToken token int totalResult = 0; // Check internal table first - foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, null)) + foreach (Node node in routingTable.GetKNearestNeighbour(targetHash, null)) { - ValueHash256 nodeHash = nodeHashProvider.GetHash(node); + ValueHash256 nodeHash = node.IdHash; seen.TryAdd(nodeHash, node); queryQueue.Enqueue((nodeHash, node), nodeHash); @@ -80,7 +88,7 @@ [EnumeratorCancellation] CancellationToken token while (true) { token.ThrowIfCancellationRequested(); - if (!queryQueue.TryDequeue(out (ValueHash256 hash, TNode node) toQuery, out ValueHash256 hash256)) + if (!queryQueue.TryDequeue(out (ValueHash256 hash, Node node) toQuery, out ValueHash256 hash256)) { // No node to query and running query. if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); @@ -92,7 +100,7 @@ [EnumeratorCancellation] CancellationToken token queried.TryAdd(toQuery.hash, toQuery.node); if (_logger.IsTrace) _logger.Trace($"Query {toQuery.node} at round {currentRound}"); - TNode[]? neighbours = await WrappedFindNeighbourOp(toQuery.node); + Node[]? neighbours = await FindNeighbour(toQuery.node, target, token); if (neighbours == null || neighbours?.Length == 0) { if (_logger.IsTrace) _logger.Trace("Empty result"); @@ -101,9 +109,9 @@ [EnumeratorCancellation] CancellationToken token int queryIgnored = 0; int seenIgnored = 0; - foreach (TNode neighbour in neighbours!) + foreach (Node neighbour in neighbours!) { - ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + ValueHash256 neighbourHash = neighbour.IdHash; // Already queried, we ignore if (queried.ContainsKey(neighbourHash)) @@ -149,40 +157,10 @@ [EnumeratorCancellation] CancellationToken token if (_logger.IsTrace) _logger.Trace("Lookup operation finished."); yield break; - async Task WrappedFindNeighbourOp(TNode node) - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); - cts.CancelAfter(_findNeighbourHardTimeout); - - Stopwatch sw = Stopwatch.StartNew(); - try - { - // targetHash is implied in findNeighbourOp - TNode[]? ret = await findNeighbourOp(node, cts.Token); - _findNeighbourRate.WithLabels("ok").Inc(); - nodeHealthTracker.OnIncomingMessageFrom(node); - - return ret; - } - catch (OperationCanceledException) - { - _findNeighbourRate.WithLabels("timout").Inc(); - nodeHealthTracker.OnRequestFailed(node); - return null; - } - catch (Exception e) - { - _findNeighbourRate.WithLabels("failed").Inc(); - nodeHealthTracker.OnRequestFailed(node); - if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); - return null; - } - } - bool ShouldStop() { int round = ++currentRound; - if (totalResult >= minResult && round - closestNodeRound >= maxNonProgressingRound) + if (totalResult >= MinResult && round - closestNodeRound >= MaxNonProgressingRound) { // No closer node for more than or equal to _alpha*2 round. // Assume exit condition @@ -193,7 +171,7 @@ bool ShouldStop() return true; } - if (round >= maxRounds) + if (round >= MaxRounds) { return true; } @@ -203,4 +181,34 @@ bool ShouldStop() } private Counter _findNeighbourRate = Prometheus.Metrics.CreateCounter("lookup_find_neighbour_status", "", "status"); + + async Task FindNeighbour(Node node, PublicKey target, CancellationToken token) + { + try + { + if (_unreacheableNodes.TryGet(node.IdHash, out var lastAttempt) && + lastAttempt + TimeSpan.FromMinutes(5) > DateTimeOffset.Now) + { + return []; + } + + Node[]? ret = await discv4Adapter.FindNeighbours(node, target, token); + _findNeighbourRate.WithLabels("ok").Inc(); + + return ret; + } + catch (OperationCanceledException) + { + _unreacheableNodes.Set(node.IdHash, DateTimeOffset.Now); + _findNeighbourRate.WithLabels("timout").Inc(); + return null; + } + catch (Exception e) + { + _findNeighbourRate.WithLabels("failed").Inc(); + if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); + return null; + } + } + } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index 3fa7bb31ebe9..320fd2cc1bd0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -7,18 +7,19 @@ namespace Nethermind.Network.Discovery.Discv4; -internal record NodeSession(INodeStats NodeStats) +public record NodeSession(INodeStats NodeStats) { private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); private const int AuthenticatedRequestFailureLimit = 5; private long AuthenticatedRequestFailureCount { get; set; } private DateTimeOffset LastPongReceived { get; set; } = DateTimeOffset.MinValue; private DateTimeOffset LastPingReceived { get; set; } = DateTimeOffset.MinValue; + private DateTimeOffset LastPingSent { get; set; } = DateTimeOffset.MinValue; public bool HasReceivedPing => LastPingReceived + BondTimeout > DateTimeOffset.UtcNow; public bool NotTooManyFailure => AuthenticatedRequestFailureCount <= AuthenticatedRequestFailureLimit; public bool HasReceivedPong => LastPongReceived + BondTimeout > DateTimeOffset.UtcNow; - + public bool HasTriedPingRecently => LastPingSent + TimeSpan.FromMinutes(10) > DateTimeOffset.UtcNow; public void ResetAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount = 0; public void OnAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount++; @@ -74,4 +75,9 @@ public void RecordStatsForIncomingMsg(DiscoveryMsg msg) break; } } + + public void OnPingSent() + { + LastPingSent = DateTimeOffset.UtcNow; + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs deleted file mode 100644 index 19a6741badaf..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IITeratorAlgo.cs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; - -namespace Nethermind.Network.Discovery.Kademlia; - -/// -/// Iterate nodes round a target. Returns `IAsyncEnumerable` of nodes that it encounters along the way. -/// This mean that the returned value is not in order and may not be the closest to the target, but it try to -/// eventually get there. Additionally, the returned `TNode` may not be online unlike the standard algo which -/// requires it to be online. -/// -public interface IITeratorAlgo -{ - /// - /// The find neighbour operation here is configurable because the same algorithm is also used for finding - /// value int the network, except that it would short circuit once the value was found. - /// - /// - /// - /// - /// - /// - IAsyncEnumerable Lookup( - ValueHash256 target, - int minResult, - int maxNonProgressingRound, - int maxRounds, - Func> findNeighbourOp, - CancellationToken token - ); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index d192379771b6..6dd78fdc8179 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -3,6 +3,7 @@ using Autofac; using Nethermind.Core; +using Nethermind.Network.Discovery.Discv4; namespace Nethermind.Network.Discovery.Kademlia; @@ -27,7 +28,7 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) - .AddSingleton, NewaTrackingLookupKNearestNeighbour>() + .AddSingleton() .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); } From da1312c8b49eb733f11ecb90c178f02eaf40eda9 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 09:30:25 +0800 Subject: [PATCH 054/182] Some refinement --- .../Discv4/KademliaDiscv4AdapterTests.cs | 5 +- .../Discv4/NeighbourMsgHandlerTests.cs | 197 +++++++++++++++ .../Discv4/NodeSessionTests.cs | 233 ++++++++++++++++++ .../Kademlia/KademliaSimulation.cs | 28 ++- .../Kademlia/KademliaTests.cs | 15 +- .../DiscoveryApp.cs | 8 +- .../Discv4/DiscV4KademliaModule.cs | 7 +- .../Discv4/IKademliaDiscv4Adapter.cs | 35 +++ .../Discv4/IKademliaNodeSource.cs | 21 ++ ...restNeighbour.cs => IteratorNodeLookup.cs} | 2 +- .../Discv4/KademliaDiscv4Adapter.cs | 4 +- .../Discv4/KademliaNodeSource.cs | 56 +++-- .../Discv4/NodeSession.cs | 15 +- .../Kademlia/IServiceCollectionExtensions.cs | 49 ---- .../Kademlia/KademliaModule.cs | 1 + 15 files changed, 570 insertions(+), 106 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{NewTrackingLookupKNearestNeighbour.cs => IteratorNodeLookup.cs} (99%) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 4264c8786833..abbbfaa71c43 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -31,9 +31,10 @@ namespace Nethermind.Network.Discovery.Test.Discv4 [TestFixture] public class KademliaDiscv4AdapterTests { - private KademliaDiscv4Adapter _adapter = null!; + private IKademliaDiscv4Adapter _adapter = null!; private IKademliaMessageReceiver _kademliaMessageReceiver = null!; + private INodeHealthTracker _nodeHealthTracker = null!; private INetworkConfig _networkConfig = null!; private KademliaConfig _kademliaConfig = null!; private NodeRecord _selfNodeRecord = null!; @@ -72,6 +73,7 @@ public void Setup() _testNode = new Node(_testPublicKey, "192.168.1.1", 30303); _kademliaMessageReceiver = Substitute.For>(); + _nodeHealthTracker = Substitute.For>(); _networkConfig = Substitute.For(); _networkConfig.MaxActivePeers.Returns(25); _kademliaConfig = new KademliaConfig { CurrentNodeId = _testNode }; @@ -91,6 +93,7 @@ public void Setup() _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), + new Lazy>(() => _nodeHealthTracker), new DiscoveryConfig(), _kademliaConfig, _selfNodeRecord, diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs new file mode 100644 index 000000000000..20d06cb9bd97 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Messages; +using Nethermind.Stats.Model; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv4 +{ + [Parallelizable(ParallelScope.Self)] + [TestFixture] + public class NeighbourMsgHandlerTests + { + private NeighbourMsgHandler _handler; + private const int K = 16; + private IPEndPoint _farAddress; + private long _expirationTime; + + [SetUp] + public void Setup() + { + _handler = new NeighbourMsgHandler(K); + _farAddress = new IPEndPoint(IPAddress.Parse("192.168.1.1"), 30303); + _expirationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60; // 60 seconds in the future + } + + [Test] + public void Handle_should_return_true_when_processing_valid_message() + { + // Arrange + var nodes = CreateNodes(5); + var msg = new NeighborsMsg(_farAddress, _expirationTime, nodes); + + // Act + bool result = _handler.Handle(msg); + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void Handle_should_return_false_when_k_nodes_already_collected() + { + // Arrange + // First, fill the handler with K nodes + var initialNodes = CreateNodes(K); + var initialMsg = new NeighborsMsg(_farAddress, _expirationTime, initialNodes); + _handler.Handle(initialMsg); + + // Then try to add more nodes + var additionalNodes = CreateNodes(5); + var additionalMsg = new NeighborsMsg(_farAddress, _expirationTime, additionalNodes); + + // Act + bool result = _handler.Handle(additionalMsg); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void Handle_should_return_false_when_adding_more_than_k_nodes() + { + // Arrange + // First, fill the handler with K-1 nodes + var initialNodes = CreateNodes(K - 1); + var initialMsg = new NeighborsMsg(_farAddress, _expirationTime, initialNodes); + _handler.Handle(initialMsg); + + // Then try to add more than 1 node + var additionalNodes = CreateNodes(2); + var additionalMsg = new NeighborsMsg(_farAddress, _expirationTime, additionalNodes); + + // Act + bool result = _handler.Handle(additionalMsg); + + // Assert + result.Should().BeFalse(); + } + + [Test] + public async Task TaskCompletionSource_should_complete_when_k_nodes_collected() + { + // Arrange + var nodes = CreateNodes(K); + var msg = new NeighborsMsg(_farAddress, _expirationTime, nodes); + + // Act + _handler.Handle(msg); + + // Create a task that will complete when the TaskCompletionSource completes + Task task = _handler.TaskCompletionSource.Task; + + // Wait for the task to complete with a timeout + var completedTask = await Task.WhenAny(task, Task.Delay(100)); + + // Assert + completedTask.Should().Be(task); + task.IsCompleted.Should().BeTrue(); + task.Result.Should().HaveCount(K); + task.Result.Should().BeEquivalentTo(nodes); + } + + [Test] + public async Task TaskCompletionSource_should_complete_after_timeout_when_less_than_k_nodes_collected() + { + // Arrange + var nodes = CreateNodes(K - 5); // Less than K nodes + var msg = new NeighborsMsg(_farAddress, _expirationTime, nodes); + + // Act + _handler.Handle(msg); + + // Create a task that will complete when the TaskCompletionSource completes + Task task = _handler.TaskCompletionSource.Task; + + // Wait for the task to complete with a timeout longer than the handler's timeout + var completedTask = await Task.WhenAny(task, Task.Delay(1500)); // Handler timeout is 1 second + + // Assert + completedTask.Should().Be(task); + task.IsCompleted.Should().BeTrue(); + task.Result.Should().HaveCount(K - 5); + task.Result.Should().BeEquivalentTo(nodes); + } + + [Test] + public void Handle_should_accumulate_nodes_from_multiple_messages() + { + // Arrange + var firstBatch = CreateNodes(5); + var secondBatch = CreateNodes(5, 5); // Start from index 5 to create different nodes + + var firstMsg = new NeighborsMsg(_farAddress, _expirationTime, firstBatch); + var secondMsg = new NeighborsMsg(_farAddress, _expirationTime, secondBatch); + + // Act + _handler.Handle(firstMsg); + _handler.Handle(secondMsg); + + // Assert + _handler.TaskCompletionSource.Task.Wait(100); // Give a small timeout for any async operations + var result = _handler.TaskCompletionSource.Task.Result; + + result.Should().HaveCount(10); + result.Should().Contain(firstBatch); + result.Should().Contain(secondBatch); + } + + [Test] + public void Handle_should_only_initiate_timeout_once() + { + // Arrange + var firstBatch = CreateNodes(3); + var secondBatch = CreateNodes(3, 3); // Start from index 3 to create different nodes + + var firstMsg = new NeighborsMsg(_farAddress, _expirationTime, firstBatch); + var secondMsg = new NeighborsMsg(_farAddress, _expirationTime, secondBatch); + + // Act + _handler.Handle(firstMsg); // This should initiate the timeout + Task.Delay(500).Wait(); // Wait a bit, but less than the timeout + _handler.Handle(secondMsg); // This should not initiate another timeout + + // Assert + // We can't directly test that the timeout is only initiated once, + // but we can verify that the nodes are accumulated correctly + Task.Delay(1500).Wait(); // Wait for the timeout to complete + var result = _handler.TaskCompletionSource.Task.Result; + + result.Should().HaveCount(6); + result.Should().Contain(firstBatch); + result.Should().Contain(secondBatch); + } + + private ArraySegment CreateNodes(int count, int startIndex = 0) + { + var nodes = new Node[count]; + for (int i = 0; i < count; i++) + { + // Create a 64-byte (128 hex chars) public key by padding with zeros + string hexString = $"0x{(i + startIndex).ToString().PadLeft(2, '0')}".PadRight(130, '0'); + var publicKey = new PublicKey(hexString); + nodes[i] = new Node(publicKey, $"192.168.1.{i + startIndex + 10}", 30303); + } + return new ArraySegment(nodes); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs new file mode 100644 index 000000000000..2114df6c8f53 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using FluentAssertions; +using Nethermind.Core; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Stats; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv4 +{ + [Parallelizable(ParallelScope.Self)] + [TestFixture] + public class NodeSessionTests + { + private INodeStats _nodeStats = null!; + private ITimestamper _timestamper = null!; + private NodeSession _nodeSession = null!; + private DateTimeOffset _currentTime; + + [SetUp] + public void Setup() + { + _currentTime = new DateTimeOffset(2025, 5, 13, 21, 0, 0, TimeSpan.Zero); + _nodeStats = Substitute.For(); + _timestamper = Substitute.For(); + _timestamper.UtcNowOffset.Returns(_currentTime); + _nodeSession = new NodeSession(_nodeStats, _timestamper); + } + + [Test] + public void HasReceivedPing_should_return_false_when_no_ping_received() + { + // Act + bool result = _nodeSession.HasReceivedPing; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void HasReceivedPing_should_return_true_when_ping_received_within_bond_timeout() + { + // Arrange + _nodeSession.OnPingReceived(); + + // Act + bool result = _nodeSession.HasReceivedPing; + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void HasReceivedPing_should_return_false_when_ping_received_outside_bond_timeout() + { + // Arrange + _nodeSession.OnPingReceived(); + + // Move time forward by more than the bond timeout (12 hours) + _timestamper.UtcNowOffset.Returns(_currentTime.AddHours(13)); + + // Act + bool result = _nodeSession.HasReceivedPing; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void HasReceivedPong_should_return_false_when_no_pong_received() + { + // Act + bool result = _nodeSession.HasReceivedPong; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void HasReceivedPong_should_return_true_when_pong_received_within_bond_timeout() + { + // Arrange + _nodeSession.OnPongReceived(); + + // Act + bool result = _nodeSession.HasReceivedPong; + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void HasReceivedPong_should_return_false_when_pong_received_outside_bond_timeout() + { + // Arrange + _nodeSession.OnPongReceived(); + + // Move time forward by more than the bond timeout (12 hours) + _timestamper.UtcNowOffset.Returns(_currentTime.AddHours(13)); + + // Act + bool result = _nodeSession.HasReceivedPong; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void HasTriedPingRecently_should_return_false_when_no_ping_sent() + { + // Act + bool result = _nodeSession.HasTriedPingRecently; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void HasTriedPingRecently_should_return_true_when_ping_sent_within_10_minutes() + { + // Arrange + _nodeSession.OnPingSent(); + + // Act + bool result = _nodeSession.HasTriedPingRecently; + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void HasTriedPingRecently_should_return_false_when_ping_sent_more_than_10_minutes_ago() + { + // Arrange + _nodeSession.OnPingSent(); + + // Move time forward by more than 10 minutes + _timestamper.UtcNowOffset.Returns(_currentTime.AddMinutes(11)); + + // Act + bool result = _nodeSession.HasTriedPingRecently; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void OnPongReceived_should_update_LastPongReceived() + { + // Arrange + _nodeSession.HasReceivedPong.Should().BeFalse(); + + // Act + _nodeSession.OnPongReceived(); + + // Assert + _nodeSession.HasReceivedPong.Should().BeTrue(); + } + + [Test] + public void OnPingReceived_should_update_LastPingReceived() + { + // Arrange + _nodeSession.HasReceivedPing.Should().BeFalse(); + + // Act + _nodeSession.OnPingReceived(); + + // Assert + _nodeSession.HasReceivedPing.Should().BeTrue(); + } + + [Test] + public void OnPingSent_should_update_LastPingSent() + { + // Arrange + _nodeSession.HasTriedPingRecently.Should().BeFalse(); + + // Act + _nodeSession.OnPingSent(); + + // Assert + _nodeSession.HasTriedPingRecently.Should().BeTrue(); + } + + [Test] + public void NotTooManyFailure_should_return_true_initially() + { + // Act + bool result = _nodeSession.NotTooManyFailure; + + // Assert + result.Should().BeTrue(); + } + + [Test] + public void NotTooManyFailure_should_return_false_after_too_many_failures() + { + // Arrange + for (int i = 0; i < 6; i++) // AuthenticatedRequestFailureLimit is 5 + { + _nodeSession.OnAuthenticatedRequestFailure(); + } + + // Act + bool result = _nodeSession.NotTooManyFailure; + + // Assert + result.Should().BeFalse(); + } + + [Test] + public void ResetAuthenticatedRequestFailure_should_reset_failure_count() + { + // Arrange + for (int i = 0; i < 6; i++) + { + _nodeSession.OnAuthenticatedRequestFailure(); + } + _nodeSession.NotTooManyFailure.Should().BeFalse(); + + // Act + _nodeSession.ResetAuthenticatedRequestFailure(); + + // Assert + _nodeSession.NotTooManyFailure.Should().BeTrue(); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 4cd8fb13bb01..011574c17f38 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -7,7 +7,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Autofac; using FluentAssertions; +using Nethermind.Core; using Microsoft.Extensions.DependencyInjection; using Nethermind.Core.Crypto; using Nethermind.Logging; @@ -231,16 +233,16 @@ private class TestFabric(KademliaConfig config) private int _randomLatency = 2; public bool SimulateLatency { get; set; } = false; - internal ConcurrentDictionary _nodes = new(); + internal ConcurrentDictionary _nodes = new(); readonly ValueHashNodeHashProvider _nodeHashProvider = new ValueHashNodeHashProvider(); private readonly Random _random = new Random(0); private bool TryGetReceiver(TestNode receiverHash, out IKademliaMessageReceiver contentKademliaMessageReceiver) { contentKademliaMessageReceiver = null!; - if (_nodes.TryGetValue(receiverHash.Hash, out var serviceProvider)) + if (_nodes.TryGetValue(receiverHash.Hash, out var container)) { - contentKademliaMessageReceiver = serviceProvider!.GetRequiredService>(); + contentKademliaMessageReceiver = container!.Resolve>(); return true; } @@ -251,8 +253,9 @@ public Kademlia CreateNode(ValueHash256 nodeID) { var nodeIDTestNode = new TestNode(nodeID); - var serviceProvider = new ServiceCollection() - .ConfigureKademliaComponents() + var builder = new ContainerBuilder(); + builder + .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Error)) .AddSingleton>(_nodeHashProvider) .AddSingleton>(_nodeHashProvider) @@ -266,12 +269,13 @@ public Kademlia CreateNode(ValueHash256 nodeID) UseNewLookup = config.UseNewLookup }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) - .AddSingleton>() - .BuildServiceProvider(); + .AddSingleton>(); + + var container = builder.Build(); + + _nodes[nodeID] = container; - _nodes[nodeID] = serviceProvider; - - return serviceProvider.GetRequiredService>(); + return container.Resolve>(); } private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender @@ -322,9 +326,9 @@ private void Debug(string debugString) public async Task Bootstrap(CancellationToken token) { - foreach (KeyValuePair kv in _nodes) + foreach (KeyValuePair kv in _nodes) { - await kv.Value.GetRequiredService>().Bootstrap(token); + await kv.Value.Resolve>().Bootstrap(token); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index e26cfb02b693..9f0db7f95923 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -5,8 +5,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Autofac; using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; +using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; @@ -21,16 +22,18 @@ public class KademliaTests private Kademlia CreateKad(KademliaConfig config) { - return new ServiceCollection() - .ConfigureKademliaComponents() + var builder = new ContainerBuilder(); + builder + .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Trace)) .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) - .AddSingleton>() - .BuildServiceProvider() - .GetRequiredService>(); + .AddSingleton>(); + + var container = builder.Build(); + return container.Resolve>(); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index d5215af1fe33..009a106bc0e6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -35,13 +35,13 @@ public class DiscoveryApp : IDiscoveryApp private PublicKey _masterNode = null!; private readonly NodeRecord _selfNodeRecorrd; - private KademliaDiscv4Adapter _discv4Adapter = null!; + private IKademliaDiscv4Adapter _discv4Adapter = null!; private IKademlia _kademlia = null!; private NettyDiscoveryHandler? _discoveryHandler; private readonly ILifetimeScope _rootLifetimeScope; - private KademliaNodeSource _kademliaNodeSource = null!; + private IKademliaNodeSource _kademliaNodeSource = null!; private Task? _runningTask; private readonly IProcessExitSource _processExitSouce; @@ -98,8 +98,8 @@ public void Initialize(PublicKey masterPublicKey) new DiscV4KademliaModule(_selfNodeRecorrd, _masterNode, _bootNodes))); _kademlia = _kademliaServices.Resolve>(); - _discv4Adapter = _kademliaServices.Resolve(); - _kademliaNodeSource = _kademliaServices.Resolve(); + _discv4Adapter = _kademliaServices.Resolve(); + _kademliaNodeSource = _kademliaServices.Resolve(); } public Task StartAsync() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 12b7f287677e..c25d37dbe07e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -20,7 +20,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton, NodeNodeHashProvider>() .AddSingleton, NodeNodeHashProvider>() .AddSingleton(selfNodeRecord) - .AddSingleton() + .AddSingleton() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), @@ -32,7 +32,9 @@ protected override void Load(ContainerBuilder builder) RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PongTimeout), BootNodes = bootNodes }) - .AddSingleton, KademliaDiscv4Adapter>(); + .AddSingleton() + .AddSingleton() + .AddSingleton>(c => c.Resolve()); } } @@ -62,4 +64,3 @@ public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) return new PublicKey(randomBytes); } } - diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs new file mode 100644 index 000000000000..a4d77deedf11 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Messages; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv4; + +/// +/// Interface for the KademliaDiscv4Adapter, which handles discovery protocol v4 message processing. +/// +public interface IKademliaDiscv4Adapter : IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable +{ + /// + /// Gets or sets the message sender used to send discovery messages. + /// + IMsgSender? MsgSender { get; set; } + + /// + /// Gets the session for a specific node. + /// + /// The node to get the session for. + /// The node session. + NodeSession GetSession(Node node); + + /// + /// Sends an ENR request to a node and returns the response. + /// + /// The node to send the request to. + /// Cancellation token. + /// The ENR response message. + Task SendEnrRequest(Node receiver, CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs new file mode 100644 index 000000000000..bbd8dcef4116 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv4; + +/// +/// Interface for discovering nodes in a Kademlia distributed hash table network. +/// +public interface IKademliaNodeSource +{ + /// + /// Discovers nodes in the network. + /// + /// Cancellation token to stop the discovery process. + /// An asynchronous enumerable of discovered nodes. + IAsyncEnumerable DiscoverNodes(CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NewTrackingLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs similarity index 99% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/NewTrackingLookupKNearestNeighbour.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs index fd6355570787..d871c3d2905d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NewTrackingLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs @@ -24,7 +24,7 @@ namespace Nethermind.Network.Discovery.Discv4; public class IteratorNodeLookup( IRoutingTable routingTable, KademliaConfig kademliaConfig, - KademliaDiscv4Adapter discv4Adapter, + IKademliaDiscv4Adapter discv4Adapter, ILogManager logManager) : IIteratorNodeLookup { private readonly ILogger _logger = logManager.GetClassLogger(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index a29c5079a2f1..8222901f6543 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -29,7 +29,7 @@ public class KademliaDiscv4Adapter( ITimestamper timestamper, IProcessExitSource processExitSource, ILogManager logManager -): IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable +): IKademliaDiscv4Adapter { private readonly TimeSpan _requestEnrTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _findNeighbourTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); @@ -53,7 +53,7 @@ ILogManager logManager public NodeSession GetSession(Node node) { if (_sessions.TryGet(node.IdHash, out var session)) return session; - session = new NodeSession(nodeStatsManager.GetOrAdd(node)); + session = new NodeSession(nodeStatsManager.GetOrAdd(node), timestamper); _sessions.Set(node.IdHash, session); return session; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 12eee2e3e528..3a761e404e94 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -14,20 +14,34 @@ namespace Nethermind.Network.Discovery.Discv4; // TODO: Unit test, remove metric -public class KademliaNodeSource( - IKademlia kademlia, - IRoutingTable routingTable, - IIteratorNodeLookup lookup2, - KademliaDiscv4Adapter discv4Adapter, - IDiscoveryConfig discoveryConfig, - ILogManager logManager -) +public class KademliaNodeSource : IKademliaNodeSource { - ILogger _logger = logManager.GetClassLogger(); - - private Counter _discoverRound = Prometheus.Metrics.CreateCounter("kademlia_discover_rounds", "discovery rounds"); - private Counter _discoverPingResult = Prometheus.Metrics.CreateCounter("kademlia_discover_ping", "discovery rounds", "result"); - private Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_routing_table_size", "discovery rounds", "result"); + private readonly IKademlia _kademlia; + private readonly IRoutingTable _routingTable; + private readonly IIteratorNodeLookup _lookup; + private readonly IKademliaDiscv4Adapter _discv4Adapter; + private readonly IDiscoveryConfig _discoveryConfig; + private readonly ILogger _logger; + + private readonly Counter _discoverRound = Prometheus.Metrics.CreateCounter("kademlia_discover_rounds", "discovery rounds"); + private readonly Counter _discoverPingResult = Prometheus.Metrics.CreateCounter("kademlia_discover_ping", "discovery rounds", "result"); + private readonly Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_routing_table_size", "discovery rounds", "result"); + + public KademliaNodeSource( + IKademlia kademlia, + IRoutingTable routingTable, + IIteratorNodeLookup lookup2, + IKademliaDiscv4Adapter discv4Adapter, + IDiscoveryConfig discoveryConfig, + ILogManager logManager) + { + _kademlia = kademlia; + _routingTable = routingTable; + _lookup = lookup2; + _discv4Adapter = discv4Adapter; + _discoveryConfig = discoveryConfig; + _logger = logManager.GetClassLogger(); + } public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { @@ -44,18 +58,18 @@ async Task DiscoverAsync(PublicKey target) bool anyFound = false; int count = 0; - await foreach (var node in lookup2.Lookup(target, token)) + await foreach (var node in _lookup.Lookup(target, token)) { - if (!discv4Adapter.GetSession(node).HasReceivedPong) + if (!_discv4Adapter.GetSession(node).HasReceivedPong) { - if (discv4Adapter.GetSession(node).HasTriedPingRecently) + if (_discv4Adapter.GetSession(node).HasTriedPingRecently) { // Tried ping before and did not receive a response continue; } try { - await discv4Adapter.Ping(node, token); + await _discv4Adapter.Ping(node, token); _discoverPingResult.WithLabels("ok").Inc(); } catch (OperationCanceledException) @@ -65,7 +79,7 @@ async Task DiscoverAsync(PublicKey target) } } - _kademliaSize.Set(routingTable.Size); + _kademliaSize.Set(_routingTable.Size); anyFound = true; count++; total++; @@ -87,7 +101,7 @@ async Task DiscoverAsync(PublicKey target) } } - Task discoverTask = Task.WhenAll(Enumerable.Range(0, discoveryConfig.ConcurrentDiscoveryJob).Select((_) => Task.Run(async () => + Task discoverTask = Task.WhenAll(Enumerable.Range(0, _discoveryConfig.ConcurrentDiscoveryJob).Select((_) => Task.Run(async () => { Random random = new(); byte[] randomBytes = new byte[64]; @@ -119,7 +133,7 @@ async Task DiscoverAsync(PublicKey target) try { - kademlia.OnNodeAdded += Handler; + _kademlia.OnNodeAdded += Handler; await foreach (Node node in ch.Reader.ReadAllAsync(token)) { @@ -129,7 +143,7 @@ async Task DiscoverAsync(PublicKey target) finally { await discoverTask; - kademlia.OnNodeAdded -= Handler; + _kademlia.OnNodeAdded -= Handler; } yield break; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index 320fd2cc1bd0..c467b89d2172 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -1,13 +1,14 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Core; using Nethermind.Network.Discovery.Messages; using Nethermind.Stats; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv4; -public record NodeSession(INodeStats NodeStats) +public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) { private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); private const int AuthenticatedRequestFailureLimit = 5; @@ -16,15 +17,15 @@ public record NodeSession(INodeStats NodeStats) private DateTimeOffset LastPingReceived { get; set; } = DateTimeOffset.MinValue; private DateTimeOffset LastPingSent { get; set; } = DateTimeOffset.MinValue; - public bool HasReceivedPing => LastPingReceived + BondTimeout > DateTimeOffset.UtcNow; + public bool HasReceivedPing => LastPingReceived + BondTimeout > Timestamper.UtcNowOffset; public bool NotTooManyFailure => AuthenticatedRequestFailureCount <= AuthenticatedRequestFailureLimit; - public bool HasReceivedPong => LastPongReceived + BondTimeout > DateTimeOffset.UtcNow; - public bool HasTriedPingRecently => LastPingSent + TimeSpan.FromMinutes(10) > DateTimeOffset.UtcNow; + public bool HasReceivedPong => LastPongReceived + BondTimeout > Timestamper.UtcNowOffset; + public bool HasTriedPingRecently => LastPingSent + TimeSpan.FromMinutes(10) > Timestamper.UtcNowOffset; public void ResetAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount = 0; public void OnAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount++; - public void OnPongReceived() => LastPongReceived = DateTimeOffset.UtcNow; - public void OnPingReceived() => LastPingReceived = DateTimeOffset.UtcNow; + public void OnPongReceived() => LastPongReceived = Timestamper.UtcNowOffset; + public void OnPingReceived() => LastPingReceived = Timestamper.UtcNowOffset; public void RecordStatsForOutgoingMsg(DiscoveryMsg msg) { @@ -78,6 +79,6 @@ public void RecordStatsForIncomingMsg(DiscoveryMsg msg) public void OnPingSent() { - LastPingSent = DateTimeOffset.UtcNow; + LastPingSent = Timestamper.UtcNowOffset; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs deleted file mode 100644 index d081a5f46d30..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IServiceCollectionExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Lantern.Discv5.Enr; -using Microsoft.Extensions.DependencyInjection; -using Nethermind.Logging; - -namespace Nethermind.Network.Discovery.Kademlia; - -public static class IServiceCollectionExtensions -{ - /// - /// Configure the service collection with kademlia services. The following - /// dependencies are expected: - /// - /// - - /// - - /// - - /// - - /// - /// Additionally, the transport layer is expected to call the method in - /// when external message is received. - /// - /// - /// - /// The type of node - /// - public static IServiceCollection ConfigureKademliaComponents(this IServiceCollection collection) where TNode : notnull - { - return collection - .AddSingleton, Kademlia>() - .AddSingleton, KademliaKademliaMessageReceiver>() - .AddSingleton>() - .AddSingleton>() - .AddSingleton>(provider => - { - KademliaConfig config = provider.GetRequiredService>(); - if (config.UseNewLookup) - { - return provider.GetRequiredService>(); - } - - return provider.GetRequiredService>(); - }) - .AddSingleton>() - .AddSingleton, NodeHealthTracker>() - .AddSingleton, KBucketTree>(); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 6dd78fdc8179..3a56c30a00bc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -29,6 +29,7 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) .AddSingleton() + .AddSingleton>() .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); } From aa133ab313c7627cb94155f9a3111ea7fe9453dc Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 09:46:19 +0800 Subject: [PATCH 055/182] Change the unit test --- .../Nethermind.Core/ManualTimestamper.cs | 4 + .../Discv4/NeighbourMsgHandlerTests.cs | 149 ++----------- .../Discv4/NodeSessionTests.cs | 195 ++---------------- .../Discv4/NeighbourMsgHandler.cs | 2 +- .../Discv4/NodeSession.cs | 8 +- 5 files changed, 47 insertions(+), 311 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/ManualTimestamper.cs b/src/Nethermind/Nethermind.Core/ManualTimestamper.cs index e241edc18cf2..5dca28aa8f49 100644 --- a/src/Nethermind/Nethermind.Core/ManualTimestamper.cs +++ b/src/Nethermind/Nethermind.Core/ManualTimestamper.cs @@ -35,5 +35,9 @@ public void Set(DateTime utcNow) { UtcNow = utcNow; } + + public DateTimeOffset UtcNowOffset => new(UtcNow); + + public UnixTime UnixTime => new(UtcNow); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs index 20d06cb9bd97..38777239fc60 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs @@ -33,152 +33,39 @@ public void Setup() } [Test] - public void Handle_should_return_true_when_processing_valid_message() + public async Task When_TotalNodesLessThanK_ThenDontFinish_UntilTimeout() { - // Arrange var nodes = CreateNodes(5); var msg = new NeighborsMsg(_farAddress, _expirationTime, nodes); - // Act - bool result = _handler.Handle(msg); + _handler.Handle(msg).Should().BeTrue(); + _handler.TaskCompletionSource.Task.IsCompleted.Should().BeFalse(); - // Assert - result.Should().BeTrue(); + await _handler.TaskCompletionSource.Task; } [Test] - public void Handle_should_return_false_when_k_nodes_already_collected() + public void When_TotalNodesLessEqualToK_ThenFinishImmediately() { - // Arrange - // First, fill the handler with K nodes - var initialNodes = CreateNodes(K); - var initialMsg = new NeighborsMsg(_farAddress, _expirationTime, initialNodes); - _handler.Handle(initialMsg); - - // Then try to add more nodes - var additionalNodes = CreateNodes(5); - var additionalMsg = new NeighborsMsg(_farAddress, _expirationTime, additionalNodes); - - // Act - bool result = _handler.Handle(additionalMsg); - - // Assert - result.Should().BeFalse(); - } - - [Test] - public void Handle_should_return_false_when_adding_more_than_k_nodes() - { - // Arrange - // First, fill the handler with K-1 nodes - var initialNodes = CreateNodes(K - 1); - var initialMsg = new NeighborsMsg(_farAddress, _expirationTime, initialNodes); - _handler.Handle(initialMsg); - - // Then try to add more than 1 node - var additionalNodes = CreateNodes(2); - var additionalMsg = new NeighborsMsg(_farAddress, _expirationTime, additionalNodes); - - // Act - bool result = _handler.Handle(additionalMsg); - - // Assert - result.Should().BeFalse(); - } - - [Test] - public async Task TaskCompletionSource_should_complete_when_k_nodes_collected() - { - // Arrange - var nodes = CreateNodes(K); + var nodes = CreateNodes(8); var msg = new NeighborsMsg(_farAddress, _expirationTime, nodes); - // Act - _handler.Handle(msg); - - // Create a task that will complete when the TaskCompletionSource completes - Task task = _handler.TaskCompletionSource.Task; - - // Wait for the task to complete with a timeout - var completedTask = await Task.WhenAny(task, Task.Delay(100)); - - // Assert - completedTask.Should().Be(task); - task.IsCompleted.Should().BeTrue(); - task.Result.Should().HaveCount(K); - task.Result.Should().BeEquivalentTo(nodes); + _handler.Handle(msg).Should().BeTrue(); + _handler.Handle(msg).Should().BeTrue(); + _handler.Handle(msg).Should().BeFalse(); + _handler.TaskCompletionSource.Task.IsCompleted.Should().BeTrue(); } [Test] - public async Task TaskCompletionSource_should_complete_after_timeout_when_less_than_k_nodes_collected() + public async Task When_TotalNodesDoesNotAddUp_DontTakeMessage() { - // Arrange - var nodes = CreateNodes(K - 5); // Less than K nodes + var nodes = CreateNodes(10); var msg = new NeighborsMsg(_farAddress, _expirationTime, nodes); - // Act - _handler.Handle(msg); - - // Create a task that will complete when the TaskCompletionSource completes - Task task = _handler.TaskCompletionSource.Task; - - // Wait for the task to complete with a timeout longer than the handler's timeout - var completedTask = await Task.WhenAny(task, Task.Delay(1500)); // Handler timeout is 1 second - - // Assert - completedTask.Should().Be(task); - task.IsCompleted.Should().BeTrue(); - task.Result.Should().HaveCount(K - 5); - task.Result.Should().BeEquivalentTo(nodes); - } - - [Test] - public void Handle_should_accumulate_nodes_from_multiple_messages() - { - // Arrange - var firstBatch = CreateNodes(5); - var secondBatch = CreateNodes(5, 5); // Start from index 5 to create different nodes - - var firstMsg = new NeighborsMsg(_farAddress, _expirationTime, firstBatch); - var secondMsg = new NeighborsMsg(_farAddress, _expirationTime, secondBatch); - - // Act - _handler.Handle(firstMsg); - _handler.Handle(secondMsg); - - // Assert - _handler.TaskCompletionSource.Task.Wait(100); // Give a small timeout for any async operations - var result = _handler.TaskCompletionSource.Task.Result; - - result.Should().HaveCount(10); - result.Should().Contain(firstBatch); - result.Should().Contain(secondBatch); - } - - [Test] - public void Handle_should_only_initiate_timeout_once() - { - // Arrange - var firstBatch = CreateNodes(3); - var secondBatch = CreateNodes(3, 3); // Start from index 3 to create different nodes - - var firstMsg = new NeighborsMsg(_farAddress, _expirationTime, firstBatch); - var secondMsg = new NeighborsMsg(_farAddress, _expirationTime, secondBatch); - - // Act - _handler.Handle(firstMsg); // This should initiate the timeout - Task.Delay(500).Wait(); // Wait a bit, but less than the timeout - _handler.Handle(secondMsg); // This should not initiate another timeout - - // Assert - // We can't directly test that the timeout is only initiated once, - // but we can verify that the nodes are accumulated correctly - Task.Delay(1500).Wait(); // Wait for the timeout to complete - var result = _handler.TaskCompletionSource.Task.Result; - - result.Should().HaveCount(6); - result.Should().Contain(firstBatch); - result.Should().Contain(secondBatch); + _handler.Handle(msg).Should().BeTrue(); + _handler.Handle(msg).Should().BeFalse(); + _handler.TaskCompletionSource.Task.IsCompleted.Should().BeFalse(); + await _handler.TaskCompletionSource.Task; } private ArraySegment CreateNodes(int count, int startIndex = 0) @@ -186,9 +73,7 @@ private ArraySegment CreateNodes(int count, int startIndex = 0) var nodes = new Node[count]; for (int i = 0; i < count; i++) { - // Create a 64-byte (128 hex chars) public key by padding with zeros - string hexString = $"0x{(i + startIndex).ToString().PadLeft(2, '0')}".PadRight(130, '0'); - var publicKey = new PublicKey(hexString); + var publicKey = TestItem.PublicKeys[i]; nodes[i] = new Node(publicKey, $"192.168.1.{i + startIndex + 10}", 30303); } return new ArraySegment(nodes); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs index 2114df6c8f53..6278e37b7ae2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs @@ -6,7 +6,6 @@ using Nethermind.Core; using Nethermind.Network.Discovery.Discv4; using Nethermind.Stats; -using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -17,216 +16,62 @@ namespace Nethermind.Network.Discovery.Test.Discv4 public class NodeSessionTests { private INodeStats _nodeStats = null!; - private ITimestamper _timestamper = null!; + private ManualTimestamper _timestamper = null!; private NodeSession _nodeSession = null!; - private DateTimeOffset _currentTime; [SetUp] public void Setup() { - _currentTime = new DateTimeOffset(2025, 5, 13, 21, 0, 0, TimeSpan.Zero); _nodeStats = Substitute.For(); - _timestamper = Substitute.For(); - _timestamper.UtcNowOffset.Returns(_currentTime); + _timestamper = new ManualTimestamper(); + DateTimeOffset currentTime = new DateTimeOffset(2025, 5, 13, 21, 0, 0, TimeSpan.Zero); + _timestamper.Set(currentTime.LocalDateTime); _nodeSession = new NodeSession(_nodeStats, _timestamper); } [Test] - public void HasReceivedPing_should_return_false_when_no_ping_received() + public void Test_HasReceivedPing() { - // Act - bool result = _nodeSession.HasReceivedPing; - - // Assert - result.Should().BeFalse(); - } - - [Test] - public void HasReceivedPing_should_return_true_when_ping_received_within_bond_timeout() - { - // Arrange - _nodeSession.OnPingReceived(); - - // Act - bool result = _nodeSession.HasReceivedPing; - - // Assert - result.Should().BeTrue(); - } - - [Test] - public void HasReceivedPing_should_return_false_when_ping_received_outside_bond_timeout() - { - // Arrange + _nodeSession.HasReceivedPing.Should().BeFalse(); _nodeSession.OnPingReceived(); - - // Move time forward by more than the bond timeout (12 hours) - _timestamper.UtcNowOffset.Returns(_currentTime.AddHours(13)); - - // Act - bool result = _nodeSession.HasReceivedPing; - - // Assert - result.Should().BeFalse(); - } - - [Test] - public void HasReceivedPong_should_return_false_when_no_pong_received() - { - // Act - bool result = _nodeSession.HasReceivedPong; - - // Assert - result.Should().BeFalse(); - } - - [Test] - public void HasReceivedPong_should_return_true_when_pong_received_within_bond_timeout() - { - // Arrange - _nodeSession.OnPongReceived(); - - // Act - bool result = _nodeSession.HasReceivedPong; - - // Assert - result.Should().BeTrue(); - } - - [Test] - public void HasReceivedPong_should_return_false_when_pong_received_outside_bond_timeout() - { - // Arrange - _nodeSession.OnPongReceived(); - - // Move time forward by more than the bond timeout (12 hours) - _timestamper.UtcNowOffset.Returns(_currentTime.AddHours(13)); - - // Act - bool result = _nodeSession.HasReceivedPong; - - // Assert - result.Should().BeFalse(); - } - - [Test] - public void HasTriedPingRecently_should_return_false_when_no_ping_sent() - { - // Act - bool result = _nodeSession.HasTriedPingRecently; - - // Assert - result.Should().BeFalse(); - } - - [Test] - public void HasTriedPingRecently_should_return_true_when_ping_sent_within_10_minutes() - { - // Arrange - _nodeSession.OnPingSent(); - - // Act - bool result = _nodeSession.HasTriedPingRecently; - - // Assert - result.Should().BeTrue(); - } - - [Test] - public void HasTriedPingRecently_should_return_false_when_ping_sent_more_than_10_minutes_ago() - { - // Arrange - _nodeSession.OnPingSent(); - - // Move time forward by more than 10 minutes - _timestamper.UtcNowOffset.Returns(_currentTime.AddMinutes(11)); - - // Act - bool result = _nodeSession.HasTriedPingRecently; - - // Assert - result.Should().BeFalse(); + _nodeSession.HasReceivedPing.Should().BeTrue(); + _timestamper.Add(NodeSession.BondTimeout); + _nodeSession.HasReceivedPing.Should().BeFalse(); } [Test] - public void OnPongReceived_should_update_LastPongReceived() + public void Test_HasReceivedPong() { - // Arrange _nodeSession.HasReceivedPong.Should().BeFalse(); - - // Act _nodeSession.OnPongReceived(); - - // Assert _nodeSession.HasReceivedPong.Should().BeTrue(); + _timestamper.Add(NodeSession.BondTimeout); + _nodeSession.HasReceivedPong.Should().BeFalse(); } [Test] - public void OnPingReceived_should_update_LastPingReceived() - { - // Arrange - _nodeSession.HasReceivedPing.Should().BeFalse(); - - // Act - _nodeSession.OnPingReceived(); - - // Assert - _nodeSession.HasReceivedPing.Should().BeTrue(); - } - - [Test] - public void OnPingSent_should_update_LastPingSent() + public void Test_HasTriedPingRecently() { - // Arrange _nodeSession.HasTriedPingRecently.Should().BeFalse(); - - // Act _nodeSession.OnPingSent(); - - // Assert _nodeSession.HasTriedPingRecently.Should().BeTrue(); + _timestamper.Add(NodeSession.PingRetryTimeout); + _nodeSession.HasTriedPingRecently.Should().BeFalse(); } [Test] - public void NotTooManyFailure_should_return_true_initially() - { - // Act - bool result = _nodeSession.NotTooManyFailure; - - // Assert - result.Should().BeTrue(); - } - - [Test] - public void NotTooManyFailure_should_return_false_after_too_many_failures() + public void Test_NotTooManyFailures() { - // Arrange - for (int i = 0; i < 6; i++) // AuthenticatedRequestFailureLimit is 5 - { - _nodeSession.OnAuthenticatedRequestFailure(); - } - - // Act - bool result = _nodeSession.NotTooManyFailure; - - // Assert - result.Should().BeFalse(); - } + _nodeSession.NotTooManyFailure.Should().BeTrue(); + _nodeSession.OnAuthenticatedRequestFailure(); + _nodeSession.NotTooManyFailure.Should().BeTrue(); - [Test] - public void ResetAuthenticatedRequestFailure_should_reset_failure_count() - { - // Arrange - for (int i = 0; i < 6; i++) + for (int i = 0; i < NodeSession.AuthenticatedRequestFailureLimit; i++) { _nodeSession.OnAuthenticatedRequestFailure(); } _nodeSession.NotTooManyFailure.Should().BeFalse(); - - // Act _nodeSession.ResetAuthenticatedRequestFailure(); - - // Assert _nodeSession.NotTooManyFailure.Should().BeTrue(); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs index 2a3ece8b01aa..e608f5b011d0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs @@ -34,7 +34,7 @@ public bool Handle(DiscoveryMsg msg) // Some client (nethermind) only respond with one request. Task.Run(async () => { - if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false) == false) return; + if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false)) return; await Task.Delay(_secondRequestTimeout); TaskCompletionSource.TrySetResult(_current); }); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index c467b89d2172..6dd15f859c68 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -10,8 +10,10 @@ namespace Nethermind.Network.Discovery.Discv4; public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) { - private static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); - private const int AuthenticatedRequestFailureLimit = 5; + public static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); + public static readonly TimeSpan PingRetryTimeout = TimeSpan.FromMinutes(10); + public const int AuthenticatedRequestFailureLimit = 5; + private long AuthenticatedRequestFailureCount { get; set; } private DateTimeOffset LastPongReceived { get; set; } = DateTimeOffset.MinValue; private DateTimeOffset LastPingReceived { get; set; } = DateTimeOffset.MinValue; @@ -20,7 +22,7 @@ public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) public bool HasReceivedPing => LastPingReceived + BondTimeout > Timestamper.UtcNowOffset; public bool NotTooManyFailure => AuthenticatedRequestFailureCount <= AuthenticatedRequestFailureLimit; public bool HasReceivedPong => LastPongReceived + BondTimeout > Timestamper.UtcNowOffset; - public bool HasTriedPingRecently => LastPingSent + TimeSpan.FromMinutes(10) > Timestamper.UtcNowOffset; + public bool HasTriedPingRecently => LastPingSent + PingRetryTimeout > Timestamper.UtcNowOffset; public void ResetAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount = 0; public void OnAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount++; From f42a7b2b8309739b33179553948580921bfa0258 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 09:54:17 +0800 Subject: [PATCH 056/182] Reduce timeout --- .../Discv4/NeighbourMsgHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs index e608f5b011d0..38478bb68406 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs @@ -11,7 +11,8 @@ public class NeighbourMsgHandler(int k) : ITaskCompleter private Node[] _current = Array.Empty(); public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - private static readonly TimeSpan _secondRequestTimeout = TimeSpan.FromSeconds(1); + // The peer should send the two packet pretty much immediately. In any case, if the second packet is loss, its not a huge deal. + private static readonly TimeSpan _secondRequestTimeout = TimeSpan.FromMilliseconds(100); private bool _timeoutInitiated = false; public bool Handle(DiscoveryMsg msg) @@ -31,7 +32,7 @@ public bool Handle(DiscoveryMsg msg) } else { - // Some client (nethermind) only respond with one request. + // Some client (nethermind, besu) only respond with one request. Task.Run(async () => { if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false)) return; From 319e6adfa729cc56e4c91da94df760605adcd2f4 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 16:17:48 +0800 Subject: [PATCH 057/182] Unit tests --- .../Discv4/IteratorNodeLookupTests.cs | 233 ++++++++++++++++ .../Discv4/KademliaDiscv4AdapterTests.cs | 19 +- .../Discv4/KademliaNodeSourceTests.cs | 261 ++++++++++++++++++ .../Discv4/KademliaNodeSource.cs | 18 +- 4 files changed, 514 insertions(+), 17 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs new file mode 100644 index 000000000000..ff642bf15d0b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv4 +{ + [Parallelizable(ParallelScope.Self)] + [TestFixture] + public class IteratorNodeLookupTests + { + private IRoutingTable _routingTable = null!; + private IKademliaDiscv4Adapter _discv4Adapter = null!; + private IteratorNodeLookup _lookup = null!; + private Node _currentNode = null!; + private PublicKey _targetKey = null!; + + [SetUp] + public void Setup() + { + _currentNode = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + _targetKey = TestItem.PublicKeyB; + + _routingTable = Substitute.For>(); + KademliaConfig kademliaConfig = new KademliaConfig { CurrentNodeId = _currentNode }; + _discv4Adapter = Substitute.For(); + ILogManager logManager = Substitute.For(); + + _lookup = new IteratorNodeLookup(_routingTable, kademliaConfig, _discv4Adapter, logManager); + } + + [TearDown] + public async Task TearDown() + { + await _discv4Adapter.DisposeAsync(); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_return_nodes_from_routing_table(CancellationToken token) + { + Node[] expectedNodes = + [ + new(TestItem.PublicKeyC, "192.168.1.3", 30303), + new(TestItem.PublicKeyD, "192.168.1.4", 30303) + ]; + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns(expectedNodes); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); + + result.Should().BeEquivalentTo(expectedNodes); + _routingTable.Received(1).GetKNearestNeighbour( + Arg.Is(h => h == _targetKey.Hash), + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_query_nodes_and_return_neighbours(CancellationToken token) + { + Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); + Node neighbourNode = new Node(TestItem.PublicKeyD, "192.168.1.4", 30303); + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([initialNode]); + + _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + .Returns([neighbourNode]); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); + + result.Should().HaveCount(2); + result.Should().Contain(initialNode); + result.Should().Contain(neighbourNode); + + await _discv4Adapter.Received(1).FindNeighbours( + Arg.Is(n => n == initialNode), + Arg.Is(k => k == _targetKey), + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_not_query_self_node(CancellationToken token) + { + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([_currentNode]); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); + + result.Should().HaveCount(1); + result.Should().Contain(_currentNode); + + await _discv4Adapter.DidNotReceive().FindNeighbours( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_handle_empty_neighbour_response(CancellationToken token) + { + Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([initialNode]); + + _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + .Returns([]); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); + + result.Should().HaveCount(1); + result.Should().Contain(initialNode); + + await _discv4Adapter.Received(1).FindNeighbours( + Arg.Is(n => n == initialNode), + Arg.Is(k => k == _targetKey), + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_handle_exception_in_find_neighbours(CancellationToken token) + { + Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns(new[] { initialNode }); + + _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + .Returns(Task.FromException(new Exception("Test exception"))); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); + + result.Should().HaveCount(1); + result.Should().Contain(initialNode); + + await _discv4Adapter.Received(1).FindNeighbours( + Arg.Is(n => n == initialNode), + Arg.Is(k => k == _targetKey), + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public void Lookup_should_respect_cancellation_token(CancellationToken token) + { + Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([initialNode]); + + using CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + Assert.ThrowsAsync(async () => + await _lookup.Lookup(_targetKey, cts.Token).ToListAsync()); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_not_query_same_node_twice(CancellationToken token) + { + Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); + Node neighbourNode = new Node(TestItem.PublicKeyD, "192.168.1.4", 30303); + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([initialNode]); + + _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + .Returns([neighbourNode]); + + _discv4Adapter.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) + .Returns([initialNode]); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); + + result.Should().HaveCount(2); + result.Should().Contain(initialNode); + result.Should().Contain(neighbourNode); + + await _discv4Adapter.Received(1).FindNeighbours( + Arg.Is(n => n == initialNode), + Arg.Is(k => k == _targetKey), + Arg.Any()); + + await _discv4Adapter.Received(1).FindNeighbours( + Arg.Is(n => n == neighbourNode), + Arg.Is(k => k == _targetKey), + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_not_return_duplicate_nodes(CancellationToken token) + { + Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); + Node neighbourNode = new Node(TestItem.PublicKeyD, "192.168.1.4", 30303); + + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([initialNode]); + + _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + .Returns([neighbourNode]); + + _discv4Adapter.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) + .Returns([initialNode, neighbourNode]); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); + + result.Should().HaveCount(2); + result.Should().Contain(initialNode); + result.Should().Contain(neighbourNode); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index abbbfaa71c43..b23e2cd19f7c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -140,7 +140,7 @@ private T AddReceiverFarAddress(T msg) where T : DiscoveryMsg } [Test] - [CancelAfter(5000)] + [CancelAfter(10000)] public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token) { _msgSender @@ -166,7 +166,7 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => } [Test] - [CancelAfter(5000)] + [CancelAfter(10000)] public async Task FindNeighbours_should_return_nodes(CancellationToken token) { var expected = Enumerable.Repeat(new Node(TestItem.PublicKeyD, "192.168.1.3", 30303), 16).ToArray(); @@ -194,7 +194,7 @@ public async Task FindNeighbours_should_return_nodes(CancellationToken token) } [Test] - [CancelAfter(5000)] + [CancelAfter(10000)] public async Task SendEnrRequest_should_ping_then_enr_request_and_return_response(CancellationToken token) { var expectedResponse = new EnrResponseMsg( @@ -219,7 +219,8 @@ public async Task SendEnrRequest_should_ping_then_enr_request_and_return_respons } [Test] - public async Task OnIncomingMsg_ping_should_respond_with_pong() + [CancelAfter(10000)] + public async Task OnIncomingMsg_ping_should_respond_with_pong(CancellationToken token) { PingMsg pingMsg = new PingMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address); pingMsg.FarAddress = _receiver.Address; @@ -229,14 +230,15 @@ public async Task OnIncomingMsg_ping_should_respond_with_pong() await Task.Delay(100); - await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _receiver.Id), Arg.Any()); + await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _receiver.Id), token); await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); } [Test] - public async Task OnIncomingMsg_find_node_should_respond_with_neighbors() + [CancelAfter(10000)] + public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(CancellationToken token) { ConfigureBondCallback(); @@ -257,7 +259,7 @@ public async Task OnIncomingMsg_find_node_should_respond_with_neighbors() await _kademliaMessageReceiver.Received(1).FindNeighbours( Arg.Is(n => n.Id == _receiver.Id), Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), - Arg.Any()); + token); // Send out two message instead of one because of MTU limit. await _msgSender.Received(1).SendMsg(Arg.Is(m => @@ -269,7 +271,8 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => } [Test] - public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response() + [CancelAfter(10000)] + public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(CancellationToken token) { ConfigureBondCallback(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs new file mode 100644 index 000000000000..5364163e10aa --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Core.Utils; +using Nethermind.Stats; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv4 +{ + [Parallelizable(ParallelScope.Self)] + [TestFixture] + public class KademliaNodeSourceTests + { + private IKademlia _kademlia = null!; + private IRoutingTable _routingTable = null!; + private IIteratorNodeLookup _lookup = null!; + private IKademliaDiscv4Adapter _discv4Adapter = null!; + private KademliaNodeSource _nodeSource = null!; + private NodeSession _nodeSession = null!; + private INodeStats _nodeStats = null!; + private ManualTimestamper _timestamper = null!; + private DiscoveryConfig _discoveryConfig = null!; + + [SetUp] + public void Setup() + { + _kademlia = Substitute.For>(); + _routingTable = Substitute.For>(); + _lookup = Substitute.For(); + _discv4Adapter = Substitute.For(); + + _discoveryConfig = new DiscoveryConfig + { + ConcurrentDiscoveryJob = 2 + }; + + _nodeStats = Substitute.For(); + _timestamper = new ManualTimestamper(); + _timestamper.Set(new DateTimeOffset(2025, 5, 13, 21, 0, 0, TimeSpan.Zero).UtcDateTime); + + _nodeSession = new NodeSession(_nodeStats, _timestamper); + _discv4Adapter.GetSession(Arg.Any()).Returns(_nodeSession); + + _nodeSource = new KademliaNodeSource( + _kademlia, + _routingTable, + _lookup, + _discv4Adapter, + _discoveryConfig, + LimboLogs.Instance); + } + + [TearDown] + public async Task TearDown() + { + await _discv4Adapter.DisposeAsync(); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToken token) + { + Node node1 = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + Node node2 = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + _nodeSession.OnPongReceived(); + + _lookup.Lookup(Arg.Any(), token) + .Returns(CreateAsyncEnumerable(node1, node2)); + _discv4Adapter.Ping(node1, token) + .Returns(Task.CompletedTask); + _discv4Adapter.Ping(node2, token) + .Returns(Task.CompletedTask); + + var enumerator = _nodeSource.DiscoverNodes(token).GetAsyncEnumerator(token); + await enumerator.MoveNextAsync(token); + enumerator.Current.Should().Be(node1); + await enumerator.MoveNextAsync(token); + enumerator.Current.Should().Be(node2); + + _lookup.Received().Lookup(Arg.Any(), token); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(CancellationToken token) + { + Node node = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + _lookup.Lookup(Arg.Any(), token) + .Returns(CreateAsyncEnumerable(node)); + + IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); + IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await enumerator.MoveNextAsync(); + + // Assert - Verify that ping was called + await _discv4Adapter.Received(2).Ping( + Arg.Is(n => n == node), + token); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_skip_nodes_that_have_tried_ping_recently_without_pong(CancellationToken token) + { + Node node1 = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + Node node2 = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + NodeSession session1 = new NodeSession(_nodeStats, _timestamper); + NodeSession session2 = new NodeSession(_nodeStats, _timestamper); + _discv4Adapter.GetSession(node1).Returns(session1); + _discv4Adapter.GetSession(node2).Returns(session2); + + // Set up session1 to have tried ping recently without pong + session1.OnPingSent(); + + // Set up session2 to have received a pong + session2.OnPongReceived(); + + _lookup.Lookup(Arg.Any(), token) + .Returns(CreateAsyncEnumerable(node1, node2)); + + IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); + + IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await enumerator.MoveNextAsync(); + enumerator.Current.Should().Be(node2); + + await _discv4Adapter.DidNotReceive().Ping( + Arg.Is(n => n == node1), + token); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken token) + { + Node node1 = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + Node node2 = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + _discv4Adapter.Ping(node1, token) + .Returns(Task.FromException(new OperationCanceledException())); + _discv4Adapter.Ping(node2, token) + .Returns(Task.CompletedTask); + + _lookup.Lookup(Arg.Any(), token) + .Returns(CreateAsyncEnumerable(node1, node2)); + + IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); + + IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await enumerator.MoveNextAsync(); + enumerator.Current.Should().Be(node2); + + await _discv4Adapter.Received(2).Ping( + Arg.Is(n => n == node1), + token); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(CancellationToken token) + { + Node node1 = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + Node node2 = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + _nodeSession.OnPongReceived(); + + _lookup.Lookup(Arg.Any(), token) + .Returns(CreateAsyncEnumerable(node1)); + + IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); + + IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await enumerator.MoveNextAsync(); + + // Simulate node added event + _kademlia.OnNodeAdded += Raise.Event>(null, node2); + + // Continue iterating + await enumerator.MoveNextAsync(); + + Assert.That(enumerator.Current, Is.EqualTo(node2)); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_not_emit_duplicate_nodes(CancellationToken token) + { + Node node = new Node(TestItem.PublicKeyC, "192.168.1.1", 30303); + + _nodeSession.OnPongReceived(); + + _lookup.Lookup(Arg.Any(), Arg.Any()) + .Returns(CreateAsyncEnumerable(node, node)); + + using AutoCancelTokenSource shortTimeout = token.CreateChildTokenSource(TimeSpan.FromMilliseconds(100)); + IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(shortTimeout.Token); + + IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await enumerator.MoveNextAsync(); + + Func> act = () => enumerator.MoveNextAsync().AsTask(); + await act.Should().ThrowAsync(); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_use_multiple_concurrent_discovery_jobs(CancellationToken token) + { + Node node1 = new Node(TestItem.PublicKeyA, "192.168.1.1", 30303); + Node node2 = new Node(TestItem.PublicKeyB, "192.168.1.2", 30303); + + _nodeSession.OnPongReceived(); + + // Set up the lookup to return different nodes for different calls + int callCount = 0; + _lookup.Lookup(Arg.Any(), token) + .Returns(_ => + { + callCount++; + return callCount == 1 + ? CreateAsyncEnumerable(node1) + : CreateAsyncEnumerable(node2); + }); + + IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); + + IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(); + await enumerator.MoveNextAsync(); + await enumerator.MoveNextAsync(); + + // Assert - Verify that lookup was called at least twice + _lookup.Received(2).Lookup( + Arg.Any(), + token); + } + + private static async IAsyncEnumerable CreateAsyncEnumerable(params IEnumerable items) + { + foreach (T item in items) + { + await Task.Yield(); // Add an await to make the method truly async + yield return item; + } + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 3a761e404e94..22bc02627cbf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -47,7 +47,7 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); Channel ch = Channel.CreateBounded(64); - ConcurrentDictionary _writtenNodes = new(); + ConcurrentDictionary writtenNodes = new(); int duplicated = 0; int total = 0; @@ -83,7 +83,7 @@ async Task DiscoverAsync(PublicKey target) anyFound = true; count++; total++; - if (!_writtenNodes.TryAdd(node.IdHash, node.IdHash)) + if (!writtenNodes.TryAdd(node.IdHash, node.IdHash)) { duplicated++; continue; @@ -113,6 +113,12 @@ async Task DiscoverAsync(PublicKey target) { random.NextBytes(randomBytes); await DiscoverAsync(new PublicKey(randomBytes)); + + // Prevent high CPU when all node is not reachable due to network connectivity issue. + if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) + { + await Task.Delay(TimeSpan.FromSeconds(1), token); + } } catch (OperationCanceledException) { @@ -122,12 +128,6 @@ async Task DiscoverAsync(PublicKey target) { if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); } - - // Prevent high CPU when all node is not reachable due to network connectivity issue. - if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) - { - await Task.Delay(TimeSpan.FromSeconds(1), token); - } } }))); @@ -150,7 +150,7 @@ async Task DiscoverAsync(PublicKey target) void Handler(object? _, Node addedNode) { - _writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); + writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); ch.Writer.TryWrite(addedNode); // Ignore if channel full } } From 97c4211f1c088b6e02ceec255c544fcf40fcafab Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 16:21:24 +0800 Subject: [PATCH 058/182] NSubstitute --- .../Discv4/IteratorNodeLookupTests.cs | 6 +++--- .../Discv4/KademliaDiscv4AdapterTests.cs | 4 ++-- .../Discv4/KademliaNodeSourceTests.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index ff642bf15d0b..b7e16ce29737 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -160,7 +160,7 @@ await _discv4Adapter.Received(1).FindNeighbours( [Test] [CancelAfter(10000)] - public void Lookup_should_respect_cancellation_token(CancellationToken token) + public async Task Lookup_should_respect_cancellation_token(CancellationToken token) { Node initialNode = new Node(TestItem.PublicKeyC, "192.168.1.3", 30303); @@ -170,8 +170,8 @@ public void Lookup_should_respect_cancellation_token(CancellationToken token) using CancellationTokenSource cts = new CancellationTokenSource(); cts.Cancel(); - Assert.ThrowsAsync(async () => - await _lookup.Lookup(_targetKey, cts.Token).ToListAsync()); + Func act = async () => await _lookup.Lookup(_targetKey, cts.Token).ToListAsync(); + await act.Should().ThrowAsync(); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index b23e2cd19f7c..f2889efb5fbc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -230,7 +230,7 @@ public async Task OnIncomingMsg_ping_should_respond_with_pong(CancellationToken await Task.Delay(100); - await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _receiver.Id), token); + await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _receiver.Id), Arg.Any()); await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); @@ -259,7 +259,7 @@ public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(Cancella await _kademliaMessageReceiver.Received(1).FindNeighbours( Arg.Is(n => n.Id == _receiver.Id), Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), - token); + Arg.Any()); // Send out two message instead of one because of MTU limit. await _msgSender.Received(1).SendMsg(Arg.Is(m => diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs index 5364163e10aa..9f63406655e2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -193,7 +193,7 @@ public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(Cancellat // Continue iterating await enumerator.MoveNextAsync(); - Assert.That(enumerator.Current, Is.EqualTo(node2)); + enumerator.Current.Should().Be(node2); } [Test] From d7e1a7ad1520d18ea0eb4283587338cc8472f4f6 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 16:24:42 +0800 Subject: [PATCH 059/182] Whitespace --- .../Discv4/KademliaDiscv4AdapterTests.cs | 4 ++-- .../Kademlia/KademliaSimulation.cs | 12 ++++++------ .../Kademlia/KademliaTests.cs | 6 +++--- .../Discv4/DiscV4KademliaModule.cs | 2 +- .../Discv4/EnrResponseHandler.cs | 3 ++- .../Discv4/IKademliaDiscv4Adapter.cs | 4 ++-- .../Discv4/IMessageHandler.cs | 2 +- .../Discv4/KademliaDiscv4Adapter.cs | 5 +++-- .../Kademlia/IKademliaMessageSender.cs | 2 +- .../Kademlia/INodeHashProvider.cs | 3 ++- .../Kademlia/KBucketTree.cs | 6 +++--- .../Kademlia/Kademlia.cs | 2 +- .../Kademlia/KademliaMessageReceiver.cs | 2 +- .../Kademlia/LookupKNearestNeighbour.cs | 7 ++++--- .../Kademlia/OriginalLookupKNearestNeighbour.cs | 9 +++++---- 15 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index f2889efb5fbc..a0bcf98f9155 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -78,7 +78,7 @@ public void Setup() _networkConfig.MaxActivePeers.Returns(25); _kademliaConfig = new KademliaConfig { CurrentNodeId = _testNode }; - _selfNodeRecord = CreateNodeRecord();; + _selfNodeRecord = CreateNodeRecord(); ; _logManager = LimboLogs.Instance; _timestamper = Substitute.For(); @@ -184,7 +184,7 @@ public async Task FindNeighbours_should_return_nodes(CancellationToken token) Task.Run(() => _adapter.OnIncomingMsg(neighbors)); ArraySegment neighbours2 = expected[12..]; - var neighbors2 = new NeighborsMsg( _receiver.Address, _timestamper.UnixTime.SecondsLong + 1, neighbours2); + var neighbors2 = new NeighborsMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 1, neighbours2); neighbors2 = AddReceiverFarAddress(neighbors2); Task.Run(() => _adapter.OnIncomingMsg(neighbors2)); }); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 011574c17f38..d26ff328d164 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -22,7 +22,7 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; [TestFixture(false, 3, 0)] [TestFixture(true, 1, 0)] [TestFixture(true, 1, 4)] -[TestFixture(true, 3, 0)] +[TestFixture(true, 3, 0)] [TestFixture(true, 3, 4)] public class KademliaSimulation { @@ -99,7 +99,7 @@ public async Task TestKNearestNeighbour() .Select(n => n.Hash) .ToHashSet() .Should() - .BeEquivalentTo(new HashSet() {node1Hash }); + .BeEquivalentTo(new HashSet() { node1Hash }); Kademlia node2 = fabric.CreateNode(node2Hash); fabric.CreateNode(node3Hash); @@ -113,7 +113,7 @@ public async Task TestKNearestNeighbour() .Select(n => n.Hash) .ToHashSet() .Should() - .BeEquivalentTo(new HashSet() {node1Hash, node2Hash, node3Hash }); + .BeEquivalentTo(new HashSet() { node1Hash, node2Hash, node3Hash }); (await node1.LookupNodesClosest(node3Hash, cts.Token, 1)) .First().Hash @@ -196,7 +196,7 @@ private static ValueHash256 RandomKeccak(Random rand) return val; } - private class ValueHashNodeHashProvider: INodeHashProvider, IKeyOperator + private class ValueHashNodeHashProvider : INodeHashProvider, IKeyOperator { public ValueHash256 GetHash(TestNode node) { @@ -270,9 +270,9 @@ public Kademlia CreateNode(ValueHash256 nodeID) }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton>(); - + var container = builder.Build(); - + _nodes[nodeID] = container; return container.Resolve>(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 9f0db7f95923..9b6dc894e2c2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -31,7 +31,7 @@ private Kademlia CreateKad(KademliaConfig>(); - + var container = builder.Build(); return container.Resolve>(); } @@ -70,7 +70,7 @@ public async Task TestTooManyNode() Beta = 0, }); - ValueHash256[] testHashes = Enumerable.Range(0, 10).Select((k) => Hash256XorUtils.GetRandomHashAtDistance( ValueKeccak.Zero, 250) ).ToArray(); + ValueHash256[] testHashes = Enumerable.Range(0, 10).Select((k) => Hash256XorUtils.GetRandomHashAtDistance(ValueKeccak.Zero, 250)).ToArray(); foreach (ValueHash256 valueHash256 in testHashes[..10]) { kad.AddOrRefresh(valueHash256); @@ -163,7 +163,7 @@ public async Task TestTooManyNodeWithAcceleratedLookup() kad.GetAllAtDistance(250).ToHashSet().Should().BeEquivalentTo(testHashes[10..].ToHashSet()); } - private class ValueHashNodeHashProvider: INodeHashProvider, IKeyOperator + private class ValueHashNodeHashProvider : INodeHashProvider, IKeyOperator { public ValueHash256 GetHash(ValueHash256 node) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index c25d37dbe07e..f6865f74793a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery; -public class DiscV4KademliaModule(NodeRecord selfNodeRecord, PublicKey masterNode, IReadOnlyList bootNodes): Module +public class DiscV4KademliaModule(NodeRecord selfNodeRecord, PublicKey masterNode, IReadOnlyList bootNodes) : Module { protected override void Load(ContainerBuilder builder) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs index 531d1e5859bf..738aca8be866 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs @@ -5,7 +5,8 @@ namespace Nethermind.Network.Discovery.Discv4; -public class EnrResponseHandler : ITaskCompleter { +public class EnrResponseHandler : ITaskCompleter +{ public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public bool Handle(DiscoveryMsg msg) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs index a4d77deedf11..4f581e0ff21b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs @@ -17,14 +17,14 @@ public interface IKademliaDiscv4Adapter : IKademliaMessageSender IMsgSender? MsgSender { get; set; } - + /// /// Gets the session for a specific node. /// /// The node to get the session for. /// The node session. NodeSession GetSession(Node node); - + /// /// Sends an ENR request to a node and returns the response. /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs index 4a00ab38e58b..5126746f735c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs @@ -11,7 +11,7 @@ internal interface IMessageHandler } -internal interface ITaskCompleter: IMessageHandler +internal interface ITaskCompleter : IMessageHandler { TaskCompletionSource TaskCompletionSource { get; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 8222901f6543..a6daf296f658 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -29,7 +29,7 @@ public class KademliaDiscv4Adapter( ITimestamper timestamper, IProcessExitSource processExitSource, ILogManager logManager -): IKademliaDiscv4Adapter +) : IKademliaDiscv4Adapter { private readonly TimeSpan _requestEnrTimeout = TimeSpan.FromSeconds(10); private readonly TimeSpan _findNeighbourTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); @@ -119,7 +119,8 @@ private async Task CallAndWaitForResponse( NodeSession session, DiscoveryMsg msg, CancellationToken token - ) { + ) + { await using CancellationTokenRegistration unregister = token.RegisterToCompletionSource(messageHandler.TaskCompletionSource); AddMessageHandler(msgType, receiver.IdHash, messageHandler); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs index e27ca6400ceb..7cafdd1e4568 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs @@ -19,7 +19,7 @@ public interface IKademliaMessageSender /// Application should call this class on incoming messages. /// /// -public interface IKademliaMessageReceiver: IKademliaMessageSender +public interface IKademliaMessageReceiver : IKademliaMessageSender { } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs index 4106a6a3eb7f..e9b205583101 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs @@ -20,7 +20,8 @@ public interface INodeHashProvider ValueHash256 GetHash(TNode node); } -public interface IKeyOperator { +public interface IKeyOperator +{ TKey GetKey(TNode node); ValueHash256 GetKeyHash(TKey key); TKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 92d8baa82497..0801ab3df831 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -8,7 +8,7 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class KBucketTree: IRoutingTable where TNode : notnull +public class KBucketTree : IRoutingTable where TNode : notnull { private class TreeNode { @@ -351,8 +351,8 @@ private void LogTreeStructureRecursive(TreeNode node, string indent, bool last, } sb.AppendLine($"Node (Depth: {depth})"); - LogTreeStructureRecursive(node.Left!, indent, false, depth+1, sb); - LogTreeStructureRecursive(node.Right!, indent, true, depth+1, sb); + LogTreeStructureRecursive(node.Left!, indent, false, depth + 1, sb); + LogTreeStructureRecursive(node.Right!, indent, true, depth + 1, sb); } private void LogTreeStatistics() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index 1d3b5930b221..99b2384f3dfe 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -151,7 +151,7 @@ public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool exclu ValueHash256? excludeHash = null; if (excluding != null) excludeHash = _nodeHashProvider.GetHash(excluding); ValueHash256 hash = _keyOperator.GetKeyHash(target); - return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); + return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); } public event EventHandler OnNodeAdded diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs index a5164b623e29..b6d4e29b2447 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs @@ -8,7 +8,7 @@ namespace Nethermind.Network.Discovery.Kademlia; public class KademliaKademliaMessageReceiver( IKademlia kademlia, INodeHealthTracker healthTracker -): IKademliaMessageReceiver where TNode : notnull +) : IKademliaMessageReceiver where TNode : notnull { public Task Ping(TNode sender, CancellationToken token) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs index fe2ac0949100..629b000d590e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs @@ -22,7 +22,7 @@ public class LookupKNearestNeighbour( INodeHashProvider nodeHashProvider, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager): ILookupAlgo where TNode : notnull + ILogManager logManager) : ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; private readonly ILogger _logger = logManager.GetClassLogger>(); @@ -32,7 +32,8 @@ public async Task Lookup( int k, Func> findNeighbourOp, CancellationToken token - ) { + ) + { if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -212,7 +213,7 @@ bool ShouldStopDueToNoBetterResult(out int round) using var _ = queueLock.Acquire(); round = Interlocked.Increment(ref currentRound); - if (finalResult.Count >= k && round - closestNodeRound >= (config.Alpha*2)) + if (finalResult.Count >= k && round - closestNodeRound >= (config.Alpha * 2)) { // No closer node for more than or equal to _alpha*2 round. // Assume exit condition diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs index f3267f0c7e54..d3b90b7a4336 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs @@ -15,7 +15,7 @@ public class OriginalLookupKNearestNeighbour( INodeHashProvider nodeHashProvider, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager): ILookupAlgo where TNode : notnull + ILogManager logManager) : ILookupAlgo where TNode : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; private readonly ILogger _logger = logManager.GetClassLogger>(); @@ -25,7 +25,8 @@ public async Task Lookup( int k, Func> findNeighbourOp, CancellationToken token - ) { + ) + { if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); Dictionary queried = new(); @@ -36,10 +37,10 @@ CancellationToken token Hash256XorUtils.Compare(h1, h2, targetHash)); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue bestSeen = new (comparer); + PriorityQueue bestSeen = new(comparer); // Ordered by lowest distance. Will not get popped for next round, but will at final collection. - PriorityQueue bestSeenAllTime = new (comparer); + PriorityQueue bestSeenAllTime = new(comparer); ValueHash256 closestNodeHash = Hash256XorUtils.GetOppositeHash(targetHash); (ValueHash256 nodeHash, TNode node)[] roundQuery = routingTable.GetKNearestNeighbour(targetHash, default) From 4ed05f4967d1eae935ea6c8796028fe66f68d188 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 16:33:42 +0800 Subject: [PATCH 060/182] Remove some code --- src/Nethermind/Directory.Packages.props | 3 +- .../Nethermind.Core/Nethermind.Core.csproj | 1 - .../NettyDiscoveryHandlerTests.cs | 45 +++++++++---------- src/Nethermind/Nethermind.Network/PeerPool.cs | 12 ----- 4 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/Nethermind/Directory.Packages.props b/src/Nethermind/Directory.Packages.props index 6cb96faee9b2..6d9ace38d3d8 100644 --- a/src/Nethermind/Directory.Packages.props +++ b/src/Nethermind/Directory.Packages.props @@ -68,7 +68,6 @@ - @@ -86,4 +85,4 @@ - \ No newline at end of file + diff --git a/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj b/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj index a62645e96c8d..54af4e25febe 100644 --- a/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj +++ b/src/Nethermind/Nethermind.Core/Nethermind.Core.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs index 980539a79ad9..8377862c49e0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs @@ -16,6 +16,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Test.Builders; using Nethermind.Serialization.Rlp; @@ -29,12 +30,11 @@ namespace Nethermind.Network.Discovery.Test [TestFixture] public class NettyDiscoveryHandlerTests { - /* private readonly PrivateKey _privateKey = new("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee"); private readonly PrivateKey _privateKey2 = new("3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"); private List _channels = new(); private List _discoveryHandlers = new(); - private List _discoveryManagersMocks = new(); + private List _kademliaAdaptersMocks = new(); private readonly IPEndPoint _address = new(IPAddress.Loopback, 10001); private readonly IPEndPoint _address2 = new(IPAddress.Loopback, 10002); private int _channelActivatedCounter; @@ -44,19 +44,19 @@ public async Task Initialize() { _channels = new List(); _discoveryHandlers = new List(); - _discoveryManagersMocks = new List(); + _kademliaAdaptersMocks = new List(); _channelActivatedCounter = 0; - IDiscoveryManager? discoveryManagerMock = Substitute.For(); + IKademliaDiscv4Adapter? kademliaAdapterMock = Substitute.For(); IMessageSerializationService? messageSerializationService = Build.A.SerializationService().WithDiscovery(_privateKey).TestObject; - IDiscoveryManager? discoveryManagerMock2 = Substitute.For(); + IKademliaDiscv4Adapter? kademliaAdapterMock2 = Substitute.For(); IMessageSerializationService? messageSerializationService2 = Build.A.SerializationService().WithDiscovery(_privateKey).TestObject; - await StartUdpChannel("127.0.0.1", 10001, discoveryManagerMock, messageSerializationService); - await StartUdpChannel("127.0.0.1", 10002, discoveryManagerMock2, messageSerializationService2); + await StartUdpChannel("127.0.0.1", 10001, kademliaAdapterMock, messageSerializationService); + await StartUdpChannel("127.0.0.1", 10002, kademliaAdapterMock2, messageSerializationService2); - _discoveryManagersMocks.Add(discoveryManagerMock); - _discoveryManagersMocks.Add(discoveryManagerMock2); + _kademliaAdaptersMocks.Add(kademliaAdapterMock); + _kademliaAdaptersMocks.Add(kademliaAdapterMock2); Assert.That(() => _channelActivatedCounter, Is.EqualTo(2).After(1000, 100)); } @@ -81,7 +81,7 @@ public async Task PingSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); + await _kademliaAdaptersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); PingMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, _address2, _address, new byte[32]) { @@ -90,7 +90,7 @@ public async Task PingSentReceivedTest() await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); + await _kademliaAdaptersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Ping)); AssertMetrics(258); } @@ -108,7 +108,7 @@ public async Task PongSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); + await _kademliaAdaptersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); PongMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new byte[] { 1, 2, 3 }) { @@ -116,7 +116,7 @@ public async Task PongSentReceivedTest() }; await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); + await _kademliaAdaptersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); AssertMetrics(240); } @@ -134,7 +134,7 @@ public async Task FindNodeSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); + await _kademliaAdaptersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); FindNodeMsg msg2 = new(_privateKey2.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new byte[] { 1, 2, 3 }) { @@ -143,7 +143,7 @@ public async Task FindNodeSentReceivedTest() await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); + await _kademliaAdaptersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.FindNode)); AssertMetrics(216); } @@ -161,7 +161,7 @@ public async Task NeighborsSentReceivedTest() await _discoveryHandlers[0].SendMsg(msg); await SleepWhileWaiting(); - await _discoveryManagersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); + await _kademliaAdaptersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); NeighborsMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new List().ToArray()) { @@ -170,7 +170,7 @@ public async Task NeighborsSentReceivedTest() await _discoveryHandlers[1].SendMsg(msg2); await SleepWhileWaiting(); - await _discoveryManagersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); + await _kademliaAdaptersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); AssertMetrics(210); } @@ -202,7 +202,7 @@ private static void AssertMetrics(int value) Metrics.DiscoveryBytesReceived.Should().Be(value); } - private async Task StartUdpChannel(string address, int port, IDiscoveryManager discoveryManager, IMessageSerializationService service) + private async Task StartUdpChannel(string address, int port, IKademliaDiscv4Adapter kademliaAdapter, IMessageSerializationService service) { MultithreadEventLoopGroup group = new(1); @@ -210,20 +210,20 @@ private async Task StartUdpChannel(string address, int port, IDiscoveryManager d bootstrap .Group(group) .ChannelFactory(() => new SocketDatagramChannel(AddressFamily.InterNetwork)) - .Handler(new ActionChannelInitializer(x => InitializeChannel(x, discoveryManager, service))); + .Handler(new ActionChannelInitializer(x => InitializeChannel(x, kademliaAdapter, service))); _channels.Add(await bootstrap.BindAsync(IPAddress.Parse(address), port)); } - private void InitializeChannel(IDatagramChannel channel, IDiscoveryMsgListener discoveryManager, IMessageSerializationService service) + private void InitializeChannel(IDatagramChannel channel, IKademliaDiscv4Adapter kademliaAdapter, IMessageSerializationService service) { - NettyDiscoveryHandler handler = new(discoveryManager, channel, service, new Timestamper(), LimboLogs.Instance); + NettyDiscoveryHandler handler = new(kademliaAdapter, channel, service, new Timestamper(), LimboLogs.Instance); handler.OnChannelActivated += (_, _) => { _channelActivatedCounter++; }; _discoveryHandlers.Add(handler); - discoveryManager.MsgSender = handler; + kademliaAdapter.MsgSender = handler; channel.Pipeline .AddLast(new LoggingHandler(DotNetty.Handlers.Logging.LogLevel.TRACE)) .AddLast(handler); @@ -233,6 +233,5 @@ private static async Task SleepWhileWaiting() { await Task.Delay((TestContext.CurrentContext.CurrentRepeatCount + 1) * 300); } - */ } } diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index 40f2d591691e..f65f5d1795d2 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -16,7 +16,6 @@ using Nethermind.Network.P2P; using Nethermind.Stats; using Nethermind.Stats.Model; -using Prometheus; namespace Nethermind.Network { @@ -285,21 +284,10 @@ private async Task FeedFromNodeSource() await Task.Delay(1000, token); } - int prevCount = PeerCount; GetOrAdd(node); - if (PeerCount == prevCount) - { - _addStates.WithLabels("duplicate").Inc(); - } - else if (PeerCount > prevCount) - { - _addStates.WithLabels("add").Inc(); - } } } - private Counter _addStates = Prometheus.Metrics.CreateCounter("peer_pool_add", "add", "status"); - public async Task StopAsync() { _cancellationTokenSource.Cancel(); From aef2d120c595b61c46623bd769ea6d5a1ebcb59b Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 16:42:09 +0800 Subject: [PATCH 061/182] Remove more metric --- .../Discv4/KademliaNodeSourceTests.cs | 3 --- .../Discv4/IteratorNodeLookup.cs | 11 +---------- .../Discv4/KademliaDiscv4Adapter.cs | 10 ---------- .../Discv4/KademliaNodeSource.cs | 12 ------------ 4 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs index 9f63406655e2..93d83095c772 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -26,7 +26,6 @@ namespace Nethermind.Network.Discovery.Test.Discv4 public class KademliaNodeSourceTests { private IKademlia _kademlia = null!; - private IRoutingTable _routingTable = null!; private IIteratorNodeLookup _lookup = null!; private IKademliaDiscv4Adapter _discv4Adapter = null!; private KademliaNodeSource _nodeSource = null!; @@ -39,7 +38,6 @@ public class KademliaNodeSourceTests public void Setup() { _kademlia = Substitute.For>(); - _routingTable = Substitute.For>(); _lookup = Substitute.For(); _discv4Adapter = Substitute.For(); @@ -57,7 +55,6 @@ public void Setup() _nodeSource = new KademliaNodeSource( _kademlia, - _routingTable, _lookup, _discv4Adapter, _discoveryConfig, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs index d871c3d2905d..9a82d4a85712 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Diagnostics; using System.Runtime.CompilerServices; using Nethermind.Core.Caching; using Nethermind.Core.Crypto; @@ -10,7 +9,6 @@ using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; using NonBlocking; -using Prometheus; namespace Nethermind.Network.Discovery.Discv4; @@ -180,8 +178,6 @@ bool ShouldStop() } } - private Counter _findNeighbourRate = Prometheus.Metrics.CreateCounter("lookup_find_neighbour_status", "", "status"); - async Task FindNeighbour(Node node, PublicKey target, CancellationToken token) { try @@ -192,20 +188,15 @@ bool ShouldStop() return []; } - Node[]? ret = await discv4Adapter.FindNeighbours(node, target, token); - _findNeighbourRate.WithLabels("ok").Inc(); - - return ret; + return await discv4Adapter.FindNeighbours(node, target, token); } catch (OperationCanceledException) { _unreacheableNodes.Set(node.IdHash, DateTimeOffset.Now); - _findNeighbourRate.WithLabels("timout").Inc(); return null; } catch (Exception e) { - _findNeighbourRate.WithLabels("failed").Inc(); if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); return null; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index a6daf296f658..93b5927ec38b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -14,7 +14,6 @@ using Nethermind.Stats; using Nethermind.Stats.Model; using NonBlocking; -using Prometheus; namespace Nethermind.Network.Discovery.Discv4; @@ -202,14 +201,10 @@ public async Task SendEnrRequest(Node receiver, CancellationToke }, token); } - private Counter _rejectedIncomingMessage = - Prometheus.Metrics.CreateCounter("rejected_incoming_message", "Unhaandled", "type"); - private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg, CancellationToken token) { if (!session.HasReceivedPong) { - _rejectedIncomingMessage.WithLabels(msg.MsgType.ToString()).Inc(); if (_logger.IsDebug) _logger.Debug($"Rejecting enr request from unbonded peer {node}"); return; } @@ -222,7 +217,6 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms { if (!session.HasReceivedPong) { - _rejectedIncomingMessage.WithLabels(msg.MsgType.ToString()).Inc(); if (_logger.IsDebug) _logger.Debug($"Rejecting findNode request from unbonded peer {node}"); return; } @@ -255,9 +249,6 @@ private async Task HandlePing(Node node, NodeSession session, PingMsg ping, Canc } } - private Counter _unhandledDiscoveryMesssage = - Prometheus.Metrics.CreateCounter("unhandled_disc_message", "Unhaandled", "type"); - public async Task OnIncomingMsg(DiscoveryMsg msg) { try @@ -294,7 +285,6 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) case MsgType.Neighbors: case MsgType.Pong: case MsgType.EnrResponse: - _unhandledDiscoveryMesssage.WithLabels(msgType.ToString()).Inc(); break; default: if (_logger.IsError) _logger.Error($"Unsupported msgType: {msgType}"); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 22bc02627cbf..5a74ad701c18 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -9,7 +9,6 @@ using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -using Prometheus; namespace Nethermind.Network.Discovery.Discv4; @@ -17,26 +16,19 @@ namespace Nethermind.Network.Discovery.Discv4; public class KademliaNodeSource : IKademliaNodeSource { private readonly IKademlia _kademlia; - private readonly IRoutingTable _routingTable; private readonly IIteratorNodeLookup _lookup; private readonly IKademliaDiscv4Adapter _discv4Adapter; private readonly IDiscoveryConfig _discoveryConfig; private readonly ILogger _logger; - private readonly Counter _discoverRound = Prometheus.Metrics.CreateCounter("kademlia_discover_rounds", "discovery rounds"); - private readonly Counter _discoverPingResult = Prometheus.Metrics.CreateCounter("kademlia_discover_ping", "discovery rounds", "result"); - private readonly Gauge _kademliaSize = Prometheus.Metrics.CreateGauge("kademlia_routing_table_size", "discovery rounds", "result"); - public KademliaNodeSource( IKademlia kademlia, - IRoutingTable routingTable, IIteratorNodeLookup lookup2, IKademliaDiscv4Adapter discv4Adapter, IDiscoveryConfig discoveryConfig, ILogManager logManager) { _kademlia = kademlia; - _routingTable = routingTable; _lookup = lookup2; _discv4Adapter = discv4Adapter; _discoveryConfig = discoveryConfig; @@ -53,7 +45,6 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance async Task DiscoverAsync(PublicKey target) { - _discoverRound.Inc(); if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); bool anyFound = false; int count = 0; @@ -70,16 +61,13 @@ async Task DiscoverAsync(PublicKey target) try { await _discv4Adapter.Ping(node, token); - _discoverPingResult.WithLabels("ok").Inc(); } catch (OperationCanceledException) { - _discoverPingResult.WithLabels("timeout").Inc(); continue; } } - _kademliaSize.Set(_routingTable.Size); anyFound = true; count++; total++; From e2eb7411d824cf92703575b08a3a22613dd54268 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 17:13:17 +0800 Subject: [PATCH 062/182] Gonna commit this first --- .../DiscoveryPersistenceManagerTests.cs | 199 ++++++++++++++++++ .../DiscoveryApp.cs | 112 ++-------- .../DiscoveryPersistenceManager.cs | 141 +++++++++++++ .../Discv4/DiscV4KademliaModule.cs | 4 +- .../EthereumRunnerTests.cs | 2 + 5 files changed, 362 insertions(+), 96 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs new file mode 100644 index 000000000000..6d81dc783af0 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Config; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Stats; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test +{ + [Parallelizable(ParallelScope.Self)] + public class DiscoveryPersistenceManagerTests + { + private INetworkStorage _networkStorage = null!; + private INodeStatsManager _nodeStatsManager = null!; + private IKademliaDiscv4Adapter _discv4Adapter = null!; + private IDiscoveryConfig _discoveryConfig = null!; + private ILogManager _logManager = null!; + private IKademlia _kademlia = null!; + private DiscoveryPersistenceManager _persistenceManager = null!; + + [SetUp] + public void Setup() + { + _networkStorage = Substitute.For(); + _nodeStatsManager = Substitute.For(); + _discv4Adapter = Substitute.For(); + _discoveryConfig = Substitute.For(); + _logManager = LimboLogs.Instance; + _kademlia = Substitute.For>(); + + _discoveryConfig.DiscoveryPersistenceInterval.Returns(1000); + + _persistenceManager = new DiscoveryPersistenceManager( + _networkStorage, + _nodeStatsManager, + _discv4Adapter, + _discoveryConfig, + _logManager); + } + + [Test] + public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node() + { + // Arrange + var networkNodes = new[] + { + new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0), + new NetworkNode(TestItem.PublicKeyB, "192.168.1.2", 30303, 0) + }; + + _networkStorage.GetPersistedNodes().Returns(networkNodes); + + // Act + await _persistenceManager.AddPersistedNodes(CancellationToken.None); + + // Assert + await _discv4Adapter.Received(networkNodes.Length).Ping( + Arg.Is(n => networkNodes.Any(nn => nn.NodeId.Equals(n.Id) && nn.Host == n.Host && nn.Port == n.Port)), + Arg.Any()); + } + + [Test] + public async Task AddPersistedNodes_Should_Skip_Invalid_Nodes() + { + // Arrange + var validNode = new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0); + // An invalid node with null NodeId + var invalidNode = new NetworkNode(null, "192.168.1.2", 30303, 0); + + var networkNodes = new[] { validNode, invalidNode }; + + _networkStorage.GetPersistedNodes().Returns(networkNodes); + + // Act + await _persistenceManager.AddPersistedNodes(CancellationToken.None); + + // Assert - only one ping should be attempted + await _discv4Adapter.Received(1).Ping( + Arg.Is(n => n.Id.Equals(validNode.NodeId) && n.Host == validNode.Host && n.Port == validNode.Port), + Arg.Any()); + } + + [Test] + public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions() + { + // Arrange + var networkNodes = new[] + { + new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0), + new NetworkNode(TestItem.PublicKeyB, "192.168.1.2", 30303, 0) + }; + + _networkStorage.GetPersistedNodes().Returns(networkNodes); + + // First ping succeeds, second one throws + _discv4Adapter.Ping( + Arg.Is(n => n.Id.Equals(networkNodes[0].NodeId)), + Arg.Any()) + .Returns(Task.CompletedTask); + + _discv4Adapter.Ping( + Arg.Is(n => n.Id.Equals(networkNodes[1].NodeId)), + Arg.Any()) + .Returns(x => throw new Exception("Test exception")); + + // Act & Assert - should not throw + await _persistenceManager.AddPersistedNodes(CancellationToken.None); + } + + [Test] + public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() + { + // Arrange + var nodes = new[] + { + new Node(TestItem.PublicKeyA, "192.168.1.1", 30303), + new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) + }; + + var cancellationSource = new CancellationTokenSource(); + + _kademlia.IterateNodes().Returns(nodes.ToAsyncEnumerable()); + + // Act - start the persistence process + var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationSource.Token); + + // Wait a bit to allow at least one persistence cycle to complete + await Task.Delay(50); + + // Cancel the task so we can complete the test + cancellationSource.Cancel(); + + try + { + await persistenceTask; + } + catch (OperationCanceledException) + { + // Expected + } + + // Assert + _networkStorage.Received().StartBatch(); + _networkStorage.Received().UpdateNodes(Arg.Is>(nn => + nn.Count() == nodes.Length && + nn.All(n => nodes.Any(node => node.Id.Equals(n.NodeId) && node.Host == n.Host && node.Port == n.Port)))); + _networkStorage.Received().Commit(); + } + + [Test] + public async Task RunDiscoveryPersistenceCommit_Should_Handle_Exceptions() + { + // Arrange + var nodes = new[] + { + new Node(TestItem.PublicKeyA, "192.168.1.1", 30303), + new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) + }; + + var cancellationSource = new CancellationTokenSource(); + + _kademlia.IterateNodes().Returns(nodes.ToAsyncEnumerable()); + _networkStorage.When(x => x.StartBatch()).Throw(new Exception("Test exception")); + + // Act - start the persistence process + var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationSource.Token); + + // Wait a bit to allow at least one persistence cycle to complete + await Task.Delay(50); + + // Cancel the task so we can complete the test + cancellationSource.Cancel(); + + try + { + await persistenceTask; + } + catch (OperationCanceledException) + { + // Expected + } + + // If we got here without other exceptions, the error was properly handled + Assert.Pass(); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 009a106bc0e6..5b8b21d30261 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -1,19 +1,15 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Autofac; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; using Nethermind.Config; using Nethermind.Core; -using Nethermind.Core.Attributes; using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; -using Nethermind.Network.Enr; -using Nethermind.Stats; using Nethermind.Stats.Model; using LogLevel = DotNetty.Handlers.Logging.LogLevel; @@ -26,46 +22,44 @@ public class DiscoveryApp : IDiscoveryApp private readonly ILogManager _logManager; private readonly ILogger _logger; private readonly IMessageSerializationService _messageSerializationService; - private readonly INetworkStorage _discoveryStorage; private readonly INetworkConfig _networkConfig; - private readonly INodeStatsManager _nodeStatsManager; + private DiscoveryPersistenceManager? _persistenceManager; + /* + private readonly INetworkStorage _discoveryStorage; + private readonly INodeStatsManager _nodeStatsManager; private ILifetimeScope? _kademliaServices; + private DiscoveryPersistenceManager? _persistenceManager; + private PublicKey _masterNode = null!; + private readonly NodeRecord _selfNodeRecorrd; + private readonly ILifetimeScope _rootLifetimeScope; + */ private readonly List _bootNodes; - private PublicKey _masterNode = null!; - private readonly NodeRecord _selfNodeRecorrd; private IKademliaDiscv4Adapter _discv4Adapter = null!; private IKademlia _kademlia = null!; private NettyDiscoveryHandler? _discoveryHandler; - private readonly ILifetimeScope _rootLifetimeScope; private IKademliaNodeSource _kademliaNodeSource = null!; private Task? _runningTask; private readonly IProcessExitSource _processExitSouce; public DiscoveryApp( - NodeRecord selfNodeRecord, - ILifetimeScope lifetimeScope, - INodeStatsManager nodeStatsManager, IMessageSerializationService? msgSerializationService, - INetworkStorage? discoveryStorage, + DiscoveryPersistenceManager persistenceManager, INetworkConfig? networkConfig, IDiscoveryConfig? discoveryConfig, ITimestamper? timestamper, IProcessExitSource processExitSource, ILogManager? logManager) { - _selfNodeRecorrd = selfNodeRecord; - _rootLifetimeScope = lifetimeScope; - _nodeStatsManager = nodeStatsManager; _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); _logger = _logManager.GetClassLogger(); _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); _messageSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); - _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); + _persistenceManager = persistenceManager; _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); _processExitSouce = processExitSource ?? throw new ArgumentNullException(nameof(processExitSource)); @@ -92,7 +86,7 @@ public DiscoveryApp( public void Initialize(PublicKey masterPublicKey) { - _masterNode = masterPublicKey; + /* _kademliaServices = _rootLifetimeScope .BeginLifetimeScope((builder) => builder.AddModule( new DiscV4KademliaModule(_selfNodeRecorrd, _masterNode, _bootNodes))); @@ -100,6 +94,8 @@ public void Initialize(PublicKey masterPublicKey) _kademlia = _kademliaServices.Resolve>(); _discv4Adapter = _kademliaServices.Resolve(); _kademliaNodeSource = _kademliaServices.Resolve(); + _persistenceManager = _kademliaServices.Resolve(); + */ } public Task StartAsync() @@ -146,7 +142,7 @@ public async Task StopAsync() } if (_logger.IsInfo) _logger.Info("Discovery shutdown complete.. please wait for all components to close"); - _kademliaServices?.DisposeAsync(); + // _kademliaServices?.DisposeAsync(); } public void AddNodeToDiscovery(Node node) @@ -208,9 +204,9 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) try { // Step 1 - read nodes and stats from db - await AddPersistedNodes(cancellationToken); + await _persistenceManager!.AddPersistedNodes(cancellationToken); - Task persistenceTask = RunDiscoveryPersistenceCommit(cancellationToken); + Task persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); try { @@ -231,80 +227,6 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) if (_logger.IsDebug) _logger.Error("DEBUG/ERROR Error during discovery initialization", e); } } - - private async Task AddPersistedNodes(CancellationToken cancellationToken) - { - NetworkNode[] nodes = _discoveryStorage.GetPersistedNodes(); - foreach (NetworkNode networkNode in nodes) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } - - Node node; - try - { - node = new Node(networkNode.NodeId, networkNode.Host, networkNode.Port); - } - catch (Exception) - { - if (_logger.IsDebug) - _logger.Error( - $"ERROR/DEBUG peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - continue; - } - - try - { - // If when it receive Pong, it should automatically add to routing table if not full. - await _discv4Adapter.Ping(node, cancellationToken); - } - catch (OperationCanceledException) - { - continue; - } - catch (Exception) - { - if (_logger.IsDebug) - _logger.Error( - $"ERROR/DEBUG error when pinging persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - continue; - } - - if (_logger.IsTrace) - _logger.Trace($"Adding persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - } - - if (_logger.IsDebug) _logger.Debug($"Added persisted discovery nodes: {nodes.Length}"); - } - - [Todo(Improve.Allocations, "Remove ToArray here - address as a part of the network DB rewrite")] - private async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationToken) - { - if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); - PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_discoveryConfig.DiscoveryPersistenceInterval)); - - while (!cancellationToken.IsCancellationRequested - && await timer.WaitForNextTickAsync(cancellationToken)) - { - try - { - _discoveryStorage.StartBatch(); - - var nodes = _kademlia.IterateNodes().ToArray(); - _discoveryStorage.UpdateNodes(nodes.Select(x => new NetworkNode(x.Id, x.Host, - x.Port, _nodeStatsManager.GetNewPersistedReputation(x))).ToArray()); - - _discoveryStorage.Commit(); - } - catch (Exception ex) - { - _logger.Error($"Error during discovery commit: {ex}"); - } - } - } - public IAsyncEnumerable DiscoverNodes(CancellationToken token) { return _kademliaNodeSource.DiscoverNodes(token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs new file mode 100644 index 000000000000..91756d2ffb90 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Config; +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Stats; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery +{ + /// + /// Manages persistence operations for the discovery process, including loading nodes from storage + /// and periodic saving of discovered nodes. + /// + public class DiscoveryPersistenceManager + { + private readonly INetworkStorage _discoveryStorage; + private readonly INodeStatsManager _nodeStatsManager; + private readonly IKademliaDiscv4Adapter _discv4Adapter; + private readonly IKademlia _kademlia; + private readonly ILogger _logger; + private readonly int _persistenceInterval; + + /// + /// Initializes a new instance of the class. + /// + /// The network storage for persisting discovery nodes. + /// Manager for node statistics. + /// Adapter for Discv4 protocol communication. + /// Configuration for the discovery process. + /// Log manager for logging events. + /// Thrown if any required parameter is null. + public DiscoveryPersistenceManager( + INetworkStorage discoveryStorage, + INodeStatsManager nodeStatsManager, + IKademliaDiscv4Adapter discv4Adapter, + IKademlia kademlia, + IDiscoveryConfig discoveryConfig, + ILogManager logManager) + { + _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); + _nodeStatsManager = nodeStatsManager ?? throw new ArgumentNullException(nameof(nodeStatsManager)); + _discv4Adapter = discv4Adapter ?? throw new ArgumentNullException(nameof(discv4Adapter)); + _kademlia = kademlia; + _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); + _persistenceInterval = discoveryConfig?.DiscoveryPersistenceInterval ?? throw new ArgumentNullException(nameof(discoveryConfig)); + } + + /// + /// Loads persisted nodes from storage and pings them to verify their availability. + /// + /// Cancellation token to stop the operation. + /// A task representing the asynchronous operation. + public async Task AddPersistedNodes(CancellationToken cancellationToken) + { + NetworkNode[] nodes = _discoveryStorage.GetPersistedNodes(); + foreach (NetworkNode networkNode in nodes) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + Node node; + try + { + node = new Node(networkNode.NodeId, networkNode.Host, networkNode.Port); + } + catch (Exception) + { + if (_logger.IsDebug) + _logger.Error( + $"ERROR/DEBUG peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); + continue; + } + + try + { + // If when it receive Pong, it should automatically add to routing table if not full. + await _discv4Adapter.Ping(node, cancellationToken); + } + catch (OperationCanceledException) + { + continue; + } + catch (Exception) + { + if (_logger.IsDebug) + _logger.Error( + $"ERROR/DEBUG error when pinging persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); + continue; + } + + if (_logger.IsTrace) + _logger.Trace($"Adding persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); + } + + if (_logger.IsDebug) _logger.Debug($"Added persisted discovery nodes: {nodes.Length}"); + } + + /// + /// Periodically commits discovered nodes to persistent storage. + /// + /// The Kademlia instance containing nodes to persist. + /// Cancellation token to stop the operation. + /// A task representing the asynchronous operation. + public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationToken) + { + if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); + PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_persistenceInterval)); + + while (!cancellationToken.IsCancellationRequested + && await timer.WaitForNextTickAsync(cancellationToken)) + { + try + { + _discoveryStorage.StartBatch(); + + var nodes = _kademlia.IterateNodes().ToArray(); + _discoveryStorage.UpdateNodes(nodes.Select(x => new NetworkNode(x.Id, x.Host, + x.Port, _nodeStatsManager.GetNewPersistedReputation(x))).ToArray()); + + _discoveryStorage.Commit(); + } + catch (Exception ex) + { + _logger.Error($"Error during discovery commit: {ex}"); + } + } + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index f6865f74793a..b3f2482e1fe2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -34,7 +34,9 @@ protected override void Load(ContainerBuilder builder) }) .AddSingleton() .AddSingleton() - .AddSingleton>(c => c.Resolve()); + .AddSingleton>(c => c.Resolve()) + .AddSingleton() + .AddSingleton(); } } diff --git a/src/Nethermind/Nethermind.Runner.Test/EthereumRunnerTests.cs b/src/Nethermind/Nethermind.Runner.Test/EthereumRunnerTests.cs index 3414bea2e782..71cfe5de784b 100644 --- a/src/Nethermind/Nethermind.Runner.Test/EthereumRunnerTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/EthereumRunnerTests.cs @@ -283,6 +283,8 @@ public async Task Smoke_CanResolveAllSteps((string file, ConfigProvider configPr typeof(IProtectedPrivateKey), typeof(PublicKey), typeof(IPrivateKeyGenerator), + typeof(INetworkStorage), + typeof(NetworkStorage), typeof(string), ]; From b50807a983f4696acb1639477657ec0ca21ff333 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 20:33:19 +0800 Subject: [PATCH 063/182] Extract node record provider --- .../CompositeDiscoveryApp.cs | 49 ++++--------------- .../DiscoveryApp.cs | 10 ++-- .../Discv5/DiscoveryV5App.cs | 10 ++-- .../INodeRecordProvider.cs | 11 +++++ .../NodeRecordProvider.cs | 39 +++++++++++++++ .../Nethermind.Network/IDiscoveryApp.cs | 1 - 6 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/INodeRecordProvider.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index facbf63dbdea..21d6503a7cff 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -16,9 +16,7 @@ using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Discovery.Serializers; using Nethermind.Network.Enr; using Nethermind.Stats; using Nethermind.Stats.Model; @@ -36,7 +34,7 @@ public class CompositeDiscoveryApp : IDiscoveryApp private readonly INetworkConfig _networkConfig; private readonly IDiscoveryConfig _discoveryConfig; private readonly IInitConfig _initConfig; - private readonly IEthereumEcdsa _ethereumEcdsa; + private readonly INodeRecordProvider _nodeRecordProvider; private readonly IMessageSerializationService _serializationService; private readonly ILogManager _logManager; private readonly ITimestamper _timestamper; @@ -54,7 +52,7 @@ public CompositeDiscoveryApp( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey? nodeKey, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IInitConfig initConfig, - IEthereumEcdsa? ethereumEcdsa, IMessageSerializationService? serializationService, + INodeRecordProvider nodeRecordProvider, IMessageSerializationService? serializationService, ILogManager? logManager, ITimestamper? timestamper, ICryptoRandom? cryptoRandom, INodeStatsManager? nodeStatsManager, IIPResolver? ipResolver, IChannelFactory? channelFactory = null ) @@ -63,7 +61,7 @@ public CompositeDiscoveryApp( _networkConfig = networkConfig; _discoveryConfig = discoveryConfig; _initConfig = initConfig; - _ethereumEcdsa = ethereumEcdsa ?? throw new ArgumentNullException(nameof(ethereumEcdsa)); + _nodeRecordProvider = nodeRecordProvider; _serializationService = serializationService ?? throw new ArgumentNullException(nameof(serializationService)); _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); @@ -73,23 +71,17 @@ public CompositeDiscoveryApp( _connections = new DiscoveryConnectionsPool(logManager.GetClassLogger(), _networkConfig, _discoveryConfig); _channelFactory = channelFactory; - Initialize(nodeKey.PublicKey); - } - - public void Initialize(PublicKey masterPublicKey) - { - var nodeKeyProvider = new SameKeyGenerator(_nodeKey.Unprotect()); List allNodeSources = new(); if ((_discoveryConfig.DiscoveryVersion & DiscoveryVersion.V4) != 0) { - InitDiscoveryV4(_discoveryConfig, nodeKeyProvider); + InitDiscoveryV4(_discoveryConfig); allNodeSources.Add(_v4!); } if ((_discoveryConfig.DiscoveryVersion & DiscoveryVersion.V5) != 0) { - InitDiscoveryV5(nodeKeyProvider); + InitDiscoveryV5(); allNodeSources.Add(_v5!); } @@ -138,9 +130,9 @@ public void AddNodeToDiscovery(Node node) _v5?.AddNodeToDiscovery(node); } - private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator privateKeyProvider) + private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig) { - NodeRecord selfNodeRecord = PrepareNodeRecord(privateKeyProvider); + NodeRecord selfNodeRecord = _nodeRecordProvider.Current; NodeDistanceCalculator nodeDistanceCalculator = new(discoveryConfig); @@ -182,6 +174,7 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator _logManager); _v4 = new DiscoveryApp( + _nodeKey, nodesLocator, discoveryManager, nodeTable, @@ -192,38 +185,16 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig, SameKeyGenerator discoveryConfig, _timestamper, _logManager); - - _v4.Initialize(_nodeKey.PublicKey); } - private void InitDiscoveryV5(SameKeyGenerator privateKeyProvider) + private void InitDiscoveryV5() { SimpleFilePublicKeyDb discv5DiscoveryDb = new( "EnrDiscoveryDB", DiscoveryNodesDbPath.GetApplicationResourcePath(_initConfig.BaseDbPath), _logManager); - _v5 = new DiscoveryV5App(privateKeyProvider, _ipResolver, _networkConfig, _discoveryConfig, discv5DiscoveryDb, _logManager); - _v5.Initialize(_nodeKey.PublicKey); - } - - private NodeRecord PrepareNodeRecord(SameKeyGenerator privateKeyProvider) - { - NodeRecord selfNodeRecord = new(); - selfNodeRecord.SetEntry(IdEntry.Instance); - selfNodeRecord.SetEntry(new IpEntry(_ipResolver.ExternalIp)); - selfNodeRecord.SetEntry(new TcpEntry(_networkConfig.P2PPort)); - selfNodeRecord.SetEntry(new UdpEntry(_networkConfig.DiscoveryPort)); - selfNodeRecord.SetEntry(new Secp256K1Entry(_nodeKey.CompressedPublicKey)); - selfNodeRecord.EnrSequence = 1; - NodeRecordSigner enrSigner = new(_ethereumEcdsa, privateKeyProvider.Generate()); - enrSigner.Sign(selfNodeRecord); - if (!enrSigner.Verify(selfNodeRecord)) - { - throw new NetworkingException("Self ENR initialization failed", NetworkExceptionType.Discovery); - } - - return selfNodeRecord; + _v5 = new DiscoveryV5App(_nodeKey, _ipResolver, _networkConfig, _discoveryConfig, discv5DiscoveryDb, _logManager); } public IAsyncEnumerable DiscoverNodes(CancellationToken cancellationToken) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 9734d3364288..8933aee15649 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -4,6 +4,7 @@ using System.Net.NetworkInformation; using System.Runtime.CompilerServices; using System.Threading.Channels; +using Autofac.Features.AttributeFilters; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; using Nethermind.Config; @@ -38,7 +39,9 @@ public class DiscoveryApp : IDiscoveryApp private NettyDiscoveryHandler? _discoveryHandler; private Task? _storageCommitTask; - public DiscoveryApp(INodesLocator nodesLocator, + public DiscoveryApp( + [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + INodesLocator nodesLocator, IDiscoveryManager? discoveryManager, INodeTable? nodeTable, IMessageSerializationService? msgSerializationService, @@ -62,12 +65,9 @@ public DiscoveryApp(INodesLocator nodesLocator, _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); _discoveryStorage.StartBatch(); - } - public void Initialize(PublicKey masterPublicKey) - { _discoveryManager.NodeDiscovered += OnNodeDiscovered; - _nodeTable.Initialize(masterPublicKey); + _nodeTable.Initialize(nodeKey.PublicKey); if (_nodeTable.MasterNode is null) { throw new NetworkingException( diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 2ed878eba3cb..1c050c7c3d7e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -5,6 +5,7 @@ using System.Net; using System.Runtime.CompilerServices; using System.Threading.Channels; +using Autofac.Features.AttributeFilters; using DotNetty.Transport.Channels; using Lantern.Discv5.Enr; using Lantern.Discv5.Enr.Entries; @@ -38,7 +39,7 @@ public class DiscoveryV5App : IDiscoveryApp private readonly IServiceProvider _serviceProvider; private readonly SessionOptions _sessionOptions; - public DiscoveryV5App(SameKeyGenerator privateKeyProvider, IIPResolver? ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IDb discoveryDb, ILogManager logManager) + public DiscoveryV5App([KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, IIPResolver? ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IDb discoveryDb, ILogManager logManager) { ArgumentNullException.ThrowIfNull(ipResolver); @@ -47,11 +48,12 @@ public DiscoveryV5App(SameKeyGenerator privateKeyProvider, IIPResolver? ipResolv IdentityVerifierV4 identityVerifier = new(); + PrivateKey privateKey = nodeKey.Unprotect(); _sessionOptions = new() { - Signer = new IdentitySignerV4(privateKeyProvider.Generate().KeyBytes), + Signer = new IdentitySignerV4(privateKey.KeyBytes), Verifier = identityVerifier, - SessionKeys = new SessionKeys(privateKeyProvider.Generate().KeyBytes), + SessionKeys = new SessionKeys(privateKey.KeyBytes), }; string[] bootstrapNodes = [.. (discoveryConfig.Bootnodes ?? "").Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Distinct()]; @@ -190,8 +192,6 @@ private bool TryGetNodeFromEnr(IEnr enr, [NotNullWhen(true)] out Node? node) public event EventHandler? NodeRemoved; - public void Initialize(PublicKey masterPublicKey) { } - public void InitializeChannel(IChannel channel) { var handler = _serviceProvider.GetRequiredService(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/INodeRecordProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/INodeRecordProvider.cs new file mode 100644 index 000000000000..f11bb084a757 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/INodeRecordProvider.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Enr; + +namespace Nethermind.Network.Discovery; + +public interface INodeRecordProvider +{ + public NodeRecord Current { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs new file mode 100644 index 000000000000..f5e15dac1396 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac.Features.AttributeFilters; +using Nethermind.Crypto;using Nethermind.Network.Config; +using Nethermind.Network.Enr; + +namespace Nethermind.Network.Discovery; + +public class NodeRecordProvider( + [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + IPResolver ipResolver, + IEthereumEcdsa ethereumEcdsa, + INetworkConfig networkConfig +): INodeRecordProvider { + + NodeRecord? _nodeRecord = null; + public NodeRecord Current => _nodeRecord ??= PrepareNodeRecord(); + + private NodeRecord PrepareNodeRecord() + { + // TODO: Add forkid + NodeRecord selfNodeRecord = new(); + selfNodeRecord.SetEntry(IdEntry.Instance); + selfNodeRecord.SetEntry(new IpEntry(ipResolver.ExternalIp)); + selfNodeRecord.SetEntry(new TcpEntry(networkConfig.P2PPort)); + selfNodeRecord.SetEntry(new UdpEntry(networkConfig.DiscoveryPort)); + selfNodeRecord.SetEntry(new Secp256K1Entry(nodeKey.CompressedPublicKey)); + selfNodeRecord.EnrSequence = 1; + NodeRecordSigner enrSigner = new(ethereumEcdsa, nodeKey.Unprotect()); + enrSigner.Sign(selfNodeRecord); + if (!enrSigner.Verify(selfNodeRecord)) + { + throw new NetworkingException("Self ENR initialization failed", NetworkExceptionType.Discovery); + } + + return selfNodeRecord; + } +} diff --git a/src/Nethermind/Nethermind.Network/IDiscoveryApp.cs b/src/Nethermind/Nethermind.Network/IDiscoveryApp.cs index 3c130f30595b..7c65f2b7a6c5 100644 --- a/src/Nethermind/Nethermind.Network/IDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network/IDiscoveryApp.cs @@ -10,7 +10,6 @@ namespace Nethermind.Network { public interface IDiscoveryApp : INodeSource { - void Initialize(PublicKey masterPublicKey); void InitializeChannel(IChannel channel); Task StartAsync(); Task StopAsync(); From 31ab6f7b3bd5d10c00b7c86fd8e34e4407d7a7e5 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 20:40:55 +0800 Subject: [PATCH 064/182] DiscV5 to DI --- src/Nethermind/Nethermind.Db/DbNames.cs | 1 + .../Modules/DiscoveryModule.cs | 15 ++++++++++++++- .../CompositeDiscoveryApp.cs | 19 ++++--------------- .../Discv5/DiscoveryV5App.cs | 8 +++++++- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Nethermind/Nethermind.Db/DbNames.cs b/src/Nethermind/Nethermind.Db/DbNames.cs index 7b8459430f9d..782fff2be3d2 100644 --- a/src/Nethermind/Nethermind.Db/DbNames.cs +++ b/src/Nethermind/Nethermind.Db/DbNames.cs @@ -17,5 +17,6 @@ public static class DbNames public const string Bloom = "bloom"; public const string Metadata = "metadata"; public const string BlobTransactions = "blobTransactions"; + public const string DiscV5Db = "discV5"; } } diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 71b9b1050d3a..ac0a843886cb 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -12,6 +12,7 @@ using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Discovery; +using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Discovery.Serializers; using Nethermind.Network.Dns; @@ -23,6 +24,8 @@ namespace Nethermind.Init.Modules; public class DiscoveryModule(IInitConfig initConfig, INetworkConfig networkConfig) : Module { + private const string DiscoveryNodesDbPath = "discoveryNodes"; + protected override void Load(ContainerBuilder builder) { builder @@ -118,7 +121,17 @@ protected override void Load(ContainerBuilder builder) if (!initConfig.DiscoveryEnabled) builder.AddSingleton(); else - builder.AddSingleton(); + { + builder + .AddSingleton() + .AddKeyedSingleton(DbNames.DiscV5Db, (ctx) => new SimpleFilePublicKeyDb ( + "EnrDiscoveryDB", + DiscoveryNodesDbPath.GetApplicationResourcePath(initConfig.BaseDbPath), + ctx.Resolve())) + .AddSingleton() + ; + } + if (!networkConfig.OnlyStaticPeers) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index 21d6503a7cff..e13b2d61679f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -9,7 +9,6 @@ using DotNetty.Transport.Channels.Sockets; using Nethermind.Api; using Nethermind.Core; -using Nethermind.Core.Crypto; using Nethermind.Crypto; using Nethermind.Db; using Nethermind.Logging; @@ -40,7 +39,6 @@ public class CompositeDiscoveryApp : IDiscoveryApp private readonly ITimestamper _timestamper; private readonly ICryptoRandom? _cryptoRandom; private readonly INodeStatsManager _nodeStatsManager; - private readonly IIPResolver _ipResolver; private readonly IConnectionsPool _connections; private readonly IChannelFactory? _channelFactory; @@ -54,7 +52,9 @@ public CompositeDiscoveryApp( INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IInitConfig initConfig, INodeRecordProvider nodeRecordProvider, IMessageSerializationService? serializationService, ILogManager? logManager, ITimestamper? timestamper, ICryptoRandom? cryptoRandom, - INodeStatsManager? nodeStatsManager, IIPResolver? ipResolver, IChannelFactory? channelFactory = null + INodeStatsManager? nodeStatsManager, + Func discoveryV5Factory, + IChannelFactory? channelFactory = null ) { _nodeKey = nodeKey ?? throw new ArgumentNullException(nameof(nodeKey)); @@ -67,7 +67,6 @@ public CompositeDiscoveryApp( _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); _cryptoRandom = cryptoRandom; _nodeStatsManager = nodeStatsManager ?? throw new ArgumentNullException(nameof(nodeStatsManager)); - _ipResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); _connections = new DiscoveryConnectionsPool(logManager.GetClassLogger(), _networkConfig, _discoveryConfig); _channelFactory = channelFactory; @@ -81,7 +80,7 @@ public CompositeDiscoveryApp( if ((_discoveryConfig.DiscoveryVersion & DiscoveryVersion.V5) != 0) { - InitDiscoveryV5(); + _v5 = discoveryV5Factory(); allNodeSources.Add(_v5!); } @@ -187,16 +186,6 @@ private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig) _logManager); } - private void InitDiscoveryV5() - { - SimpleFilePublicKeyDb discv5DiscoveryDb = new( - "EnrDiscoveryDB", - DiscoveryNodesDbPath.GetApplicationResourcePath(_initConfig.BaseDbPath), - _logManager); - - _v5 = new DiscoveryV5App(_nodeKey, _ipResolver, _networkConfig, _discoveryConfig, discv5DiscoveryDb, _logManager); - } - public IAsyncEnumerable DiscoverNodes(CancellationToken cancellationToken) { return _compositeNodeSource.DiscoverNodes(cancellationToken); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 1c050c7c3d7e..131e75b68e20 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -39,7 +39,13 @@ public class DiscoveryV5App : IDiscoveryApp private readonly IServiceProvider _serviceProvider; private readonly SessionOptions _sessionOptions; - public DiscoveryV5App([KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, IIPResolver? ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IDb discoveryDb, ILogManager logManager) + public DiscoveryV5App( + [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + IIPResolver? ipResolver, + INetworkConfig networkConfig, + IDiscoveryConfig discoveryConfig, + [KeyFilter(DbNames.DiscV5Db)] IDb discoveryDb, + ILogManager logManager) { ArgumentNullException.ThrowIfNull(ipResolver); From adde8e051090f3b3246052054ca99734915d4f55 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 21:02:50 +0800 Subject: [PATCH 065/182] DiscV4 to DI --- .../Modules/DiscoveryModule.cs | 26 +++++ .../CompositeDiscoveryApp.cs | 106 ++---------------- .../DiscoveryApp.cs | 2 +- .../DiscoveryManager.cs | 3 +- .../Lifecycle/NodeLifecycleManagerFactory.cs | 11 ++ .../NodeRecordProvider.cs | 2 +- .../Nethermind.Network/INetworkStorage.cs | 1 + 7 files changed, 51 insertions(+), 100 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index ac0a843886cb..68f20fea810a 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -13,7 +13,9 @@ using Nethermind.Network.Config; using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Lifecycle; using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.RoutingTable; using Nethermind.Network.Discovery.Serializers; using Nethermind.Network.Dns; using Nethermind.Network.Enr; @@ -124,11 +126,35 @@ protected override void Load(ContainerBuilder builder) { builder .AddSingleton() + .AddSingleton() + .AddKeyedSingleton(DbNames.DiscV5Db, (ctx) => new SimpleFilePublicKeyDb ( "EnrDiscoveryDB", DiscoveryNodesDbPath.GetApplicationResourcePath(initConfig.BaseDbPath), ctx.Resolve())) .AddSingleton() + + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddKeyedSingleton(INetworkStorage.DiscV4, (ctx) => + { + ILogManager logManager = ctx.Resolve(); + SimpleFilePublicKeyDb discoveryDb = new( + "DiscoveryDB", + DiscoveryNodesDbPath.GetApplicationResourcePath(initConfig.BaseDbPath), + logManager); + + NetworkStorage discoveryStorage = new( + discoveryDb, + logManager); + return discoveryStorage; + }) + .AddSingleton() + .AddSingleton() + .AddSingleton() + ; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index e13b2d61679f..c3c5c6392816 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -3,21 +3,12 @@ using System.Net.Sockets; using System.Runtime.InteropServices; -using Autofac.Features.AttributeFilters; using DotNetty.Transport.Bootstrapping; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; -using Nethermind.Api; -using Nethermind.Core; -using Nethermind.Crypto; -using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; -using Nethermind.Network.Discovery.Lifecycle; -using Nethermind.Network.Discovery.RoutingTable; -using Nethermind.Network.Enr; -using Nethermind.Stats; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery; @@ -27,18 +18,7 @@ namespace Nethermind.Network.Discovery; /// public class CompositeDiscoveryApp : IDiscoveryApp { - private const string DiscoveryNodesDbPath = "discoveryNodes"; - - private readonly IProtectedPrivateKey _nodeKey; private readonly INetworkConfig _networkConfig; - private readonly IDiscoveryConfig _discoveryConfig; - private readonly IInitConfig _initConfig; - private readonly INodeRecordProvider _nodeRecordProvider; - private readonly IMessageSerializationService _serializationService; - private readonly ILogManager _logManager; - private readonly ITimestamper _timestamper; - private readonly ICryptoRandom? _cryptoRandom; - private readonly INodeStatsManager _nodeStatsManager; private readonly IConnectionsPool _connections; private readonly IChannelFactory? _channelFactory; @@ -47,38 +27,27 @@ public class CompositeDiscoveryApp : IDiscoveryApp private INodeSource _compositeNodeSource = null!; public CompositeDiscoveryApp( - [KeyFilter(IProtectedPrivateKey.NodeKey)] - IProtectedPrivateKey? nodeKey, - INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, IInitConfig initConfig, - INodeRecordProvider nodeRecordProvider, IMessageSerializationService? serializationService, - ILogManager? logManager, ITimestamper? timestamper, ICryptoRandom? cryptoRandom, - INodeStatsManager? nodeStatsManager, - Func discoveryV5Factory, + INetworkConfig networkConfig, + IDiscoveryConfig discoveryConfig, + ILogManager logManager, + Func discoveryV5Factory, // These two are factory because they are optional. + Func discoveryV4Factory, IChannelFactory? channelFactory = null ) { - _nodeKey = nodeKey ?? throw new ArgumentNullException(nameof(nodeKey)); _networkConfig = networkConfig; - _discoveryConfig = discoveryConfig; - _initConfig = initConfig; - _nodeRecordProvider = nodeRecordProvider; - _serializationService = serializationService ?? throw new ArgumentNullException(nameof(serializationService)); - _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); - _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - _cryptoRandom = cryptoRandom; - _nodeStatsManager = nodeStatsManager ?? throw new ArgumentNullException(nameof(nodeStatsManager)); - _connections = new DiscoveryConnectionsPool(logManager.GetClassLogger(), _networkConfig, _discoveryConfig); + _connections = new DiscoveryConnectionsPool(logManager.GetClassLogger(), _networkConfig, discoveryConfig); _channelFactory = channelFactory; List allNodeSources = new(); - if ((_discoveryConfig.DiscoveryVersion & DiscoveryVersion.V4) != 0) + if ((discoveryConfig.DiscoveryVersion & DiscoveryVersion.V4) != 0) { - InitDiscoveryV4(_discoveryConfig); + _v4 = discoveryV4Factory(); allNodeSources.Add(_v4!); } - if ((_discoveryConfig.DiscoveryVersion & DiscoveryVersion.V5) != 0) + if ((discoveryConfig.DiscoveryVersion & DiscoveryVersion.V5) != 0) { _v5 = discoveryV5Factory(); allNodeSources.Add(_v5!); @@ -129,63 +98,6 @@ public void AddNodeToDiscovery(Node node) _v5?.AddNodeToDiscovery(node); } - private void InitDiscoveryV4(IDiscoveryConfig discoveryConfig) - { - NodeRecord selfNodeRecord = _nodeRecordProvider.Current; - - NodeDistanceCalculator nodeDistanceCalculator = new(discoveryConfig); - - NodeTable nodeTable = new(nodeDistanceCalculator, discoveryConfig, _networkConfig, _logManager); - EvictionManager evictionManager = new(nodeTable, _logManager); - - NodeLifecycleManagerFactory nodeLifeCycleFactory = new( - nodeTable, - evictionManager, - _nodeStatsManager, - selfNodeRecord, - discoveryConfig, - _timestamper, - _logManager); - - // ToDo: DiscoveryDB is registered outside dbProvider - bad - SimpleFilePublicKeyDb discoveryDb = new( - "DiscoveryDB", - DiscoveryNodesDbPath.GetApplicationResourcePath(_initConfig.BaseDbPath), - _logManager); - - NetworkStorage discoveryStorage = new( - discoveryDb, - _logManager); - - DiscoveryManager discoveryManager = new( - nodeLifeCycleFactory, - nodeTable, - discoveryStorage, - discoveryConfig, - _networkConfig, - _logManager - ); - - NodesLocator nodesLocator = new( - nodeTable, - discoveryManager, - discoveryConfig, - _logManager); - - _v4 = new DiscoveryApp( - _nodeKey, - nodesLocator, - discoveryManager, - nodeTable, - _serializationService, - _cryptoRandom, - discoveryStorage, - _networkConfig, - discoveryConfig, - _timestamper, - _logManager); - } - public IAsyncEnumerable DiscoverNodes(CancellationToken cancellationToken) { return _compositeNodeSource.DiscoverNodes(cancellationToken); diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 8933aee15649..0c31b9d789d0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -46,7 +46,7 @@ public DiscoveryApp( INodeTable? nodeTable, IMessageSerializationService? msgSerializationService, ICryptoRandom? cryptoRandom, - INetworkStorage? discoveryStorage, + [KeyFilter(INetworkStorage.DiscV4)] INetworkStorage? discoveryStorage, INetworkConfig? networkConfig, IDiscoveryConfig? discoveryConfig, ITimestamper? timestamper, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs index 48f0edb4b711..7d3c5953dacd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Autofac.Features.AttributeFilters; using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; @@ -36,7 +37,7 @@ public class DiscoveryManager : IDiscoveryManager public DiscoveryManager( INodeLifecycleManagerFactory? nodeLifecycleManagerFactory, INodeTable? nodeTable, - INetworkStorage? discoveryStorage, + [KeyFilter(INetworkStorage.DiscV4)] INetworkStorage? discoveryStorage, IDiscoveryConfig? discoveryConfig, INetworkConfig? networkConfig, ILogManager? logManager) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs index b3a107b3d53b..6de6e8433086 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManagerFactory.cs @@ -20,6 +20,17 @@ public class NodeLifecycleManagerFactory : INodeLifecycleManagerFactory private readonly INodeStatsManager _nodeStatsManager; private readonly NodeRecord _selfNodeRecord; + public NodeLifecycleManagerFactory(INodeTable nodeTable, + IEvictionManager evictionManager, + INodeStatsManager nodeStatsManager, + INodeRecordProvider nodeRecordProvider, + IDiscoveryConfig discoveryConfig, + ITimestamper timestamper, + ILogManager? logManager) + : this(nodeTable, evictionManager, nodeStatsManager, nodeRecordProvider.Current, discoveryConfig, timestamper, logManager) + { + } + public NodeLifecycleManagerFactory(INodeTable nodeTable, IEvictionManager evictionManager, INodeStatsManager nodeStatsManager, diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs index f5e15dac1396..40ef9234af15 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery; public class NodeRecordProvider( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, - IPResolver ipResolver, + IIPResolver ipResolver, IEthereumEcdsa ethereumEcdsa, INetworkConfig networkConfig ): INodeRecordProvider { diff --git a/src/Nethermind/Nethermind.Network/INetworkStorage.cs b/src/Nethermind/Nethermind.Network/INetworkStorage.cs index de58eb3737a4..11ec641be096 100644 --- a/src/Nethermind/Nethermind.Network/INetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/INetworkStorage.cs @@ -10,6 +10,7 @@ namespace Nethermind.Network public interface INetworkStorage { public const string PeerDb = "PeerDb"; + public const string DiscV4 = "DiscV4"; NetworkNode[] GetPersistedNodes(); int PersistedNodesCount { get; } From 5253f1c0129c2eb6e0c4e406a1a14fb8263c3b5d Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 14 May 2025 21:23:23 +0800 Subject: [PATCH 066/182] Consolidate the network storage initialization --- src/Nethermind/Nethermind.Db/DbNames.cs | 3 +- .../Modules/ContainerBuilderExtensions.cs | 38 +++++++++++++++++++ .../Modules/DiscoveryModule.cs | 33 +--------------- .../Steps/InitializeNetwork.cs | 5 +-- .../DiscoveryApp.cs | 3 +- .../DiscoveryManager.cs | 3 +- .../Discv5/DiscoveryV5App.cs | 2 +- .../Nethermind.Network/INetworkStorage.cs | 3 -- .../Nethermind.Network/NodesLoader.cs | 3 +- src/Nethermind/Nethermind.Network/PeerPool.cs | 3 +- .../Nethermind.Network/ProtocolsManager.cs | 3 +- 11 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs diff --git a/src/Nethermind/Nethermind.Db/DbNames.cs b/src/Nethermind/Nethermind.Db/DbNames.cs index 782fff2be3d2..e0f96bbc6468 100644 --- a/src/Nethermind/Nethermind.Db/DbNames.cs +++ b/src/Nethermind/Nethermind.Db/DbNames.cs @@ -17,6 +17,7 @@ public static class DbNames public const string Bloom = "bloom"; public const string Metadata = "metadata"; public const string BlobTransactions = "blobTransactions"; - public const string DiscV5Db = "discV5"; + public const string DiscoveryNodes = "discoveryNodes"; + public const string PeersDb = "peers"; } } diff --git a/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs b/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs new file mode 100644 index 000000000000..f436524ae25e --- /dev/null +++ b/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac; +using Nethermind.Api; +using Nethermind.Core; +using Nethermind.Db; +using Nethermind.Logging; +using Nethermind.Network; + +namespace Nethermind.Init.Modules; + +public static class ContainerBuilderExtensions +{ + // Register some set of component that is meant to expose `INetworkStorage`. + // These are stored outside of rocksdb using `SimpleFilePublicKeyDb`. + public static ContainerBuilder AddNetworkStorage(this ContainerBuilder builder, string dbName) + { + return builder + .AddKeyedSingleton(dbName, ctx => + { + ILogManager logManager = ctx.Resolve(); + IInitConfig initConfig = ctx.Resolve(); + + return initConfig.DiagnosticMode == DiagnosticMode.MemDb + ? new MemDb(dbName) + : new SimpleFilePublicKeyDb(dbName, dbName.GetApplicationResourcePath(initConfig.BaseDbPath), + logManager); + }) + .AddKeyedSingleton(dbName, ctx => ctx.ResolveKeyed(dbName)) + .AddKeyedSingleton(dbName, ctx => + { + ILogManager logManager = ctx.Resolve(); + IFullDb db = ctx.ResolveKeyed(dbName); + return new NetworkStorage(db, logManager); + }); + } +} diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 68f20fea810a..b338b8bdfe12 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -26,8 +26,6 @@ namespace Nethermind.Init.Modules; public class DiscoveryModule(IInitConfig initConfig, INetworkConfig networkConfig) : Module { - private const string DiscoveryNodesDbPath = "discoveryNodes"; - protected override void Load(ContainerBuilder builder) { builder @@ -55,18 +53,7 @@ protected override void Load(ContainerBuilder builder) .Bind() // Used by NodesLoader, and ProtocolsManager which add entry on sync peer connected - .AddKeyedSingleton(INetworkStorage.PeerDb, (ctx) => - { - ILogManager logManager = ctx.Resolve(); - - // ToDo: PeersDB is registered outside dbProvider - string dbName = INetworkStorage.PeerDb; - IFullDb peersDb = initConfig.DiagnosticMode == DiagnosticMode.MemDb - ? new MemDb(dbName) - : new SimpleFilePublicKeyDb(dbName, InitializeNetwork.PeersDbPath.GetApplicationResourcePath(initConfig.BaseDbPath), - logManager); - return new NetworkStorage(peersDb, logManager); - }) + .AddNetworkStorage(DbNames.PeersDb) .Bind() .AddComposite() @@ -128,29 +115,13 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddSingleton() - .AddKeyedSingleton(DbNames.DiscV5Db, (ctx) => new SimpleFilePublicKeyDb ( - "EnrDiscoveryDB", - DiscoveryNodesDbPath.GetApplicationResourcePath(initConfig.BaseDbPath), - ctx.Resolve())) + .AddNetworkStorage(DbNames.DiscoveryNodes) .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddKeyedSingleton(INetworkStorage.DiscV4, (ctx) => - { - ILogManager logManager = ctx.Resolve(); - SimpleFilePublicKeyDb discoveryDb = new( - "DiscoveryDB", - DiscoveryNodesDbPath.GetApplicationResourcePath(initConfig.BaseDbPath), - logManager); - - NetworkStorage discoveryStorage = new( - discoveryDb, - logManager); - return discoveryStorage; - }) .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index 38ee48e4d361..11b4f9f21147 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -12,6 +12,7 @@ using Nethermind.Blockchain.Synchronization; using Nethermind.Core; using Nethermind.Crypto; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network; using Nethermind.Network.Config; @@ -57,8 +58,6 @@ public static long Estimate(uint arenaCount, int arenaOrder) typeof(InitializeBlockchain))] public class InitializeNetwork : IStep { - public const string PeersDbPath = "peers"; - private readonly IApiWithNetwork _api; private readonly INodeStatsManager _nodeStatsManager; private readonly ISynchronizer _synchronizer; @@ -83,7 +82,7 @@ public InitializeNetwork( NodeSourceToDiscV4Feeder enrDiscoveryAppFeeder, IDiscoveryApp discoveryApp, Lazy peerPool, // Require IRlpxPeer to be created first, hence, lazy. - [KeyFilter(INetworkStorage.PeerDb)] INetworkStorage peerStorage, + [KeyFilter(DbNames.PeersDb)] INetworkStorage peerStorage, INetworkConfig networkConfig, ISyncConfig syncConfig, IInitConfig initConfig, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 0c31b9d789d0..8a3232b6f187 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -13,6 +13,7 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Crypto; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Lifecycle; @@ -46,7 +47,7 @@ public DiscoveryApp( INodeTable? nodeTable, IMessageSerializationService? msgSerializationService, ICryptoRandom? cryptoRandom, - [KeyFilter(INetworkStorage.DiscV4)] INetworkStorage? discoveryStorage, + [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage? discoveryStorage, INetworkConfig? networkConfig, IDiscoveryConfig? discoveryConfig, ITimestamper? timestamper, diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs index 7d3c5953dacd..57af87e56cfe 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryManager.cs @@ -8,6 +8,7 @@ using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Lifecycle; @@ -37,7 +38,7 @@ public class DiscoveryManager : IDiscoveryManager public DiscoveryManager( INodeLifecycleManagerFactory? nodeLifecycleManagerFactory, INodeTable? nodeTable, - [KeyFilter(INetworkStorage.DiscV4)] INetworkStorage? discoveryStorage, + [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage? discoveryStorage, IDiscoveryConfig? discoveryConfig, INetworkConfig? networkConfig, ILogManager? logManager) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 131e75b68e20..2a1248a976b3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -44,7 +44,7 @@ public DiscoveryV5App( IIPResolver? ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, - [KeyFilter(DbNames.DiscV5Db)] IDb discoveryDb, + [KeyFilter(DbNames.DiscoveryNodes)] IDb discoveryDb, ILogManager logManager) { ArgumentNullException.ThrowIfNull(ipResolver); diff --git a/src/Nethermind/Nethermind.Network/INetworkStorage.cs b/src/Nethermind/Nethermind.Network/INetworkStorage.cs index 11ec641be096..0aa0fbdc6015 100644 --- a/src/Nethermind/Nethermind.Network/INetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/INetworkStorage.cs @@ -9,9 +9,6 @@ namespace Nethermind.Network { public interface INetworkStorage { - public const string PeerDb = "PeerDb"; - public const string DiscV4 = "DiscV4"; - NetworkNode[] GetPersistedNodes(); int PersistedNodesCount { get; } diff --git a/src/Nethermind/Nethermind.Network/NodesLoader.cs b/src/Nethermind/Nethermind.Network/NodesLoader.cs index c0a3a10e2a51..76ba673e3bab 100644 --- a/src/Nethermind/Nethermind.Network/NodesLoader.cs +++ b/src/Nethermind/Nethermind.Network/NodesLoader.cs @@ -7,6 +7,7 @@ using System.Threading; using Autofac.Features.AttributeFilters; using Nethermind.Config; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Rlpx; @@ -29,7 +30,7 @@ public class NodesLoader : INodeSource public NodesLoader( INetworkConfig networkConfig, INodeStatsManager stats, - [KeyFilter(INetworkStorage.PeerDb)] INetworkStorage peerStorage, + [KeyFilter(DbNames.PeersDb)] INetworkStorage peerStorage, IRlpxHost rlpxHost, ILogManager logManager) { diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index 439598d4957f..b68b7b1c4d1c 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -11,6 +11,7 @@ using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.P2P; @@ -50,7 +51,7 @@ public class PeerPool : IPeerPool public PeerPool( INodeSource nodeSource, INodeStatsManager nodeStatsManager, - [KeyFilter(INetworkStorage.PeerDb)] INetworkStorage peerStorage, + [KeyFilter(DbNames.PeersDb)] INetworkStorage peerStorage, INetworkConfig networkConfig, ILogManager logManager, ITrustedNodesManager trustedNodesManager) diff --git a/src/Nethermind/Nethermind.Network/ProtocolsManager.cs b/src/Nethermind/Nethermind.Network/ProtocolsManager.cs index a2f7cc4ce990..d99f33a237d6 100644 --- a/src/Nethermind/Nethermind.Network/ProtocolsManager.cs +++ b/src/Nethermind/Nethermind.Network/ProtocolsManager.cs @@ -11,6 +11,7 @@ using Nethermind.Config; using Nethermind.Consensus; using Nethermind.Consensus.Scheduler; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Contract.P2P; @@ -85,7 +86,7 @@ public ProtocolsManager( IRlpxHost rlpxHost, INodeStatsManager nodeStatsManager, IProtocolValidator protocolValidator, - [KeyFilter(INetworkStorage.PeerDb)] INetworkStorage peerStorage, + [KeyFilter(DbNames.PeersDb)] INetworkStorage peerStorage, ForkInfo forkInfo, IGossipPolicy gossipPolicy, INetworkConfig networkConfig, From 1adb534fd40496da53fc94e6559138c635eeb245 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 15 May 2025 09:54:20 +0800 Subject: [PATCH 067/182] Allow more margin --- .../EngineModuleTests.PayloadProduction.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.PayloadProduction.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.PayloadProduction.cs index 24cd1dc700a5..3cc3192ce3e1 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.PayloadProduction.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.PayloadProduction.cs @@ -174,11 +174,8 @@ public async Task getPayloadV1_waits_for_block_production(TimeSpan txDelay, Time await Task.Delay(PayloadPreparationService.GetPayloadWaitForNonEmptyBlockMillisecondsDelay); - ExecutionPayload getPayloadResult = (await rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId))).Data!; - - await Task.Delay(PayloadPreparationService.GetPayloadWaitForNonEmptyBlockMillisecondsDelay); - - Assert.That(getPayloadResult.Transactions, Has.Length.InRange(minCount, maxCount)); + Assert.That(() => rpc.engine_getPayloadV1(Bytes.FromHexString(payloadId)).Result.Data!.Transactions, + Has.Length.InRange(minCount, maxCount).After(2000, 1)); // Polling interval need to be short or it might miss it. } [Test] From f0e1b36bc8cb5217959f7a0f1d21a0a07655dbef Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 15 May 2025 10:01:06 +0800 Subject: [PATCH 068/182] Override discovery db in test also --- .../Modules/TestEnvironmentModule.cs | 3 +++ .../Modules/ContainerBuilderExtensions.cs | 14 ++++++++++---- .../Nethermind.Init/Modules/DiscoveryModule.cs | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs b/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs index ff6a8b03b548..588dc28c364b 100644 --- a/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs +++ b/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs @@ -38,6 +38,9 @@ protected override void Load(ContainerBuilder builder) builder .AddSingleton(new TestLogManager(LogLevel.Error)) // Limbologs actually have IsTrace set to true, so actually slow. .AddSingleton(TestMemDbProvider.Init()) + // These two dont use db provider + .AddKeyedSingleton(DbNames.PeersDb, (_) => new MemDb()) + .AddKeyedSingleton(DbNames.DiscoveryNodes, (_) => new MemDb()) .AddSingleton(new InMemoryDictionaryFileStoreFactory()) .AddSingleton(networkConfig => new LocalChannelFactory(networkGroup ?? nameof(TestEnvironmentModule), networkConfig)) diff --git a/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs b/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs index f436524ae25e..78835d530500 100644 --- a/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs +++ b/src/Nethermind/Nethermind.Init/Modules/ContainerBuilderExtensions.cs @@ -12,9 +12,15 @@ namespace Nethermind.Init.Modules; public static class ContainerBuilderExtensions { - // Register some set of component that is meant to expose `INetworkStorage`. - // These are stored outside of rocksdb using `SimpleFilePublicKeyDb`. - public static ContainerBuilder AddNetworkStorage(this ContainerBuilder builder, string dbName) + /// + /// Register some set of component that is meant to expose `INetworkStorage`. + /// These are stored outside of rocksdb using `SimpleFilePublicKeyDb`. + /// + /// The container builder + /// Service key + /// Path relative to BaseDbPath to store db + /// + public static ContainerBuilder AddNetworkStorage(this ContainerBuilder builder, string dbName, string storePath) { return builder .AddKeyedSingleton(dbName, ctx => @@ -24,7 +30,7 @@ public static ContainerBuilder AddNetworkStorage(this ContainerBuilder builder, return initConfig.DiagnosticMode == DiagnosticMode.MemDb ? new MemDb(dbName) - : new SimpleFilePublicKeyDb(dbName, dbName.GetApplicationResourcePath(initConfig.BaseDbPath), + : new SimpleFilePublicKeyDb(dbName, storePath.GetApplicationResourcePath(initConfig.BaseDbPath), logManager); }) .AddKeyedSingleton(dbName, ctx => ctx.ResolveKeyed(dbName)) diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index b338b8bdfe12..78dfb7dd5b9d 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -53,7 +53,7 @@ protected override void Load(ContainerBuilder builder) .Bind() // Used by NodesLoader, and ProtocolsManager which add entry on sync peer connected - .AddNetworkStorage(DbNames.PeersDb) + .AddNetworkStorage(DbNames.PeersDb, "peers") .Bind() .AddComposite() @@ -115,7 +115,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddSingleton() - .AddNetworkStorage(DbNames.DiscoveryNodes) + .AddNetworkStorage(DbNames.DiscoveryNodes, "discoveryNodes") .AddSingleton() .AddSingleton() From ec29d128f4fc684c8014872bd662dd359e1e3368 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 15 May 2025 10:22:48 +0800 Subject: [PATCH 069/182] Whitespace --- .../Nethermind.Network.Discovery/NodeRecordProvider.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs index 40ef9234af15..20163d18a888 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: LGPL-3.0-only using Autofac.Features.AttributeFilters; -using Nethermind.Crypto;using Nethermind.Network.Config; +using Nethermind.Crypto; +using Nethermind.Network.Config; using Nethermind.Network.Enr; namespace Nethermind.Network.Discovery; @@ -12,7 +13,8 @@ public class NodeRecordProvider( IIPResolver ipResolver, IEthereumEcdsa ethereumEcdsa, INetworkConfig networkConfig -): INodeRecordProvider { +) : INodeRecordProvider +{ NodeRecord? _nodeRecord = null; public NodeRecord Current => _nodeRecord ??= PrepareNodeRecord(); From 6a2022f97f334a46b1c576f06062a51fb4394dcb Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 15 May 2025 11:08:05 +0800 Subject: [PATCH 070/182] Fix build --- .../Modules/DiscoveryModule.cs | 9 --- .../DiscoveryPersistenceManagerTests.cs | 11 +++- .../DiscoveryApp.cs | 66 +++++++++++++------ .../DiscoveryPersistenceManager.cs | 20 +++--- .../Discv4/DiscV4KademliaModule.cs | 3 +- .../Discv4/KademliaDiscv4Adapter.cs | 7 +- 6 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 78dfb7dd5b9d..fe48c4a7f7ff 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -7,15 +7,12 @@ using Nethermind.Core; using Nethermind.Crypto; using Nethermind.Db; -using Nethermind.Init.Steps; using Nethermind.Logging; using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv5; -using Nethermind.Network.Discovery.Lifecycle; using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; using Nethermind.Network.Discovery.Serializers; using Nethermind.Network.Dns; using Nethermind.Network.Enr; @@ -118,12 +115,6 @@ protected override void Load(ContainerBuilder builder) .AddNetworkStorage(DbNames.DiscoveryNodes, "discoveryNodes") .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() .AddSingleton() ; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index 6d81dc783af0..1ff3b1462389 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -9,9 +9,9 @@ using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; -using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; @@ -46,10 +46,18 @@ public void Setup() _networkStorage, _nodeStatsManager, _discv4Adapter, + _kademlia, _discoveryConfig, _logManager); } + [TearDown] + public void Teardown() + { + _discv4Adapter?.DisposeAsync(); + } + + /* [Test] public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node() { @@ -195,5 +203,6 @@ public async Task RunDiscoveryPersistenceCommit_Should_Handle_Exceptions() // If we got here without other exceptions, the error was properly handled Assert.Pass(); } + */ } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 82c9d5611ab2..c79ad4cdb81b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -1,11 +1,14 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Autofac; +using Autofac.Features.AttributeFilters; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; @@ -15,52 +18,49 @@ namespace Nethermind.Network.Discovery; -public class DiscoveryApp : IDiscoveryApp +public class DiscoveryApp : IDiscoveryApp, IAsyncDisposable { - private readonly IDiscoveryConfig _discoveryConfig; private readonly ITimestamper _timestamper; private readonly ILogManager _logManager; private readonly ILogger _logger; private readonly IMessageSerializationService _messageSerializationService; private readonly INetworkConfig _networkConfig; - private DiscoveryPersistenceManager? _persistenceManager; - - private readonly List _bootNodes; - - private IKademliaDiscv4Adapter _discv4Adapter = null!; - private IKademlia _kademlia = null!; private NettyDiscoveryHandler? _discoveryHandler; - private IKademliaNodeSource _kademliaNodeSource = null!; + private IKademliaNodeSource _kademliaNodeSource; + private DiscoveryPersistenceManager _persistenceManager; + private IKademliaDiscv4Adapter _discv4Adapter; + private IKademlia _kademlia; + private readonly ILifetimeScope _discv4Services; + private Task? _runningTask; private readonly IProcessExitSource _processExitSouce; public DiscoveryApp( - IMessageSerializationService? msgSerializationService, - DiscoveryPersistenceManager persistenceManager, - INetworkConfig? networkConfig, - IDiscoveryConfig? discoveryConfig, - ITimestamper? timestamper, + ILifetimeScope rootScope, + [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + IMessageSerializationService msgSerializationService, + INetworkConfig networkConfig, + IDiscoveryConfig discoveryConfig, + ITimestamper timestamper, IProcessExitSource processExitSource, - ILogManager? logManager) + ILogManager logManager) { _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); _logger = _logManager.GetClassLogger(); - _discoveryConfig = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); + IDiscoveryConfig discoveryConfig1 = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); _messageSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); - _persistenceManager = persistenceManager; _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); _processExitSouce = processExitSource ?? throw new ArgumentNullException(nameof(processExitSource)); - _bootNodes = new List(); - NetworkNode[] bootnodes = NetworkNode.ParseNodes(_discoveryConfig.Bootnodes, _logger); + var bootNodes = new List(); + NetworkNode[] bootnodes = NetworkNode.ParseNodes(discoveryConfig1.Bootnodes, _logger); if (bootnodes.Length == 0) { if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); - return; } for (int i = 0; i < bootnodes.Length; i++) @@ -71,8 +71,28 @@ public DiscoveryApp( _logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); } - _bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.Port)); + bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.Port)); } + + _discv4Services = rootScope.BeginLifetimeScope( + (builder) => builder + .AddModule(new DiscV4KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddSingleton() + .AddSingleton() + ); + + (_kademliaNodeSource, _persistenceManager, _discv4Adapter, _kademlia) = _discv4Services.Resolve(); + } + + /// + /// Just a small class to make resolve easier + /// + private record DiscV4Services( + IKademliaNodeSource NodeSource, + DiscoveryPersistenceManager PersistenceManager, + IKademliaDiscv4Adapter Discv4Adapter, + IKademlia Kademlia) + { } public Task StartAsync() @@ -210,4 +230,8 @@ public IAsyncEnumerable DiscoverNodes(CancellationToken token) } public event EventHandler? NodeRemoved { add { } remove { } } + public ValueTask DisposeAsync() + { + return _discv4Services.DisposeAsync(); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs index 91756d2ffb90..a30d12ba11d3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -1,14 +1,10 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Autofac.Features.AttributeFilters; using Nethermind.Config; using Nethermind.Core.Crypto; -using Nethermind.Kademlia; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; @@ -40,19 +36,19 @@ public class DiscoveryPersistenceManager /// Log manager for logging events. /// Thrown if any required parameter is null. public DiscoveryPersistenceManager( - INetworkStorage discoveryStorage, + [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage discoveryStorage, INodeStatsManager nodeStatsManager, IKademliaDiscv4Adapter discv4Adapter, IKademlia kademlia, IDiscoveryConfig discoveryConfig, ILogManager logManager) { - _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); - _nodeStatsManager = nodeStatsManager ?? throw new ArgumentNullException(nameof(nodeStatsManager)); - _discv4Adapter = discv4Adapter ?? throw new ArgumentNullException(nameof(discv4Adapter)); + _discoveryStorage = discoveryStorage; + _nodeStatsManager = nodeStatsManager; + _discv4Adapter = discv4Adapter; _kademlia = kademlia; - _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _persistenceInterval = discoveryConfig?.DiscoveryPersistenceInterval ?? throw new ArgumentNullException(nameof(discoveryConfig)); + _logger = logManager.GetClassLogger(); + _persistenceInterval = discoveryConfig.DiscoveryPersistenceInterval; } /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index b3f2482e1fe2..2d03e60ebe3c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery; -public class DiscV4KademliaModule(NodeRecord selfNodeRecord, PublicKey masterNode, IReadOnlyList bootNodes) : Module +public class DiscV4KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module { protected override void Load(ContainerBuilder builder) { @@ -19,7 +19,6 @@ protected override void Load(ContainerBuilder builder) .AddModule(new KademliaModule()) .AddSingleton, NodeNodeHashProvider>() .AddSingleton, NodeNodeHashProvider>() - .AddSingleton(selfNodeRecord) .AddSingleton() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 93b5927ec38b..e5fee0cc5edc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -9,7 +9,6 @@ using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; using Nethermind.Stats; using Nethermind.Stats.Model; @@ -23,7 +22,7 @@ public class KademliaDiscv4Adapter( Lazy> nodeHealthTracker, IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, - NodeRecord selfNodeRecord, + INodeRecordProvider nodeRecordProvider, INodeStatsManager nodeStatsManager, ITimestamper timestamper, IProcessExitSource processExitSource, @@ -163,7 +162,7 @@ public async Task Ping(Node receiver, CancellationToken token) token = cts.Token; PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); - msg.EnrSequence = selfNodeRecord.EnrSequence; // optional and does not seems to be used anywhere. + msg.EnrSequence = nodeRecordProvider.Current.EnrSequence; // optional and does not seems to be used anywhere. NodeSession session = GetSession(receiver); @@ -210,7 +209,7 @@ private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMs } Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await SendMessage(session, new EnrResponseMsg(node.Address, selfNodeRecord, Keccak.Compute(requestRlp.Bytes)), token); + await SendMessage(session, new EnrResponseMsg(node.Address, nodeRecordProvider.Current, Keccak.Compute(requestRlp.Bytes)), token); } private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg, CancellationToken token) From 2e76e446d5315795a0a03a8862a438e42f11bcdc Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Fri, 16 May 2025 18:40:30 +0800 Subject: [PATCH 071/182] Tests --- .../Modules/DiscoveryModule.cs | 2 - .../DiscoveryPersistenceManagerTests.cs | 103 +++++------------- .../Discv4/KademliaDiscv4AdapterTests.cs | 4 +- .../DiscoveryApp.cs | 2 +- .../DiscoveryPersistenceManager.cs | 17 +-- 5 files changed, 40 insertions(+), 88 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 736f9a8ce27d..fe48c4a7f7ff 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -12,9 +12,7 @@ using Nethermind.Network.Config; using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv5; -using Nethermind.Network.Discovery.Lifecycle; using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.RoutingTable; using Nethermind.Network.Discovery.Serializers; using Nethermind.Network.Dns; using Nethermind.Network.Enr; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index 1ff3b1462389..663dc254aabb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using Nethermind.Config; using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; using Nethermind.Core.Test.Builders; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; @@ -36,12 +38,13 @@ public void Setup() _networkStorage = Substitute.For(); _nodeStatsManager = Substitute.For(); _discv4Adapter = Substitute.For(); - _discoveryConfig = Substitute.For(); + _discoveryConfig = new DiscoveryConfig() + { + DiscoveryPersistenceInterval = 100, + }; _logManager = LimboLogs.Instance; _kademlia = Substitute.For>(); - _discoveryConfig.DiscoveryPersistenceInterval.Returns(1000); - _persistenceManager = new DiscoveryPersistenceManager( _networkStorage, _nodeStatsManager, @@ -57,11 +60,10 @@ public void Teardown() _discv4Adapter?.DisposeAsync(); } - /* [Test] - public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node() + [CancelAfter(10000)] + public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node(CancellationToken cancellationToken) { - // Arrange var networkNodes = new[] { new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0), @@ -70,40 +72,17 @@ public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node() _networkStorage.GetPersistedNodes().Returns(networkNodes); - // Act - await _persistenceManager.AddPersistedNodes(CancellationToken.None); + await _persistenceManager.LoadPersistedNodes(cancellationToken); - // Assert await _discv4Adapter.Received(networkNodes.Length).Ping( Arg.Is(n => networkNodes.Any(nn => nn.NodeId.Equals(n.Id) && nn.Host == n.Host && nn.Port == n.Port)), Arg.Any()); } [Test] - public async Task AddPersistedNodes_Should_Skip_Invalid_Nodes() - { - // Arrange - var validNode = new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0); - // An invalid node with null NodeId - var invalidNode = new NetworkNode(null, "192.168.1.2", 30303, 0); - - var networkNodes = new[] { validNode, invalidNode }; - - _networkStorage.GetPersistedNodes().Returns(networkNodes); - - // Act - await _persistenceManager.AddPersistedNodes(CancellationToken.None); - - // Assert - only one ping should be attempted - await _discv4Adapter.Received(1).Ping( - Arg.Is(n => n.Id.Equals(validNode.NodeId) && n.Host == validNode.Host && n.Port == validNode.Port), - Arg.Any()); - } - - [Test] - public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions() + [CancelAfter(10000)] + public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions(CancellationToken cancellationToken) { - // Arrange var networkNodes = new[] { new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0), @@ -123,52 +102,42 @@ public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions() Arg.Any()) .Returns(x => throw new Exception("Test exception")); - // Act & Assert - should not throw - await _persistenceManager.AddPersistedNodes(CancellationToken.None); + await _persistenceManager.LoadPersistedNodes(cancellationToken); } [Test] public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() { - // Arrange var nodes = new[] { new Node(TestItem.PublicKeyA, "192.168.1.1", 30303), new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) }; + var asIps = nodes.Select((n) => n.Address).ToArray(); - var cancellationSource = new CancellationTokenSource(); - - _kademlia.IterateNodes().Returns(nodes.ToAsyncEnumerable()); + var cancellationSource = new CancellationTokenSource() + .ThatCancelAfter(TimeSpan.FromMilliseconds(5000)); - // Act - start the persistence process var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationSource.Token); // Wait a bit to allow at least one persistence cycle to complete - await Task.Delay(50); - - // Cancel the task so we can complete the test - cancellationSource.Cancel(); + await Task.Delay(_discoveryConfig.DiscoveryPersistenceInterval + 10, cancellationSource.Token); - try - { - await persistenceTask; - } - catch (OperationCanceledException) - { - // Expected - } + await cancellationSource.CancelAsync(); - // Assert _networkStorage.Received().StartBatch(); - _networkStorage.Received().UpdateNodes(Arg.Is>(nn => - nn.Count() == nodes.Length && - nn.All(n => nodes.Any(node => node.Id.Equals(n.NodeId) && node.Host == n.Host && node.Port == n.Port)))); + /* + _networkStorage.Received().UpdateNodes(Arg.Is>((IEnumerable nn) => + { + return Enumerable.SequenceEqual(nn.Select((n) => new IPEndPoint(n.HostIp, n.Port)), asIps); + })); + */ _networkStorage.Received().Commit(); } [Test] - public async Task RunDiscoveryPersistenceCommit_Should_Handle_Exceptions() + [CancelAfter(10000)] + public async Task RunDiscoveryPersistenceCommit_Should_Handle_Exceptions(CancellationToken cancellationToken) { // Arrange var nodes = new[] @@ -177,32 +146,20 @@ public async Task RunDiscoveryPersistenceCommit_Should_Handle_Exceptions() new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) }; - var cancellationSource = new CancellationTokenSource(); - - _kademlia.IterateNodes().Returns(nodes.ToAsyncEnumerable()); + _kademlia.IterateNodes().Returns(nodes); _networkStorage.When(x => x.StartBatch()).Throw(new Exception("Test exception")); // Act - start the persistence process - var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationSource.Token); + var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); // Wait a bit to allow at least one persistence cycle to complete - await Task.Delay(50); + await Task.Delay(50, cancellationToken); - // Cancel the task so we can complete the test - cancellationSource.Cancel(); - - try - { - await persistenceTask; - } - catch (OperationCanceledException) - { - // Expected - } + // Cancel the task so we can complete the test - No need for this as the CancelAfter will handle cancellation + // We can leave the rest of the code in the test unchanged // If we got here without other exceptions, the error was properly handled Assert.Pass(); } - */ } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index a0bcf98f9155..000e9a111bae 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -90,13 +90,15 @@ public void Setup() builder.WithDiscovery(TestItem.PrivateKeyB); _receiverSerializationManager = builder.TestObject; + INodeRecordProvider nodeRecordProvider = Substitute.For(); + nodeRecordProvider.Current.Returns(_selfNodeRecord); _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), new Lazy>(() => _nodeHealthTracker), new DiscoveryConfig(), _kademliaConfig, - _selfNodeRecord, + nodeRecordProvider, Substitute.For(), _timestamper, Substitute.For(), diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 175fd99ef001..efffbb86d2c3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -205,7 +205,7 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) try { // Step 1 - read nodes and stats from db - await _persistenceManager!.AddPersistedNodes(cancellationToken); + await _persistenceManager!.LoadPersistedNodes(cancellationToken); Task persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs index a30d12ba11d3..8ae75cfb7f70 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -56,15 +56,12 @@ public DiscoveryPersistenceManager( /// /// Cancellation token to stop the operation. /// A task representing the asynchronous operation. - public async Task AddPersistedNodes(CancellationToken cancellationToken) + public async Task LoadPersistedNodes(CancellationToken cancellationToken) { NetworkNode[] nodes = _discoveryStorage.GetPersistedNodes(); foreach (NetworkNode networkNode in nodes) { - if (cancellationToken.IsCancellationRequested) - { - break; - } + if (cancellationToken.IsCancellationRequested) break; Node node; try @@ -106,7 +103,6 @@ public async Task AddPersistedNodes(CancellationToken cancellationToken) /// /// Periodically commits discovered nodes to persistent storage. /// - /// The Kademlia instance containing nodes to persist. /// Cancellation token to stop the operation. /// A task representing the asynchronous operation. public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationToken) @@ -114,16 +110,15 @@ public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationTo if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_persistenceInterval)); - while (!cancellationToken.IsCancellationRequested - && await timer.WaitForNextTickAsync(cancellationToken)) + while (!cancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync(cancellationToken)) { try { _discoveryStorage.StartBatch(); - var nodes = _kademlia.IterateNodes().ToArray(); - _discoveryStorage.UpdateNodes(nodes.Select(x => new NetworkNode(x.Id, x.Host, - x.Port, _nodeStatsManager.GetNewPersistedReputation(x))).ToArray()); + _discoveryStorage.UpdateNodes(_kademlia + .IterateNodes() + .Select(x => new NetworkNode(x.Id, x.Host, x.Port, _nodeStatsManager.GetNewPersistedReputation(x)))); _discoveryStorage.Commit(); } From 9c3ae6b2790fc2f6bef6ccb624c9f26281278a40 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Tue, 20 May 2025 14:11:17 +0800 Subject: [PATCH 072/182] Slight code reduction --- .../DiscoveryPersistenceManagerTests.cs | 59 +++++-------------- .../DiscoveryApp.cs | 48 ++++++--------- .../Discv4/DiscV4KademliaModule.cs | 2 + 3 files changed, 35 insertions(+), 74 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index 663dc254aabb..b5a42812a387 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -2,15 +2,15 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; +using FluentAssertions; using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Core.Test.Builders; +using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; @@ -24,6 +24,7 @@ namespace Nethermind.Network.Discovery.Test [Parallelizable(ParallelScope.Self)] public class DiscoveryPersistenceManagerTests { + private MemDb _discoveryDb = null!; private INetworkStorage _networkStorage = null!; private INodeStatsManager _nodeStatsManager = null!; private IKademliaDiscv4Adapter _discv4Adapter = null!; @@ -35,7 +36,10 @@ public class DiscoveryPersistenceManagerTests [SetUp] public void Setup() { - _networkStorage = Substitute.For(); + NetworkNodeDecoder.Init(); + + _discoveryDb = new MemDb(); + _networkStorage = new NetworkStorage(_discoveryDb, LimboLogs.Instance); _nodeStatsManager = Substitute.For(); _discv4Adapter = Substitute.For(); _discoveryConfig = new DiscoveryConfig() @@ -58,6 +62,7 @@ public void Setup() public void Teardown() { _discv4Adapter?.DisposeAsync(); + _discoveryDb.Dispose(); } [Test] @@ -70,7 +75,7 @@ public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node(CancellationToke new NetworkNode(TestItem.PublicKeyB, "192.168.1.2", 30303, 0) }; - _networkStorage.GetPersistedNodes().Returns(networkNodes); + _networkStorage.UpdateNodes(networkNodes); await _persistenceManager.LoadPersistedNodes(cancellationToken); @@ -89,7 +94,7 @@ public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions(CancellationTo new NetworkNode(TestItem.PublicKeyB, "192.168.1.2", 30303, 0) }; - _networkStorage.GetPersistedNodes().Returns(networkNodes); + _networkStorage.UpdateNodes(networkNodes); // First ping succeeds, second one throws _discv4Adapter.Ping( @@ -113,53 +118,19 @@ public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() new Node(TestItem.PublicKeyA, "192.168.1.1", 30303), new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) }; - var asIps = nodes.Select((n) => n.Address).ToArray(); - - var cancellationSource = new CancellationTokenSource() - .ThatCancelAfter(TimeSpan.FromMilliseconds(5000)); - - var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationSource.Token); - - // Wait a bit to allow at least one persistence cycle to complete - await Task.Delay(_discoveryConfig.DiscoveryPersistenceInterval + 10, cancellationSource.Token); - - await cancellationSource.CancelAsync(); - - _networkStorage.Received().StartBatch(); - /* - _networkStorage.Received().UpdateNodes(Arg.Is>((IEnumerable nn) => - { - return Enumerable.SequenceEqual(nn.Select((n) => new IPEndPoint(n.HostIp, n.Port)), asIps); - })); - */ - _networkStorage.Received().Commit(); - } - [Test] - [CancelAfter(10000)] - public async Task RunDiscoveryPersistenceCommit_Should_Handle_Exceptions(CancellationToken cancellationToken) - { - // Arrange - var nodes = new[] - { - new Node(TestItem.PublicKeyA, "192.168.1.1", 30303), - new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) - }; + var cls = new CancellationTokenSource().ThatCancelAfter(TimeSpan.FromMilliseconds(5000)); _kademlia.IterateNodes().Returns(nodes); - _networkStorage.When(x => x.StartBatch()).Throw(new Exception("Test exception")); - // Act - start the persistence process - var persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); + _ = _persistenceManager.RunDiscoveryPersistenceCommit(cls.Token); // Wait a bit to allow at least one persistence cycle to complete - await Task.Delay(50, cancellationToken); + await Task.Delay(_discoveryConfig.DiscoveryPersistenceInterval * 2, cls.Token); - // Cancel the task so we can complete the test - No need for this as the CancelAfter will handle cancellation - // We can leave the rest of the code in the test unchanged + await cls.CancelAsync(); - // If we got here without other exceptions, the error was properly handled - Assert.Pass(); + _discoveryDb.Count.Should().Be(2); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index efffbb86d2c3..87e6c73975bf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -24,44 +24,33 @@ namespace Nethermind.Network.Discovery; public class DiscoveryApp : IDiscoveryApp, IAsyncDisposable { - private readonly ITimestamper _timestamper; - private readonly ILogManager _logManager; private readonly ILogger _logger; - private readonly IMessageSerializationService _messageSerializationService; private readonly INetworkConfig _networkConfig; - - private NettyDiscoveryHandler? _discoveryHandler; - - private IKademliaNodeSource _kademliaNodeSource; - private DiscoveryPersistenceManager _persistenceManager; - private IKademliaDiscv4Adapter _discv4Adapter; - private IKademlia _kademlia; + private readonly IKademliaNodeSource _kademliaNodeSource; + private readonly DiscoveryPersistenceManager _persistenceManager; + private readonly IKademliaDiscv4Adapter _discv4Adapter; + private readonly IKademlia _kademlia; + private readonly Func _discoveryHandlerFactory; private readonly ILifetimeScope _discv4Services; + private NettyDiscoveryHandler? _discoveryHandler; private Task? _runningTask; - private readonly IProcessExitSource _processExitSouce; + private readonly IProcessExitSource _processExitSource; public DiscoveryApp( ILifetimeScope rootScope, [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, - IMessageSerializationService msgSerializationService, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, - ITimestamper timestamper, IProcessExitSource processExitSource, ILogManager logManager) { - _logManager = logManager ?? throw new ArgumentNullException(nameof(logManager)); - _logger = _logManager.GetClassLogger(); - IDiscoveryConfig discoveryConfig1 = discoveryConfig ?? throw new ArgumentNullException(nameof(discoveryConfig)); - _messageSerializationService = - msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); - _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); - _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - _processExitSouce = processExitSource ?? throw new ArgumentNullException(nameof(processExitSource)); + _logger = logManager.GetClassLogger(); + _networkConfig = networkConfig; + _processExitSource = processExitSource; var bootNodes = new List(); - NetworkNode[] bootnodes = NetworkNode.ParseNodes(discoveryConfig1.Bootnodes, _logger); + NetworkNode[] bootnodes = NetworkNode.ParseNodes(discoveryConfig.Bootnodes, _logger); if (bootnodes.Length == 0) { if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); @@ -81,11 +70,10 @@ public DiscoveryApp( _discv4Services = rootScope.BeginLifetimeScope( (builder) => builder .AddModule(new DiscV4KademliaModule(nodeKey.PublicKey, bootNodes)) - .AddSingleton() .AddSingleton() ); - (_kademliaNodeSource, _persistenceManager, _discv4Adapter, _kademlia) = _discv4Services.Resolve(); + (_kademliaNodeSource, _persistenceManager, _discv4Adapter, _kademlia, _discoveryHandlerFactory) = _discv4Services.Resolve(); } /// @@ -95,8 +83,9 @@ private record DiscV4Services( IKademliaNodeSource NodeSource, DiscoveryPersistenceManager PersistenceManager, IKademliaDiscv4Adapter Discv4Adapter, - IKademlia Kademlia) - { + IKademlia Kademlia, + Func NettyDiscoveryHandlerFactory + ) { } public Task StartAsync() @@ -160,8 +149,7 @@ private void Initialize() public void InitializeChannel(IChannel channel) { - _discoveryHandler = new NettyDiscoveryHandler(_discv4Adapter, channel, _messageSerializationService, - _timestamper, _logManager); + _discoveryHandler = _discoveryHandlerFactory(channel); _discv4Adapter.MsgSender = _discoveryHandler; _discoveryHandler.OnChannelActivated += OnChannelActivated; @@ -179,7 +167,7 @@ private void OnChannelActivated(object? sender, EventArgs e) // Explicitly use TaskScheduler.Default, otherwise it will use dotnetty's task scheduler which have a habit of // not working sometimes. _runningTask = Task.Factory - .StartNew(() => OnChannelActivated(_processExitSouce.Token), _processExitSouce.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) + .StartNew(() => OnChannelActivated(_processExitSource.Token), _processExitSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) .ContinueWith ( t => @@ -192,7 +180,7 @@ private void OnChannelActivated(object? sender, EventArgs e) (Exception)new NetworkingException(faultMessage, NetworkExceptionType.Discovery); } - if (t.IsCompleted && !_processExitSouce.Token.IsCancellationRequested) + if (t.IsCompleted && !_processExitSource.Token.IsCancellationRequested) { _logger.Debug("Discovery App initialized."); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 2d03e60ebe3c..39780e6a9491 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -33,8 +33,10 @@ protected override void Load(ContainerBuilder builder) }) .AddSingleton() .AddSingleton() + .Bind() .AddSingleton>(c => c.Resolve()) .AddSingleton() + .AddSingleton() .AddSingleton(); } } From 30c22be0d0236c13f1060052958fa1a6d1d98ccb Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 09:47:51 +0800 Subject: [PATCH 073/182] Reducing change --- src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs | 1 - src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 87e6c73975bf..1162cda8dfd4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -132,7 +132,6 @@ public async Task StopAsync() } if (_logger.IsInfo) _logger.Info("Discovery shutdown complete.. please wait for all components to close"); - // _kademliaServices?.DisposeAsync(); } public void AddNodeToDiscovery(Node node) diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 610a7e193557..0390c4424688 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -22,7 +22,7 @@ public class NodeRecord private Hash256? _contentHash; - public SortedDictionary Entries { get; } = new(); + private SortedDictionary Entries { get; } = new(); /// /// This field is used when this is deserialized and an unknown entry is encountered. From 14a1adae4a5e2b50b6c717f724ef384a0a07dc29 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 09:48:31 +0800 Subject: [PATCH 074/182] Reducing change --- src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs index 30ca51718dc6..91b1e614cb96 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs @@ -61,7 +61,5 @@ public static class EnrContentKey /// public const string Udp6 = "udp6"; public static ReadOnlySpan Udp6U8 => "udp6"u8; - - public static HashSet KnownKeys = [Id, Eth, Ip, Ip6, Secp256K1, Tcp, Tcp6, Udp, Udp6]; } } From 45962ba13398200e8bed59598db79a708c03aa79 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 09:49:07 +0800 Subject: [PATCH 075/182] Reducing change --- .../Nethermind.Network.Discovery/NettyDiscoveryHandler.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index 208fcf50c039..579944b565f2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -144,8 +144,7 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg) try { msg = Deserialize(type, new ArraySegment(msgBytes, 0, size)); - IPEndPoint endPoint = (IPEndPoint)address; - msg.FarAddress = endPoint; + msg.FarAddress = (IPEndPoint)address; } catch (Exception e) { From fa41f0c8bdd6adac442bd890a288dea0eba6f327 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 09:52:04 +0800 Subject: [PATCH 076/182] Reducing change --- .../Nethermind.Network.Discovery/NettyDiscoveryHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index 4d668e74866b..579944b565f2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -20,20 +20,20 @@ namespace Nethermind.Network.Discovery; public class NettyDiscoveryHandler : NettyDiscoveryBaseHandler, IMsgSender { private readonly ILogger _logger; - private readonly IDiscoveryManager _discoveryManager; + private readonly IDiscoveryMsgListener _discoveryMsgListener; private readonly IChannel _channel; private readonly IMessageSerializationService _msgSerializationService; private readonly ITimestamper _timestamper; public NettyDiscoveryHandler( - IDiscoveryManager? discoveryManager, + IDiscoveryMsgListener? discoveryManager, IChannel? channel, IMessageSerializationService? msgSerializationService, ITimestamper? timestamper, ILogManager? logManager) : base(logManager) { _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _discoveryManager = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); + _discoveryMsgListener = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); _channel = channel ?? throw new ArgumentNullException(nameof(channel)); _msgSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); @@ -177,7 +177,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket // Explicitly run it on the default scheduler to prevent something down the line hanging netty task scheduler. Task.Factory.StartNew( - () => _discoveryManager.OnIncomingMsg(msg), + () => _discoveryMsgListener.OnIncomingMsg(msg), CancellationToken.None, TaskCreationOptions.RunContinuationsAsynchronously, TaskScheduler.Default From 3d93ace2c5ca8278185605c489849e76875719ec Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 09:55:42 +0800 Subject: [PATCH 077/182] Nodes to array segment --- .../DiscoveryMessageSerializerTests.cs | 2 +- .../NodeLifecycleManagerTests.cs | 2 +- .../Lifecycle/NodeLifecycleManager.cs | 4 ++-- .../Messages/NeighborsMsg.cs | 12 ++++++------ .../Serializers/NeighborsMsgSerializer.cs | 10 +++++----- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs index ef9e96510bc4..ed8a2cdc9df6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs @@ -189,7 +189,7 @@ public void NeighborsMessageTest() Assert.That(deserializedMessage.FarPublicKey, Is.EqualTo(message.FarPublicKey)); Assert.That(deserializedMessage.ExpirationTime, Is.EqualTo(message.ExpirationTime)); - for (int i = 0; i < message.Nodes.Length; i++) + for (int i = 0; i < message.Nodes.Count; i++) { Assert.That(deserializedMessage.Nodes[i].Host, Is.EqualTo(message.Nodes[i].Host)); Assert.That(deserializedMessage.Nodes[i].Port, Is.EqualTo(message.Nodes[i].Port)); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs index 58731acc2972..4554065f49e8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NodeLifecycleManagerTests.cs @@ -145,7 +145,7 @@ public async Task handling_findnode_msg_will_limit_result_to_12() Assert.That(sentMsg, Is.Not.Null); _nodeTable.Buckets[0].BondedItemsCount.Should().Be(32); - sentMsg!.Nodes.Length.Should().Be(12); + sentMsg!.Nodes.Count.Should().Be(12); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs index 44159f144884..af13f43a036d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Lifecycle/NodeLifecycleManager.cs @@ -170,7 +170,7 @@ public void ProcessNeighborsMsg(NeighborsMsg? msg) return; } - if (_lastNeighbourSize + msg.Nodes.Length == 16) + if (_lastNeighbourSize + msg.Nodes.Count == 16) { // Turns out, other client will split the neighbour msg to two msg, whose size sum up to 16. // Happens about 70% of the time. @@ -181,7 +181,7 @@ public void ProcessNeighborsMsg(NeighborsMsg? msg) ProcessNodes(msg); } - _lastNeighbourSize = msg.Nodes.Length; + _lastNeighbourSize = msg.Nodes.Count; _isNeighborsExpected = false; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs index c1ced0592f96..aaa2c77c2dda 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs @@ -9,21 +9,21 @@ namespace Nethermind.Network.Discovery.Messages; public class NeighborsMsg : DiscoveryMsg { - public Node[] Nodes { get; init; } + public ArraySegment Nodes { get; init; } - public NeighborsMsg(IPEndPoint farAddress, long expirationTime, Node[] nodes) : base(farAddress, expirationTime) + public NeighborsMsg(IPEndPoint farAddress, long expirationTime, ArraySegment nodes) : base(farAddress, expirationTime) { - Nodes = nodes ?? throw new ArgumentNullException(nameof(nodes)); + Nodes = nodes; } - public NeighborsMsg(PublicKey farPublicKey, long expirationTime, Node[] nodes) : base(farPublicKey, expirationTime) + public NeighborsMsg(PublicKey farPublicKey, long expirationTime, ArraySegment nodes) : base(farPublicKey, expirationTime) { - Nodes = nodes ?? throw new ArgumentNullException(nameof(nodes)); + Nodes = nodes; } public override string ToString() { - return base.ToString() + $", Nodes: {(Nodes.Length != 0 ? string.Join(",", Nodes.Select(static x => x.ToString())) : "empty")}"; + return base.ToString() + $", Nodes: {(Nodes.Count != 0 ? string.Join(",", Nodes.Select(static x => x.ToString())) : "empty")}"; } public override MsgType MsgType => MsgType.Neighbors; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs index 5cfac7d044a8..2fac4ab14313 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs @@ -44,10 +44,10 @@ public void Serialize(IByteBuffer byteBuffer, NeighborsMsg msg) PrepareBufferForSerialization(byteBuffer, totalLength, (byte)msg.MsgType); NettyRlpStream stream = new(byteBuffer); stream.StartSequence(contentLength); - if (msg.Nodes.Length != 0) + if (msg.Nodes.Count != 0) { stream.StartSequence(nodesContentLength); - for (int i = 0; i < msg.Nodes.Length; i++) + for (int i = 0; i < msg.Nodes.Count; i++) { Node node = msg.Nodes[i]; SerializeNode(stream, node.Address, node.Id.Bytes); @@ -82,10 +82,10 @@ private static Node[] DeserializeNodes(RlpStream rlpStream) return rlpStream.DecodeArray(_decodeItem); } - private static int GetNodesLength(Node[] nodes, out int contentLength) + private static int GetNodesLength(ArraySegment nodes, out int contentLength) { contentLength = 0; - for (int i = 0; i < nodes.Length; i++) + for (int i = 0; i < nodes.Count; i++) { Node node = nodes[i]; contentLength += Rlp.LengthOfSequence(GetLengthSerializeNode(node.Address, node.Id.Bytes)); @@ -103,7 +103,7 @@ private static (int totalLength, int contentLength, int nodesContentLength) GetL { int nodesContentLength = 0; int contentLength = 0; - if (msg.Nodes.Length != 0) + if (msg.Nodes.Count != 0) { contentLength += GetNodesLength(msg.Nodes, out nodesContentLength); } From f1eeeef7a10bab2ad2741a9463fb1edda72ebcc8 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 10:23:25 +0800 Subject: [PATCH 078/182] Extract some logic out to DiscoveryPersistenceManager.cs --- .../Modules/DiscoveryModule.cs | 1 + .../DiscoveryPersistenceManagerTests.cs | 87 +++++++++ .../DiscoveryApp.cs | 168 +++++------------- .../DiscoveryPersistenceManager.cs | 137 ++++++++++++++ 4 files changed, 268 insertions(+), 125 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 78dfb7dd5b9d..686b85396432 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -124,6 +124,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() ; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs new file mode 100644 index 000000000000..3a221bb5955c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Db; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Lifecycle; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test +{ + [Parallelizable(ParallelScope.Self)] + public class DiscoveryPersistenceManagerTests + { + private MemDb _discoveryDb = null!; + private INetworkStorage _networkStorage = null!; + private IDiscoveryConfig _discoveryConfig = null!; + private ILogManager _logManager = null!; + private DiscoveryPersistenceManager _persistenceManager = null!; + private IDiscoveryManager _discoveryManager; + + [SetUp] + public void Setup() + { + NetworkNodeDecoder.Init(); + + _discoveryDb = new MemDb(); + _networkStorage = new NetworkStorage(_discoveryDb, LimboLogs.Instance); + _discoveryConfig = new DiscoveryConfig() + { + DiscoveryPersistenceInterval = 100, + }; + _logManager = LimboLogs.Instance; + _discoveryManager = Substitute.For(); + + _persistenceManager = new DiscoveryPersistenceManager( + _networkStorage, + _discoveryManager, + _discoveryConfig, + _logManager); + } + + [TearDown] + public void Teardown() + { + _discoveryDb.Dispose(); + } + + [Test] + public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() + { + var nodes = new[] + { + new Node(TestItem.PublicKeyA, "192.168.1.1", 30303), + new Node(TestItem.PublicKeyB, "192.168.1.2", 30303) + }; + + var cls = new CancellationTokenSource().ThatCancelAfter(TimeSpan.FromMilliseconds(5000)); + + var lifecycleManagers = nodes.Select((node) => + { + INodeLifecycleManager lifecycle = Substitute.For(); + lifecycle.ManagedNode.Returns(node); + return lifecycle; + }).ToArray(); + + _discoveryManager.GetNodeLifecycleManagers().Returns(lifecycleManagers); + + _ = _persistenceManager.RunDiscoveryPersistenceCommit(cls.Token); + + // Wait a bit to allow at least one persistence cycle to complete + await Task.Delay(_discoveryConfig.DiscoveryPersistenceInterval * 2, cls.Token); + + await cls.CancelAsync(); + + _discoveryDb.Count.Should().Be(2); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 8a3232b6f187..0f95896132a2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -35,10 +35,12 @@ public class DiscoveryApp : IDiscoveryApp private readonly IMessageSerializationService _messageSerializationService; private readonly ICryptoRandom _cryptoRandom; private readonly INetworkStorage _discoveryStorage; + private readonly DiscoveryPersistenceManager _persistenceManager; + private readonly IProcessExitSource _processExitSource; private readonly INetworkConfig _networkConfig; private NettyDiscoveryHandler? _discoveryHandler; - private Task? _storageCommitTask; + private Task? _runningTask; public DiscoveryApp( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, @@ -48,6 +50,8 @@ public DiscoveryApp( IMessageSerializationService? msgSerializationService, ICryptoRandom? cryptoRandom, [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage? discoveryStorage, + DiscoveryPersistenceManager discoveryPersistenceManager, + IProcessExitSource processExitSource, INetworkConfig? networkConfig, IDiscoveryConfig? discoveryConfig, ITimestamper? timestamper, @@ -64,6 +68,8 @@ public DiscoveryApp( msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); _cryptoRandom = cryptoRandom ?? throw new ArgumentNullException(nameof(cryptoRandom)); _discoveryStorage = discoveryStorage ?? throw new ArgumentNullException(nameof(discoveryStorage)); + _persistenceManager = discoveryPersistenceManager; + _processExitSource = processExitSource; _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); _discoveryStorage.StartBatch(); @@ -98,20 +104,35 @@ public async Task StopAsync() if (_logger.IsDebug) _logger.Debug("Stopping discovery timer"); if (_logger.IsDebug) _logger.Debug("Stopping discovery persistence timer"); - _appShutdownSource.Cancel(); + try + { + if (_runningTask is not null) + { + await _runningTask; + } + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error("Error in discovery task", e); + } - if (_storageCommitTask is not null) + try { - await _storageCommitTask.ContinueWith(x => + if (_discoveryHandler is not null) { - if (x.IsFailedButNotCanceled()) - { - if (_logger.IsError) _logger.Error("Error during discovery persistence stop.", x.Exception); - } - }); + _discoveryHandler.OnChannelActivated -= OnChannelActivated; + } + + NetworkChange.NetworkAvailabilityChanged -= ResetUnreachableStatus; + } + catch (Exception e) + { + _logger.Error("Error during discovery cleanup", e); } - Cleanup(); if (_logger.IsInfo) _logger.Info("Discovery shutdown complete.. please wait for all components to close"); } @@ -154,8 +175,6 @@ public void InitializeChannel(IChannel channel) .AddLast(_discoveryHandler); } - private readonly CancellationTokenSource _appShutdownSource = new(); - private void OnChannelActivated(object? sender, EventArgs e) { if (_logger.IsDebug) _logger.Debug("Activated discovery channel."); @@ -163,8 +182,8 @@ private void OnChannelActivated(object? sender, EventArgs e) // Make sure this is non blocking code, otherwise netty will not process messages // Explicitly use TaskScheduler.Default, otherwise it will use dotnetty's task scheduler which have a habit of // not working sometimes. - Task.Factory - .StartNew(() => OnChannelActivated(_appShutdownSource.Token), _appShutdownSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) + _runningTask = Task.Factory + .StartNew(() => OnChannelActivated(_processExitSource.Token), _processExitSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) .ContinueWith ( t => @@ -177,7 +196,7 @@ private void OnChannelActivated(object? sender, EventArgs e) (Exception)new NetworkingException(faultMessage, NetworkExceptionType.Discovery); } - if (t.IsCompleted && !_appShutdownSource.IsCancellationRequested) + if (t.IsCompleted && !_processExitSource.Token.IsCancellationRequested) { _logger.Debug("Discovery App initialized."); } @@ -190,7 +209,7 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) try { //Step 1 - read nodes and stats from db - AddPersistedNodes(cancellationToken); + await _persistenceManager.LoadPersistedNodes(cancellationToken); //Step 2 - initialize bootnodes if (_logger.IsDebug) _logger.Debug("Initializing bootnodes."); @@ -222,90 +241,22 @@ private async Task OnChannelActivated(CancellationToken cancellationToken) return; } - InitializeDiscoveryPersistenceTimer(); - InitializeDiscoveryTimer(); - } - catch (Exception e) - { - if (_logger.IsDebug) _logger.Error("DEBUG/ERROR Error during discovery initialization", e); - } - } + Task persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); - private void AddPersistedNodes(CancellationToken cancellationToken) - { - NetworkNode[] nodes = _discoveryStorage.GetPersistedNodes(); - foreach (NetworkNode networkNode in nodes) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } - - if (!_discoveryManager.NodesFilter.Set(networkNode.HostIp)) - { - // Already seen this node ip recently - continue; - } - - Node node; try { - node = new Node(networkNode.NodeId, networkNode.Host, networkNode.Port); - } - catch (Exception) - { - if (_logger.IsDebug) - _logger.Error( - $"ERROR/DEBUG peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - continue; - } - - INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node, true); - if (manager is null) - { - if (_logger.IsDebug) - { - _logger.Debug( - $"Skipping persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}, manager couldn't be created"); - } - - continue; + // Step 2 - run the standard kademlia routine + await RunDiscoveryProcess(); } - - manager.NodeStats.CurrentPersistedNodeReputation = networkNode.Reputation; - if (_logger.IsTrace) - _logger.Trace($"Adding persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - } - - if (_logger.IsDebug) _logger.Debug($"Added persisted discovery nodes: {nodes.Length}"); - } - - private void InitializeDiscoveryTimer() - { - if (_logger.IsDebug) _logger.Debug("Starting discovery timer"); - _ = RunDiscoveryProcess(); - } - - private void InitializeDiscoveryPersistenceTimer() - { - if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); - _storageCommitTask = RunDiscoveryPersistenceCommit(); - } - - private void Cleanup() - { - try - { - if (_discoveryHandler is not null) + finally { - _discoveryHandler.OnChannelActivated -= OnChannelActivated; + // Block until persistence is finished + await persistenceTask; } - - NetworkChange.NetworkAvailabilityChanged -= ResetUnreachableStatus; } catch (Exception e) { - _logger.Error("Error during discovery cleanup", e); + if (_logger.IsDebug) _logger.Error("DEBUG/ERROR Error during discovery initialization", e); } } @@ -400,7 +351,7 @@ private async Task InitializeBootnodes(CancellationToken cancellationToken private async Task RunDiscoveryProcess() { byte[] randomId = new byte[64]; - CancellationToken cancellationToken = _appShutdownSource.Token; + CancellationToken cancellationToken = _processExitSource.Token; PeriodicTimer timer = new(TimeSpan.FromMilliseconds(10)); long lastTickMs = Environment.TickCount64; @@ -454,39 +405,6 @@ private async Task RunDiscoveryProcess() } } - [Todo(Improve.Allocations, "Remove ToArray here - address as a part of the network DB rewrite")] - private async Task RunDiscoveryPersistenceCommit() - { - CancellationToken cancellationToken = _appShutdownSource.Token; - PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_discoveryConfig.DiscoveryPersistenceInterval)); - - while (!cancellationToken.IsCancellationRequested - && await timer.WaitForNextTickAsync(cancellationToken)) - { - try - { - IReadOnlyCollection managers = _discoveryManager.GetNodeLifecycleManagers(); - DateTime utcNow = DateTime.UtcNow; - //we need to update all notes to update reputation - _discoveryStorage.UpdateNodes(managers.Select(x => new NetworkNode(x.ManagedNode.Id, x.ManagedNode.Host, - x.ManagedNode.Port, x.NodeStats.NewPersistedNodeReputation(utcNow))).ToArray()); - - if (!_discoveryStorage.AnyPendingChange()) - { - if (_logger.IsTrace) _logger.Trace("No changes in discovery storage, skipping commit."); - continue; - } - - _discoveryStorage.Commit(); - _discoveryStorage.StartBatch(); - } - catch (Exception ex) - { - _logger.Error($"Error during discovery commit: {ex}"); - } - } - } - private void OnNodeDiscovered(object? sender, NodeEventArgs e) { NodeAdded?.Invoke(this, e); diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs new file mode 100644 index 000000000000..257b18ce7ae5 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac.Features.AttributeFilters; +using Nethermind.Config; +using Nethermind.Db; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Lifecycle; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery +{ + /// + /// Manages persistence operations for the discovery process, including loading nodes from storage + /// and periodic saving of discovered nodes. + /// + public class DiscoveryPersistenceManager + { + private readonly INetworkStorage _discoveryStorage; + private readonly IDiscoveryManager _discoveryManager; + private readonly ILogger _logger; + private readonly int _persistenceInterval; + + /// + /// Initializes a new instance of the class. + /// + /// The network storage for persisting discovery nodes. + /// Manager for node statistics. + /// Adapter for Discv4 protocol communication. + /// Configuration for the discovery process. + /// Log manager for logging events. + /// Thrown if any required parameter is null. + public DiscoveryPersistenceManager( + [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage discoveryStorage, + IDiscoveryManager discoveryManager, + IDiscoveryConfig discoveryConfig, + ILogManager logManager) + { + _discoveryStorage = discoveryStorage; + _discoveryManager = discoveryManager; + _logger = logManager.GetClassLogger(); + _persistenceInterval = discoveryConfig.DiscoveryPersistenceInterval; + } + + /// + /// Loads persisted nodes from storage and pings them to verify their availability. + /// + /// Cancellation token to stop the operation. + /// A task representing the asynchronous operation. + public Task LoadPersistedNodes(CancellationToken cancellationToken) + { + NetworkNode[] nodes = _discoveryStorage.GetPersistedNodes(); + foreach (NetworkNode networkNode in nodes) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (!_discoveryManager.NodesFilter.Set(networkNode.HostIp)) + { + // Already seen this node ip recently + continue; + } + + Node node; + try + { + node = new Node(networkNode.NodeId, networkNode.Host, networkNode.Port); + } + catch (Exception) + { + if (_logger.IsDebug) + _logger.Error( + $"ERROR/DEBUG peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); + continue; + } + + INodeLifecycleManager? manager = _discoveryManager.GetNodeLifecycleManager(node, true); + if (manager is null) + { + if (_logger.IsDebug) + { + _logger.Debug( + $"Skipping persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}, manager couldn't be created"); + } + + continue; + } + + manager.NodeStats.CurrentPersistedNodeReputation = networkNode.Reputation; + if (_logger.IsTrace) + _logger.Trace($"Adding persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); + } + + if (_logger.IsDebug) _logger.Debug($"Added persisted discovery nodes: {nodes.Length}"); + + return Task.CompletedTask; + } + + /// + /// Periodically commits discovered nodes to persistent storage. + /// + /// Cancellation token to stop the operation. + /// A task representing the asynchronous operation. + public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationToken) + { + if (_logger.IsDebug) _logger.Debug("Starting discovery persistence timer"); + PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_persistenceInterval)); + + while (!cancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync(cancellationToken)) + { + try + { + IReadOnlyCollection managers = _discoveryManager.GetNodeLifecycleManagers(); + DateTime utcNow = DateTime.UtcNow; + //we need to update all notes to update reputation + _discoveryStorage.UpdateNodes(managers.Select(x => new NetworkNode(x.ManagedNode.Id, x.ManagedNode.Host, + x.ManagedNode.Port, x.NodeStats.NewPersistedNodeReputation(utcNow))).ToArray()); + + if (!_discoveryStorage.AnyPendingChange()) + { + if (_logger.IsTrace) _logger.Trace("No changes in discovery storage, skipping commit."); + continue; + } + + _discoveryStorage.Commit(); + _discoveryStorage.StartBatch(); + } + catch (Exception ex) + { + _logger.Error($"Error during discovery commit: {ex}"); + } + } + } + } +} From e0431a88415226c5072d595c12056b266f691afd Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 19:14:18 +0800 Subject: [PATCH 079/182] Remove unnecessary comment --- .../Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs | 1 - .../Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index e5fee0cc5edc..0bcb167f4fdd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -16,7 +16,6 @@ namespace Nethermind.Network.Discovery.Discv4; -// TODO: Hard rate limit. public class KademliaDiscv4Adapter( Lazy> kademliaMessageReceiver, // Cyclic dependency Lazy> nodeHealthTracker, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 5a74ad701c18..0e3e0bf97b28 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -12,7 +12,6 @@ namespace Nethermind.Network.Discovery.Discv4; -// TODO: Unit test, remove metric public class KademliaNodeSource : IKademliaNodeSource { private readonly IKademlia _kademlia; From 77540b6fe0840b550cc1a48302bff5c0b0b58fa9 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 19:27:51 +0800 Subject: [PATCH 080/182] Remove unnecessary class --- .../Discv4/KademliaDiscv4AdapterTests.cs | 17 ++++++------- .../Kademlia/KademliaSimulation.cs | 22 +++++++++++++---- .../DiscoveryPersistenceManager.cs | 3 --- .../Discv4/KademliaDiscv4Adapter.cs | 5 ++-- .../Kademlia/IKademlia.cs | 1 + .../Kademlia/IKademliaMessageSender.cs | 9 +------ .../Kademlia/KademliaMessageReceiver.cs | 24 ------------------- .../Kademlia/KademliaModule.cs | 10 +++++++- 8 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 000e9a111bae..63f04ab62dba 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -33,7 +33,7 @@ public class KademliaDiscv4AdapterTests { private IKademliaDiscv4Adapter _adapter = null!; - private IKademliaMessageReceiver _kademliaMessageReceiver = null!; + private IKademlia _kademliaMessageReceiver = null!; private INodeHealthTracker _nodeHealthTracker = null!; private INetworkConfig _networkConfig = null!; private KademliaConfig _kademliaConfig = null!; @@ -72,7 +72,7 @@ public void Setup() _testPublicKey = TestItem.PublicKeyA; _testNode = new Node(_testPublicKey, "192.168.1.1", 30303); - _kademliaMessageReceiver = Substitute.For>(); + _kademliaMessageReceiver = Substitute.For>(); _nodeHealthTracker = Substitute.For>(); _networkConfig = Substitute.For(); _networkConfig.MaxActivePeers.Returns(25); @@ -94,7 +94,7 @@ public void Setup() nodeRecordProvider.Current.Returns(_selfNodeRecord); _adapter = new KademliaDiscv4Adapter( - new Lazy>(() => _kademliaMessageReceiver), + new Lazy>(() => _kademliaMessageReceiver), new Lazy>(() => _nodeHealthTracker), new DiscoveryConfig(), _kademliaConfig, @@ -232,7 +232,6 @@ public async Task OnIncomingMsg_ping_should_respond_with_pong(CancellationToken await Task.Delay(100); - await _kademliaMessageReceiver.Received(1).Ping(Arg.Is(n => n.Id == _receiver.Id), Arg.Any()); await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); @@ -248,20 +247,18 @@ public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(Cancella findNodeMsg = AddReceiverFarAddress(findNodeMsg); Node[] expectedNodes = Enumerable.Repeat(new Node(TestItem.PublicKeyD, "192.168.1.3", 30303), 16).ToArray(); - _kademliaMessageReceiver.FindNeighbours( - Arg.Any(), + _kademliaMessageReceiver.GetKNeighbour( Arg.Any(), - Arg.Any()) + Arg.Any()) .Returns(expectedNodes); await _adapter.OnIncomingMsg(findNodeMsg); await Task.Delay(100); - await _kademliaMessageReceiver.Received(1).FindNeighbours( - Arg.Is(n => n.Id == _receiver.Id), + _kademliaMessageReceiver.GetKNeighbour( Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), - Arg.Any()); + Arg.Is(n => n.Id == _receiver.Id)); // Send out two message instead of one because of MTU limit. await _msgSender.Received(1).SendMsg(Arg.Is(m => diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index d26ff328d164..a5a4b3a4d95c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -237,12 +237,12 @@ private class TestFabric(KademliaConfig config) readonly ValueHashNodeHashProvider _nodeHashProvider = new ValueHashNodeHashProvider(); private readonly Random _random = new Random(0); - private bool TryGetReceiver(TestNode receiverHash, out IKademliaMessageReceiver contentKademliaMessageReceiver) + private bool TryGetReceiver(TestNode receiverHash, out ReceiverForNode contentKademliaMessageReceiver) { contentKademliaMessageReceiver = null!; if (_nodes.TryGetValue(receiverHash.Hash, out var container)) { - contentKademliaMessageReceiver = container!.Resolve>(); + contentKademliaMessageReceiver = container!.Resolve(); return true; } @@ -286,7 +286,7 @@ public async Task Ping(TestNode node, CancellationToken token) await fabric.DoSimulateLatency(token); fabric.Debug($"ping from {sender} to {node}"); - if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) + if (fabric.TryGetReceiver(node, out ReceiverForNode receiver)) { await receiver.Ping(sender, token); return; @@ -301,7 +301,7 @@ public async Task FindNeighbours(TestNode node, ValueHash256 hash, C await fabric.DoSimulateLatency(token); fabric.Debug($"findn from {sender} to {node}"); - if (fabric.TryGetReceiver(node, out IKademliaMessageReceiver receiver)) + if (fabric.TryGetReceiver(node, out ReceiverForNode receiver)) { return (await receiver.FindNeighbours(sender, hash, token)).Select((node) => new TestNode(node.Hash)).ToArray(); } @@ -310,6 +310,20 @@ public async Task FindNeighbours(TestNode node, ValueHash256 hash, C } } + private class ReceiverForNode(IKademlia kademlia, INodeHealthTracker nodeHealthTracker) + { + public Task Ping(TestNode node, CancellationToken token) + { + nodeHealthTracker.OnIncomingMessageFrom(node); + return Task.CompletedTask; + } + + public Task FindNeighbours(TestNode node, ValueHash256 hash, CancellationToken token) + { + return Task.FromResult(kademlia.GetKNeighbour(hash, node)); + } + } + private Task DoSimulateLatency(CancellationToken token) { if (!SimulateLatency) return Task.CompletedTask; diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs index 26f2b40b33d9..8ae75cfb7f70 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -9,9 +9,6 @@ using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats; -using Nethermind.Db; -using Nethermind.Logging; -using Nethermind.Network.Discovery.Lifecycle; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 0bcb167f4fdd..b7c1ac5f19f7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -17,7 +17,7 @@ namespace Nethermind.Network.Discovery.Discv4; public class KademliaDiscv4Adapter( - Lazy> kademliaMessageReceiver, // Cyclic dependency + Lazy> kademlia, // Cyclic dependency Lazy> nodeHealthTracker, IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, @@ -220,7 +220,7 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms } PublicKey publicKey = new PublicKey(msg.SearchedNodeId); - Node[] nodes = await kademliaMessageReceiver.Value.FindNeighbours(node, publicKey, token); + Node[] nodes = kademlia.Value.GetKNeighbour(publicKey, node, false); if (nodes.Length <= 12) { await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes), token); @@ -236,7 +236,6 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms private async Task HandlePing(Node node, NodeSession session, PingMsg ping, CancellationToken token) { if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); - await kademliaMessageReceiver.Value.Ping(node, token); PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); session.OnPingReceived(); await SendMessage(session, msg, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index 1e817bb8462e..42e3ad1661f4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -8,6 +8,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// /// Main kademlia interface. High level code is expected to interface with this interface. /// +/// /// public interface IKademlia { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs index 7cafdd1e4568..a7ac60e24f3a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs @@ -8,6 +8,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// /// Should be exposed by application to kademlia so that kademlia can send out message. /// +/// /// public interface IKademliaMessageSender { @@ -15,11 +16,3 @@ public interface IKademliaMessageSender Task FindNeighbours(TNode receiver, TKey target, CancellationToken token); } -/// -/// Application should call this class on incoming messages. -/// -/// -public interface IKademliaMessageReceiver : IKademliaMessageSender -{ -} - diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs deleted file mode 100644 index b6d4e29b2447..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaMessageReceiver.cs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; - -namespace Nethermind.Network.Discovery.Kademlia; - -public class KademliaKademliaMessageReceiver( - IKademlia kademlia, - INodeHealthTracker healthTracker -) : IKademliaMessageReceiver where TNode : notnull -{ - public Task Ping(TNode sender, CancellationToken token) - { - healthTracker.OnIncomingMessageFrom(sender); - return Task.CompletedTask; - } - - public Task FindNeighbours(TNode sender, TKey target, CancellationToken token) - { - healthTracker.OnIncomingMessageFrom(sender); - return Task.FromResult(kademlia.GetKNeighbour(target, sender)); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 3a56c30a00bc..2e38549d51a0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -7,6 +7,15 @@ namespace Nethermind.Network.Discovery.Kademlia; +/// +/// A kademlia module. +/// Application is expeccted to expose a +/// for the table maintenance to function. +/// Additionally, application is expected to call +/// and respectedly. +/// +/// +/// public class KademliaModule : Module where TNode : notnull { protected override void Load(ContainerBuilder builder) @@ -15,7 +24,6 @@ protected override void Load(ContainerBuilder builder) builder .AddSingleton, Kademlia>() - .AddSingleton, KademliaKademliaMessageReceiver>() .AddSingleton>() .AddSingleton>() .AddSingleton>(provider => From d5ba1ffb63e53074a6cb54d082eb880f9c6bc5ac Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 20:36:45 +0800 Subject: [PATCH 081/182] Fix simulation --- .../Kademlia/KademliaSimulation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index a5a4b3a4d95c..72e7679b3ff6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -269,6 +269,7 @@ public Kademlia CreateNode(ValueHash256 nodeID) UseNewLookup = config.UseNewLookup }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) + .AddSingleton() .AddSingleton>(); var container = builder.Build(); @@ -320,6 +321,7 @@ public Task Ping(TestNode node, CancellationToken token) public Task FindNeighbours(TestNode node, ValueHash256 hash, CancellationToken token) { + nodeHealthTracker.OnIncomingMessageFrom(node); return Task.FromResult(kademlia.GetKNeighbour(hash, node)); } } From e7a54c34ce7be9d7f48e6574fc5666503b0858b0 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 20:59:55 +0800 Subject: [PATCH 082/182] Fix build --- .../NettyDiscoveryHandlerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs index 5d7a125dc62e..2a461560a538 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs @@ -16,6 +16,7 @@ using Nethermind.Core.Test.Modules; using Nethermind.Crypto; using Nethermind.Logging; +using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Test.Builders; From 9cd429e247b6e4c90f99d8d1c77587f109a52c75 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 21:06:02 +0800 Subject: [PATCH 083/182] More attempted lookup --- .../Kademlia/KademliaSimulation.cs | 15 +++++++-------- .../Kademlia/INodeHealthTracker.cs | 10 ++++++++++ .../Kademlia/KademliaConfig.cs | 5 ++--- .../Kademlia/KademliaModule.cs | 6 +++--- .../Kademlia/NodeHealthTracker.cs | 6 ------ 5 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 72e7679b3ff6..76b53434b1b8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -10,7 +10,6 @@ using Autofac; using FluentAssertions; using Nethermind.Core; -using Microsoft.Extensions.DependencyInjection; using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; @@ -19,23 +18,23 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; -[TestFixture(false, 3, 0)] -[TestFixture(true, 1, 0)] -[TestFixture(true, 1, 4)] [TestFixture(true, 3, 0)] -[TestFixture(true, 3, 4)] +[TestFixture(false, 1, 0)] +[TestFixture(false, 1, 4)] +[TestFixture(false, 3, 0)] +[TestFixture(false, 3, 4)] public class KademliaSimulation { private readonly KademliaConfig _config; - public KademliaSimulation(bool useNewLookup, int alpha, int beta) + public KademliaSimulation(bool useOriginalLookup, int alpha, int beta) { _config = new KademliaConfig() { KSize = 20, Alpha = alpha, Beta = beta, - UseNewLookup = useNewLookup, + UseOriginalLookup = useOriginalLookup, }; } @@ -266,7 +265,7 @@ public Kademlia CreateNode(ValueHash256 nodeID) Alpha = config.Alpha, Beta = config.Beta, RefreshInterval = TimeSpan.FromHours(1), - UseNewLookup = config.UseNewLookup + UseOriginalLookup = config.UseOriginalLookup }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs new file mode 100644 index 000000000000..99431729de97 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia; + +public interface INodeHealthTracker +{ + void OnIncomingMessageFrom(TNode sender); + void OnRequestFailed(TNode node); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index c2034efce7e0..95012d5b0ac4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -22,7 +22,6 @@ public class KademliaConfig /// /// Beta, as in B in kademlia the kademlia paper, 4.2 Accelerated Lookups - /// Only works with tree based routing table. /// public int Beta { get; set; } = 2; @@ -32,9 +31,9 @@ public class KademliaConfig public TimeSpan RefreshInterval { get; set; } = TimeSpan.FromMinutes(30); /// - /// Use a different algorithm for the neighbour and value lookup. + /// Use a the original lookup algorithm as in the paper. Slower. /// - public bool UseNewLookup { get; set; } = true; + public bool UseOriginalLookup { get; set; } /// /// The timeout for each find neighbour call lookup diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 2e38549d51a0..ac180d7ea522 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -29,12 +29,12 @@ protected override void Load(ContainerBuilder builder) .AddSingleton>(provider => { KademliaConfig config = provider.Resolve>(); - if (config.UseNewLookup) + if (config.UseOriginalLookup) { - return provider.Resolve>(); + return provider.Resolve>(); } - return provider.Resolve>(); + return provider.Resolve>(); }) .AddSingleton() .AddSingleton>() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs index bf32db077ad3..5a26792e609b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs @@ -8,12 +8,6 @@ namespace Nethermind.Network.Discovery.Kademlia; -public interface INodeHealthTracker -{ - void OnIncomingMessageFrom(TNode sender); - void OnRequestFailed(TNode node); -} - public class NodeHealthTracker( KademliaConfig config, IRoutingTable routingTable, From 594a157bf79ddfa3b0adc296bfde4bbdc13bd8e6 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 21:37:52 +0800 Subject: [PATCH 084/182] Trying to simplify --- .../Kademlia/KademliaSimulation.cs | 8 +------- .../Kademlia/KademliaTests.cs | 8 +------- .../Discv4/DiscV4KademliaModule.cs | 10 ++-------- .../Kademlia/DoubleEndedLru.cs | 3 --- .../Kademlia/FromKeyNodeHashProvider.cs | 11 +++++++++++ .../Kademlia/IKademlia.cs | 1 - .../Kademlia/IKeyOperator.cs | 19 +++++++++++++++++++ .../Kademlia/INodeHashProvider.cs | 15 +-------------- .../Kademlia/Kademlia.cs | 18 +++++++----------- .../Kademlia/KademliaModule.cs | 19 ++++++++++++------- 10 files changed, 54 insertions(+), 58 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 76b53434b1b8..bae8ec647087 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -195,13 +195,8 @@ private static ValueHash256 RandomKeccak(Random rand) return val; } - private class ValueHashNodeHashProvider : INodeHashProvider, IKeyOperator + private class ValueHashNodeHashProvider : IKeyOperator { - public ValueHash256 GetHash(TestNode node) - { - return node.Hash; - } - public ValueHash256 GetKey(TestNode node) { return node.Hash; @@ -256,7 +251,6 @@ public Kademlia CreateNode(ValueHash256 nodeID) builder .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Error)) - .AddSingleton>(_nodeHashProvider) .AddSingleton>(_nodeHashProvider) .AddSingleton(new KademliaConfig() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 9b6dc894e2c2..8a23cd413fe4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -26,7 +26,6 @@ private Kademlia CreateKad(KademliaConfig()) .AddSingleton(new TestLogManager(LogLevel.Trace)) - .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) @@ -163,13 +162,8 @@ public async Task TestTooManyNodeWithAcceleratedLookup() kad.GetAllAtDistance(250).ToHashSet().Should().BeEquivalentTo(testHashes[10..].ToHashSet()); } - private class ValueHashNodeHashProvider : INodeHashProvider, IKeyOperator + private class ValueHashNodeHashProvider : IKeyOperator { - public ValueHash256 GetHash(ValueHash256 node) - { - return node; - } - public ValueHash256 GetKey(ValueHash256 node) { return node; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 39780e6a9491..b6f09759eb2d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -6,7 +6,6 @@ using Nethermind.Core.Crypto; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; -using Nethermind.Network.Enr; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery; @@ -17,7 +16,6 @@ protected override void Load(ContainerBuilder builder) { builder .AddModule(new KademliaModule()) - .AddSingleton, NodeNodeHashProvider>() .AddSingleton, NodeNodeHashProvider>() .AddSingleton() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() @@ -31,6 +29,7 @@ protected override void Load(ContainerBuilder builder) RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PongTimeout), BootNodes = bootNodes }) + .AddSingleton() .AddSingleton() .AddSingleton() .Bind() @@ -41,13 +40,8 @@ protected override void Load(ContainerBuilder builder) } } -public class NodeNodeHashProvider : INodeHashProvider, IKeyOperator +public class NodeNodeHashProvider : IKeyOperator { - public ValueHash256 GetHash(Node node) - { - return node.Id.Hash; - } - public PublicKey GetKey(Node node) { return node.Id; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs index bf60a4fc0c77..4957655e9aaf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs @@ -1,17 +1,14 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Lantern.Discv5.WireProtocol.Table; using Nethermind.Core.Crypto; using Nethermind.Core.Threading; using NonBlocking; namespace Nethermind.Network.Discovery.Kademlia; -// TODO: Combine with LruCace? public class DoubleEndedLru(int capacity) where TNode : notnull { - // Double check if can be done locklesly private McsLock _lock = new McsLock(); private LinkedList<(ValueHash256, TNode)> _queue = new(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs new file mode 100644 index 000000000000..d5cba4f86ebf --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +public class FromKeyNodeHashProvider(IKeyOperator keyOperator): INodeHashProvider +{ + public ValueHash256 GetHash(TNode node) => keyOperator.GetNodeHash(node); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index 42e3ad1661f4..15b28a52a00a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -12,7 +12,6 @@ namespace Nethermind.Network.Discovery.Kademlia; /// public interface IKademlia { - /// /// Add node to the table. /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs new file mode 100644 index 000000000000..b6a0a9edf3b7 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// Define operations for and . +/// +/// +/// +public interface IKeyOperator +{ + TKey GetKey(TNode node); + ValueHash256 GetKeyHash(TKey key); + ValueHash256 GetNodeHash(TNode node) => GetKeyHash(GetKey(node)); + TKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs index e9b205583101..c3cda5c61506 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs @@ -6,23 +6,10 @@ namespace Nethermind.Network.Discovery.Kademlia; /// -/// Translate the TNode key into a which is -/// finally used for implementing the distance calculation. -/// Should this get replaced with an INode.GetHash where TNode need to implement INode? I can't decide. That would make -/// the internal methods cleaner, but it would mean TNode need to be a wrapper or have to implement some interface, -/// which may not be possible. One of the important optimization is to have a cached TNode[], so if TNode is a wrapper, -/// it would need to be unwrapped during serialization. But then again, it could be insignificant or the serialization -/// could be specialized. +/// Just a convenient interface with only one generic parameter. /// /// public interface INodeHashProvider { ValueHash256 GetHash(TNode node); } - -public interface IKeyOperator -{ - TKey GetKey(TNode node); - ValueHash256 GetKeyHash(TKey key); - TKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index 99b2384f3dfe..d87da70ac005 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -10,7 +10,6 @@ namespace Nethermind.Network.Discovery.Kademlia; public class Kademlia : IKademlia where TNode : notnull { private readonly IKademliaMessageSender _kademliaMessageSender; - private readonly INodeHashProvider _nodeHashProvider; private readonly IKeyOperator _keyOperator; private readonly IRoutingTable _routingTable; private readonly ILookupAlgo _lookupAlgo; @@ -18,14 +17,12 @@ public class Kademlia : IKademlia where TNode : notnul private readonly ILogger _logger; private readonly TNode _currentNodeId; - private readonly TKey _currentNodeIdAsKey; private readonly ValueHash256 _currentNodeIdAsHash; private readonly int _kSize; private readonly TimeSpan _refreshInterval; private readonly IReadOnlyList _bootNodes; public Kademlia( - INodeHashProvider nodeHashProvider, IKeyOperator keyOperator, IKademliaMessageSender sender, IRoutingTable routingTable, @@ -34,7 +31,6 @@ public Kademlia( INodeHealthTracker nodeHealthTracker, KademliaConfig config) { - _nodeHashProvider = nodeHashProvider; _keyOperator = keyOperator; _kademliaMessageSender = sender; _routingTable = routingTable; @@ -43,8 +39,7 @@ public Kademlia( _logger = logManager.GetClassLogger>(); _currentNodeId = config.CurrentNodeId; - _currentNodeIdAsKey = _keyOperator.GetKey(_currentNodeId); - _currentNodeIdAsHash = _nodeHashProvider.GetHash(_currentNodeId); + _currentNodeIdAsHash = _keyOperator.GetNodeHash(_currentNodeId); _kSize = config.KSize; _refreshInterval = config.RefreshInterval; _bootNodes = config.BootNodes; @@ -59,9 +54,10 @@ public void AddOrRefresh(TNode node) // It add to routing table and does the whole refresh logid. _nodeHealthTracker.OnIncomingMessageFrom(node); } + public void Remove(TNode node) { - _routingTable.Remove(_nodeHashProvider.GetHash(node)); + _routingTable.Remove(_keyOperator.GetNodeHash(node)); } public TNode[] GetAllAtDistance(int i) @@ -71,7 +67,7 @@ public TNode[] GetAllAtDistance(int i) private bool SameAsSelf(TNode node) { - return _nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; + return _keyOperator.GetNodeHash(node) == _currentNodeIdAsHash; } public Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) @@ -124,10 +120,10 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } }); - // TODO: Gonna need to decide a better log for this. if (_logger.IsInfo) _logger.Info($"Online bootnodes: {onlineBootNodes}"); - await LookupNodesClosest(_currentNodeIdAsKey, token); + TKey currentNodeIdAsKey = _keyOperator.GetKey(_currentNodeId); + await LookupNodesClosest(currentNodeIdAsKey, token); token.ThrowIfCancellationRequested(); @@ -149,7 +145,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false) { ValueHash256? excludeHash = null; - if (excluding != null) excludeHash = _nodeHashProvider.GetHash(excluding); + if (excluding != null) excludeHash = _keyOperator.GetNodeHash(excluding); ValueHash256 hash = _keyOperator.GetKeyHash(target); return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index ac180d7ea522..fcc16b7371ed 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -3,19 +3,24 @@ using Autofac; using Nethermind.Core; -using Nethermind.Network.Discovery.Discv4; namespace Nethermind.Network.Discovery.Kademlia; /// /// A kademlia module. -/// Application is expeccted to expose a -/// for the table maintenance to function. +/// Application is expected to expose a +/// - +/// - +/// - +/// for the table bootstrap and maintenance to function. +/// Call to start the table. /// Additionally, application is expected to call -/// and respectedly. +/// and respectedly which allow it to detect bad peer +/// from the table and add new peer as they send message. +/// Any authentication or session is handled externally. /// -/// -/// +/// Key is the type that represent the target or hash. +/// Type of the node. public class KademliaModule : Module where TNode : notnull { protected override void Load(ContainerBuilder builder) @@ -36,7 +41,7 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) - .AddSingleton() + .AddSingleton, FromKeyNodeHashProvider>() .AddSingleton>() .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); From 2d7c42c0bf618bdd6dc7220127cf3abf49b0b304 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Wed, 21 May 2025 22:03:25 +0800 Subject: [PATCH 085/182] KBucket does not need --- .../Nethermind.Network.Discovery/Kademlia/KBucketTree.cs | 2 +- .../Nethermind.Network.Discovery/Kademlia/KademliaModule.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 0801ab3df831..e787880b486c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -8,7 +8,7 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class KBucketTree : IRoutingTable where TNode : notnull +public class KBucketTree : IRoutingTable where TNode : notnull { private class TreeNode { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index fcc16b7371ed..f0bb7795eee2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -42,8 +42,8 @@ protected override void Load(ContainerBuilder builder) return provider.Resolve>(); }) .AddSingleton, FromKeyNodeHashProvider>() - .AddSingleton>() - .AddSingleton, KBucketTree>() + .AddSingleton>() + .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); } } From 0c49205b512ac884d3116c4bbebec584a20417f2 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:00:38 +0800 Subject: [PATCH 086/182] Slight cleanup --- .../DiscoveryApp.cs | 4 --- .../Discv4/DiscV4KademliaModule.cs | 32 +++-------------- .../Discv4/IKademliaDiscv4Adapter.cs | 2 +- .../Discv4/KademliaDiscv4Adapter.cs | 34 +++++++++---------- .../Discv4/NodeNodeHashProvider.cs | 30 ++++++++++++++++ .../IDiscoveryConfig.cs | 24 +++++++++++-- 6 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 3f4a0a813204..4c1de8366b22 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -2,9 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using Autofac; -using System.Net.NetworkInformation; -using System.Runtime.CompilerServices; -using System.Threading.Channels; using Autofac.Features.AttributeFilters; using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; @@ -12,7 +9,6 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index b6f09759eb2d..f6ba1a33fa6f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -4,11 +4,10 @@ using Autofac; using Nethermind.Core; using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public class DiscV4KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module { @@ -20,44 +19,21 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { - CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), + CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), // It actually only need masterNode. KSize = discoveryConfig.BucketSize, Alpha = discoveryConfig.Concurrency, Beta = discoveryConfig.BitsPerHop, LookupFindNeighbourHardTimout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), // TODO: This seems very low. - RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PongTimeout), + RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), BootNodes = bootNodes }) .AddSingleton() - .AddSingleton() .AddSingleton() .Bind() - .AddSingleton>(c => c.Resolve()) + .Bind, IKademliaDiscv4Adapter>() .AddSingleton() .AddSingleton() .AddSingleton(); } } - -public class NodeNodeHashProvider : IKeyOperator -{ - public PublicKey GetKey(Node node) - { - return node.Id; - } - - public ValueHash256 GetKeyHash(PublicKey key) - { - return key.Hash; - } - - public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) - { - // Obviously, we can't generate this. So we just randomly pick something. - // I guess we can brute force it if needed. - Span randomBytes = new byte[64]; - Random.Shared.NextBytes(randomBytes); - return new PublicKey(randomBytes); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs index 4f581e0ff21b..378251043ad1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Discv4; /// -/// Interface for the KademliaDiscv4Adapter, which handles discovery protocol v4 message processing. +/// Interfaces between and discv4. Largely handles the transport and session handling. /// public interface IKademliaDiscv4Adapter : IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index b7c1ac5f19f7..3ce999e37d56 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -28,15 +28,11 @@ public class KademliaDiscv4Adapter( ILogManager logManager ) : IKademliaDiscv4Adapter { - private readonly TimeSpan _requestEnrTimeout = TimeSpan.FromSeconds(10); + private readonly TimeSpan _requestEnrTimeout = TimeSpan.FromMilliseconds(discoveryConfig.EnrTimeout); private readonly TimeSpan _findNeighbourTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); - private readonly TimeSpan _pingTimeout = TimeSpan.FromSeconds(1); - private readonly TimeSpan _waitAfterPongDelay = TimeSpan.FromMilliseconds(500); - - /// - /// This is the value set by other clients based on real network tests. - /// - private const int ExpirationTimeInSeconds = 20; + private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); + private readonly TimeSpan _expirationTime = TimeSpan.FromMilliseconds(discoveryConfig.MessageExpiryTime); + private readonly TimeSpan _waitAfterPongDelay = TimeSpan.FromMilliseconds(discoveryConfig.BondWaitTime); private readonly ILogger _logger = logManager.GetClassLogger(); private readonly RateLimiter _outboundRateLimiter = new(discoveryConfig.MaxOutgoingMessagePerSecond); @@ -57,6 +53,7 @@ public NodeSession GetSession(Node node) private async Task EnsureOutgoingMessageBondedPeer(Node node, NodeSession nodeSession, CancellationToken token) { + // If we have received ping, then we have ponged which mean we should be bonded from their point of view if (nodeSession is { HasReceivedPing: true, NotTooManyFailure: true }) return; if (_logger.IsTrace) _logger.Trace($"Ensure session for node {node}"); @@ -150,7 +147,7 @@ private async Task SendMessage(NodeSession session, DiscoveryMsg msg, Cancellati private long CalculateExpirationTime() { - return ExpirationTimeInSeconds + timestamper.UnixTime.SecondsLong; + return (long)(_expirationTime.TotalSeconds + timestamper.UnixTime.SecondsLong); } #endregion @@ -159,15 +156,12 @@ public async Task Ping(Node receiver, CancellationToken token) { using var cts = token.CreateChildTokenSource(_pingTimeout); token = cts.Token; + NodeSession session = GetSession(receiver); PingMsg msg = new PingMsg(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address); msg.EnrSequence = nodeRecordProvider.Current.EnrSequence; // optional and does not seems to be used anywhere. - - NodeSession session = GetSession(receiver); - session.OnPingSent(); _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, session, msg, token); - session.OnPongReceived(); } @@ -229,7 +223,7 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms { // Split into two because the size of message when nodes is > 12 is larger than mtu size. await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12]), token); - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..]), token); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..16]), token); } } @@ -242,6 +236,8 @@ private async Task HandlePing(Node node, NodeSession session, PingMsg ping, Canc if (!session.HasReceivedPong) { + // If we have never received any pong, then this peer is not bonded and we should not respond to any auth request. + // Send a ping to bond the peer. await Ping(node, token); } } @@ -267,18 +263,20 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) { case MsgType.Ping: PingMsg ping = (PingMsg)msg; - if (!ValidatePingAddress(ping!)) - { - return; - } + if (!ValidatePingAddress(ping)) return; await HandlePing(node, session, ping, token); + nodeHealthTracker.Value.OnIncomingMessageFrom(node); break; case MsgType.FindNode: await HandleFindNode(node, session, (FindNodeMsg)msg, token); + nodeHealthTracker.Value.OnIncomingMessageFrom(node); break; case MsgType.EnrRequest: await HandleEnrRequest(node, session, (EnrRequestMsg)msg, token); + nodeHealthTracker.Value.OnIncomingMessageFrom(node); break; + + // Unsolicited response. case MsgType.Neighbors: case MsgType.Pong: case MsgType.EnrResponse: diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs new file mode 100644 index 000000000000..acd2c365fab4 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv4; + +public class NodeNodeHashProvider : IKeyOperator +{ + public PublicKey GetKey(Node node) + { + return node.Id; + } + + public ValueHash256 GetKeyHash(PublicKey key) + { + return key.Hash; + } + + public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) + { + // Obviously, we can't generate this. So we just randomly pick something. + // I guess we can brute force it if needed. + Span randomBytes = new byte[64]; + Random.Shared.NextBytes(randomBytes); + return new PublicKey(randomBytes); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs index ef7910abe023..a9106f17d2e4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs @@ -51,10 +51,28 @@ public interface IDiscoveryConfig : IConfig int SendNodeTimeout { get; } /// - /// Pong Timeout in ms + /// Enr request timeout in ms /// - [ConfigItem(DefaultValue = "15000")] - int PongTimeout { get; set; } + [ConfigItem(DefaultValue = "500")] + long EnrTimeout { get; set; } + + /// + /// Ping timeout in ms + /// + [ConfigItem(DefaultValue = "1000")] + long PingTimeout { get; set; } + + /// + /// Message expiry time in MS + /// + [ConfigItem(DefaultValue = "30000")] + long MessageExpiryTime { get; set; } + + /// + /// Time to wait after attempting to bond with a ping message + /// + [ConfigItem(DefaultValue = "500")] + int BondWaitTime { get; set; } /// /// Boot Node Pong Timeout in ms From 9806c3025a51666a2a966f71f0b24d277bdd448e Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:05:29 +0800 Subject: [PATCH 087/182] Fix build --- .../Nethermind.Network.Discovery/DiscoveryConfig.cs | 6 ++++-- .../Discv4/DiscV4KademliaModule.cs | 1 + .../Nethermind.Network.Discovery/IDiscoveryConfig.cs | 10 +++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs index 9961734c6ea4..fd0531cb22e0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConfig.cs @@ -18,8 +18,10 @@ public class DiscoveryConfig : IDiscoveryConfig public int EvictionCheckInterval { get; set; } = 75; public int SendNodeTimeout { get; set; } = 500; - - public int PongTimeout { get; set; } = 1000 * 5; + public long EnrTimeout { get; set; } = 1000; + public long PingTimeout { get; set; } = 1000; + public long MessageExpiryTime { get; set; } = 30000; + public int BondWaitTime { get; set; } = 500; public int BootnodePongTimeout { get; set; } = 1000 * 100; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index f6ba1a33fa6f..d4a996cb29b9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -26,6 +26,7 @@ protected override void Load(ContainerBuilder builder) LookupFindNeighbourHardTimout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), // TODO: This seems very low. RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), + RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), BootNodes = bootNodes }) .AddSingleton() diff --git a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs index a9106f17d2e4..f1b095714138 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryConfig.cs @@ -18,6 +18,7 @@ public interface IDiscoveryConfig : IConfig /// Buckets count. /// [ConfigItem(DisabledForCli = true)] + [Obsolete] int BucketsCount { get; set; } /// @@ -36,12 +37,14 @@ public interface IDiscoveryConfig : IConfig /// Max Discovery Rounds /// [ConfigItem(DefaultValue = "8")] + [Obsolete] int MaxDiscoveryRounds { get; } /// /// Eviction check interval in ms /// [ConfigItem(DefaultValue = "75")] + [Obsolete] int EvictionCheckInterval { get; } /// @@ -53,7 +56,7 @@ public interface IDiscoveryConfig : IConfig /// /// Enr request timeout in ms /// - [ConfigItem(DefaultValue = "500")] + [ConfigItem(DefaultValue = "1000")] long EnrTimeout { get; set; } /// @@ -78,12 +81,14 @@ public interface IDiscoveryConfig : IConfig /// Boot Node Pong Timeout in ms /// [ConfigItem(DefaultValue = "100000")] + [Obsolete] int BootnodePongTimeout { get; } /// /// Pong Timeout in ms /// [ConfigItem(DefaultValue = "3")] + [Obsolete] int PingRetryCount { get; } /// @@ -102,6 +107,7 @@ public interface IDiscoveryConfig : IConfig /// Time between discovery cycles in milliseconds /// [ConfigItem(DefaultValue = "50")] + [Obsolete] int DiscoveryNewCycleWaitTime { get; } /// @@ -125,9 +131,11 @@ public interface IDiscoveryConfig : IConfig /// Count of NodeLifecycleManagers to remove in one cleanup cycle /// [ConfigItem(DefaultValue = "4000")] + [Obsolete] int NodeLifecycleManagersCleanupCount { get; } [ConfigItem(DefaultValue = "0.05")] + [Obsolete] float DropFullBucketNodeProbability { get; set; } [ConfigItem(Description = "Limit number of outgoing discovery message per second.", DefaultValue = "100", HiddenFromDocs = true)] From fc91f9d424e3e05d26f8c0a2c8db58922d874fd3 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:14:53 +0800 Subject: [PATCH 088/182] Rename and comment --- .../Discv4/DiscV4KademliaModule.cs | 29 +++++++++++++------ ...ashProvider.cs => PublicKeyKeyOperator.cs} | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{NodeNodeHashProvider.cs => PublicKeyKeyOperator.cs} (92%) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index d4a996cb29b9..7c3fc7ba276f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -9,14 +9,31 @@ namespace Nethermind.Network.Discovery.Discv4; +/// +/// Specify the discv4 kademlia components. Mainly provide transport for . +/// Because kademlia can and probably will be reused outside of discv4, this module is meant to be added within a child +/// lifecycle in to prevent unexpected conflict. +/// +/// +/// public class DiscV4KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module { protected override void Load(ContainerBuilder builder) { builder - .AddModule(new KademliaModule()) - .AddSingleton, NodeNodeHashProvider>() + // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. .AddSingleton() + .AddSingleton() + + // Some transport wiring. + .AddSingleton() + .Bind() + .AddSingleton() + + // Register the main kademlia module and integration + .AddModule(new KademliaModule()) + .Bind, IKademliaDiscv4Adapter>() + .AddSingleton, PublicKeyKeyOperator>() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), // It actually only need masterNode. @@ -29,12 +46,6 @@ protected override void Load(ContainerBuilder builder) RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), BootNodes = bootNodes }) - .AddSingleton() - .AddSingleton() - .Bind() - .Bind, IKademliaDiscv4Adapter>() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + ; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs similarity index 92% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs index acd2c365fab4..aa40f0510c1b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeNodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv4; -public class NodeNodeHashProvider : IKeyOperator +public class PublicKeyKeyOperator : IKeyOperator { public PublicKey GetKey(Node node) { From a709097cc2766b2328326f4dc1702d5994cafd49 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:17:31 +0800 Subject: [PATCH 089/182] Remove original lookup code --- .../Kademlia/KademliaSimulation.cs | 14 +- .../Kademlia/KademliaConfig.cs | 5 - .../Kademlia/KademliaModule.cs | 12 +- .../OriginalLookupKNearestNeighbour.cs | 175 ------------------ 4 files changed, 7 insertions(+), 199 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index bae8ec647087..66fee9feb69e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -18,23 +18,22 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; -[TestFixture(true, 3, 0)] -[TestFixture(false, 1, 0)] -[TestFixture(false, 1, 4)] -[TestFixture(false, 3, 0)] -[TestFixture(false, 3, 4)] +[TestFixture(3, 0)] +[TestFixture(1, 0)] +[TestFixture(1, 4)] +[TestFixture(3, 0)] +[TestFixture(3, 4)] public class KademliaSimulation { private readonly KademliaConfig _config; - public KademliaSimulation(bool useOriginalLookup, int alpha, int beta) + public KademliaSimulation(int alpha, int beta) { _config = new KademliaConfig() { KSize = 20, Alpha = alpha, Beta = beta, - UseOriginalLookup = useOriginalLookup, }; } @@ -259,7 +258,6 @@ public Kademlia CreateNode(ValueHash256 nodeID) Alpha = config.Alpha, Beta = config.Beta, RefreshInterval = TimeSpan.FromHours(1), - UseOriginalLookup = config.UseOriginalLookup }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index 95012d5b0ac4..74d1d5fffb22 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -30,11 +30,6 @@ public class KademliaConfig /// public TimeSpan RefreshInterval { get; set; } = TimeSpan.FromMinutes(30); - /// - /// Use a the original lookup algorithm as in the paper. Slower. - /// - public bool UseOriginalLookup { get; set; } - /// /// The timeout for each find neighbour call lookup /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index f0bb7795eee2..419fb5d24491 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -30,17 +30,7 @@ protected override void Load(ContainerBuilder builder) builder .AddSingleton, Kademlia>() .AddSingleton>() - .AddSingleton>() - .AddSingleton>(provider => - { - KademliaConfig config = provider.Resolve>(); - if (config.UseOriginalLookup) - { - return provider.Resolve>(); - } - - return provider.Resolve>(); - }) + .AddSingleton, LookupKNearestNeighbour>() .AddSingleton, FromKeyNodeHashProvider>() .AddSingleton>() .AddSingleton, KBucketTree>() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs deleted file mode 100644 index d3b90b7a4336..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/OriginalLookupKNearestNeighbour.cs +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Diagnostics; -using Nethermind.Core.Crypto; -using Nethermind.Logging; - -namespace Nethermind.Network.Discovery.Kademlia; - -/// -/// This find nearest k query follows the kademlia paper faithfully, but does not do much parallelism. -/// -public class OriginalLookupKNearestNeighbour( - IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, - INodeHealthTracker nodeHealthTracker, - KademliaConfig config, - ILogManager logManager) : ILookupAlgo where TNode : notnull -{ - private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; - private readonly ILogger _logger = logManager.GetClassLogger>(); - - public async Task Lookup( - ValueHash256 targetHash, - int k, - Func> findNeighbourOp, - CancellationToken token - ) - { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); - - Dictionary queried = new(); - Dictionary queriedAndResponded = new(); - Dictionary seen = new(); - - IComparer comparer = Comparer.Create((h1, h2) => - Hash256XorUtils.Compare(h1, h2, targetHash)); - - // Ordered by lowest distance. Will get popped for next round. - PriorityQueue bestSeen = new(comparer); - - // Ordered by lowest distance. Will not get popped for next round, but will at final collection. - PriorityQueue bestSeenAllTime = new(comparer); - - ValueHash256 closestNodeHash = Hash256XorUtils.GetOppositeHash(targetHash); - (ValueHash256 nodeHash, TNode node)[] roundQuery = routingTable.GetKNearestNeighbour(targetHash, default) - .Take(config.Alpha) - .Select((node) => (nodeHashProvider.GetHash(node), node)) - .ToArray(); - - foreach ((ValueHash256 nodeHash, TNode node) entry in roundQuery) - { - (ValueHash256 nodeHash, TNode node) = entry; - seen.Add(nodeHash, node); - bestSeen.Enqueue(node, nodeHash); - bestSeenAllTime.Enqueue(node, nodeHash); - } - - int roundNumber = 0; - while (roundQuery.Length > 0) - { - // TODO: The paper mentioned that the next round can start immediately while waiting - // for the result of previous round. - token.ThrowIfCancellationRequested(); - - if (_logger.IsTrace) _logger.Trace($"Round {++roundNumber}"); - - foreach (var kv in roundQuery) - { - queried.TryAdd(kv.nodeHash, kv.node); - } - - (TNode NodeId, TNode[]? Neighbours)[] currentRoundResponse = await Task.WhenAll( - roundQuery.Select((hn) => WrappedFindNeighbourHop(hn.Item2))); - - bool hasCloserThanClosest = false; - foreach ((TNode NodeId, TNode[]? Neighbours) response in currentRoundResponse) - { - if (response.Neighbours == null) continue; // Timeout or failed to get response - if (_logger.IsTrace) _logger.Trace($"Received {response.Neighbours.Length} from {response.NodeId}"); - - queriedAndResponded.TryAdd(nodeHashProvider.GetHash(response.NodeId), response.NodeId); - - foreach (TNode neighbour in response.Neighbours) - { - ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); - // Already queried, we ignore - if (queried.ContainsKey(neighbourHash)) continue; - - // When seen already dont record - if (!seen.TryAdd(neighbourHash, neighbour)) continue; - - bestSeen.Enqueue(neighbour, neighbourHash); - bestSeenAllTime.Enqueue(neighbour, neighbourHash); - - if (comparer.Compare(neighbourHash, closestNodeHash) < 0) - { - hasCloserThanClosest = true; - closestNodeHash = neighbourHash; - } - } - } - - if (!hasCloserThanClosest) - { - // end condition it seems - break; - } - - int toTake = Math.Min(config.Alpha, bestSeen.Count); - roundQuery = Enumerable.Range(0, toTake).Select((_) => - { - TNode node = bestSeen.Dequeue(); - return (nodeHashProvider.GetHash(node), node); - }).ToArray(); - } - - // At this point need to query for the maxNode. - List result = []; - while (result.Count < k && bestSeenAllTime.Count > 0) - { - token.ThrowIfCancellationRequested(); - TNode nextLowest = bestSeenAllTime.Dequeue(); - ValueHash256 nextLowestHash = nodeHashProvider.GetHash(nextLowest); - - if (queriedAndResponded.ContainsKey(nextLowestHash)) - { - result.Add(nextLowest); - continue; - } - - if (queried.ContainsKey(nextLowestHash)) - { - // Queried but not responded - continue; - } - - // TODO: In parallel? - // So the paper mentioned that node that it need to query findnode for node that was not queried. - Stopwatch sw = Stopwatch.StartNew(); - (_, TNode[]? nextCandidate) = await WrappedFindNeighbourHop(nextLowest); - if (nextCandidate != null) - { - result.Add(nextLowest); - } - } - - return result.ToArray(); - - async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourHop(TNode node) - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); - cts.CancelAfter(_findNeighbourHardTimeout); - - try - { - // targetHash is implied in findNeighbourOp - var res = await findNeighbourOp(node, cts.Token); - nodeHealthTracker.OnIncomingMessageFrom(node); - return (node, res); - } - catch (OperationCanceledException) - { - nodeHealthTracker.OnRequestFailed(node); - return (node, null); - } - catch (Exception e) - { - nodeHealthTracker.OnRequestFailed(node); - _logger.Error($"Find neighbour op failed. {e}"); - return (node, null); - } - } - } -} From 58313f7d5f2067524f6f10d9f3ad2aa5fd2f45c8 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:21:52 +0800 Subject: [PATCH 090/182] Fix test --- src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs | 2 -- .../Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 249f207987cc..2f36dbe12372 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -114,8 +114,6 @@ protected override void Load(ContainerBuilder builder) .AddNetworkStorage(DbNames.DiscoveryNodes, "discoveryNodes") .AddSingleton() - - .AddSingleton() .AddSingleton() ; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 7c3fc7ba276f..02b973f498f6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -21,6 +21,8 @@ public class DiscV4KademliaModule(PublicKey masterNode, IReadOnlyList boot protected override void Load(ContainerBuilder builder) { builder + .AddSingleton() + // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. .AddSingleton() .AddSingleton() From 9ffdeba6d53dca3f4dc8956881bf190be9c6d69f Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:23:43 +0800 Subject: [PATCH 091/182] Reduce code --- .../Nethermind.Network.Discovery/Kademlia/KademliaModule.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 419fb5d24491..12acb90d9c44 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -29,10 +29,8 @@ protected override void Load(ContainerBuilder builder) builder .AddSingleton, Kademlia>() - .AddSingleton>() .AddSingleton, LookupKNearestNeighbour>() .AddSingleton, FromKeyNodeHashProvider>() - .AddSingleton>() .AddSingleton, KBucketTree>() .AddSingleton, NodeHealthTracker>(); } From d0e910ec85ebbe3184c467f19ae7a651473261aa Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:43:15 +0800 Subject: [PATCH 092/182] More xor test --- .../Kademlia/Hash256XorUtilsTests.cs | 59 +++++++++++++---- .../Kademlia/Hash256XORUtils.cs | 64 +++++-------------- .../Kademlia/KBucketTree.cs | 4 +- 3 files changed, 64 insertions(+), 63 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs index e8ba34053099..a95edc0611dc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs @@ -15,19 +15,52 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; public class Hash256XorUtilsTests { - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", 0)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 256)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0xF000000000000000000000000000000000000000000000000000000000000000", 256)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0xE000000000000000000000000000000000000000000000000000000000000000", 256)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x7000000000000000000000000000000000000000000000000000000000000000", 255)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0F00000000000000000000000000000000000000000000000000000000000000", 252)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0E00000000000000000000000000000000000000000000000000000000000000", 252)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0700000000000000000000000000000000000000000000000000000000000000", 251)] - [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x000E000000000000000000000000000000000000000000000000000000000000", 244)] - public void TestDistance(string hash1, string hash2, int expectedDistance) + + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", 0)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 256)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf000000000000000000000000000000000000000000000000000000000000000", + "0xf000000000000000000000000000000000000000000000000000000000000000", 256)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe000000000000000000000000000000000000000000000000000000000000000", + "0xe000000000000000000000000000000000000000000000000000000000000000", 256)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x7000000000000000000000000000000000000000000000000000000000000000", + "0x7000000000000000000000000000000000000000000000000000000000000000", 255)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0f00000000000000000000000000000000000000000000000000000000000000", + "0x0f00000000000000000000000000000000000000000000000000000000000000", 252)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0e00000000000000000000000000000000000000000000000000000000000000", + "0x0e00000000000000000000000000000000000000000000000000000000000000", 252)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0700000000000000000000000000000000000000000000000000000000000000", + "0x0700000000000000000000000000000000000000000000000000000000000000", 251)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x000e000000000000000000000000000000000000000000000000000000000000", + "0x000e000000000000000000000000000000000000000000000000000000000000", 244)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x000000000000000000000000000000000000000000000000000000000000000f", 4)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000f0000f", + "0x0000000000000000000000000000000000000000000000000000000000f0000f", 24)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000000000000000000000000000000000000000f000f", + "0x00000000000000000000000000000000000000000000000000000000000f000f", 20)] + [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", + "0x000000000000000000000000000000000000000000000000000000000001000f", + "0x000000000000000000000000000000000000000000000000000000000001000f", 17)] + public void TestDistance(string hash1, string hash2, string xosString, int expectedDistance) { - Hash256XorUtils.CalculateDistance(new ValueHash256(hash1), new ValueHash256(hash2)).Should().Be(expectedDistance); - Hash256XorUtils.CalculateDistance(new ValueHash256(hash2), new ValueHash256(hash1)).Should().Be(expectedDistance); + ValueHash256 xor = Hash256XorUtils.XorDistance(new ValueHash256(hash1), new ValueHash256(hash2)); + xor.ToString().Should().Be(xosString.ToLower()); + Hash256XorUtils.CalculateLogDistance(new ValueHash256(hash1), new ValueHash256(hash2)).Should().Be(expectedDistance); + Hash256XorUtils.CalculateLogDistance(new ValueHash256(hash2), new ValueHash256(hash1)).Should().Be(expectedDistance); } [Test] @@ -40,7 +73,7 @@ public void TestGetRandomHash() void TestForDistance(int distance) { var randHash = Hash256XorUtils.GetRandomHashAtDistance(randomized, distance, rand); - Hash256XorUtils.CalculateDistance(randomized, randHash).Should().Be(distance); + Hash256XorUtils.CalculateLogDistance(randomized, randHash).Should().Be(distance); } for (int i = 1; i < 256; i++) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs index 96f61a54b5cb..a831e098bab8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XORUtils.cs @@ -1,24 +1,20 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Buffers.Binary; using System.Numerics; -using System.Runtime.Intrinsics; using Nethermind.Core.Crypto; -using Nethermind.Int256; namespace Nethermind.Network.Discovery.Kademlia; public static class Hash256XorUtils { - public static int CalculateDistance(ValueHash256 h1, ValueHash256 h2) + public static int CalculateLogDistance(ValueHash256 h1, ValueHash256 h2) { + ValueHash256 xor = XorDistance(h1, h2); int zeros = 0; for (int i = 0; i < 32; i += 1) { - byte b1 = h1.Bytes[i]; - byte b2 = h2.Bytes[i]; - byte xord = (byte)(b1 ^ b2); + byte xord = xor.Bytes[i]; if (xord == 0) { zeros += 8; @@ -34,24 +30,24 @@ public static int CalculateDistance(ValueHash256 h1, ValueHash256 h2) break; } - return MaxDistance - zeros; } - public static UInt256 CalculateDistanceUInt256(ValueHash256 h1, ValueHash256 h2) - { - ValueHash256 xored = XorDistance(h1, h2); - // TODO: Make this more efficirent/simd it. - for (int i = 0; i < 32; i++) - { - xored.BytesAsSpan[i] = (byte)(h1.BytesAsSpan[i] ^ h2.BytesAsSpan[i]); - } + public static int MaxDistance => 256; - UInt256 XORed = new UInt256(xored.BytesAsSpan, true); - return XORed; + public static int Compare(ValueHash256 a, ValueHash256 b, ValueHash256 c) + { + ValueHash256 ac = XorDistance(a, c); + ValueHash256 bc = XorDistance(b, c); + return ac.CompareTo(bc); } - public static int MaxDistance => 256; + public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) + { + ValueHash256 bc = new ValueHash256(); + (new Vector(hash1.BytesAsSpan) ^ new Vector(hash2.BytesAsSpan)).CopyTo(bc.BytesAsSpan); + return bc; + } public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int distance) { @@ -71,18 +67,7 @@ public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int return CopyForRandom(currentHash, randomized, MaxDistance - distance); } - public static int Compare(ValueHash256 a, ValueHash256 b, ValueHash256 c) - { - ValueHash256 ac = new ValueHash256(); - (new Vector(a.BytesAsSpan) ^ new Vector(c.BytesAsSpan)).CopyTo(ac.BytesAsSpan); - - ValueHash256 bc = new ValueHash256(); - (new Vector(b.BytesAsSpan) ^ new Vector(c.BytesAsSpan)).CopyTo(bc.BytesAsSpan); - - return ac.CompareTo(bc); - } - - public static ValueHash256 CopyForRandom(ValueHash256 currentHash, ValueHash256 randomizedHash, int distance) + private static ValueHash256 CopyForRandom(ValueHash256 currentHash, ValueHash256 randomizedHash, int distance) { if (distance >= 256) return currentHash; @@ -111,21 +96,4 @@ public static ValueHash256 CopyForRandom(ValueHash256 currentHash, ValueHash256 return randomizedHash; } - - public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) - { - byte[] xorBytes = new byte[hash1.Bytes.Length]; - for (int i = 0; i < xorBytes.Length; i++) - { - xorBytes[i] = (byte)(hash1.Bytes[i] ^ hash2.Bytes[i]); - } - return new ValueHash256(xorBytes); - } - - public static ValueHash256 GetOppositeHash(ValueHash256 hash) - { - ValueHash256 opposite = new ValueHash256(); - (~(new Vector(hash.BytesAsSpan))).CopyTo(opposite.BytesAsSpan); - return opposite; - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index e787880b486c..3d95ce1a9b18 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -52,7 +52,7 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 nodeHash, TNode node, out TreeNode current = _root; // As in, what would be the depth of the node assuming all branch on the traversal is populated. - int logDistance = Hash256XorUtils.MaxDistance - Hash256XorUtils.CalculateDistance(_currentNodeHash, nodeHash); + int logDistance = Hash256XorUtils.MaxDistance - Hash256XorUtils.CalculateLogDistance(_currentNodeHash, nodeHash); int depth = 0; while (true) { @@ -172,7 +172,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L if (depth <= targetDepth) { result.AddRange(node.Bucket.GetAllWithHash() - .Where(kv => Hash256XorUtils.CalculateDistance(kv.Item1, _currentNodeHash) == distance) + .Where(kv => Hash256XorUtils.CalculateLogDistance(kv.Item1, _currentNodeHash) == distance) .Select(kv => kv.Item2)); } else From 6d1c6d8351e2647ae2153c523b21b30fa8b54748 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 22 May 2025 09:45:42 +0800 Subject: [PATCH 093/182] Whitespace --- src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs | 3 ++- .../Kademlia/FromKeyNodeHashProvider.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 4c1de8366b22..0cffb66daa6c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -81,7 +81,8 @@ private record DiscV4Services( IKademliaDiscv4Adapter Discv4Adapter, IKademlia Kademlia, Func NettyDiscoveryHandlerFactory - ) { + ) + { } public Task StartAsync() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs index d5cba4f86ebf..ff9c01de1330 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs @@ -5,7 +5,7 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class FromKeyNodeHashProvider(IKeyOperator keyOperator): INodeHashProvider +public class FromKeyNodeHashProvider(IKeyOperator keyOperator) : INodeHashProvider { public ValueHash256 GetHash(TNode node) => keyOperator.GetNodeHash(node); } From 0d9509c90d7ab3132e38929a5b147d45ba9f789a Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Fri, 4 Jul 2025 15:16:39 +0800 Subject: [PATCH 094/182] Make the node lookup generic --- .../DiscoveryApp.cs | 1 + .../Discv4/DiscV4KademliaModule.cs | 1 - .../Discv4/IIteratorNodeLookup.cs | 12 ----- .../Discv4/KademliaNodeSource.cs | 42 ++++++--------- .../Kademlia/IIteratorNodeLookup.cs | 9 ++++ .../IteratorNodeLookup.cs | 52 +++++++++---------- .../Kademlia/KademliaModule.cs | 2 + 7 files changed, 53 insertions(+), 66 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs rename src/Nethermind/Nethermind.Network.Discovery/{Discv4 => Kademlia}/IteratorNodeLookup.cs (75%) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 00a77a6ae809..e99972f4a6d4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -8,6 +8,7 @@ using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Core.ServiceStopper; using Nethermind.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 02b973f498f6..31b8c54a0631 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -25,7 +25,6 @@ protected override void Load(ContainerBuilder builder) // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. .AddSingleton() - .AddSingleton() // Some transport wiring. .AddSingleton() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs deleted file mode 100644 index 925edd8c4430..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IIteratorNodeLookup.cs +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Discv4; - -public interface IIteratorNodeLookup -{ - IAsyncEnumerable Lookup(PublicKey target, CancellationToken token); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 0e3e0bf97b28..3bbe3163dc9d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -12,27 +12,15 @@ namespace Nethermind.Network.Discovery.Discv4; -public class KademliaNodeSource : IKademliaNodeSource +public class KademliaNodeSource( + IKademlia kademlia, + IIteratorNodeLookup lookup2, + IKademliaDiscv4Adapter discv4Adapter, + IDiscoveryConfig discoveryConfig, + ILogManager logManager) + : IKademliaNodeSource { - private readonly IKademlia _kademlia; - private readonly IIteratorNodeLookup _lookup; - private readonly IKademliaDiscv4Adapter _discv4Adapter; - private readonly IDiscoveryConfig _discoveryConfig; - private readonly ILogger _logger; - - public KademliaNodeSource( - IKademlia kademlia, - IIteratorNodeLookup lookup2, - IKademliaDiscv4Adapter discv4Adapter, - IDiscoveryConfig discoveryConfig, - ILogManager logManager) - { - _kademlia = kademlia; - _lookup = lookup2; - _discv4Adapter = discv4Adapter; - _discoveryConfig = discoveryConfig; - _logger = logManager.GetClassLogger(); - } + private readonly ILogger _logger = logManager.GetClassLogger(); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { @@ -48,18 +36,18 @@ async Task DiscoverAsync(PublicKey target) bool anyFound = false; int count = 0; - await foreach (var node in _lookup.Lookup(target, token)) + await foreach (var node in lookup2.Lookup(target, token)) { - if (!_discv4Adapter.GetSession(node).HasReceivedPong) + if (!discv4Adapter.GetSession(node).HasReceivedPong) { - if (_discv4Adapter.GetSession(node).HasTriedPingRecently) + if (discv4Adapter.GetSession(node).HasTriedPingRecently) { // Tried ping before and did not receive a response continue; } try { - await _discv4Adapter.Ping(node, token); + await discv4Adapter.Ping(node, token); } catch (OperationCanceledException) { @@ -88,7 +76,7 @@ async Task DiscoverAsync(PublicKey target) } } - Task discoverTask = Task.WhenAll(Enumerable.Range(0, _discoveryConfig.ConcurrentDiscoveryJob).Select((_) => Task.Run(async () => + Task discoverTask = Task.WhenAll(Enumerable.Range(0, discoveryConfig.ConcurrentDiscoveryJob).Select((_) => Task.Run(async () => { Random random = new(); byte[] randomBytes = new byte[64]; @@ -120,7 +108,7 @@ async Task DiscoverAsync(PublicKey target) try { - _kademlia.OnNodeAdded += Handler; + kademlia.OnNodeAdded += Handler; await foreach (Node node in ch.Reader.ReadAllAsync(token)) { @@ -130,7 +118,7 @@ async Task DiscoverAsync(PublicKey target) finally { await discoverTask; - _kademlia.OnNodeAdded -= Handler; + kademlia.OnNodeAdded -= Handler; } yield break; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs new file mode 100644 index 000000000000..c23bfe490615 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia; + +public interface IIteratorNodeLookup +{ + IAsyncEnumerable Lookup(TKey target, CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs similarity index 75% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs index 9a82d4a85712..a551afc0fd1a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs @@ -7,26 +7,26 @@ using Nethermind.Core.Extensions; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; -using Nethermind.Stats.Model; using NonBlocking; namespace Nethermind.Network.Discovery.Discv4; /// -/// Special lookup made specially for DiscV4 as the standard lookup is too slow or unnecessarily parallelized. +/// Special lookup made specially for node discovery as the standard lookup is too slow or unnecessarily parallelized. /// Instead of returning k closest node, it just returns the nodes that it found along the way and stopped early. /// This is useful for node discovery as trying to get the k closest node is not completely necessary, as the main goal /// is to reach all node. The lookup is not parallelized as it is expected to be parallelized at a higher level with /// each worker having different target to look into. /// -public class IteratorNodeLookup( - IRoutingTable routingTable, - KademliaConfig kademliaConfig, - IKademliaDiscv4Adapter discv4Adapter, - ILogManager logManager) : IIteratorNodeLookup +public class IteratorNodeLookup( + IRoutingTable routingTable, + KademliaConfig kademliaConfig, + IKademliaMessageSender msgSender, + IKeyOperator keyOperator, + ILogManager logManager) : IIteratorNodeLookup where TNode : notnull { - private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly ValueHash256 _currentNodeIdAsHash = kademliaConfig.CurrentNodeId.IdHash; + private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ValueHash256 _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); // Small lru of unreachable nodes, prevent retrying. Pretty effective, although does not improve discovery overall. private readonly LruCache _unreacheableNodes = new(256, ""); @@ -39,27 +39,27 @@ public class IteratorNodeLookup( private const int MaxNonProgressingRound = 3; private const int MinResult = 128; - private bool SameAsSelf(Node node) + private bool SameAsSelf(TNode node) { - return node.IdHash == _currentNodeIdAsHash; + return keyOperator.GetNodeHash(node) == _currentNodeIdAsHash; } - public async IAsyncEnumerable Lookup(PublicKey target, [EnumeratorCancellation] CancellationToken token) + public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation] CancellationToken token) { - ValueHash256 targetHash = target.Hash; + ValueHash256 targetHash = keyOperator.GetKeyHash(target); if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using var cts = token.CreateChildTokenSource(); token = cts.Token; - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(ValueHash256, Node), ValueHash256> queryQueue = new(comparer); + PriorityQueue<(ValueHash256, TNode), ValueHash256> queryQueue = new(comparer); // Used to determine if the worker should stop ValueHash256 bestNodeId = ValueKeccak.Zero; @@ -68,9 +68,9 @@ public async IAsyncEnumerable Lookup(PublicKey target, [EnumeratorCancella int totalResult = 0; // Check internal table first - foreach (Node node in routingTable.GetKNearestNeighbour(targetHash, null)) + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, null)) { - ValueHash256 nodeHash = node.IdHash; + ValueHash256 nodeHash = keyOperator.GetNodeHash(node); seen.TryAdd(nodeHash, node); queryQueue.Enqueue((nodeHash, node), nodeHash); @@ -86,7 +86,7 @@ public async IAsyncEnumerable Lookup(PublicKey target, [EnumeratorCancella while (true) { token.ThrowIfCancellationRequested(); - if (!queryQueue.TryDequeue(out (ValueHash256 hash, Node node) toQuery, out ValueHash256 hash256)) + if (!queryQueue.TryDequeue(out (ValueHash256 hash, TNode node) toQuery, out ValueHash256 hash256)) { // No node to query and running query. if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); @@ -98,7 +98,7 @@ public async IAsyncEnumerable Lookup(PublicKey target, [EnumeratorCancella queried.TryAdd(toQuery.hash, toQuery.node); if (_logger.IsTrace) _logger.Trace($"Query {toQuery.node} at round {currentRound}"); - Node[]? neighbours = await FindNeighbour(toQuery.node, target, token); + TNode[]? neighbours = await FindNeighbour(toQuery.node, target, token); if (neighbours == null || neighbours?.Length == 0) { if (_logger.IsTrace) _logger.Trace("Empty result"); @@ -107,9 +107,9 @@ public async IAsyncEnumerable Lookup(PublicKey target, [EnumeratorCancella int queryIgnored = 0; int seenIgnored = 0; - foreach (Node neighbour in neighbours!) + foreach (TNode neighbour in neighbours!) { - ValueHash256 neighbourHash = neighbour.IdHash; + ValueHash256 neighbourHash = keyOperator.GetNodeHash(neighbour); // Already queried, we ignore if (queried.ContainsKey(neighbourHash)) @@ -178,21 +178,21 @@ bool ShouldStop() } } - async Task FindNeighbour(Node node, PublicKey target, CancellationToken token) + async Task FindNeighbour(TNode node, TKey target, CancellationToken token) { try { - if (_unreacheableNodes.TryGet(node.IdHash, out var lastAttempt) && + if (_unreacheableNodes.TryGet(keyOperator.GetNodeHash(node), out var lastAttempt) && lastAttempt + TimeSpan.FromMinutes(5) > DateTimeOffset.Now) { return []; } - return await discv4Adapter.FindNeighbours(node, target, token); + return await msgSender.FindNeighbours(node, target, token); } catch (OperationCanceledException) { - _unreacheableNodes.Set(node.IdHash, DateTimeOffset.Now); + _unreacheableNodes.Set(keyOperator.GetNodeHash(node), DateTimeOffset.Now); return null; } catch (Exception e) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 12acb90d9c44..d8374789a68f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -3,6 +3,7 @@ using Autofac; using Nethermind.Core; +using Nethermind.Network.Discovery.Discv4; namespace Nethermind.Network.Discovery.Kademlia; @@ -32,6 +33,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton, LookupKNearestNeighbour>() .AddSingleton, FromKeyNodeHashProvider>() .AddSingleton, KBucketTree>() + .AddSingleton, IteratorNodeLookup>() .AddSingleton, NodeHealthTracker>(); } } From 32922dd9469eda279b3779f68c1db3e2c9e87e2c Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Fri, 4 Jul 2025 15:18:01 +0800 Subject: [PATCH 095/182] Address comment --- .../Kademlia/KademliaTests.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 8a23cd413fe4..8e30bef82925 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -20,20 +20,16 @@ public class KademliaTests { private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); - private Kademlia CreateKad(KademliaConfig config) - { - var builder = new ContainerBuilder(); - builder + private Kademlia CreateKad(KademliaConfig config) => + new ContainerBuilder() .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Trace)) .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) - .AddSingleton>(); - - var container = builder.Build(); - return container.Resolve>(); - } + .AddSingleton>() + .Build() + .Resolve>(); [Test] public void TestNewNodeAdded() From f013df688886869842f3f11b6cfc7c4b9d5c37ce Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Fri, 4 Jul 2025 20:04:44 +0800 Subject: [PATCH 096/182] Remove strange --- .../Kademlia/Hash256XorUtilsTests.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs index a95edc0611dc..83249ce2c88e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs @@ -96,23 +96,4 @@ public void TestDistanceCompare() Hash256XorUtils.Compare(h1, h2, h3).Should().BeLessThan(0); } - - [TestCase] - public void Strange() - { - ValueHash256 a = new ValueHash256("0x1a0c466f5d75e4d8ad6765d5f519dbc82b7c343b37f88500ec5e64005393b30d"); - ValueHash256 b = new ValueHash256("0x82bf3eb6be6c2d15511b0dc6c68c97bad52b834b11656c6104af44123e565a3d"); - - Vector aBig = new Vector(a.BytesAsSpan); - Vector bBig = new Vector(b.BytesAsSpan); - - ValueHash256 xored = new ValueHash256(); - (aBig ^ bBig).CopyTo(xored.BytesAsSpan); - - Console.Error.WriteLine($"The three {a} {b} {xored}"); - - // Hash256DistanceCalculator calculator = new Hash256DistanceCalculator(); - // Console.Error.WriteLine($"Distance {calculator.CalculateDistance(a, b)} {calculator.BigIntLogDist(a, b)}"); - // Console.Error.WriteLine($"Distanceb {calculator.BigIntDist(a, b)} "); - } } From 0335ea5ac41f7f85e0cf543a59f21f515a0e47c5 Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Fri, 4 Jul 2025 20:15:18 +0800 Subject: [PATCH 097/182] Fix build --- .../Discv4/IteratorNodeLookupTests.cs | 45 +++++++++---------- .../Discv4/KademliaNodeSourceTests.cs | 4 +- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index b7e16ce29737..d881f45aa344 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -23,8 +23,8 @@ namespace Nethermind.Network.Discovery.Test.Discv4 public class IteratorNodeLookupTests { private IRoutingTable _routingTable = null!; - private IKademliaDiscv4Adapter _discv4Adapter = null!; - private IteratorNodeLookup _lookup = null!; + private IteratorNodeLookup _lookup = null!; + private IKademliaMessageSender _msgSender = null!; private Node _currentNode = null!; private PublicKey _targetKey = null!; @@ -36,16 +36,15 @@ public void Setup() _routingTable = Substitute.For>(); KademliaConfig kademliaConfig = new KademliaConfig { CurrentNodeId = _currentNode }; - _discv4Adapter = Substitute.For(); + _msgSender = Substitute.For>(); ILogManager logManager = Substitute.For(); - _lookup = new IteratorNodeLookup(_routingTable, kademliaConfig, _discv4Adapter, logManager); - } - - [TearDown] - public async Task TearDown() - { - await _discv4Adapter.DisposeAsync(); + _lookup = new IteratorNodeLookup( + _routingTable, + kademliaConfig, + _msgSender, + new PublicKeyKeyOperator(), + logManager); } [Test] @@ -79,7 +78,7 @@ public async Task Lookup_should_query_nodes_and_return_neighbours(CancellationTo _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns([initialNode]); - _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) .Returns([neighbourNode]); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); @@ -88,7 +87,7 @@ public async Task Lookup_should_query_nodes_and_return_neighbours(CancellationTo result.Should().Contain(initialNode); result.Should().Contain(neighbourNode); - await _discv4Adapter.Received(1).FindNeighbours( + await _msgSender.Received(1).FindNeighbours( Arg.Is(n => n == initialNode), Arg.Is(k => k == _targetKey), Arg.Any()); @@ -106,7 +105,7 @@ public async Task Lookup_should_not_query_self_node(CancellationToken token) result.Should().HaveCount(1); result.Should().Contain(_currentNode); - await _discv4Adapter.DidNotReceive().FindNeighbours( + await _msgSender.DidNotReceive().FindNeighbours( Arg.Any(), Arg.Any(), Arg.Any()); @@ -121,7 +120,7 @@ public async Task Lookup_should_handle_empty_neighbour_response(CancellationToke _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns([initialNode]); - _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) .Returns([]); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); @@ -129,7 +128,7 @@ public async Task Lookup_should_handle_empty_neighbour_response(CancellationToke result.Should().HaveCount(1); result.Should().Contain(initialNode); - await _discv4Adapter.Received(1).FindNeighbours( + await _msgSender.Received(1).FindNeighbours( Arg.Is(n => n == initialNode), Arg.Is(k => k == _targetKey), Arg.Any()); @@ -144,7 +143,7 @@ public async Task Lookup_should_handle_exception_in_find_neighbours(Cancellation _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns(new[] { initialNode }); - _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) .Returns(Task.FromException(new Exception("Test exception"))); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); @@ -152,7 +151,7 @@ public async Task Lookup_should_handle_exception_in_find_neighbours(Cancellation result.Should().HaveCount(1); result.Should().Contain(initialNode); - await _discv4Adapter.Received(1).FindNeighbours( + await _msgSender.Received(1).FindNeighbours( Arg.Is(n => n == initialNode), Arg.Is(k => k == _targetKey), Arg.Any()); @@ -184,10 +183,10 @@ public async Task Lookup_should_not_query_same_node_twice(CancellationToken toke _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns([initialNode]); - _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) .Returns([neighbourNode]); - _discv4Adapter.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) .Returns([initialNode]); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); @@ -196,12 +195,12 @@ public async Task Lookup_should_not_query_same_node_twice(CancellationToken toke result.Should().Contain(initialNode); result.Should().Contain(neighbourNode); - await _discv4Adapter.Received(1).FindNeighbours( + await _msgSender.Received(1).FindNeighbours( Arg.Is(n => n == initialNode), Arg.Is(k => k == _targetKey), Arg.Any()); - await _discv4Adapter.Received(1).FindNeighbours( + await _msgSender.Received(1).FindNeighbours( Arg.Is(n => n == neighbourNode), Arg.Is(k => k == _targetKey), Arg.Any()); @@ -217,10 +216,10 @@ public async Task Lookup_should_not_return_duplicate_nodes(CancellationToken tok _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns([initialNode]); - _discv4Adapter.FindNeighbours(initialNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) .Returns([neighbourNode]); - _discv4Adapter.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) + _msgSender.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) .Returns([initialNode, neighbourNode]); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs index 93d83095c772..0816f08ac238 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -26,7 +26,7 @@ namespace Nethermind.Network.Discovery.Test.Discv4 public class KademliaNodeSourceTests { private IKademlia _kademlia = null!; - private IIteratorNodeLookup _lookup = null!; + private IIteratorNodeLookup _lookup = null!; private IKademliaDiscv4Adapter _discv4Adapter = null!; private KademliaNodeSource _nodeSource = null!; private NodeSession _nodeSession = null!; @@ -38,7 +38,7 @@ public class KademliaNodeSourceTests public void Setup() { _kademlia = Substitute.For>(); - _lookup = Substitute.For(); + _lookup = Substitute.For>(); _discv4Adapter = Substitute.For(); _discoveryConfig = new DiscoveryConfig From 35fdd3aa91b1a1f717f0f9b825908df0394f348c Mon Sep 17 00:00:00 2001 From: Aliaksei Osipau Date: Tue, 19 May 2026 15:22:33 +0300 Subject: [PATCH 098/182] Some fixes (#11654) --- .../Modules/DiscoveryModule.cs | 1 - .../DiscoveryPersistenceManagerTests.cs | 20 ++++ .../Discv4/KademliaDiscv4AdapterTests.cs | 37 +++++- .../Discv4/NeighbourMsgHandlerTests.cs | 1 - .../Kademlia/Hash256XorUtilsTests.cs | 4 - .../Kademlia/KBucketTests.cs | 23 ++-- .../Kademlia/NodeHealthTrackerTests.cs | 109 ++++++++++++++++++ .../CompositeDiscoveryApp.cs | 22 ++++ .../DiscoveryPersistenceManager.cs | 3 +- .../Discv4/KademliaDiscv4Adapter.cs | 30 ++++- .../Kademlia/DoubleEndedLru.cs | 4 +- .../Kademlia/Hash256XorUtils.cs | 16 ++- .../Kademlia/KBucket.cs | 4 +- .../Kademlia/KBucketTree.cs | 6 +- .../Kademlia/Kademlia.cs | 2 +- .../Kademlia/NodeHealthTracker.cs | 5 +- src/Nethermind/Nethermind.Network/PeerPool.cs | 3 +- .../Discovery/XdcDiscoveryApp.cs | 2 - 18 files changed, 254 insertions(+), 38 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index d8d732a5dad2..fbb5c369fdce 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -4,7 +4,6 @@ using Autofac; using Autofac.Features.AttributeFilters; using Nethermind.Api; -using Nethermind.Config; using Nethermind.Core; using Nethermind.Crypto; using Nethermind.Db; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index 21ee49910f9e..eb88f42dab97 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -108,6 +108,26 @@ public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions(CancellationTo await _persistenceManager.LoadPersistedNodes(cancellationToken); } + [Test] + [CancelAfter(10000)] + public async Task AddPersistedNodes_Should_Restore_Reputation(CancellationToken cancellationToken) + { + const int reputation = 123; + NetworkNode networkNode = new(TestItem.PublicKeyA, "192.168.1.1", 30303, reputation); + INodeStats nodeStats = Substitute.For(); + + _networkStorage.UpdateNodes([networkNode]); + _nodeStatsManager.GetOrAdd(Arg.Any()).Returns(nodeStats); + + await _persistenceManager.LoadPersistedNodes(cancellationToken); + + _nodeStatsManager.Received(1).GetOrAdd(Arg.Is(n => + n.Id.Equals(networkNode.NodeId) && + n.Host == networkNode.Host && + n.Port == networkNode.Port)); + nodeStats.Received(1).CurrentPersistedNodeReputation = reputation; + } + [Test] public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 2aec12b57218..441517ccdd52 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -19,7 +19,6 @@ using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; -using Nethermind.Specs; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; @@ -82,6 +81,7 @@ public void Setup() _timestamper = Substitute.For(); _timestamper.UnixTime.Returns(new UnixTime(new(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); _msgSender = Substitute.For(); + _msgSender.SendMsg(Arg.Any()).Returns(Task.CompletedTask); _receiver = new(TestItem.PublicKeyB, "192.168.1.2", 30303); SerializationBuilder builder = new(); @@ -90,6 +90,8 @@ public void Setup() INodeRecordProvider nodeRecordProvider = Substitute.For(); nodeRecordProvider.Current.Returns(_selfNodeRecord); + INodeStatsManager nodeStatsManager = Substitute.For(); + nodeStatsManager.GetOrAdd(Arg.Any()).Returns(Substitute.For()); _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), @@ -97,7 +99,7 @@ public void Setup() new DiscoveryConfig(), _kademliaConfig, nodeRecordProvider, - Substitute.For(), + nodeStatsManager, _timestamper, Substitute.For(), _logManager @@ -215,10 +217,41 @@ public async Task SendEnrRequest_should_ping_then_enr_request_and_return_respons Assert.That(result.NodeRecord.GetHex(), Is.EqualTo(_selfNodeRecord.GetHex())); } + [Test] + [CancelAfter(10000)] + public async Task Timed_out_response_handler_should_not_consume_later_unsolicited_message(CancellationToken token) + { + ConfigureBondCallback(); + + PingMsg pingMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address); + pingMsg.FarAddress = _receiver.Address; + pingMsg = AddReceiverFarAddress(pingMsg); + await _adapter.OnIncomingMsg(pingMsg); + + using CancellationTokenSource requestTimeout = CancellationTokenSource.CreateLinkedTokenSource(token); + requestTimeout.CancelAfter(50); + + Assert.ThrowsAsync(Is.InstanceOf(), async () => await _adapter.SendEnrRequest(_receiver, requestTimeout.Token)); + + _nodeHealthTracker.ClearReceivedCalls(); + + EnrResponseMsg response = new( + _receiver.Address, + _selfNodeRecord, + new(new byte[32])); + response = AddReceiverFarAddress(response); + + await _adapter.OnIncomingMsg(response); + + _nodeHealthTracker.DidNotReceive().OnIncomingMessageFrom(Arg.Is(n => n.Id.Equals(_receiver.Id))); + } + [Test] [CancelAfter(10000)] public async Task OnIncomingMsg_ping_should_respond_with_pong(CancellationToken token) { + ConfigureBondCallback(); + PingMsg pingMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _kademliaConfig.CurrentNodeId.Address); pingMsg.FarAddress = _receiver.Address; pingMsg = AddReceiverFarAddress(pingMsg); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs index cb00d5fa1e21..ffc3ad57160c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Linq; using System.Net; using System.Threading.Tasks; using Nethermind.Core.Crypto; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs index 78f0a496fee1..e88cdf643c14 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs @@ -2,10 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.Intrinsics; using Nethermind.Core.Crypto; using Nethermind.Network.Discovery.Kademlia; using NUnit.Framework; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index 86d600fa824b..ed4ece474a3f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -1,14 +1,9 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; using Nethermind.Core.Crypto; using Nethermind.Network.Discovery.Kademlia; -using NSubstitute; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -33,13 +28,13 @@ public void TryAddOrRefresh_ShouldLimitToK() bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); } - bucket.GetAll().ToHashSet().Should().BeEquivalentTo(toAdd[..5].ToHashSet()); - bucket.GetAllWithHash().Select(it => it.Item2).ToHashSet().Should().BeEquivalentTo(toAdd[..5].ToHashSet()); + Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(toAdd[..5].ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (it, it)).ToHashSet())); foreach (ValueHash256 valueHash256 in toAdd[..5]) { - bucket.ContainsNode(valueHash256).Should().BeTrue(); - bucket.GetByHash(valueHash256).Should().NotBeNull(); + Assert.That(bucket.ContainsNode(valueHash256), Is.True); + Assert.That(bucket.GetByHash(valueHash256), Is.EqualTo(valueHash256)); } } @@ -62,11 +57,11 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); } - bucket.GetAll().Should().BeSameAs(nodes); + Assert.That(bucket.GetAll(), Is.SameAs(nodes)); } [Test] - public void RemoteAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() + public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() { KBucket bucket = new(5); @@ -79,8 +74,8 @@ public void RemoteAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() bucket.RemoveAndReplace(toAdd[0]); - bucket.GetAll().ToHashSet() - .Should() - .BeEquivalentTo((toAdd[1..5].Concat(toAdd[9..10])).ToHashSet()); + ValueHash256[] expected = [.. toAdd[1..5], toAdd[9]]; + Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(expected.ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (it, it)).ToHashSet())); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs new file mode 100644 index 000000000000..8f5bb3e4bf30 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class NodeHealthTrackerTests +{ + [Test] + public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSelectsSelf() + { + const string self = "self"; + const string remote = "remote"; + RoutingTableStub routingTable = new() { ToRefresh = self }; + NodeHealthTracker tracker = new( + new KademliaConfig { CurrentNodeId = self }, + routingTable, + new StringNodeHashProvider(), + Substitute.For>(), + LimboLogs.Instance); + + tracker.OnIncomingMessageFrom(remote); + + Assert.That(routingTable.AddCalls, Has.Count.EqualTo(2)); + Assert.That(routingTable.AddCalls[1].Hash, Is.EqualTo(ValueKeccak.Compute(self))); + Assert.That(routingTable.AddCalls[1].Node, Is.EqualTo(self)); + } + + [Test] + public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() + { + const string self = "self"; + const string remote = "remote"; + RoutingTableStub routingTable = new(); + NodeHealthTracker tracker = new( + new KademliaConfig { CurrentNodeId = self, NodeRequestFailureThreshold = 1 }, + routingTable, + new StringNodeHashProvider(), + Substitute.For>(), + LimboLogs.Instance); + + tracker.OnRequestFailed(remote); + tracker.OnRequestFailed(remote); + tracker.OnRequestFailed(remote); + + Assert.That(routingTable.RemoveCalls, Has.Count.EqualTo(1)); + Assert.That(routingTable.RemoveCalls[0], Is.EqualTo(ValueKeccak.Compute(remote))); + } + + private sealed class StringNodeHashProvider : INodeHashProvider + { + public ValueHash256 GetHash(string node) => ValueKeccak.Compute(node); + } + + private sealed class RoutingTableStub : IRoutingTable + { + public string ToRefresh { get; init; } = string.Empty; + + public List<(ValueHash256 Hash, string Node)> AddCalls { get; } = []; + + public List RemoveCalls { get; } = []; + + public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, string item, out string? toRefresh) + { + AddCalls.Add((hash, item)); + if (AddCalls.Count == 1) + { + toRefresh = ToRefresh; + return BucketAddResult.Full; + } + + toRefresh = null; + return BucketAddResult.Refreshed; + } + + public bool Remove(in ValueHash256 hash) + { + RemoveCalls.Add(hash); + return true; + } + + public string[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude = null, bool excludeSelf = false) => + throw new NotSupportedException(); + + public string[] GetAllAtDistance(int i) => throw new NotSupportedException(); + + public IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() => + throw new NotSupportedException(); + + public string? GetByHash(ValueHash256 nodeId) => throw new NotSupportedException(); + + public void LogDebugInfo() => throw new NotSupportedException(); + + public event EventHandler? OnNodeAdded + { + add { } + remove { } + } + + public int Size => AddCalls.Count; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index 24d95a897a6d..a76f18a70356 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -26,6 +26,7 @@ public class CompositeDiscoveryApp : IDiscoveryApp private readonly IChannelFactory? _channelFactory; private readonly IDiscoveryApp[] _discoveryApps; private readonly CompositeNodeSource _compositeNodeSource; + private readonly ILogger _logger; public CompositeDiscoveryApp( INetworkConfig networkConfig, @@ -39,6 +40,7 @@ public CompositeDiscoveryApp( _networkConfig = networkConfig; _connections = new DiscoveryConnectionsPool(logManager.GetClassLogger(), _networkConfig, discoveryConfig); _channelFactory = channelFactory; + _logger = logManager.GetClassLogger(); List discoveryApps = new(2); @@ -92,6 +94,7 @@ public async Task StopAsync() finally { _compositeNodeSource.Dispose(); + await DisposeDiscoveryApps(); } } @@ -127,6 +130,25 @@ private Task WhenAllDiscoveryApps(Func action) return result; } + private async Task DisposeDiscoveryApps() + { + IDiscoveryApp[] discoveryApps = _discoveryApps; + for (int i = 0; i < discoveryApps.Length; i++) + { + if (discoveryApps[i] is IAsyncDisposable asyncDisposable) + { + try + { + await asyncDisposable.DisposeAsync(); + } + catch (Exception e) + { + if (_logger.IsWarn) _logger.Warn($"Error disposing discovery app {discoveryApps[i]}: {e}"); + } + } + } + } + public IAsyncEnumerable DiscoverNodes(CancellationToken cancellationToken) => _compositeNodeSource.DiscoverNodes(cancellationToken); public event EventHandler? NodeRemoved diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs index 96d0c472364e..f53d026d2e04 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -71,7 +71,8 @@ public async Task LoadPersistedNodes(CancellationToken cancellationToken) try { - // If when it receive Pong, it should automatically add to routing table if not full. + // Reputation must be set before Ping so the routing table has the correct reputation when the Pong is received. + _nodeStatsManager.GetOrAdd(node).CurrentPersistedNodeReputation = networkNode.Reputation; await _discv4Adapter.Ping(node, cancellationToken); } catch (OperationCanceledException) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 274672eac6ba..877cae2a25e4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -94,12 +94,36 @@ private void RemoveMessageHandler( MsgType msgType, ValueHash256 nodeId, IMessageHandler handler) { (ValueHash256 nodeId, MsgType msgType) key = (nodeId, msgType); - if (_incomingMessageHandlers.TryRemove(new KeyValuePair<(ValueHash256, MsgType), IMessageHandler[]>(key, [handler]))) return; while (true) { if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? current)) return; - IMessageHandler[] newValue = [.. current.Where((it) => it != handler)]; + + int newLength = 0; + for (int i = 0; i < current.Length; i++) + { + if (!ReferenceEquals(current[i], handler)) newLength++; + } + + if (newLength == current.Length) return; + + if (newLength == 0) + { + if (_incomingMessageHandlers.TryRemove(new KeyValuePair<(ValueHash256, MsgType), IMessageHandler[]>(key, current))) return; + continue; + } + + IMessageHandler[] newValue = new IMessageHandler[newLength]; + int newIndex = 0; + for (int i = 0; i < current.Length; i++) + { + IMessageHandler currentHandler = current[i]; + if (!ReferenceEquals(currentHandler, handler)) + { + newValue[newIndex++] = currentHandler; + } + } + if (_incomingMessageHandlers.TryUpdate(key, newValue, current)) return; } } @@ -317,7 +341,7 @@ private bool HandleViaMessageHandlers(Node node, DiscoveryMsg msg) } } - return true; + return false; } public ValueTask DisposeAsync() => ValueTask.CompletedTask; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs index 0bd99ed7fc12..bde5859e7240 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs @@ -40,18 +40,20 @@ public BucketAddResult AddOrRefresh(in ValueHash256 hash, TNode node) return BucketAddResult.Full; } - public bool TryPopHead(out TNode? node) + public bool TryPopHead(out ValueHash256 hash, out TNode? node) { using McsLock.Disposable _ = _lock.Acquire(); LinkedListNode<(ValueHash256, TNode)>? front = _queue.First; if (front == null) { + hash = default; node = default; return false; } _queue.Remove(front); + hash = front.Value.Item1; node = front.Value.Item2; _hashMapping.TryRemove(front.Value.Item1, out front); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs index fce0bb0a6338..2322595c67dc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs @@ -45,7 +45,21 @@ public static int Compare(ValueHash256 a, ValueHash256 b, ValueHash256 c) public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) { ValueHash256 bc = new(); - (new Vector(hash1.BytesAsSpan) ^ new Vector(hash2.BytesAsSpan)).CopyTo(bc.BytesAsSpan); + ReadOnlySpan hash1Bytes = hash1.BytesAsSpan; + ReadOnlySpan hash2Bytes = hash2.BytesAsSpan; + Span result = bc.BytesAsSpan; + + int i = 0; + for (; i <= result.Length - Vector.Count; i += Vector.Count) + { + (new Vector(hash1Bytes[i..]) ^ new Vector(hash2Bytes[i..])).CopyTo(result[i..]); + } + + for (; i < result.Length; i++) + { + result[i] = (byte)(hash1Bytes[i] ^ hash2Bytes[i]); + } + return bc; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs index d4f2baac9e9d..839a2f4704f2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs @@ -50,9 +50,9 @@ public bool RemoveAndReplace(in ValueHash256 hash) { if (!_items.Remove(hash)) return false; - if (_replacement.TryPopHead(out TNode? replacement)) + if (_replacement.TryPopHead(out ValueHash256 replacementHash, out TNode? replacement)) { - _items.AddOrRefresh(hash, replacement!); + _items.AddOrRefresh(replacementHash, replacement!); } _cachedArray = _items.GetAll(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index d69233300021..31d78ac2e143 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -83,7 +83,11 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 nodeHash, TNode node, out } } - public TNode? GetByHash(ValueHash256 hash) => GetBucketForHash(hash).GetByHash(hash); + public TNode? GetByHash(ValueHash256 hash) + { + using McsLock.Disposable _ = _lock.Acquire(); + return GetBucketForHash(hash).GetByHash(hash); + } private KBucket GetBucketForHash(ValueHash256 nodeHash) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index 723a09a69956..ab891a99f4f6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -98,7 +98,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => { // Should be added on Pong. await _kademliaMessageSender.Ping(node, token); - onlineBootNodes++; + System.Threading.Interlocked.Increment(ref onlineBootNodes); } catch (OperationCanceledException) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs index 82daee750826..f0db26fe5c00 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs @@ -78,7 +78,7 @@ public void OnIncomingMessageFrom(TNode node) if (SameAsSelf(toRefresh)) { // Move the current node entry to the front of its bucket. - routingTable.TryAddOrRefresh(_currentNodeIdAsHash, node, out TNode? _); + routingTable.TryAddOrRefresh(_currentNodeIdAsHash, toRefresh, out TNode? _); } else { @@ -89,7 +89,7 @@ public void OnIncomingMessageFrom(TNode node) } /// - /// Call when a requset to a node failed. This is used by other algorithm for health checks. + /// Call when a request to a node failed. This is used by other algorithm for health checks. /// /// public void OnRequestFailed(TNode node) @@ -105,6 +105,7 @@ public void OnRequestFailed(TNode node) { routingTable.Remove(hash); _peerFailures.Delete(hash); + return; } _peerFailures.Set(hash, currentFailure + 1); diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index aca558dd3fd9..6e7ff08d6f5f 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -273,8 +273,7 @@ private async Task FeedFromNodeSource() await foreach (Node node in _nodeSource.DiscoverNodes(token)) { - while (PeerCount >= _networkConfig.CandidatePeerCountCleanupThreshold || - (PeerCount >= _networkConfig.MaxCandidatePeerCount && ActivePeerCount >= _networkConfig.MaxActivePeers)) + while (PeerCount >= _networkConfig.MaxCandidatePeerCount || ActivePeerCount >= _networkConfig.MaxActivePeers) { if (_logger.IsDebug) _logger.Debug("Peer cleanup threshold reached. Throttling discovery."); await Task.Delay(1000, token); diff --git a/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs b/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs index a8bc3db4813f..893ff1f50345 100644 --- a/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs @@ -4,10 +4,8 @@ using Autofac; using Autofac.Features.AttributeFilters; using Nethermind.Config; -using Nethermind.Core; using Nethermind.Crypto; using Nethermind.Logging; -using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Discovery; From 221c120c69a4f413b8a70bd9116cdd510ff560b5 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 21 May 2026 16:58:06 +0300 Subject: [PATCH 099/182] Review --- .../Nethermind.Core/Caching/LruCache.cs | 41 +++++++++++++++++ .../Discv4/IteratorNodeLookupTests.cs | 2 + .../Discv4/KademliaDiscv4AdapterTests.cs | 24 +++++++--- .../Kademlia/KademliaSimulation.cs | 1 + .../Kademlia/KademliaTests.cs | 24 ++++++++++ .../Kademlia/NodeHealthTrackerTests.cs | 6 +++ .../DiscoveryApp.cs | 12 ++++- .../DiscoveryPersistenceManager.cs | 16 ++----- .../Discv4/KademliaDiscv4Adapter.cs | 26 +++++------ .../Discv4/NeighbourMsgHandler.cs | 2 +- .../Discv4/NodeSession.cs | 35 +++++++++------ .../Discv4/PublicKeyKeyOperator.cs | 10 ++++- .../Kademlia/DoubleEndedLru.cs | 8 +--- .../Kademlia/IKademlia.cs | 5 +++ .../Kademlia/IRoutingTable.cs | 1 + .../Kademlia/IteratorNodeLookup.cs | 6 ++- .../Kademlia/KBucketTree.cs | 44 ++++++++++++------- .../Kademlia/Kademlia.cs | 38 +++++++++++++++- .../Kademlia/KademliaConfig.cs | 5 +++ 19 files changed, 231 insertions(+), 75 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 63727ac27c7a..56f195b7551c 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -71,6 +71,47 @@ public bool TryGet(TKey key, out TValue value) return false; } + /// + /// Sets a missing cached value or atomically returns the existing one for the specified key. + /// + /// The cache key. + /// State passed to without requiring a closure. + /// Factory used to create the value when the key is missing. + /// Type of the factory state. + /// The existing value, or the value created by . + public TValue SetOrGet(TKey key, TState state, Func valueFactory) + { + ArgumentNullException.ThrowIfNull(valueFactory); + + using McsLock.Disposable lockRelease = _lock.Acquire(); + + if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) + { + TValue value = node.Value.Value; + LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); + return value; + } + + TValue newValue = valueFactory(key, state); + if (newValue is null) + { + return newValue; + } + + if (_cacheMap.Count >= _maxCapacity) + { + Replace(key, newValue); + } + else + { + LinkedListNode newNode = new(new(key, newValue)); + LinkedListNode.AddMostRecent(ref _leastRecentlyUsed, newNode); + _cacheMap.Add(key, newNode); + } + + return newValue; + } + public bool Set(TKey key, TValue val) { using McsLock.Disposable lockRelease = _lock.Acquire(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index 0cc283faab7c..6fd1f6cd98cf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Logging; @@ -43,6 +44,7 @@ public void Setup() kademliaConfig, _msgSender, new PublicKeyKeyOperator(), + new ManualTimestamper(new DateTime(2025, 5, 13, 21, 0, 0, DateTimeKind.Utc)), logManager); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 441517ccdd52..ac4797ceb20b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -40,6 +40,7 @@ public class KademliaDiscv4AdapterTests private ILogManager _logManager = null!; private ITimestamper _timestamper = null!; private IMsgSender _msgSender = null!; + private INodeStatsManager _nodeStatsManager = null!; private Node _testNode = null!; private PublicKey _testPublicKey = null!; @@ -90,8 +91,8 @@ public void Setup() INodeRecordProvider nodeRecordProvider = Substitute.For(); nodeRecordProvider.Current.Returns(_selfNodeRecord); - INodeStatsManager nodeStatsManager = Substitute.For(); - nodeStatsManager.GetOrAdd(Arg.Any()).Returns(Substitute.For()); + _nodeStatsManager = Substitute.For(); + _nodeStatsManager.GetOrAdd(Arg.Any()).Returns(Substitute.For()); _adapter = new KademliaDiscv4Adapter( new Lazy>(() => _kademliaMessageReceiver), @@ -99,7 +100,7 @@ public void Setup() new DiscoveryConfig(), _kademliaConfig, nodeRecordProvider, - nodeStatsManager, + _nodeStatsManager, _timestamper, Substitute.For(), _logManager @@ -107,6 +108,17 @@ public void Setup() _adapter.MsgSender = _msgSender; } + [Test] + public async Task GetSession_should_return_single_session_for_concurrent_calls() + { + Node[] nodes = Enumerable.Repeat(_receiver, 128).ToArray(); + + NodeSession[] sessions = await Task.WhenAll(nodes.Select(node => Task.Run(() => _adapter.GetSession(node)))); + + Assert.That(sessions.All(session => ReferenceEquals(session, sessions[0])), Is.True); + _nodeStatsManager.Received(1).GetOrAdd(Arg.Is(node => node.Id == _receiver.Id)); + } + private NodeRecord CreateNodeRecord() { NodeRecord selfNodeRecord = new(); @@ -274,7 +286,7 @@ public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(Cancella FindNodeMsg findNodeMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); findNodeMsg = AddReceiverFarAddress(findNodeMsg); - Node[] expectedNodes = Enumerable.Repeat(new Node(TestItem.PublicKeyD, "192.168.1.3", 30303), 16).ToArray(); + Node[] expectedNodes = Enumerable.Repeat(new Node(TestItem.PublicKeyD, "192.168.1.3", 30303), 20).ToArray(); _kademliaMessageReceiver.GetKNeighbour( Arg.Any(), Arg.Any()) @@ -288,13 +300,13 @@ public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(Cancella Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), Arg.Is(n => n.Id == _receiver.Id)); - // Send out two message instead of one because of MTU limit. + // Send out two messages instead of one because of MTU limit. await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && m.Nodes.Count == 12)); await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && - m.Nodes.Count == 4)); + m.Nodes.Count == 8)); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 02eb721bd963..730a04cd3aa4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -230,6 +230,7 @@ public Kademlia CreateNode(ValueHash256 nodeID) builder .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Error)) + .AddSingleton(new ManualTimestamper(new DateTime(2025, 5, 13, 21, 0, 0, DateTimeKind.Utc))) .AddSingleton>(_nodeHashProvider) .AddSingleton(new KademliaConfig { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 2a52f3500962..352858d403b6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -23,6 +23,7 @@ private Kademlia CreateKad(KademliaConfig()) .AddSingleton(new TestLogManager(LogLevel.Trace)) + .AddSingleton(new ManualTimestamper(new System.DateTime(2025, 5, 13, 21, 0, 0, System.DateTimeKind.Utc))) .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) @@ -50,6 +51,29 @@ public void TestNewNodeAdded() Assert.That(nodeAddedTriggered, Is.EqualTo(1)); } + [Test] + public void TestNodeRemoved() + { + Kademlia kad = CreateKad(new KademliaConfig + { + KSize = 5, + Beta = 0, + }); + + int nodeRemovedTriggered = 0; + ValueHash256 testHash = new("0x1111111111111111111111111111111111111111111111111111111111111111"); + kad.AddOrRefresh(testHash); + kad.OnNodeRemoved += (sender, hash256) => + { + nodeRemovedTriggered++; + Assert.That(hash256, Is.EqualTo(testHash)); + }; + + kad.Remove(testHash); + + Assert.That(nodeRemovedTriggered, Is.EqualTo(1)); + } + [Test] public async Task TestTooManyNode() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index 8f5bb3e4bf30..c105f264df28 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -104,6 +104,12 @@ public event EventHandler? OnNodeAdded remove { } } + public event EventHandler? OnNodeRemoved + { + add { } + remove { } + } + public int Size => AddCalls.Count; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index e29ed102aa28..646830b94d3f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -83,6 +83,7 @@ public DiscoveryApp( }); (_kademliaNodeSource, _persistenceManager, _discv4Adapter, _kademlia, _discoveryHandlerFactory) = _discv4Services.Resolve(); + _kademlia.OnNodeRemoved += OnKademliaNodeRemoved; } /// @@ -248,6 +249,13 @@ private async Task ActivateAsync(CancellationToken cancellationToken) public IAsyncEnumerable DiscoverNodes(CancellationToken token) => _kademliaNodeSource.DiscoverNodes(token); - public event EventHandler? NodeRemoved { add { } remove { } } - public ValueTask DisposeAsync() => _discv4Services.DisposeAsync(); + private void OnKademliaNodeRemoved(object? sender, Node node) => NodeRemoved?.Invoke(sender, new NodeEventArgs(node)); + + public event EventHandler? NodeRemoved; + + public async ValueTask DisposeAsync() + { + _kademlia.OnNodeRemoved -= OnKademliaNodeRemoved; + await _discv4Services.DisposeAsync(); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs index f53d026d2e04..e16de580719d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -58,13 +58,9 @@ public async Task LoadPersistedNodes(CancellationToken cancellationToken) { node = new Node(networkNode.NodeId, networkNode.Host, networkNode.Port); } - catch (Exception) + catch (Exception e) { - if (_logger.IsDebug) - { - _logger.Error( - $"ERROR/DEBUG peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - } + _logger.DebugError($"Peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}. {e}"); continue; } @@ -79,13 +75,9 @@ public async Task LoadPersistedNodes(CancellationToken cancellationToken) { continue; } - catch (Exception) + catch (Exception e) { - if (_logger.IsDebug) - { - _logger.Error( - $"ERROR/DEBUG error when pinging persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}"); - } + if (_logger.IsDebug) _logger.Debug($"Error when pinging persisted node {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}. {e}"); continue; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 877cae2a25e4..50c70a18bbdb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -29,6 +29,8 @@ public class KademliaDiscv4Adapter( ILogManager logManager ) : IKademliaDiscv4Adapter { + private const int MaxNodesPerNeighborsMsg = 12; + private readonly TimeSpan _requestEnrTimeout = TimeSpan.FromMilliseconds(discoveryConfig.EnrTimeout); private readonly TimeSpan _findNeighbourTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); @@ -44,13 +46,10 @@ ILogManager logManager #region Authentication and utils - public NodeSession GetSession(Node node) - { - if (_sessions.TryGet(node.IdHash, out NodeSession? session)) return session; - session = new NodeSession(nodeStatsManager.GetOrAdd(node), timestamper); - _sessions.Set(node.IdHash, session); - return session; - } + public NodeSession GetSession(Node node) => _sessions.SetOrGet( + node.IdHash.ValueHash256, + (node, nodeStatsManager, timestamper), + static (_, state) => new NodeSession(state.nodeStatsManager.GetOrAdd(state.node), state.timestamper)); private async Task EnsureOutgoingMessageBondedPeer(Node node, NodeSession nodeSession, CancellationToken token) { @@ -236,15 +235,16 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms PublicKey publicKey = new(msg.SearchedNodeId); Node[] nodes = kademlia.Value.GetKNeighbour(publicKey, node, false); - if (nodes.Length <= 12) + if (nodes.Length == 0) { await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes), token); + return; } - else + + for (int i = 0; i < nodes.Length; i += MaxNodesPerNeighborsMsg) { - // Split into two because the size of message when nodes is > 12 is larger than mtu size. - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[..12]), token); - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[12..16]), token); + int batchEnd = Math.Min(i + MaxNodesPerNeighborsMsg, nodes.Length); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[i..batchEnd]), token); } } @@ -330,7 +330,7 @@ private bool ValidatePingAddress(PingMsg msg) private bool HandleViaMessageHandlers(Node node, DiscoveryMsg msg) { - (Hash256 IdHash, MsgType MsgType) key = (node.IdHash, msg.MsgType); + (ValueHash256 IdHash, MsgType MsgType) key = (node.IdHash.ValueHash256, msg.MsgType); if (!_incomingMessageHandlers.TryGetValue(key, out IMessageHandler[]? handlers)) return false; foreach (IMessageHandler messageHandler in handlers!) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs index 6da2a5b1c423..08170d544ffe 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs @@ -35,7 +35,7 @@ public bool Handle(DiscoveryMsg msg) // Some client (nethermind, besu) only respond with one request. Task.Run(async () => { - if (Interlocked.CompareExchange(ref _timeoutInitiated, !_timeoutInitiated, false)) return; + if (Interlocked.CompareExchange(ref _timeoutInitiated, true, false)) return; await Task.Delay(_secondRequestTimeout); TaskCompletionSource.TrySetResult(_current); }); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index 265db389a26f..ac414db92771 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -14,20 +14,29 @@ public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) public static readonly TimeSpan PingRetryTimeout = TimeSpan.FromMinutes(10); public const int AuthenticatedRequestFailureLimit = 5; - private long AuthenticatedRequestFailureCount { get; set; } - private DateTimeOffset LastPongReceived { get; set; } = DateTimeOffset.MinValue; - private DateTimeOffset LastPingReceived { get; set; } = DateTimeOffset.MinValue; - private DateTimeOffset LastPingSent { get; set; } = DateTimeOffset.MinValue; + private int _authenticatedRequestFailureCount; + private long _lastPongReceivedTicks; + private long _lastPingReceivedTicks; + private long _lastPingSentTicks; - public bool HasReceivedPing => LastPingReceived + BondTimeout > Timestamper.UtcNowOffset; - public bool NotTooManyFailure => AuthenticatedRequestFailureCount <= AuthenticatedRequestFailureLimit; - public bool HasReceivedPong => LastPongReceived + BondTimeout > Timestamper.UtcNowOffset; - public bool HasTriedPingRecently => LastPingSent + PingRetryTimeout > Timestamper.UtcNowOffset; - public void ResetAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount = 0; - public void OnAuthenticatedRequestFailure() => AuthenticatedRequestFailureCount++; + public bool HasReceivedPing => Volatile.Read(ref _lastPingReceivedTicks) + BondTimeout.Ticks > Timestamper.UtcNow.Ticks; + public bool NotTooManyFailure => Volatile.Read(ref _authenticatedRequestFailureCount) <= AuthenticatedRequestFailureLimit; + public bool HasReceivedPong => Volatile.Read(ref _lastPongReceivedTicks) + BondTimeout.Ticks > Timestamper.UtcNow.Ticks; + public bool HasTriedPingRecently => Volatile.Read(ref _lastPingSentTicks) + PingRetryTimeout.Ticks > Timestamper.UtcNow.Ticks; + public void ResetAuthenticatedRequestFailure() => Interlocked.Exchange(ref _authenticatedRequestFailureCount, 0); - public void OnPongReceived() => LastPongReceived = Timestamper.UtcNowOffset; - public void OnPingReceived() => LastPingReceived = Timestamper.UtcNowOffset; + public void OnAuthenticatedRequestFailure() + { + while (true) + { + int failureCount = Volatile.Read(ref _authenticatedRequestFailureCount); + if (failureCount > AuthenticatedRequestFailureLimit) return; + if (Interlocked.CompareExchange(ref _authenticatedRequestFailureCount, failureCount + 1, failureCount) == failureCount) return; + } + } + + public void OnPongReceived() => Volatile.Write(ref _lastPongReceivedTicks, Timestamper.UtcNow.Ticks); + public void OnPingReceived() => Volatile.Write(ref _lastPingReceivedTicks, Timestamper.UtcNow.Ticks); public void RecordStatsForOutgoingMsg(DiscoveryMsg msg) { @@ -79,5 +88,5 @@ public void RecordStatsForIncomingMsg(DiscoveryMsg msg) } } - public void OnPingSent() => LastPingSent = Timestamper.UtcNowOffset; + public void OnPingSent() => Volatile.Write(ref _lastPingSentTicks, Timestamper.UtcNow.Ticks); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs index 06fe18184e6f..1b0556dd9153 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs @@ -13,10 +13,16 @@ public class PublicKeyKeyOperator : IKeyOperator public ValueHash256 GetKeyHash(PublicKey key) => key.Hash; + /// + /// Creates a random discv4 lookup target. + /// + /// + /// Discv4 FINDNODE carries a public key, while bucket refresh starts from a desired node-id hash prefix. + /// Constructing a public key whose Keccak hash lands in that prefix is not practical, so this uses a random + /// 64-byte target and treats discv4 bucket refresh as best-effort sampling. + /// public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) { - // Obviously, we can't generate this. So we just randomly pick something. - // I guess we can brute force it if needed. Span randomBytes = new byte[64]; Random.Shared.NextBytes(randomBytes); return new PublicKey(randomBytes); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs index bde5859e7240..27c947840a6f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs @@ -32,12 +32,8 @@ public BucketAddResult AddOrRefresh(in ValueHash256 hash, TNode node) } listNode = _queue.AddFirst((hash, node)); - if (_hashMapping.TryAdd(hash, listNode) && _queue.Count <= capacity) return BucketAddResult.Added; - - _queue.Remove((hash, node)); - _hashMapping.TryRemove(hash, out listNode); - - return BucketAddResult.Full; + _hashMapping.TryAdd(hash, listNode); + return BucketAddResult.Added; } public bool TryPopHead(out ValueHash256 hash, out TNode? node) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs index ed24eb728d18..1c6c090d52f9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs @@ -57,6 +57,11 @@ public interface IKademlia /// event EventHandler OnNodeAdded; + /// + /// Called when a TNode is removed from the routing table. + /// + event EventHandler OnNodeRemoved; + /// /// Iterate all nodes with no ordering /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs index f24f75bd8ccc..52ff09bc0a31 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs @@ -15,5 +15,6 @@ public interface IRoutingTable where TNode : notnull TNode? GetByHash(ValueHash256 nodeId); void LogDebugInfo(); event EventHandler? OnNodeAdded; + event EventHandler? OnNodeRemoved; int Size { get; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs index 251502625fd3..b251cd724f52 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Runtime.CompilerServices; +using Nethermind.Core; using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; @@ -24,6 +25,7 @@ public class IteratorNodeLookup( KademliaConfig kademliaConfig, IKademliaMessageSender msgSender, IKeyOperator keyOperator, + ITimestamper timestamper, ILogManager logManager) : IIteratorNodeLookup where TNode : notnull { private readonly ILogger _logger = logManager.GetClassLogger>(); @@ -181,7 +183,7 @@ bool ShouldStop() try { if (_unreacheableNodes.TryGet(keyOperator.GetNodeHash(node), out DateTimeOffset lastAttempt) && - lastAttempt + TimeSpan.FromMinutes(5) > DateTimeOffset.Now) + lastAttempt + TimeSpan.FromMinutes(5) > timestamper.UtcNowOffset) { return []; } @@ -190,7 +192,7 @@ bool ShouldStop() } catch (OperationCanceledException) { - _unreacheableNodes.Set(keyOperator.GetNodeHash(node), DateTimeOffset.Now); + _unreacheableNodes.Set(keyOperator.GetNodeHash(node), timestamper.UtcNowOffset); return null; } catch (Exception e) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 31d78ac2e143..160b1c278468 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -97,12 +97,12 @@ private KBucket GetBucketForHash(ValueHash256 nodeHash) { if (current.IsLeaf) { - _logger.Debug($"Reached leaf node at depth {depth}"); + if (_logger.IsDebug) _logger.Debug($"Reached leaf node at depth {depth}"); return current.Bucket; } bool goRight = GetBit(nodeHash, depth); - _logger.Debug($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); + if (_logger.IsDebug) _logger.Debug($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); current = goRight ? current.Right! : current.Left!; depth++; @@ -112,7 +112,7 @@ private KBucket GetBucketForHash(ValueHash256 nodeHash) private bool ShouldSplit(int depth, int targetLogDistance) { bool shouldSplit = depth < 256 && targetLogDistance + _b >= depth; - _logger.Debug($"ShouldSplit at depth {depth}: {shouldSplit}"); + if (_logger.IsDebug) _logger.Debug($"ShouldSplit at depth {depth}: {shouldSplit}"); return shouldSplit; } @@ -123,7 +123,7 @@ private void SplitBucket(int depth, TreeNode node) rightPrefixBytes[depth / 8] |= (byte)(1 << (7 - (depth % 8))); node.Right = new TreeNode(_k, new ValueHash256(rightPrefixBytes)); - _logger.Debug($"Created children at depth {depth + 1}"); + if (_logger.IsDebug) _logger.Debug($"Created children at depth {depth + 1}"); // The reverse is because the bucket is iterated from the most recent. Without it // reading would have reversed this order. @@ -132,30 +132,38 @@ private void SplitBucket(int depth, TreeNode node) ValueHash256 itemHash = item.Item1; TreeNode? targetNode = GetBit(itemHash, depth) ? node.Right : node.Left; targetNode.Bucket.TryAddOrRefresh(itemHash, item.Item2, out _); - _logger.Debug($"Moved item {item} to {(GetBit(itemHash, depth) ? "right" : "left")} child"); + if (_logger.IsDebug) _logger.Debug($"Moved item {item} to {(GetBit(itemHash, depth) ? "right" : "left")} child"); } node.Bucket.Clear(); - _logger.Debug($"Finished splitting bucket. Left count: {node.Left.Bucket.Count}, Right count: {node.Right.Bucket.Count}"); + if (_logger.IsDebug) _logger.Debug($"Finished splitting bucket. Left count: {node.Left.Bucket.Count}, Right count: {node.Right.Bucket.Count}"); } public bool Remove(in ValueHash256 nodeHash) { using McsLock.Disposable _ = _lock.Acquire(); - _logger.Debug($"Attempting to remove node {nodeHash} with hash {nodeHash}"); + if (_logger.IsDebug) _logger.Debug($"Attempting to remove node {nodeHash} with hash {nodeHash}"); - return GetBucketForHash(nodeHash).RemoveAndReplace(nodeHash); + KBucket bucket = GetBucketForHash(nodeHash); + TNode? removedNode = bucket.GetByHash(nodeHash); + bool removed = bucket.RemoveAndReplace(nodeHash); + if (removed && removedNode is not null) + { + OnNodeRemoved?.Invoke(this, removedNode); + } + + return removed; } public TNode[] GetAllAtDistance(int distance) { using McsLock.Disposable _ = _lock.Acquire(); - _logger.Debug($"Getting all nodes at distance {distance}"); + if (_logger.IsDebug) _logger.Debug($"Getting all nodes at distance {distance}"); List result = []; GetAllAtDistanceRecursive(_root, 0, distance, result); - _logger.Debug($"Found {result.Count} nodes at distance {distance}"); + if (_logger.IsDebug) _logger.Debug($"Found {result.Count} nodes at distance {distance}"); return [.. result]; } @@ -378,12 +386,15 @@ void TraverseTree(TreeNode node, int depth) TraverseTree(_root, 0); - _logger.Debug($"Tree Statistics:\n" + - $"Total Nodes: {totalNodes}\n" + - $"Total Buckets: {totalBuckets}\n" + - $"Max Depth: {maxDepth}\n" + - $"Total Items: {totalItems}\n" + - $"Average Items per Bucket: {(double)totalItems / totalBuckets:F2}"); + if (_logger.IsDebug) + { + _logger.Debug($"Tree Statistics:\n" + + $"Total Nodes: {totalNodes}\n" + + $"Total Buckets: {totalBuckets}\n" + + $"Max Depth: {maxDepth}\n" + + $"Total Items: {totalItems}\n" + + $"Average Items per Bucket: {(double)totalItems / totalBuckets:F2}"); + } } private void LogTreeStructure() { @@ -399,6 +410,7 @@ public void LogDebugInfo() } public event EventHandler? OnNodeAdded; + public event EventHandler? OnNodeRemoved; public int Size { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index ab891a99f4f6..c86add930401 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics; +using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Logging; @@ -20,7 +21,11 @@ public class Kademlia : IKademlia where TNode : notnul private readonly ValueHash256 _currentNodeIdAsHash; private readonly int _kSize; private readonly TimeSpan _refreshInterval; + private readonly TimeSpan _bucketRefreshInterval; private readonly IReadOnlyList _bootNodes; + private readonly ITimestamper _timestamper; + private readonly Dictionary _lastBucketRefreshTicks = []; + private readonly object _lastBucketRefreshLock = new(); public Kademlia( IKeyOperator keyOperator, @@ -29,6 +34,7 @@ public Kademlia( ILookupAlgo lookupAlgo, ILogManager logManager, INodeHealthTracker nodeHealthTracker, + ITimestamper timestamper, KademliaConfig config) { _keyOperator = keyOperator; @@ -42,7 +48,9 @@ public Kademlia( _currentNodeIdAsHash = _keyOperator.GetNodeHash(_currentNodeId); _kSize = config.KSize; _refreshInterval = config.RefreshInterval; + _bucketRefreshInterval = config.BucketRefreshInterval; _bootNodes = config.BootNodes; + _timestamper = timestamper; AddOrRefresh(_currentNodeId); } @@ -113,10 +121,12 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => token.ThrowIfCancellationRequested(); - // Refreshes all bucket. one by one. That is not empty. - // A refresh means to do a k-nearest node lookup for a random hash for that particular bucket. + // Refresh stale non-empty buckets one by one. A refresh means to do a k-nearest node lookup for a random hash + // for that particular bucket. foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { + if (!ShouldRefreshBucket(Prefix, Bucket)) continue; + TKey? keyToLookup = _keyOperator.CreateRandomKeyAtDistance(Prefix, Distance); await LookupNodesClosest(keyToLookup, token); } @@ -128,6 +138,24 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } } + private bool ShouldRefreshBucket(ValueHash256 prefix, KBucket bucket) + { + if (bucket.Count == 0) return false; + + long nowTicks = _timestamper.UtcNow.Ticks; + lock (_lastBucketRefreshLock) + { + if (_lastBucketRefreshTicks.TryGetValue(prefix, out long lastRefreshTicks) && + nowTicks - lastRefreshTicks < _bucketRefreshInterval.Ticks) + { + return false; + } + + _lastBucketRefreshTicks[prefix] = nowTicks; + return true; + } + } + public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false) { ValueHash256? excludeHash = null; @@ -142,6 +170,12 @@ public event EventHandler OnNodeAdded remove => _routingTable.OnNodeAdded -= value; } + public event EventHandler OnNodeRemoved + { + add => _routingTable.OnNodeRemoved += value; + remove => _routingTable.OnNodeRemoved -= value; + } + public IEnumerable IterateNodes() { foreach ((ValueHash256 _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index 74d1d5fffb22..b7e107876296 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -30,6 +30,11 @@ public class KademliaConfig /// public TimeSpan RefreshInterval { get; set; } = TimeSpan.FromMinutes(30); + /// + /// Minimum age before an individual non-empty bucket is refreshed again. + /// + public TimeSpan BucketRefreshInterval { get; set; } = TimeSpan.FromHours(1); + /// /// The timeout for each find neighbour call lookup /// From 3291f46f4942e7b7193268e30a9c9edcf68be764 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 21 May 2026 17:08:11 +0300 Subject: [PATCH 100/182] Simplify --- .../Discv4/NodeSession.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index ac414db92771..284df6f1bda5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -24,16 +24,7 @@ public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) public bool HasReceivedPong => Volatile.Read(ref _lastPongReceivedTicks) + BondTimeout.Ticks > Timestamper.UtcNow.Ticks; public bool HasTriedPingRecently => Volatile.Read(ref _lastPingSentTicks) + PingRetryTimeout.Ticks > Timestamper.UtcNow.Ticks; public void ResetAuthenticatedRequestFailure() => Interlocked.Exchange(ref _authenticatedRequestFailureCount, 0); - - public void OnAuthenticatedRequestFailure() - { - while (true) - { - int failureCount = Volatile.Read(ref _authenticatedRequestFailureCount); - if (failureCount > AuthenticatedRequestFailureLimit) return; - if (Interlocked.CompareExchange(ref _authenticatedRequestFailureCount, failureCount + 1, failureCount) == failureCount) return; - } - } + public void OnAuthenticatedRequestFailure() => Interlocked.Increment(ref _authenticatedRequestFailureCount); public void OnPongReceived() => Volatile.Write(ref _lastPongReceivedTicks, Timestamper.UtcNow.Ticks); public void OnPingReceived() => Volatile.Write(ref _lastPingReceivedTicks, Timestamper.UtcNow.Ticks); From 375a88c01beb15a1b3644da958a8021f516dc2bb Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 21 May 2026 17:13:47 +0300 Subject: [PATCH 101/182] Fix spelling --- .../Kademlia/KademliaSimulation.cs | 2 +- .../Kademlia/IteratorNodeLookup.cs | 6 +++--- .../Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 730a04cd3aa4..730e271a887a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -273,7 +273,7 @@ public async Task FindNeighbours(TestNode node, ValueHash256 hash, C Interlocked.Increment(ref fabric.FindNeighbourCount); await fabric.DoSimulateLatency(token); - fabric.Debug($"findn from {sender} to {node}"); + fabric.Debug($"find neighbours from {sender} to {node}"); if (fabric.TryGetReceiver(node, out ReceiverForNode receiver)) { return (await receiver.FindNeighbours(sender, hash, token)).Select((node) => new TestNode(node.Hash)).ToArray(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs index b251cd724f52..7394da2aec88 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs @@ -32,7 +32,7 @@ public class IteratorNodeLookup( private readonly ValueHash256 _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); // Small lru of unreachable nodes, prevent retrying. Pretty effective, although does not improve discovery overall. - private readonly LruCache _unreacheableNodes = new(256, ""); + private readonly LruCache _unreachableNodes = new(256, ""); // The maximum round per lookup. Higher means that it will 'see' deeper into the network, but come at a latency // cost of trying many node for increasingly lower new node. @@ -182,7 +182,7 @@ bool ShouldStop() { try { - if (_unreacheableNodes.TryGet(keyOperator.GetNodeHash(node), out DateTimeOffset lastAttempt) && + if (_unreachableNodes.TryGet(keyOperator.GetNodeHash(node), out DateTimeOffset lastAttempt) && lastAttempt + TimeSpan.FromMinutes(5) > timestamper.UtcNowOffset) { return []; @@ -192,7 +192,7 @@ bool ShouldStop() } catch (OperationCanceledException) { - _unreacheableNodes.Set(keyOperator.GetNodeHash(node), timestamper.UtcNowOffset); + _unreachableNodes.Set(keyOperator.GetNodeHash(node), timestamper.UtcNowOffset); return null; } catch (Exception e) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index b7e107876296..66768a537229 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -16,7 +16,7 @@ public class KademliaConfig public int KSize { get; set; } = 16; /// - /// Alpha, as in the parallelism of the lookup algorith. + /// Alpha, as in the parallelism of the lookup algorithm. /// public int Alpha { get; set; } = 3; From bf8866e6ce8317ec62934271e7e2f21a3f2ab2d4 Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Fri, 22 May 2026 09:51:35 +0200 Subject: [PATCH 102/182] test(discovery): dedupe discv4/kademlia tests - NodeSessionTests: collapse three flag-and-timeout tests into one [TestCaseSource] - IteratorNodeLookupTests: extract routing-table / find-neighbours stub helpers - KademliaDiscv4AdapterTests: reuse ConfigureBondCallback in ping test - KademliaSimulation: drop accidentally duplicated [TestFixture(3, 0)] Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Discv4/IteratorNodeLookupTests.cs | 148 ++++++------------ .../Discv4/KademliaDiscv4AdapterTests.cs | 16 +- .../Discv4/NodeSessionTests.cs | 51 +++--- .../Kademlia/KademliaSimulation.cs | 1 - 4 files changed, 75 insertions(+), 141 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index 6fd1f6cd98cf..519cfb9f4fcd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -22,6 +22,9 @@ namespace Nethermind.Network.Discovery.Test.Discv4 [TestFixture] public class IteratorNodeLookupTests { + private static readonly Node InitialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); + private static readonly Node NeighbourNode = new(TestItem.PublicKeyD, "192.168.1.4", 30303); + private IRoutingTable _routingTable = null!; private IteratorNodeLookup _lookup = null!; private IKademliaMessageSender _msgSender = null!; @@ -48,18 +51,30 @@ public void Setup() logManager); } + private void RoutingTableReturns(params Node[] nodes) => + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns(nodes); + + private void FindNeighboursReturns(Node from, params Node[] result) => + _msgSender.FindNeighbours(from, _targetKey, Arg.Any()) + .Returns(result); + + private void FindNeighboursThrows(Node from, Exception exception) => + _msgSender.FindNeighbours(from, _targetKey, Arg.Any()) + .Returns(Task.FromException(exception)); + + private Task AssertFindNeighboursCalledOnce(Node node) => + _msgSender.Received(1).FindNeighbours( + Arg.Is(n => n == node), + Arg.Is(k => k == _targetKey), + Arg.Any()); + [Test] [CancelAfter(10000)] public async Task Lookup_should_return_nodes_from_routing_table(CancellationToken token) { - Node[] expectedNodes = - [ - new(TestItem.PublicKeyC, "192.168.1.3", 30303), - new(TestItem.PublicKeyD, "192.168.1.4", 30303) - ]; - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns(expectedNodes); + Node[] expectedNodes = [InitialNode, NeighbourNode]; + RoutingTableReturns(expectedNodes); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); @@ -73,38 +88,24 @@ public async Task Lookup_should_return_nodes_from_routing_table(CancellationToke [CancelAfter(10000)] public async Task Lookup_should_query_nodes_and_return_neighbours(CancellationToken token) { - Node initialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - Node neighbourNode = new(TestItem.PublicKeyD, "192.168.1.4", 30303); - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([initialNode]); - - _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) - .Returns([neighbourNode]); + RoutingTableReturns(InitialNode); + FindNeighboursReturns(InitialNode, NeighbourNode); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result, Does.Contain(initialNode)); - Assert.That(result, Does.Contain(neighbourNode)); - - await _msgSender.Received(1).FindNeighbours( - Arg.Is(n => n == initialNode), - Arg.Is(k => k == _targetKey), - Arg.Any()); + Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); + await AssertFindNeighboursCalledOnce(InitialNode); } [Test] [CancelAfter(10000)] public async Task Lookup_should_not_query_self_node(CancellationToken token) { - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([_currentNode]); + RoutingTableReturns(_currentNode); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result, Does.Contain(_currentNode)); + Assert.That(result, Is.EquivalentTo(new[] { _currentNode })); await _msgSender.DidNotReceive().FindNeighbours( Arg.Any(), @@ -116,56 +117,33 @@ await _msgSender.DidNotReceive().FindNeighbours( [CancelAfter(10000)] public async Task Lookup_should_handle_empty_neighbour_response(CancellationToken token) { - Node initialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([initialNode]); - - _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) - .Returns([]); + RoutingTableReturns(InitialNode); + FindNeighboursReturns(InitialNode); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result, Does.Contain(initialNode)); - - await _msgSender.Received(1).FindNeighbours( - Arg.Is(n => n == initialNode), - Arg.Is(k => k == _targetKey), - Arg.Any()); + Assert.That(result, Is.EquivalentTo(new[] { InitialNode })); + await AssertFindNeighboursCalledOnce(InitialNode); } [Test] [CancelAfter(10000)] public async Task Lookup_should_handle_exception_in_find_neighbours(CancellationToken token) { - Node initialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([initialNode]); - - _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) - .Returns(Task.FromException(new Exception("Test exception"))); + RoutingTableReturns(InitialNode); + FindNeighboursThrows(InitialNode, new Exception("Test exception")); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - Assert.That(result, Has.Count.EqualTo(1)); - Assert.That(result, Does.Contain(initialNode)); - - await _msgSender.Received(1).FindNeighbours( - Arg.Is(n => n == initialNode), - Arg.Is(k => k == _targetKey), - Arg.Any()); + Assert.That(result, Is.EquivalentTo(new[] { InitialNode })); + await AssertFindNeighboursCalledOnce(InitialNode); } [Test] [CancelAfter(10000)] - public async Task Lookup_should_respect_cancellation_token(CancellationToken token) + public void Lookup_should_respect_cancellation_token(CancellationToken token) { - Node initialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([initialNode]); + RoutingTableReturns(InitialNode); using CancellationTokenSource cts = new(); cts.Cancel(); @@ -177,56 +155,28 @@ public async Task Lookup_should_respect_cancellation_token(CancellationToken tok [CancelAfter(10000)] public async Task Lookup_should_not_query_same_node_twice(CancellationToken token) { - Node initialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - Node neighbourNode = new(TestItem.PublicKeyD, "192.168.1.4", 30303); - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([initialNode]); - - _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) - .Returns([neighbourNode]); - - _msgSender.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) - .Returns([initialNode]); + RoutingTableReturns(InitialNode); + FindNeighboursReturns(InitialNode, NeighbourNode); + FindNeighboursReturns(NeighbourNode, InitialNode); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); - Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result, Does.Contain(initialNode)); - Assert.That(result, Does.Contain(neighbourNode)); - - await _msgSender.Received(1).FindNeighbours( - Arg.Is(n => n == initialNode), - Arg.Is(k => k == _targetKey), - Arg.Any()); - - await _msgSender.Received(1).FindNeighbours( - Arg.Is(n => n == neighbourNode), - Arg.Is(k => k == _targetKey), - Arg.Any()); + Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); + await AssertFindNeighboursCalledOnce(InitialNode); + await AssertFindNeighboursCalledOnce(NeighbourNode); } [Test] [CancelAfter(10000)] public async Task Lookup_should_not_return_duplicate_nodes(CancellationToken token) { - Node initialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - Node neighbourNode = new(TestItem.PublicKeyD, "192.168.1.4", 30303); - - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([initialNode]); - - _msgSender.FindNeighbours(initialNode, _targetKey, Arg.Any()) - .Returns([neighbourNode]); - - _msgSender.FindNeighbours(neighbourNode, _targetKey, Arg.Any()) - .Returns([initialNode, neighbourNode]); + RoutingTableReturns(InitialNode); + FindNeighboursReturns(InitialNode, NeighbourNode); + FindNeighboursReturns(NeighbourNode, InitialNode, NeighbourNode); List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); - Assert.That(result, Has.Count.EqualTo(2)); - Assert.That(result, Does.Contain(initialNode)); - Assert.That(result, Does.Contain(neighbourNode)); + Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index ac4797ceb20b..ad7b4bd4984d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -154,21 +154,7 @@ private T AddReceiverFarAddress(T msg) where T : DiscoveryMsg [CancelAfter(10000)] public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token) { - _msgSender - .When(x => x.SendMsg(Arg.Any())) - .Do(ci => - { - PingMsg sent = (PingMsg)ci[0]!; - IByteBuffer buffer = _receiverSerializationManager.ZeroSerialize(sent); - PingMsg msg = _receiverSerializationManager.Deserialize(buffer); - - PongMsg pong = new( - msg.FarPublicKey!, - _timestamper.UnixTime.SecondsLong + 1, - sent.Mdc!); - pong.FarAddress = _receiver.Address; - Task.Run(() => _adapter.OnIncomingMsg(pong)); - }); + ConfigureBondCallback(); await _adapter.Ping(_receiver, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs index 78123b8466a9..909ae06779fc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs @@ -28,34 +28,33 @@ public void Setup() _nodeSession = new(_nodeStats, _timestamper); } - [Test] - public void Test_HasReceivedPing() - { - Assert.That(_nodeSession.HasReceivedPing, Is.False); - _nodeSession.OnPingReceived(); - Assert.That(_nodeSession.HasReceivedPing, Is.True); - _timestamper.Add(NodeSession.BondTimeout); - Assert.That(_nodeSession.HasReceivedPing, Is.False); - } - - [Test] - public void Test_HasReceivedPong() - { - Assert.That(_nodeSession.HasReceivedPong, Is.False); - _nodeSession.OnPongReceived(); - Assert.That(_nodeSession.HasReceivedPong, Is.True); - _timestamper.Add(NodeSession.BondTimeout); - Assert.That(_nodeSession.HasReceivedPong, Is.False); - } + private static readonly TestCaseData[] FlagTimeoutCases = + [ + new TestCaseData( + (Func)(s => s.HasReceivedPing), + (Action)(s => s.OnPingReceived()), + NodeSession.BondTimeout).SetName(nameof(NodeSession.HasReceivedPing)), + new TestCaseData( + (Func)(s => s.HasReceivedPong), + (Action)(s => s.OnPongReceived()), + NodeSession.BondTimeout).SetName(nameof(NodeSession.HasReceivedPong)), + new TestCaseData( + (Func)(s => s.HasTriedPingRecently), + (Action)(s => s.OnPingSent()), + NodeSession.PingRetryTimeout).SetName(nameof(NodeSession.HasTriedPingRecently)), + ]; - [Test] - public void Test_HasTriedPingRecently() + [TestCaseSource(nameof(FlagTimeoutCases))] + public void Flag_is_set_on_event_and_cleared_after_timeout( + Func getter, + Action trigger, + TimeSpan timeout) { - Assert.That(_nodeSession.HasTriedPingRecently, Is.False); - _nodeSession.OnPingSent(); - Assert.That(_nodeSession.HasTriedPingRecently, Is.True); - _timestamper.Add(NodeSession.PingRetryTimeout); - Assert.That(_nodeSession.HasTriedPingRecently, Is.False); + Assert.That(getter(_nodeSession), Is.False); + trigger(_nodeSession); + Assert.That(getter(_nodeSession), Is.True); + _timestamper.Add(timeout); + Assert.That(getter(_nodeSession), Is.False); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 730e271a887a..2228819b6205 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -17,7 +17,6 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; -[TestFixture(3, 0)] [TestFixture(1, 0)] [TestFixture(1, 4)] [TestFixture(3, 0)] From 9a2c6b9b7a0073873610458de6c7cf60589dc1bb Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Fri, 22 May 2026 09:58:40 +0200 Subject: [PATCH 103/182] refactor(discovery): address review comments - EnrResponseHandler / PongMsgHandler: collapse Handle to expression body - DoubleEndedLru.GetByHash: collapse to ternary expression body - IteratorNodeLookup.ShouldStop: drop redundant if, return condition directly - IteratorNodeLookup.FindNeighbour: collapse unreachable check + send to single return - NodeSession.RecordStatsFor*Msg: dedupe via shared helper with switch expression (relies on Discovery*Out/Discovery*In pairs being adjacent in NodeStatsEventType) - KademliaDiscv4Adapter: drop #region - ITaskCompleter: move to its own file - TalkReqAndRespHandler.HandleResponse: collapse to one line Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Discv4/EnrResponseHandler.cs | 9 +-- .../Discv4/IMessageHandler.cs | 6 -- .../Discv4/ITaskCompleter.cs | 9 +++ .../Discv4/KademliaDiscv4Adapter.cs | 4 -- .../Discv4/NodeSession.cs | 61 +++++-------------- .../Discv4/PongMsgHandler.cs | 10 +-- .../Kademlia/DoubleEndedLru.cs | 11 +--- .../Kademlia/IteratorNodeLookup.cs | 18 ++---- .../TalkReqAndRespHandler.cs | 5 +- 9 files changed, 37 insertions(+), 96 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs index 738aca8be866..bdbc2ea0f78a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs @@ -9,12 +9,5 @@ public class EnrResponseHandler : ITaskCompleter { public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - public bool Handle(DiscoveryMsg msg) - { - if (msg is EnrResponseMsg resp && TaskCompletionSource.TrySetResult(resp)) - { - return true; - } - return false; - } + public bool Handle(DiscoveryMsg msg) => msg is EnrResponseMsg resp && TaskCompletionSource.TrySetResult(resp); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs index 5126746f735c..e3be64a4b8cc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs @@ -9,9 +9,3 @@ internal interface IMessageHandler { bool Handle(DiscoveryMsg msg); } - - -internal interface ITaskCompleter : IMessageHandler -{ - TaskCompletionSource TaskCompletionSource { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs new file mode 100644 index 000000000000..ddc9dc6a2192 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv4; + +internal interface ITaskCompleter : IMessageHandler +{ + TaskCompletionSource TaskCompletionSource { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 50c70a18bbdb..b353c1438ca1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -44,8 +44,6 @@ ILogManager logManager private readonly ConcurrentDictionary<(ValueHash256, MsgType), IMessageHandler[]> _incomingMessageHandlers = new(); private readonly LruCache _sessions = new(discoveryConfig.MaxNodeLifecycleManagersCount, "node_sessions"); - #region Authentication and utils - public NodeSession GetSession(Node node) => _sessions.SetOrGet( node.IdHash.ValueHash256, (node, nodeStatsManager, timestamper), @@ -168,8 +166,6 @@ private async Task SendMessage(NodeSession session, DiscoveryMsg msg, Cancellati private long CalculateExpirationTime() => (long)(_expirationTime.TotalSeconds + timestamper.UnixTime.SecondsLong); - #endregion - public async Task Ping(Node receiver, CancellationToken token) { using AutoCancelTokenSource cts = token.CreateChildTokenSource(_pingTimeout); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index 284df6f1bda5..30a10933a274 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -29,54 +29,25 @@ public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) public void OnPongReceived() => Volatile.Write(ref _lastPongReceivedTicks, Timestamper.UtcNow.Ticks); public void OnPingReceived() => Volatile.Write(ref _lastPingReceivedTicks, Timestamper.UtcNow.Ticks); - public void RecordStatsForOutgoingMsg(DiscoveryMsg msg) - { - switch (msg.MsgType) - { - case MsgType.Ping: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingOut); - break; - case MsgType.FindNode: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeOut); - break; - case MsgType.EnrRequest: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestOut); - break; - case MsgType.Neighbors: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursOut); - break; - case MsgType.Pong: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongOut); - break; - case MsgType.EnrResponse: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseOut); - break; - } - } + public void RecordStatsForOutgoingMsg(DiscoveryMsg msg) => RecordStatsForMsg(msg, outgoing: true); + public void RecordStatsForIncomingMsg(DiscoveryMsg msg) => RecordStatsForMsg(msg, outgoing: false); - public void RecordStatsForIncomingMsg(DiscoveryMsg msg) + private void RecordStatsForMsg(DiscoveryMsg msg, bool outgoing) { - switch (msg.MsgType) + // The Discovery* members in NodeStatsEventType are laid out as ...Out, ...In pairs, + // so the incoming counterpart is always one position after the outgoing one. + NodeStatsEventType eventType = msg.MsgType switch { - case MsgType.Ping: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPingIn); - break; - case MsgType.FindNode: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryFindNodeIn); - break; - case MsgType.EnrRequest: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrRequestIn); - break; - case MsgType.Neighbors: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryNeighboursIn); - break; - case MsgType.Pong: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryPongIn); - break; - case MsgType.EnrResponse: - NodeStats.AddNodeStatsEvent(NodeStatsEventType.DiscoveryEnrResponseIn); - break; - } + MsgType.Ping => NodeStatsEventType.DiscoveryPingOut, + MsgType.FindNode => NodeStatsEventType.DiscoveryFindNodeOut, + MsgType.EnrRequest => NodeStatsEventType.DiscoveryEnrRequestOut, + MsgType.Neighbors => NodeStatsEventType.DiscoveryNeighboursOut, + MsgType.Pong => NodeStatsEventType.DiscoveryPongOut, + MsgType.EnrResponse => NodeStatsEventType.DiscoveryEnrResponseOut, + _ => NodeStatsEventType.None, + }; + if (eventType == NodeStatsEventType.None) return; + NodeStats.AddNodeStatsEvent(outgoing ? eventType : eventType + 1); } public void OnPingSent() => Volatile.Write(ref _lastPingSentTicks, Timestamper.UtcNow.Ticks); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs index 25e72e886c85..5751cad3769f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs @@ -10,12 +10,6 @@ public class PongMsgHandler(PingMsg ping) : ITaskCompleter { public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - public bool Handle(DiscoveryMsg msg) - { - if (msg is PongMsg pong && Bytes.AreEqual(pong.PingMdc, ping.Mdc) && TaskCompletionSource.TrySetResult(pong)) - { - return true; - } - return false; - } + public bool Handle(DiscoveryMsg msg) => + msg is PongMsg pong && Bytes.AreEqual(pong.PingMdc, ping.Mdc) && TaskCompletionSource.TrySetResult(pong); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs index 27c947840a6f..4bc4ca0481a7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs @@ -90,13 +90,6 @@ public bool Remove(ValueHash256 hash) public bool Contains(in ValueHash256 hash) => _hashMapping.ContainsKey(hash); - public TNode? GetByHash(ValueHash256 hash) - { - if (_hashMapping.TryGetValue(hash, out LinkedListNode<(ValueHash256, TNode)>? listNode)) - { - return listNode.Value.Item2; - } - - return default; - } + public TNode? GetByHash(ValueHash256 hash) => + _hashMapping.TryGetValue(hash, out LinkedListNode<(ValueHash256, TNode)>? listNode) ? listNode.Value.Item2 : default; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs index 7394da2aec88..c87f9c4e05d6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs @@ -169,12 +169,7 @@ bool ShouldStop() return true; } - if (round >= MaxRounds) - { - return true; - } - - return false; + return round >= MaxRounds; } } @@ -182,13 +177,10 @@ bool ShouldStop() { try { - if (_unreachableNodes.TryGet(keyOperator.GetNodeHash(node), out DateTimeOffset lastAttempt) && - lastAttempt + TimeSpan.FromMinutes(5) > timestamper.UtcNowOffset) - { - return []; - } - - return await msgSender.FindNeighbours(node, target, token); + return _unreachableNodes.TryGet(keyOperator.GetNodeHash(node), out DateTimeOffset lastAttempt) && + lastAttempt + TimeSpan.FromMinutes(5) > timestamper.UtcNowOffset + ? [] + : await msgSender.FindNeighbours(node, target, token); } catch (OperationCanceledException) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs index 0085b5414efc..deb036d3fdd7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs @@ -15,7 +15,6 @@ internal class TalkReqAndRespHandler : ITalkReqAndRespHandler //We currently don't advertise any supported protocols EmptyProtocolResponse; - public byte[]? HandleResponse(byte[] response) => - //We don't care about anything returned here at the moment - []; + // We don't care about anything returned here at the moment + public byte[]? HandleResponse(byte[] response) => []; } From e8e11e968c7d2ea72475d04e31c80f972eca6679 Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Fri, 22 May 2026 10:41:47 +0200 Subject: [PATCH 104/182] fix(discovery): address review findings + fill test gaps Important: - KBucketTree: raise OnNodeAdded/OnNodeRemoved after releasing _lock to avoid re-entrancy deadlocks via external subscribers. - LookupKNearestNeighbour: atomic CAS swap of roundComplete TCS and drop the misused (token) state argument; mark `finished` access with Volatile. - Kademlia.Bootstrap: catch non-OCE exceptions from individual ping calls and from the Bootstrap iteration in Run, so a transient network error no longer kills the discovery loop. - EnrResponseHandler: validate resp.RequestKeccak against the MDC of the EnrRequest just sent. EnrRequestMsg.Hash is now also set by the serializer on send. - KademliaNodeSource: unsubscribe Handler and complete the channel writer before awaiting the producer task in the finally block. - DoubleEndedLru/KBucket: GetAll / GetAllWithHash now materialize arrays under the inner lock rather than returning the live LinkedList. Nits: - Rename LookupFindNeighbourHardTimout -> LookupFindNeighbourHardTimeout. - Rename KademliaNodeSource ctor param lookup2 -> lookup. - Replace LINQ chains on KBucketTree hot paths (GetAllAtDistance, GetKNearestNeighbour, SplitBucket) with manual loops. - Hash256XorUtils.MaxDistance -> public const int. - Drop two stale comments and fix `does not seems` typo. Tests: - KBucketTreeTests: split preserves LRU order; GetAllAtDistance covers the deeper-bucket branch. - LookupKNearestNeighbourTests: mid-flight cancellation across Alpha=1/3. - NodeHealthTrackerTests: TryRefresh removes node on ping timeout, keeps it on success. - KademliaDiscv4AdapterTests: SendEnrRequest now wires request hash; added rejection test for unsolicited / wrong-keccak ENR responses. - DiscoveryPersistenceManagerTests: skip nodes that fail Node ctor. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DiscoveryPersistenceManagerTests.cs | 27 ++++ .../Discv4/KademliaDiscv4AdapterTests.cs | 33 ++++- .../Kademlia/KBucketTreeTests.cs | 76 ++++++++++ .../Kademlia/LookupKNearestNeighbourTests.cs | 111 +++++++++++++++ .../Kademlia/NodeHealthTrackerTests.cs | 83 ++++++++++- .../Discv4/DiscV4KademliaModule.cs | 2 +- .../Discv4/EnrResponseHandler.cs | 9 +- .../Discv4/KademliaDiscv4Adapter.cs | 4 +- .../Discv4/KademliaNodeSource.cs | 7 +- .../Kademlia/DoubleEndedLru.cs | 18 ++- .../Kademlia/Hash256XorUtils.cs | 2 +- .../Kademlia/KBucket.cs | 2 +- .../Kademlia/KBucketTree.cs | 132 +++++++++--------- .../Kademlia/Kademlia.cs | 22 ++- .../Kademlia/KademliaConfig.cs | 2 +- .../Kademlia/LookupKNearestNeighbour.cs | 19 ++- .../Messages/EnrRequestMsg.cs | 2 +- .../Serializers/EnrRequestMsgSerializer.cs | 4 + 18 files changed, 459 insertions(+), 96 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index eb88f42dab97..11345f17f7b4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -82,6 +82,33 @@ await _discv4Adapter.Received(networkNodes.Length).Ping( Arg.Any()); } + [Test] + [CancelAfter(10000)] + public async Task AddPersistedNodes_Should_Skip_Nodes_That_Fail_Node_Construction(CancellationToken cancellationToken) + { + INetworkStorage storageMock = Substitute.For(); + NetworkNode goodNode = new(TestItem.PublicKeyA, "192.168.1.1", 30303, 0); + NetworkNode badNode = new(TestItem.PublicKeyB, "192.168.1.2", -1, 0); + storageMock.GetPersistedNodes().Returns([badNode, goodNode]); + + DiscoveryPersistenceManager manager = new( + storageMock, + _nodeStatsManager, + _discv4Adapter, + _kademlia, + _discoveryConfig, + _logManager); + + await manager.LoadPersistedNodes(cancellationToken); + + await _discv4Adapter.Received(1).Ping( + Arg.Is(n => n.Id.Equals(goodNode.NodeId)), + Arg.Any()); + await _discv4Adapter.DidNotReceive().Ping( + Arg.Is(n => n.Id.Equals(badNode.NodeId)), + Arg.Any()); + } + [Test] [CancelAfter(10000)] public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions(CancellationToken cancellationToken) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index ad7b4bd4984d..9969b6547f82 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -194,18 +194,16 @@ public async Task FindNeighbours_should_return_nodes(CancellationToken token) [CancelAfter(10000)] public async Task SendEnrRequest_should_ping_then_enr_request_and_return_response(CancellationToken token) { - EnrResponseMsg expectedResponse = new( - _receiver.Address, - _selfNodeRecord, - new(new byte[32])); - ConfigureBondCallback(); + byte[] requestHash = TestItem.KeccakA.BytesToArray(); _msgSender .When(x => x.SendMsg(Arg.Any())) .Do(ci => { - EnrResponseMsg response = AddReceiverFarAddress(expectedResponse); + EnrRequestMsg sent = (EnrRequestMsg)ci[0]!; + sent.Hash = requestHash; + EnrResponseMsg response = AddReceiverFarAddress(new EnrResponseMsg(_receiver.Address, _selfNodeRecord, new Hash256(requestHash))); Task.Run(() => _adapter.OnIncomingMsg(response)); }); @@ -215,6 +213,29 @@ public async Task SendEnrRequest_should_ping_then_enr_request_and_return_respons Assert.That(result.NodeRecord.GetHex(), Is.EqualTo(_selfNodeRecord.GetHex())); } + [Test] + [CancelAfter(10000)] + public void SendEnrRequest_should_reject_unsolicited_response_with_wrong_keccak(CancellationToken token) + { + ConfigureBondCallback(); + + _msgSender + .When(x => x.SendMsg(Arg.Any())) + .Do(ci => + { + EnrRequestMsg sent = (EnrRequestMsg)ci[0]!; + sent.Hash = TestItem.KeccakA.BytesToArray(); + EnrResponseMsg response = AddReceiverFarAddress(new EnrResponseMsg(_receiver.Address, _selfNodeRecord, TestItem.KeccakB)); + Task.Run(() => _adapter.OnIncomingMsg(response)); + }); + + using CancellationTokenSource shortTimeout = CancellationTokenSource.CreateLinkedTokenSource(token); + shortTimeout.CancelAfter(500); + + Assert.ThrowsAsync(Is.InstanceOf(), + async () => await _adapter.SendEnrRequest(_receiver, shortTimeout.Token)); + } + [Test] [CancelAfter(10000)] public async Task Timed_out_response_handler_should_not_consume_later_unsolicited_message(CancellationToken token) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs new file mode 100644 index 000000000000..b4e6e496ef3d --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class KBucketTreeTests +{ + private static readonly ValueHash256 SelfHash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + + private static KBucketTree CreateTree(int k = 4, int beta = 0) => + new( + new KademliaConfig { CurrentNodeId = SelfHash, KSize = k, Beta = beta }, + new IdentityNodeHashProvider(), + LimboLogs.Instance); + + [Test] + public void Split_should_preserve_lru_order_in_child_buckets() + { + KBucketTree tree = CreateTree(k: 2, beta: 0); + + ValueHash256 left0 = HashAtDistance(255, 0x10); + ValueHash256 left1 = HashAtDistance(255, 0x11); + ValueHash256 right0 = HashAtDistance(254, 0x20); + ValueHash256 right1 = HashAtDistance(254, 0x21); + + tree.TryAddOrRefresh(left0, left0, out _); + tree.TryAddOrRefresh(right0, right0, out _); + tree.TryAddOrRefresh(left1, left1, out _); + tree.TryAddOrRefresh(right1, right1, out _); + + ValueHash256[] leftBucket = tree.GetAllAtDistance(255); + ValueHash256[] rightBucket = tree.GetAllAtDistance(254); + + Assert.That(leftBucket[0], Is.EqualTo(left1)); + Assert.That(leftBucket[1], Is.EqualTo(left0)); + Assert.That(rightBucket[0], Is.EqualTo(right1)); + Assert.That(rightBucket[1], Is.EqualTo(right0)); + } + + [Test] + public void GetAllAtDistance_should_include_nodes_in_deeper_split_buckets() + { + KBucketTree tree = CreateTree(k: 2, beta: 4); + + ValueHash256 deep1 = HashAtDistance(252, 0x40); + ValueHash256 deep2 = HashAtDistance(252, 0x41); + ValueHash256 deep3 = HashAtDistance(252, 0x42); + + tree.TryAddOrRefresh(deep1, deep1, out _); + tree.TryAddOrRefresh(deep2, deep2, out _); + tree.TryAddOrRefresh(deep3, deep3, out _); + + HashSet atDistance = tree.GetAllAtDistance(252).ToHashSet(); + Assert.That(atDistance, Is.SupersetOf(new[] { deep1, deep2 })); + Assert.That(atDistance.IsSubsetOf(new[] { deep1, deep2, deep3 }), Is.True); + } + + private static ValueHash256 HashAtDistance(int distance, byte tag) + { + ValueHash256 h = Hash256XorUtils.GetRandomHashAtDistance(SelfHash, distance, new Random(tag)); + return h; + } + + private sealed class IdentityNodeHashProvider : INodeHashProvider + { + public ValueHash256 GetHash(ValueHash256 node) => node; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs new file mode 100644 index 000000000000..c5cda37f716a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Core.Crypto; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class LookupKNearestNeighbourTests +{ + private static readonly ValueHash256 Self = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + + [TestCase(1)] + [TestCase(3)] + [CancelAfter(10000)] + public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, CancellationToken token) + { + IRoutingTable routingTable = Substitute.For>(); + ValueHash256 seed = new("0x1100000000000000000000000000000000000000000000000000000000000000"); + routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns([seed]); + + INodeHealthTracker health = Substitute.For>(); + + LookupKNearestNeighbour lookup = new( + routingTable, + new IdentityNodeHashProvider(), + health, + new KademliaConfig + { + CurrentNodeId = Self, + Alpha = alpha, + KSize = 8, + LookupFindNeighbourHardTimeout = TimeSpan.FromSeconds(30), + }, + LimboLogs.Instance); + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + + Task task = lookup.Lookup( + seed, + 8, + async (_, t) => + { + await Task.Delay(Timeout.Infinite, t); + return null; + }, + cts.Token); + + cts.CancelAfter(100); + + ValueHash256[] _ = await task; + health.Received().OnRequestFailed(seed); + } + + [TestCase(1)] + [TestCase(3)] + [CancelAfter(10000)] + public async Task Lookup_should_return_results_with_different_alpha(int alpha, CancellationToken token) + { + IRoutingTable routingTable = Substitute.For>(); + ValueHash256[] seeds = + [ + new("0x1100000000000000000000000000000000000000000000000000000000000000"), + new("0x2200000000000000000000000000000000000000000000000000000000000000"), + new("0x3300000000000000000000000000000000000000000000000000000000000000"), + ]; + routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + .Returns(seeds); + + Dictionary neighbours = new() + { + [seeds[0]] = [new("0x4400000000000000000000000000000000000000000000000000000000000000")], + [seeds[1]] = [new("0x5500000000000000000000000000000000000000000000000000000000000000")], + [seeds[2]] = [], + }; + + LookupKNearestNeighbour lookup = new( + routingTable, + new IdentityNodeHashProvider(), + Substitute.For>(), + new KademliaConfig + { + CurrentNodeId = Self, + Alpha = alpha, + KSize = 8, + LookupFindNeighbourHardTimeout = TimeSpan.FromSeconds(10), + }, + LimboLogs.Instance); + + ValueHash256[] result = await lookup.Lookup( + Self, + 8, + (node, _) => Task.FromResult(neighbours.GetValueOrDefault(node, [])), + token); + + Assert.That(result, Is.Not.Empty); + } + + private sealed class IdentityNodeHashProvider : INodeHashProvider + { + public ValueHash256 GetHash(ValueHash256 node) => node; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index c105f264df28..dade1e3ef83b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; @@ -33,6 +35,71 @@ public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSe Assert.That(routingTable.AddCalls[1].Node, Is.EqualTo(self)); } + [Test] + [CancelAfter(10000)] + public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(CancellationToken token) + { + const string self = "self"; + const string remote = "remote"; + const string stale = "stale"; + RoutingTableStub routingTable = new() { ToRefresh = stale }; + IKademliaMessageSender sender = Substitute.For>(); + sender.Ping(stale, Arg.Any()) + .Returns(Task.FromException(new OperationCanceledException())); + + NodeHealthTracker tracker = new( + new KademliaConfig + { + CurrentNodeId = self, + RefreshPingTimeout = TimeSpan.FromMilliseconds(50), + }, + routingTable, + new StringNodeHashProvider(), + sender, + LimboLogs.Instance); + + tracker.OnIncomingMessageFrom(remote); + + ValueHash256 staleHash = ValueKeccak.Compute(stale); + await AssertEventuallyAsync(() => routingTable.RemoveCalls.Contains(staleHash), token); + } + + [Test] + [CancelAfter(10000)] + public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken token) + { + const string self = "self"; + const string remote = "remote"; + const string stale = "stale"; + RoutingTableStub routingTable = new() { ToRefresh = stale }; + IKademliaMessageSender sender = Substitute.For>(); + sender.Ping(stale, Arg.Any()).Returns(Task.CompletedTask); + + NodeHealthTracker tracker = new( + new KademliaConfig { CurrentNodeId = self }, + routingTable, + new StringNodeHashProvider(), + sender, + LimboLogs.Instance); + + tracker.OnIncomingMessageFrom(remote); + + ValueHash256 staleHash = ValueKeccak.Compute(stale); + // OnIncomingMessageFrom inside TryRefresh's success branch re-adds the stale node — wait for that. + await AssertEventuallyAsync(() => routingTable.HasAddedNode(staleHash), token); + Assert.That(routingTable.RemoveCalls, Does.Not.Contain(staleHash)); + } + + private static async Task AssertEventuallyAsync(Func condition, CancellationToken token) + { + for (int i = 0; i < 50; i++) + { + if (condition()) return; + await Task.Delay(50, token); + } + Assert.Fail("Condition not met within timeout."); + } + [Test] public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() { @@ -69,7 +136,7 @@ private sealed class RoutingTableStub : IRoutingTable public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, string item, out string? toRefresh) { - AddCalls.Add((hash, item)); + lock (AddCalls) AddCalls.Add((hash, item)); if (AddCalls.Count == 1) { toRefresh = ToRefresh; @@ -80,9 +147,21 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, string item, out st return BucketAddResult.Refreshed; } + public bool HasAddedNode(ValueHash256 hash) + { + lock (AddCalls) + { + foreach ((ValueHash256 h, string _) in AddCalls) + { + if (h == hash) return true; + } + } + return false; + } + public bool Remove(in ValueHash256 hash) { - RemoveCalls.Add(hash); + lock (RemoveCalls) RemoveCalls.Add(hash); return true; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 9f02b2841cb9..32c8e5b41f52 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -40,7 +40,7 @@ protected override void Load(ContainerBuilder builder) => builder Alpha = discoveryConfig.Concurrency, Beta = discoveryConfig.BitsPerHop, - LookupFindNeighbourHardTimout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), // TODO: This seems very low. + LookupFindNeighbourHardTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), // TODO: This seems very low. RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), BootNodes = bootNodes diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs index bdbc2ea0f78a..a9f01cbacd19 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs @@ -1,13 +1,18 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Core.Extensions; using Nethermind.Network.Discovery.Messages; namespace Nethermind.Network.Discovery.Discv4; -public class EnrResponseHandler : ITaskCompleter +public class EnrResponseHandler(EnrRequestMsg request) : ITaskCompleter { public TaskCompletionSource TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - public bool Handle(DiscoveryMsg msg) => msg is EnrResponseMsg resp && TaskCompletionSource.TrySetResult(resp); + public bool Handle(DiscoveryMsg msg) => + msg is EnrResponseMsg resp + && request.Hash is { } expected + && Bytes.AreEqual(resp.RequestKeccak.Bytes, expected.Span) + && TaskCompletionSource.TrySetResult(resp); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index b353c1438ca1..eaabb83db0bd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -174,7 +174,7 @@ public async Task Ping(Node receiver, CancellationToken token) PingMsg msg = new(receiver.Address, CalculateExpirationTime(), kademliaConfig.CurrentNodeId.Address) { - EnrSequence = nodeRecordProvider.Current.EnrSequence // optional and does not seems to be used anywhere. + EnrSequence = nodeRecordProvider.Current.EnrSequence // optional and does not seem to be used anywhere. }; session.OnPingSent(); _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, session, msg, token); @@ -205,7 +205,7 @@ public async Task SendEnrRequest(Node receiver, CancellationToke EnrRequestMsg msg = new(receiver.Address, CalculateExpirationTime()); - return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(), receiver, session, msg, token); + return await CallAndWaitForResponse(MsgType.EnrResponse, new EnrResponseHandler(msg), receiver, session, msg, token); }, token); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 95a2410fb02f..74d94092ede5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -14,7 +14,7 @@ namespace Nethermind.Network.Discovery.Discv4; public class KademliaNodeSource( IKademlia kademlia, - IIteratorNodeLookup lookup2, + IIteratorNodeLookup lookup, IKademliaDiscv4Adapter discv4Adapter, IDiscoveryConfig discoveryConfig, ILogManager logManager) @@ -36,7 +36,7 @@ async Task DiscoverAsync(PublicKey target) bool anyFound = false; int count = 0; - await foreach (Node node in lookup2.Lookup(target, token)) + await foreach (Node node in lookup.Lookup(target, token)) { if (!discv4Adapter.GetSession(node).HasReceivedPong) { @@ -117,8 +117,9 @@ async Task DiscoverAsync(PublicKey target) } finally { - await discoverTask; kademlia.OnNodeAdded -= Handler; + ch.Writer.TryComplete(); + await discoverTask; } yield break; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs index 4bc4ca0481a7..27beaeaa3e0e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs @@ -84,9 +84,23 @@ public bool Remove(ValueHash256 hash) return false; } - public TNode[] GetAll() => [.. _hashMapping.Select(kv => kv.Value.Value.Item2)]; + public TNode[] GetAll() + { + using McsLock.Disposable _ = _lock.Acquire(); + TNode[] result = new TNode[_queue.Count]; + int i = 0; + foreach ((ValueHash256, TNode node) entry in _queue) result[i++] = entry.node; + return result; + } - public IEnumerable<(ValueHash256, TNode)> GetAllWithHash() => _queue; + public (ValueHash256, TNode)[] GetAllWithHash() + { + using McsLock.Disposable _ = _lock.Acquire(); + (ValueHash256, TNode)[] result = new (ValueHash256, TNode)[_queue.Count]; + int i = 0; + foreach ((ValueHash256, TNode) entry in _queue) result[i++] = entry; + return result; + } public bool Contains(in ValueHash256 hash) => _hashMapping.ContainsKey(hash); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs index 2322595c67dc..4f042e06435b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs @@ -33,7 +33,7 @@ public static int CalculateLogDistance(ValueHash256 h1, ValueHash256 h2) return MaxDistance - zeros; } - public static int MaxDistance => 256; + public const int MaxDistance = 256; public static int Compare(ValueHash256 a, ValueHash256 b, ValueHash256 c) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs index 839a2f4704f2..2eeff2a902ec 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs @@ -44,7 +44,7 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNo public TNode[] GetAll() => _cachedArray; - public IEnumerable<(ValueHash256, TNode)> GetAllWithHash() => _items.GetAllWithHash(); + public (ValueHash256, TNode)[] GetAllWithHash() => _items.GetAllWithHash(); public bool RemoveAndReplace(in ValueHash256 hash) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs index 160b1c278468..d21506b2744d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs @@ -40,47 +40,50 @@ public KBucketTree(KademliaConfig config, INodeHashProvider nodeHa public BucketAddResult TryAddOrRefresh(in ValueHash256 nodeHash, TNode node, out TNode? toRefresh) { - using McsLock.Disposable _ = _lock.Acquire(); - - if (_logger.IsDebug) _logger.Debug($"Adding node {node} with XOR distance {Hash256XorUtils.XorDistance(_currentNodeHash, nodeHash)}"); - - TreeNode current = _root; - // As in, what would be the depth of the node assuming all branch on the traversal is populated. - int logDistance = Hash256XorUtils.MaxDistance - Hash256XorUtils.CalculateLogDistance(_currentNodeHash, nodeHash); - int depth = 0; - while (true) + BucketAddResult resp; + bool fireAdded; + using (_lock.Acquire()) { - if (current.IsLeaf) + if (_logger.IsDebug) _logger.Debug($"Adding node {node} with XOR distance {Hash256XorUtils.XorDistance(_currentNodeHash, nodeHash)}"); + + TreeNode current = _root; + // As in, what would be the depth of the node assuming all branch on the traversal is populated. + int logDistance = Hash256XorUtils.MaxDistance - Hash256XorUtils.CalculateLogDistance(_currentNodeHash, nodeHash); + int depth = 0; + while (true) { - if (_logger.IsTrace) _logger.Trace($"Reached leaf node at depth {depth}"); - BucketAddResult resp = current.Bucket.TryAddOrRefresh(nodeHash, node, out toRefresh); - if (resp == BucketAddResult.Added) - { - OnNodeAdded?.Invoke(this, node); - } - if (resp is BucketAddResult.Added or BucketAddResult.Refreshed) + if (current.IsLeaf) { - if (_logger.IsDebug) _logger.Debug($"Successfully added/refreshed node {node} in bucket at depth {depth}"); - return resp; + if (_logger.IsTrace) _logger.Trace($"Reached leaf node at depth {depth}"); + resp = current.Bucket.TryAddOrRefresh(nodeHash, node, out toRefresh); + fireAdded = resp == BucketAddResult.Added; + if (resp is BucketAddResult.Added or BucketAddResult.Refreshed) + { + if (_logger.IsDebug) _logger.Debug($"Successfully added/refreshed node {node} in bucket at depth {depth}"); + break; + } + + if (resp == BucketAddResult.Full && ShouldSplit(depth, logDistance)) + { + if (_logger.IsTrace) _logger.Trace($"Splitting bucket at depth {depth}"); + SplitBucket(depth, current); + continue; + } + + if (_logger.IsDebug) _logger.Debug($"Failed to add node {nodeHash} {node}. Bucket at depth {depth} is full. {_k} {current.Bucket.Count}"); + break; } - if (resp == BucketAddResult.Full && ShouldSplit(depth, logDistance)) - { - if (_logger.IsTrace) _logger.Trace($"Splitting bucket at depth {depth}"); - SplitBucket(depth, current); - continue; - } + bool goRight = GetBit(nodeHash, depth); + if (_logger.IsTrace) _logger.Trace($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); - if (_logger.IsDebug) _logger.Debug($"Failed to add node {nodeHash} {node}. Bucket at depth {depth} is full. {_k} {current.Bucket.GetAllWithHash().Count()}"); - return resp; + current = goRight ? current.Right! : current.Left!; + depth++; } - - bool goRight = GetBit(nodeHash, depth); - if (_logger.IsTrace) _logger.Trace($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); - - current = goRight ? current.Right! : current.Left!; - depth++; } + + if (fireAdded) OnNodeAdded?.Invoke(this, node); + return resp; } public TNode? GetByHash(ValueHash256 hash) @@ -125,14 +128,14 @@ private void SplitBucket(int depth, TreeNode node) if (_logger.IsDebug) _logger.Debug($"Created children at depth {depth + 1}"); - // The reverse is because the bucket is iterated from the most recent. Without it - // reading would have reversed this order. - foreach ((ValueHash256, TNode) item in node.Bucket.GetAllWithHash().Reverse()) + // Iterate from oldest to newest so the new buckets preserve original LRU order. + (ValueHash256, TNode)[] items = node.Bucket.GetAllWithHash(); + for (int i = items.Length - 1; i >= 0; i--) { - ValueHash256 itemHash = item.Item1; + (ValueHash256 itemHash, TNode value) = items[i]; TreeNode? targetNode = GetBit(itemHash, depth) ? node.Right : node.Left; - targetNode.Bucket.TryAddOrRefresh(itemHash, item.Item2, out _); - if (_logger.IsDebug) _logger.Debug($"Moved item {item} to {(GetBit(itemHash, depth) ? "right" : "left")} child"); + targetNode.Bucket.TryAddOrRefresh(itemHash, value, out _); + if (_logger.IsDebug) _logger.Debug($"Moved item ({itemHash}, {value}) to {(GetBit(itemHash, depth) ? "right" : "left")} child"); } node.Bucket.Clear(); @@ -141,18 +144,18 @@ private void SplitBucket(int depth, TreeNode node) public bool Remove(in ValueHash256 nodeHash) { - using McsLock.Disposable _ = _lock.Acquire(); - - if (_logger.IsDebug) _logger.Debug($"Attempting to remove node {nodeHash} with hash {nodeHash}"); - - KBucket bucket = GetBucketForHash(nodeHash); - TNode? removedNode = bucket.GetByHash(nodeHash); - bool removed = bucket.RemoveAndReplace(nodeHash); - if (removed && removedNode is not null) + bool removed; + TNode? removedNode; + using (_lock.Acquire()) { - OnNodeRemoved?.Invoke(this, removedNode); + if (_logger.IsDebug) _logger.Debug($"Attempting to remove node {nodeHash} with hash {nodeHash}"); + + KBucket bucket = GetBucketForHash(nodeHash); + removedNode = bucket.GetByHash(nodeHash); + removed = bucket.RemoveAndReplace(nodeHash); } + if (removed && removedNode is not null) OnNodeRemoved?.Invoke(this, removedNode); return removed; } @@ -174,9 +177,13 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L { if (depth <= targetDepth) { - result.AddRange(node.Bucket.GetAllWithHash() - .Where(kv => Hash256XorUtils.CalculateLogDistance(kv.Item1, _currentNodeHash) == distance) - .Select(kv => kv.Item2)); + foreach ((ValueHash256 hash, TNode item) in node.Bucket.GetAllWithHash()) + { + if (Hash256XorUtils.CalculateLogDistance(hash, _currentNodeHash) == distance) + { + result.Add(item); + } + } } else { @@ -311,21 +318,20 @@ public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bo } } - IEnumerable<(ValueHash256, TNode)> iterator = IterateNeighbour(hash); - - if (exclude != null) - { - iterator = iterator - .Where(kv => kv.Item1 != exclude.Value); - } - - if (excludeSelf) + TNode[] resultArr = new TNode[_k]; + int count = 0; + foreach ((ValueHash256 itemHash, TNode item) in IterateNeighbour(hash)) { - iterator = iterator - .Where(kv => kv.Item1 != _currentNodeHash); + if (exclude != null && itemHash == exclude.Value) continue; + if (excludeSelf && itemHash == _currentNodeHash) continue; + resultArr[count++] = item; + if (count == _k) break; } - return [.. iterator.Take(_k).Select(kv => kv.Item2)]; + if (count == _k) return resultArr; + TNode[] truncated = new TNode[count]; + Array.Copy(resultArr, truncated, count); + return truncated; } private bool GetBit(ValueHash256 hash, int index) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs index c86add930401..b2a6b727bf53 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs @@ -57,9 +57,7 @@ public Kademlia( public TNode CurrentNode => _currentNodeId; - public void AddOrRefresh(TNode node) => - // It add to routing table and does the whole refresh logid. - _nodeHealthTracker.OnIncomingMessageFrom(node); + public void AddOrRefresh(TNode node) => _nodeHealthTracker.OnIncomingMessageFrom(node); public void Remove(TNode node) => _routingTable.Remove(_keyOperator.GetNodeHash(node)); @@ -86,8 +84,18 @@ public async Task Run(CancellationToken token) { while (true) { - await Bootstrap(token); - // The main loop can potentially be parallelized with multiple concurrent lookups to improve efficiency. + try + { + await Bootstrap(token); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error("Bootstrap iteration failed.", e); + } await Task.Delay(_refreshInterval, token); } @@ -112,6 +120,10 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => { // Unreachable } + catch (Exception e) + { + if (_logger.IsDebug) _logger.Debug($"Bootnode ping failed for {node}. {e}"); + } }); if (_logger.IsInfo) _logger.Info($"Online bootnodes: {onlineBootNodes}"); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs index 66768a537229..4ab4b410746d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs @@ -38,7 +38,7 @@ public class KademliaConfig /// /// The timeout for each find neighbour call lookup /// - public TimeSpan LookupFindNeighbourHardTimout { get; set; } = TimeSpan.FromSeconds(10); + public TimeSpan LookupFindNeighbourHardTimeout { get; set; } = TimeSpan.FromSeconds(10); /// /// The timeout for a ping message during a refresh after which the node is considered to be offline. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs index d21ac13c4926..78571ddf5b32 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs @@ -24,7 +24,7 @@ public class LookupKNearestNeighbour( KademliaConfig config, ILogManager logManager) : ILookupAlgo where TNode : notnull { - private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimout; + private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimeout; private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( @@ -62,7 +62,7 @@ CancellationToken token bestSeen.Enqueue((nodeHash, node), nodeHash); } - TaskCompletionSource roundComplete = new(token); + TaskCompletionSource roundComplete = new(TaskCreationOptions.RunContinuationsAsynchronously); int closestNodeRound = 0; int currentRound = 0; int queryingTask = 0; @@ -70,7 +70,7 @@ CancellationToken token Task[] worker = [.. Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => { - while (!finished) + while (!Volatile.Read(ref finished)) { token.ThrowIfCancellationRequested(); if (!TryGetNodeToQuery(out (ValueHash256 hash, TNode node)? toQuery)) @@ -78,7 +78,7 @@ CancellationToken token if (queryingTask > 0) { // Need to wait for all querying tasks first here. - await Task.WhenAny(roundComplete.Task, Task.Delay(100, token)); + await Task.WhenAny(Volatile.Read(ref roundComplete).Task, Task.Delay(100, token)); continue; } @@ -104,7 +104,14 @@ CancellationToken token finally { Interlocked.Decrement(ref queryingTask); - if (roundComplete.TrySetResult()) roundComplete = new TaskCompletionSource(token); + TaskCompletionSource current = Volatile.Read(ref roundComplete); + if (current.TrySetResult()) + { + Interlocked.CompareExchange( + ref roundComplete, + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously), + current); + } } } }, token))]; @@ -112,7 +119,7 @@ CancellationToken token // When any of the worker is finished, we consider the whole query as done. // This prevent this operation from hanging on a timed out request await Task.WhenAny(worker); - finished = true; + Volatile.Write(ref finished, true); await cts.CancelAsync(); return CompileResult(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs index fb4bbc6b40db..6ce00eef3e32 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs @@ -13,7 +13,7 @@ public class EnrRequestMsg : DiscoveryMsg { public override MsgType MsgType => MsgType.EnrRequest; - public Memory? Hash { get; } + public Memory? Hash { get; set; } public EnrRequestMsg(IPEndPoint farAddress, long expirationDate) : base(farAddress, expirationDate) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs index 2b43c626561f..cca1af0b8cb9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs @@ -25,6 +25,10 @@ public void Serialize(IByteBuffer byteBuffer, EnrRequestMsg msg) byteBuffer.ResetIndex(); AddSignatureAndMdc(byteBuffer, length + 1); + + byteBuffer.MarkReaderIndex(); + msg.Hash = byteBuffer.Slice(0, 32).ReadAllBytesAsArray(); + byteBuffer.ResetReaderIndex(); } public EnrRequestMsg Deserialize(IByteBuffer msgBytes) From d745ab515a39a4696ecff7ea3e7b5d334fa69f4a Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Fri, 22 May 2026 10:51:06 +0200 Subject: [PATCH 105/182] test(discovery): dedupe new Kademlia and persistence tests - Extract IdentityNodeHashProvider into a shared file so KBucketTreeTests and LookupKNearestNeighbourTests stop redefining it. - KBucketTreeTests: pull common KBucketTree construction into CreateTree and Add helpers; replace duplicated Assert.That index lookups with single Is.EqualTo array comparisons. - LookupKNearestNeighbourTests: extract CreateLookup factory and named seed/neighbour hash constants so the two test methods only differ in what they're actually asserting. - NodeHealthTrackerTests: extract a CreateTracker helper (and Self/Remote /Stale constants) so each test body only sets the bits unique to its scenario; promote StringNodeHashProvider to a singleton. - DiscoveryPersistenceManagerTests: shared NodeA / NodeB fixtures plus a CreateManager helper and a PingReceived assertion helper; drop the per-test ad-hoc NetworkNode arrays. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DiscoveryPersistenceManagerTests.cs | 72 ++++------ .../Kademlia/IdentityNodeHashProvider.cs | 14 ++ .../Kademlia/KBucketTreeTests.cs | 57 ++++---- .../Kademlia/LookupKNearestNeighbourTests.cs | 74 ++++------ .../Kademlia/NodeHealthTrackerTests.cs | 130 +++++++++--------- 5 files changed, 156 insertions(+), 191 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index 11345f17f7b4..7c29036427e4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -22,6 +22,9 @@ namespace Nethermind.Network.Discovery.Test [Parallelizable(ParallelScope.Self)] public class DiscoveryPersistenceManagerTests { + private static readonly NetworkNode NodeA = new(TestItem.PublicKeyA, "192.168.1.1", 30303, 0); + private static readonly NetworkNode NodeB = new(TestItem.PublicKeyB, "192.168.1.2", 30303, 0); + private MemDb _discoveryDb = null!; private INetworkStorage _networkStorage = null!; private INodeStatsManager _nodeStatsManager = null!; @@ -47,13 +50,7 @@ public void Setup() _logManager = LimboLogs.Instance; _kademlia = Substitute.For>(); - _persistenceManager = new DiscoveryPersistenceManager( - _networkStorage, - _nodeStatsManager, - _discv4Adapter, - _kademlia, - _discoveryConfig, - _logManager); + _persistenceManager = CreateManager(_networkStorage); } [TearDown] @@ -63,22 +60,30 @@ public async Task Teardown() _discoveryDb.Dispose(); } + private DiscoveryPersistenceManager CreateManager(INetworkStorage storage) => new( + storage, + _nodeStatsManager, + _discv4Adapter, + _kademlia, + _discoveryConfig, + _logManager); + + private static Task PingReceived(IKademliaDiscv4Adapter adapter, NetworkNode node, int times = 1) => + adapter.Received(times).Ping( + Arg.Is(n => n.Id.Equals(node.NodeId)), + Arg.Any()); + [Test] [CancelAfter(10000)] public async Task AddPersistedNodes_Should_Ping_Each_Valid_Node(CancellationToken cancellationToken) { - NetworkNode[] networkNodes = - [ - new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0), - new NetworkNode(TestItem.PublicKeyB, "192.168.1.2", 30303, 0) - ]; - - _networkStorage.UpdateNodes(networkNodes); + NetworkNode[] nodes = [NodeA, NodeB]; + _networkStorage.UpdateNodes(nodes); await _persistenceManager.LoadPersistedNodes(cancellationToken); - await _discv4Adapter.Received(networkNodes.Length).Ping( - Arg.Is(n => networkNodes.Any(nn => nn.NodeId.Equals(n.Id) && nn.Host == n.Host && nn.Port == n.Port)), + await _discv4Adapter.Received(nodes.Length).Ping( + Arg.Is(n => nodes.Any(nn => nn.NodeId.Equals(n.Id) && nn.Host == n.Host && nn.Port == n.Port)), Arg.Any()); } @@ -87,23 +92,12 @@ await _discv4Adapter.Received(networkNodes.Length).Ping( public async Task AddPersistedNodes_Should_Skip_Nodes_That_Fail_Node_Construction(CancellationToken cancellationToken) { INetworkStorage storageMock = Substitute.For(); - NetworkNode goodNode = new(TestItem.PublicKeyA, "192.168.1.1", 30303, 0); NetworkNode badNode = new(TestItem.PublicKeyB, "192.168.1.2", -1, 0); - storageMock.GetPersistedNodes().Returns([badNode, goodNode]); + storageMock.GetPersistedNodes().Returns([badNode, NodeA]); - DiscoveryPersistenceManager manager = new( - storageMock, - _nodeStatsManager, - _discv4Adapter, - _kademlia, - _discoveryConfig, - _logManager); + await CreateManager(storageMock).LoadPersistedNodes(cancellationToken); - await manager.LoadPersistedNodes(cancellationToken); - - await _discv4Adapter.Received(1).Ping( - Arg.Is(n => n.Id.Equals(goodNode.NodeId)), - Arg.Any()); + await PingReceived(_discv4Adapter, NodeA); await _discv4Adapter.DidNotReceive().Ping( Arg.Is(n => n.Id.Equals(badNode.NodeId)), Arg.Any()); @@ -113,23 +107,11 @@ await _discv4Adapter.DidNotReceive().Ping( [CancelAfter(10000)] public async Task AddPersistedNodes_Should_Handle_Ping_Exceptions(CancellationToken cancellationToken) { - NetworkNode[] networkNodes = - [ - new NetworkNode(TestItem.PublicKeyA, "192.168.1.1", 30303, 0), - new NetworkNode(TestItem.PublicKeyB, "192.168.1.2", 30303, 0) - ]; - - _networkStorage.UpdateNodes(networkNodes); + _networkStorage.UpdateNodes([NodeA, NodeB]); - // First ping succeeds, second one throws - _discv4Adapter.Ping( - Arg.Is(n => n.Id.Equals(networkNodes[0].NodeId)), - Arg.Any()) + _discv4Adapter.Ping(Arg.Is(n => n.Id.Equals(NodeA.NodeId)), Arg.Any()) .Returns(Task.CompletedTask); - - _discv4Adapter.Ping( - Arg.Is(n => n.Id.Equals(networkNodes[1].NodeId)), - Arg.Any()) + _discv4Adapter.Ping(Arg.Is(n => n.Id.Equals(NodeB.NodeId)), Arg.Any()) .Returns(x => throw new Exception("Test exception")); await _persistenceManager.LoadPersistedNodes(cancellationToken); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs new file mode 100644 index 000000000000..df4b9fb6cedf --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +internal sealed class IdentityNodeHashProvider : INodeHashProvider +{ + public static readonly IdentityNodeHashProvider Instance = new(); + + public ValueHash256 GetHash(ValueHash256 node) => node; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs index b4e6e496ef3d..2280e2d5d179 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Collections.Generic; using System.Linq; using Nethermind.Core.Crypto; using Nethermind.Logging; @@ -15,11 +14,16 @@ public class KBucketTreeTests { private static readonly ValueHash256 SelfHash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); - private static KBucketTree CreateTree(int k = 4, int beta = 0) => - new( - new KademliaConfig { CurrentNodeId = SelfHash, KSize = k, Beta = beta }, - new IdentityNodeHashProvider(), - LimboLogs.Instance); + private static KBucketTree CreateTree(int k = 4, int beta = 0) => new( + new KademliaConfig { CurrentNodeId = SelfHash, KSize = k, Beta = beta }, + IdentityNodeHashProvider.Instance, + LimboLogs.Instance); + + private static void Add(KBucketTree tree, ValueHash256 hash) => + tree.TryAddOrRefresh(hash, hash, out _); + + private static ValueHash256 HashAtDistance(int distance, byte tag) => + Hash256XorUtils.GetRandomHashAtDistance(SelfHash, distance, new Random(tag)); [Test] public void Split_should_preserve_lru_order_in_child_buckets() @@ -31,18 +35,13 @@ public void Split_should_preserve_lru_order_in_child_buckets() ValueHash256 right0 = HashAtDistance(254, 0x20); ValueHash256 right1 = HashAtDistance(254, 0x21); - tree.TryAddOrRefresh(left0, left0, out _); - tree.TryAddOrRefresh(right0, right0, out _); - tree.TryAddOrRefresh(left1, left1, out _); - tree.TryAddOrRefresh(right1, right1, out _); - - ValueHash256[] leftBucket = tree.GetAllAtDistance(255); - ValueHash256[] rightBucket = tree.GetAllAtDistance(254); + Add(tree, left0); + Add(tree, right0); + Add(tree, left1); + Add(tree, right1); - Assert.That(leftBucket[0], Is.EqualTo(left1)); - Assert.That(leftBucket[1], Is.EqualTo(left0)); - Assert.That(rightBucket[0], Is.EqualTo(right1)); - Assert.That(rightBucket[1], Is.EqualTo(right0)); + Assert.That(tree.GetAllAtDistance(255), Is.EqualTo(new[] { left1, left0 })); + Assert.That(tree.GetAllAtDistance(254), Is.EqualTo(new[] { right1, right0 })); } [Test] @@ -54,23 +53,13 @@ public void GetAllAtDistance_should_include_nodes_in_deeper_split_buckets() ValueHash256 deep2 = HashAtDistance(252, 0x41); ValueHash256 deep3 = HashAtDistance(252, 0x42); - tree.TryAddOrRefresh(deep1, deep1, out _); - tree.TryAddOrRefresh(deep2, deep2, out _); - tree.TryAddOrRefresh(deep3, deep3, out _); - - HashSet atDistance = tree.GetAllAtDistance(252).ToHashSet(); - Assert.That(atDistance, Is.SupersetOf(new[] { deep1, deep2 })); - Assert.That(atDistance.IsSubsetOf(new[] { deep1, deep2, deep3 }), Is.True); - } + Add(tree, deep1); + Add(tree, deep2); + Add(tree, deep3); - private static ValueHash256 HashAtDistance(int distance, byte tag) - { - ValueHash256 h = Hash256XorUtils.GetRandomHashAtDistance(SelfHash, distance, new Random(tag)); - return h; - } - - private sealed class IdentityNodeHashProvider : INodeHashProvider - { - public ValueHash256 GetHash(ValueHash256 node) => node; + ValueHash256[] expectedCandidates = [deep1, deep2, deep3]; + ValueHash256[] result = tree.GetAllAtDistance(252); + Assert.That(result, Is.SupersetOf(new[] { deep1, deep2 })); + Assert.That(result.All(expectedCandidates.Contains), Is.True); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index c5cda37f716a..089622bcf079 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -16,36 +16,47 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; public class LookupKNearestNeighbourTests { private static readonly ValueHash256 Self = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + private static readonly ValueHash256 Seed1 = new("0x1100000000000000000000000000000000000000000000000000000000000000"); + private static readonly ValueHash256 Seed2 = new("0x2200000000000000000000000000000000000000000000000000000000000000"); + private static readonly ValueHash256 Seed3 = new("0x3300000000000000000000000000000000000000000000000000000000000000"); + private static readonly ValueHash256 N1 = new("0x4400000000000000000000000000000000000000000000000000000000000000"); + private static readonly ValueHash256 N2 = new("0x5500000000000000000000000000000000000000000000000000000000000000"); - [TestCase(1)] - [TestCase(3)] - [CancelAfter(10000)] - public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, CancellationToken token) + private static (LookupKNearestNeighbour Lookup, IRoutingTable Routing, INodeHealthTracker Health) CreateLookup(int alpha, TimeSpan hardTimeout, ValueHash256[] seeds) { - IRoutingTable routingTable = Substitute.For>(); - ValueHash256 seed = new("0x1100000000000000000000000000000000000000000000000000000000000000"); - routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns([seed]); + IRoutingTable routing = Substitute.For>(); + routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); INodeHealthTracker health = Substitute.For>(); LookupKNearestNeighbour lookup = new( - routingTable, - new IdentityNodeHashProvider(), + routing, + IdentityNodeHashProvider.Instance, health, new KademliaConfig { CurrentNodeId = Self, Alpha = alpha, KSize = 8, - LookupFindNeighbourHardTimeout = TimeSpan.FromSeconds(30), + LookupFindNeighbourHardTimeout = hardTimeout, }, LimboLogs.Instance); + return (lookup, routing, health); + } + + [TestCase(1)] + [TestCase(3)] + [CancelAfter(10000)] + public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, CancellationToken token) + { + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + CreateLookup(alpha, TimeSpan.FromSeconds(30), [Seed1]); + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); Task task = lookup.Lookup( - seed, + Seed1, 8, async (_, t) => { @@ -56,8 +67,8 @@ public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, Ca cts.CancelAfter(100); - ValueHash256[] _ = await task; - health.Received().OnRequestFailed(seed); + _ = await task; + health.Received().OnRequestFailed(Seed1); } [TestCase(1)] @@ -65,36 +76,16 @@ public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, Ca [CancelAfter(10000)] public async Task Lookup_should_return_results_with_different_alpha(int alpha, CancellationToken token) { - IRoutingTable routingTable = Substitute.For>(); - ValueHash256[] seeds = - [ - new("0x1100000000000000000000000000000000000000000000000000000000000000"), - new("0x2200000000000000000000000000000000000000000000000000000000000000"), - new("0x3300000000000000000000000000000000000000000000000000000000000000"), - ]; - routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns(seeds); + (LookupKNearestNeighbour lookup, _, _) = + CreateLookup(alpha, TimeSpan.FromSeconds(10), [Seed1, Seed2, Seed3]); Dictionary neighbours = new() { - [seeds[0]] = [new("0x4400000000000000000000000000000000000000000000000000000000000000")], - [seeds[1]] = [new("0x5500000000000000000000000000000000000000000000000000000000000000")], - [seeds[2]] = [], + [Seed1] = [N1], + [Seed2] = [N2], + [Seed3] = [], }; - LookupKNearestNeighbour lookup = new( - routingTable, - new IdentityNodeHashProvider(), - Substitute.For>(), - new KademliaConfig - { - CurrentNodeId = Self, - Alpha = alpha, - KSize = 8, - LookupFindNeighbourHardTimeout = TimeSpan.FromSeconds(10), - }, - LimboLogs.Instance); - ValueHash256[] result = await lookup.Lookup( Self, 8, @@ -103,9 +94,4 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C Assert.That(result, Is.Not.Empty); } - - private sealed class IdentityNodeHashProvider : INodeHashProvider - { - public ValueHash256 GetHash(ValueHash256 node) => node; - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index dade1e3ef83b..3ee3330a6819 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -15,79 +15,93 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; public class NodeHealthTrackerTests { - [Test] - public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSelectsSelf() + private const string Self = "self"; + private const string Remote = "remote"; + private const string Stale = "stale"; + + private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( + string? toRefresh = null, + int failureThreshold = 5, + TimeSpan? refreshPingTimeout = null, + IKademliaMessageSender? sender = null) { - const string self = "self"; - const string remote = "remote"; - RoutingTableStub routingTable = new() { ToRefresh = self }; + RoutingTableStub routing = new() { ToRefresh = toRefresh ?? string.Empty }; + sender ??= Substitute.For>(); + KademliaConfig config = new() + { + CurrentNodeId = Self, + NodeRequestFailureThreshold = failureThreshold, + }; + if (refreshPingTimeout is { } timeout) config.RefreshPingTimeout = timeout; + NodeHealthTracker tracker = new( - new KademliaConfig { CurrentNodeId = self }, - routingTable, - new StringNodeHashProvider(), - Substitute.For>(), + config, + routing, + StringNodeHashProvider.Instance, + sender, LimboLogs.Instance); + return (tracker, routing, sender); + } - tracker.OnIncomingMessageFrom(remote); + [Test] + public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSelectsSelf() + { + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); - Assert.That(routingTable.AddCalls, Has.Count.EqualTo(2)); - Assert.That(routingTable.AddCalls[1].Hash, Is.EqualTo(ValueKeccak.Compute(self))); - Assert.That(routingTable.AddCalls[1].Node, Is.EqualTo(self)); + tracker.OnIncomingMessageFrom(Remote); + + Assert.That(routing.AddCalls, Has.Count.EqualTo(2)); + Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(ValueKeccak.Compute(Self))); + Assert.That(routing.AddCalls[1].Node, Is.EqualTo(Self)); } [Test] [CancelAfter(10000)] public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(CancellationToken token) { - const string self = "self"; - const string remote = "remote"; - const string stale = "stale"; - RoutingTableStub routingTable = new() { ToRefresh = stale }; IKademliaMessageSender sender = Substitute.For>(); - sender.Ping(stale, Arg.Any()) + sender.Ping(Stale, Arg.Any()) .Returns(Task.FromException(new OperationCanceledException())); - NodeHealthTracker tracker = new( - new KademliaConfig - { - CurrentNodeId = self, - RefreshPingTimeout = TimeSpan.FromMilliseconds(50), - }, - routingTable, - new StringNodeHashProvider(), - sender, - LimboLogs.Instance); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + toRefresh: Stale, + refreshPingTimeout: TimeSpan.FromMilliseconds(50), + sender: sender); - tracker.OnIncomingMessageFrom(remote); + tracker.OnIncomingMessageFrom(Remote); - ValueHash256 staleHash = ValueKeccak.Compute(stale); - await AssertEventuallyAsync(() => routingTable.RemoveCalls.Contains(staleHash), token); + await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(ValueKeccak.Compute(Stale)), token); } [Test] [CancelAfter(10000)] public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken token) { - const string self = "self"; - const string remote = "remote"; - const string stale = "stale"; - RoutingTableStub routingTable = new() { ToRefresh = stale }; IKademliaMessageSender sender = Substitute.For>(); - sender.Ping(stale, Arg.Any()).Returns(Task.CompletedTask); + sender.Ping(Stale, Arg.Any()).Returns(Task.CompletedTask); - NodeHealthTracker tracker = new( - new KademliaConfig { CurrentNodeId = self }, - routingTable, - new StringNodeHashProvider(), - sender, - LimboLogs.Instance); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + toRefresh: Stale, + sender: sender); - tracker.OnIncomingMessageFrom(remote); + tracker.OnIncomingMessageFrom(Remote); - ValueHash256 staleHash = ValueKeccak.Compute(stale); - // OnIncomingMessageFrom inside TryRefresh's success branch re-adds the stale node — wait for that. - await AssertEventuallyAsync(() => routingTable.HasAddedNode(staleHash), token); - Assert.That(routingTable.RemoveCalls, Does.Not.Contain(staleHash)); + ValueHash256 staleHash = ValueKeccak.Compute(Stale); + await AssertEventuallyAsync(() => routing.HasAddedNode(staleHash), token); + Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); + } + + [Test] + public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() + { + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); + + tracker.OnRequestFailed(Remote); + tracker.OnRequestFailed(Remote); + tracker.OnRequestFailed(Remote); + + Assert.That(routing.RemoveCalls, Has.Count.EqualTo(1)); + Assert.That(routing.RemoveCalls[0], Is.EqualTo(ValueKeccak.Compute(Remote))); } private static async Task AssertEventuallyAsync(Func condition, CancellationToken token) @@ -100,29 +114,9 @@ private static async Task AssertEventuallyAsync(Func condition, Cancellati Assert.Fail("Condition not met within timeout."); } - [Test] - public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() - { - const string self = "self"; - const string remote = "remote"; - RoutingTableStub routingTable = new(); - NodeHealthTracker tracker = new( - new KademliaConfig { CurrentNodeId = self, NodeRequestFailureThreshold = 1 }, - routingTable, - new StringNodeHashProvider(), - Substitute.For>(), - LimboLogs.Instance); - - tracker.OnRequestFailed(remote); - tracker.OnRequestFailed(remote); - tracker.OnRequestFailed(remote); - - Assert.That(routingTable.RemoveCalls, Has.Count.EqualTo(1)); - Assert.That(routingTable.RemoveCalls[0], Is.EqualTo(ValueKeccak.Compute(remote))); - } - private sealed class StringNodeHashProvider : INodeHashProvider { + public static readonly StringNodeHashProvider Instance = new(); public ValueHash256 GetHash(string node) => ValueKeccak.Compute(node); } From c6f7aab1e3625ecad0d99cf771680e540704b2d9 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 22 May 2026 22:48:36 +0300 Subject: [PATCH 106/182] Extract generic Kademlia and harden native discv5 --- Directory.Packages.props | 1 - .../Nethermind.Config.csproj | 2 +- .../Nethermind.Config/NetworkNode.cs | 32 +- .../Nethermind.Crypto/SecP256k1Agreement.cs | 45 ++ .../BucketAddResult.cs | 2 +- .../DoubleEndedLru.cs | 42 +- .../FromKeyNodeHashProvider.cs | 5 +- .../Hash256XorUtils.cs | 50 +- .../IIteratorNodeLookup.cs | 2 +- .../IKademlia.cs | 8 +- .../IKademliaMessageSender.cs | 2 +- .../IKeyOperator.cs | 10 +- .../ILookupAlgo.cs | 5 +- .../INodeHashProvider.cs | 5 +- .../INodeHealthTracker.cs | 2 +- .../IRoutingTable.cs | 14 +- .../KBucket.cs | 15 +- .../KBucketTree.cs | 49 +- .../Kademlia.cs | 19 +- .../KademliaConfig.cs | 2 +- .../Nethermind.Kademlia/KademliaHash.cs | 85 +++ .../LookupKNearestNeighbour.cs | 29 +- .../Nethermind.Kademlia.csproj | 17 + .../NodeHealthTracker.cs | 13 +- .../DiscoveryPersistenceManagerTests.cs | 2 +- .../DiscoveryV5AppTests.cs | 137 +++- .../Discv4/IteratorNodeLookupTests.cs | 10 +- .../Discv4/KademliaDiscv4AdapterTests.cs | 2 +- .../Discv4/KademliaNodeSourceTests.cs | 48 +- .../Discv5/Discv5CodecTests.cs | 194 +++++ .../Discv5/Discv5KademliaAdapterTests.cs | 79 ++ .../Discv5/Discv5WireTests.cs | 190 +++++ .../E2EDiscoveryTests.cs | 1 - .../Kademlia/Hash256XorUtilsTests.cs | 18 +- .../Kademlia/IdentityNodeHashProvider.cs | 6 +- .../Kademlia/KBucketTests.cs | 24 +- .../Kademlia/KBucketTreeTests.cs | 8 +- .../Kademlia/KademliaSimulation.cs | 15 +- .../Kademlia/KademliaTests.cs | 24 +- .../Kademlia/LookupKNearestNeighbourTests.cs | 8 +- .../Kademlia/NodeHealthTrackerTests.cs | 72 +- .../Nethermind.Network.Discovery.Test.csproj | 1 + .../NettyDiscoveryV5HandlerTests.cs | 22 +- .../DiscoveryApp.cs | 2 +- .../DiscoveryPersistenceManager.cs | 2 +- .../Discv4/DiscV4KademliaModule.cs | 1 + .../Discv4/IKademliaDiscv4Adapter.cs | 2 +- .../IteratorNodeLookup.cs | 27 +- .../Discv4/KademliaDiscv4Adapter.cs | 4 +- .../Discv4/KademliaNodeSource.cs | 23 +- .../Discv4/PublicKeyKeyOperator.cs | 6 +- .../Discv5/DiscV5KademliaModule.cs | 38 + .../Discv5/DiscoveryV5App.cs | 495 ++++++------- .../Discv5/DiscoveryV5Report.cs | 8 +- .../Discv5/Discv5KademliaAdapter.cs | 699 ++++++++++++++++++ .../Discv5/Discv5MessageCodec.cs | 144 ++++ .../Discv5/Discv5Messages.cs | 52 ++ .../Discv5/Discv5NodeRecordConverter.cs | 42 ++ .../Discv5/Discv5NodeSource.cs | 62 ++ .../Discv5/Discv5PacketCodec.cs | 562 ++++++++++++++ .../Discv5/IDiscv5KademliaAdapter.cs | 23 + .../Discv5/NettyDiscoveryV5Handler.cs | 66 +- .../Discv5/NetworkNodeExtensions.cs | 28 - .../Kademlia/KademliaModule.cs | 1 + .../Messages/PingMsg.cs | 2 +- .../Nethermind.Network.Discovery.csproj | 2 +- .../NettyDiscoveryHandler.cs | 14 +- .../Serializers/DiscoveryMsgSerializerBase.cs | 6 +- .../Serializers/PingMsgSerializer.cs | 2 +- .../TalkReqAndRespHandler.cs | 20 - .../NodeRecordSignerTests.cs | 3 +- .../Nethermind.Network.Enr.csproj | 2 +- .../Nethermind.Network.Enr/NodeRecord.cs | 74 +- .../NodeRecordSigner.cs | 18 +- .../CompositeNodeSourceTests.cs | 18 + .../Nethermind.Network/CompositeNodeSource.cs | 16 +- .../Nethermind.Runner/packages.lock.json | 76 +- src/Nethermind/Nethermind.slnx | 1 + 78 files changed, 3084 insertions(+), 774 deletions(-) create mode 100644 src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/BucketAddResult.cs (77%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/DoubleEndedLru.cs (61%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/FromKeyNodeHashProvider.cs (62%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/Hash256XorUtils.cs (56%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/IIteratorNodeLookup.cs (82%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/IKademlia.cs (88%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/IKademliaMessageSender.cs (91%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/IKeyOperator.cs (60%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/ILookupAlgo.cs (90%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/INodeHashProvider.cs (71%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/INodeHealthTracker.cs (82%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/IRoutingTable.cs (55%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/KBucket.cs (78%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/KBucketTree.cs (90%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/Kademlia.cs (91%) rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/KademliaConfig.cs (97%) create mode 100644 src/Nethermind/Nethermind.Kademlia/KademliaHash.cs rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/LookupKNearestNeighbour.cs (89%) create mode 100644 src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj rename src/Nethermind/{Nethermind.Network.Discovery/Kademlia => Nethermind.Kademlia}/NodeHealthTracker.cs (90%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs rename src/Nethermind/Nethermind.Network.Discovery/{Kademlia => Discv4}/IteratorNodeLookup.cs (88%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/NetworkNodeExtensions.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7dd6c2a9b0ad..fcdc70e606f4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -77,7 +77,6 @@ - diff --git a/src/Nethermind/Nethermind.Config/Nethermind.Config.csproj b/src/Nethermind/Nethermind.Config/Nethermind.Config.csproj index f0797f7a8cb5..44c4c6b1b596 100644 --- a/src/Nethermind/Nethermind.Config/Nethermind.Config.csproj +++ b/src/Nethermind/Nethermind.Config/Nethermind.Config.csproj @@ -7,11 +7,11 @@ - + diff --git a/src/Nethermind/Nethermind.Config/NetworkNode.cs b/src/Nethermind/Nethermind.Config/NetworkNode.cs index 3c95db5d4cf0..5a4c654444e3 100644 --- a/src/Nethermind/Nethermind.Config/NetworkNode.cs +++ b/src/Nethermind/Nethermind.Config/NetworkNode.cs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: LGPL-3.0-only #nullable enable -using Lantern.Discv5.Enr; -using Lantern.Discv5.Enr.Entries; -using Lantern.Discv5.Enr.Identity; -using Lantern.Discv5.Enr.Identity.V4; using Nethermind.Core.Crypto; using Nethermind.Logging; +using Nethermind.Network.Enr; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -20,11 +17,8 @@ namespace Nethermind.Config; /// public class NetworkNode { - private static readonly EnrFactory _enrFactory = new(new EnrEntryRegistry()); - private static readonly IIdentityVerifier identityVerifier = new IdentityVerifierV4(); - private readonly Enode? _enode; - private readonly Enr? _enr; + private readonly NodeRecord? _enr; [MemberNotNullWhen(true, nameof(Enode))] [MemberNotNullWhen(false, nameof(Enr))] @@ -42,7 +36,7 @@ public NetworkNode(string nodeString) } else { - _enr = _enrFactory.CreateFromString(nodeString, identityVerifier); + _enr = NodeRecord.FromEnrString(nodeString); } } @@ -82,7 +76,7 @@ public static NetworkNode[] ParseNodes(string[]? nodeRecords, ILogger logger) return [.. nodes]; } - public override string ToString() => IsEnode ? Enode.ToString() : Enr.ToString(); + public override string ToString() => IsEnode ? Enode.ToString() : Enr.EnrString; public NetworkNode(PublicKey publicKey, string ip, int port, long reputation = 0) : this(new Enode(publicKey, IPAddress.Parse(ip), port)) => Reputation = reputation; @@ -91,11 +85,19 @@ public NetworkNode(PublicKey publicKey, string ip, int port, long reputation = 0 public Enode? Enode => _enode; - public Enr? Enr => _enr; + public NodeRecord? Enr => _enr; - public PublicKey NodeId => IsEnode ? Enode.PublicKey : new PublicKey(Enr.GetEntry(EnrEntryKey.Secp256K1).Value); - public string Host => IsEnode ? Enode.HostIp.ToString() : Enr.GetEntry(EnrEntryKey.Ip).Value.ToString(); - public IPAddress HostIp => IsEnode ? Enode.HostIp : Enr.GetEntry(EnrEntryKey.Ip).Value; - public int Port => IsEnode ? Enode.Port : Enr.GetEntry(EnrEntryKey.Tcp).Value; + public PublicKey NodeId => IsEnode ? Enode.PublicKey : GetEnrPublicKey(); + public string Host => IsEnode ? Enode.HostIp.ToString() : HostIp.ToString(); + public IPAddress HostIp => IsEnode ? Enode.HostIp : Enr!.GetObj(EnrContentKey.Ip) ?? IPAddress.None; + public int Port => IsEnode ? Enode.Port : Enr!.GetValue(EnrContentKey.Tcp) ?? 0; public long Reputation { get; set; } + + private PublicKey GetEnrPublicKey() + { + CompressedPublicKey publicKey = Enr!.GetObj(EnrContentKey.SecP256k1) + ?? throw new InvalidOperationException("ENR is missing secp256k1 public key."); + + return publicKey.Decompress(); + } } diff --git a/src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs b/src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs new file mode 100644 index 000000000000..88d3b1df0e56 --- /dev/null +++ b/src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using System; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; + +namespace Nethermind.Crypto; + +/// +/// secp256k1 key-agreement helpers for protocols that need the serialized shared EC point. +/// +public static class SecP256k1Agreement +{ + /// + /// Computes the compressed shared EC point for ECDH. + /// + public static byte[] AgreeCompressed(PublicKey publicKey, PrivateKey privateKey) + { + ArgumentNullException.ThrowIfNull(publicKey); + ArgumentNullException.ThrowIfNull(privateKey); + + ECPoint point = BouncyCrypto.DomainParameters.Curve.DecodePoint(publicKey.PrefixedBytes); + return AgreeCompressed(point, privateKey); + } + + /// + /// Computes the compressed shared EC point for ECDH. + /// + public static byte[] AgreeCompressed(CompressedPublicKey publicKey, PrivateKey privateKey) + { + ArgumentNullException.ThrowIfNull(publicKey); + ArgumentNullException.ThrowIfNull(privateKey); + + ECPoint point = BouncyCrypto.DomainParameters.Curve.DecodePoint(publicKey.Bytes); + return AgreeCompressed(point, privateKey); + } + + private static byte[] AgreeCompressed(ECPoint point, PrivateKey privateKey) + { + BigInteger privateScalar = new(1, privateKey.KeyBytes); + return point.Multiply(privateScalar).Normalize().GetEncoded(compressed: true); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs b/src/Nethermind/Nethermind.Kademlia/BucketAddResult.cs similarity index 77% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs rename to src/Nethermind/Nethermind.Kademlia/BucketAddResult.cs index b4108865dfa1..4c85db0dd461 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/BucketAddResult.cs +++ b/src/Nethermind/Nethermind.Kademlia/BucketAddResult.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public enum BucketAddResult { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs similarity index 61% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs rename to src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 27beaeaa3e0e..833ba37ea04f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -1,25 +1,24 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; using Nethermind.Core.Threading; using NonBlocking; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class DoubleEndedLru(int capacity) where TNode : notnull { private readonly McsLock _lock = new(); - private readonly LinkedList<(ValueHash256, TNode)> _queue = new(); - private readonly ConcurrentDictionary> _hashMapping = new(); + private readonly LinkedList<(KademliaHash, TNode)> _queue = new(); + private readonly ConcurrentDictionary> _hashMapping = new(); public int Count => _queue.Count; - public BucketAddResult AddOrRefresh(in ValueHash256 hash, TNode node) + public BucketAddResult AddOrRefresh(in KademliaHash hash, TNode node) { using McsLock.Disposable _ = _lock.Acquire(); - if (_hashMapping.TryGetValue(hash, out LinkedListNode<(ValueHash256, TNode)>? listNode)) + if (_hashMapping.TryGetValue(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) { _queue.Remove(listNode); _queue.AddFirst(listNode); @@ -36,11 +35,11 @@ public BucketAddResult AddOrRefresh(in ValueHash256 hash, TNode node) return BucketAddResult.Added; } - public bool TryPopHead(out ValueHash256 hash, out TNode? node) + public bool TryPopHead(out KademliaHash hash, out TNode? node) { using McsLock.Disposable _ = _lock.Acquire(); - LinkedListNode<(ValueHash256, TNode)>? front = _queue.First; + LinkedListNode<(KademliaHash, TNode)>? front = _queue.First; if (front == null) { hash = default; @@ -60,7 +59,7 @@ public bool TryGetLast(out TNode? last) { using McsLock.Disposable _ = _lock.Acquire(); - LinkedListNode<(ValueHash256, TNode)>? lastNode = _queue.Last; + LinkedListNode<(KademliaHash, TNode)>? lastNode = _queue.Last; if (lastNode == null) { last = default; @@ -71,11 +70,11 @@ public bool TryGetLast(out TNode? last) return true; } - public bool Remove(ValueHash256 hash) + public bool Remove(KademliaHash hash) { using McsLock.Disposable _ = _lock.Acquire(); - if (_hashMapping.TryRemove(hash, out LinkedListNode<(ValueHash256, TNode)>? listNode)) + if (_hashMapping.TryRemove(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) { _queue.Remove(listNode); return true; @@ -89,21 +88,28 @@ public TNode[] GetAll() using McsLock.Disposable _ = _lock.Acquire(); TNode[] result = new TNode[_queue.Count]; int i = 0; - foreach ((ValueHash256, TNode node) entry in _queue) result[i++] = entry.node; + foreach ((KademliaHash, TNode node) entry in _queue) result[i++] = entry.node; return result; } - public (ValueHash256, TNode)[] GetAllWithHash() + public (KademliaHash, TNode)[] GetAllWithHash() { using McsLock.Disposable _ = _lock.Acquire(); - (ValueHash256, TNode)[] result = new (ValueHash256, TNode)[_queue.Count]; + (KademliaHash, TNode)[] result = new (KademliaHash, TNode)[_queue.Count]; int i = 0; - foreach ((ValueHash256, TNode) entry in _queue) result[i++] = entry; + foreach ((KademliaHash, TNode) entry in _queue) result[i++] = entry; return result; } - public bool Contains(in ValueHash256 hash) => _hashMapping.ContainsKey(hash); + public bool Contains(in KademliaHash hash) => _hashMapping.ContainsKey(hash); - public TNode? GetByHash(ValueHash256 hash) => - _hashMapping.TryGetValue(hash, out LinkedListNode<(ValueHash256, TNode)>? listNode) ? listNode.Value.Item2 : default; + public TNode? GetByHash(KademliaHash hash) + { + if (_hashMapping.TryGetValue(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) + { + return listNode.Value.Item2; + } + + return default; + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs b/src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs similarity index 62% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs rename to src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs index ff9c01de1330..8e0e7cd56a64 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/FromKeyNodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs @@ -1,11 +1,10 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class FromKeyNodeHashProvider(IKeyOperator keyOperator) : INodeHashProvider { - public ValueHash256 GetHash(TNode node) => keyOperator.GetNodeHash(node); + public KademliaHash GetHash(TNode node) => keyOperator.GetNodeHash(node); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs b/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs similarity index 56% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs rename to src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs index 4f042e06435b..9d922a22257d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256XorUtils.cs +++ b/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs @@ -2,15 +2,14 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Numerics; -using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public static class Hash256XorUtils { - public static int CalculateLogDistance(ValueHash256 h1, ValueHash256 h2) + public static int CalculateLogDistance(KademliaHash h1, KademliaHash h2) { - ValueHash256 xor = XorDistance(h1, h2); + KademliaHash xor = XorDistance(h1, h2); int zeros = 0; for (int i = 0; i < 32; i += 1) { @@ -35,19 +34,18 @@ public static int CalculateLogDistance(ValueHash256 h1, ValueHash256 h2) public const int MaxDistance = 256; - public static int Compare(ValueHash256 a, ValueHash256 b, ValueHash256 c) + public static int Compare(KademliaHash a, KademliaHash b, KademliaHash c) { - ValueHash256 ac = XorDistance(a, c); - ValueHash256 bc = XorDistance(b, c); + KademliaHash ac = XorDistance(a, c); + KademliaHash bc = XorDistance(b, c); return ac.CompareTo(bc); } - public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) + public static KademliaHash XorDistance(KademliaHash hash1, KademliaHash hash2) { - ValueHash256 bc = new(); - ReadOnlySpan hash1Bytes = hash1.BytesAsSpan; - ReadOnlySpan hash2Bytes = hash2.BytesAsSpan; - Span result = bc.BytesAsSpan; + ReadOnlySpan hash1Bytes = hash1.Bytes; + ReadOnlySpan hash2Bytes = hash2.Bytes; + Span result = stackalloc byte[KademliaHash.Length]; int i = 0; for (; i <= result.Length - Vector.Count; i += Vector.Count) @@ -60,12 +58,12 @@ public static ValueHash256 XorDistance(ValueHash256 hash1, ValueHash256 hash2) result[i] = (byte)(hash1Bytes[i] ^ hash2Bytes[i]); } - return bc; + return KademliaHash.FromBytes(result); } - public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int distance) => GetRandomHashAtDistance(currentHash, distance, Random.Shared); + public static KademliaHash GetRandomHashAtDistance(KademliaHash currentHash, int distance) => GetRandomHashAtDistance(currentHash, distance, Random.Shared); - public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int distance, Random random) + public static KademliaHash GetRandomHashAtDistance(KademliaHash currentHash, int distance, Random random) { // TODO: Just add a min/max range per bucket and randomized between them. if (distance == MaxDistance) @@ -73,23 +71,23 @@ public static ValueHash256 GetRandomHashAtDistance(ValueHash256 currentHash, int return currentHash; } - ValueHash256 randomized = new(); - random.NextBytes(randomized.BytesAsSpan); + Span randomized = stackalloc byte[KademliaHash.Length]; + random.NextBytes(randomized); return CopyForRandom(currentHash, randomized, MaxDistance - distance); } - private static ValueHash256 CopyForRandom(ValueHash256 currentHash, ValueHash256 randomizedHash, int distance) + private static KademliaHash CopyForRandom(KademliaHash currentHash, Span randomizedHash, int distance) { if (distance >= 256) return currentHash; - currentHash.Bytes[0..(distance / 8)].CopyTo(randomizedHash.BytesAsSpan); + currentHash.Bytes[0..(distance / 8)].CopyTo(randomizedHash); int remainingBit = distance % 8; int remainingBitByte = distance / 8; byte mask = (byte)(~((1 << (8 - remainingBit)) - 1)); - byte randomized = randomizedHash.BytesAsSpan[remainingBitByte]; - byte original = currentHash.BytesAsSpan[remainingBitByte]; - randomizedHash.BytesAsSpan[remainingBitByte] = (byte)((original & mask) | (randomized & (~mask))); + byte randomized = randomizedHash[remainingBitByte]; + byte original = currentHash.Bytes[remainingBitByte]; + randomizedHash[remainingBitByte] = (byte)((original & mask) | (randomized & (~mask))); if (distance <= 255) { @@ -98,13 +96,13 @@ private static ValueHash256 CopyForRandom(ValueHash256 currentHash, ValueHash256 int nextBit = distance % 8; int nextBitByte = distance / 8; mask = (byte)(1 << (7 - nextBit)); - randomized = randomizedHash.BytesAsSpan[nextBitByte]; - byte opposite = (byte)~(currentHash.BytesAsSpan[nextBitByte]); + randomized = randomizedHash[nextBitByte]; + byte opposite = (byte)~(currentHash.Bytes[nextBitByte]); byte final = (byte)((opposite & mask) | (randomized & ~(mask))); - randomizedHash.BytesAsSpan[nextBitByte] = final; + randomizedHash[nextBitByte] = final; } - return randomizedHash; + return KademliaHash.FromBytes(randomizedHash); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs b/src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs similarity index 82% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs rename to src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs index c23bfe490615..18828c4a3b61 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IIteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public interface IIteratorNodeLookup { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs similarity index 88% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs rename to src/Nethermind/Nethermind.Kademlia/IKademlia.cs index 1c6c090d52f9..b786b1ae6a9c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; /// /// Main kademlia interface. High level code is expected to interface with this interface. @@ -52,6 +52,12 @@ public interface IKademlia /// TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false); + /// + /// Return all table entries whose hash is at the requested log distance from the local node. + /// + /// The XOR log distance from the local node. + TNode[] GetAllAtDistance(int distance); + /// /// Called when a TNode is added to the routing table. /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs similarity index 91% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs rename to src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs index ea22ed620ec2..c19767de888d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaMessageSender.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; /// /// Should be exposed by application to kademlia so that kademlia can send out message. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs b/src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs similarity index 60% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs rename to src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs index b6a0a9edf3b7..34774eaaf707 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKeyOperator.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs @@ -1,9 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; - -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; /// /// Define operations for and . @@ -13,7 +11,7 @@ namespace Nethermind.Network.Discovery.Kademlia; public interface IKeyOperator { TKey GetKey(TNode node); - ValueHash256 GetKeyHash(TKey key); - ValueHash256 GetNodeHash(TNode node) => GetKeyHash(GetKey(node)); - TKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth); + KademliaHash GetKeyHash(TKey key); + KademliaHash GetNodeHash(TNode node) => GetKeyHash(GetKey(node)); + TKey CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs b/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs similarity index 90% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs rename to src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs index 7315c29ee3c7..65944e0f390b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/ILookupAlgo.cs +++ b/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs @@ -1,9 +1,8 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; /// /// Main find closest-k node within the network. See the kademlia paper, 2.3. @@ -23,7 +22,7 @@ public interface ILookupAlgo /// /// Task Lookup( - ValueHash256 targetHash, + KademliaHash targetHash, int k, Func> findNeighbourOp, CancellationToken token diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs similarity index 71% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs rename to src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs index c3cda5c61506..8394a758c7db 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs @@ -1,9 +1,8 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; /// /// Just a convenient interface with only one generic parameter. @@ -11,5 +10,5 @@ namespace Nethermind.Network.Discovery.Kademlia; /// public interface INodeHashProvider { - ValueHash256 GetHash(TNode node); + KademliaHash GetHash(TNode node); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/INodeHealthTracker.cs similarity index 82% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs rename to src/Nethermind/Nethermind.Kademlia/INodeHealthTracker.cs index 99431729de97..92ee75d17b0f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/INodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/INodeHealthTracker.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public interface INodeHealthTracker { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs similarity index 55% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs rename to src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs index 52ff09bc0a31..144b52320462 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IRoutingTable.cs +++ b/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs @@ -1,18 +1,16 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; - -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public interface IRoutingTable where TNode : notnull { - BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh); - bool Remove(in ValueHash256 hash); - TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude = null, bool excludeSelf = false); + BucketAddResult TryAddOrRefresh(in KademliaHash hash, TNode item, out TNode? toRefresh); + bool Remove(in KademliaHash hash); + TNode[] GetKNearestNeighbour(KademliaHash hash, KademliaHash? exclude = null, bool excludeSelf = false); TNode[] GetAllAtDistance(int i); - IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets(); - TNode? GetByHash(ValueHash256 nodeId); + IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> IterateBuckets(); + TNode? GetByHash(KademliaHash nodeId); void LogDebugInfo(); event EventHandler? OnNodeAdded; event EventHandler? OnNodeRemoved; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs similarity index 78% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs rename to src/Nethermind/Nethermind.Kademlia/KBucket.cs index 2eeff2a902ec..235726863646 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -1,9 +1,8 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class KBucket(int k) where TNode : notnull { @@ -22,7 +21,7 @@ public class KBucket(int k) where TNode : notnull /// /// /// - public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNode? toRefresh) + public BucketAddResult TryAddOrRefresh(in KademliaHash hash, TNode item, out TNode? toRefresh) { BucketAddResult addResult = _items.AddOrRefresh(hash, item); if (addResult == BucketAddResult.Added) @@ -44,13 +43,13 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, TNode item, out TNo public TNode[] GetAll() => _cachedArray; - public (ValueHash256, TNode)[] GetAllWithHash() => _items.GetAllWithHash(); + public (KademliaHash, TNode)[] GetAllWithHash() => _items.GetAllWithHash(); - public bool RemoveAndReplace(in ValueHash256 hash) + public bool RemoveAndReplace(in KademliaHash hash) { if (!_items.Remove(hash)) return false; - if (_replacement.TryPopHead(out ValueHash256 replacementHash, out TNode? replacement)) + if (_replacement.TryPopHead(out KademliaHash replacementHash, out TNode? replacement)) { _items.AddOrRefresh(replacementHash, replacement!); } @@ -66,7 +65,7 @@ public void Clear() _cachedArray = _items.GetAll(); } - public bool ContainsNode(in ValueHash256 hash) => _items.Contains(hash); + public bool ContainsNode(in KademliaHash hash) => _items.Contains(hash); - public TNode? GetByHash(ValueHash256 hash) => _items.GetByHash(hash); + public TNode? GetByHash(KademliaHash hash) => _items.GetByHash(hash); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs similarity index 90% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs rename to src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index d21506b2744d..f9f56a78dd32 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -2,27 +2,26 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Text; -using Nethermind.Core.Crypto; using Nethermind.Core.Threading; using Nethermind.Logging; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class KBucketTree : IRoutingTable where TNode : notnull { - private class TreeNode(int k, ValueHash256 prefix) + private class TreeNode(int k, KademliaHash prefix) { public KBucket Bucket { get; } = new KBucket(k); public TreeNode? Left { get; set; } public TreeNode? Right { get; set; } - public ValueHash256 Prefix { get; } = prefix; + public KademliaHash Prefix { get; } = prefix; public bool IsLeaf => Left == null && Right == null; } private readonly TreeNode _root; private readonly int _b; private readonly int _k; - private readonly ValueHash256 _currentNodeHash; + private readonly KademliaHash _currentNodeHash; private readonly ILogger _logger; // TODO: Double check and probably make lockless @@ -33,12 +32,12 @@ public KBucketTree(KademliaConfig config, INodeHashProvider nodeHa _k = config.KSize; _b = config.Beta; _currentNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); - _root = new TreeNode(config.KSize, new ValueHash256()); + _root = new TreeNode(config.KSize, KademliaHash.Zero); _logger = logManager.GetClassLogger>(); if (_logger.IsDebug) _logger.Debug($"Initialized KBucketTree with k={_k}, currentNodeId={_currentNodeHash}"); } - public BucketAddResult TryAddOrRefresh(in ValueHash256 nodeHash, TNode node, out TNode? toRefresh) + public BucketAddResult TryAddOrRefresh(in KademliaHash nodeHash, TNode node, out TNode? toRefresh) { BucketAddResult resp; bool fireAdded; @@ -86,13 +85,13 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 nodeHash, TNode node, out return resp; } - public TNode? GetByHash(ValueHash256 hash) + public TNode? GetByHash(KademliaHash hash) { using McsLock.Disposable _ = _lock.Acquire(); return GetBucketForHash(hash).GetByHash(hash); } - private KBucket GetBucketForHash(ValueHash256 nodeHash) + private KBucket GetBucketForHash(KademliaHash nodeHash) { TreeNode current = _root; int depth = 0; @@ -124,15 +123,15 @@ private void SplitBucket(int depth, TreeNode node) node.Left = new TreeNode(_k, node.Prefix); byte[] rightPrefixBytes = node.Prefix.Bytes.ToArray(); rightPrefixBytes[depth / 8] |= (byte)(1 << (7 - (depth % 8))); - node.Right = new TreeNode(_k, new ValueHash256(rightPrefixBytes)); + node.Right = new TreeNode(_k, KademliaHash.FromBytes(rightPrefixBytes)); if (_logger.IsDebug) _logger.Debug($"Created children at depth {depth + 1}"); // Iterate from oldest to newest so the new buckets preserve original LRU order. - (ValueHash256, TNode)[] items = node.Bucket.GetAllWithHash(); + (KademliaHash, TNode)[] items = node.Bucket.GetAllWithHash(); for (int i = items.Length - 1; i >= 0; i--) { - (ValueHash256 itemHash, TNode value) = items[i]; + (KademliaHash itemHash, TNode value) = items[i]; TreeNode? targetNode = GetBit(itemHash, depth) ? node.Right : node.Left; targetNode.Bucket.TryAddOrRefresh(itemHash, value, out _); if (_logger.IsDebug) _logger.Debug($"Moved item ({itemHash}, {value}) to {(GetBit(itemHash, depth) ? "right" : "left")} child"); @@ -142,7 +141,7 @@ private void SplitBucket(int depth, TreeNode node) if (_logger.IsDebug) _logger.Debug($"Finished splitting bucket. Left count: {node.Left.Bucket.Count}, Right count: {node.Right.Bucket.Count}"); } - public bool Remove(in ValueHash256 nodeHash) + public bool Remove(in KademliaHash nodeHash) { bool removed; TNode? removedNode; @@ -177,7 +176,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L { if (depth <= targetDepth) { - foreach ((ValueHash256 hash, TNode item) in node.Bucket.GetAllWithHash()) + foreach ((KademliaHash hash, TNode item) in node.Bucket.GetAllWithHash()) { if (Hash256XorUtils.CalculateLogDistance(hash, _currentNodeHash) == distance) { @@ -225,7 +224,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L } } - public IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() + public IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> IterateBuckets() { using McsLock.Disposable _ = _lock.Acquire(); @@ -233,7 +232,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L return DoIterateBucketRandomHashes(_root, 0).ToArray(); } - private IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> DoIterateBucketRandomHashes(TreeNode node, int depth) + private IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> DoIterateBucketRandomHashes(TreeNode node, int depth) { if (node.IsLeaf) { @@ -241,30 +240,30 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L } else { - foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) + foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) { yield return bucketInfo; } - foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) + foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) { yield return bucketInfo; } } } - private IEnumerable<(ValueHash256, TNode)> IterateNeighbour(ValueHash256 hash) + private IEnumerable<(KademliaHash, TNode)> IterateNeighbour(KademliaHash hash) { foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(_root, 0, hash)) { - foreach ((ValueHash256, TNode) entry in treeNode.Bucket.GetAllWithHash()) + foreach ((KademliaHash, TNode) entry in treeNode.Bucket.GetAllWithHash()) { yield return entry; } } } - private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNode, int depth, ValueHash256 target) + private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNode, int depth, KademliaHash target) { if (currentNode.IsLeaf) { @@ -299,7 +298,7 @@ private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNod } } - public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bool excludeSelf) + public TNode[] GetKNearestNeighbour(KademliaHash hash, KademliaHash? exclude, bool excludeSelf) { using McsLock.Disposable _ = _lock.Acquire(); @@ -320,7 +319,7 @@ public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bo TNode[] resultArr = new TNode[_k]; int count = 0; - foreach ((ValueHash256 itemHash, TNode item) in IterateNeighbour(hash)) + foreach ((KademliaHash itemHash, TNode item) in IterateNeighbour(hash)) { if (exclude != null && itemHash == exclude.Value) continue; if (excludeSelf && itemHash == _currentNodeHash) continue; @@ -334,7 +333,7 @@ public TNode[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude, bo return truncated; } - private bool GetBit(ValueHash256 hash, int index) + private bool GetBit(KademliaHash hash, int index) { int byteIndex = index / 8; int bitIndex = index % 8; @@ -423,7 +422,7 @@ public int Size get { int total = 0; - foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) in IterateBuckets()) + foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) in IterateBuckets()) { total += Bucket.Count; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs similarity index 91% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs rename to src/Nethermind/Nethermind.Kademlia/Kademlia.cs index b2a6b727bf53..29ddec43787a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -3,10 +3,9 @@ using System.Diagnostics; using Nethermind.Core; -using Nethermind.Core.Crypto; using Nethermind.Logging; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class Kademlia : IKademlia where TNode : notnull { @@ -18,13 +17,13 @@ public class Kademlia : IKademlia where TNode : notnul private readonly ILogger _logger; private readonly TNode _currentNodeId; - private readonly ValueHash256 _currentNodeIdAsHash; + private readonly KademliaHash _currentNodeIdAsHash; private readonly int _kSize; private readonly TimeSpan _refreshInterval; private readonly TimeSpan _bucketRefreshInterval; private readonly IReadOnlyList _bootNodes; private readonly ITimestamper _timestamper; - private readonly Dictionary _lastBucketRefreshTicks = []; + private readonly Dictionary _lastBucketRefreshTicks = []; private readonly object _lastBucketRefreshLock = new(); public Kademlia( @@ -72,7 +71,7 @@ public Task LookupNodesClosest(TKey key, CancellationToken token, int? { if (SameAsSelf(nextNode)) { - ValueHash256 keyHash = _keyOperator.GetKeyHash(key); + KademliaHash keyHash = _keyOperator.GetKeyHash(key); return _routingTable.GetKNearestNeighbour(keyHash); } return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); @@ -135,7 +134,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => // Refresh stale non-empty buckets one by one. A refresh means to do a k-nearest node lookup for a random hash // for that particular bucket. - foreach ((ValueHash256 Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) + foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { if (!ShouldRefreshBucket(Prefix, Bucket)) continue; @@ -150,7 +149,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } } - private bool ShouldRefreshBucket(ValueHash256 prefix, KBucket bucket) + private bool ShouldRefreshBucket(KademliaHash prefix, KBucket bucket) { if (bucket.Count == 0) return false; @@ -170,9 +169,9 @@ private bool ShouldRefreshBucket(ValueHash256 prefix, KBucket bucket) public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false) { - ValueHash256? excludeHash = null; + KademliaHash? excludeHash = null; if (excluding != null) excludeHash = _keyOperator.GetNodeHash(excluding); - ValueHash256 hash = _keyOperator.GetKeyHash(target); + KademliaHash hash = _keyOperator.GetKeyHash(target); return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); } @@ -190,7 +189,7 @@ public event EventHandler OnNodeRemoved public IEnumerable IterateNodes() { - foreach ((ValueHash256 _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) + foreach ((KademliaHash _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) { foreach (TNode node in Bucket.GetAll()) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs rename to src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs index 4ab4b410746d..1e12e459c647 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class KademliaConfig { diff --git a/src/Nethermind/Nethermind.Kademlia/KademliaHash.cs b/src/Nethermind/Nethermind.Kademlia/KademliaHash.cs new file mode 100644 index 000000000000..3e1d7740043e --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/KademliaHash.cs @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; + +namespace Nethermind.Kademlia; + +/// +/// Fixed-width 256-bit identifier used by the Kademlia routing table and XOR-distance operations. +/// +public readonly struct KademliaHash : IComparable, IEquatable +{ + private readonly ValueHash256 _value; + + /// + /// Number of bytes in a Kademlia hash. + /// + public const int Length = 32; + + /// + /// The all-zero hash value. + /// + public static KademliaHash Zero { get; } = new(new ValueHash256()); + + /// + /// Creates a hash from a hexadecimal string. + /// + /// A 32-byte hexadecimal string, with or without the 0x prefix. + public KademliaHash(string hex) + : this(new ValueHash256(hex)) + { + } + + private KademliaHash(ValueHash256 value) => _value = value; + + /// + /// Gets the hash bytes. + /// + public ReadOnlySpan Bytes => _value.BytesAsSpan; + + /// + /// Creates a hash from exactly 32 bytes. + /// + /// The bytes to copy into the hash. + /// Thrown when is not 32 bytes long. + public static KademliaHash FromBytes(ReadOnlySpan bytes) + { + if (bytes.Length != Length) + { + throw new ArgumentException($"Kademlia hash must be {Length} bytes.", nameof(bytes)); + } + + return new KademliaHash(new ValueHash256(bytes)); + } + + /// + /// Copies the hash into a new byte array. + /// + public byte[] ToArray() => _value.Bytes.ToArray(); + + /// + public int CompareTo(KademliaHash other) => _value.CompareTo(other._value); + + /// + public bool Equals(KademliaHash other) => _value == other._value; + + /// + public override bool Equals(object? obj) => obj is KademliaHash other && Equals(other); + + /// + public override int GetHashCode() => _value.GetHashCode(); + + /// + public override string ToString() => _value.ToString(); + + /// + /// Returns whether two hashes contain the same bytes. + /// + public static bool operator ==(KademliaHash left, KademliaHash right) => left.Equals(right); + + /// + /// Returns whether two hashes contain different bytes. + /// + public static bool operator !=(KademliaHash left, KademliaHash right) => !left.Equals(right); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs similarity index 89% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs rename to src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 78571ddf5b32..27051ac8efc2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -2,12 +2,11 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics.CodeAnalysis; -using Nethermind.Core.Crypto; using Nethermind.Core.Threading; using Nethermind.Logging; using NonBlocking; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; /// /// This find nearest k query does not follow the kademlia paper faithfully. Instead of distinct rounds, it has @@ -28,7 +27,7 @@ public class LookupKNearestNeighbour( private readonly ILogger _logger = logManager.GetClassLogger>(); public async Task Lookup( - ValueHash256 targetHash, + KademliaHash targetHash, int k, Func> findNeighbourOp, CancellationToken token @@ -39,25 +38,25 @@ CancellationToken token using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); token = cts.Token; - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); - IComparer comparer = Comparer.Create((h1, h2) => + IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); - IComparer comparerReverse = Comparer.Create((h1, h2) => + IComparer comparerReverse = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h2, h1, targetHash)); McsLock queueLock = new(); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(ValueHash256, TNode), ValueHash256> bestSeen = new(comparer); + PriorityQueue<(KademliaHash, TNode), KademliaHash> bestSeen = new(comparer); // Ordered by highest distance. Added on result. Get popped as result. - PriorityQueue<(ValueHash256, TNode), ValueHash256> finalResult = new(comparerReverse); + PriorityQueue<(KademliaHash, TNode), KademliaHash> finalResult = new(comparerReverse); foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, default)) { - ValueHash256 nodeHash = nodeHashProvider.GetHash(node); + KademliaHash nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); bestSeen.Enqueue((nodeHash, node), nodeHash); } @@ -73,7 +72,7 @@ CancellationToken token while (!Volatile.Read(ref finished)) { token.ThrowIfCancellationRequested(); - if (!TryGetNodeToQuery(out (ValueHash256 hash, TNode node)? toQuery)) + if (!TryGetNodeToQuery(out (KademliaHash hash, TNode node)? toQuery)) { if (queryingTask > 0) { @@ -151,7 +150,7 @@ CancellationToken token } } - bool TryGetNodeToQuery([NotNullWhen(true)] out (ValueHash256, TNode)? toQuery) + bool TryGetNodeToQuery([NotNullWhen(true)] out (KademliaHash, TNode)? toQuery) { using McsLock.Disposable _ = queueLock.Acquire(); if (bestSeen.Count == 0) @@ -167,7 +166,7 @@ bool TryGetNodeToQuery([NotNullWhen(true)] out (ValueHash256, TNode)? toQuery) return true; } - void ProcessResult(ValueHash256 hash, TNode toQuery, (TNode, TNode[]? neighbours)? valueTuple, int round) + void ProcessResult(KademliaHash hash, TNode toQuery, (TNode, TNode[]? neighbours)? valueTuple, int round) { using McsLock.Disposable _ = queueLock.Acquire(); @@ -182,7 +181,7 @@ void ProcessResult(ValueHash256 hash, TNode toQuery, (TNode, TNode[]? neighbours foreach (TNode neighbour in neighbours) { - ValueHash256 neighbourHash = nodeHashProvider.GetHash(neighbour); + KademliaHash neighbourHash = nodeHashProvider.GetHash(neighbour); // Already queried, we ignore if (queried.ContainsKey(neighbourHash)) continue; @@ -200,7 +199,7 @@ void ProcessResult(ValueHash256 hash, TNode toQuery, (TNode, TNode[]? neighbours } // If the worst item in final result is worst that this neighbour, update closes node round - if (finalResult.TryPeek(out (ValueHash256 hash, TNode node) worstResult, out ValueHash256 _) && comparer.Compare(neighbourHash, worstResult.hash) < 0) + if (finalResult.TryPeek(out (KademliaHash hash, TNode node) worstResult, out KademliaHash _) && comparer.Compare(neighbourHash, worstResult.hash) < 0) { closestNodeRound = round; } diff --git a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj new file mode 100644 index 000000000000..7a5c0ed94a8d --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj @@ -0,0 +1,17 @@ + + + + enable + enable + + + + + + + + + + + + diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs similarity index 90% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs rename to src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index f0db26fe5c00..d17759cb77eb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -2,11 +2,10 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Caching; -using Nethermind.Core.Crypto; using Nethermind.Logging; using NonBlocking; -namespace Nethermind.Network.Discovery.Kademlia; +namespace Nethermind.Kademlia; public class NodeHealthTracker( KademliaConfig config, @@ -18,16 +17,16 @@ ILogManager logManager { private readonly ILogger _logger = logManager.GetClassLogger>(); - private readonly ConcurrentDictionary _isRefreshing = new(); - private readonly LruCache _peerFailures = new(1024, "peer failure"); - private readonly ValueHash256 _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); + private readonly ConcurrentDictionary _isRefreshing = new(); + private readonly LruCache _peerFailures = new(1024, "peer failure"); + private readonly KademliaHash _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); private readonly TimeSpan _refreshPingTimeout = config.RefreshPingTimeout; private bool SameAsSelf(TNode node) => nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; private void TryRefresh(TNode toRefresh) { - ValueHash256 nodeHash = nodeHashProvider.GetHash(toRefresh); + KademliaHash nodeHash = nodeHashProvider.GetHash(toRefresh); if (_isRefreshing.TryAdd(nodeHash, true)) { Task.Run(async () => @@ -94,7 +93,7 @@ public void OnIncomingMessageFrom(TNode node) /// public void OnRequestFailed(TNode node) { - ValueHash256 hash = nodeHashProvider.GetHash(node); + KademliaHash hash = nodeHashProvider.GetHash(node); if (!_peerFailures.TryGet(hash, out int currentFailure)) { _peerFailures.Set(hash, 1); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs index 7c29036427e4..6a36e565d82e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs @@ -11,7 +11,7 @@ using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 76119fe3b6f6..70ae1272e08c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -1,10 +1,8 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Lantern.Discv5.Enr; -using Lantern.Discv5.Enr.Entries; -using Lantern.Discv5.Enr.Identity.V4; -using NSubstitute; +using Autofac; +using Autofac.Features.AttributeFilters; using Nethermind.Config; using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.Modules; @@ -13,12 +11,14 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; using NUnit.Framework; using System.Collections.Generic; using System.Net; -using ENR = Lantern.Discv5.Enr.Enr; +using System.Threading; +using System.Threading.Tasks; namespace Nethermind.Network.Discovery.Test; @@ -28,8 +28,8 @@ public class DiscoveryV5AppTests { private MemDb _discoveryDb = null!; private MemDb _legacyDiscoveryDb = null!; - private IdentityVerifierV4 _identityVerifier = null!; private DiscoveryV5App _discoveryV5App = null!; + private readonly List _containers = []; [OneTimeSetUp] public void OneTimeSetup() => Rlp.RegisterDecoder(typeof(NetworkNode), new NetworkNodeDecoder()); @@ -39,7 +39,6 @@ public void Setup() { _discoveryDb = new MemDb(); _legacyDiscoveryDb = new MemDb(); - _identityVerifier = new IdentityVerifierV4(); _discoveryV5App = CreateDiscoveryV5App(IPAddress.Parse("8.8.8.8")); } @@ -50,36 +49,59 @@ private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp) Bootnodes = [], ExternalIp = externalIp.ToString() }; + IProtectedPrivateKey nodeKey = new InsecureProtectedPrivateKey(TestItem.PrivateKeyF); + IIPResolver ipResolver = new FixedIpResolver(networkConfig); + EthereumEcdsa ecdsa = new(0); + ContainerBuilder builder = new(); + builder.RegisterInstance(LimboLogs.Instance).As(); + builder.RegisterInstance(networkConfig).As(); + builder.RegisterInstance(ipResolver).As(); + builder.RegisterInstance(nodeKey).Keyed(IProtectedPrivateKey.NodeKey); + builder.RegisterInstance(ecdsa).As().As(); + builder.RegisterInstance(new CryptoRandom()).As(); + builder.RegisterType().As().WithAttributeFiltering().SingleInstance(); + IContainer container = builder.Build(); + _containers.Add(container); + return new DiscoveryV5App( - new InsecureProtectedPrivateKey(TestItem.PrivateKeyF), - new FixedIpResolver(networkConfig), + container, + nodeKey, + ipResolver, networkConfig, new DiscoveryConfig { }, _discoveryDb, _legacyDiscoveryDb, + new ProcessExitSource(CancellationToken.None), LimboLogs.Instance ); } [TearDown] - public void Teardown() + public async Task Teardown() { + if (_discoveryV5App is not null) + { + await _discoveryV5App.DisposeAsync(); + } + for (int i = 0; i < _containers.Count; i++) + { + _containers[i].Dispose(); + } + _containers.Clear(); _discoveryDb.Dispose(); _legacyDiscoveryDb.Dispose(); } - private ENR CreateTestEnrBytes(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303) + private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null) { - IdentitySignerV4 signer = new(privateKey.KeyBytes); - - ENR enr = new EnrBuilder() - .WithIdentityScheme(_identityVerifier, signer) - .WithEntry(EnrEntryKey.Id, new EntryId("v4")) - .WithEntry(EnrEntryKey.Ip, new EntryIp(ipAddress ?? IPAddress.Loopback)) - .WithEntry(EnrEntryKey.Secp256K1, new EntrySecp256K1(signer.PublicKey)) - .WithEntry(EnrEntryKey.Tcp, new EntryTcp(port)) - .WithEntry(EnrEntryKey.Udp, new EntryUdp(port)) - .Build(); + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress ?? IPAddress.Loopback)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new TcpEntry(port)); + enr.SetEntry(new UdpEntry(udpPort ?? port)); + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); return enr; } @@ -88,14 +110,14 @@ private ENR CreateTestEnrBytes(Nethermind.Crypto.PrivateKey privateKey, IPAddres public void Should_Migrate_Correctly() { PrivateKey testPrivateKey1 = TestItem.PrivateKeyA; - ENR enr1 = CreateTestEnrBytes(testPrivateKey1); - _legacyDiscoveryDb[enr1.NodeId] = enr1.EncodeRecord(); + NodeRecord enr1 = CreateTestEnr(testPrivateKey1); + _legacyDiscoveryDb[testPrivateKey1.PublicKey.Hash.Bytes] = enr1.ToRlpBytes(); PrivateKey testPrivateKey2 = TestItem.PrivateKeyB; - ENR enr2 = CreateTestEnrBytes(testPrivateKey2); - _legacyDiscoveryDb[enr2.NodeId] = enr2.EncodeRecord(); + NodeRecord enr2 = CreateTestEnr(testPrivateKey2); + _legacyDiscoveryDb[testPrivateKey2.PublicKey.Hash.Bytes] = enr2.ToRlpBytes(); - List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); + List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); using (Assert.EnterMultipleScope()) { @@ -114,7 +136,7 @@ public void Should_Stop_Migration_From_V4_DB() NetworkNode enode2 = new(TestItem.PublicKeyB, IPAddress.Loopback.ToString(), 1, 1); _legacyDiscoveryDb[enode2.NodeId.Bytes] = Rlp.Encode(enode2).Bytes; - List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); + List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); using (Assert.EnterMultipleScope()) { @@ -124,10 +146,31 @@ public void Should_Stop_Migration_From_V4_DB() } } + [Test] + public void Should_Skip_Malformed_Legacy_Records_And_Migrate_Valid_Ones() + { + NetworkNode enode = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 1, 1); + _legacyDiscoveryDb[enode.NodeId.Bytes] = Rlp.Encode(enode).Bytes; + + PrivateKey validPrivateKey = TestItem.PrivateKeyB; + NodeRecord validEnr = CreateTestEnr(validPrivateKey); + _legacyDiscoveryDb[validPrivateKey.PublicKey.Hash.Bytes] = validEnr.ToRlpBytes(); + + List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(loadedEnrs, Has.Count.EqualTo(1)); + Assert.That(loadedEnrs[0].EnrString, Is.EqualTo(validEnr.EnrString)); + Assert.That(_legacyDiscoveryDb, Has.Count.EqualTo(1), "Malformed legacy records should remain untouched"); + Assert.That(_discoveryDb, Has.Count.EqualTo(1), "Valid records should still be migrated"); + } + } + [Test] public void Should_Reject_Private_Ip_Enr() { - ENR enr = CreateTestEnrBytes(TestItem.PrivateKeyA, IPAddress.Loopback); + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Loopback); bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); @@ -139,7 +182,7 @@ public void Should_Reject_Private_Ip_Enr() public void Should_Accept_Private_Ip_Enr_On_Private_Deployment() { DiscoveryV5App privateDiscoveryApp = CreateDiscoveryV5App(IPAddress.Loopback); - ENR enr = CreateTestEnrBytes(TestItem.PrivateKeyA, IPAddress.Loopback); + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Loopback); bool result = privateDiscoveryApp.TryGetNodeFromEnr(enr, out Node? node); @@ -151,7 +194,7 @@ public void Should_Accept_Private_Ip_Enr_On_Private_Deployment() [Test] public void Should_Accept_Public_Ip_Enr() { - ENR enr = CreateTestEnrBytes(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); @@ -160,12 +203,24 @@ public void Should_Accept_Public_Ip_Enr() Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); } + [Test] + public void Should_Use_Udp_Port_From_Enr() + { + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), port: 30303, udpPort: 30304); + + bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + + Assert.That(result, Is.True); + Assert.That(node, Is.Not.Null); + Assert.That(node!.Port, Is.EqualTo(30304)); + } + [Test] public void TryEnqueueNewEnr_Should_Deduplicate() { - Queue queue = new(); - HashSet seenNodes = []; - ENR enr = CreateTestEnrBytes(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); + Queue queue = new(); + HashSet seenNodes = []; + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, enr), Is.True); Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, enr), Is.False); @@ -175,14 +230,14 @@ public void TryEnqueueNewEnr_Should_Deduplicate() [Test] public void TryEnqueueNewEnr_Should_Respect_Tracked_Cap() { - Queue queue = new(); - HashSet seenNodes = []; + Queue queue = new(); + HashSet seenNodes = []; for (int i = 0; i < DiscoveryV5App.MaxTrackedEnrsPerWalk; i++) { - seenNodes.Add(Substitute.For()); + seenNodes.Add(new NodeRecord()); } - ENR candidate = CreateTestEnrBytes(TestItem.PrivateKeyB, IPAddress.Parse("1.1.1.1"), port: 30304); + NodeRecord candidate = CreateTestEnr(TestItem.PrivateKeyB, IPAddress.Parse("1.1.1.1"), port: 30304); Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, candidate), Is.False); Assert.That(queue.Count, Is.EqualTo(0)); @@ -191,15 +246,15 @@ public void TryEnqueueNewEnr_Should_Respect_Tracked_Cap() [Test] public void TryEnqueueNewEnr_Should_Respect_Pending_Cap() { - Queue queue = new(); - ENR existing = CreateTestEnrBytes(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); + Queue queue = new(); + NodeRecord existing = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); for (int i = 0; i < DiscoveryV5App.MaxPendingEnrsPerWalk; i++) { queue.Enqueue(existing); } - HashSet seenNodes = []; - ENR candidate = CreateTestEnrBytes(TestItem.PrivateKeyB, IPAddress.Parse("1.1.1.1"), port: 30304); + HashSet seenNodes = []; + NodeRecord candidate = CreateTestEnr(TestItem.PrivateKeyB, IPAddress.Parse("1.1.1.1"), port: 30304); Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, candidate), Is.False); Assert.That(queue.Count, Is.EqualTo(DiscoveryV5App.MaxPendingEnrsPerWalk)); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index 519cfb9f4fcd..fad7f7261738 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -11,7 +11,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -52,7 +52,7 @@ public void Setup() } private void RoutingTableReturns(params Node[] nodes) => - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns(nodes); private void FindNeighboursReturns(Node from, params Node[] result) => @@ -80,8 +80,8 @@ public async Task Lookup_should_return_nodes_from_routing_table(CancellationToke Assert.That(result, Is.EquivalentTo(expectedNodes)); _routingTable.Received(1).GetKNearestNeighbour( - Arg.Is(h => h == _targetKey.Hash), - Arg.Any()); + Arg.Is(h => h == TargetHash), + Arg.Any()); } [Test] @@ -178,5 +178,7 @@ public async Task Lookup_should_not_return_duplicate_nodes(CancellationToken tok Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); } + + private KademliaHash TargetHash => KademliaHash.FromBytes(_targetKey.Hash.Bytes); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 9969b6547f82..dbe5902b4b2c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -15,7 +15,7 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Messages; using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs index dc27fe10fab5..0cdec0be6cb1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Nethermind.Core; @@ -12,7 +13,7 @@ using Nethermind.Core.Utils; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; @@ -71,11 +72,11 @@ public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToke Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); _nodeSession.OnPongReceived(); - _lookup.Lookup(Arg.Any(), token) + _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1, node2)); - _discv4Adapter.Ping(node1, token) + _discv4Adapter.Ping(node1, Arg.Any()) .Returns(Task.CompletedTask); - _discv4Adapter.Ping(node2, token) + _discv4Adapter.Ping(node2, Arg.Any()) .Returns(Task.CompletedTask); IAsyncEnumerator enumerator = _nodeSource.DiscoverNodes(token).GetAsyncEnumerator(token); @@ -84,7 +85,7 @@ public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToke await enumerator.MoveNextAsync(); Assert.That(enumerator.Current, Is.EqualTo(node2)); - _lookup.Received().Lookup(Arg.Any(), token); + _lookup.Received().Lookup(Arg.Any(), Arg.Any()); } [Test] @@ -92,7 +93,7 @@ public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToke public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(CancellationToken token) { Node node = new(TestItem.PublicKeyA, "192.168.1.1", 30303); - _lookup.Lookup(Arg.Any(), token) + _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node)); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -102,7 +103,7 @@ public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(Ca // Assert - Verify that ping was called await _discv4Adapter.Received(2).Ping( Arg.Is(n => n == node), - token); + Arg.Any()); } [Test] @@ -123,7 +124,7 @@ public async Task DiscoverNodes_should_skip_nodes_that_have_tried_ping_recently_ // Set up session2 to have received a pong session2.OnPongReceived(); - _lookup.Lookup(Arg.Any(), token) + _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1, node2)); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -134,7 +135,7 @@ public async Task DiscoverNodes_should_skip_nodes_that_have_tried_ping_recently_ await _discv4Adapter.DidNotReceive().Ping( Arg.Is(n => n == node1), - token); + Arg.Any()); } [Test] @@ -144,12 +145,12 @@ public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken tok Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); - _discv4Adapter.Ping(node1, token) + _discv4Adapter.Ping(node1, Arg.Any()) .Returns(Task.FromException(new OperationCanceledException())); - _discv4Adapter.Ping(node2, token) + _discv4Adapter.Ping(node2, Arg.Any()) .Returns(Task.CompletedTask); - _lookup.Lookup(Arg.Any(), token) + _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1, node2)); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -160,7 +161,7 @@ public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken tok await _discv4Adapter.Received(2).Ping( Arg.Is(n => n == node1), - token); + Arg.Any()); } [Test] @@ -172,7 +173,7 @@ public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(Cancellat _nodeSession.OnPongReceived(); - _lookup.Lookup(Arg.Any(), token) + _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1)); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -220,7 +221,7 @@ public async Task DiscoverNodes_should_use_multiple_concurrent_discovery_jobs(Ca // Set up the lookup to return different nodes for different calls int callCount = 0; - _lookup.Lookup(Arg.Any(), token) + _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(_ => { callCount++; @@ -238,7 +239,22 @@ public async Task DiscoverNodes_should_use_multiple_concurrent_discovery_jobs(Ca // Assert - Verify that lookup was called at least twice _lookup.Received(2).Lookup( Arg.Any(), - token); + Arg.Any()); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_stop_background_jobs_when_enumeration_is_disposed(CancellationToken token) + { + _discoveryConfig.ConcurrentDiscoveryJob = 1; + Node node = new(TestItem.PublicKeyA, "192.168.1.1", 30303); + _nodeSession.OnPongReceived(); + _lookup.Lookup(Arg.Any(), Arg.Any()) + .Returns(CreateAsyncEnumerable(node)); + + List nodes = await _nodeSource.DiscoverNodes(CancellationToken.None).Take(1).ToListAsync(token); + + Assert.That(nodes, Is.EqualTo(new[] { node })); } private static async IAsyncEnumerable CreateAsyncEnumerable(params IEnumerable items) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs new file mode 100644 index 000000000000..764d80142875 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Modules; +using Nethermind.Crypto; +using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Enr; +using NUnit.Framework; +using System.Net; + +namespace Nethermind.Network.Discovery.Test.Discv5; + +public class Discv5CodecTests +{ + private static readonly byte[] NodeAId = Bytes.FromHexString("0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb"); + private static readonly byte[] NodeBId = Bytes.FromHexString("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9"); + private const string GethNodeAPrivateKey = "0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f"; + private const string GethNodeBPrivateKey = "0x66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628"; + + [Test] + public void CompressedAgreement_Matches_Devp2p_Vector() + { + CompressedPublicKey publicKey = new("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231"); + PrivateKey privateKey = new("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736"); + + byte[] sharedSecret = SecP256k1Agreement.AgreeCompressed(publicKey, privateKey); + + Assert.That(sharedSecret.ToHexString(true), Is.EqualTo("0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e")); + } + + [Test] + public void KeyDerivation_Matches_Devp2p_Vector() + { + CompressedPublicKey destinationPublicKey = new("0x0317931e6e0840220642f230037d285d122bc59063221ef3226b1f403ddc69ca91"); + PrivateKey ephemeralPrivateKey = new("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736"); + byte[] secret = SecP256k1Agreement.AgreeCompressed(destinationPublicKey, ephemeralPrivateKey); + byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); + + (byte[] initiatorKey, byte[] recipientKey) = Discv5PacketCodec.DeriveKeysForTest(secret, NodeAId, NodeBId, challengeData); + + Assert.That(initiatorKey.ToHexString(true), Is.EqualTo("0xdccc82d81bd610f4f76d3ebe97a40571")); + Assert.That(recipientKey.ToHexString(true), Is.EqualTo("0xac74bb8773749920b0d3a8881c173ec5")); + } + + [Test] + public void IdNonceSignature_Matches_Devp2p_Vector() + { + PrivateKey staticKey = new("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736"); + byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); + byte[] ephemeralPublicKey = Bytes.FromHexString("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231"); + byte[] signingHash = Discv5PacketCodec.CalculateIdSignatureHashForTest(challengeData, ephemeralPublicKey, NodeBId); + + Signature signature = new Ecdsa().Sign(staticKey, new ValueHash256(signingHash)); + + Assert.That(signature.Bytes.ToArray().ToHexString(true), Is.EqualTo("0x94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6")); + } + + [Test] + public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() + { + byte[] packetBytes = Bytes.FromHexString( + "0x00000000000000000000000000000000088b3d4342774649325f313964a39e55" + + "ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + + "4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); + + bool decoded = Discv5PacketCodec.TryDecode(packetBytes, NodeBId, out Discv5Packet packet); + bool decrypted = Discv5PacketCodec.TryDecryptMessageForTest(packet, new byte[16], out Discv5Message message); + + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(Discv5PacketFlag.Ordinary)); + Assert.That(packet.AuthData, Is.EqualTo(NodeAId)); + Assert.That(decrypted, Is.True); + Assert.That(message, Is.InstanceOf()); + Discv5Ping ping = (Discv5Ping)message; + Assert.That(ping.RequestId, Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + Assert.That(ping.EnrSequence, Is.EqualTo(2)); + } + + [Test] + public void PacketCodec_Decodes_WhoAreYou_GoEthereum_Vector() + { + byte[] packetBytes = Bytes.FromHexString( + "0x00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad" + + "1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d"); + byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); + + bool decoded = Discv5PacketCodec.TryDecode(packetBytes, NodeBId, out Discv5Packet packet); + Discv5PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + Discv5Challenge challenge = codec.DecodeWhoAreYou(packet); + + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(Discv5PacketFlag.WhoAreYou)); + Assert.That(challenge.RequestNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c")); + Assert.That(challenge.IdNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c0d0e0f10")); + Assert.That(challenge.EnrSequence, Is.Zero); + Assert.That(challenge.ChallengeData, Is.EqualTo(challengeData)); + } + + [TestCase( + "0x00000000000000000000000000000000088b3d4342774649305f313964a39e55" + + "ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + + "4c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef" + + "268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfb" + + "a776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1" + + "f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d83" + + "9cf8", + 1UL, + "0x4f9fac6de7567d1e3b1241dffe90f662", + "0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000001", + false)] + [TestCase( + "0x00000000000000000000000000000000088b3d4342774649305f313964a39e55" + + "ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + + "4c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be9856" + + "2fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b2" + + "1481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1" + + "f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6" + + "cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1" + + "2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a" + + "80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e" + + "4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b1394" + + "71", + 0UL, + "0x53b1c075f41876423154e157470c2f48", + "0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000", + true)] + public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( + string packetHex, + ulong challengeEnrSequence, + string expectedReadKeyHex, + string challengeDataHex, + bool includesRecord) + { + byte[] packetBytes = Bytes.FromHexString(packetHex); + Discv5Challenge challenge = new( + Bytes.FromHexString("0x0102030405060708090a0b0c"), + Bytes.FromHexString("0x0102030405060708090a0b0c0d0e0f10"), + challengeEnrSequence, + Bytes.FromHexString(challengeDataHex)); + Discv5PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + NodeRecord? knownRecord = includesRecord ? null : CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); + + bool decoded = Discv5PacketCodec.TryDecode(packetBytes, NodeBId, out Discv5Packet packet); + bool decrypted = codec.TryDecryptHandshake(packet, challenge, knownRecord, out Discv5Session session, out Discv5Message message, out NodeRecord? nodeRecord); + + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(Discv5PacketFlag.Handshake)); + Assert.That(decrypted, Is.True); + Assert.That(session.ReadKey.ToHexString(true), Is.EqualTo(expectedReadKeyHex)); + Assert.That(message, Is.InstanceOf()); + Discv5Ping ping = (Discv5Ping)message; + Assert.That(ping.RequestId, Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + Assert.That(ping.EnrSequence, Is.EqualTo(1)); + Assert.That(nodeRecord is not null, Is.EqualTo(includesRecord)); + } + + [Test] + public void MessageCodec_Roundtrips_FindNode() + { + Discv5FindNode message = new([0, 0, 0, 1], [255, 254, 256]); + + Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + + Assert.That(decoded, Is.InstanceOf()); + Discv5FindNode decodedFindNode = (Discv5FindNode)decoded; + Assert.That(decodedFindNode.RequestId, Is.EqualTo(message.RequestId)); + Assert.That(decodedFindNode.Distances, Is.EqualTo(message.Distances)); + } + + private static Discv5PacketCodec CreateCodec(PrivateKey privateKey) + => new( + new InsecureProtectedPrivateKey(privateKey), + new TestNodeRecordProvider(privateKey), + new CryptoRandom(), + new EthereumEcdsa(0)); + + private static NodeRecord CreateNodeRecord(PrivateKey privateKey) + { + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(IdEntry.Instance); + nodeRecord.SetEntry(new IpEntry(IPAddress.Loopback)); + nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + nodeRecord.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(nodeRecord); + return nodeRecord; + } + + private sealed class TestNodeRecordProvider(PrivateKey privateKey) : INodeRecordProvider + { + public NodeRecord Current { get; } = CreateNodeRecord(privateKey); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs new file mode 100644 index 000000000000..5b970419be1a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv5; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv5; + +public class Discv5KademliaAdapterTests +{ + private IKademlia _kademlia = null!; + + [SetUp] + public void SetUp() => _kademlia = Substitute.For>(); + + [Test] + public void GetNodesAtDistances_ShouldMapEachDistanceToKademliaTable() + { + Node nodeA = CreateNode(TestItem.PublicKeyA, 1); + Node nodeB = CreateNode(TestItem.PublicKeyB, 2); + Node nodeC = CreateNode(TestItem.PublicKeyC, 3); + + _kademlia.GetAllAtDistance(10).Returns([nodeA, nodeB]); + _kademlia.GetAllAtDistance(11).Returns([nodeB, nodeC]); + _kademlia.ClearReceivedCalls(); + + Discv5KademliaAdapter adapter = CreateAdapter(); + + Node[] result = adapter.GetNodesAtDistances([10, 11]); + + Assert.That(result, Is.EqualTo(new[] { nodeA, nodeB, nodeC })); + _kademlia.Received(1).GetAllAtDistance(10); + _kademlia.Received(1).GetAllAtDistance(11); + } + + [Test] + public void GetNodesAtDistances_ShouldExcludeRequester() + { + Node requester = CreateNode(TestItem.PublicKeyA, 1); + Node returned = CreateNode(TestItem.PublicKeyB, 2); + + _kademlia.GetAllAtDistance(10).Returns([requester, returned]); + + Discv5KademliaAdapter adapter = CreateAdapter(); + + Node[] result = adapter.GetNodesAtDistances([10], requester); + + Assert.That(result, Is.EqualTo(new[] { returned })); + } + + [TestCase(-1)] + [TestCase(257)] + public void GetNodesAtDistances_ShouldRejectInvalidDistance(int distance) + { + Discv5KademliaAdapter adapter = CreateAdapter(); + + Assert.Throws(() => adapter.GetNodesAtDistances([distance])); + } + + private Discv5KademliaAdapter CreateAdapter() => new( + new Lazy>(_kademlia), + null!, + null!, + null!, + new DiscoveryConfig(), + new CryptoRandom(), + LimboLogs.Instance); + + private static Node CreateNode(PublicKey publicKey, int hostSuffix) => + new(publicKey, $"192.168.1.{hostSuffix}", 30303); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs new file mode 100644 index 000000000000..c912d1259ef3 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using DotNetty.Buffers; +using DotNetty.Transport.Channels; +using DotNetty.Transport.Channels.Embedded; +using DotNetty.Transport.Channels.Sockets; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Core.Test.Modules; +using Nethermind.Crypto; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Enr; +using Nethermind.Network.Discovery.Discv5; +using Nethermind.Serialization.Rlp; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv5; + +public class Discv5WireTests +{ + [Test] + public async Task Ping_Completes_After_WhoAreYou_Handshake() + { + IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); + IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); + TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) + { + Enr = peerB.NodeRecordProvider.Current.EnrString + }; + + using CancellationTokenSource cancellationSource = new(10_000); + Task runA = peerA.Adapter.RunAsync(cancellationSource.Token); + Task runB = peerB.Adapter.RunAsync(cancellationSource.Token); + + Task pingTask = peerA.Adapter.Ping(nodeB, cancellationSource.Token); + await PumpUntilComplete(pingTask, peerA, peerB, cancellationSource.Token); + await pingTask; + + await cancellationSource.CancelAsync(); + await Task.WhenAll(runA, runB); + + peerA.Kademlia.Received().AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyB.PublicKey) && !string.IsNullOrEmpty(node.Enr))); + peerB.Kademlia.Received().AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyA.PublicKey) && !string.IsNullOrEmpty(node.Enr))); + } + + [Test] + public async Task FindNeighbours_Returns_Records_At_Requested_Distance() + { + IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); + IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); + IPEndPoint endpointC = IPEndPoint.Parse("127.0.0.1:10002"); + TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + TestPeer peerC = CreatePeer(TestItem.PrivateKeyC, endpointC); + Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) + { + Enr = peerB.NodeRecordProvider.Current.EnrString + }; + Node nodeC = new(TestItem.PrivateKeyC.PublicKey, endpointC) + { + Enr = peerC.NodeRecordProvider.Current.EnrString + }; + int[] requestedDistances = GetLookupDistances(nodeB, TestItem.PrivateKeyC.PublicKey); + for (int i = 0; i < requestedDistances.Length; i++) + { + peerB.Kademlia.GetAllAtDistance(requestedDistances[i]).Returns([]); + } + + peerB.Kademlia.GetAllAtDistance(requestedDistances[0]).Returns([nodeC]); + + using CancellationTokenSource cancellationSource = new(10_000); + Task runA = peerA.Adapter.RunAsync(cancellationSource.Token); + Task runB = peerB.Adapter.RunAsync(cancellationSource.Token); + + Task findTask = peerA.Adapter.FindNeighbours(nodeB, TestItem.PrivateKeyC.PublicKey, cancellationSource.Token); + await PumpUntilComplete(findTask, peerA, peerB, cancellationSource.Token); + Node[] nodes = await findTask; + + await cancellationSource.CancelAsync(); + await Task.WhenAll(runA, runB); + + Assert.That(nodes, Has.Length.EqualTo(1)); + Assert.That(nodes[0].Id, Is.EqualTo(TestItem.PrivateKeyC.PublicKey)); + peerA.Kademlia.Received().AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyC.PublicKey))); + } + + private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint) + { + IKademlia kademlia = Substitute.For>(); + NettyDiscoveryV5Handler handler = new(new TestLogManager()); + EmbeddedChannel channel = new(); + handler.InitializeChannel(channel); + + TestNodeRecordProvider nodeRecordProvider = new(privateKey, endpoint); + Discv5KademliaAdapter adapter = new( + new Lazy>(kademlia), + handler, + new Discv5PacketCodec( + new InsecureProtectedPrivateKey(privateKey), + nodeRecordProvider, + new CryptoRandom(), + new EthereumEcdsa(0)), + nodeRecordProvider, + new DiscoveryConfig(), + new CryptoRandom(), + LimboLogs.Instance); + + return new TestPeer(adapter, handler, channel, kademlia, nodeRecordProvider, endpoint); + } + + private static async Task PumpUntilComplete(Task task, TestPeer peerA, TestPeer peerB, CancellationToken token) + { + while (!task.IsCompleted) + { + Pump(peerA, peerB); + Pump(peerB, peerA); + await Task.Delay(10, token); + } + + Pump(peerA, peerB); + Pump(peerB, peerA); + } + + private static void Pump(TestPeer from, TestPeer to) + { + while (from.Channel.ReadOutbound() is { } packet) + { + byte[] data = packet.Content.ReadAllBytesAsArray(); + IChannelHandlerContext context = Substitute.For(); + to.Handler.ChannelRead(context, new DatagramPacket(Unpooled.WrappedBuffer(data), from.Endpoint, to.Endpoint)); + } + } + + private static int[] GetLookupDistances(Node receiver, PublicKey target) + { + KademliaHash receiverHash = KademliaHash.FromBytes(receiver.Id.Hash.Bytes); + KademliaHash targetHash = KademliaHash.FromBytes(target.Hash.Bytes); + int distance = Hash256XorUtils.CalculateLogDistance(receiverHash, targetHash); + + List distances = [distance]; + if (distance > 0) + { + distances.Add(distance - 1); + } + + if (distance < Hash256XorUtils.MaxDistance) + { + distances.Add(distance + 1); + } + + return [.. distances]; + } + + private sealed record TestPeer( + Discv5KademliaAdapter Adapter, + NettyDiscoveryV5Handler Handler, + EmbeddedChannel Channel, + IKademlia Kademlia, + TestNodeRecordProvider NodeRecordProvider, + IPEndPoint Endpoint); + + private sealed class TestNodeRecordProvider : INodeRecordProvider + { + public TestNodeRecordProvider(PrivateKey privateKey, IPEndPoint endpoint) + { + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(IdEntry.Instance); + nodeRecord.SetEntry(new IpEntry(endpoint.Address)); + nodeRecord.SetEntry(new TcpEntry(endpoint.Port)); + nodeRecord.SetEntry(new UdpEntry(endpoint.Port)); + nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + nodeRecord.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(nodeRecord); + Current = nodeRecord; + } + + public NodeRecord Current { get; } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs index 42e71b2cc693..c4bfa246ccb2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs @@ -67,7 +67,6 @@ private IContainer CreateNode(PrivateKey nodeKey, IEnode? bootEnode = null) [Parallelizable(ParallelScope.None)] public async Task TestDiscovery() { - if (discoveryVersion == DiscoveryVersion.V5) Assert.Ignore("DiscV5 does not seems to work."); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource().ThatCancelAfter(TestTimeout); await using IContainer boot = CreateNode(TestItem.PrivateKeys[0]); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs index e88cdf643c14..756b36357272 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -52,7 +51,7 @@ public class Hash256XorUtilsTests "0x000000000000000000000000000000000000000000000000000000000001000f", 17)] public void TestDistance(string hash1, string hash2, string xosString, int expectedDistance) { - ValueHash256 xor = Hash256XorUtils.XorDistance(new(hash1), new(hash2)); + KademliaHash xor = Hash256XorUtils.XorDistance(new(hash1), new(hash2)); Assert.That(xor.ToString(), Is.EqualTo(xosString.ToLower())); Assert.That(Hash256XorUtils.CalculateLogDistance(new(hash1), new(hash2)), Is.EqualTo(expectedDistance)); Assert.That(Hash256XorUtils.CalculateLogDistance(new(hash2), new(hash1)), Is.EqualTo(expectedDistance)); @@ -62,12 +61,13 @@ public void TestDistance(string hash1, string hash2, string xosString, int expec public void TestGetRandomHash() { Random rand = new(0); - ValueHash256 randomized = new(); - rand.NextBytes(randomized.BytesAsSpan); + Span randomizedBytes = stackalloc byte[KademliaHash.Length]; + rand.NextBytes(randomizedBytes); + KademliaHash randomized = KademliaHash.FromBytes(randomizedBytes); void TestForDistance(int distance) { - ValueHash256 randHash = Hash256XorUtils.GetRandomHashAtDistance(randomized, distance, rand); + KademliaHash randHash = Hash256XorUtils.GetRandomHashAtDistance(randomized, distance, rand); Assert.That(Hash256XorUtils.CalculateLogDistance(randomized, randHash), Is.EqualTo(distance)); } @@ -85,9 +85,9 @@ void TestForDistance(int distance) [TestCase] public void TestDistanceCompare() { - ValueHash256 h1 = new("0x0010000000000000000000000000000000000000000000000000000000000000"); - ValueHash256 h2 = new("0x0110000000000000000000000000000000000000000000000000000000000000"); - ValueHash256 h3 = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + KademliaHash h1 = new("0x0010000000000000000000000000000000000000000000000000000000000000"); + KademliaHash h2 = new("0x0110000000000000000000000000000000000000000000000000000000000000"); + KademliaHash h3 = new("0x0000000000000000000000000000000000000000000000000000000000000000"); Assert.That(Hash256XorUtils.Compare(h1, h2, h3), Is.LessThan(0)); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs index df4b9fb6cedf..8229005a101a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -10,5 +10,7 @@ internal sealed class IdentityNodeHashProvider : INodeHashProvider { public static readonly IdentityNodeHashProvider Instance = new(); - public ValueHash256 GetHash(ValueHash256 node) => node; + public static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + + public KademliaHash GetHash(ValueHash256 node) => ToKademliaHash(node); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index ed4ece474a3f..df517bbd1314 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -3,7 +3,7 @@ using System.Linq; using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -19,22 +19,22 @@ public void TryAddOrRefresh_ShouldLimitToK() foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); } // Again foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); } Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(toAdd[..5].ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (it, it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (ToKademliaHash(it), it)).ToHashSet())); foreach (ValueHash256 valueHash256 in toAdd[..5]) { - Assert.That(bucket.ContainsNode(valueHash256), Is.True); - Assert.That(bucket.GetByHash(valueHash256), Is.EqualTo(valueHash256)); + Assert.That(bucket.ContainsNode(ToKademliaHash(valueHash256)), Is.True); + Assert.That(bucket.GetByHash(ToKademliaHash(valueHash256)), Is.EqualTo(valueHash256)); } } @@ -47,14 +47,14 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); } ValueHash256[] nodes = bucket.GetAll(); foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); } Assert.That(bucket.GetAll(), Is.SameAs(nodes)); @@ -69,13 +69,15 @@ public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(valueHash256, valueHash256, out _); + bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); } - bucket.RemoveAndReplace(toAdd[0]); + bucket.RemoveAndReplace(ToKademliaHash(toAdd[0])); ValueHash256[] expected = [.. toAdd[1..5], toAdd[9]]; Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(expected.ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (it, it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (ToKademliaHash(it), it)).ToHashSet())); } + + private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs index 2280e2d5d179..f86518d7e08e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs @@ -4,8 +4,8 @@ using System; using System.Linq; using Nethermind.Core.Crypto; +using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -20,10 +20,12 @@ public class KBucketTreeTests LimboLogs.Instance); private static void Add(KBucketTree tree, ValueHash256 hash) => - tree.TryAddOrRefresh(hash, hash, out _); + tree.TryAddOrRefresh(IdentityNodeHashProvider.ToKademliaHash(hash), hash, out _); private static ValueHash256 HashAtDistance(int distance, byte tag) => - Hash256XorUtils.GetRandomHashAtDistance(SelfHash, distance, new Random(tag)); + ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(IdentityNodeHashProvider.ToKademliaHash(SelfHash), distance, new Random(tag))); + + private static ValueHash256 ToValueHash(KademliaHash hash) => new(hash.Bytes); [Test] public void Split_should_preserve_lru_order_in_child_buckets() diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 34352ce9657b..17168acb4d2a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -11,6 +11,7 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Logging; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Kademlia; using NonBlocking; using NUnit.Framework; @@ -146,7 +147,7 @@ public async Task SimulateLargeKNearestNeighbour() { TestNode[] nodesClosest = await mainNode.LookupNodesClosest(targetNode, cts.Token); HashSet expectedNodeClosestK = nodeIds - .Order(Comparer.Create((n1, n2) => Hash256XorUtils.Compare(n1, n2, targetNode))) + .Order(Comparer.Create((n1, n2) => Hash256XorUtils.Compare(ToKademliaHash(n1), ToKademliaHash(n2), ToKademliaHash(targetNode)))) .Take(_config.KSize) .ToHashSet(); @@ -184,16 +185,20 @@ private static ValueHash256 RandomKeccak(Random rand) return val; } + private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + + private static ValueHash256 ToValueHash(KademliaHash hash) => new(hash.Bytes); + private class ValueHashNodeHashProvider : IKeyOperator { public ValueHash256 GetKey(TestNode node) => node.Hash; - public ValueHash256 GetKeyHash(ValueHash256 key) => key; + public KademliaHash GetKeyHash(ValueHash256 key) => ToKademliaHash(key); - public ValueHash256 CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) => - Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth); + public ValueHash256 CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth) => + ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth)); - public ValueHash256 GetHash(ValueHash256 key) => key; + public KademliaHash GetHash(ValueHash256 key) => ToKademliaHash(key); } private class TestFabric(KademliaConfig config) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 352858d403b6..f67246c43e13 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -9,6 +9,7 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Logging; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Kademlia; using NSubstitute; using NUnit.Framework; @@ -88,7 +89,7 @@ public async Task TestTooManyNode() Beta = 0, }); - ValueHash256[] testHashes = Enumerable.Range(0, 10).Select((k) => Hash256XorUtils.GetRandomHashAtDistance(ValueKeccak.Zero, 250)).ToArray(); + ValueHash256[] testHashes = Enumerable.Range(0, 10).Select((k) => RandomValueHashAtDistance(ValueKeccak.Zero, 250)).ToArray(); foreach (ValueHash256 valueHash256 in testHashes[..10]) { kad.AddOrRefresh(valueHash256); @@ -157,16 +158,16 @@ public async Task TestTooManyNodeWithAcceleratedLookup() ValueHash256[] testHashes = new IEnumerable[] { Enumerable.Range(0, 5).Select((k) => - Hash256XorUtils.GetRandomHashAtDistance(new("0x0000000000000000000000000000000000000000000000000000000000000000"), 248) + RandomValueHashAtDistance(new("0x0000000000000000000000000000000000000000000000000000000000000000"), 248) ), Enumerable.Range(0, 5).Select((k) => - Hash256XorUtils.GetRandomHashAtDistance(new("0x0100000000000000000000000000000000000000000000000000000000000000"), 248) + RandomValueHashAtDistance(new("0x0100000000000000000000000000000000000000000000000000000000000000"), 248) ), Enumerable.Range(0, 5).Select((k) => - Hash256XorUtils.GetRandomHashAtDistance(new("0x0200000000000000000000000000000000000000000000000000000000000000"), 248) + RandomValueHashAtDistance(new("0x0200000000000000000000000000000000000000000000000000000000000000"), 248) ), Enumerable.Range(0, 5).Select((k) => - Hash256XorUtils.GetRandomHashAtDistance(new("0x0300000000000000000000000000000000000000000000000000000000000000"), 248) + RandomValueHashAtDistance(new("0x0300000000000000000000000000000000000000000000000000000000000000"), 248) ), }.SelectMany(it => it).ToArray(); @@ -181,13 +182,20 @@ public async Task TestTooManyNodeWithAcceleratedLookup() Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(testHashes[10..].ToHashSet())); } + private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + + private static ValueHash256 ToValueHash(KademliaHash hash) => new(hash.Bytes); + + private static ValueHash256 RandomValueHashAtDistance(ValueHash256 currentHash, int distance) => + ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(ToKademliaHash(currentHash), distance)); + private class ValueHashNodeHashProvider : IKeyOperator { public ValueHash256 GetKey(ValueHash256 node) => node; - public ValueHash256 GetKeyHash(ValueHash256 key) => key; + public KademliaHash GetKeyHash(ValueHash256 key) => ToKademliaHash(key); - public ValueHash256 CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) => - Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth); + public ValueHash256 CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth) => + ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth)); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 089622bcf079..d8215cd2c80a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -6,8 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Nethermind.Core.Crypto; +using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; using NSubstitute; using NUnit.Framework; @@ -25,7 +25,7 @@ public class LookupKNearestNeighbourTests private static (LookupKNearestNeighbour Lookup, IRoutingTable Routing, INodeHealthTracker Health) CreateLookup(int alpha, TimeSpan hardTimeout, ValueHash256[] seeds) { IRoutingTable routing = Substitute.For>(); - routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); + routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); INodeHealthTracker health = Substitute.For>(); @@ -56,7 +56,7 @@ public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, Ca using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); Task task = lookup.Lookup( - Seed1, + IdentityNodeHashProvider.ToKademliaHash(Seed1), 8, async (_, t) => { @@ -87,7 +87,7 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C }; ValueHash256[] result = await lookup.Lookup( - Self, + IdentityNodeHashProvider.ToKademliaHash(Self), 8, (node, _) => Task.FromResult(neighbours.GetValueOrDefault(node, [])), token); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index 3ee3330a6819..8eb99bf6660d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -6,8 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Nethermind.Core.Crypto; +using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; using NSubstitute; using NUnit.Framework; @@ -19,14 +19,14 @@ public class NodeHealthTrackerTests private const string Remote = "remote"; private const string Stale = "stale"; - private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( + private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( string? toRefresh = null, int failureThreshold = 5, TimeSpan? refreshPingTimeout = null, - IKademliaMessageSender? sender = null) + IKademliaMessageSender? sender = null) { RoutingTableStub routing = new() { ToRefresh = toRefresh ?? string.Empty }; - sender ??= Substitute.For>(); + sender ??= Substitute.For>(); KademliaConfig config = new() { CurrentNodeId = Self, @@ -34,7 +34,7 @@ private static (NodeHealthTracker Tracker, RoutingTableStu }; if (refreshPingTimeout is { } timeout) config.RefreshPingTimeout = timeout; - NodeHealthTracker tracker = new( + NodeHealthTracker tracker = new( config, routing, StringNodeHashProvider.Instance, @@ -46,12 +46,12 @@ private static (NodeHealthTracker Tracker, RoutingTableStu [Test] public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSelectsSelf() { - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); tracker.OnIncomingMessageFrom(Remote); Assert.That(routing.AddCalls, Has.Count.EqualTo(2)); - Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(ValueKeccak.Compute(Self))); + Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(ToKademliaHash(ValueKeccak.Compute(Self)))); Assert.That(routing.AddCalls[1].Node, Is.EqualTo(Self)); } @@ -59,34 +59,35 @@ public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSe [CancelAfter(10000)] public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(CancellationToken token) { - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()) .Returns(Task.FromException(new OperationCanceledException())); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, refreshPingTimeout: TimeSpan.FromMilliseconds(50), sender: sender); tracker.OnIncomingMessageFrom(Remote); - await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(ValueKeccak.Compute(Stale)), token); + KademliaHash staleHash = ToKademliaHash(ValueKeccak.Compute(Stale)); + await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(staleHash), token); } [Test] [CancelAfter(10000)] public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken token) { - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()).Returns(Task.CompletedTask); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, sender: sender); tracker.OnIncomingMessageFrom(Remote); - ValueHash256 staleHash = ValueKeccak.Compute(Stale); + KademliaHash staleHash = ToKademliaHash(ValueKeccak.Compute(Stale)); await AssertEventuallyAsync(() => routing.HasAddedNode(staleHash), token); Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); } @@ -94,14 +95,14 @@ public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken t [Test] public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() { - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); tracker.OnRequestFailed(Remote); tracker.OnRequestFailed(Remote); tracker.OnRequestFailed(Remote); Assert.That(routing.RemoveCalls, Has.Count.EqualTo(1)); - Assert.That(routing.RemoveCalls[0], Is.EqualTo(ValueKeccak.Compute(Remote))); + Assert.That(routing.RemoveCalls[0], Is.EqualTo(ToKademliaHash(ValueKeccak.Compute(Remote)))); } private static async Task AssertEventuallyAsync(Func condition, CancellationToken token) @@ -114,24 +115,33 @@ private static async Task AssertEventuallyAsync(Func condition, Cancellati Assert.Fail("Condition not met within timeout."); } + private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + private sealed class StringNodeHashProvider : INodeHashProvider { public static readonly StringNodeHashProvider Instance = new(); - public ValueHash256 GetHash(string node) => ValueKeccak.Compute(node); + + public KademliaHash GetHash(string node) => ToKademliaHash(ValueKeccak.Compute(node)); } private sealed class RoutingTableStub : IRoutingTable { public string ToRefresh { get; init; } = string.Empty; - public List<(ValueHash256 Hash, string Node)> AddCalls { get; } = []; + public List<(KademliaHash Hash, string Node)> AddCalls { get; } = []; - public List RemoveCalls { get; } = []; + public List RemoveCalls { get; } = []; - public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, string item, out string? toRefresh) + public BucketAddResult TryAddOrRefresh(in KademliaHash hash, string item, out string? toRefresh) { - lock (AddCalls) AddCalls.Add((hash, item)); - if (AddCalls.Count == 1) + bool isFirstAdd; + lock (AddCalls) + { + AddCalls.Add((hash, item)); + isFirstAdd = AddCalls.Count == 1; + } + + if (isFirstAdd) { toRefresh = ToRefresh; return BucketAddResult.Full; @@ -141,11 +151,11 @@ public BucketAddResult TryAddOrRefresh(in ValueHash256 hash, string item, out st return BucketAddResult.Refreshed; } - public bool HasAddedNode(ValueHash256 hash) + public bool HasAddedNode(KademliaHash hash) { lock (AddCalls) { - foreach ((ValueHash256 h, string _) in AddCalls) + foreach ((KademliaHash h, string _) in AddCalls) { if (h == hash) return true; } @@ -153,21 +163,21 @@ public bool HasAddedNode(ValueHash256 hash) return false; } - public bool Remove(in ValueHash256 hash) + public bool Remove(in KademliaHash hash) { lock (RemoveCalls) RemoveCalls.Add(hash); return true; } - public string[] GetKNearestNeighbour(ValueHash256 hash, ValueHash256? exclude = null, bool excludeSelf = false) => + public string[] GetKNearestNeighbour(KademliaHash hash, KademliaHash? exclude = null, bool excludeSelf = false) => throw new NotSupportedException(); public string[] GetAllAtDistance(int i) => throw new NotSupportedException(); - public IEnumerable<(ValueHash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() => + public IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> IterateBuckets() => throw new NotSupportedException(); - public string? GetByHash(ValueHash256 nodeId) => throw new NotSupportedException(); + public string? GetByHash(KademliaHash nodeId) => throw new NotSupportedException(); public void LogDebugInfo() => throw new NotSupportedException(); @@ -183,6 +193,12 @@ public event EventHandler? OnNodeRemoved remove { } } - public int Size => AddCalls.Count; + public int Size + { + get + { + lock (AddCalls) return AddCalls.Count; + } + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Nethermind.Network.Discovery.Test.csproj b/src/Nethermind/Nethermind.Network.Discovery.Test/Nethermind.Network.Discovery.Test.csproj index 856eed9eca29..1de10f0f91a0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Nethermind.Network.Discovery.Test.csproj +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Nethermind.Network.Discovery.Test.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index d5b340b07f9d..8c801191425e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -11,7 +11,6 @@ using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Embedded; using DotNetty.Transport.Channels.Sockets; -using FluentAssertions; using Nethermind.Logging; using Nethermind.Serialization.Rlp; using NSubstitute; @@ -46,9 +45,9 @@ public async Task ForwardsSentMessageToChannel() await _handler.SendAsync(data, to); DatagramPacket packet = _channel.ReadOutbound(); - packet.Should().NotBeNull(); - packet.Content.ReadAllBytesAsArray().Should().BeEquivalentTo(data); - packet.Recipient.Should().Be(to); + Assert.That(packet, Is.Not.Null); + Assert.That(packet.Content.ReadAllBytesAsArray(), Is.EqualTo(data)); + Assert.That(packet.Recipient, Is.EqualTo(to)); } [Test] @@ -62,17 +61,17 @@ public async Task ForwardsReceivedMessageToReader() IAsyncEnumerator enumerator = _handler .ReadMessagesAsync(cancellationSource.Token) .GetAsyncEnumerator(cancellationSource.Token); + ValueTask readTask = enumerator.MoveNextAsync(); IChannelHandlerContext ctx = Substitute.For(); _handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer(data), from, to)); - (await enumerator.MoveNextAsync()).Should().BeTrue(); + Assert.That(await readTask, Is.True); UdpReceiveResult forwardedPacket = enumerator.Current; - forwardedPacket.Should().NotBeNull(); - forwardedPacket.Buffer.Should().BeEquivalentTo(data); - forwardedPacket.RemoteEndPoint.Should().Be(from); + Assert.That(forwardedPacket.Buffer, Is.EqualTo(data)); + Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(from)); } [TestCase(0)] @@ -88,6 +87,7 @@ public async Task SkipsMessagesOfInvalidSize(int size) IAsyncEnumerator enumerator = _handler .ReadMessagesAsync(cancellationSource.Token) .GetAsyncEnumerator(cancellationSource.Token); + ValueTask readTask = enumerator.MoveNextAsync(); IChannelHandlerContext ctx = Substitute.For(); @@ -96,9 +96,9 @@ public async Task SkipsMessagesOfInvalidSize(int size) _handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer((byte[])invalidData.Clone()), from, to)); _handler.Close(); - (await enumerator.MoveNextAsync()).Should().BeTrue(); - enumerator.Current.Buffer.Should().BeEquivalentTo(data); - (await enumerator.MoveNextAsync()).Should().BeFalse(); + Assert.That(await readTask, Is.True); + Assert.That(enumerator.Current.Buffer, Is.EqualTo(data)); + Assert.That(await enumerator.MoveNextAsync(), Is.False); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 646830b94d3f..061c13062cb3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -13,7 +13,7 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats.Model; using LogLevel = DotNetty.Handlers.Logging.LogLevel; diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs index e16de580719d..670a66e22c5d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs @@ -7,7 +7,7 @@ using Nethermind.Db; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 32c8e5b41f52..4ff8c9397b9d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -4,6 +4,7 @@ using Autofac; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs index 378251043ad1..f8fdf0b36d93 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Messages; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs similarity index 88% rename from src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs index c87f9c4e05d6..8bc79f3b4097 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs @@ -4,11 +4,10 @@ using System.Runtime.CompilerServices; using Nethermind.Core; using Nethermind.Core.Caching; -using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Core.Utils; +using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; using NonBlocking; namespace Nethermind.Network.Discovery.Discv4; @@ -29,10 +28,10 @@ public class IteratorNodeLookup( ILogManager logManager) : IIteratorNodeLookup where TNode : notnull { private readonly ILogger _logger = logManager.GetClassLogger>(); - private readonly ValueHash256 _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); + private readonly KademliaHash _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); // Small lru of unreachable nodes, prevent retrying. Pretty effective, although does not improve discovery overall. - private readonly LruCache _unreachableNodes = new(256, ""); + private readonly LruCache _unreachableNodes = new(256, ""); // The maximum round per lookup. Higher means that it will 'see' deeper into the network, but come at a latency // cost of trying many node for increasingly lower new node. @@ -46,23 +45,23 @@ public class IteratorNodeLookup( public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation] CancellationToken token) { - ValueHash256 targetHash = keyOperator.GetKeyHash(target); + KademliaHash targetHash = keyOperator.GetKeyHash(target); if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using AutoCancelTokenSource cts = token.CreateChildTokenSource(); token = cts.Token; - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); - IComparer comparer = Comparer.Create((h1, h2) => + IComparer comparer = Comparer.Create((h1, h2) => Hash256XorUtils.Compare(h1, h2, targetHash)); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(ValueHash256, TNode), ValueHash256> queryQueue = new(comparer); + PriorityQueue<(KademliaHash, TNode), KademliaHash> queryQueue = new(comparer); // Used to determine if the worker should stop - ValueHash256 bestNodeId = ValueKeccak.Zero; + KademliaHash bestNodeId = KademliaHash.Zero; int closestNodeRound = 0; int currentRound = 0; int totalResult = 0; @@ -70,14 +69,14 @@ public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation // Check internal table first foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, null)) { - ValueHash256 nodeHash = keyOperator.GetNodeHash(node); + KademliaHash nodeHash = keyOperator.GetNodeHash(node); seen.TryAdd(nodeHash, node); queryQueue.Enqueue((nodeHash, node), nodeHash); yield return node; - if (bestNodeId == ValueKeccak.Zero || comparer.Compare(nodeHash, bestNodeId) < 0) + if (bestNodeId == KademliaHash.Zero || comparer.Compare(nodeHash, bestNodeId) < 0) { bestNodeId = nodeHash; } @@ -86,7 +85,7 @@ public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation while (true) { token.ThrowIfCancellationRequested(); - if (!queryQueue.TryDequeue(out (ValueHash256 hash, TNode node) toQuery, out ValueHash256 hash256)) + if (!queryQueue.TryDequeue(out (KademliaHash hash, TNode node) toQuery, out KademliaHash hash256)) { // No node to query and running query. if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); @@ -109,7 +108,7 @@ public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation int seenIgnored = 0; foreach (TNode neighbour in neighbours!) { - ValueHash256 neighbourHash = keyOperator.GetNodeHash(neighbour); + KademliaHash neighbourHash = keyOperator.GetNodeHash(neighbour); // Already queried, we ignore if (queried.ContainsKey(neighbourHash)) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index eaabb83db0bd..bf38b0a85597 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -8,7 +8,7 @@ using Nethermind.Core.Extensions; using Nethermind.Core.Utils; using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Messages; using Nethermind.Serialization.Rlp; using Nethermind.Stats; @@ -240,7 +240,7 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms for (int i = 0; i < nodes.Length; i += MaxNodesPerNeighborsMsg) { int batchEnd = Math.Min(i + MaxNodesPerNeighborsMsg, nodes.Length); - await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes[i..batchEnd]), token); + await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), new ArraySegment(nodes, i, batchEnd - i)), token); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 74d94092ede5..8a04efdf4318 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -7,7 +7,7 @@ using System.Threading.Channels; using Nethermind.Core.Crypto; using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv4; @@ -25,6 +25,8 @@ public class KademliaNodeSource( public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); + using CancellationTokenSource disposeCts = CancellationTokenSource.CreateLinkedTokenSource(token); + CancellationToken discoveryToken = disposeCts.Token; Channel ch = Channel.CreateBounded(64); ConcurrentDictionary writtenNodes = new(); int duplicated = 0; @@ -36,7 +38,7 @@ async Task DiscoverAsync(PublicKey target) bool anyFound = false; int count = 0; - await foreach (Node node in lookup.Lookup(target, token)) + await foreach (Node node in lookup.Lookup(target, discoveryToken)) { if (!discv4Adapter.GetSession(node).HasReceivedPong) { @@ -47,7 +49,7 @@ async Task DiscoverAsync(PublicKey target) } try { - await discv4Adapter.Ping(node, token); + await discv4Adapter.Ping(node, discoveryToken); } catch (OperationCanceledException) { @@ -63,7 +65,7 @@ async Task DiscoverAsync(PublicKey target) duplicated++; continue; } - await ch.Writer.WriteAsync(node, token); + await ch.Writer.WriteAsync(node, discoveryToken); } if (!anyFound) @@ -80,7 +82,7 @@ async Task DiscoverAsync(PublicKey target) { Random random = new(); byte[] randomBytes = new byte[64]; - while (!token.IsCancellationRequested) + while (!discoveryToken.IsCancellationRequested) { Stopwatch iterationTime = Stopwatch.StartNew(); @@ -92,7 +94,7 @@ async Task DiscoverAsync(PublicKey target) // Prevent high CPU when all node is not reachable due to network connectivity issue. if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) { - await Task.Delay(TimeSpan.FromSeconds(1), token); + await Task.Delay(TimeSpan.FromSeconds(1), discoveryToken); } } catch (OperationCanceledException) @@ -118,8 +120,15 @@ async Task DiscoverAsync(PublicKey target) finally { kademlia.OnNodeAdded -= Handler; + await disposeCts.CancelAsync(); ch.Writer.TryComplete(); - await discoverTask; + try + { + await discoverTask; + } + catch (OperationCanceledException) when (discoveryToken.IsCancellationRequested) + { + } } yield break; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs index 1b0556dd9153..13d9fd2e6066 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Crypto; -using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Kademlia; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv4; @@ -11,7 +11,7 @@ public class PublicKeyKeyOperator : IKeyOperator { public PublicKey GetKey(Node node) => node.Id; - public ValueHash256 GetKeyHash(PublicKey key) => key.Hash; + public KademliaHash GetKeyHash(PublicKey key) => KademliaHash.FromBytes(key.Hash.Bytes); /// /// Creates a random discv4 lookup target. @@ -21,7 +21,7 @@ public class PublicKeyKeyOperator : IKeyOperator /// Constructing a public key whose Keccak hash lands in that prefix is not practical, so this uses a random /// 64-byte target and treats discv4 bucket refresh as best-effort sampling. /// - public PublicKey CreateRandomKeyAtDistance(ValueHash256 nodePrefix, int depth) + public PublicKey CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth) { Span randomBytes = new byte[64]; Random.Shared.NextBytes(randomBytes); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs new file mode 100644 index 000000000000..6da5b1f976c8 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5; + +/// +/// Specifies the protocol-specific Kademlia services used by discv5. +/// +public class DiscV5KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module +{ + protected override void Load(ContainerBuilder builder) => builder + .AddSingleton() + .AddSingleton() + .Bind, IDiscv5KademliaAdapter>() + .AddSingleton() + .AddSingleton() + .AddModule(new KademliaModule()) + .AddSingleton, PublicKeyKeyOperator>() + .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() + { + CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), + KSize = discoveryConfig.BucketSize, + Alpha = discoveryConfig.Concurrency, + Beta = discoveryConfig.BitsPerHop, + LookupFindNeighbourHardTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), + RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), + RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), + BootNodes = bootNodes + }); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index a08d87a9cd9a..264835d3f259 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -1,196 +1,211 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Autofac; using Autofac.Features.AttributeFilters; using DotNetty.Transport.Channels; -using Lantern.Discv5.Enr; -using Lantern.Discv5.Enr.Entries; -using Lantern.Discv5.Enr.Identity.V4; -using Lantern.Discv5.WireProtocol; -using Lantern.Discv5.WireProtocol.Connection; -using Lantern.Discv5.WireProtocol.Session; -using Lantern.Discv5.WireProtocol.Table; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using NBitcoin.Secp256k1; +using Nethermind.Config; using Nethermind.Core; -using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.ServiceStopper; using Nethermind.Crypto; using Nethermind.Db; +using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Enr; using Nethermind.Stats.Model; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading.Channels; -using ENR = Lantern.Discv5.Enr.Enr; [assembly: InternalsVisibleTo("Nethermind.Network.Discovery.Test")] namespace Nethermind.Network.Discovery.Discv5; -public sealed class DiscoveryV5App : IDiscoveryApp +public sealed class DiscoveryV5App : IDiscoveryApp, IAsyncDisposable { internal const int MaxPendingEnrsPerWalk = 4_096; internal const int MaxTrackedEnrsPerWalk = MaxPendingEnrsPerWalk * 2; - private readonly IDiscv5Protocol _discv5Protocol; - private readonly Logging.ILogger _logger; + + private readonly ILogger _logger; private readonly IDb _discoveryDb; private readonly IDb _legacyDiscoveryDb; private readonly ILogManager _logManager; - private readonly CancellationTokenSource _appShutdownSource = new(); - private DiscoveryV5Report? _discoveryReport; - private readonly IServiceProvider _serviceProvider; - private readonly SessionOptions _sessionOptions; - private readonly EnrFactory _enrFactory; private readonly bool _allowNonRoutableEnrs; - private readonly RateLimiter _outgoingMessageRateLimiter; + private readonly IKademliaNodeSource _kademliaNodeSource; + private readonly IDiscv5KademliaAdapter _discv5Adapter; + private readonly IKademlia _kademlia; + private readonly Func _discoveryHandlerFactory; + private readonly ILifetimeScope _discv5Services; + private readonly CancellationTokenSource _stopCts; + + private NettyDiscoveryV5Handler? _discoveryHandler; + private DiscoveryV5Report? _discoveryReport; + private Task? _runningTask; public DiscoveryV5App( + ILifetimeScope rootScope, [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, IIPResolver ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, [KeyFilter(DbNames.DiscoveryV5Nodes)] IDb discoveryDb, [KeyFilter(DbNames.DiscoveryNodes)] IDb legacyDiscoveryDb, - ILogManager logManager) + IProcessExitSource processExitSource, + ILogManager logManager, + Action? configureDiscv5Services = null) { _logger = logManager.GetClassLogger(); _discoveryDb = discoveryDb; _legacyDiscoveryDb = legacyDiscoveryDb; _logManager = logManager; _allowNonRoutableEnrs = ShouldAcceptNonRoutableEnrs(ipResolver.ExternalIp); - IdentityVerifierV4 identityVerifier = new(); + _stopCts = CancellationTokenSource.CreateLinkedTokenSource(processExitSource.Token); + + List bootNodes = CreateBootNodes(networkConfig, discoveryConfig); + ITimestamper timestamper = rootScope.ResolveOptional() ?? Timestamper.Default; - PrivateKey privateKey = nodeKey.Unprotect(); - _sessionOptions = new() + _discv5Services = rootScope.BeginLifetimeScope(builder => { - Signer = new IdentitySignerV4(privateKey.KeyBytes), - Verifier = identityVerifier, - SessionKeys = new SessionKeys(privateKey.KeyBytes), - }; + builder.RegisterInstance(discoveryConfig).As(); + builder.RegisterInstance(timestamper).As(); + builder + .AddModule(new DiscV5KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddSingleton(); - IServiceCollection services = new ServiceCollection() - .AddSingleton() - .AddSingleton(_sessionOptions.Verifier) - .AddSingleton(_sessionOptions.Signer); - - _enrFactory = new EnrFactory(new EnrEntryRegistry()); - - ENR[] bootstrapEnrs = [ - .. networkConfig.Bootnodes.Select(bn => bn.ToEnr(_sessionOptions.Verifier, _sessionOptions.Signer)), - .. discoveryConfig.UseDefaultDiscv5Bootnodes ? GetDefaultDiscv5Bootnodes().Select(ToEnr) : [], - .. LoadStoredEnrs(), - ]; - - EnrBuilder enrBuilder = new EnrBuilder() - .WithIdentityScheme(_sessionOptions.Verifier, _sessionOptions.Signer) - .WithEntry(EnrEntryKey.Id, new EntryId("v4")) - .WithEntry(EnrEntryKey.Ip, new EntryIp(ipResolver.ExternalIp)) - .WithEntry(EnrEntryKey.Secp256K1, new EntrySecp256K1(_sessionOptions.Signer.PublicKey)) - .WithEntry(EnrEntryKey.Tcp, new EntryTcp(networkConfig.P2PPort)) - .WithEntry(EnrEntryKey.Udp, new EntryUdp(networkConfig.DiscoveryPort)); - - IDiscv5ProtocolBuilder discv5Builder = new Discv5ProtocolBuilder(services) - .WithConnectionOptions(new ConnectionOptions - { - UdpPort = networkConfig.DiscoveryPort - }) - .WithSessionOptions(_sessionOptions) - .WithTableOptions(new TableOptions([.. bootstrapEnrs.Select(enr => enr.ToString())])) - .WithEnrBuilder(enrBuilder) - .WithTalkResponder(new TalkReqAndRespHandler()) - .WithLoggerFactory(new NethermindLoggerFactory(logManager, true, Microsoft.Extensions.Logging.LogLevel.Debug)) - .WithServices(s => - { - s.AddSingleton(logManager); - NettyDiscoveryV5Handler.Register(s); - }); + configureDiscv5Services?.Invoke(builder); + }); - _discv5Protocol = NetworkHelper.HandlePortTakenError(discv5Builder.Build, networkConfig.DiscoveryPort); + (_kademliaNodeSource, _discv5Adapter, _kademlia, _discoveryHandlerFactory) = _discv5Services.Resolve(); + } - _serviceProvider = discv5Builder.GetServiceProvider(); - _outgoingMessageRateLimiter = new RateLimiter(discoveryConfig.MaxOutgoingMessagePerSecond); + private record DiscV5Services( + IKademliaNodeSource NodeSource, + IDiscv5KademliaAdapter Discv5Adapter, + IKademlia Kademlia, + Func NettyDiscoveryHandlerFactory + ) + { } - private static string[] GetDefaultDiscv5Bootnodes() => - JsonSerializer.Deserialize(typeof(DiscoveryV5App).Assembly.GetManifestResourceStream("Nethermind.Network.Discovery.Discv5.discv5-bootnodes.json")!) ?? []; - private ENR ToEnr(string enrString) => _enrFactory.CreateFromString(enrString, _sessionOptions.Verifier!); + private List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig) + { + List bootNodes = []; + HashSet seen = []; - private ENR ToEnr(byte[] enrBytes) => _enrFactory.CreateFromBytes(enrBytes, _sessionOptions.Verifier!); + NetworkNode[] configuredBootnodes = networkConfig.Bootnodes; + for (int i = 0; i < configuredBootnodes.Length; i++) + { + AddBootNode(bootNodes, seen, configuredBootnodes[i]); + } + + if (discoveryConfig.UseDefaultDiscv5Bootnodes) + { + string[] defaultBootnodes = GetDefaultDiscv5Bootnodes(); + for (int i = 0; i < defaultBootnodes.Length; i++) + { + AddBootNode(bootNodes, seen, NodeRecord.FromEnrString(defaultBootnodes[i])); + } + } - private ENR ToEnr(Node node) => new EnrBuilder() - .WithIdentityScheme(_sessionOptions.Verifier!, _sessionOptions.Signer!) - .WithEntry(EnrEntryKey.Id, new EntryId("v4")) - .WithEntry(EnrEntryKey.Ip, new EntryIp(node.Address.Address)) - .WithEntry(EnrEntryKey.Secp256K1, new EntrySecp256K1(node.Id.PrefixedBytes)) - .WithEntry(EnrEntryKey.Tcp, new EntryTcp(node.Address.Port)) - .WithEntry(EnrEntryKey.Udp, new EntryUdp(node.Address.Port)) - .Build(); + List storedEnrs = LoadStoredEnrs(); + for (int i = 0; i < storedEnrs.Count; i++) + { + AddBootNode(bootNodes, seen, storedEnrs[i]); + } - internal bool TryGetNodeFromEnr(IEnr enr, [NotNullWhen(true)] out Node? node) + if (bootNodes.Count == 0 && _logger.IsWarn) + { + _logger.Warn("No discv5 bootnodes specified in configuration"); + } + + return bootNodes; + } + + private void AddBootNode(List bootNodes, HashSet seen, NetworkNode networkNode) { - static PublicKey? GetPublicKeyFromEnr(IEnr entry) + Node node = new(networkNode.NodeId, networkNode.Host, networkNode.Port); + if (networkNode.IsEnr) { - byte[] keyBytes = entry.GetEntry(EnrEntryKey.Secp256K1).Value; - return Context.Instance.TryCreatePubKey(keyBytes, out _, out ECPubKey? key) ? new PublicKey(key.ToBytes(false)) : null; + node.Enr = networkNode.ToString(); } - node = null; - if (!enr.HasKey(EnrEntryKey.Tcp)) + AddBootNode(bootNodes, seen, node); + } + + private void AddBootNode(List bootNodes, HashSet seen, NodeRecord nodeRecord) + { + if (TryGetNodeFromEnr(nodeRecord, out Node? node)) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, no TCP port."); - return false; + AddBootNode(bootNodes, seen, node); } - if (!enr.HasKey(EnrEntryKey.Ip)) + } + + private static void AddBootNode(List bootNodes, HashSet seen, Node node) + { + if (seen.Add(node.IdHash)) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, no IP."); - return false; + node.IsBootnode = true; + bootNodes.Add(node); } - if (!enr.HasKey(EnrEntryKey.Secp256K1)) + } + + private static string[] GetDefaultDiscv5Bootnodes() => + JsonSerializer.Deserialize(typeof(DiscoveryV5App).Assembly.GetManifestResourceStream("Nethermind.Network.Discovery.Discv5.discv5-bootnodes.json")!) ?? []; + + internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? node) + { + node = null; + + PublicKey? key = GetPublicKeyFromEnr(enr); + if (key is null) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, no signature."); + if (_logger.IsTrace) _logger.Trace("Enr declined, unable to extract public key."); return false; } - if (enr.HasKey(EnrEntryKey.Eth2)) + + IPAddress? ip = enr.GetObj(EnrContentKey.Ip); + if (ip is null) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, ETH2 detected."); + if (_logger.IsTrace) _logger.Trace("Enr declined, no IP."); return false; } - PublicKey? key = GetPublicKeyFromEnr(enr); - if (key is null) + int? discoveryPort = GetDiscoveryPort(enr); + if (discoveryPort is null) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, unable to extract public key."); + if (_logger.IsTrace) _logger.Trace("Enr declined, no discovery UDP port."); return false; } - IPAddress ip = enr.GetEntry(EnrEntryKey.Ip).Value; - int tcpPort = enr.GetEntry(EnrEntryKey.Tcp).Value; if (!IsDiscoveryAddressAcceptable(ip, _allowNonRoutableEnrs)) { if (_logger.IsTrace) _logger.Trace($"Enr declined, non-routable IP {ip}."); return false; } - if ((uint)tcpPort > ushort.MaxValue || tcpPort == 0) + if ((uint)discoveryPort.Value > ushort.MaxValue || discoveryPort.Value == 0) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, invalid TCP port {tcpPort}."); + if (_logger.IsTrace) _logger.Trace($"Enr declined, invalid discovery UDP port {discoveryPort.Value}."); return false; } - node = new(key, ip.ToString(), tcpPort) + node = new Node(key, ip.ToString(), discoveryPort.Value) { - Enr = enr.ToString() + Enr = enr.EnrString }; return true; } + private static int? GetDiscoveryPort(NodeRecord enr) => + enr.GetValue(EnrContentKey.Udp) ?? enr.GetValue(EnrContentKey.Tcp); + + private static PublicKey? GetPublicKeyFromEnr(NodeRecord enr) => + enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); + internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allowNonRoutable) { if (IPAddress.Any.Equals(ipAddress) || IPAddress.IPv6Any.Equals(ipAddress) || IPAddress.Broadcast.Equals(ipAddress)) @@ -198,7 +213,7 @@ internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allo return false; } - if (ipAddress.IsIPv6Multicast || IsIPv4Multicast(ipAddress)) + if (ipAddress.IsIPv6Multicast || NodeFilter.IsIPv4Multicast(ipAddress)) { return false; } @@ -209,15 +224,12 @@ internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allo internal static bool IsDiscoveryAddressRoutable(IPAddress ipAddress) => IsDiscoveryAddressAcceptable(ipAddress, allowNonRoutable: false); - private static bool IsIPv4Multicast(IPAddress ipAddress) - => NodeFilter.IsIPv4Multicast(ipAddress); - private static bool ShouldAcceptNonRoutableEnrs(IPAddress externalIp) => !IPAddress.Any.Equals(externalIp) && !IPAddress.None.Equals(externalIp) && NodeFilter.IsLoopbackOrPrivateOrLinkLocal(externalIp); - internal static bool TryEnqueueNewEnr(Queue nodesToCheck, HashSet seenNodes, IEnr enr) + internal static bool TryEnqueueNewEnr(Queue nodesToCheck, HashSet seenNodes, NodeRecord enr) { if (seenNodes.Count >= MaxTrackedEnrsPerWalk || nodesToCheck.Count >= MaxPendingEnrsPerWalk || !seenNodes.Add(enr)) { @@ -228,9 +240,16 @@ internal static bool TryEnqueueNewEnr(Queue nodesToCheck, HashSet se return true; } - internal List LoadStoredEnrs() + internal List LoadStoredEnrs() { - List enrs = [.. _discoveryDb.GetAllValues().Select(ToEnr)]; + List enrs = []; + foreach (byte[] enrBytes in _discoveryDb.GetAllValues()) + { + if (TryLoadStoredEnr(enrBytes, out NodeRecord? enr)) + { + enrs.Add(enr); + } + } if (enrs.Count is not 0) { @@ -249,24 +268,23 @@ internal List LoadStoredEnrs() continue; } - try + if (!TryLoadStoredEnr(kv.Value, out NodeRecord? enr)) { - ENR enr = ToEnr(kv.Value); - - if (enrs.Count is 0) - { - migrateBatch = _discoveryDb.StartWriteBatch(); - deleteBatch = _legacyDiscoveryDb.StartWriteBatch(); - } + continue; + } - enrs.Add(enr); - migrateBatch![enr.NodeId] = kv.Value; - deleteBatch![kv.Key] = null; + if (enrs.Count is 0) + { + migrateBatch = _discoveryDb.StartWriteBatch(); + deleteBatch = _legacyDiscoveryDb.StartWriteBatch(); } - catch + + enrs.Add(enr); + PublicKey? publicKey = GetPublicKeyFromEnr(enr); + if (publicKey is not null) { - // The database has enodes only - return []; + migrateBatch![publicKey.Hash.Bytes] = kv.Value; + deleteBatch![kv.Key] = null; } } } @@ -279,184 +297,125 @@ internal List LoadStoredEnrs() return enrs; } + private bool TryLoadStoredEnr(byte[] enrBytes, [NotNullWhen(true)] out NodeRecord? enr) + { + try + { + enr = NodeRecord.FromBytes(enrBytes); + return true; + } + catch (Exception e) + { + enr = null; + if (_logger.IsDebug) _logger.Debug($"Skipping stored discv5 ENR that cannot be decoded: {e}"); + return false; + } + } + public event EventHandler? NodeRemoved { add { } remove { } } public void InitializeChannel(IChannel channel) { - NettyDiscoveryV5Handler handler = _serviceProvider.GetRequiredService(); - handler.InitializeChannel(channel); - channel.Pipeline.AddLast(handler); + _discoveryHandler = _discoveryHandlerFactory(); + _discoveryHandler.InitializeChannel(channel); + channel.Pipeline.AddLast(_discoveryHandler); } - public async Task StartAsync() + public Task StartAsync() { - await _discv5Protocol.InitAsync(); - - if (_logger.IsDebug) _logger.Debug($"Initially discovered {_discv5Protocol.GetActiveNodes.Count()} active peers, {_discv5Protocol.GetAllNodes.Count()} in total."); - - _discoveryReport = new DiscoveryV5Report(_discv5Protocol, _logManager, _appShutdownSource.Token); + _discoveryReport = new DiscoveryV5Report(_kademlia, _logManager, _stopCts.Token); + _runningTask = Task.Factory.StartNew(static state => ((DiscoveryV5App)state!).ActivateAsync(), this, _stopCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); + return Task.CompletedTask; } - public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) + private async Task ActivateAsync() { - Channel discoveredNodesChannel = Channel.CreateBounded(1); - - async Task DiscoverAsync(IEnumerable startingNode, ArrayPoolSpan nodeId, bool disposeNodeId = true) + try { - try - { - static int[] GetDistances(byte[] srcNodeId, in ArrayPoolSpan destNodeId) - { - const int WiderDistanceRange = 3; - - int[] distances = new int[WiderDistanceRange]; - distances[0] = TableUtility.Log2Distance(srcNodeId, destNodeId); - - for (int n = 1, i = 1; n < WiderDistanceRange; i++) - { - if (distances[0] - i > 0) - { - distances[n++] = distances[0] - i; - } - if (distances[0] + i <= 256) - { - distances[n++] = distances[0] + i; - } - } - - return distances; - } - - Queue nodesToCheck = new(startingNode); - HashSet seenNodes = [.. startingNode]; - HashSet checkedNodes = []; - - while (!token.IsCancellationRequested) - { - if (!nodesToCheck.TryDequeue(out IEnr? newEntry)) - { - return; - } - - if (TryGetNodeFromEnr(newEntry, out Node? node2)) - { - await discoveredNodesChannel.Writer.WriteAsync(node2!, token); - - if (_logger.IsDebug) _logger.Debug($"A node discovered via discv5: {newEntry} = {node2}."); - - _discoveryReport?.NodeFound(); - } - - if (!checkedNodes.Add(newEntry)) - { - continue; - } - - await _outgoingMessageRateLimiter.WaitAsync(token); - foreach (IEnr newEnr in await _discv5Protocol.SendFindNodeAsync(newEntry, GetDistances(newEntry.NodeId, in nodeId)) ?? []) - { - TryEnqueueNewEnr(nodesToCheck, seenNodes, newEnr); - } - } - } - finally - { - if (disposeNodeId) - { - nodeId.Dispose(); - } - } + await Task.WhenAll(_discv5Adapter.RunAsync(_stopCts.Token), _kademlia.Run(_stopCts.Token)); } - - IEnumerable GetStartingNodes() => _discv5Protocol.GetAllNodes; - Random random = new(); - - const int RandomNodesToLookupCount = 3; - - Task discoverTask = Task.Run(async () => + catch (OperationCanceledException) { - using ArrayPoolSpan selfNodeId = new(32); - _discv5Protocol.SelfEnr.NodeId.CopyTo(selfNodeId); - - while (!token.IsCancellationRequested) - { - try - { - using ArrayPoolList discoverTasks = new(RandomNodesToLookupCount); - - discoverTasks.Add(DiscoverAsync(GetStartingNodes(), selfNodeId, false)); + if (_logger.IsInfo) _logger.Info("Discovery V5 App stopped"); + } + catch (Exception e) + { + _logger.DebugError("Error during discovery v5 initialization", e); + } + } - for (int i = 0; i < RandomNodesToLookupCount; i++) - { - ArrayPoolSpan randomNodeId = new(32); - random.NextBytes(randomNodeId); - discoverTasks.Add(DiscoverAsync(GetStartingNodes(), randomNodeId)); - } + public IAsyncEnumerable DiscoverNodes(CancellationToken token) => _kademliaNodeSource.DiscoverNodes(token); - await Task.WhenAll(discoverTasks); - await Task.Delay(TimeSpan.FromSeconds(2), token); - } - catch (OperationCanceledException) - { - if (_logger.IsTrace) _logger.Trace($"Discovery has been stopped."); - } - catch (Exception ex) - { - if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); - } - } - }, token); + public async Task StopAsync() + { + try + { + await _stopCts.CancelAsync(); + } + catch (ObjectDisposedException) + { + } try { - await foreach (Node node in discoveredNodesChannel.Reader.ReadAllAsync(token)) + if (_runningTask is not null) { - yield return node; + await _runningTask; } } - finally + catch (OperationCanceledException) { - await discoverTask; } + catch (Exception e) + { + if (_logger.IsError) _logger.Error("Error in discovery v5 task", e); + } + + PersistKnownEnrs(); + + await _discv5Adapter.DisposeAsync(); + _discoveryHandler?.Close(); + _stopCts.Dispose(); } - public async Task StopAsync() + private void PersistKnownEnrs() { - IEnumerable activeNodeEnrs = _discv5Protocol.GetAllNodes; _discoveryDb.Clear(); IWriteBatch? batch = null; - try { - foreach (IEnr enr in activeNodeEnrs) + foreach (Node node in _kademlia.IterateNodes()) { + if (string.IsNullOrEmpty(node.Enr)) + { + continue; + } + + NodeRecord enr; + try + { + enr = NodeRecord.FromEnrString(node.Enr); + } + catch (Exception e) + { + if (_logger.IsDebug) _logger.Debug($"Skipping malformed discv5 ENR while persisting {node}: {e}"); + continue; + } + batch ??= _discoveryDb.StartWriteBatch(); - batch[enr.NodeId] = enr.EncodeRecord(); + batch[node.IdHash.Bytes] = enr.ToRlpBytes(); } } finally { batch?.Dispose(); } - - try - { - await _discv5Protocol.StopAsync(); - } - catch (Exception ex) - { - if (_logger.IsWarn) _logger.Warn($"Error when attempting to stop discv5: {ex}"); - } - - await _appShutdownSource.CancelAsync(); } string IStoppableService.Description => "discv5"; - public void AddNodeToDiscovery(Node node) - { - IRoutingTable routingTable = _serviceProvider.GetRequiredService(); - routingTable.UpdateFromEnr(ToEnr(node)); - } + public void AddNodeToDiscovery(Node node) => _kademlia.AddOrRefresh(node); + + public ValueTask DisposeAsync() => _discv5Services.DisposeAsync(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs index ed9da22a9d58..3ddfc1184842 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Lantern.Discv5.WireProtocol; +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; using Nethermind.Logging; +using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv5; @@ -11,7 +13,7 @@ internal class DiscoveryV5Report int RecentlyChecked = 0; int TotalChecked = 0; - public DiscoveryV5Report(IDiscv5Protocol discv5Protocol, ILogManager logManager, CancellationToken token) + public DiscoveryV5Report(IKademlia kademlia, ILogManager logManager, CancellationToken token) { ILogger logger = logManager.GetClassLogger(); if (!logger.IsDebug) @@ -23,7 +25,7 @@ public DiscoveryV5Report(IDiscv5Protocol discv5Protocol, ILogManager logManager, { while (!token.IsCancellationRequested) { - logger.Debug($"Nodes checked: {Interlocked.Exchange(ref RecentlyChecked, 0)}, in total {TotalChecked}. Kademlia table state: {discv5Protocol.GetActiveNodes.Count()} active nodes, {discv5Protocol.GetAllNodes.Count()} all nodes."); + logger.Debug($"Nodes checked: {Interlocked.Exchange(ref RecentlyChecked, 0)}, in total {TotalChecked}. Kademlia table state: {kademlia.IterateNodes().Count()} nodes."); await Task.Delay(10_000, token); } }, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs new file mode 100644 index 000000000000..bddf47a423a5 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -0,0 +1,699 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Crypto; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Enr; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5; + +/// +/// Maps discv5 FINDNODE distance requests onto the protocol-specific Kademlia table. +/// +public class Discv5KademliaAdapter( + Lazy> kademlia, + NettyDiscoveryV5Handler discoveryHandler, + Discv5PacketCodec packetCodec, + INodeRecordProvider nodeRecordProvider, + IDiscoveryConfig discoveryConfig, + ICryptoRandom cryptoRandom, + ILogManager logManager) : IDiscv5KademliaAdapter +{ + private const int MaxFindNodeRecords = 16; + private const int MaxEnrsPerNodesMessage = 3; + private const int MaxSessions = 4_096; + private const int MaxSentChallenges = 4_096; + private const int MaxPendingRequests = 4_096; + private const int MaxResponseHandlers = 1_024; + private const int MaxKnownRecords = 16_384; + private const int MaxNodesResponseMessages = 16; + private const int MaxNodesResponseRecords = 64; + private const long SentChallengeTtlMilliseconds = 60_000; + + private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); + private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly ConcurrentDictionary _sessions = new(); + private readonly ConcurrentQueue _sessionKeys = new(); + private readonly ConcurrentDictionary _sentChallenges = new(); + private readonly ConcurrentQueue _sentChallengeKeys = new(); + private long _lastSentChallengeTrimMilliseconds; + private readonly ConcurrentDictionary _pendingByNonce = new(); + private readonly ConcurrentQueue _pendingNonceKeys = new(); + private readonly ConcurrentDictionary _responseHandlers = new(); + private readonly ConcurrentQueue _responseHandlerKeys = new(); + private readonly ConcurrentDictionary _knownRecords = new(); + private readonly ConcurrentQueue _knownRecordKeys = new(); + + /// + public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = null) + { + ArgumentNullException.ThrowIfNull(distances); + + HashSet seen = []; + List result = []; + Hash256? excludedHash = excluding?.IdHash; + + foreach (int distance in distances) + { + if (distance < 0 || distance > Hash256XorUtils.MaxDistance) + { + throw new ArgumentOutOfRangeException(nameof(distances), distance, $"Distance must be between 0 and {Hash256XorUtils.MaxDistance}."); + } + + Node[] nodes = kademlia.Value.GetAllAtDistance(distance); + for (int i = 0; i < nodes.Length; i++) + { + Node node = nodes[i]; + if (excludedHash is not null && node.IdHash.Equals(excludedHash)) + { + continue; + } + + if (seen.Add(node.IdHash)) + { + result.Add(node); + } + } + } + + return [.. result]; + } + + /// + public async Task Ping(Node receiver, CancellationToken token) + { + RegisterKnownRecord(receiver); + byte[] requestId = CreateRequestId(); + Discv5Ping ping = new(requestId, nodeRecordProvider.Current.EnrSequence); + PongResponseHandler responseHandler = new(receiver); + + await SendRequest(receiver, ping, Discv5MessageType.Pong, responseHandler, _pingTimeout, token); + kademlia.Value.AddOrRefresh(receiver); + } + + /// + public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) + { + RegisterKnownRecord(receiver); + int[] distances = GetLookupDistances(receiver, target); + byte[] requestId = CreateRequestId(); + Discv5FindNode findNode = new(requestId, distances); + NodesResponseHandler responseHandler = new(receiver, distances); + + await SendRequest(receiver, findNode, Discv5MessageType.Nodes, responseHandler, _findNodeTimeout, token); + Node[] nodes = responseHandler.GetNodes(); + for (int i = 0; i < nodes.Length; i++) + { + kademlia.Value.AddOrRefresh(nodes[i]); + } + + return nodes; + } + + public async Task RunAsync(CancellationToken token) + { + try + { + await foreach (UdpReceiveResult result in discoveryHandler.ReadMessagesAsync(token)) + { + await HandlePacket(result, token); + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error("Error in discv5 packet loop", e); + } + } + + /// + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + private async Task SendRequest( + Node receiver, + Discv5Message request, + Discv5MessageType responseType, + IResponseHandler responseHandler, + TimeSpan timeout, + CancellationToken token) + { + ResponseKey responseKey = new(receiver.Id.Hash, RequestIdToString(request.RequestId), responseType); + SetBounded(_responseHandlers, _responseHandlerKeys, responseKey, responseHandler, MaxResponseHandlers); + + using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); + timeoutCts.CancelAfter(timeout); + + PendingNonceKey? pendingNonceKey = null; + try + { + pendingNonceKey = await SendMessage(receiver, request); + await responseHandler.Task.WaitAsync(timeoutCts.Token); + } + catch (OperationCanceledException) + { + throw; + } + finally + { + _responseHandlers.TryRemove(responseKey, out _); + if (pendingNonceKey is not null) + { + _pendingByNonce.TryRemove(pendingNonceKey.Value, out _); + } + } + } + + private async Task SendMessage(Node receiver, Discv5Message message) + { + SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); + if (TryGetSession(sessionKey, out Discv5Session? session)) + { + byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, session.GetNextNonce(cryptoRandom)); + await discoveryHandler.SendAsync(packet, receiver.Address); + return null; + } + + byte[] nonce = cryptoRandom.GenerateRandomBytes(Discv5PacketCodec.NonceSize); + byte[] encryptionKey = cryptoRandom.GenerateRandomBytes(16); + PendingRequest pendingRequest = new(receiver, message); + PendingNonceKey pendingNonceKey = new(receiver.Address, NonceToString(nonce)); + SetBounded(_pendingByNonce, _pendingNonceKeys, pendingNonceKey, pendingRequest, MaxPendingRequests); + + byte[] initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); + try + { + await discoveryHandler.SendAsync(initialPacket, receiver.Address); + return pendingNonceKey; + } + catch + { + _pendingByNonce.TryRemove(pendingNonceKey, out _); + throw; + } + } + + private async Task SendResponse(Node receiver, Discv5Message message, CancellationToken token) + { + SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); + if (!TryGetSession(sessionKey, out Discv5Session? session)) + { + return; + } + + byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, session.GetNextNonce(cryptoRandom)); + await discoveryHandler.SendAsync(packet, receiver.Address); + } + + private async Task HandlePacket(UdpReceiveResult udpPacket, CancellationToken token) + { + if (!packetCodec.TryDecode(udpPacket.Buffer, out Discv5Packet packet)) + { + return; + } + + try + { + switch (packet.Flag) + { + case Discv5PacketFlag.WhoAreYou: + await HandleWhoAreYou(udpPacket.RemoteEndPoint, packet, token); + break; + case Discv5PacketFlag.Ordinary: + await HandleOrdinary(udpPacket.RemoteEndPoint, packet, token); + break; + case Discv5PacketFlag.Handshake: + await HandleHandshake(udpPacket.RemoteEndPoint, packet, token); + break; + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + } + catch (Exception e) + { + if (_logger.IsDebug) _logger.Debug($"Error handling discv5 packet from {udpPacket.RemoteEndPoint}: {e}"); + } + } + + private async Task HandleWhoAreYou(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) + { + PendingNonceKey pendingNonceKey = new(endpoint, NonceToString(packet.Nonce)); + if (!_pendingByNonce.TryRemove(pendingNonceKey, out PendingRequest? pendingRequest)) + { + return; + } + + Discv5Challenge challenge = packetCodec.DecodeWhoAreYou(packet); + byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Discv5Session session); + SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash, endpoint), session); + await discoveryHandler.SendAsync(handshakePacket, endpoint); + } + + private async Task HandleOrdinary(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) + { + if (!Discv5PacketCodec.TryGetSourceNodeId(packet, out byte[] sourceNodeId)) + { + return; + } + + Hash256 nodeId = new(sourceNodeId); + SessionKey sessionKey = new(nodeId, endpoint); + if (!TryGetSession(sessionKey, out Discv5Session? session) || + !packetCodec.TryDecryptMessage(packet, session.ReadKey, out Discv5Message message)) + { + await SendWhoAreYou(endpoint, packet, sourceNodeId); + return; + } + + await HandleMessage(session.RemotePublicKey, endpoint, message, token); + } + + private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) + { + if (!Discv5PacketCodec.TryGetSourceNodeId(packet, out byte[] sourceNodeId)) + { + return; + } + + Hash256 nodeId = new(sourceNodeId); + ChallengeKey challengeKey = new(nodeId, endpoint); + if (!_sentChallenges.TryRemove(challengeKey, out SentChallenge sentChallenge) || + IsExpired(sentChallenge, Environment.TickCount64)) + { + return; + } + + TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); + if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Discv5Session session, out Discv5Message message, out NodeRecord? nodeRecord)) + { + return; + } + + if (nodeRecord is not null) + { + SetKnownRecord(nodeId, nodeRecord); + } + + SetSession(new SessionKey(nodeId, endpoint), session); + await HandleMessage(session.RemotePublicKey, endpoint, message, token, nodeRecord ?? knownRecord); + } + + private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket, byte[] destinationNodeId) + { + Hash256 nodeId = new(destinationNodeId); + ulong enrSequence = TryGetKnownRecord(nodeId, out NodeRecord? record) ? record.EnrSequence : 0UL; + byte[] packet = packetCodec.EncodeWhoAreYou(destinationNodeId, requestPacket.Nonce, enrSequence, out Discv5Challenge challenge); + SetSentChallenge(new ChallengeKey(nodeId, endpoint), challenge); + await discoveryHandler.SendAsync(packet, endpoint); + } + + private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, Discv5Message message, CancellationToken token, NodeRecord? nodeRecord = null) + { + Node remoteNode = new(remotePublicKey, endpoint) + { + Enr = GetKnownEnr(remotePublicKey.Hash, nodeRecord) + }; + if (HandleResponse(remotePublicKey.Hash, message)) + { + kademlia.Value.AddOrRefresh(remoteNode); + return; + } + + switch (message) + { + case Discv5Ping ping: + await SendResponse( + remoteNode, + new Discv5Pong(ping.RequestId, nodeRecordProvider.Current.EnrSequence, endpoint.Address, endpoint.Port), + token); + kademlia.Value.AddOrRefresh(remoteNode); + break; + case Discv5FindNode findNode: + await HandleFindNode(remoteNode, findNode, token); + kademlia.Value.AddOrRefresh(remoteNode); + break; + case Discv5TalkReq talkReq: + await SendResponse(remoteNode, new Discv5TalkResp(talkReq.RequestId, []), token); + break; + } + } + + private string? GetKnownEnr(Hash256 nodeId, NodeRecord? nodeRecord) + { + if (nodeRecord is not null) + { + return nodeRecord.EnrString; + } + + return _knownRecords.TryGetValue(nodeId, out NodeRecord? knownRecord) ? knownRecord.EnrString : null; + } + + private bool HandleResponse(Hash256 nodeId, Discv5Message message) + { + ResponseKey responseKey = new(nodeId, RequestIdToString(message.RequestId), message.MessageType); + return _responseHandlers.TryGetValue(responseKey, out IResponseHandler? handler) && handler.Handle(message); + } + + private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, CancellationToken token) + { + NodeRecord[] records = GetFindNodeRecords(findNode.Distances, remoteNode); + if (records.Length == 0) + { + await SendResponse(remoteNode, new Discv5Nodes(findNode.RequestId, 1, []), token); + return; + } + + int total = (records.Length + MaxEnrsPerNodesMessage - 1) / MaxEnrsPerNodesMessage; + for (int i = 0; i < records.Length; i += MaxEnrsPerNodesMessage) + { + int count = Math.Min(MaxEnrsPerNodesMessage, records.Length - i); + NodeRecord[] chunk = records.AsSpan(i, count).ToArray(); + await SendResponse(remoteNode, new Discv5Nodes(findNode.RequestId, total, chunk), token); + } + } + + private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) + { + HashSet seen = []; + List result = []; + bool includedSelf = false; + for (int i = 0; i < distances.Length && result.Count < MaxFindNodeRecords; i++) + { + int distance = distances[i]; + if (distance < 0 || distance > Hash256XorUtils.MaxDistance) + { + continue; + } + + if (distance == 0) + { + if (!includedSelf) + { + result.Add(nodeRecordProvider.Current); + includedSelf = true; + } + + continue; + } + + Node[] nodes = GetNodesAtDistances([distance], requester); + for (int j = 0; j < nodes.Length && result.Count < MaxFindNodeRecords; j++) + { + Node node = nodes[j]; + if (string.IsNullOrEmpty(node.Enr) || !seen.Add(node.Id.Hash)) + { + continue; + } + + NodeRecord? record = GetFindNodeRecord(node); + if (record is not null) + { + result.Add(record); + } + } + } + + return [.. result]; + } + + private NodeRecord? GetFindNodeRecord(Node node) + { + if (TryGetKnownRecord(node.Id.Hash, out NodeRecord? knownRecord)) + { + return knownRecord; + } + + try + { + return NodeRecord.FromEnrString(node.Enr); + } + catch (Exception e) + { + if (_logger.IsTrace) _logger.Trace($"Unable to parse discv5 FINDNODE ENR for {node}: {e}"); + return null; + } + } + + private void RegisterKnownRecord(Node node) + { + if (string.IsNullOrEmpty(node.Enr)) + { + return; + } + + try + { + SetKnownRecord(node.Id.Hash, NodeRecord.FromEnrString(node.Enr)); + } + catch (Exception e) + { + if (_logger.IsTrace) _logger.Trace($"Unable to parse known discv5 ENR for {node}: {e}"); + } + } + + private int[] GetLookupDistances(Node receiver, PublicKey target) + { + KademliaHash receiverHash = KademliaHash.FromBytes(receiver.Id.Hash.Bytes); + KademliaHash targetHash = KademliaHash.FromBytes(target.Hash.Bytes); + int distance = Hash256XorUtils.CalculateLogDistance(receiverHash, targetHash); + + List distances = [distance]; + if (distance > 0) + { + distances.Add(distance - 1); + } + + if (distance < Hash256XorUtils.MaxDistance) + { + distances.Add(distance + 1); + } + + return [.. distances]; + } + + private byte[] CreateRequestId() + { + byte[] requestId = cryptoRandom.GenerateRandomBytes(sizeof(ulong)); + return requestId.AsSpan().WithoutLeadingZeros().ToArray(); + } + + private static string RequestIdToString(byte[] requestId) => Convert.ToHexString(requestId); + + private static string NonceToString(byte[] nonce) => Convert.ToHexString(nonce); + + private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGetValue(sessionKey, out session); + + private void SetSession(SessionKey sessionKey, Discv5Session session) + => SetBounded(_sessions, _sessionKeys, sessionKey, session, MaxSessions); + + private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGetValue(nodeId, out record); + + private void SetKnownRecord(Hash256 nodeId, NodeRecord record) + => SetBounded(_knownRecords, _knownRecordKeys, nodeId, record, MaxKnownRecords); + + private void SetSentChallenge(ChallengeKey challengeKey, Discv5Challenge challenge) + { + long now = Environment.TickCount64; + TryTrimExpiredChallenges(now); + SetBounded(_sentChallenges, _sentChallengeKeys, challengeKey, new SentChallenge(challenge, now), MaxSentChallenges); + } + + private void TryTrimExpiredChallenges(long now) + { + long lastTrim = Volatile.Read(ref _lastSentChallengeTrimMilliseconds); + if (now - lastTrim <= SentChallengeTtlMilliseconds || + Interlocked.CompareExchange(ref _lastSentChallengeTrimMilliseconds, now, lastTrim) != lastTrim) + { + return; + } + + TrimExpiredChallenges(now); + } + + private void TrimExpiredChallenges(long now) + { + foreach (KeyValuePair kv in _sentChallenges) + { + if (IsExpired(kv.Value, now)) + { + _sentChallenges.TryRemove(kv.Key, out _); + } + } + } + + private static bool IsExpired(SentChallenge challenge, long now) + => now - challenge.CreatedAtMilliseconds > SentChallengeTtlMilliseconds; + + private static void SetBounded( + ConcurrentDictionary dictionary, + ConcurrentQueue insertionOrder, + TKey key, + TValue value, + int maxCount) + where TKey : notnull + { + if (dictionary.TryAdd(key, value)) + { + insertionOrder.Enqueue(key); + } + else + { + dictionary[key] = value; + } + + TrimBounded(dictionary, insertionOrder, maxCount); + } + + private static void TrimBounded( + ConcurrentDictionary dictionary, + ConcurrentQueue insertionOrder, + int maxCount) + where TKey : notnull + { + while (dictionary.Count > maxCount && insertionOrder.TryDequeue(out TKey? key)) + { + dictionary.TryRemove(key, out _); + } + + if (dictionary.Count <= maxCount) + { + return; + } + + foreach (KeyValuePair kv in dictionary) + { + if (dictionary.Count <= maxCount) + { + return; + } + + dictionary.TryRemove(kv.Key, out _); + } + } + + private readonly record struct SessionKey(Hash256 NodeId, IPEndPoint Endpoint); + + private readonly record struct ChallengeKey(Hash256 NodeId, IPEndPoint Endpoint); + + private readonly record struct PendingNonceKey(IPEndPoint Endpoint, string Nonce); + + private readonly record struct ResponseKey(Hash256 NodeId, string RequestId, Discv5MessageType MessageType); + + private sealed record PendingRequest(Node Receiver, Discv5Message Message); + + private readonly record struct SentChallenge(Discv5Challenge Challenge, long CreatedAtMilliseconds); + + private interface IResponseHandler + { + Task Task { get; } + + bool Handle(Discv5Message message); + } + + private sealed class PongResponseHandler(Node receiver) : IResponseHandler + { + private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task Task => _completion.Task; + + public bool Handle(Discv5Message message) + { + if (message is not Discv5Pong pong) + { + return false; + } + + receiver.ValidatedProtocol = true; + _completion.TrySetResult(); + return true; + } + } + + private sealed class NodesResponseHandler(Node receiver, int[] requestedDistances) : IResponseHandler + { + private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly List _nodes = []; + private readonly HashSet _seenNodeIds = []; + private int? _total; + private int _received; + + public Task Task => _completion.Task; + + public bool Handle(Discv5Message message) + { + if (message is not Discv5Nodes nodes) + { + return false; + } + + if (_completion.Task.IsCompleted) + { + return true; + } + + if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) + { + _completion.TrySetResult(); + return true; + } + + if (_total is not null && _total.Value != nodes.Total) + { + _completion.TrySetResult(); + return true; + } + + _total ??= nodes.Total; + _received++; + + for (int i = 0; i < nodes.Records.Length && _nodes.Count < MaxNodesResponseRecords; i++) + { + NodeRecord record = nodes.Records[i]; + if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable: true, out Node? node) || + !_seenNodeIds.Add(node.Id.Hash) || + !MatchesRequestedDistance(node, requestedDistances)) + { + continue; + } + + _nodes.Add(node); + } + + if (_received >= _total || _nodes.Count >= MaxNodesResponseRecords) + { + _completion.TrySetResult(); + } + + return true; + } + + public Node[] GetNodes() => [.. _nodes]; + + private bool MatchesRequestedDistance(Node node, int[] requestedDistances) + { + KademliaHash receiverHash = KademliaHash.FromBytes(receiver.Id.Hash.Bytes); + KademliaHash nodeHash = KademliaHash.FromBytes(node.Id.Hash.Bytes); + int distance = Hash256XorUtils.CalculateLogDistance(receiverHash, nodeHash); + for (int i = 0; i < requestedDistances.Length; i++) + { + if (requestedDistances[i] == distance) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs new file mode 100644 index 000000000000..09b1c7193c70 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5; + +internal static class Discv5MessageCodec +{ + private const int MaxRequestIdLength = 8; + + public static byte[] Encode(Discv5Message message) + { + Rlp data = message switch + { + Discv5Ping ping => Rlp.Encode(Rlp.Encode(ping.RequestId), Rlp.Encode(ping.EnrSequence)), + Discv5Pong pong => Rlp.Encode( + Rlp.Encode(pong.RequestId), + Rlp.Encode(pong.EnrSequence), + Rlp.Encode(pong.RecipientIp.GetAddressBytes()), + Rlp.Encode(pong.RecipientPort)), + Discv5FindNode findNode => Rlp.Encode(Rlp.Encode(findNode.RequestId), Rlp.Encode(findNode.Distances)), + Discv5Nodes nodes => Rlp.Encode( + Rlp.Encode(nodes.RequestId), + Rlp.Encode(nodes.Total), + EncodeNodeRecords(nodes.Records)), + Discv5TalkReq talkReq => Rlp.Encode( + Rlp.Encode(talkReq.RequestId), + Rlp.Encode(talkReq.Protocol), + Rlp.Encode(talkReq.Request)), + Discv5TalkResp talkResp => Rlp.Encode(Rlp.Encode(talkResp.RequestId), Rlp.Encode(talkResp.Response)), + _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") + }; + + byte[] result = new byte[data.Length + 1]; + result[0] = (byte)message.MessageType; + data.Bytes.CopyTo(result.AsSpan(1)); + return result; + } + + public static Discv5Message Decode(ReadOnlySpan message) + { + if (message.IsEmpty) + { + throw new RlpException("Empty discv5 message."); + } + + Discv5MessageType messageType = (Discv5MessageType)message[0]; + Rlp.ValueDecoderContext ctx = new(message[1..]); + int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + + byte[] requestId = DecodeRequestId(ref ctx); + Discv5Message decoded = messageType switch + { + Discv5MessageType.Ping => new Discv5Ping(requestId, ctx.DecodeULong()), + Discv5MessageType.Pong => DecodePong(requestId, ref ctx), + Discv5MessageType.FindNode => new Discv5FindNode(requestId, DecodeDistances(ref ctx)), + Discv5MessageType.Nodes => DecodeNodes(requestId, ref ctx), + Discv5MessageType.TalkReq => new Discv5TalkReq(requestId, ctx.DecodeByteArray(), ctx.DecodeByteArray()), + Discv5MessageType.TalkResp => new Discv5TalkResp(requestId, ctx.DecodeByteArray()), + _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") + }; + + ctx.Check(checkPosition); + ctx.CheckEnd(); + return decoded; + } + + private static Rlp EncodeNodeRecords(NodeRecord[] records) + { + Rlp[] encodedRecords = new Rlp[records.Length]; + for (int i = 0; i < records.Length; i++) + { + encodedRecords[i] = new Rlp(records[i].ToRlpBytes()); + } + + return Rlp.Encode(encodedRecords); + } + + private static byte[] DecodeRequestId(ref Rlp.ValueDecoderContext ctx) + { + byte[] requestId = ctx.DecodeByteArray(); + if (requestId.Length > MaxRequestIdLength) + { + throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {MaxRequestIdLength}."); + } + + return requestId; + } + + private static Discv5Pong DecodePong(byte[] requestId, ref Rlp.ValueDecoderContext ctx) + { + ulong enrSequence = ctx.DecodeULong(); + IPAddress recipientIp = new(ctx.DecodeByteArray()); + int recipientPort = ctx.DecodePositiveInt(); + return new Discv5Pong(requestId, enrSequence, recipientIp, recipientPort); + } + + private static int[] DecodeDistances(ref Rlp.ValueDecoderContext ctx) + { + int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + int count = ctx.PeekNumberOfItemsRemaining(checkPosition); + int[] distances = new int[count]; + for (int i = 0; i < count; i++) + { + distances[i] = ctx.DecodePositiveInt(); + } + + ctx.Check(checkPosition); + return distances; + } + + private static Discv5Nodes DecodeNodes(byte[] requestId, ref Rlp.ValueDecoderContext ctx) + { + int total = ctx.DecodePositiveInt(); + int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + int count = ctx.PeekNumberOfItemsRemaining(checkPosition); + NodeRecord[] records = new NodeRecord[count]; + int validRecords = 0; + for (int i = 0; i < count; i++) + { + ReadOnlySpan record = ctx.PeekNextItem(); + try + { + records[validRecords++] = NodeRecord.FromBytes(record); + } + catch (Exception) + { + } + + ctx.SkipItem(); + } + + ctx.Check(checkPosition); + if (validRecords != records.Length) + { + Array.Resize(ref records, validRecords); + } + + return new Discv5Nodes(requestId, total, records); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs new file mode 100644 index 000000000000..1f9844894dfe --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Network.Enr; + +namespace Nethermind.Network.Discovery.Discv5; + +internal enum Discv5MessageType : byte +{ + Ping = 0x01, + Pong = 0x02, + FindNode = 0x03, + Nodes = 0x04, + TalkReq = 0x05, + TalkResp = 0x06 +} + +internal abstract record Discv5Message(byte[] RequestId) +{ + public abstract Discv5MessageType MessageType { get; } +} + +internal sealed record Discv5Ping(byte[] RequestId, ulong EnrSequence) : Discv5Message(RequestId) +{ + public override Discv5MessageType MessageType => Discv5MessageType.Ping; +} + +internal sealed record Discv5Pong(byte[] RequestId, ulong EnrSequence, IPAddress RecipientIp, int RecipientPort) : Discv5Message(RequestId) +{ + public override Discv5MessageType MessageType => Discv5MessageType.Pong; +} + +internal sealed record Discv5FindNode(byte[] RequestId, int[] Distances) : Discv5Message(RequestId) +{ + public override Discv5MessageType MessageType => Discv5MessageType.FindNode; +} + +internal sealed record Discv5Nodes(byte[] RequestId, int Total, NodeRecord[] Records) : Discv5Message(RequestId) +{ + public override Discv5MessageType MessageType => Discv5MessageType.Nodes; +} + +internal sealed record Discv5TalkReq(byte[] RequestId, byte[] Protocol, byte[] Request) : Discv5Message(RequestId) +{ + public override Discv5MessageType MessageType => Discv5MessageType.TalkReq; +} + +internal sealed record Discv5TalkResp(byte[] RequestId, byte[] Response) : Discv5Message(RequestId) +{ + public override Discv5MessageType MessageType => Discv5MessageType.TalkResp; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs new file mode 100644 index 000000000000..d0e367c21ae1 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Diagnostics.CodeAnalysis; +using System.Net; +using Nethermind.Core.Crypto; +using Nethermind.Network.Enr; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5; + +internal static class Discv5NodeRecordConverter +{ + public static bool TryGetNodeFromEnr(NodeRecord enr, bool allowNonRoutable, [NotNullWhen(true)] out Node? node) + { + node = null; + + PublicKey? key = enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); + IPAddress? ip = enr.GetObj(EnrContentKey.Ip); + int? discoveryPort = enr.GetValue(EnrContentKey.Udp) ?? enr.GetValue(EnrContentKey.Tcp); + if (key is null || ip is null || discoveryPort is null) + { + return false; + } + + if (!DiscoveryV5App.IsDiscoveryAddressAcceptable(ip, allowNonRoutable)) + { + return false; + } + + if ((uint)discoveryPort.Value > ushort.MaxValue || discoveryPort.Value == 0) + { + return false; + } + + node = new Node(key, ip.ToString(), discoveryPort.Value) + { + Enr = enr.EnrString + }; + return true; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs new file mode 100644 index 000000000000..72fdcbdc8792 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5; + +public class Discv5NodeSource( + IKademlia kademlia, + KademliaConfig kademliaConfig, + ILogManager logManager) + : IKademliaNodeSource +{ + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly Hash256 _currentNodeHash = kademliaConfig.CurrentNodeId.IdHash; + + public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) + { + if (_logger.IsDebug) _logger.Debug("Starting discv5 node source"); + + Channel channel = Channel.CreateBounded(64); + ConcurrentDictionary writtenNodes = new(); + + foreach (Node node in kademlia.IterateNodes()) + { + if (!IsSelf(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) + { + yield return node; + } + } + + kademlia.OnNodeAdded += Handler; + try + { + await foreach (Node node in channel.Reader.ReadAllAsync(token)) + { + yield return node; + } + } + finally + { + kademlia.OnNodeAdded -= Handler; + } + + void Handler(object? _, Node node) + { + if (!IsSelf(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) + { + channel.Writer.TryWrite(node); + } + } + } + + private bool IsSelf(Node node) => node.IdHash.Equals(_currentNodeHash); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs new file mode 100644 index 000000000000..213f7e7f61cc --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs @@ -0,0 +1,562 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Buffers.Binary; +using System.Security.Cryptography; +using System.Text; +using Autofac.Features.AttributeFilters; +using Nethermind.Core.Crypto; +using Nethermind.Crypto; +using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5; + +internal enum Discv5PacketFlag : byte +{ + Ordinary = 0, + WhoAreYou = 1, + Handshake = 2 +} + +internal sealed record Discv5Packet( + Discv5PacketFlag Flag, + byte[] MaskingIv, + byte[] Nonce, + byte[] Header, + byte[] AuthData, + byte[] Message, + byte[] MessageAd) +{ + public byte[] ChallengeData => MessageAd; +} + +internal sealed record Discv5Challenge(byte[] RequestNonce, byte[] IdNonce, ulong EnrSequence, byte[] ChallengeData); + +internal sealed record Discv5Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] WriteKey) +{ + private long _nonceCounter; + + public byte[] RemoteNodeId => RemotePublicKey.Hash.BytesToArray(); + + public byte[] GetNextNonce(ICryptoRandom random) + { + byte[] nonce = new byte[Discv5PacketCodec.NonceSize]; + BinaryPrimitives.WriteUInt32BigEndian(nonce, unchecked((uint)Interlocked.Increment(ref _nonceCounter))); + random.GenerateRandomBytes(nonce.AsSpan(sizeof(uint))); + return nonce; + } +} + +public sealed class Discv5PacketCodec( + [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + INodeRecordProvider nodeRecordProvider, + ICryptoRandom cryptoRandom, + IEcdsa ecdsa) +{ + public const int NonceSize = 12; + + private const int MaskingIvSize = 16; + private const int StaticHeaderSize = 23; + private const int NodeIdSize = 32; + private const int WhoAreYouAuthDataSize = 24; + private const int IdNonceSize = 16; + private const int AesKeySize = 16; + private const int AesGcmTagSize = 16; + private const int Version = 1; + private const int IdSignatureSize = 64; + private const int EphemeralPublicKeySize = 33; + private const int HandshakeAuthDataHeadSize = NodeIdSize + 2; + + private static readonly byte[] ProtocolId = "discv5"u8.ToArray(); + private static readonly byte[] KeyAgreementInfoPrefix = Encoding.ASCII.GetBytes("discovery v5 key agreement"); + private static readonly byte[] IdentityProofText = Encoding.ASCII.GetBytes("discovery v5 identity proof"); + + private readonly PrivateKey _privateKey = nodeKey.Unprotect(); + private readonly PublicKey _publicKey = nodeKey.PublicKey; + private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; + private readonly ICryptoRandom _cryptoRandom = cryptoRandom; + private readonly IEcdsa _ecdsa = ecdsa; + + internal byte[] LocalNodeId => _publicKey.Hash.BytesToArray(); + + internal byte[] EncodeOrdinary(PublicKey destination, byte[] encryptionKey, Discv5Message message, byte[]? nonce = null) + { + byte[] actualNonce = nonce ?? CreateNonce(); + byte[] authData = LocalNodeId.ToArray(); + return EncodePacket(destination.Hash.BytesToArray(), Discv5PacketFlag.Ordinary, actualNonce, authData, encryptionKey, message); + } + + internal byte[] EncodeWhoAreYou(byte[] destinationNodeId, byte[] requestNonce, ulong enrSequence, out Discv5Challenge challenge) + { + byte[] idNonce = _cryptoRandom.GenerateRandomBytes(IdNonceSize); + byte[] authData = new byte[WhoAreYouAuthDataSize]; + idNonce.CopyTo(authData, 0); + BinaryPrimitives.WriteUInt64BigEndian(authData.AsSpan(IdNonceSize), enrSequence); + + byte[] packet = EncodePacket(destinationNodeId, Discv5PacketFlag.WhoAreYou, requestNonce, authData, null, null, out byte[] challengeData); + challenge = new Discv5Challenge(requestNonce.ToArray(), idNonce, enrSequence, challengeData); + return packet; + } + + internal byte[] EncodeHandshake(PublicKey destination, Discv5Challenge challenge, Discv5Message message, out Discv5Session session) + { + using PrivateKey ephemeralKey = new PrivateKeyGenerator(_cryptoRandom).Generate(); + DeriveKeys( + destination, + ephemeralKey, + LocalNodeId, + destination.Hash.Bytes, + challenge.ChallengeData, + out byte[] initiatorKey, + out byte[] recipientKey); + + byte[] ephemeralPublicKey = ephemeralKey.CompressedPublicKey.Bytes; + byte[] idSignature = SignIdNonce(challenge.ChallengeData, ephemeralPublicKey, destination.Hash.BytesToArray()); + byte[] record = challenge.EnrSequence < _nodeRecordProvider.Current.EnrSequence + ? _nodeRecordProvider.Current.ToRlpBytes() + : []; + + byte[] authData = new byte[HandshakeAuthDataHeadSize + idSignature.Length + ephemeralPublicKey.Length + record.Length]; + LocalNodeId.CopyTo(authData, 0); + authData[NodeIdSize] = IdSignatureSize; + authData[NodeIdSize + 1] = EphemeralPublicKeySize; + idSignature.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize)); + ephemeralPublicKey.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize + idSignature.Length)); + record.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize + idSignature.Length + ephemeralPublicKey.Length)); + + session = new Discv5Session(destination, recipientKey, initiatorKey); + return EncodePacket(destination.Hash.BytesToArray(), Discv5PacketFlag.Handshake, CreateNonce(), authData, initiatorKey, message); + } + + internal bool TryDecode(ReadOnlySpan packet, out Discv5Packet decoded) + => TryDecode(packet, LocalNodeId, out decoded); + + internal static bool TryDecode(ReadOnlySpan packet, ReadOnlySpan localNodeId, out Discv5Packet decoded) + { + decoded = null!; + if (packet.Length < MaskingIvSize + StaticHeaderSize) + { + return false; + } + + byte[] maskingIv = packet[..MaskingIvSize].ToArray(); + byte[] staticHeader = AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize)); + if (!staticHeader.AsSpan(0, ProtocolId.Length).SequenceEqual(ProtocolId)) + { + return false; + } + + int authDataSize = BinaryPrimitives.ReadUInt16BigEndian(staticHeader.AsSpan(StaticHeaderSize - sizeof(ushort))); + int headerSize = StaticHeaderSize + authDataSize; + if (packet.Length < MaskingIvSize + headerSize) + { + return false; + } + + byte[] header = AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, headerSize)); + if (!header.AsSpan(0, ProtocolId.Length).SequenceEqual(ProtocolId)) + { + return false; + } + + int version = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(ProtocolId.Length)); + if (version != Version) + { + return false; + } + + Discv5PacketFlag flag = (Discv5PacketFlag)header[ProtocolId.Length + sizeof(ushort)]; + byte[] nonce = header.AsSpan(ProtocolId.Length + sizeof(ushort) + sizeof(byte), NonceSize).ToArray(); + byte[] authData = header.AsSpan(StaticHeaderSize, authDataSize).ToArray(); + byte[] message = packet[(MaskingIvSize + headerSize)..].ToArray(); + byte[] messageAd = new byte[MaskingIvSize + header.Length]; + maskingIv.CopyTo(messageAd, 0); + header.CopyTo(messageAd.AsSpan(MaskingIvSize)); + + decoded = new Discv5Packet(flag, maskingIv, nonce, header, authData, message, messageAd); + return true; + } + + internal bool TryDecryptMessage(Discv5Packet packet, byte[] encryptionKey, out Discv5Message message) + => TryDecryptMessageForTest(packet, encryptionKey, out message); + + internal static bool TryDecryptMessageForTest(Discv5Packet packet, byte[] encryptionKey, out Discv5Message message) + { + message = null!; + if (packet.Message.Length < AesGcmTagSize) + { + return false; + } + + byte[] plaintext = new byte[packet.Message.Length - AesGcmTagSize]; + try + { + using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); + aesGcm.Decrypt( + packet.Nonce, + packet.Message.AsSpan(0, plaintext.Length), + packet.Message.AsSpan(plaintext.Length, AesGcmTagSize), + plaintext, + packet.MessageAd); + } + catch (CryptographicException) + { + return false; + } + + message = Discv5MessageCodec.Decode(plaintext); + return true; + } + + internal Discv5Challenge DecodeWhoAreYou(Discv5Packet packet) + { + if (packet.AuthData.Length != WhoAreYouAuthDataSize) + { + throw new RlpException("Invalid WHOAREYOU authdata length."); + } + + byte[] idNonce = packet.AuthData.AsSpan(0, IdNonceSize).ToArray(); + ulong enrSequence = BinaryPrimitives.ReadUInt64BigEndian(packet.AuthData.AsSpan(IdNonceSize)); + return new Discv5Challenge(packet.Nonce, idNonce, enrSequence, packet.ChallengeData); + } + + internal bool TryDecryptHandshake( + Discv5Packet packet, + Discv5Challenge challenge, + NodeRecord? knownRecord, + out Discv5Session session, + out Discv5Message message, + out NodeRecord? nodeRecord) + { + session = null!; + message = null!; + nodeRecord = null; + + if (!TryReadHandshakeAuthData(packet.AuthData, out byte[] sourceNodeId, out byte[] idSignature, out CompressedPublicKey ephemeralPublicKey, out byte[] recordBytes)) + { + return false; + } + + if (recordBytes.Length > 0) + { + try + { + nodeRecord = NodeRecord.FromBytes(recordBytes); + } + catch (Exception) + { + return false; + } + } + + NodeRecord? record = nodeRecord ?? knownRecord; + CompressedPublicKey? remoteCompressedPublicKey = record?.GetObj(EnrContentKey.SecP256k1); + if (remoteCompressedPublicKey is null || !remoteCompressedPublicKey.Decompress().Hash.Bytes.SequenceEqual(sourceNodeId)) + { + return false; + } + + if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature, challenge.ChallengeData, ephemeralPublicKey.Bytes, LocalNodeId)) + { + return false; + } + + PublicKey remotePublicKey = remoteCompressedPublicKey.Decompress(); + DeriveKeys(ephemeralPublicKey, sourceNodeId, LocalNodeId, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); + + if (!TryDecryptMessage(packet, initiatorKey, out message)) + { + return false; + } + + session = new Discv5Session(remotePublicKey, initiatorKey, recipientKey); + return true; + } + + internal static bool TryGetSourceNodeId(Discv5Packet packet, out byte[] sourceNodeId) + { + sourceNodeId = []; + switch (packet.Flag) + { + case Discv5PacketFlag.Ordinary when packet.AuthData.Length == NodeIdSize: + sourceNodeId = packet.AuthData.ToArray(); + return true; + case Discv5PacketFlag.Handshake when packet.AuthData.Length >= HandshakeAuthDataHeadSize: + sourceNodeId = packet.AuthData.AsSpan(0, NodeIdSize).ToArray(); + return true; + default: + return false; + } + } + + private byte[] EncodePacket( + byte[] destinationNodeId, + Discv5PacketFlag flag, + byte[] nonce, + byte[] authData, + byte[]? encryptionKey, + Discv5Message? message) + => EncodePacket(destinationNodeId, flag, nonce, authData, encryptionKey, message, out _); + + private byte[] EncodePacket( + byte[] destinationNodeId, + Discv5PacketFlag flag, + byte[] nonce, + byte[] authData, + byte[]? encryptionKey, + Discv5Message? message, + out byte[] messageAd) + { + byte[] maskingIv = _cryptoRandom.GenerateRandomBytes(MaskingIvSize); + byte[] header = CreateHeader(flag, nonce, authData); + messageAd = new byte[MaskingIvSize + header.Length]; + maskingIv.CopyTo(messageAd, 0); + header.CopyTo(messageAd.AsSpan(MaskingIvSize)); + + byte[] encryptedMessage = []; + if (message is not null) + { + ArgumentNullException.ThrowIfNull(encryptionKey); + + encryptedMessage = EncryptMessage(encryptionKey, nonce, Discv5MessageCodec.Encode(message), messageAd); + } + + byte[] maskedHeader = AesCtrTransform(destinationNodeId.AsSpan(0, AesKeySize), maskingIv, header); + byte[] packet = new byte[MaskingIvSize + maskedHeader.Length + encryptedMessage.Length]; + maskingIv.CopyTo(packet, 0); + maskedHeader.CopyTo(packet.AsSpan(MaskingIvSize)); + encryptedMessage.CopyTo(packet.AsSpan(MaskingIvSize + maskedHeader.Length)); + return packet; + } + + private static byte[] CreateHeader(Discv5PacketFlag flag, byte[] nonce, byte[] authData) + { + if (nonce.Length != NonceSize) + { + throw new ArgumentException($"Nonce must be {NonceSize} bytes.", nameof(nonce)); + } + + byte[] header = new byte[StaticHeaderSize + authData.Length]; + ProtocolId.CopyTo(header, 0); + BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(ProtocolId.Length), Version); + header[ProtocolId.Length + sizeof(ushort)] = (byte)flag; + nonce.CopyTo(header.AsSpan(ProtocolId.Length + sizeof(ushort) + sizeof(byte))); + BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(StaticHeaderSize - sizeof(ushort)), checked((ushort)authData.Length)); + authData.CopyTo(header.AsSpan(StaticHeaderSize)); + return header; + } + + private byte[] CreateNonce() => _cryptoRandom.GenerateRandomBytes(NonceSize); + + private static byte[] EncryptMessage(byte[] encryptionKey, byte[] nonce, byte[] plaintext, byte[] messageAd) + { + byte[] encrypted = new byte[plaintext.Length + AesGcmTagSize]; + using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); + aesGcm.Encrypt( + nonce, + plaintext, + encrypted.AsSpan(0, plaintext.Length), + encrypted.AsSpan(plaintext.Length, AesGcmTagSize), + messageAd); + return encrypted; + } + + private static byte[] AesCtrTransform(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan input) + { + byte[] output = new byte[input.Length]; + using Aes aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.Key = key.ToArray(); + + using ICryptoTransform encryptor = aes.CreateEncryptor(); + Span counter = stackalloc byte[MaskingIvSize]; + iv.CopyTo(counter); + Span keyStream = stackalloc byte[MaskingIvSize]; + byte[] counterBlock = new byte[MaskingIvSize]; + byte[] keyStreamBlock = new byte[MaskingIvSize]; + + int offset = 0; + while (offset < input.Length) + { + counter.CopyTo(counterBlock); + encryptor.TransformBlock(counterBlock, 0, counterBlock.Length, keyStreamBlock, 0); + keyStreamBlock.CopyTo(keyStream); + + int blockLength = Math.Min(MaskingIvSize, input.Length - offset); + for (int i = 0; i < blockLength; i++) + { + output[offset + i] = (byte)(input[offset + i] ^ keyStream[i]); + } + + IncrementCounter(counter); + offset += blockLength; + } + + return output; + } + + private static void IncrementCounter(Span counter) + { + for (int i = counter.Length - 1; i >= 0; i--) + { + counter[i]++; + if (counter[i] != 0) + { + return; + } + } + } + + private static void DeriveKeys( + PublicKey remotePublicKey, + PrivateKey ephemeralPrivateKey, + ReadOnlySpan initiatorNodeId, + ReadOnlySpan recipientNodeId, + byte[] challengeData, + out byte[] initiatorKey, + out byte[] recipientKey) + { + byte[] secret = SecP256k1Agreement.AgreeCompressed(remotePublicKey, ephemeralPrivateKey); + DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); + } + + private void DeriveKeys( + CompressedPublicKey ephemeralPublicKey, + ReadOnlySpan initiatorNodeId, + ReadOnlySpan recipientNodeId, + byte[] challengeData, + out byte[] initiatorKey, + out byte[] recipientKey) + { + byte[] secret = SecP256k1Agreement.AgreeCompressed(ephemeralPublicKey, _privateKey); + DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); + } + + private static void DeriveKeys( + byte[] secret, + ReadOnlySpan initiatorNodeId, + ReadOnlySpan recipientNodeId, + byte[] challengeData, + out byte[] initiatorKey, + out byte[] recipientKey) + { + byte[] prk = HMACSHA256.HashData(challengeData, secret); + byte[] info = new byte[KeyAgreementInfoPrefix.Length + NodeIdSize + NodeIdSize]; + KeyAgreementInfoPrefix.CopyTo(info, 0); + initiatorNodeId.CopyTo(info.AsSpan(KeyAgreementInfoPrefix.Length)); + recipientNodeId.CopyTo(info.AsSpan(KeyAgreementInfoPrefix.Length + NodeIdSize)); + + byte[] keyData = HkdfExpand(prk, info, AesKeySize * 2); + initiatorKey = keyData[..AesKeySize]; + recipientKey = keyData[AesKeySize..]; + } + + internal static (byte[] InitiatorKey, byte[] RecipientKey) DeriveKeysForTest( + byte[] secret, + byte[] initiatorNodeId, + byte[] recipientNodeId, + byte[] challengeData) + { + DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out byte[] initiatorKey, out byte[] recipientKey); + return (initiatorKey, recipientKey); + } + + private static byte[] HkdfExpand(byte[] prk, byte[] info, int length) + { + byte[] result = new byte[length]; + byte[] previous = []; + int offset = 0; + byte counter = 1; + using HMACSHA256 hmac = new(prk); + while (offset < length) + { + byte[] input = new byte[previous.Length + info.Length + 1]; + previous.CopyTo(input, 0); + info.CopyTo(input.AsSpan(previous.Length)); + input[^1] = counter++; + previous = hmac.ComputeHash(input); + int copyLength = Math.Min(previous.Length, length - offset); + previous.AsSpan(0, copyLength).CopyTo(result.AsSpan(offset)); + offset += copyLength; + } + + return result; + } + + private byte[] SignIdNonce(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + { + byte[] signingHash = CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); + Signature signature = _ecdsa.Sign(_privateKey, new ValueHash256(signingHash)); + return signature.Bytes.ToArray(); + } + + private bool VerifyIdSignature(CompressedPublicKey signer, byte[] signatureBytes, byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + { + byte[] signingHash = CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); + for (int recoveryId = 0; recoveryId <= 1; recoveryId++) + { + Signature signature = new(signatureBytes, recoveryId); + CompressedPublicKey? recovered = _ecdsa.RecoverCompressedPublicKey(signature, new ValueHash256(signingHash)); + if (signer.Equals(recovered)) + { + return true; + } + } + + return false; + } + + internal static byte[] CalculateIdSignatureHashForTest(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + => CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); + + private static byte[] CalculateIdSignatureHash(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + { + byte[] signingInput = new byte[IdentityProofText.Length + challengeData.Length + ephemeralPublicKey.Length + recipientNodeId.Length]; + IdentityProofText.CopyTo(signingInput, 0); + challengeData.CopyTo(signingInput.AsSpan(IdentityProofText.Length)); + ephemeralPublicKey.CopyTo(signingInput.AsSpan(IdentityProofText.Length + challengeData.Length)); + recipientNodeId.CopyTo(signingInput.AsSpan(IdentityProofText.Length + challengeData.Length + ephemeralPublicKey.Length)); + return SHA256.HashData(signingInput); + } + + private static bool TryReadHandshakeAuthData( + byte[] authData, + out byte[] sourceNodeId, + out byte[] idSignature, + out CompressedPublicKey ephemeralPublicKey, + out byte[] record) + { + sourceNodeId = []; + idSignature = []; + ephemeralPublicKey = null!; + record = []; + + if (authData.Length < HandshakeAuthDataHeadSize) + { + return false; + } + + sourceNodeId = authData.AsSpan(0, NodeIdSize).ToArray(); + int signatureSize = authData[NodeIdSize]; + int ephemeralKeySize = authData[NodeIdSize + 1]; + if (signatureSize != IdSignatureSize || ephemeralKeySize != EphemeralPublicKeySize) + { + return false; + } + + int signatureOffset = HandshakeAuthDataHeadSize; + int ephemeralKeyOffset = signatureOffset + signatureSize; + int recordOffset = ephemeralKeyOffset + ephemeralKeySize; + if (authData.Length < recordOffset) + { + return false; + } + + idSignature = authData.AsSpan(signatureOffset, signatureSize).ToArray(); + ephemeralPublicKey = new CompressedPublicKey(authData.AsSpan(ephemeralKeyOffset, ephemeralKeySize)); + record = authData.AsSpan(recordOffset).ToArray(); + return true; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs new file mode 100644 index 000000000000..6bbd3df997c6 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5; + +/// +/// Adapts discv5 distance-based FINDNODE requests to the protocol-specific Kademlia routing table. +/// +public interface IDiscv5KademliaAdapter : IKademliaMessageSender, IAsyncDisposable +{ + /// + /// Gets known nodes at the requested log distances from the local node. + /// + /// The requested XOR log distances. + /// An optional node to exclude from the result. + Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = null); + + Task RunAsync(CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index fa0397044666..dbd8f151e891 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -5,9 +5,9 @@ using System.Net.Sockets; using System.Threading.Channels; using DotNetty.Buffers; +using DotNetty.Common.Utilities; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; -using Lantern.Discv5.WireProtocol.Connection; using Microsoft.Extensions.DependencyInjection; using Nethermind.Logging; using Nethermind.Serialization.Rlp; @@ -15,24 +15,32 @@ namespace Nethermind.Network.Discovery; /// -/// Adapter, integrating DotNetty externally-managed with Lantern.Discv5 +/// DotNetty UDP bridge used by the native discv5 implementation. /// -public class NettyDiscoveryV5Handler(ILogManager loggerManager) : NettyDiscoveryBaseHandler(loggerManager), IUdpConnection +public class NettyDiscoveryV5Handler(ILogManager loggerManager) : NettyDiscoveryBaseHandler(loggerManager) { private const int MaxMessagesBuffered = 1024; private readonly ILogger _logger = loggerManager.GetClassLogger(); - private readonly Channel _inboundQueue = Channel.CreateBounded(MaxMessagesBuffered); + private readonly Channel _inboundQueue = Channel.CreateBounded(MaxMessagesBuffered); private IChannel? _nettyChannel; + private int _activeReaders; public void InitializeChannel(IChannel channel) => _nettyChannel = channel; protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket msg) { - UdpReceiveResult udpPacket = new(msg.Content.ReadAllBytesAsArray(), (IPEndPoint)msg.Sender); + msg.Retain(); + DatagramPacket queuedPacket = msg; - if (!_inboundQueue.Writer.TryWrite(udpPacket) && _logger.IsDebug) + if (_inboundQueue.Writer.TryWrite(queuedPacket)) + { + return; + } + + ReferenceCountUtil.Release(queuedPacket); + if (_logger.IsDebug) { _logger.Warn("Skipping discovery v5 message as inbound buffer is full"); } @@ -55,13 +63,47 @@ public async Task SendAsync(byte[] data, IPEndPoint destination) } } - public IAsyncEnumerable ReadMessagesAsync(CancellationToken token = default) => - _inboundQueue.Reader.ReadAllAsync(token); + public async IAsyncEnumerable ReadMessagesAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token = default) + { + Interlocked.Increment(ref _activeReaders); + try + { + await foreach (DatagramPacket packet in _inboundQueue.Reader.ReadAllAsync(token)) + { + try + { + yield return new UdpReceiveResult(packet.Content.ReadAllBytesAsArray(), (IPEndPoint)packet.Sender); + } + finally + { + ReferenceCountUtil.Release(packet); + } + } + } + finally + { + Interlocked.Decrement(ref _activeReaders); + ReleaseQueuedPackets(); + } + } public Task ListenAsync(CancellationToken token = default) => Task.CompletedTask; - public void Close() => _inboundQueue.Writer.Complete(); + public void Close() + { + _inboundQueue.Writer.TryComplete(); + if (Volatile.Read(ref _activeReaders) == 0) + { + ReleaseQueuedPackets(); + } + } + + private void ReleaseQueuedPackets() + { + while (_inboundQueue.Reader.TryRead(out DatagramPacket? packet)) + { + ReferenceCountUtil.Release(packet); + } + } - public static void Register(IServiceCollection services) => services - .AddSingleton() - .AddSingleton(static p => p.GetRequiredService()); + public static void Register(IServiceCollection services) => services.AddSingleton(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NetworkNodeExtensions.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NetworkNodeExtensions.cs deleted file mode 100644 index 62053cb01754..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NetworkNodeExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Lantern.Discv5.Enr; -using Lantern.Discv5.Enr.Entries; -using Lantern.Discv5.Enr.Identity; -using NBitcoin.Secp256k1; -using Nethermind.Config; - -namespace Nethermind.Network.Discovery.Discv5; - -public static class NetworkNodeExtensions -{ - public static Lantern.Discv5.Enr.Enr ToEnr(this NetworkNode node, IIdentityVerifier verifier, IIdentitySigner signer) - { - if (node.IsEnr) return node.Enr; - - Enode enode = node.Enode; - return new EnrBuilder() - .WithIdentityScheme(verifier, signer) - .WithEntry(EnrEntryKey.Id, new EntryId("v4")) - .WithEntry(EnrEntryKey.Ip, new EntryIp(enode.HostIp)) - .WithEntry(EnrEntryKey.Secp256K1, new EntrySecp256K1(Context.Instance.CreatePubKey(enode.PublicKey.PrefixedBytes).ToBytes(false))) - .WithEntry(EnrEntryKey.Tcp, new EntryTcp(enode.Port)) - .WithEntry(EnrEntryKey.Udp, new EntryUdp(enode.DiscoveryPort)) - .Build(); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index d8374789a68f..ed4252e4d30c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -3,6 +3,7 @@ using Autofac; using Nethermind.Core; +using Nethermind.Kademlia; using Nethermind.Network.Discovery.Discv4; namespace Nethermind.Network.Discovery.Kademlia; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs index 5326865f767b..08cfd353b141 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs @@ -20,7 +20,7 @@ public class PingMsg : DiscoveryMsg /// /// https://eips.ethereum.org/EIPS/eip-868 /// - public long? EnrSequence { get; set; } + public ulong? EnrSequence { get; set; } public PingMsg(PublicKey farPublicKey, long expirationTime, IPEndPoint source, IPEndPoint destination, byte[] mdc) : base(farPublicKey, expirationTime) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj b/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj index 63a3cddbf7a8..bf37362bb6ef 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj +++ b/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj @@ -14,12 +14,12 @@ + - diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index 9e92dc31c165..e257fc692d6e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -109,10 +109,6 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg, out b EndPoint address = packet.Sender; int size = content.ReadableBytes; - using ArrayPoolDisposableReturn handle = ArrayPoolDisposableReturn.Rent(size, out byte[] msgBytes); - - content.ReadBytes(msgBytes, 0, size); - Interlocked.Add(ref Metrics.DiscoveryBytesReceived, size); if (size < 98) @@ -121,11 +117,14 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg, out b return false; } - if (FromMsgTypeByte(msgBytes[97]) is not { } type) + int readerIndex = content.ReaderIndex; + byte msgTypeByte = content.GetByte(readerIndex + 97); + if (FromMsgTypeByte(msgTypeByte) is not { } type) { - if (_logger.IsDebug) _logger.Debug($"Unsupported message type: {msgBytes[97]}, sender: {address}, message {msgBytes.AsSpan(0, size).ToHexString()}"); + if (_logger.IsDebug) _logger.Debug($"Unsupported message type: {msgTypeByte}, sender: {address}"); return false; } + if (_logger.IsTrace) _logger.Trace($"Received message: {type}"); if (address is IPEndPoint remoteEndpoint && !TryAcceptInbound(remoteEndpoint)) @@ -135,6 +134,9 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg, out b return false; } + using ArrayPoolDisposableReturn handle = ArrayPoolDisposableReturn.Rent(size, out byte[] msgBytes); + content.GetBytes(readerIndex, msgBytes, 0, size); + try { msg = Deserialize(type, new ArraySegment(msgBytes, 0, size)); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs index de5c2e0cb577..691eaa0f7d24 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs @@ -37,7 +37,7 @@ protected void Serialize(byte type, Span data, IByteBuffer byteBuffer) byteBuffer.SetWriterIndex(startWriteIndex + 32 + 65); byteBuffer.WriteByte(type); - byteBuffer.WriteBytes(data.ToArray(), 0, data.Length); + byteBuffer.WriteBytes(data); byteBuffer.SetReaderIndex(startReadIndex + 32 + 65); ValueHash256 toSign = ValueKeccak.Compute(byteBuffer.ReadAllBytesAsSpan()); @@ -54,7 +54,7 @@ protected void Serialize(byte type, Span data, IByteBuffer byteBuffer) byteBuffer.SetReaderIndex(startReadIndex); byteBuffer.SetWriterIndex(startWriteIndex); - byteBuffer.WriteBytes(mdc.BytesAsSpan.ToArray(), 0, 32); + byteBuffer.WriteBytes(mdc.BytesAsSpan); byteBuffer.SetWriterIndex(startWriteIndex + length); } @@ -82,7 +82,7 @@ protected void AddSignatureAndMdc(IByteBuffer byteBuffer, int dataLength) byteBuffer.SetReaderIndex(startReadIndex); byteBuffer.SetWriterIndex(startWriteIndex); - byteBuffer.WriteBytes(mdc.BytesAsSpan.ToArray(), 0, 32); + byteBuffer.WriteBytes(mdc.BytesAsSpan); byteBuffer.SetReaderIndex(startReadIndex); byteBuffer.SetWriterIndex(startWriteIndex + length); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs index 875fd4d98bb6..23d5f3f31c9f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs @@ -69,7 +69,7 @@ public PingMsg Deserialize(IByteBuffer msgBytes) { if (ctx.Position < ctx.Length) { - long enrSequence = ctx.DecodeLong(); + ulong enrSequence = ctx.DecodeULong(); msg.EnrSequence = enrSequence; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs deleted file mode 100644 index deb036d3fdd7..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/TalkReqAndRespHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Lantern.Discv5.WireProtocol.Messages; - -namespace Nethermind.Network.Discovery; - -/// https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#talkreq-request-0x05 -internal class TalkReqAndRespHandler : ITalkReqAndRespHandler -{ - //Must send an empty response if no protocols are matched - private static readonly byte[][] EmptyProtocolResponse = [[]]; - - public byte[][]? HandleRequest(byte[] protocol, byte[] request) => - //We currently don't advertise any supported protocols - EmptyProtocolResponse; - - // We don't care about anything returned here at the moment - public byte[]? HandleResponse(byte[] response) => []; -} diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index 449af3b6dafa..98f277cff3af 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -108,8 +108,7 @@ public void Throws_when_record_is_t() rlpStream.StartSequence(500); rlpStream.Encode(bytes[..500]); rlpStream.Position = 0; - signer.Invoking(s => s.Deserialize(rlpStream)).Should().Throw() - .Where(ex => ex.NetworkExceptionType == NetworkExceptionType.Discovery); + signer.Invoking(s => s.Deserialize(rlpStream)).Should().Throw(); } [TestCase("f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f")] diff --git a/src/Nethermind/Nethermind.Network.Enr/Nethermind.Network.Enr.csproj b/src/Nethermind/Nethermind.Network.Enr/Nethermind.Network.Enr.csproj index 24fcd5b1e3dc..f28a1362d4de 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Nethermind.Network.Enr.csproj +++ b/src/Nethermind/Nethermind.Network.Enr/Nethermind.Network.Enr.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 110e2f7437d7..c41a59a9de1f 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -1,13 +1,11 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Text; -using DotNetty.Buffers; -using DotNetty.Codecs.Base64; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Crypto; using Nethermind.Serialization.Rlp; +using Convert = System.Convert; namespace Nethermind.Network.Enr; @@ -16,7 +14,7 @@ namespace Nethermind.Network.Enr; /// public class NodeRecord { - private long _enrSequence; + private ulong _enrSequence; private string? _enrString; @@ -36,7 +34,7 @@ public class NodeRecord /// update to the node data. Setting sequence on this class wipes out and /// . /// - public long EnrSequence + public ulong EnrSequence { get => _enrSequence; set @@ -90,6 +88,39 @@ private Hash256 CalculateContentHash() public NodeRecord() => SetEntry(IdEntry.Instance); + public static NodeRecord FromEnrString(string enrString) + { + const string prefix = "enr:"; + if (!enrString.StartsWith(prefix, StringComparison.Ordinal)) + { + throw new ArgumentException("ENR must start with the 'enr:' prefix.", nameof(enrString)); + } + + string base64 = enrString[prefix.Length..].Replace('-', '+').Replace('_', '/'); + int padding = (4 - base64.Length % 4) % 4; + if (padding is not 0) + { + base64 = string.Concat(base64, new string('=', padding)); + } + + return FromBytes(Convert.FromBase64String(base64)); + } + + public static NodeRecord FromBytes(ReadOnlySpan bytes) + => FromBytes(bytes.ToArray()); + + public static NodeRecord FromBytes(byte[] bytes) + { + NodeRecordSigner signer = new(new Ecdsa()); + NodeRecord nodeRecord = signer.Deserialize(new RlpStream(bytes)); + if (!signer.Verify(nodeRecord)) + { + throw new RlpException("Invalid ENR signature."); + } + + return nodeRecord; + } + /// /// Sets one of the record entries. Entries are then automatically sorted by keys. /// @@ -195,6 +226,14 @@ public string GetHex() return rlpStream.Data.AsSpan().ToHexString(); } + public byte[] ToRlpBytes() + { + int rlpLength = GetRlpLengthWithSignature(); + RlpStream rlpStream = new(rlpLength); + Encode(rlpStream); + return rlpStream.Data.ToArray()!; + } + /// /// Applies Rlp([signature, seq, k, v, ...]]). /// @@ -218,28 +257,9 @@ private string CreateEnrString() RequireSignature(); const string prefix = "enr:"; - int rlpLength = GetRlpLengthWithSignature(); - IByteBuffer buffer = NethermindBuffers.Default.Buffer(rlpLength); - try - { - NettyRlpStream rlpStream = new(buffer); - Encode(rlpStream); - IByteBuffer resultBuffer = Base64.Encode(buffer, Base64Dialect.URL_SAFE); - try - { - string base64String = resultBuffer.ReadString(resultBuffer.ReadableBytes, Encoding.UTF8); - int skipLast = base64String[^2] == '=' ? 2 : base64String[^1] == '=' ? 1 : 0; - return string.Concat(prefix, base64String.AsSpan(0, base64String.Length - skipLast)); - } - finally - { - resultBuffer.Release(); - } - } - finally - { - buffer.Release(); - } + string base64String = Convert.ToBase64String(ToRlpBytes()).Replace('+', '-').Replace('/', '_'); + int skipLast = base64String[^2] == '=' ? 2 : base64String[^1] == '=' ? 1 : 0; + return string.Concat(prefix, base64String.AsSpan(0, base64String.Length - skipLast)); } private void RequireSignature() diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index 2fff1a5c1dc0..c684750e5ea9 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -11,17 +11,25 @@ namespace Nethermind.Network.Enr; /// /// https://eips.ethereum.org/EIPS/eip-778 /// -public class NodeRecordSigner(IEcdsa? ethereumEcdsa, PrivateKey? privateKey) : INodeRecordSigner +public class NodeRecordSigner(IEcdsa? ethereumEcdsa, PrivateKey? privateKey = null) : INodeRecordSigner { private readonly IEcdsa _ecdsa = ethereumEcdsa ?? throw new ArgumentNullException(nameof(ethereumEcdsa)); - private readonly PrivateKey _privateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey)); + private readonly PrivateKey? _privateKey = privateKey; /// /// Signs the node record with own private key. /// /// - public void Sign(NodeRecord nodeRecord) => nodeRecord.Signature = _ecdsa.Sign(_privateKey, in nodeRecord.ContentHash.ValueHash256); + public void Sign(NodeRecord nodeRecord) + { + if (_privateKey is null) + { + throw new InvalidOperationException("Cannot sign an ENR without a private key."); + } + + nodeRecord.Signature = _ecdsa.Sign(_privateKey, in nodeRecord.ContentHash.ValueHash256); + } /// /// Deserializes a from an . @@ -46,14 +54,14 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) int startPosition = ctx.Position; int recordRlpLength = ctx.ReadSequenceLength(); if (recordRlpLength > 300) - throw new NetworkingException("RLP received for ENR is bigger than 300 bytes", NetworkExceptionType.Discovery); + throw new RlpException("RLP received for ENR is bigger than 300 bytes"); NodeRecord nodeRecord = new(); ReadOnlySpan sigBytes = ctx.DecodeByteArraySpan(RlpLimit.L65); Signature signature = new(sigBytes, 0); bool canVerify = true; - long enrSequence = ctx.DecodeLong(); + ulong enrSequence = ctx.DecodeULong(); while (ctx.Position < startPosition + recordRlpLength) { ReadOnlySpan key = ctx.DecodeByteArraySpan(); diff --git a/src/Nethermind/Nethermind.Network.Test/CompositeNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Test/CompositeNodeSourceTests.cs index 12768a65ea94..f461be580a04 100644 --- a/src/Nethermind/Nethermind.Network.Test/CompositeNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/CompositeNodeSourceTests.cs @@ -1,6 +1,10 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Nethermind.Core.Test.Builders; using Nethermind.Stats.Model; using NUnit.Framework; @@ -22,4 +26,18 @@ public void CompositeNodeSource_ShouldIgnoreNodeRemoved_AfterDispose() Assert.That(removedNode, Is.Null); } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_ShouldStopInnerSources_WhenEnumerationIsDisposed(CancellationToken token) + { + TestNodeSource innerSource = new(); + CompositeNodeSource compositeNodeSource = new(innerSource); + Node node = new(TestItem.PublicKeyA, "1.2.3.4", 1234); + innerSource.AddNode(node); + + List nodes = await compositeNodeSource.DiscoverNodes(CancellationToken.None).Take(1).ToListAsync(token); + + Assert.That(nodes, Is.EqualTo(new[] { node })); + } } diff --git a/src/Nethermind/Nethermind.Network/CompositeNodeSource.cs b/src/Nethermind/Nethermind.Network/CompositeNodeSource.cs index ac9ad7e2c0a4..c5fe1070fde7 100644 --- a/src/Nethermind/Nethermind.Network/CompositeNodeSource.cs +++ b/src/Nethermind/Nethermind.Network/CompositeNodeSource.cs @@ -20,13 +20,15 @@ public class CompositeNodeSource : INodeSource, IDisposable public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken cancellationToken) { + using CancellationTokenSource disposeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + CancellationToken feedToken = disposeCts.Token; Channel ch = Channel.CreateBounded(1); using ArrayPoolList feedTasks = _nodeSources.Select(async innerSource => { - await foreach (Node node in innerSource.DiscoverNodes(cancellationToken)) + await foreach (Node node in innerSource.DiscoverNodes(feedToken)) { - await ch.Writer.WriteAsync(node, cancellationToken); + await ch.Writer.WriteAsync(node, feedToken); } }).ToPooledList(_nodeSources.Length * 16); @@ -39,7 +41,15 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance } finally { - await Task.WhenAll(feedTasks.AsSpan()); + await disposeCts.CancelAsync(); + ch.Writer.TryComplete(); + try + { + await Task.WhenAll(feedTasks.AsSpan()); + } + catch (OperationCanceledException) when (feedToken.IsCancellationRequested) + { + } } } diff --git a/src/Nethermind/Nethermind.Runner/packages.lock.json b/src/Nethermind/Nethermind.Runner/packages.lock.json index cd4959123197..f90ec8f16608 100644 --- a/src/Nethermind/Nethermind.Runner/packages.lock.json +++ b/src/Nethermind/Nethermind.Runner/packages.lock.json @@ -134,11 +134,6 @@ "NETStandard.Library": "1.6.1" } }, - "Keccak256": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "duyRtj4I3+yZZZC7Ma5S/cxzWn5CLPRcXeXtmBcLS3TpjwLm74afQEGzfYEWma8H/dbpUiHl2ozYszKuQ8QpEg==" - }, "libsodium": { "type": "Transitive", "resolved": "1.0.20", @@ -283,33 +278,11 @@ "System.CodeDom": "4.4.0" } }, - "Multiformats.Base": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "uMUDZLjkdI7zrkRFCC7tPV//1y9NnFNQnvyrzoLrn9lPNvSGQhHoA5BEBxO58S5Ow3R580UP8W6mfWDKtIuSYQ==" - }, - "Multiformats.Hash": { - "type": "Transitive", - "resolved": "1.5.0", - "contentHash": "f9HstrBNHUWs0WFhYH7H4H3VatzTVop+XWp0QDFW7f9JzeIj2fnz21P0IrgwR8H6wl1ujAEh+5yf30XlqRDcaQ==", - "dependencies": { - "BinaryEncoding": "1.4.0", - "Multiformats.Base": "2.0.1", - "Portable.BouncyCastle": "1.8.5", - "System.Composition": "1.2.0", - "murmurhash": "1.0.2" - } - }, "murmurhash": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "Yw9+sYL3qdTEXDKAEeiXsVwsP2K2nyWOxgvbDD1w5j+yu0CYk5edLvGmmJHqqFxuBFrVsgb7iF2XGprRlt+SEA==" }, - "NBitcoin.Secp256k1": { - "type": "Transitive", - "resolved": "3.1.5", - "contentHash": "HGOj4qoTGdHQ6lYjGOmYrxMgbTyyXonunPq+btFalAedumQ2tJxykiMlygEGNnEUoOXCAIV4fvbCdCthFw3LOQ==" - }, "Nethermind.DotNetty.Codecs": { "type": "Transitive", "resolved": "1.0.2.76", @@ -495,33 +468,11 @@ "libsodium": "1.0.16" } }, - "PierTwo.Lantern.Discv5.Enr": { - "type": "Transitive", - "resolved": "1.0.0-preview.8", - "contentHash": "NI1titqkA2KwIgNdPMJuLPNirgAPTNaL7K7x2Qf6RQpPI6AbMoGO0ny6CL4H/VLMVYQVzT1NzwLqJ78wNeUYJg==", - "dependencies": { - "Keccak256": "1.0.0", - "Multiformats.Base": "2.0.2", - "Multiformats.Hash": "1.5.0", - "NBitcoin.Secp256k1": "3.1.5", - "PierTwo.Lantern.Discv5.Rlp": "1.0.0-preview.8" - } - }, - "PierTwo.Lantern.Discv5.Rlp": { - "type": "Transitive", - "resolved": "1.0.0-preview.8", - "contentHash": "d50BMHF1g7rgcJLJmu7ytqFYRmMfkBkc2VddzTFVmEVPzb2Uk7genfObgwqMtvmHbYk6zQE57f2r5oZwU5B08g==" - }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.8.5", - "contentHash": "EaCgmntbH1sOzemRTqyXSqYjB6pLH7VCYHhhDYZ59guHSD5qPwhIYa7kfy0QUlmTRt9IXhaXdFhNuBUArp70Ng==" - }, "prometheus-net": { "type": "Transitive", "resolved": "8.2.1", @@ -662,6 +613,7 @@ "type": "Project", "dependencies": { "Nethermind.Core": "[1.39.0-unstable, )", + "Nethermind.Network.Enr": "[1.39.0-unstable, )", "NonBlocking": "[2.1.2, )", "System.Configuration.ConfigurationManager": "[10.0.8, )" } @@ -944,6 +896,14 @@ "Nethermind.Init": "[1.39.0-unstable, )" } }, + "nethermind.kademlia": { + "type": "Project", + "dependencies": { + "Nethermind.Core": "[1.39.0-unstable, )", + "Nethermind.Logging": "[1.39.0-unstable, )", + "NonBlocking": "[2.1.2, )" + } + }, "nethermind.keystore": { "type": "Project", "dependencies": { @@ -1028,9 +988,9 @@ "Nethermind.Api": "[1.39.0-unstable, )", "Nethermind.Crypto": "[1.39.0-unstable, )", "Nethermind.Facade": "[1.39.0-unstable, )", + "Nethermind.Kademlia": "[1.39.0-unstable, )", "Nethermind.Network": "[1.39.0-unstable, )", - "Nethermind.Network.Enr": "[1.39.0-unstable, )", - "PierTwo.Lantern.Discv5.WireProtocol": "[1.0.0-preview.8, )" + "Nethermind.Network.Enr": "[1.39.0-unstable, )" } }, "nethermind.network.dns": { @@ -1045,7 +1005,7 @@ "type": "Project", "dependencies": { "Nethermind.Crypto": "[1.39.0-unstable, )", - "Nethermind.Network": "[1.39.0-unstable, )" + "Nethermind.Serialization.Rlp": "[1.39.0-unstable, )" } }, "nethermind.network.stats": { @@ -1603,18 +1563,6 @@ "resolved": "2.1.0.5", "contentHash": "F/4WoNK1rYCMGZM6B1LVlgxf2wLogJc2ohMZxwmJw7Aky2Hc1IgFZvEj/cxcv5QQSFTvPN5AWYKomFXHukOUIg==" }, - "PierTwo.Lantern.Discv5.WireProtocol": { - "type": "CentralTransitive", - "requested": "[1.0.0-preview.8, )", - "resolved": "1.0.0-preview.8", - "contentHash": "mSHH0TEVdN2dQhvVnBrAUbSQiszO4YcjKkCurQJJxzBoYCp6R//ckfRa87fFkdqWKXJFHPJf2fWgd0vSmyB/Cw==", - "dependencies": { - "BouncyCastle.Cryptography": "2.4.0", - "NBitcoin.Secp256k1": "3.1.5", - "PierTwo.Lantern.Discv5.Enr": "1.0.0-preview.8", - "PierTwo.Lantern.Discv5.Rlp": "1.0.0-preview.8" - } - }, "Polly": { "type": "CentralTransitive", "requested": "[8.6.6, )", diff --git a/src/Nethermind/Nethermind.slnx b/src/Nethermind/Nethermind.slnx index 997680b319bf..587dce14db42 100644 --- a/src/Nethermind/Nethermind.slnx +++ b/src/Nethermind/Nethermind.slnx @@ -120,6 +120,7 @@ + From 043f4e928a13ba504403ea778995112372cea03c Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Sat, 23 May 2026 12:50:49 +0300 Subject: [PATCH 107/182] Align discv5 discovery lifecycle --- .../Modules/DiscoveryModule.cs | 10 +- .../Nethermind.Kademlia/DoubleEndedLru.cs | 1 + src/Nethermind/Nethermind.Kademlia/KBucket.cs | 16 +- .../Nethermind.Kademlia/Kademlia.cs | 4 + .../DiscoveryV5AppTests.cs | 30 ++- .../Kademlia/KBucketTests.cs | 14 ++ .../Kademlia/KademliaTests.cs | 14 ++ .../DiscoveryApp.cs | 162 +++------------ .../Discv5/DiscoveryV5App.cs | 169 ++++++++-------- .../Discv5/DiscoveryV5Report.cs | 39 ---- .../Discv5/Discv5NodeSource.cs | 19 +- .../Discv5/NettyDiscoveryV5Handler.cs | 4 + .../Discv5/discv5-bootnodes.json | 8 +- .../KademliaDiscoveryApp.cs | 185 ++++++++++++++++++ .../NodesLoaderTests.cs | 25 ++- .../Nethermind.Network/NodesLoader.cs | 5 +- .../Nethermind.Runner/configs/hoodi.json | 16 +- 17 files changed, 439 insertions(+), 282 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index fbb5c369fdce..b4c762325627 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -25,6 +25,14 @@ public class DiscoveryModule(IInitConfig initConfig, INetworkConfig networkConfi { protected override void Load(ContainerBuilder builder) { + builder.RegisterType() + .AsSelf() + .WithAttributeFiltering() + .WithParameter( + static (parameterInfo, _) => parameterInfo.ParameterType == typeof(bool) && parameterInfo.Name == "loadBootnodesAsPeerCandidates", + static (_, context) => (context.Resolve().DiscoveryVersion & DiscoveryVersion.V4) != 0) + .SingleInstance(); + builder // Enr discovery uses DNS to get some bootnodes. .AddSingleton((ethereumEcdsa, logManager) => @@ -43,8 +51,6 @@ protected override void Load(ContainerBuilder builder) .AddSingleton(logManager => new StaticNodesManager(initConfig.StaticNodesPath.GetApplicationResourcePath(initConfig.DataDir), logManager)) // This load from file. - .AddSingleton() - .AddSingleton((logManager) => new TrustedNodesManager(initConfig.TrustedNodesPath.GetApplicationResourcePath(initConfig.DataDir), logManager)) diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 833ba37ea04f..366a0dbbf3b9 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -21,6 +21,7 @@ public BucketAddResult AddOrRefresh(in KademliaHash hash, TNode node) if (_hashMapping.TryGetValue(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) { _queue.Remove(listNode); + listNode.Value = (hash, node); _queue.AddFirst(listNode); return BucketAddResult.Refreshed; } diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index 235726863646..a900cf6d3942 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -23,8 +23,10 @@ public class KBucket(int k) where TNode : notnull /// public BucketAddResult TryAddOrRefresh(in KademliaHash hash, TNode item, out TNode? toRefresh) { + TNode? previous = _items.GetByHash(hash); BucketAddResult addResult = _items.AddOrRefresh(hash, item); - if (addResult == BucketAddResult.Added) + if (addResult == BucketAddResult.Added || + (addResult == BucketAddResult.Refreshed && ShouldUpdateCachedArray(previous, item))) { _cachedArray = _items.GetAll(); } @@ -68,4 +70,16 @@ public void Clear() public bool ContainsNode(in KademliaHash hash) => _items.Contains(hash); public TNode? GetByHash(KademliaHash hash) => _items.GetByHash(hash); + + private static bool ShouldUpdateCachedArray(TNode? previous, TNode item) + { + if (previous is null) + { + return false; + } + + return typeof(TNode).IsValueType + ? !EqualityComparer.Default.Equals(previous, item) + : !ReferenceEquals(previous, item); + } } diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index 29ddec43787a..ff88b26e6e84 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -52,6 +52,10 @@ public Kademlia( _timestamper = timestamper; AddOrRefresh(_currentNodeId); + for (int i = 0; i < _bootNodes.Count; i++) + { + AddOrRefresh(_bootNodes[i]); + } } public TNode CurrentNode => _currentNodeId; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 70ae1272e08c..d4f66cdc341f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -92,13 +92,16 @@ public async Task Teardown() _legacyDiscoveryDb.Dispose(); } - private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null) + private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true) { NodeRecord enr = new(); enr.SetEntry(IdEntry.Instance); enr.SetEntry(new IpEntry(ipAddress ?? IPAddress.Loopback)); enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - enr.SetEntry(new TcpEntry(port)); + if (includeTcp) + { + enr.SetEntry(new TcpEntry(port)); + } enr.SetEntry(new UdpEntry(udpPort ?? port)); enr.EnrSequence = 1; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); @@ -215,6 +218,29 @@ public void Should_Use_Udp_Port_From_Enr() Assert.That(node!.Port, Is.EqualTo(30304)); } + [Test] + public void Should_Use_Udp_Port_From_Configured_Enr_Bootnode() + { + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), udpPort: 9001, includeTcp: false); + NetworkConfig networkConfig = new() + { + Bootnodes = [new NetworkNode(enr.EnrString)] + }; + DiscoveryConfig discoveryConfig = new() + { + UseDefaultDiscv5Bootnodes = false + }; + + List bootNodes = _discoveryV5App.CreateBootNodes(networkConfig, discoveryConfig); + + using (Assert.EnterMultipleScope()) + { + Assert.That(bootNodes, Has.Count.EqualTo(1)); + Assert.That(bootNodes[0].Port, Is.EqualTo(9001)); + Assert.That(bootNodes[0].Enr, Is.EqualTo(enr.EnrString)); + } + } + [Test] public void TryEnqueueNewEnr_Should_Deduplicate() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index df517bbd1314..4f0a83135963 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -60,6 +60,20 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() Assert.That(bucket.GetAll(), Is.SameAs(nodes)); } + [Test] + public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNewInstance() + { + KBucket bucket = new(5); + KademliaHash hash = KademliaHash.FromBytes(ValueKeccak.Compute("node").BytesAsSpan); + + bucket.TryAddOrRefresh(hash, "old", out _); + bucket.TryAddOrRefresh(hash, "new", out _); + + Assert.That(bucket.GetByHash(hash), Is.EqualTo("new")); + Assert.That(bucket.GetAll(), Is.EqualTo(new[] { "new" })); + Assert.That(bucket.GetAllWithHash(), Is.EqualTo(new[] { (hash, "new") })); + } + [Test] public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index f67246c43e13..31774eaf3c8d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -75,6 +75,20 @@ public void TestNodeRemoved() Assert.That(nodeRemovedTriggered, Is.EqualTo(1)); } + [Test] + public void ShouldSeedBootnodes() + { + ValueHash256 bootNode = ValueKeccak.Compute("bootnode"); + Kademlia kad = CreateKad(new KademliaConfig + { + KSize = 5, + Beta = 0, + BootNodes = [bootNode], + }); + + Assert.That(kad.IterateNodes(), Does.Contain(bootNode)); + } + [Test] public async Task TestTooManyNode() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index 061c13062cb3..a2ee6839370b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -19,20 +19,14 @@ namespace Nethermind.Network.Discovery; -public class DiscoveryApp : IDiscoveryApp, IAsyncDisposable +public class DiscoveryApp : KademliaDiscoveryApp { - private readonly ILogger _logger; - private readonly INetworkConfig _networkConfig; - private readonly IKademliaNodeSource _kademliaNodeSource; private readonly DiscoveryPersistenceManager _persistenceManager; private readonly IKademliaDiscv4Adapter _discv4Adapter; - private readonly IKademlia _kademlia; private readonly Func _discoveryHandlerFactory; private readonly ILifetimeScope _discv4Services; - private readonly CancellationTokenSource _stopCts; private NettyDiscoveryHandler? _discoveryHandler; - private Task? _runningTask; public DiscoveryApp( ILifetimeScope rootScope, @@ -42,16 +36,13 @@ public DiscoveryApp( IProcessExitSource processExitSource, ILogManager logManager, Action? configureDiscv4Services = null) + : base("discv4", networkConfig, processExitSource, logManager.GetClassLogger()) { - _logger = logManager.GetClassLogger(); - _networkConfig = networkConfig; - _stopCts = CancellationTokenSource.CreateLinkedTokenSource(processExitSource.Token); - List bootNodes = []; NetworkNode[] bootnodes = networkConfig.Bootnodes; if (bootnodes.Length == 0) { - if (_logger.IsWarn) _logger.Warn("No bootnodes specified in configuration"); + if (Logger.IsWarn) Logger.Warn("No bootnodes specified in configuration"); } for (int i = 0; i < bootnodes.Length; i++) @@ -59,13 +50,13 @@ public DiscoveryApp( NetworkNode bootnode = bootnodes[i]; if (!bootnode.IsEnode) { - if (_logger.IsTrace) _logger.Trace($"Ignoring ENR in discovery V4: {bootnode}"); + if (Logger.IsTrace) Logger.Trace($"Ignoring ENR in discovery V4: {bootnode}"); continue; } if (bootnode.NodeId is null) { - _logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); + Logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); continue; } @@ -82,8 +73,11 @@ public DiscoveryApp( configureDiscv4Services?.Invoke(builder); }); - (_kademliaNodeSource, _persistenceManager, _discv4Adapter, _kademlia, _discoveryHandlerFactory) = _discv4Services.Resolve(); - _kademlia.OnNodeRemoved += OnKademliaNodeRemoved; + DiscV4Services services = _discv4Services.Resolve(); + _persistenceManager = services.PersistenceManager; + _discv4Adapter = services.Discv4Adapter; + _discoveryHandlerFactory = services.NettyDiscoveryHandlerFactory; + UseKademliaServices(services.NodeSource, services.Kademlia); } /// @@ -99,53 +93,7 @@ Func NettyDiscoveryHandlerFactory { } - public Task StartAsync() - { - try - { - Initialize(); - return Task.CompletedTask; - } - catch (Exception e) - { - _logger.Error("Error during discovery app start process", e); - throw; - } - } - - public async Task StopAsync() - { - DetachEventHandlers(); - - try - { - await _stopCts.CancelAsync(); - } - catch (ObjectDisposedException) - { - } - - try - { - if (_runningTask is not null) - { - await _runningTask; - } - } - catch (OperationCanceledException) - { - } - catch (Exception e) - { - if (_logger.IsError) _logger.Error("Error in discovery task", e); - } - - _stopCts.Dispose(); - - if (_logger.IsInfo) _logger.Info("Discovery shutdown complete. Please wait for all components to close"); - } - - private void DetachEventHandlers() + protected override void DetachEventHandlers() { try { @@ -153,21 +101,10 @@ private void DetachEventHandlers() } catch (Exception e) { - _logger.Error("Error during discovery cleanup", e); + Logger.Error("Error during discovery cleanup", e); } } - string IStoppableService.Description => "discv4"; - - public void AddNodeToDiscovery(Node node) => _kademlia.AddOrRefresh(node); - - private void Initialize() - { - if (_logger.IsDebug) - _logger.Debug($"Discovery : udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); - ThisNodeInfo.AddInfo("Discovery :", $"udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); - } - protected virtual NettyDiscoveryHandler CreateDiscoveryHandler(IChannel channel) { NettyDiscoveryHandler discoveryHandler = _discoveryHandlerFactory(channel); @@ -175,7 +112,7 @@ protected virtual NettyDiscoveryHandler CreateDiscoveryHandler(IChannel channel) return discoveryHandler; } - public void InitializeChannel(IChannel channel) + public override void InitializeChannel(IChannel channel) { _discoveryHandler = CreateDiscoveryHandler(channel); _discoveryHandler.OnChannelActivated += OnChannelActivated; @@ -185,77 +122,24 @@ public void InitializeChannel(IChannel channel) .AddLast(_discoveryHandler); } - private void OnChannelActivated(object? sender, EventArgs e) - { - if (_logger.IsDebug) _logger.Debug("Activated discovery channel."); - - // Make sure this is non blocking code, otherwise netty will not process messages - // Explicitly use TaskScheduler.Default, otherwise it will use dotnetty's task scheduler which have a habit of - // not working sometimes. - if (_stopCts.IsCancellationRequested) return; - _runningTask = StartActivationAsync(_stopCts.Token); - } - - private async Task StartActivationAsync(CancellationToken cancellationToken) + protected override async Task RunDiscoveryAsync(CancellationToken cancellationToken) { - const string faultMessage = "Cannot activate channel."; - - try - { - await Task.Factory.StartNew(static state => ((DiscoveryApp)state!).ActivateAsync(), this, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); - if (!cancellationToken.IsCancellationRequested && _logger.IsDebug) _logger.Debug("Discovery App initialized."); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } - catch (Exception) - { - if (_logger.IsInfo) _logger.Info(faultMessage); - throw; - } - } + //Step 1 - read nodes and stats from db + await _persistenceManager.LoadPersistedNodes(cancellationToken); - private Task ActivateAsync() => ActivateAsync(_stopCts.Token); + Task persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); - private async Task ActivateAsync(CancellationToken cancellationToken) - { try { - //Step 1 - read nodes and stats from db - await _persistenceManager.LoadPersistedNodes(cancellationToken); - - Task persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); - - try - { - // Step 2 - run the standard kademlia routine - await _kademlia.Run(cancellationToken); - } - finally - { - // Block until persistence is finished - await persistenceTask; - } - } - catch (OperationCanceledException) - { - if (_logger.IsInfo) _logger.Info("Discovery App stopped"); + // Step 2 - run the standard kademlia routine + await Kademlia.Run(cancellationToken); } - catch (Exception e) + finally { - _logger.DebugError("Error during discovery initialization", e); + // Block until persistence is finished + await persistenceTask; } } - public IAsyncEnumerable DiscoverNodes(CancellationToken token) => _kademliaNodeSource.DiscoverNodes(token); - - private void OnKademliaNodeRemoved(object? sender, Node node) => NodeRemoved?.Invoke(sender, new NodeEventArgs(node)); - - public event EventHandler? NodeRemoved; - - public async ValueTask DisposeAsync() - { - _kademlia.OnNodeRemoved -= OnKademliaNodeRemoved; - await _discv4Services.DisposeAsync(); - } + protected override ValueTask DisposeAsyncCore() => _discv4Services.DisposeAsync(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 264835d3f259..cffa809ddb7b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -25,26 +25,19 @@ namespace Nethermind.Network.Discovery.Discv5; -public sealed class DiscoveryV5App : IDiscoveryApp, IAsyncDisposable +public sealed class DiscoveryV5App : KademliaDiscoveryApp { internal const int MaxPendingEnrsPerWalk = 4_096; internal const int MaxTrackedEnrsPerWalk = MaxPendingEnrsPerWalk * 2; - private readonly ILogger _logger; private readonly IDb _discoveryDb; private readonly IDb _legacyDiscoveryDb; - private readonly ILogManager _logManager; private readonly bool _allowNonRoutableEnrs; - private readonly IKademliaNodeSource _kademliaNodeSource; private readonly IDiscv5KademliaAdapter _discv5Adapter; - private readonly IKademlia _kademlia; private readonly Func _discoveryHandlerFactory; private readonly ILifetimeScope _discv5Services; - private readonly CancellationTokenSource _stopCts; private NettyDiscoveryV5Handler? _discoveryHandler; - private DiscoveryV5Report? _discoveryReport; - private Task? _runningTask; public DiscoveryV5App( ILifetimeScope rootScope, @@ -57,13 +50,11 @@ public DiscoveryV5App( IProcessExitSource processExitSource, ILogManager logManager, Action? configureDiscv5Services = null) + : base("discv5", networkConfig, processExitSource, logManager.GetClassLogger()) { - _logger = logManager.GetClassLogger(); _discoveryDb = discoveryDb; _legacyDiscoveryDb = legacyDiscoveryDb; - _logManager = logManager; _allowNonRoutableEnrs = ShouldAcceptNonRoutableEnrs(ipResolver.ExternalIp); - _stopCts = CancellationTokenSource.CreateLinkedTokenSource(processExitSource.Token); List bootNodes = CreateBootNodes(networkConfig, discoveryConfig); ITimestamper timestamper = rootScope.ResolveOptional() ?? Timestamper.Default; @@ -79,7 +70,10 @@ public DiscoveryV5App( configureDiscv5Services?.Invoke(builder); }); - (_kademliaNodeSource, _discv5Adapter, _kademlia, _discoveryHandlerFactory) = _discv5Services.Resolve(); + DiscV5Services services = _discv5Services.Resolve(); + _discv5Adapter = services.Discv5Adapter; + _discoveryHandlerFactory = services.NettyDiscoveryHandlerFactory; + UseKademliaServices(services.NodeSource, services.Kademlia); } private record DiscV5Services( @@ -91,15 +85,18 @@ Func NettyDiscoveryHandlerFactory { } - private List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig) + internal List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig) { List bootNodes = []; HashSet seen = []; + BootNodeStats configuredStats = new(); + BootNodeStats defaultStats = new(); + BootNodeStats storedStats = new(); NetworkNode[] configuredBootnodes = networkConfig.Bootnodes; for (int i = 0; i < configuredBootnodes.Length; i++) { - AddBootNode(bootNodes, seen, configuredBootnodes[i]); + configuredStats.Record(AddBootNode(bootNodes, seen, configuredBootnodes[i])); } if (discoveryConfig.UseDefaultDiscv5Bootnodes) @@ -107,50 +104,62 @@ private List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConfi string[] defaultBootnodes = GetDefaultDiscv5Bootnodes(); for (int i = 0; i < defaultBootnodes.Length; i++) { - AddBootNode(bootNodes, seen, NodeRecord.FromEnrString(defaultBootnodes[i])); + defaultStats.Record(AddBootNode(bootNodes, seen, NodeRecord.FromEnrString(defaultBootnodes[i]))); } } List storedEnrs = LoadStoredEnrs(); for (int i = 0; i < storedEnrs.Count; i++) { - AddBootNode(bootNodes, seen, storedEnrs[i]); + storedStats.Record(AddBootNode(bootNodes, seen, storedEnrs[i])); } - if (bootNodes.Count == 0 && _logger.IsWarn) + if (Logger.IsInfo) { - _logger.Warn("No discv5 bootnodes specified in configuration"); + Logger.Info($"Discv5 bootnodes accepted: {bootNodes.Count} ({configuredStats.Added}/{configuredStats.Total} configured, {defaultStats.Added}/{defaultStats.Total} default, {storedStats.Added}/{storedStats.Total} stored, duplicates: {configuredStats.Duplicates + defaultStats.Duplicates + storedStats.Duplicates}, skipped: {configuredStats.Skipped + defaultStats.Skipped + storedStats.Skipped}, use default discv5 bootnodes: {discoveryConfig.UseDefaultDiscv5Bootnodes})."); + } + + if (bootNodes.Count == 0 && Logger.IsWarn) + { + Logger.Warn("No discv5 bootnodes specified in configuration"); } return bootNodes; } - private void AddBootNode(List bootNodes, HashSet seen, NetworkNode networkNode) + private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, NetworkNode networkNode) { - Node node = new(networkNode.NodeId, networkNode.Host, networkNode.Port); if (networkNode.IsEnr) { - node.Enr = networkNode.ToString(); + return AddBootNode(bootNodes, seen, networkNode.Enr); } - AddBootNode(bootNodes, seen, node); + Node node = new(networkNode.NodeId, networkNode.Host, networkNode.Port); + return AddBootNode(bootNodes, seen, node); } - private void AddBootNode(List bootNodes, HashSet seen, NodeRecord nodeRecord) + private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, NodeRecord nodeRecord) { if (TryGetNodeFromEnr(nodeRecord, out Node? node)) { - AddBootNode(bootNodes, seen, node); + return AddBootNode(bootNodes, seen, node); } + + return BootNodeAddResult.Skipped; } - private static void AddBootNode(List bootNodes, HashSet seen, Node node) + private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, Node node) { - if (seen.Add(node.IdHash)) + if (!seen.Add(node.IdHash)) { - node.IsBootnode = true; - bootNodes.Add(node); + if (Logger.IsTrace) Logger.Trace($"Skipping duplicate discv5 bootnode {node:s}."); + return BootNodeAddResult.Duplicate; } + + node.IsBootnode = true; + bootNodes.Add(node); + if (Logger.IsDebug) Logger.Debug($"Accepted discv5 bootnode {node:s}, has ENR: {!string.IsNullOrEmpty(node.Enr)}."); + return BootNodeAddResult.Added; } private static string[] GetDefaultDiscv5Bootnodes() => @@ -163,33 +172,33 @@ internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? no PublicKey? key = GetPublicKeyFromEnr(enr); if (key is null) { - if (_logger.IsTrace) _logger.Trace("Enr declined, unable to extract public key."); + if (Logger.IsTrace) Logger.Trace("Enr declined, unable to extract public key."); return false; } IPAddress? ip = enr.GetObj(EnrContentKey.Ip); if (ip is null) { - if (_logger.IsTrace) _logger.Trace("Enr declined, no IP."); + if (Logger.IsTrace) Logger.Trace("Enr declined, no IP."); return false; } int? discoveryPort = GetDiscoveryPort(enr); if (discoveryPort is null) { - if (_logger.IsTrace) _logger.Trace("Enr declined, no discovery UDP port."); + if (Logger.IsTrace) Logger.Trace("Enr declined, no discovery UDP port."); return false; } if (!IsDiscoveryAddressAcceptable(ip, _allowNonRoutableEnrs)) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, non-routable IP {ip}."); + if (Logger.IsTrace) Logger.Trace($"Enr declined, non-routable IP {ip}."); return false; } if ((uint)discoveryPort.Value > ushort.MaxValue || discoveryPort.Value == 0) { - if (_logger.IsTrace) _logger.Trace($"Enr declined, invalid discovery UDP port {discoveryPort.Value}."); + if (Logger.IsTrace) Logger.Trace($"Enr declined, invalid discovery UDP port {discoveryPort.Value}."); return false; } @@ -307,75 +316,43 @@ private bool TryLoadStoredEnr(byte[] enrBytes, [NotNullWhen(true)] out NodeRecor catch (Exception e) { enr = null; - if (_logger.IsDebug) _logger.Debug($"Skipping stored discv5 ENR that cannot be decoded: {e}"); + if (Logger.IsDebug) Logger.Debug($"Skipping stored discv5 ENR that cannot be decoded: {e}"); return false; } } - public event EventHandler? NodeRemoved { add { } remove { } } - - public void InitializeChannel(IChannel channel) + public override void InitializeChannel(IChannel channel) { _discoveryHandler = _discoveryHandlerFactory(); _discoveryHandler.InitializeChannel(channel); + _discoveryHandler.OnChannelActivated += OnChannelActivated; channel.Pipeline.AddLast(_discoveryHandler); } - public Task StartAsync() - { - _discoveryReport = new DiscoveryV5Report(_kademlia, _logManager, _stopCts.Token); - _runningTask = Task.Factory.StartNew(static state => ((DiscoveryV5App)state!).ActivateAsync(), this, _stopCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); - return Task.CompletedTask; - } - - private async Task ActivateAsync() + protected override void DetachEventHandlers() { try { - await Task.WhenAll(_discv5Adapter.RunAsync(_stopCts.Token), _kademlia.Run(_stopCts.Token)); - } - catch (OperationCanceledException) - { - if (_logger.IsInfo) _logger.Info("Discovery V5 App stopped"); + if (_discoveryHandler is not null) + { + _discoveryHandler.OnChannelActivated -= OnChannelActivated; + } } catch (Exception e) { - _logger.DebugError("Error during discovery v5 initialization", e); + Logger.Error("Error during discovery v5 cleanup", e); } } - public IAsyncEnumerable DiscoverNodes(CancellationToken token) => _kademliaNodeSource.DiscoverNodes(token); + protected override Task RunDiscoveryAsync(CancellationToken cancellationToken) => + Task.WhenAll(_discv5Adapter.RunAsync(cancellationToken), Kademlia.Run(cancellationToken)); - public async Task StopAsync() + protected override async Task StopAsyncCore() { - try - { - await _stopCts.CancelAsync(); - } - catch (ObjectDisposedException) - { - } - - try - { - if (_runningTask is not null) - { - await _runningTask; - } - } - catch (OperationCanceledException) - { - } - catch (Exception e) - { - if (_logger.IsError) _logger.Error("Error in discovery v5 task", e); - } - PersistKnownEnrs(); await _discv5Adapter.DisposeAsync(); _discoveryHandler?.Close(); - _stopCts.Dispose(); } private void PersistKnownEnrs() @@ -385,7 +362,7 @@ private void PersistKnownEnrs() IWriteBatch? batch = null; try { - foreach (Node node in _kademlia.IterateNodes()) + foreach (Node node in Kademlia.IterateNodes()) { if (string.IsNullOrEmpty(node.Enr)) { @@ -399,7 +376,7 @@ private void PersistKnownEnrs() } catch (Exception e) { - if (_logger.IsDebug) _logger.Debug($"Skipping malformed discv5 ENR while persisting {node}: {e}"); + if (Logger.IsDebug) Logger.Debug($"Skipping malformed discv5 ENR while persisting {node}: {e}"); continue; } @@ -413,9 +390,37 @@ private void PersistKnownEnrs() } } - string IStoppableService.Description => "discv5"; + protected override ValueTask DisposeAsyncCore() => _discv5Services.DisposeAsync(); - public void AddNodeToDiscovery(Node node) => _kademlia.AddOrRefresh(node); + private enum BootNodeAddResult + { + Added, + Duplicate, + Skipped + } + + private sealed class BootNodeStats + { + public int Total { get; private set; } + public int Added { get; private set; } + public int Duplicates { get; private set; } + public int Skipped { get; private set; } - public ValueTask DisposeAsync() => _discv5Services.DisposeAsync(); + public void Record(BootNodeAddResult result) + { + Total++; + switch (result) + { + case BootNodeAddResult.Added: + Added++; + break; + case BootNodeAddResult.Duplicate: + Duplicates++; + break; + case BootNodeAddResult.Skipped: + Skipped++; + break; + } + } + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs deleted file mode 100644 index 3ddfc1184842..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5Report.cs +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Kademlia; -using Nethermind.Logging; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Discv5; - -internal class DiscoveryV5Report -{ - int RecentlyChecked = 0; - int TotalChecked = 0; - - public DiscoveryV5Report(IKademlia kademlia, ILogManager logManager, CancellationToken token) - { - ILogger logger = logManager.GetClassLogger(); - if (!logger.IsDebug) - { - return; - } - - _ = Task.Run(async () => - { - while (!token.IsCancellationRequested) - { - logger.Debug($"Nodes checked: {Interlocked.Exchange(ref RecentlyChecked, 0)}, in total {TotalChecked}. Kademlia table state: {kademlia.IterateNodes().Count()} nodes."); - await Task.Delay(10_000, token); - } - }, token); - } - - public void NodeFound() - { - Interlocked.Increment(ref RecentlyChecked); - Interlocked.Increment(ref TotalChecked); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs index 72fdcbdc8792..21c02daad521 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs @@ -27,15 +27,19 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance Channel channel = Channel.CreateBounded(64); ConcurrentDictionary writtenNodes = new(); + int initialNodes = 0; foreach (Node node in kademlia.IterateNodes()) { - if (!IsSelf(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) + if (!IsExcluded(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) { + initialNodes++; yield return node; } } + if (_logger.IsDebug) _logger.Debug($"Discv5 node source emitted {initialNodes} initial nodes from the routing table."); + kademlia.OnNodeAdded += Handler; try { @@ -51,12 +55,19 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance void Handler(object? _, Node node) { - if (!IsSelf(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) + if (!IsExcluded(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) { - channel.Writer.TryWrite(node); + if (channel.Writer.TryWrite(node)) + { + if (_logger.IsDebug) _logger.Debug($"Discv5 node source queued discovered node {node:s}."); + } + else if (_logger.IsTrace) + { + _logger.Trace($"Discv5 node source queue is full, dropping discovered node {node:s}."); + } } } } - private bool IsSelf(Node node) => node.IdHash.Equals(_currentNodeHash); + private bool IsExcluded(Node node) => node.IsBootnode || node.IdHash.Equals(_currentNodeHash); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index dbd8f151e891..65387f088329 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -29,6 +29,8 @@ public class NettyDiscoveryV5Handler(ILogManager loggerManager) : NettyDiscovery public void InitializeChannel(IChannel channel) => _nettyChannel = channel; + public override void ChannelActive(IChannelHandlerContext context) => OnChannelActivated?.Invoke(this, EventArgs.Empty); + protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket msg) { msg.Retain(); @@ -106,4 +108,6 @@ private void ReleaseQueuedPackets() } public static void Register(IServiceCollection services) => services.AddSingleton(); + + public event EventHandler? OnChannelActivated; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/discv5-bootnodes.json b/src/Nethermind/Nethermind.Network.Discovery/Discv5/discv5-bootnodes.json index 3612dfd4de8d..332e200b5ec4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/discv5-bootnodes.json +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/discv5-bootnodes.json @@ -1,6 +1,6 @@ [ - "enr:-KG4QMOEswP62yzDjSwWS4YEjtTZ5PO6r65CPqYBkgTTkrpaedQ8uEUo1uMALtJIvb2w_WWEVmg5yt1UAuK1ftxUU7QDhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQEnfA2iXNlY3AyNTZrMaEDfol8oLr6XJ7FsdAYE7lpJhKMls4G_v6qQOGKJUWGb_uDdGNwgiMog3VkcIIjKA", - "enr:-KG4QF4B5WrlFcRhUU6dZETwY5ZzAXnA0vGC__L1Kdw602nDZwXSTs5RFXFIFUnbQJmhNGVU6OIX7KVrCSTODsz1tK4DhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQExNYEiXNlY3AyNTZrMaECQmM9vp7KhaXhI-nqL_R0ovULLCFSFTa9CPPSdb1zPX6DdGNwgiMog3VkcIIjKA", + "enr:-Iu4QLm7bZGdAt9NSeJG0cEnJohWcQTQaI9wFLu3Q7eHIDfrI4cwtzvEW3F3VbG9XdFXlrHyFGeXPn9snTCQJ9bnMRABgmlkgnY0gmlwhAOTJQCJc2VjcDI1NmsxoQIZdZD6tDYpkpEfVo5bgiU8MGRjhcOmHGD2nErK0UKRrIN0Y3CCIyiDdWRwgiMo", + "enr:-Iu4QEDJ4Wa_UQNbK8Ay1hFEkXvd8psolVK6OhfTL9irqz3nbXxxWyKwEplPfkju4zduVQj6mMhUCm9R2Lc4YM5jPcIBgmlkgnY0gmlwhANrfESJc2VjcDI1NmsxoQJCYz2-nsqFpeEj6eov9HSi9QssIVIVNr0I89J1vXM9foN0Y3CCIyiDdWRwgiMo", "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", @@ -13,5 +13,7 @@ "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", - "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM" + "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM", + "enr:-IS4QPi-onjNsT5xAIAenhCGTDl4z-4UOR25Uq-3TmG4V3kwB9ljLTb_Kp1wdjHNj-H8VVLRBSSWVZo3GUe3z6k0E-IBgmlkgnY0gmlwhKB3_qGJc2VjcDI1NmsxoQMvAfgB4cJXvvXeM6WbCG86CstbSxbQBSGx31FAwVtOTYN1ZHCCIyg", + "enr:-KG4QPUf8-g_jU-KrwzG42AGt0wWM1BTnQxgZXlvCEIfTQ5hSmptkmgmMbRkpOqv6kzb33SlhPHJp7x4rLWWiVq5lSECgmlkgnY0gmlwhFPlR9KDaXA2kCoGxcAJAAAVAAAAAAAAABCJc2VjcDI1NmsxoQLdUv9Eo9sxCt0tc_CheLOWnX59yHJtkBSOL7kpxdJ6GYN1ZHCCIyiEdWRwNoIjKA" ] diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs new file mode 100644 index 000000000000..e8df95ad5e8c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using DotNetty.Transport.Channels; +using Nethermind.Config; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.ServiceStopper; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery; + +public abstract class KademliaDiscoveryApp( + string description, + INetworkConfig networkConfig, + IProcessExitSource processExitSource, + ILogger logger) : IDiscoveryApp, IAsyncDisposable +{ + private readonly string _description = description; + private readonly INetworkConfig _networkConfig = networkConfig; + private readonly CancellationTokenSource _stopCts = CancellationTokenSource.CreateLinkedTokenSource(processExitSource.Token); + private IKademliaNodeSource? _kademliaNodeSource; + private IKademlia? _kademlia; + private Task? _runningTask; + + protected ILogger Logger { get; } = logger; + + protected IKademlia Kademlia => _kademlia ?? throw new InvalidOperationException("Kademlia services were not initialized."); + + public Task StartAsync() + { + try + { + Initialize(); + return Task.CompletedTask; + } + catch (Exception e) + { + Logger.Error($"Error during {_description} app start process", e); + throw; + } + } + + public async Task StopAsync() + { + DetachEventHandlers(); + + try + { + await _stopCts.CancelAsync(); + } + catch (ObjectDisposedException) + { + } + + try + { + if (_runningTask is not null) + { + await _runningTask; + } + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + if (Logger.IsError) Logger.Error($"Error in {_description} task", e); + } + + try + { + await StopAsyncCore(); + } + finally + { + _stopCts.Dispose(); + } + + if (Logger.IsInfo) Logger.Info($"{_description} shutdown complete. Please wait for all components to close"); + } + + string IStoppableService.Description => _description; + + public abstract void InitializeChannel(IChannel channel); + + public void AddNodeToDiscovery(Node node) => Kademlia.AddOrRefresh(node); + + public IAsyncEnumerable DiscoverNodes(CancellationToken token) + => (_kademliaNodeSource ?? throw new InvalidOperationException("Kademlia services were not initialized.")).DiscoverNodes(token); + + public event EventHandler? NodeRemoved; + + public async ValueTask DisposeAsync() + { + if (_kademlia is not null) + { + _kademlia.OnNodeRemoved -= OnKademliaNodeRemoved; + } + + await DisposeAsyncCore(); + } + + protected void UseKademliaServices(IKademliaNodeSource kademliaNodeSource, IKademlia kademlia) + { + _kademliaNodeSource = kademliaNodeSource; + _kademlia = kademlia; + _kademlia.OnNodeRemoved += OnKademliaNodeRemoved; + } + + protected virtual void Initialize() + { + if (Logger.IsDebug) + { + Logger.Debug($"Discovery : udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); + } + + ThisNodeInfo.AddInfo("Discovery :", $"udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); + } + + protected void OnChannelActivated(object? sender, EventArgs e) + { + if (Logger.IsDebug) Logger.Debug("Activated discovery channel."); + + if (_stopCts.IsCancellationRequested) + { + return; + } + + _runningTask = StartActivationAsync(_stopCts.Token); + } + + protected virtual void DetachEventHandlers() + { + } + + protected virtual Task StopAsyncCore() => Task.CompletedTask; + + protected virtual ValueTask DisposeAsyncCore() => ValueTask.CompletedTask; + + protected abstract Task RunDiscoveryAsync(CancellationToken cancellationToken); + + private async Task StartActivationAsync(CancellationToken cancellationToken) + { + const string faultMessage = "Cannot activate channel."; + + try + { + await Task.Factory.StartNew(static state => ((KademliaDiscoveryApp)state!).ActivateAsync(), this, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); + if (!cancellationToken.IsCancellationRequested && Logger.IsDebug) Logger.Debug($"{_description} App initialized."); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + } + catch (Exception) + { + if (Logger.IsInfo) Logger.Info(faultMessage); + throw; + } + } + + private Task ActivateAsync() => ActivateAsync(_stopCts.Token); + + private async Task ActivateAsync(CancellationToken cancellationToken) + { + try + { + await RunDiscoveryAsync(cancellationToken); + } + catch (OperationCanceledException) + { + if (Logger.IsInfo) Logger.Info($"{_description} App stopped"); + } + catch (Exception e) + { + Logger.DebugError($"Error during {_description} initialization", e); + } + } + + private void OnKademliaNodeRemoved(object? sender, Node node) => NodeRemoved?.Invoke(sender, new NodeEventArgs(node)); +} diff --git a/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs b/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs index 2afe096ec948..57e259de7e7a 100644 --- a/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs @@ -7,7 +7,6 @@ using Nethermind.Config; using Nethermind.Logging; using Nethermind.Network.Config; -using Nethermind.Network.Discovery; using Nethermind.Network.Rlpx; using Nethermind.Stats; using Nethermind.Stats.Model; @@ -20,22 +19,24 @@ namespace Nethermind.Network.Test; public class NodesLoaderTests { private NetworkConfig _networkConfig; - private DiscoveryConfig _discoveryConfig; private INodeStatsManager _statsManager; private INetworkStorage _peerStorage; + private IRlpxHost _rlpxHost; private NodesLoader _loader; [SetUp] public void SetUp() { _networkConfig = new NetworkConfig(); - _discoveryConfig = new DiscoveryConfig(); _statsManager = Substitute.For(); _peerStorage = Substitute.For(); - IRlpxHost rlpxHost = Substitute.For(); - _loader = new NodesLoader(_networkConfig, _statsManager, _peerStorage, rlpxHost, LimboLogs.Instance); + _rlpxHost = Substitute.For(); + _loader = CreateLoader(); } + private NodesLoader CreateLoader(bool loadBootnodesAsPeerCandidates = true) => + new(_networkConfig, _statsManager, _peerStorage, _rlpxHost, LimboLogs.Instance, loadBootnodesAsPeerCandidates); + [Test] public void When_no_peers_then_no_peers_nada_zero() { @@ -62,8 +63,7 @@ public void Can_load_static_nodes() [Test] public void Can_load_bootnodes() { - _discoveryConfig.Bootnodes = new[] { new NetworkNode(enode1String), new NetworkNode(enode2String) }; - _networkConfig.Bootnodes = _discoveryConfig.Bootnodes; + _networkConfig.Bootnodes = new[] { new NetworkNode(enode1String), new NetworkNode(enode2String) }; List nodes = _loader.DiscoverNodes(default).ToBlockingEnumerable().ToList(); Assert.That(nodes.Count, Is.EqualTo(2)); foreach (Node node in nodes) @@ -72,6 +72,17 @@ public void Can_load_bootnodes() } } + [Test] + public void Does_not_load_bootnodes_as_peer_candidates_when_only_discv5_is_enabled() + { + _networkConfig.Bootnodes = new[] { new NetworkNode(enode1String), new NetworkNode(enode2String) }; + _loader = CreateLoader(loadBootnodesAsPeerCandidates: false); + + List nodes = _loader.DiscoverNodes(default).ToBlockingEnumerable().ToList(); + + Assert.That(nodes, Is.Empty); + } + [Test] public void Can_load_persisted() { diff --git a/src/Nethermind/Nethermind.Network/NodesLoader.cs b/src/Nethermind/Nethermind.Network/NodesLoader.cs index d31d433572b1..77ed0d5456cf 100644 --- a/src/Nethermind/Nethermind.Network/NodesLoader.cs +++ b/src/Nethermind/Nethermind.Network/NodesLoader.cs @@ -24,7 +24,8 @@ public class NodesLoader( INodeStatsManager stats, [KeyFilter(DbNames.PeersDb)] INetworkStorage peerStorage, IRlpxHost rlpxHost, - ILogManager logManager) : INodeSource + ILogManager logManager, + bool loadBootnodesAsPeerCandidates = true) : INodeSource { private readonly INetworkConfig _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); private readonly INodeStatsManager _stats = stats ?? throw new ArgumentNullException(nameof(stats)); @@ -37,7 +38,7 @@ public IAsyncEnumerable DiscoverNodes(CancellationToken cancellationToken) List allPeers = []; LoadPeersFromDb(allPeers); - if (!_networkConfig.OnlyStaticPeers) + if (!_networkConfig.OnlyStaticPeers && loadBootnodesAsPeerCandidates) { LoadConfigPeers(allPeers, _networkConfig.Bootnodes, n => { diff --git a/src/Nethermind/Nethermind.Runner/configs/hoodi.json b/src/Nethermind/Nethermind.Runner/configs/hoodi.json index a593098daba8..3de80b80e207 100644 --- a/src/Nethermind/Nethermind.Runner/configs/hoodi.json +++ b/src/Nethermind/Nethermind.Runner/configs/hoodi.json @@ -33,6 +33,20 @@ "Enabled": true }, "Discovery": { - "Bootnodes": "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303,enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303,enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303" + "UseDefaultDiscv5Bootnodes": false, + "Bootnodes": [ + "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", + "enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303", + "enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303", + "enr:-Mq4QLkmuSwbGBUph1r7iHopzRpdqE-gcm5LNZfcE-6T37OCZbRHi22bXZkaqnZ6XdIyEDTelnkmMEQB8w6NbnJUt9GGAZWaowaYh2F0dG5ldHOIABgAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhNEmfKCEcXVpY4IyyIlzZWNwMjU2azGhA0hGa4jZJZYQAS-z6ZFK-m4GCFnWS8wfjO0bpSQn6hyEiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QLVumWTwyOUVS4ajqq8ZuZz2ik6t3Gtq0Ozxqecj0qNZWpMnudcvTs-4jrlwYRQMQwBS8Pvtmu4ZPP2Lx3i2t7YBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhNEmfKCJc2VjcDI1NmsxoQLdRlI8aCa_ELwTJhVN8k7km7IDc3pYu-FMYBs5_FiigIN1ZHCCIyk", + "enr:-LK4QAYuLujoiaqCAs0-qNWj9oFws1B4iy-Hff1bRB7wpQCYSS-IIMxLWCn7sWloTJzC1SiH8Y7lMQ5I36ynGV1ASj4Eh2F0dG5ldHOIYAAAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQOmI5MlAu3f5WEThAYOqoygpS2wYn0XS5NV2aYq7T0a04N0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QIC89sMC0o-irosD4_23lJJ4qCGOvdUz7SmoShWx0k6AaxCFTKviEHa-sa7-EzsiXpDp0qP0xzX6nKdXJX3X-IQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQK_m0f1DzDc9Cjrspm36zuRa7072HSiMGYWLsKiVSbP34N1ZHCCIyk", + "enr:-Ku4QNkWjw5tNzo8DtWqKm7CnDdIq_y7xppD6c1EZSwjB8rMOkSFA1wJPLoKrq5UvA7wcxIotH6Usx3PAugEN2JMncIBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbHuBeJc2VjcDI1NmsxoQP3FwrhFYB60djwRjAoOjttq6du94DtkQuaN99wvgqaIYN1ZHCCIyk", + "enr:-OS4QMJGE13xEROqvKN1xnnt7U-noc51VXyM6wFMuL9LMhQDfo1p1dF_zFdS4OsnXz_vIYk-nQWnqJMWRDKvkSK6_CwDh2F0dG5ldHOIAAAAADAAAACGY2xpZW502IpMaWdodGhvdXNljDcuMC4wLWJldGEuM4RldGgykNLxmX9gAAkQAAgAAAAAAACCaWSCdjSCaXCEhse4F4RxdWljgiMqiXNlY3AyNTZrMaECef77P8k5l3PC_raLw42OAzdXfxeQ-58BJriNaqiRGJSIc3luY25ldHMAg3RjcIIjKIN1ZHCCIyg", + "enr:-LK4QDwhXMitMbC8xRiNL-XGMhRyMSOnxej-zGifjv9Nm5G8EF285phTU-CAsMHRRefZimNI7eNpAluijMQP7NDC8kEMh2F0dG5ldHOIAAAAAAAABgCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAOIT_SJc2VjcDI1NmsxoQMoHWNL4MAvh6YpQeM2SUjhUrLIPsAVPB8nyxbmckC6KIN0Y3CCIyiDdWRwgiMo", + "enr:-LK4QPYl2HnMPQ7b1es6Nf_tFYkyya5bj9IqAKOEj2cmoqVkN8ANbJJJK40MX4kciL7pZszPHw6vLNyeC-O3HUrLQv8Mh2F0dG5ldHOIAAAAAAAAAMCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAMYRG-Jc2VjcDI1NmsxoQPQ35tjr6q1qUqwAnegQmYQyfqxC_6437CObkZneI9n34N0Y3CCIyiDdWRwgiMo", + "enr:-KG4QKRSUi4IOAIK_xt5ERrwW_J47wmNCLWFh7Jo0hFE69drZsiZ5Pb5CEcM_njFTTLlIR6SCf67HTcSV1g6hCXdhWkCgmlkgnY0gmlwhLkvrBODaXA2kCoGxcAWAAAYAAAAAAAAABCJc2VjcDI1NmsxoQPU7g2jQGTz8BYbB2vLTb39S_PrcZAehwMM0b3bWsM5rIN1ZHCCIyiEdWRwNoIjKA" + ] } } From 50e86f90befbe7fa50d498ed298fe521822a3475 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 25 May 2026 14:16:44 +0300 Subject: [PATCH 108/182] Set Ethereum discovery versions --- src/Nethermind/Nethermind.Runner.Test/ConfigFilesTests.cs | 6 +++--- src/Nethermind/Nethermind.Runner/configs/hoodi.json | 1 + src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json | 3 +++ src/Nethermind/Nethermind.Runner/configs/mainnet.json | 5 ++++- .../Nethermind.Runner/configs/mainnet_archive.json | 3 +++ src/Nethermind/Nethermind.Runner/configs/sepolia.json | 5 ++++- .../Nethermind.Runner/configs/sepolia_archive.json | 3 +++ 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Runner.Test/ConfigFilesTests.cs b/src/Nethermind/Nethermind.Runner.Test/ConfigFilesTests.cs index 8ce52606fe9c..bc61edd96f64 100644 --- a/src/Nethermind/Nethermind.Runner.Test/ConfigFilesTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/ConfigFilesTests.cs @@ -153,9 +153,9 @@ public void Json_defaults_are_correct(string configWildcard, bool jsonEnabled) Test(configWildcard, static c => c.Host, "127.0.0.1"); } - [TestCase("sepolia", DiscoveryVersion.V4)] - [TestCase("hoodi", DiscoveryVersion.V4)] - [TestCase("mainnet", DiscoveryVersion.V4)] + [TestCase("sepolia", DiscoveryVersion.V5)] + [TestCase("hoodi", DiscoveryVersion.V5)] + [TestCase("mainnet", DiscoveryVersion.All)] public void Discovery_versions_are_correct(string configWildcard, DiscoveryVersion discoveryVersion) => Test(configWildcard, static c => c.DiscoveryVersion, discoveryVersion); diff --git a/src/Nethermind/Nethermind.Runner/configs/hoodi.json b/src/Nethermind/Nethermind.Runner/configs/hoodi.json index 3de80b80e207..264955a274db 100644 --- a/src/Nethermind/Nethermind.Runner/configs/hoodi.json +++ b/src/Nethermind/Nethermind.Runner/configs/hoodi.json @@ -33,6 +33,7 @@ "Enabled": true }, "Discovery": { + "DiscoveryVersion": "V5", "UseDefaultDiscv5Bootnodes": false, "Bootnodes": [ "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", diff --git a/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json b/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json index 347cae8e380a..63631f796b1d 100644 --- a/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json +++ b/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json @@ -32,5 +32,8 @@ }, "Merge": { "Enabled": true + }, + "Discovery": { + "DiscoveryVersion": "V5" } } diff --git a/src/Nethermind/Nethermind.Runner/configs/mainnet.json b/src/Nethermind/Nethermind.Runner/configs/mainnet.json index 4d40133b4e4a..082006bbb46a 100644 --- a/src/Nethermind/Nethermind.Runner/configs/mainnet.json +++ b/src/Nethermind/Nethermind.Runner/configs/mainnet.json @@ -37,5 +37,8 @@ }, "Merge": { "Enabled": true + }, + "Discovery": { + "DiscoveryVersion": "All" } -} \ No newline at end of file +} diff --git a/src/Nethermind/Nethermind.Runner/configs/mainnet_archive.json b/src/Nethermind/Nethermind.Runner/configs/mainnet_archive.json index 8df9f147d9ac..fb6df756f4b6 100644 --- a/src/Nethermind/Nethermind.Runner/configs/mainnet_archive.json +++ b/src/Nethermind/Nethermind.Runner/configs/mainnet_archive.json @@ -37,5 +37,8 @@ }, "Merge": { "FinalTotalDifficulty": "58750003716598352816469" + }, + "Discovery": { + "DiscoveryVersion": "All" } } diff --git a/src/Nethermind/Nethermind.Runner/configs/sepolia.json b/src/Nethermind/Nethermind.Runner/configs/sepolia.json index 58c4e0d61bdf..b8bda3ae36ac 100644 --- a/src/Nethermind/Nethermind.Runner/configs/sepolia.json +++ b/src/Nethermind/Nethermind.Runner/configs/sepolia.json @@ -38,5 +38,8 @@ }, "Merge": { "Enabled": true + }, + "Discovery": { + "DiscoveryVersion": "V5" } -} \ No newline at end of file +} diff --git a/src/Nethermind/Nethermind.Runner/configs/sepolia_archive.json b/src/Nethermind/Nethermind.Runner/configs/sepolia_archive.json index 5cea60e4653a..1ab682704122 100644 --- a/src/Nethermind/Nethermind.Runner/configs/sepolia_archive.json +++ b/src/Nethermind/Nethermind.Runner/configs/sepolia_archive.json @@ -35,5 +35,8 @@ }, "Pruning": { "Mode": "None" + }, + "Discovery": { + "DiscoveryVersion": "V5" } } From 0e1ba971f5564969e6fbe689c10c4e551e610b61 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 19:32:06 +0300 Subject: [PATCH 109/182] Harden discv5 ENR handling --- .../Nethermind.Kademlia/Hash256XorUtils.cs | 6 +- .../DiscoveryV5AppTests.cs | 44 ++++++++++++++- .../Discv5/Discv5CodecTests.cs | 21 +++++++ .../Discv5/Discv5KademliaAdapterTests.cs | 14 +++++ .../Kademlia/Hash256XorUtilsTests.cs | 11 +++- .../Discv5/DiscoveryV5App.cs | 6 +- .../Discv5/Discv5KademliaAdapter.cs | 44 ++++++++++++++- .../Discv5/Discv5MessageCodec.cs | 15 +---- .../Discv5/Discv5NodeRecordConverter.cs | 17 +++++- .../Discv5/Discv5PacketCodec.cs | 4 +- .../NodeRecordSignerTests.cs | 1 + .../Nethermind.Network.Enr/Ip6Entry.cs | 24 ++++++++ .../Nethermind.Network.Enr/NodeRecord.cs | 55 +++++++++++++++---- .../NodeRecordSigner.cs | 45 +++++++++++++-- .../Nethermind.Network.Enr/Tcp6Entry.cs | 18 ++++++ .../Nethermind.Network.Enr/Udp6Entry.cs | 18 ++++++ .../Nethermind.Network.Stats/Model/Node.cs | 5 +- .../Stats/NodeTests.cs | 9 +++ .../configs/hoodi_archive.json | 17 +++++- 19 files changed, 325 insertions(+), 49 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs create mode 100644 src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs create mode 100644 src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs diff --git a/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs b/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs index 9d922a22257d..21a0fd44cf9d 100644 --- a/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs +++ b/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs @@ -65,12 +65,12 @@ public static KademliaHash XorDistance(KademliaHash hash1, KademliaHash hash2) public static KademliaHash GetRandomHashAtDistance(KademliaHash currentHash, int distance, Random random) { - // TODO: Just add a min/max range per bucket and randomized between them. - if (distance == MaxDistance) + if ((uint)distance > MaxDistance) { - return currentHash; + throw new ArgumentOutOfRangeException(nameof(distance), distance, $"Distance must be between 0 and {MaxDistance}."); } + // TODO: Just add a min/max range per bucket and randomized between them. Span randomized = stackalloc byte[KademliaHash.Length]; random.NextBytes(randomized); return CopyForRandom(currentHash, randomized, MaxDistance - distance); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 4aa508181774..efec422f4653 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -92,7 +92,7 @@ public async Task Teardown() _legacyDiscoveryDb.Dispose(); } - private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true) + private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true, bool includeUdp = true) { NodeRecord enr = new(); enr.SetEntry(IdEntry.Instance); @@ -102,7 +102,23 @@ private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, { enr.SetEntry(new TcpEntry(port)); } - enr.SetEntry(new UdpEntry(udpPort ?? port)); + if (includeUdp) + { + enr.SetEntry(new UdpEntry(udpPort ?? port)); + } + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + + return enr; + } + + private static NodeRecord CreateTestIpv6Enr(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress, int udpPort) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new Ip6Entry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new Udp6Entry(udpPort)); enr.EnrSequence = 1; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); @@ -218,6 +234,30 @@ public void Should_Use_Udp_Port_From_Enr() Assert.That(node!.Port, Is.EqualTo(30304)); } + [Test] + public void Should_Reject_Tcp_Only_Enr() + { + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), includeTcp: true, includeUdp: false); + + bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + + Assert.That(result, Is.False); + Assert.That(node, Is.Null); + } + + [Test] + public void Should_Accept_Ipv6_Enr() + { + NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001); + + bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + + Assert.That(result, Is.True); + Assert.That(node, Is.Not.Null); + Assert.That(node!.Host, Is.EqualTo("2001:4860:4860::8888")); + Assert.That(node.Port, Is.EqualTo(9001)); + } + [Test] public void Should_Use_Udp_Port_From_Configured_Enr_Bootnode() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs index 764d80142875..dbbe29b27a01 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs @@ -7,7 +7,9 @@ using Nethermind.Crypto; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; using NUnit.Framework; +using System; using System.Net; namespace Nethermind.Network.Discovery.Test.Discv5; @@ -169,6 +171,25 @@ public void MessageCodec_Roundtrips_FindNode() Assert.That(decodedFindNode.Distances, Is.EqualTo(message.Distances)); } + [Test] + public void MessageCodec_Rejects_Nodes_With_Invalid_Enr() + { + byte[] invalidRecord = new byte[304]; + invalidRecord[0] = 0xf9; + invalidRecord[1] = 0x01; + invalidRecord[2] = 0x2d; + + Rlp data = Rlp.Encode( + Rlp.Encode(new byte[] { 1 }), + Rlp.Encode(1), + Rlp.Encode(new Rlp(invalidRecord))); + byte[] message = new byte[data.Length + 1]; + message[0] = (byte)Discv5MessageType.Nodes; + data.Bytes.CopyTo(message.AsSpan(1)); + + Assert.That(() => Discv5MessageCodec.Decode(message), Throws.TypeOf()); + } + private static Discv5PacketCodec CreateCodec(PrivateKey privateKey) => new( new InsecureProtectedPrivateKey(privateKey), diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index 5b970419be1a..bc65a3b68c26 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Net; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; @@ -65,6 +66,19 @@ public void GetNodesAtDistances_ShouldRejectInvalidDistance(int distance) Assert.Throws(() => adapter.GetNodesAtDistances([distance])); } + [Test] + public void TryAcceptChallenge_ShouldLimitBurstPerIp() + { + Discv5KademliaAdapter adapter = CreateAdapter(); + IPEndPoint endpoint = IPEndPoint.Parse("192.0.2.1:30303"); + + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.False); + } + private Discv5KademliaAdapter CreateAdapter() => new( new Lazy>(_kademlia), null!, diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs index 756b36357272..42d282a8081d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs @@ -71,7 +71,7 @@ void TestForDistance(int distance) Assert.That(Hash256XorUtils.CalculateLogDistance(randomized, randHash), Is.EqualTo(distance)); } - for (int i = 1; i < 256; i++) + for (int i = 0; i <= 256; i++) { rand = new(0); for (int j = 0; j < 10; j++) @@ -82,6 +82,15 @@ void TestForDistance(int distance) } + [TestCase(-1)] + [TestCase(257)] + public void GetRandomHashAtDistance_ShouldRejectInvalidDistance(int distance) + { + KademliaHash hash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + + Assert.That(() => Hash256XorUtils.GetRandomHashAtDistance(hash, distance, new Random(0)), Throws.InstanceOf()); + } + [TestCase] public void TestDistanceCompare() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index cffa809ddb7b..7d0d40e8505e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -176,14 +176,13 @@ internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? no return false; } - IPAddress? ip = enr.GetObj(EnrContentKey.Ip); + (IPAddress? ip, int? discoveryPort) = Discv5NodeRecordConverter.GetDiscoveryEndpoint(enr); if (ip is null) { if (Logger.IsTrace) Logger.Trace("Enr declined, no IP."); return false; } - int? discoveryPort = GetDiscoveryPort(enr); if (discoveryPort is null) { if (Logger.IsTrace) Logger.Trace("Enr declined, no discovery UDP port."); @@ -209,9 +208,6 @@ internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? no return true; } - private static int? GetDiscoveryPort(NodeRecord enr) => - enr.GetValue(EnrContentKey.Udp) ?? enr.GetValue(EnrContentKey.Tcp); - private static PublicKey? GetPublicKeyFromEnr(NodeRecord enr) => enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index bddf47a423a5..80d357652a07 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -10,6 +10,7 @@ using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; +using Nethermind.Network; using Nethermind.Network.Enr; using Nethermind.Stats.Model; @@ -37,6 +38,9 @@ public class Discv5KademliaAdapter( private const int MaxNodesResponseMessages = 16; private const int MaxNodesResponseRecords = 64; private const long SentChallengeTtlMilliseconds = 60_000; + private static readonly TimeSpan ChallengeRateLimitWindow = TimeSpan.FromMilliseconds(100); + private const int ChallengeRateLimitBurstPerIp = 4; + private const int ChallengeRateLimitFilterSize = 8_192; private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); @@ -52,6 +56,7 @@ public class Discv5KademliaAdapter( private readonly ConcurrentQueue _responseHandlerKeys = new(); private readonly ConcurrentDictionary _knownRecords = new(); private readonly ConcurrentQueue _knownRecordKeys = new(); + private readonly NodeFilter[] _challengeRateLimiters = CreateChallengeRateLimiters(); /// public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = null) @@ -312,9 +317,22 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket, byte[] destinationNodeId) { Hash256 nodeId = new(destinationNodeId); + ChallengeKey challengeKey = new(nodeId, endpoint); + long now = Environment.TickCount64; + if (_sentChallenges.TryGetValue(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) + { + return; + } + + if (!TryAcceptChallenge(endpoint)) + { + if (_logger.IsDebug) _logger.Debug($"Rate limiting discv5 WHOAREYOU challenge to {endpoint}."); + return; + } + ulong enrSequence = TryGetKnownRecord(nodeId, out NodeRecord? record) ? record.EnrSequence : 0UL; byte[] packet = packetCodec.EncodeWhoAreYou(destinationNodeId, requestPacket.Nonce, enrSequence, out Discv5Challenge challenge); - SetSentChallenge(new ChallengeKey(nodeId, endpoint), challenge); + SetSentChallenge(challengeKey, challenge); await discoveryHandler.SendAsync(packet, endpoint); } @@ -535,6 +553,30 @@ private void TrimExpiredChallenges(long now) private static bool IsExpired(SentChallenge challenge, long now) => now - challenge.CreatedAtMilliseconds > SentChallengeTtlMilliseconds; + internal bool TryAcceptChallenge(IPEndPoint endpoint) + { + for (int i = 0; i < _challengeRateLimiters.Length; i++) + { + if (_challengeRateLimiters[i].TryAccept(endpoint.Address, exactOnly: true)) + { + return true; + } + } + + return false; + } + + private static NodeFilter[] CreateChallengeRateLimiters() + { + NodeFilter[] filters = new NodeFilter[ChallengeRateLimitBurstPerIp]; + for (int i = 0; i < filters.Length; i++) + { + filters[i] = NodeFilter.CreateExact(ChallengeRateLimitFilterSize, ChallengeRateLimitWindow); + } + + return filters; + } + private static void SetBounded( ConcurrentDictionary dictionary, ConcurrentQueue insertionOrder, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs index 09b1c7193c70..aa72ae9c21ff 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs @@ -118,27 +118,14 @@ private static Discv5Nodes DecodeNodes(byte[] requestId, ref Rlp.ValueDecoderCon int checkPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(checkPosition); NodeRecord[] records = new NodeRecord[count]; - int validRecords = 0; for (int i = 0; i < count; i++) { ReadOnlySpan record = ctx.PeekNextItem(); - try - { - records[validRecords++] = NodeRecord.FromBytes(record); - } - catch (Exception) - { - } - + records[i] = NodeRecord.FromBytes(record); ctx.SkipItem(); } ctx.Check(checkPosition); - if (validRecords != records.Length) - { - Array.Resize(ref records, validRecords); - } - return new Discv5Nodes(requestId, total, records); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs index d0e367c21ae1..e28e943e6880 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs @@ -16,8 +16,7 @@ public static bool TryGetNodeFromEnr(NodeRecord enr, bool allowNonRoutable, [Not node = null; PublicKey? key = enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); - IPAddress? ip = enr.GetObj(EnrContentKey.Ip); - int? discoveryPort = enr.GetValue(EnrContentKey.Udp) ?? enr.GetValue(EnrContentKey.Tcp); + (IPAddress? ip, int? discoveryPort) = GetDiscoveryEndpoint(enr); if (key is null || ip is null || discoveryPort is null) { return false; @@ -39,4 +38,18 @@ public static bool TryGetNodeFromEnr(NodeRecord enr, bool allowNonRoutable, [Not }; return true; } + + internal static (IPAddress? Ip, int? Port) GetDiscoveryEndpoint(NodeRecord enr) + { + IPAddress? ip = enr.GetObj(EnrContentKey.Ip); + int? udp = enr.GetValue(EnrContentKey.Udp); + if (ip is not null && udp is not null) + { + return (ip, udp); + } + + IPAddress? ip6 = enr.GetObj(EnrContentKey.Ip6); + int? udp6 = enr.GetValue(EnrContentKey.Udp6); + return ip6 is not null && udp6 is not null ? (ip6, udp6) : (null, null); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs index 213f7e7f61cc..f348f96a2f41 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs @@ -52,7 +52,7 @@ public sealed class Discv5PacketCodec( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, INodeRecordProvider nodeRecordProvider, ICryptoRandom cryptoRandom, - IEcdsa ecdsa) + IEcdsa ecdsa) : IDisposable { public const int NonceSize = 12; @@ -80,6 +80,8 @@ public sealed class Discv5PacketCodec( internal byte[] LocalNodeId => _publicKey.Hash.BytesToArray(); + public void Dispose() => _privateKey.Dispose(); + internal byte[] EncodeOrdinary(PublicKey destination, byte[] encryptionKey, Discv5Message message, byte[]? nonce = null) { byte[] actualNonce = nonce ?? CreateNonce(); diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index bfcc92efb4a8..98d4640b24a3 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -124,6 +124,7 @@ public void Can_deserialize_and_verify_real_world_cases(string testCase) Console.WriteLine(testCase); Console.WriteLine(hex); Assert.That(signer.Verify(nodeRecord), Is.True); + Assert.That(nodeRecord.ToRlpBytes(), Is.EqualTo(Bytes.FromHexString(testCase))); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs new file mode 100644 index 000000000000..822f7549fbf3 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Enr; + +/// +/// An entry storing the IPv6 address of the node. +/// +public class Ip6Entry(IPAddress ipAddress) : EnrContentEntry(ipAddress) +{ + public override string Key => EnrContentKey.Ip6; + + protected override int GetRlpLengthOfValue() => 17; + + protected override void EncodeValue(RlpStream rlpStream) + { + Span bytes = stackalloc byte[16]; + Value.MapToIPv6().TryWriteBytes(bytes, out int _); + rlpStream.Encode(bytes); + } +} diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index c41a59a9de1f..28a4c032aefb 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -20,8 +20,12 @@ public class NodeRecord private Hash256? _contentHash; + private Signature? _signature; + private SortedDictionary Entries { get; } = new(System.StringComparer.Ordinal); + internal byte[]? OriginalRlp { get; set; } + /// /// This field is used when this is deserialized and an unknown entry is encountered. /// In such cases we do not know the RLP serialization format of such an entry and we store the original RLP @@ -74,6 +78,11 @@ public Hash256 ContentHash private Hash256 CalculateContentHash() { + if (OriginalContentRlp is not null) + { + return ValueKeccak.Compute(OriginalContentRlp).ToCommitment(); + } + KeccakRlpStream rlpStream = new(); EncodeContent(rlpStream); return rlpStream.GetHash(); @@ -82,7 +91,18 @@ private Hash256 CalculateContentHash() /// /// A signature resulting from a secp256k1 signing of the [seq, k, v, ...] content. /// - public Signature? Signature { get; set; } + public Signature? Signature + { + get => _signature; + set + { + _signature = value; + OriginalRlp = null; + OriginalContentRlp = null; + _enrString = null; + _contentHash = null; + } + } public bool Snap { get; set; } @@ -103,7 +123,9 @@ public static NodeRecord FromEnrString(string enrString) base64 = string.Concat(base64, new string('=', padding)); } - return FromBytes(Convert.FromBase64String(base64)); + NodeRecord nodeRecord = FromBytes(Convert.FromBase64String(base64)); + nodeRecord._enrString = enrString; + return nodeRecord; } public static NodeRecord FromBytes(ReadOnlySpan bytes) @@ -133,6 +155,11 @@ public void SetEntry(EnrContentEntry entry) } Entries[entry.Key] = entry; + OriginalRlp = null; + OriginalContentRlp = null; + _enrString = null; + _contentHash = null; + _signature = null; } /// @@ -195,8 +222,8 @@ private int GetContentLengthWithoutSignature() /// Needed for optimized RLP serialization when a proper length byte array has to be allocated upfront. /// /// Length of the Rlp([signature, seq, k, v, ...]) - public int GetRlpLengthWithSignature() => Rlp.LengthOfSequence( - GetContentLengthWithSignature()); + public int GetRlpLengthWithSignature() => OriginalRlp?.Length ?? Rlp.LengthOfSequence( + GetContentLengthWithSignature()); /// /// Applies Rlp([seq, k, v, ...]]). @@ -217,17 +244,15 @@ private void EncodeContent(RlpStream rlpStream) /// Added here for diagnostic purposes - hes is easier to read and compare. /// /// Rlp([signature, seq, k, v, ...]) as a hex string - public string GetHex() - { - int contentLength = GetContentLengthWithSignature(); - int totalLength = Rlp.LengthOfSequence(contentLength); - RlpStream rlpStream = new(totalLength); - Encode(rlpStream); - return rlpStream.Data.AsSpan().ToHexString(); - } + public string GetHex() => ToRlpBytes().AsSpan().ToHexString(); public byte[] ToRlpBytes() { + if (OriginalRlp is not null) + { + return OriginalRlp.ToArray(); + } + int rlpLength = GetRlpLengthWithSignature(); RlpStream rlpStream = new(rlpLength); Encode(rlpStream); @@ -240,6 +265,12 @@ public byte[] ToRlpBytes() /// An RLP stream to encode the content to. public void Encode(RlpStream rlpStream) { + if (OriginalRlp is not null) + { + rlpStream.Write(OriginalRlp); + return; + } + RequireSignature(); int contentLength = GetContentLengthWithSignature(); diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index c684750e5ea9..f22c9c8b76bb 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -28,6 +28,8 @@ public void Sign(NodeRecord nodeRecord) throw new InvalidOperationException("Cannot sign an ENR without a private key."); } + nodeRecord.OriginalRlp = null; + nodeRecord.OriginalContentRlp = null; nodeRecord.Signature = _ecdsa.Sign(_privateKey, in nodeRecord.ContentHash.ValueHash256); } @@ -55,14 +57,16 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) int recordRlpLength = ctx.ReadSequenceLength(); if (recordRlpLength > 300) throw new RlpException("RLP received for ENR is bigger than 300 bytes"); + int checkPosition = ctx.Position + recordRlpLength; NodeRecord nodeRecord = new(); + byte[]? originalContent = null; ReadOnlySpan sigBytes = ctx.DecodeByteArraySpan(RlpLimit.L65); Signature signature = new(sigBytes, 0); bool canVerify = true; ulong enrSequence = ctx.DecodeULong(); - while (ctx.Position < startPosition + recordRlpLength) + while (ctx.Position < checkPosition) { ReadOnlySpan key = ctx.DecodeByteArraySpan(); switch (key.Length) @@ -72,10 +76,19 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) nodeRecord.SetEntry(IdEntry.Instance); break; case 2 when key.SequenceEqual(EnrContentKey.IpU8): + { ReadOnlySpan ipBytes = ctx.DecodeByteArraySpan(); IPAddress address = new(ipBytes); nodeRecord.SetEntry(new IpEntry(address)); break; + } + case 3 when key.SequenceEqual(EnrContentKey.Ip6U8): + { + ReadOnlySpan ipBytes = ctx.DecodeByteArraySpan(); + IPAddress address = new(ipBytes); + nodeRecord.SetEntry(new Ip6Entry(address)); + break; + } case 3 when key.SequenceEqual(EnrContentKey.EthU8): _ = ctx.ReadSequenceLength(); _ = ctx.ReadSequenceLength(); @@ -84,13 +97,29 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) nodeRecord.SetEntry(new EthEntry(forkHash, nextBlock)); break; case 3 when key.SequenceEqual(EnrContentKey.TcpU8): + { int tcpPort = ctx.DecodePositiveInt(); nodeRecord.SetEntry(new TcpEntry(tcpPort)); break; + } + case 4 when key.SequenceEqual(EnrContentKey.Tcp6U8): + { + int tcpPort = ctx.DecodePositiveInt(); + nodeRecord.SetEntry(new Tcp6Entry(tcpPort)); + break; + } case 3 when key.SequenceEqual(EnrContentKey.UdpU8): + { int udpPort = ctx.DecodePositiveInt(); nodeRecord.SetEntry(new UdpEntry(udpPort)); break; + } + case 4 when key.SequenceEqual(EnrContentKey.Udp6U8): + { + int udpPort = ctx.DecodePositiveInt(); + nodeRecord.SetEntry(new Udp6Entry(udpPort)); + break; + } case 9 when key.SequenceEqual(EnrContentKey.SecP256k1U8): ReadOnlySpan keyBytes = ctx.DecodeByteArraySpan(); CompressedPublicKey reportedKey = new(keyBytes); @@ -102,26 +131,30 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) ctx.SkipItem(); nodeRecord.Snap = true; break; - } + } } + ctx.Check(checkPosition); + int endPosition = ctx.Position; if (!canVerify) { ctx.Position = startPosition; ctx.ReadSequenceLength(); ctx.SkipItem(); // signature - int noSigContentLength = ctx.Length - ctx.Position; + int noSigContentLength = endPosition - ctx.Position; int noSigSequenceLength = Rlp.LengthOfSequence(noSigContentLength); - byte[] originalContent = new byte[noSigSequenceLength]; + originalContent = new byte[noSigSequenceLength]; RlpStream originalContentStream = new(originalContent); originalContentStream.StartSequence(noSigContentLength); originalContentStream.Write(ctx.Read(noSigContentLength)); - ctx.Position = startPosition; - nodeRecord.OriginalContentRlp = originalContentStream.Data.ToArray()!; + ctx.Position = endPosition; + originalContent = originalContentStream.Data.ToArray()!; } nodeRecord.EnrSequence = enrSequence; nodeRecord.Signature = signature; + nodeRecord.OriginalContentRlp = originalContent; + nodeRecord.OriginalRlp = ctx.Data.Slice(startPosition, endPosition - startPosition).ToArray(); return nodeRecord; } diff --git a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs new file mode 100644 index 000000000000..85ab40b277f2 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Enr; + +/// +/// An entry storing TCP IPv6 port number. +/// +public class Tcp6Entry(int portNumber) : EnrContentEntry(portNumber) +{ + public override string Key => EnrContentKey.Tcp6; + + protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); + + protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); +} diff --git a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs new file mode 100644 index 000000000000..864a7efadc33 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Enr; + +/// +/// An entry storing UDP IPv6 port number. +/// +public class Udp6Entry(int portNumber) : EnrContentEntry(portNumber) +{ + public override string Key => EnrContentKey.Udp6; + + protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); + + protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); +} diff --git a/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs b/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs index 56bc5a2e684c..55d91caf798c 100644 --- a/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs +++ b/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs @@ -33,7 +33,7 @@ public sealed class Node : IFormattable, IEquatable /// /// Host part of the network node. /// - public string Host => _host ??= Address?.Address?.MapToIPv4()?.ToString(); + public string Host => _host ??= FormatHost(Address?.Address); private string _host; /// @@ -118,6 +118,9 @@ private void SetIPEndPoint(IPEndPoint address) _paddedPort = null; } + private static string FormatHost(IPAddress address) + => address.IsIPv4MappedToIPv6 ? address.MapToIPv4().ToString() : address.ToString(); + // xxx.xxx.xxx.xxx = 15 private string PaddedHost => _paddedHost ??= Host.PadLeft(15, ' '); private string PaddedPort diff --git a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs index 327bfb2ddef6..9056a391283b 100644 --- a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs @@ -17,6 +17,15 @@ public void Can_parse_ipv6_prefixed_ip() Node node = new(TestItem.PublicKeyA, "::ffff:73.224.122.50", 65535); Assert.That(node.Port, Is.EqualTo(65535)); Assert.That(node.Address.Address.MapToIPv4().ToString(), Is.EqualTo("73.224.122.50")); + Assert.That(node.Host, Is.EqualTo("73.224.122.50")); + } + + [Test] + public void Can_parse_native_ipv6_ip() + { + Node node = new(TestItem.PublicKeyA, "2001:4860:4860::8888", 65535); + Assert.That(node.Port, Is.EqualTo(65535)); + Assert.That(node.Host, Is.EqualTo("2001:4860:4860::8888")); } [Test] diff --git a/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json b/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json index 63631f796b1d..5742d3b372ff 100644 --- a/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json +++ b/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json @@ -34,6 +34,21 @@ "Enabled": true }, "Discovery": { - "DiscoveryVersion": "V5" + "DiscoveryVersion": "V5", + "UseDefaultDiscv5Bootnodes": false, + "Bootnodes": [ + "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", + "enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303", + "enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303", + "enr:-Mq4QLkmuSwbGBUph1r7iHopzRpdqE-gcm5LNZfcE-6T37OCZbRHi22bXZkaqnZ6XdIyEDTelnkmMEQB8w6NbnJUt9GGAZWaowaYh2F0dG5ldHOIABgAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhNEmfKCEcXVpY4IyyIlzZWNwMjU2azGhA0hGa4jZJZYQAS-z6ZFK-m4GCFnWS8wfjO0bpSQn6hyEiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QLVumWTwyOUVS4ajqq8ZuZz2ik6t3Gtq0Ozxqecj0qNZWpMnudcvTs-4jrlwYRQMQwBS8Pvtmu4ZPP2Lx3i2t7YBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhNEmfKCJc2VjcDI1NmsxoQLdRlI8aCa_ELwTJhVN8k7km7IDc3pYu-FMYBs5_FiigIN1ZHCCIyk", + "enr:-LK4QAYuLujoiaqCAs0-qNWj9oFws1B4iy-Hff1bRB7wpQCYSS-IIMxLWCn7sWloTJzC1SiH8Y7lMQ5I36ynGV1ASj4Eh2F0dG5ldHOIYAAAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQOmI5MlAu3f5WEThAYOqoygpS2wYn0XS5NV2aYq7T0a04N0Y3CCIyiDdWRwgiMo", + "enr:-Ku4QIC89sMC0o-irosD4_23lJJ4qCGOvdUz7SmoShWx0k6AaxCFTKviEHa-sa7-EzsiXpDp0qP0xzX6nKdXJX3X-IQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQK_m0f1DzDc9Cjrspm36zuRa7072HSiMGYWLsKiVSbP34N1ZHCCIyk", + "enr:-Ku4QNkWjw5tNzo8DtWqKm7CnDdIq_y7xppD6c1EZSwjB8rMOkSFA1wJPLoKrq5UvA7wcxIotH6Usx3PAugEN2JMncIBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbHuBeJc2VjcDI1NmsxoQP3FwrhFYB60djwRjAoOjttq6du94DtkQuaN99wvgqaIYN1ZHCCIyk", + "enr:-OS4QMJGE13xEROqvKN1xnnt7U-noc51VXyM6wFMuL9LMhQDfo1p1dF_zFdS4OsnXz_vIYk-nQWnqJMWRDKvkSK6_CwDh2F0dG5ldHOIAAAAADAAAACGY2xpZW502IpMaWdodGhvdXNljDcuMC4wLWJldGEuM4RldGgykNLxmX9gAAkQAAgAAAAAAACCaWSCdjSCaXCEhse4F4RxdWljgiMqiXNlY3AyNTZrMaECef77P8k5l3PC_raLw42OAzdXfxeQ-58BJriNaqiRGJSIc3luY25ldHMAg3RjcIIjKIN1ZHCCIyg", + "enr:-LK4QDwhXMitMbC8xRiNL-XGMhRyMSOnxej-zGifjv9Nm5G8EF285phTU-CAsMHRRefZimNI7eNpAluijMQP7NDC8kEMh2F0dG5ldHOIAAAAAAAABgCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAOIT_SJc2VjcDI1NmsxoQMoHWNL4MAvh6YpQeM2SUjhUrLIPsAVPB8nyxbmckC6KIN0Y3CCIyiDdWRwgiMo", + "enr:-LK4QPYl2HnMPQ7b1es6Nf_tFYkyya5bj9IqAKOEj2cmoqVkN8ANbJJJK40MX4kciL7pZszPHw6vLNyeC-O3HUrLQv8Mh2F0dG5ldHOIAAAAAAAAAMCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAMYRG-Jc2VjcDI1NmsxoQPQ35tjr6q1qUqwAnegQmYQyfqxC_6437CObkZneI9n34N0Y3CCIyiDdWRwgiMo", + "enr:-KG4QKRSUi4IOAIK_xt5ERrwW_J47wmNCLWFh7Jo0hFE69drZsiZ5Pb5CEcM_njFTTLlIR6SCf67HTcSV1g6hCXdhWkCgmlkgnY0gmlwhLkvrBODaXA2kCoGxcAWAAAYAAAAAAAAABCJc2VjcDI1NmsxoQPU7g2jQGTz8BYbB2vLTb39S_PrcZAehwMM0b3bWsM5rIN1ZHCCIyiEdWRwNoIjKA" + ] } } From cd4871882285f827f2250d95a935f78835c46b96 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 19:51:23 +0300 Subject: [PATCH 110/182] Drain Kademlia background operations --- .../LookupKNearestNeighbour.cs | 7 + .../Nethermind.Kademlia/NodeHealthTracker.cs | 151 ++++++++++++++---- .../DiscoveryV5AppTests.cs | 24 ++- .../Kademlia/LookupKNearestNeighbourTests.cs | 35 ++++ .../Kademlia/NodeHealthTrackerTests.cs | 36 +++++ .../Discv5/Discv5NodeRecordConverter.cs | 2 +- 6 files changed, 221 insertions(+), 34 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 27051ac8efc2..354a7f10eb16 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -120,6 +120,13 @@ CancellationToken token await Task.WhenAny(worker); Volatile.Write(ref finished, true); await cts.CancelAsync(); + try + { + await Task.WhenAll(worker); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + } return CompileResult(); diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index d17759cb77eb..327dd90b7b80 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -13,14 +13,18 @@ public class NodeHealthTracker( INodeHashProvider nodeHashProvider, IKademliaMessageSender kademliaMessageSender, ILogManager logManager -) : INodeHealthTracker where TNode : notnull +) : INodeHealthTracker, IDisposable where TNode : notnull { private readonly ILogger _logger = logManager.GetClassLogger>(); private readonly ConcurrentDictionary _isRefreshing = new(); + private readonly ConcurrentDictionary _refreshTasks = new(); private readonly LruCache _peerFailures = new(1024, "peer failure"); private readonly KademliaHash _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); private readonly TimeSpan _refreshPingTimeout = config.RefreshPingTimeout; + private readonly CancellationTokenSource _refreshCancellation = new(); + + private int _disposed; private bool SameAsSelf(TNode node) => nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; @@ -29,37 +33,61 @@ private void TryRefresh(TNode toRefresh) KademliaHash nodeHash = nodeHashProvider.GetHash(toRefresh); if (_isRefreshing.TryAdd(nodeHash, true)) { - Task.Run(async () => + if (Volatile.Read(ref _disposed) != 0) { - // First, we delay in case any new message come and clear the refresh task, so we don't need to send any ping. - await Task.Delay(100); - if (!_isRefreshing.ContainsKey(nodeHash)) - { - return; - } - - // OK, fine, we'll ping it. - using CancellationTokenSource cts = new(_refreshPingTimeout); - try - { - await kademliaMessageSender.Ping(toRefresh, cts.Token); - OnIncomingMessageFrom(toRefresh); - } - catch (OperationCanceledException) - { - OnRequestFailed(toRefresh); - } - catch (Exception e) - { - OnRequestFailed(toRefresh); - if (_logger.IsDebug) _logger.Debug($"Error while refreshing node {toRefresh}, {e}"); - } - - if (_isRefreshing.TryRemove(nodeHash, out _)) - { - routingTable.Remove(nodeHash); - } - }); + _isRefreshing.TryRemove(nodeHash, out _); + return; + } + + _refreshTasks[nodeHash] = RefreshAsync(toRefresh, nodeHash, _refreshCancellation.Token); + } + } + + private async Task RefreshAsync(TNode toRefresh, KademliaHash nodeHash, CancellationToken token) + { + try + { + // First, we delay in case any new message come and clear the refresh task, so we don't need to send any ping. + await Task.Delay(100, token); + if (!_isRefreshing.ContainsKey(nodeHash)) + { + return; + } + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + cts.CancelAfter(_refreshPingTimeout); + try + { + await kademliaMessageSender.Ping(toRefresh, cts.Token); + OnIncomingMessageFrom(toRefresh); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + _isRefreshing.TryRemove(nodeHash, out _); + return; + } + catch (OperationCanceledException) + { + OnRequestFailed(toRefresh); + } + catch (Exception e) + { + OnRequestFailed(toRefresh); + if (_logger.IsDebug) _logger.Debug($"Error while refreshing node {toRefresh}, {e}"); + } + + if (_isRefreshing.TryRemove(nodeHash, out _)) + { + routingTable.Remove(nodeHash); + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + _isRefreshing.TryRemove(nodeHash, out _); + } + finally + { + _refreshTasks.TryRemove(nodeHash, out _); } } @@ -109,4 +137,65 @@ public void OnRequestFailed(TNode node) _peerFailures.Set(hash, currentFailure + 1); } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + _refreshCancellation.Cancel(); + Task[] refreshTasks = new Task[_refreshTasks.Count]; + int refreshTaskCount = 0; + foreach ((_, Task refreshTask) in _refreshTasks) + { + if (refreshTaskCount == refreshTasks.Length) + { + Array.Resize(ref refreshTasks, refreshTaskCount + 1); + } + + refreshTasks[refreshTaskCount++] = refreshTask; + } + + if (refreshTaskCount == 0) + { + _refreshCancellation.Dispose(); + return; + } + + if (refreshTaskCount != refreshTasks.Length) + { + Array.Resize(ref refreshTasks, refreshTaskCount); + } + + bool completed = false; + try + { + completed = Task.WaitAll(refreshTasks, _refreshPingTimeout + TimeSpan.FromMilliseconds(500)); + } + catch (AggregateException e) + { + completed = true; + if (!HasOnlyCancellationExceptions(e) && _logger.IsDebug) _logger.Debug($"Error while disposing node health tracker. {e}"); + } + + if (completed) + { + _refreshCancellation.Dispose(); + } + } + + private static bool HasOnlyCancellationExceptions(AggregateException e) + { + foreach (Exception exception in e.InnerExceptions) + { + if (exception is not OperationCanceledException) + { + return false; + } + } + + return true; + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index efec422f4653..dd7b29160bcc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -112,13 +112,20 @@ private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, return enr; } - private static NodeRecord CreateTestIpv6Enr(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress, int udpPort) + private static NodeRecord CreateTestIpv6Enr(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress, int udpPort, bool useUdp6 = true) { NodeRecord enr = new(); enr.SetEntry(IdEntry.Instance); enr.SetEntry(new Ip6Entry(ipAddress)); enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - enr.SetEntry(new Udp6Entry(udpPort)); + if (useUdp6) + { + enr.SetEntry(new Udp6Entry(udpPort)); + } + else + { + enr.SetEntry(new UdpEntry(udpPort)); + } enr.EnrSequence = 1; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); @@ -258,6 +265,19 @@ public void Should_Accept_Ipv6_Enr() Assert.That(node.Port, Is.EqualTo(9001)); } + [Test] + public void Should_Accept_Ipv6_Enr_With_Default_Udp_Port() + { + NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001, useUdp6: false); + + bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + + Assert.That(result, Is.True); + Assert.That(node, Is.Not.Null); + Assert.That(node!.Host, Is.EqualTo("2001:4860:4860::8888")); + Assert.That(node.Port, Is.EqualTo(9001)); + } + [Test] public void Should_Use_Udp_Port_From_Configured_Enr_Bootnode() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index d8215cd2c80a..3c66c1bdbb3f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -94,4 +94,39 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C Assert.That(result, Is.Not.Empty); } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_drain_cancelled_workers_before_returning(CancellationToken token) + { + (LookupKNearestNeighbour lookup, _, _) = + CreateLookup(2, TimeSpan.FromSeconds(10), [Seed1, Seed2, Seed3, N1]); + TaskCompletionSource cancelledWorkerDrained = new(TaskCreationOptions.RunContinuationsAsynchronously); + + _ = await lookup.Lookup( + IdentityNodeHashProvider.ToKademliaHash(Self), + 1, + async (node, findToken) => + { + if (node != Seed1) + { + return []; + } + + try + { + await Task.Delay(Timeout.Infinite, findToken); + return []; + } + catch (OperationCanceledException) + { + await Task.Delay(100, CancellationToken.None); + cancelledWorkerDrained.SetResult(); + throw; + } + }, + token); + + Assert.That(cancelledWorkerDrained.Task.IsCompleted, Is.True); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index 8eb99bf6660d..38bc3a8d4a40 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -92,6 +92,42 @@ public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken t Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); } + [Test] + [CancelAfter(10000)] + public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(CancellationToken token) + { + TaskCompletionSource pingStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource pingCancelled = new(TaskCreationOptions.RunContinuationsAsynchronously); + IKademliaMessageSender sender = Substitute.For>(); + sender.Ping(Stale, Arg.Any()).Returns(async call => + { + CancellationToken pingToken = call.Arg(); + pingStarted.SetResult(); + try + { + await Task.Delay(Timeout.Infinite, pingToken); + } + catch (OperationCanceledException) + { + pingCancelled.SetResult(); + throw; + } + }); + + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + toRefresh: Stale, + refreshPingTimeout: TimeSpan.FromSeconds(10), + sender: sender); + + tracker.OnIncomingMessageFrom(Remote); + await pingStarted.Task.WaitAsync(token); + + tracker.Dispose(); + + await pingCancelled.Task.WaitAsync(token); + Assert.That(routing.RemoveCalls, Does.Not.Contain(ToKademliaHash(ValueKeccak.Compute(Stale)))); + } + [Test] public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs index e28e943e6880..01e8bd2abb4b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs @@ -49,7 +49,7 @@ internal static (IPAddress? Ip, int? Port) GetDiscoveryEndpoint(NodeRecord enr) } IPAddress? ip6 = enr.GetObj(EnrContentKey.Ip6); - int? udp6 = enr.GetValue(EnrContentKey.Udp6); + int? udp6 = enr.GetValue(EnrContentKey.Udp6) ?? udp; return ip6 is not null && udp6 is not null ? (ip6, udp6) : (null, null); } } From 6b37cdf4f40690060a09daafd3a91df2bca6db23 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 19:56:57 +0300 Subject: [PATCH 111/182] Avoid peer penalties on lookup cancellation --- .../LookupKNearestNeighbour.cs | 5 +++++ .../Kademlia/LookupKNearestNeighbourTests.cs | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 354a7f10eb16..7c6e6e018ef7 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -144,6 +144,11 @@ CancellationToken token return (node, ret); } catch (OperationCanceledException) + when (token.IsCancellationRequested) + { + return (node, null); + } + catch (OperationCanceledException) { nodeHealthTracker.OnRequestFailed(node); return (node, null); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 3c66c1bdbb3f..3e2974ea4a75 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -68,6 +68,26 @@ public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, Ca cts.CancelAfter(100); _ = await task; + health.DidNotReceive().OnRequestFailed(Seed1); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(CancellationToken token) + { + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + CreateLookup(1, TimeSpan.FromMilliseconds(50), [Seed1]); + + _ = await lookup.Lookup( + IdentityNodeHashProvider.ToKademliaHash(Seed1), + 8, + async (_, t) => + { + await Task.Delay(Timeout.Infinite, t); + return null; + }, + token); + health.Received().OnRequestFailed(Seed1); } From 6c4838384be3db5557cc471ae160f9089a14532c Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 20:00:11 +0300 Subject: [PATCH 112/182] Validate ENR key ordering and size --- .../NodeRecordSignerTests.cs | 79 +++++++++++++++++++ .../NodeRecordSigner.cs | 11 ++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index 98d4640b24a3..efd5ec63c23d 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -110,6 +110,41 @@ public void Throws_when_record_is_t() Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); } + [Test] + public void Throws_when_encoded_record_is_bigger_than_300_bytes() + { + NodeRecordSigner signer = new(new Ecdsa()); + byte[] filler = FindFillerForOversizedEncodedRecord(); + RlpStream rlpStream = CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), + ("z", stream => stream.Encode(filler), Rlp.LengthOf(filler))); + + Assert.That(rlpStream.Data.Length, Is.GreaterThan(300)); + Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); + } + + [Test] + public void Throws_when_keys_are_not_sorted() + { + NodeRecordSigner signer = new(new Ecdsa()); + RlpStream rlpStream = CreateRecord( + (EnrContentKey.Udp, static stream => stream.Encode(30303), Rlp.LengthOf(30303)), + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))); + + Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); + } + + [Test] + public void Throws_when_keys_are_duplicated() + { + NodeRecordSigner signer = new(new Ecdsa()); + RlpStream rlpStream = CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))); + + Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); + } + [TestCase("f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f")] [TestCase("f897b8406fb9316953b51793ee43316fe14f2d0ac0b356b86815175c6d231840bd6f24e504bfa6492ccc1f4b0853b02ae44fbee861f52044dd08e4a23edf6187ea5e46e71583657468c7c68420c327fc80826964827634826970847f00000189736563703235366b31a102ba4be3a4095b23fe90a850709394476bf23c9788ad124325a6163f342e05a7308374637082765f8375647082765f")] [TestCase("f89fb8401d2ab9d1937f7d3524feec8edb45e3abc4e4a01ca227615502bcad2cd68eaf804fc5865f6a5551bd5c39f56ee4d4c005c69be3efc44f2a9ff312d71de13a62de8207ab83657468c7c6843de1adaf808269648276348269708467e4b73289736563703235366b31a102bb8f962e961a1d82dac4bc32b71e491da35bcd69e18bec31aba9b9fadd0e1a1184736e6170c08374637082765f8375647082765f")] @@ -136,4 +171,48 @@ public void Cannot_verify_when_signature_missing() NodeRecord nodeRecord = new(); Assert.Throws(() => _ = signer.Verify(nodeRecord)); } + + private static RlpStream CreateRecord(params (string Key, Action EncodeValue, int ValueLength)[] entries) + { + byte[] signature = new byte[64]; + int contentLength = Rlp.LengthOf(signature) + Rlp.LengthOf(1UL); + foreach ((string key, _, int valueLength) in entries) + { + contentLength += Rlp.LengthOf(key) + valueLength; + } + + RlpStream rlpStream = new(Rlp.LengthOfSequence(contentLength)); + rlpStream.StartSequence(contentLength); + rlpStream.Encode(signature); + rlpStream.Encode(1UL); + foreach ((string key, Action encodeValue, _) in entries) + { + rlpStream.Encode(key); + encodeValue(rlpStream); + } + + rlpStream.Position = 0; + return rlpStream; + } + + private static byte[] FindFillerForOversizedEncodedRecord() + { + for (int i = 0; i <= 300; i++) + { + byte[] filler = new byte[i]; + int contentLength = + Rlp.LengthOf(new byte[64]) + + Rlp.LengthOf(1UL) + + Rlp.LengthOf(EnrContentKey.Id) + + Rlp.LengthOf("v4") + + Rlp.LengthOf("z") + + Rlp.LengthOf(filler); + if (contentLength <= 300 && Rlp.LengthOfSequence(contentLength) > 300) + { + return filler; + } + } + + throw new InvalidOperationException("Could not create oversized ENR fixture."); + } } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index f22c9c8b76bb..8fa8e1bbd99e 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -55,11 +55,12 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) { int startPosition = ctx.Position; int recordRlpLength = ctx.ReadSequenceLength(); - if (recordRlpLength > 300) - throw new RlpException("RLP received for ENR is bigger than 300 bytes"); int checkPosition = ctx.Position + recordRlpLength; + if (checkPosition - startPosition > 300) + throw new RlpException("RLP received for ENR is bigger than 300 bytes"); NodeRecord nodeRecord = new(); byte[]? originalContent = null; + byte[] previousKey = []; ReadOnlySpan sigBytes = ctx.DecodeByteArraySpan(RlpLimit.L65); Signature signature = new(sigBytes, 0); @@ -69,6 +70,12 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) while (ctx.Position < checkPosition) { ReadOnlySpan key = ctx.DecodeByteArraySpan(); + if (previousKey.Length != 0 && key.SequenceCompareTo(previousKey) <= 0) + { + throw new RlpException("ENR keys must be sorted and unique."); + } + previousKey = key.ToArray(); + switch (key.Length) { case 2 when key.SequenceEqual(EnrContentKey.IdU8): From dd1c35cb66fe1e9ec2043c11fd5cc171bfc87063 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 20:04:38 +0300 Subject: [PATCH 113/182] Require ENR v4 identity --- .../NodeRecordSignerTests.cs | 20 +++++++++++++++++++ .../NodeRecordSigner.cs | 14 ++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index efd5ec63c23d..cce917c39e34 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -145,6 +145,26 @@ public void Throws_when_keys_are_duplicated() Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); } + [Test] + public void Throws_when_id_is_missing() + { + NodeRecordSigner signer = new(new Ecdsa()); + RlpStream rlpStream = CreateRecord( + ("z", static stream => stream.Encode(Array.Empty()), Rlp.LengthOf(Array.Empty()))); + + Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); + } + + [Test] + public void Throws_when_id_is_not_v4() + { + NodeRecordSigner signer = new(new Ecdsa()); + RlpStream rlpStream = CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v5"), Rlp.LengthOf("v5"))); + + Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); + } + [TestCase("f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f")] [TestCase("f897b8406fb9316953b51793ee43316fe14f2d0ac0b356b86815175c6d231840bd6f24e504bfa6492ccc1f4b0853b02ae44fbee861f52044dd08e4a23edf6187ea5e46e71583657468c7c68420c327fc80826964827634826970847f00000189736563703235366b31a102ba4be3a4095b23fe90a850709394476bf23c9788ad124325a6163f342e05a7308374637082765f8375647082765f")] [TestCase("f89fb8401d2ab9d1937f7d3524feec8edb45e3abc4e4a01ca227615502bcad2cd68eaf804fc5865f6a5551bd5c39f56ee4d4c005c69be3efc44f2a9ff312d71de13a62de8207ab83657468c7c6843de1adaf808269648276348269708467e4b73289736563703235366b31a102bb8f962e961a1d82dac4bc32b71e491da35bcd69e18bec31aba9b9fadd0e1a1184736e6170c08374637082765f8375647082765f")] diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index 8fa8e1bbd99e..dfa4894e43c5 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -66,6 +66,7 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) Signature signature = new(sigBytes, 0); bool canVerify = true; + bool hasV4Id = false; ulong enrSequence = ctx.DecodeULong(); while (ctx.Position < checkPosition) { @@ -79,7 +80,13 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) switch (key.Length) { case 2 when key.SequenceEqual(EnrContentKey.IdU8): - ctx.SkipItem(); + ReadOnlySpan id = ctx.DecodeByteArraySpan(); + if (!id.SequenceEqual("v4"u8)) + { + throw new RlpException("Unsupported ENR identity scheme."); + } + + hasV4Id = true; nodeRecord.SetEntry(IdEntry.Instance); break; case 2 when key.SequenceEqual(EnrContentKey.IpU8): @@ -142,6 +149,11 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) } ctx.Check(checkPosition); + if (!hasV4Id) + { + throw new RlpException("ENR is missing id=v4."); + } + int endPosition = ctx.Position; if (!canVerify) { From 6ec92a53bd6acd194fc72368e79b420f915bf183 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 20:12:02 +0300 Subject: [PATCH 114/182] Validate discv5 relayed ENR addresses --- .../Discv5/Discv5KademliaAdapterTests.cs | 46 +++++++++++++++++++ .../Discv5/Discv5KademliaAdapter.cs | 5 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index bc65a3b68c26..00d34e5a6611 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -9,6 +9,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Enr; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -79,6 +80,30 @@ public void TryAcceptChallenge_ShouldLimitBurstPerIp() Assert.That(adapter.TryAcceptChallenge(endpoint), Is.False); } + [Test] + public void NodesResponseHandler_ShouldRejectNonRoutableRecordFromPublicReceiver() + { + Node receiver = new(TestItem.PublicKeyA, "8.8.8.8", 30303); + NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + Discv5KademliaAdapter.NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); + + handler.Handle(new Discv5Nodes([1], 1, [loopbackRecord])); + + Assert.That(handler.GetNodes(), Is.Empty); + } + + [Test] + public void NodesResponseHandler_ShouldAcceptNonRoutableRecordFromNonRoutableReceiver() + { + Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); + NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + Discv5KademliaAdapter.NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); + + handler.Handle(new Discv5Nodes([1], 1, [loopbackRecord])); + + Assert.That(handler.GetNodes(), Has.Length.EqualTo(1)); + } + private Discv5KademliaAdapter CreateAdapter() => new( new Lazy>(_kademlia), null!, @@ -90,4 +115,25 @@ public void TryAcceptChallenge_ShouldLimitBurstPerIp() private static Node CreateNode(PublicKey publicKey, int hostSuffix) => new(publicKey, $"192.168.1.{hostSuffix}", 30303); + + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new UdpEntry(30303)); + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + return enr; + } + + private static Discv5KademliaAdapter.NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) + { + PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); + int distance = Hash256XorUtils.CalculateLogDistance( + KademliaHash.FromBytes(receiver.Id.Hash.Bytes), + KademliaHash.FromBytes(nodeId.Hash.Bytes)); + return new Discv5KademliaAdapter.NodesResponseHandler(receiver, [distance]); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index 80d357652a07..879c1e2b9420 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -662,11 +662,12 @@ public bool Handle(Discv5Message message) } } - private sealed class NodesResponseHandler(Node receiver, int[] requestedDistances) : IResponseHandler + internal sealed class NodesResponseHandler(Node receiver, int[] requestedDistances) : IResponseHandler { private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly List _nodes = []; private readonly HashSet _seenNodeIds = []; + private readonly bool _allowNonRoutableRelays = NodeFilter.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); private int? _total; private int _received; @@ -702,7 +703,7 @@ public bool Handle(Discv5Message message) for (int i = 0; i < nodes.Records.Length && _nodes.Count < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; - if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable: true, out Node? node) || + if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || !_seenNodeIds.Add(node.Id.Hash) || !MatchesRequestedDistance(node, requestedDistances)) { From d510357fb1709ac02529a1abee90e4baf60781a6 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 20:28:42 +0300 Subject: [PATCH 115/182] Harden discv5 node ingestion --- .../DiscoveryV5AppTests.cs | 40 +++++++++++ .../Discv5/Discv5KademliaAdapterTests.cs | 12 ++++ .../Discv5/Discv5NodeSourceTests.cs | 67 ++++++++++++++++++ .../Discv5/DiscoveryV5App.cs | 68 +++++++++++++++++++ .../Discv5/Discv5NodeSource.cs | 61 ++++++++++++++--- .../NodeRecordSignerTests.cs | 10 +++ .../Nethermind.Network.Enr/NodeRecord.cs | 8 ++- 7 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index dd7b29160bcc..4abe0136eab7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -132,6 +132,11 @@ private static NodeRecord CreateTestIpv6Enr(Nethermind.Crypto.PrivateKey private return enr; } + private static NodeRecord CreateEnrForAddress(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress) => + ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 + ? CreateTestIpv6Enr(privateKey, ipAddress, 30303) + : CreateTestEnr(privateKey, ipAddress); + [Test] public void Should_Migrate_Correctly() { @@ -217,6 +222,41 @@ public void Should_Accept_Private_Ip_Enr_On_Private_Deployment() Assert.That(node!.Host, Is.EqualTo(IPAddress.Loopback.ToString())); } + [TestCase("0.1.2.3")] + [TestCase("192.0.0.1")] + [TestCase("192.0.2.1")] + [TestCase("198.18.0.1")] + [TestCase("198.51.100.1")] + [TestCase("203.0.113.1")] + [TestCase("240.0.0.1")] + [TestCase("64:ff9b::1")] + [TestCase("100::1")] + [TestCase("2001:db8::1")] + [TestCase("2002::1")] + [TestCase("3fff::1")] + public void Should_Reject_Special_Use_Ip_Enr(string ip) + { + NodeRecord enr = CreateEnrForAddress(TestItem.PrivateKeyA, IPAddress.Parse(ip)); + + bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + + Assert.That(result, Is.False); + Assert.That(node, Is.Null); + } + + [TestCase("192.0.2.1")] + [TestCase("2001:db8::1")] + public void Should_Reject_Special_Use_Ip_Enr_On_Private_Deployment(string ip) + { + DiscoveryV5App privateDiscoveryApp = CreateDiscoveryV5App(IPAddress.Loopback); + NodeRecord enr = CreateEnrForAddress(TestItem.PrivateKeyA, IPAddress.Parse(ip)); + + bool result = privateDiscoveryApp.TryGetNodeFromEnr(enr, out Node? node); + + Assert.That(result, Is.False); + Assert.That(node, Is.Null); + } + [Test] public void Should_Accept_Public_Ip_Enr() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index 00d34e5a6611..ef081cddf473 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -104,6 +104,18 @@ public void NodesResponseHandler_ShouldAcceptNonRoutableRecordFromNonRoutableRec Assert.That(handler.GetNodes(), Has.Length.EqualTo(1)); } + [Test] + public void NodesResponseHandler_ShouldRejectSpecialUseRecordFromNonRoutableReceiver() + { + Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); + NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); + Discv5KademliaAdapter.NodesResponseHandler handler = CreateNodesResponseHandler(receiver, documentationRecord); + + handler.Handle(new Discv5Nodes([1], 1, [documentationRecord])); + + Assert.That(handler.GetNodes(), Is.Empty); + } + private Discv5KademliaAdapter CreateAdapter() => new( new Lazy>(_kademlia), null!, diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs new file mode 100644 index 000000000000..f7e12414204b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Kademlia; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv5; +using Nethermind.Stats.Model; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv5; + +public class Discv5NodeSourceTests +{ + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_ShouldNotRetainDroppedNodesInRecentDedupe(CancellationToken token) + { + IKademlia kademlia = Substitute.For>(); + kademlia.IterateNodes().Returns(Array.Empty()); + Discv5NodeSource source = new( + kademlia, + new KademliaConfig { CurrentNodeId = CreateNode(0) }, + LimboLogs.Instance); + + await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); + ValueTask firstMove = enumerator.MoveNextAsync(); + await Task.Yield(); + Node firstNode = CreateNode(1); + RaiseNode(kademlia, firstNode); + + Assert.That(await firstMove.AsTask(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo(firstNode)); + + for (int i = 2; i < 66; i++) + { + RaiseNode(kademlia, CreateNode(i)); + } + + Node droppedNode = CreateNode(100); + RaiseNode(kademlia, droppedNode); + + for (int i = 2; i < 66; i++) + { + Assert.That(await enumerator.MoveNextAsync(), Is.True); + } + + ValueTask droppedMove = enumerator.MoveNextAsync(); + await Task.Yield(); + RaiseNode(kademlia, droppedNode); + + Assert.That(await droppedMove.AsTask(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo(droppedNode)); + } + + private static Node CreateNode(int index) => + new(TestItem.PublicKeys[index], $"192.168.1.{index + 1}", 30303); + + private static void RaiseNode(IKademlia kademlia, Node node) => + kademlia.OnNodeAdded += Raise.Event>(null, node); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 7d0d40e8505e..420f5e931808 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.CompilerServices; @@ -223,12 +224,79 @@ internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allo return false; } + if (IsSpecialUseAddress(ipAddress)) + { + return false; + } + return allowNonRoutable || !NodeFilter.IsLoopbackOrPrivateOrLinkLocal(ipAddress); } internal static bool IsDiscoveryAddressRoutable(IPAddress ipAddress) => IsDiscoveryAddressAcceptable(ipAddress, allowNonRoutable: false); + private static bool IsSpecialUseAddress(IPAddress ipAddress) + { + Span bytes = stackalloc byte[16]; + if (!ipAddress.TryWriteBytes(bytes, out int written)) + { + return true; + } + + if (written == 4) + { + return IsSpecialUseIPv4(bytes[..4]); + } + + if (IsIPv4MappedIPv6(bytes)) + { + return IsSpecialUseIPv4(bytes[12..]); + } + + return IsSpecialUseIPv6(bytes); + } + + private static bool IsSpecialUseIPv4(ReadOnlySpan bytes) + { + uint v4 = BinaryPrimitives.ReadUInt32BigEndian(bytes); + byte a = (byte)(v4 >> 24); + byte b = (byte)(v4 >> 16); + byte c = (byte)(v4 >> 8); + + return a == 0 // 0.0.0.0/8 + || a == 192 && b == 0 && c is 0 or 2 // 192.0.0.0/24, 192.0.2.0/24 + || a == 192 && b == 88 && c == 99 // 192.88.99.0/24 + || a == 198 && b is 18 or 19 // 198.18.0.0/15 + || a == 198 && b == 51 && c == 100 // 198.51.100.0/24 + || a == 203 && b == 0 && c == 113 // 203.0.113.0/24 + || a >= 240; // 240.0.0.0/4 + } + + private static bool IsSpecialUseIPv6(ReadOnlySpan bytes) + => bytes[0] == 0x00 && bytes[1] == 0x64 && bytes[2] == 0xff && bytes[3] == 0x9b && IsZero(bytes[4..12]) // 64:ff9b::/96 + || bytes[0] == 0x00 && bytes[1] == 0x64 && bytes[2] == 0xff && bytes[3] == 0x9b && bytes[4] == 0x00 && bytes[5] == 0x01 // 64:ff9b:1::/48 + || bytes[0] == 0x01 && IsZero(bytes[1..8]) // 100::/64 + || bytes[0] == 0x20 && bytes[1] == 0x01 && (bytes[2] & 0xfe) == 0x00 // 2001::/23 + || bytes[0] == 0x20 && bytes[1] == 0x01 && bytes[2] == 0x0d && bytes[3] == 0xb8 // 2001:db8::/32 + || bytes[0] == 0x20 && bytes[1] == 0x02 // 2002::/16 + || bytes[0] == 0x3f && bytes[1] == 0xff && (bytes[2] & 0xf0) == 0x00; // 3fff::/20 + + private static bool IsIPv4MappedIPv6(ReadOnlySpan bytes) + => IsZero(bytes[..10]) && bytes[10] == 0xff && bytes[11] == 0xff; + + private static bool IsZero(ReadOnlySpan bytes) + { + for (int i = 0; i < bytes.Length; i++) + { + if (bytes[i] != 0) + { + return false; + } + } + + return true; + } + private static bool ShouldAcceptNonRoutableEnrs(IPAddress externalIp) => !IPAddress.Any.Equals(externalIp) && !IPAddress.None.Equals(externalIp) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs index 21c02daad521..196111706092 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading.Channels; using Nethermind.Core.Crypto; @@ -18,20 +18,25 @@ public class Discv5NodeSource( ILogManager logManager) : IKademliaNodeSource { + private const int ChannelCapacity = 64; + private readonly ILogger _logger = logManager.GetClassLogger(); private readonly Hash256 _currentNodeHash = kademliaConfig.CurrentNodeId.IdHash; + private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256XorUtils.MaxDistance); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug("Starting discv5 node source"); - Channel channel = Channel.CreateBounded(64); - ConcurrentDictionary writtenNodes = new(); + Channel channel = Channel.CreateBounded(ChannelCapacity); + LinkedList recentlyWrittenNodes = []; + Dictionary> writtenNodes = []; + object writtenNodesLock = new(); int initialNodes = 0; foreach (Node node in kademlia.IterateNodes()) { - if (!IsExcluded(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) + if (!IsExcluded(node) && TryReserveNode(node.IdHash)) { initialNodes++; yield return node; @@ -55,15 +60,53 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance void Handler(object? _, Node node) { - if (!IsExcluded(node) && writtenNodes.TryAdd(node.IdHash, node.IdHash)) + if (IsExcluded(node) || !TryReserveNode(node.IdHash)) + { + return; + } + + if (channel.Writer.TryWrite(node)) { - if (channel.Writer.TryWrite(node)) + if (_logger.IsDebug) _logger.Debug($"Discv5 node source queued discovered node {node:s}."); + return; + } + + ReleaseReservedNode(node.IdHash); + if (_logger.IsTrace) + { + _logger.Trace($"Discv5 node source queue is full, dropping discovered node {node:s}."); + } + } + + bool TryReserveNode(Hash256 nodeId) + { + lock (writtenNodesLock) + { + if (writtenNodes.ContainsKey(nodeId)) { - if (_logger.IsDebug) _logger.Debug($"Discv5 node source queued discovered node {node:s}."); + return false; } - else if (_logger.IsTrace) + + LinkedListNode listNode = recentlyWrittenNodes.AddLast(nodeId); + writtenNodes.Add(nodeId, listNode); + while (writtenNodes.Count > _recentNodeLimit) + { + LinkedListNode oldestNode = recentlyWrittenNodes.First!; + recentlyWrittenNodes.RemoveFirst(); + writtenNodes.Remove(oldestNode.Value); + } + + return true; + } + } + + void ReleaseReservedNode(Hash256 nodeId) + { + lock (writtenNodesLock) + { + if (writtenNodes.Remove(nodeId, out LinkedListNode? listNode)) { - _logger.Trace($"Discv5 node source queue is full, dropping discovered node {node:s}."); + recentlyWrittenNodes.Remove(listNode); } } } diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index cce917c39e34..7ffbc55d2430 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -182,6 +182,16 @@ public void Can_deserialize_and_verify_real_world_cases(string testCase) Assert.That(nodeRecord.ToRlpBytes(), Is.EqualTo(Bytes.FromHexString(testCase))); } + [Test] + public void FromBytes_throws_when_record_has_trailing_bytes() + { + byte[] recordBytes = Bytes.FromHexString( + "f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f"); + byte[] recordWithTrailingBytes = [.. recordBytes, 0x80]; + + Assert.That(() => NodeRecord.FromBytes(recordWithTrailingBytes), Throws.TypeOf()); + } + [Test] public void Cannot_verify_when_signature_missing() diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 28a4c032aefb..a16bfab9e230 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -134,7 +134,13 @@ public static NodeRecord FromBytes(ReadOnlySpan bytes) public static NodeRecord FromBytes(byte[] bytes) { NodeRecordSigner signer = new(new Ecdsa()); - NodeRecord nodeRecord = signer.Deserialize(new RlpStream(bytes)); + RlpStream stream = new(bytes); + NodeRecord nodeRecord = signer.Deserialize(stream); + if (stream.Position != stream.Length) + { + throw new RlpException("Unexpected trailing bytes in ENR."); + } + if (!signer.Verify(nodeRecord)) { throw new RlpException("Invalid ENR signature."); From b3c20ac763079198f42b61d72a280dcdfa8f0cdf Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 20:43:07 +0300 Subject: [PATCH 116/182] Harden discv5 ENR relay validation --- .../DiscoveryV5AppTests.cs | 4 + .../Discv5/Discv5KademliaAdapterTests.cs | 62 ++++++++ .../Discv5/DiscoveryV5App.cs | 5 +- .../Discv5/Discv5KademliaAdapter.cs | 144 ++++++++++++------ 4 files changed, 164 insertions(+), 51 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 4abe0136eab7..0ba03284438e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -225,10 +225,14 @@ public void Should_Accept_Private_Ip_Enr_On_Private_Deployment() [TestCase("0.1.2.3")] [TestCase("192.0.0.1")] [TestCase("192.0.2.1")] + [TestCase("192.31.196.1")] + [TestCase("192.52.193.1")] [TestCase("198.18.0.1")] + [TestCase("192.175.48.1")] [TestCase("198.51.100.1")] [TestCase("203.0.113.1")] [TestCase("240.0.0.1")] + [TestCase("::ffff:224.0.0.1")] [TestCase("64:ff9b::1")] [TestCase("100::1")] [TestCase("2001:db8::1")] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index ef081cddf473..b1fa2d893b39 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -116,6 +116,68 @@ public void NodesResponseHandler_ShouldRejectSpecialUseRecordFromNonRoutableRece Assert.That(handler.GetNodes(), Is.Empty); } + [Test] + public void IsAcceptableNodeRecord_ShouldRejectSpecialUseRecord() + { + NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); + + Assert.That( + Discv5KademliaAdapter.IsAcceptableNodeRecord( + documentationRecord, + TestItem.PrivateKeyB.PublicKey.Hash, + allowNonRoutable: true), + Is.False); + } + + [Test] + public void IsAcceptableNodeRecord_ShouldRejectNodeIdMismatch() + { + NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.8.8")); + + Assert.That( + Discv5KademliaAdapter.IsAcceptableNodeRecord( + record, + TestItem.PrivateKeyA.PublicKey.Hash, + allowNonRoutable: false), + Is.False); + } + + [Test] + public void IsAcceptableNodeRecord_ShouldAllowNonRoutableWhenRequested() + { + NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + + Assert.That( + Discv5KademliaAdapter.IsAcceptableNodeRecord( + loopbackRecord, + TestItem.PrivateKeyB.PublicKey.Hash, + allowNonRoutable: true), + Is.True); + } + + [Test] + public void BoundedMap_ShouldRemoveInsertionOrderEntriesOnRemove() + { + Discv5KademliaAdapter.BoundedMap map = new(2); + map.Set(1, "a"); + map.Set(2, "b"); + + Assert.That(map.TryRemove(1, out string? removed), Is.True); + Assert.That(removed, Is.EqualTo("a")); + + map.Set(3, "c"); + map.Set(4, "d"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(map.Snapshot(), Has.Length.EqualTo(2)); + Assert.That(map.TryGetValue(1, out _), Is.False); + Assert.That(map.TryGetValue(2, out _), Is.False); + Assert.That(map.TryGetValue(3, out _), Is.True); + Assert.That(map.TryGetValue(4, out _), Is.True); + } + } + private Discv5KademliaAdapter CreateAdapter() => new( new Lazy>(_kademlia), null!, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 420f5e931808..e2a3d439165d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -265,11 +265,14 @@ private static bool IsSpecialUseIPv4(ReadOnlySpan bytes) return a == 0 // 0.0.0.0/8 || a == 192 && b == 0 && c is 0 or 2 // 192.0.0.0/24, 192.0.2.0/24 + || a == 192 && b == 31 && c == 196 // 192.31.196.0/24 + || a == 192 && b == 52 && c == 193 // 192.52.193.0/24 || a == 192 && b == 88 && c == 99 // 192.88.99.0/24 + || a == 192 && b == 175 && c == 48 // 192.175.48.0/24 || a == 198 && b is 18 or 19 // 198.18.0.0/15 || a == 198 && b == 51 && c == 100 // 198.51.100.0/24 || a == 203 && b == 0 && c == 113 // 203.0.113.0/24 - || a >= 240; // 240.0.0.0/4 + || a >= 224; // 224.0.0.0/4, 240.0.0.0/4 } private static bool IsSpecialUseIPv6(ReadOnlySpan bytes) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index 879c1e2b9420..f3ac2f037a51 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -45,17 +44,12 @@ public class Discv5KademliaAdapter( private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly ConcurrentDictionary _sessions = new(); - private readonly ConcurrentQueue _sessionKeys = new(); - private readonly ConcurrentDictionary _sentChallenges = new(); - private readonly ConcurrentQueue _sentChallengeKeys = new(); + private readonly BoundedMap _sessions = new(MaxSessions); + private readonly BoundedMap _sentChallenges = new(MaxSentChallenges); private long _lastSentChallengeTrimMilliseconds; - private readonly ConcurrentDictionary _pendingByNonce = new(); - private readonly ConcurrentQueue _pendingNonceKeys = new(); - private readonly ConcurrentDictionary _responseHandlers = new(); - private readonly ConcurrentQueue _responseHandlerKeys = new(); - private readonly ConcurrentDictionary _knownRecords = new(); - private readonly ConcurrentQueue _knownRecordKeys = new(); + private readonly BoundedMap _pendingByNonce = new(MaxPendingRequests); + private readonly BoundedMap _responseHandlers = new(MaxResponseHandlers); + private readonly BoundedMap _knownRecords = new(MaxKnownRecords); private readonly NodeFilter[] _challengeRateLimiters = CreateChallengeRateLimiters(); /// @@ -154,7 +148,7 @@ private async Task SendRequest( CancellationToken token) { ResponseKey responseKey = new(receiver.Id.Hash, RequestIdToString(request.RequestId), responseType); - SetBounded(_responseHandlers, _responseHandlerKeys, responseKey, responseHandler, MaxResponseHandlers); + _responseHandlers.Set(responseKey, responseHandler); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); timeoutCts.CancelAfter(timeout); @@ -193,7 +187,7 @@ private async Task SendRequest( byte[] encryptionKey = cryptoRandom.GenerateRandomBytes(16); PendingRequest pendingRequest = new(receiver, message); PendingNonceKey pendingNonceKey = new(receiver.Address, NonceToString(nonce)); - SetBounded(_pendingByNonce, _pendingNonceKeys, pendingNonceKey, pendingRequest, MaxPendingRequests); + _pendingByNonce.Set(pendingNonceKey, pendingRequest); byte[] initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); try @@ -307,6 +301,11 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can if (nodeRecord is not null) { + if (!IsAcceptableNodeRecord(nodeRecord, nodeId, NodeFilter.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + { + return; + } + SetKnownRecord(nodeId, nodeRecord); } @@ -434,7 +433,9 @@ private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) continue; } - NodeRecord? record = GetFindNodeRecord(node); + NodeRecord? record = GetFindNodeRecord( + node, + allowNonRoutableRelays: NodeFilter.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address)); if (record is not null) { result.Add(record); @@ -445,16 +446,17 @@ private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) return [.. result]; } - private NodeRecord? GetFindNodeRecord(Node node) + private NodeRecord? GetFindNodeRecord(Node node, bool allowNonRoutableRelays) { if (TryGetKnownRecord(node.Id.Hash, out NodeRecord? knownRecord)) { - return knownRecord; + return IsAcceptableNodeRecord(knownRecord, node.Id.Hash, allowNonRoutableRelays) ? knownRecord : null; } try { - return NodeRecord.FromEnrString(node.Enr); + NodeRecord record = NodeRecord.FromEnrString(node.Enr); + return IsAcceptableNodeRecord(record, node.Id.Hash, allowNonRoutableRelays) ? record : null; } catch (Exception e) { @@ -472,7 +474,11 @@ private void RegisterKnownRecord(Node node) try { - SetKnownRecord(node.Id.Hash, NodeRecord.FromEnrString(node.Enr)); + NodeRecord record = NodeRecord.FromEnrString(node.Enr); + if (IsAcceptableNodeRecord(record, node.Id.Hash, NodeFilter.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address))) + { + SetKnownRecord(node.Id.Hash, record); + } } catch (Exception e) { @@ -513,18 +519,22 @@ private byte[] CreateRequestId() private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGetValue(sessionKey, out session); private void SetSession(SessionKey sessionKey, Discv5Session session) - => SetBounded(_sessions, _sessionKeys, sessionKey, session, MaxSessions); + => _sessions.Set(sessionKey, session); private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGetValue(nodeId, out record); private void SetKnownRecord(Hash256 nodeId, NodeRecord record) - => SetBounded(_knownRecords, _knownRecordKeys, nodeId, record, MaxKnownRecords); + => _knownRecords.Set(nodeId, record); + + internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable) + => Discv5NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable, out Node? node) && + node.Id.Hash.Equals(expectedNodeId); private void SetSentChallenge(ChallengeKey challengeKey, Discv5Challenge challenge) { long now = Environment.TickCount64; TryTrimExpiredChallenges(now); - SetBounded(_sentChallenges, _sentChallengeKeys, challengeKey, new SentChallenge(challenge, now), MaxSentChallenges); + _sentChallenges.Set(challengeKey, new SentChallenge(challenge, now)); } private void TryTrimExpiredChallenges(long now) @@ -541,7 +551,7 @@ private void TryTrimExpiredChallenges(long now) private void TrimExpiredChallenges(long now) { - foreach (KeyValuePair kv in _sentChallenges) + foreach (KeyValuePair kv in _sentChallenges.Snapshot()) { if (IsExpired(kv.Value, now)) { @@ -577,50 +587,84 @@ private static NodeFilter[] CreateChallengeRateLimiters() return filters; } - private static void SetBounded( - ConcurrentDictionary dictionary, - ConcurrentQueue insertionOrder, - TKey key, - TValue value, - int maxCount) + internal sealed class BoundedMap(int maxCount) where TKey : notnull + where TValue : notnull { - if (dictionary.TryAdd(key, value)) + private readonly object _lock = new(); + private readonly Dictionary _items = []; + private readonly LinkedList _insertionOrder = []; + private readonly Dictionary> _insertionNodes = []; + + public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) { - insertionOrder.Enqueue(key); + lock (_lock) + { + if (_items.TryGetValue(key, out TValue? found)) + { + value = found; + return true; + } + + value = default; + return false; + } } - else + + public void Set(TKey key, TValue value) { - dictionary[key] = value; - } + lock (_lock) + { + if (_items.ContainsKey(key)) + { + _items[key] = value; + return; + } - TrimBounded(dictionary, insertionOrder, maxCount); - } + _items.Add(key, value); + _insertionNodes.Add(key, _insertionOrder.AddLast(key)); + Trim(); + } + } - private static void TrimBounded( - ConcurrentDictionary dictionary, - ConcurrentQueue insertionOrder, - int maxCount) - where TKey : notnull - { - while (dictionary.Count > maxCount && insertionOrder.TryDequeue(out TKey? key)) + public bool TryRemove(TKey key, [NotNullWhen(true)] out TValue? value) { - dictionary.TryRemove(key, out _); + lock (_lock) + { + if (!_items.TryGetValue(key, out TValue? found)) + { + value = default; + return false; + } + + _items.Remove(key); + if (_insertionNodes.Remove(key, out LinkedListNode? node)) + { + _insertionOrder.Remove(node); + } + + value = found; + return true; + } } - if (dictionary.Count <= maxCount) + public KeyValuePair[] Snapshot() { - return; + lock (_lock) + { + return [.. _items]; + } } - foreach (KeyValuePair kv in dictionary) + private void Trim() { - if (dictionary.Count <= maxCount) + while (_items.Count > maxCount) { - return; + LinkedListNode oldest = _insertionOrder.First!; + _insertionOrder.RemoveFirst(); + _insertionNodes.Remove(oldest.Value); + _items.Remove(oldest.Value); } - - dictionary.TryRemove(kv.Key, out _); } } From 1abb491a4f9b6c1c6f337fd574f9c490080fa592 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 20:50:43 +0300 Subject: [PATCH 117/182] Recover stale discv5 sessions --- .../Discv5/Discv5WireTests.cs | 37 +++++++++++++++++++ .../Discv5/Discv5KademliaAdapter.cs | 17 +++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs index c912d1259ef3..84899f0440b0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs @@ -54,6 +54,43 @@ public async Task Ping_Completes_After_WhoAreYou_Handshake() peerB.Kademlia.Received().AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyA.PublicKey) && !string.IsNullOrEmpty(node.Enr))); } + [Test] + public async Task Ping_Rehandshakes_After_RemoteSessionLost() + { + IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); + IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); + TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) + { + Enr = peerB.NodeRecordProvider.Current.EnrString + }; + + using CancellationTokenSource cancellationSourceA = new(10_000); + using CancellationTokenSource cancellationSourceB = new(10_000); + Task runA = peerA.Adapter.RunAsync(cancellationSourceA.Token); + Task runB = peerB.Adapter.RunAsync(cancellationSourceB.Token); + + Task firstPing = peerA.Adapter.Ping(nodeB, cancellationSourceA.Token); + await PumpUntilComplete(firstPing, peerA, peerB, cancellationSourceA.Token); + await firstPing; + + await cancellationSourceB.CancelAsync(); + await runB; + + TestPeer restartedPeerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + using CancellationTokenSource cancellationSourceRestartedB = new(10_000); + Task runRestartedB = restartedPeerB.Adapter.RunAsync(cancellationSourceRestartedB.Token); + + Task secondPing = peerA.Adapter.Ping(nodeB, cancellationSourceA.Token); + await PumpUntilComplete(secondPing, peerA, restartedPeerB, cancellationSourceA.Token); + await secondPing; + + await cancellationSourceA.CancelAsync(); + await cancellationSourceRestartedB.CancelAsync(); + await Task.WhenAll(runA, runRestartedB); + } + [Test] public async Task FindNeighbours_Returns_Records_At_Requested_Distance() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index f3ac2f037a51..493171eb8069 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -178,9 +178,20 @@ private async Task SendRequest( SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); if (TryGetSession(sessionKey, out Discv5Session? session)) { - byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, session.GetNextNonce(cryptoRandom)); - await discoveryHandler.SendAsync(packet, receiver.Address); - return null; + byte[] sessionNonce = session.GetNextNonce(cryptoRandom); + PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceToString(sessionNonce)); + _pendingByNonce.Set(sessionPendingNonceKey, new PendingRequest(receiver, message)); + byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, sessionNonce); + try + { + await discoveryHandler.SendAsync(packet, receiver.Address); + return sessionPendingNonceKey; + } + catch + { + _pendingByNonce.TryRemove(sessionPendingNonceKey, out _); + throw; + } } byte[] nonce = cryptoRandom.GenerateRandomBytes(Discv5PacketCodec.NonceSize); From c105bd091a02d1d0cde879ae68efc13a5cb247fd Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 21:03:58 +0300 Subject: [PATCH 118/182] Gate discv5 shared node ingestion --- .../DiscoveryV5AppTests.cs | 83 ++++++++++++++++++- .../Discv5/DiscoveryV5App.cs | 29 +++++++ .../KademliaDiscoveryApp.cs | 2 +- 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 0ba03284438e..877d12d988ed 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -4,17 +4,21 @@ using Autofac; using Autofac.Features.AttributeFilters; using Nethermind.Config; +using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.Modules; using Nethermind.Crypto; using Nethermind.Db; +using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; +using NSubstitute; using NUnit.Framework; +using System; using System.Collections.Generic; using System.Net; using System.Threading; @@ -42,7 +46,7 @@ public void Setup() _discoveryV5App = CreateDiscoveryV5App(IPAddress.Parse("8.8.8.8")); } - private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp) + private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp, Action? configureDiscv5Services = null) { NetworkConfig networkConfig = new() { @@ -72,7 +76,8 @@ private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp) _discoveryDb, _legacyDiscoveryDb, new ProcessExitSource(CancellationToken.None), - LimboLogs.Instance + LimboLogs.Instance, + configureDiscv5Services ); } @@ -273,6 +278,80 @@ public void Should_Accept_Public_Ip_Enr() Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); } + [Test] + public async Task AddNodeToDiscovery_ShouldSkipNodeWithoutEnr() + { + IKademlia kademlia = Substitute.For>(); + DiscoveryV5App discoveryV5App = CreateDiscoveryV5App( + IPAddress.Parse("8.8.8.8"), + builder => builder.RegisterInstance(kademlia).As>()); + + try + { + discoveryV5App.AddNodeToDiscovery(new Node(TestItem.PublicKeyA, "8.8.8.8", 30303)); + + kademlia.DidNotReceive().AddOrRefresh(Arg.Any()); + } + finally + { + await discoveryV5App.DisposeAsync(); + } + } + + [Test] + public async Task AddNodeToDiscovery_ShouldAddValidatedEnrNode() + { + IKademlia kademlia = Substitute.For>(); + DiscoveryV5App discoveryV5App = CreateDiscoveryV5App( + IPAddress.Parse("8.8.8.8"), + builder => builder.RegisterInstance(kademlia).As>()); + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), udpPort: 30304); + Node node = new(TestItem.PrivateKeyA.PublicKey, "1.1.1.1", 30303) + { + Enr = enr.EnrString + }; + + try + { + discoveryV5App.AddNodeToDiscovery(node); + + kademlia.Received(1).AddOrRefresh(Arg.Is(added => + added.Id.Equals(TestItem.PrivateKeyA.PublicKey) && + added.Host == "8.8.8.8" && + added.Port == 30304 && + added.Enr == enr.EnrString)); + } + finally + { + await discoveryV5App.DisposeAsync(); + } + } + + [Test] + public async Task AddNodeToDiscovery_ShouldSkipMismatchedEnr() + { + IKademlia kademlia = Substitute.For>(); + DiscoveryV5App discoveryV5App = CreateDiscoveryV5App( + IPAddress.Parse("8.8.8.8"), + builder => builder.RegisterInstance(kademlia).As>()); + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); + Node node = new(TestItem.PrivateKeyB.PublicKey, "8.8.8.8", 30303) + { + Enr = enr.EnrString + }; + + try + { + discoveryV5App.AddNodeToDiscovery(node); + + kademlia.DidNotReceive().AddOrRefresh(Arg.Any()); + } + finally + { + await discoveryV5App.DisposeAsync(); + } + } + [Test] public void Should_Use_Udp_Port_From_Enr() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index e2a3d439165d..a324a8f1baa7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -128,6 +128,35 @@ internal List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConf return bootNodes; } + public override void AddNodeToDiscovery(Node node) + { + if (string.IsNullOrEmpty(node.Enr)) + { + return; + } + + try + { + NodeRecord record = NodeRecord.FromEnrString(node.Enr); + if (!TryGetNodeFromEnr(record, out Node? enrNode)) + { + return; + } + + if (!enrNode.IdHash.Equals(node.IdHash)) + { + if (Logger.IsTrace) Logger.Trace($"Skipping discv5 discovery node {node:s} with mismatched ENR identity."); + return; + } + + Kademlia.AddOrRefresh(enrNode); + } + catch (Exception e) + { + if (Logger.IsTrace) Logger.Trace($"Unable to parse discv5 discovery ENR for {node}: {e}"); + } + } + private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, NetworkNode networkNode) { if (networkNode.IsEnr) diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs index e8df95ad5e8c..9ee7063c154a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs @@ -88,7 +88,7 @@ public async Task StopAsync() public abstract void InitializeChannel(IChannel channel); - public void AddNodeToDiscovery(Node node) => Kademlia.AddOrRefresh(node); + public virtual void AddNodeToDiscovery(Node node) => Kademlia.AddOrRefresh(node); public IAsyncEnumerable DiscoverNodes(CancellationToken token) => (_kademliaNodeSource ?? throw new InvalidOperationException("Kademlia services were not initialized.")).DiscoverNodes(token); From 8bf6a82d2be53aef111e8c25ae1e3a519305c8ef Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 21:17:06 +0300 Subject: [PATCH 119/182] Bound discv4 discovery node dedupe --- .../Discv4/IteratorNodeLookupTests.cs | 27 ++++++ .../Discv4/KademliaNodeSourceTests.cs | 84 ++++++++++++++++++- .../Discv4/IteratorNodeLookup.cs | 5 ++ .../Discv4/KademliaNodeSource.cs | 71 ++++++++++++++-- 4 files changed, 176 insertions(+), 11 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index fad7f7261738..fccc21a89a1b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -151,6 +151,33 @@ public void Lookup_should_respect_cancellation_token(CancellationToken token) Assert.ThrowsAsync(async () => await _lookup.Lookup(_targetKey, cts.Token).ToListAsync()); } + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_not_cache_node_as_unreachable_when_lookup_is_cancelled(CancellationToken token) + { + RoutingTableReturns(InitialNode); + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + + _msgSender.FindNeighbours(InitialNode, _targetKey, Arg.Any()) + .Returns(call => + { + cts.Cancel(); + return Task.FromException(new OperationCanceledException((CancellationToken)call[2])); + }); + + Assert.ThrowsAsync(async () => await _lookup.Lookup(_targetKey, cts.Token).ToListAsync()); + + FindNeighboursReturns(InitialNode, NeighbourNode); + + List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); + + Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); + await _msgSender.Received(2).FindNeighbours( + Arg.Is(n => n == InitialNode), + Arg.Is(k => k == _targetKey), + Arg.Any()); + } + [Test] [CancelAfter(10000)] public async Task Lookup_should_not_query_same_node_twice(CancellationToken token) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs index 0cdec0be6cb1..d51cd1116c6c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -25,7 +25,7 @@ namespace Nethermind.Network.Discovery.Test.Discv4 [TestFixture] public class KademliaNodeSourceTests { - private IKademlia _kademlia = null!; + private TestKademlia _kademlia = null!; private IIteratorNodeLookup _lookup = null!; private IKademliaDiscv4Adapter _discv4Adapter = null!; private KademliaNodeSource _nodeSource = null!; @@ -33,11 +33,12 @@ public class KademliaNodeSourceTests private INodeStats _nodeStats = null!; private ManualTimestamper _timestamper = null!; private DiscoveryConfig _discoveryConfig = null!; + private KademliaConfig _kademliaConfig = null!; [SetUp] public void Setup() { - _kademlia = Substitute.For>(); + _kademlia = new(); _lookup = Substitute.For>(); _discv4Adapter = Substitute.For(); @@ -45,6 +46,11 @@ public void Setup() { ConcurrentDiscoveryJob = 2 }; + _kademliaConfig = new() + { + CurrentNodeId = new Node(TestItem.PublicKeyA, "127.0.0.1", 30303), + KSize = 1 + }; _nodeStats = Substitute.For(); _timestamper = new(); @@ -58,6 +64,7 @@ public void Setup() _lookup, _discv4Adapter, _discoveryConfig, + _kademliaConfig, LimboLogs.Instance); } @@ -181,8 +188,7 @@ public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(Cancellat IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); - // Simulate node added event - _kademlia.OnNodeAdded += Raise.Event>(null, node2); + _kademlia.RaiseNodeAdded(node2); // Continue iterating await enumerator.MoveNextAsync(); @@ -257,6 +263,41 @@ public async Task DiscoverNodes_should_stop_background_jobs_when_enumeration_is_ Assert.That(nodes, Is.EqualTo(new[] { node })); } + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_release_event_reservation_when_channel_is_full(CancellationToken token) + { + _discoveryConfig.ConcurrentDiscoveryJob = 0; + Node starterNode = CreateNode(1000); + Node[] queuedNodes = Enumerable.Range(0, 65).Select(CreateNode).ToArray(); + + await using IAsyncEnumerator enumerator = _nodeSource.DiscoverNodes(token).GetAsyncEnumerator(token); + ValueTask firstMove = enumerator.MoveNextAsync(); + await Task.Yield(); + _kademlia.RaiseNodeAdded(starterNode); + + Assert.That(await firstMove.AsTask(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo(starterNode)); + + foreach (Node node in queuedNodes) + { + _kademlia.RaiseNodeAdded(node); + } + + for (int i = 0; i < 64; i++) + { + Assert.That(await enumerator.MoveNextAsync(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo(queuedNodes[i])); + } + + ValueTask retryMove = enumerator.MoveNextAsync(); + await Task.Yield(); + _kademlia.RaiseNodeAdded(queuedNodes[64]); + + Assert.That(await retryMove.AsTask(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo(queuedNodes[64])); + } + private static async IAsyncEnumerable CreateAsyncEnumerable(params IEnumerable items) { foreach (T item in items) @@ -265,5 +306,40 @@ private static async IAsyncEnumerable CreateAsyncEnumerable(params IEnumer yield return item; } } + + private static Node CreateNode(int index) + { + byte[] publicKey = new byte[PublicKey.LengthInBytes]; + publicKey[60] = (byte)(index >> 24); + publicKey[61] = (byte)(index >> 16); + publicKey[62] = (byte)(index >> 8); + publicKey[63] = (byte)index; + return new Node(new PublicKey(publicKey), $"192.168.{index / 256}.{index % 256}", 30303); + } + + private sealed class TestKademlia : IKademlia + { + public event EventHandler? OnNodeAdded; + public event EventHandler? OnNodeRemoved { add { } remove { } } + + public void RaiseNodeAdded(Node node) => OnNodeAdded?.Invoke(this, node); + + public void AddOrRefresh(Node node) => throw new NotSupportedException(); + + public void Remove(Node node) => throw new NotSupportedException(); + + public Task Run(CancellationToken token) => Task.CompletedTask; + + public Task Bootstrap(CancellationToken token) => Task.CompletedTask; + + public Task LookupNodesClosest(PublicKey key, CancellationToken token, int? k = null) => + Task.FromResult(Array.Empty()); + + public Node[] GetKNeighbour(PublicKey target, Node? excluding = null, bool excludeSelf = false) => []; + + public Node[] GetAllAtDistance(int distance) => []; + + public IEnumerable IterateNodes() => []; + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs index 8bc79f3b4097..15cba82b7331 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs @@ -181,6 +181,11 @@ bool ShouldStop() ? [] : await msgSender.FindNeighbours(node, target, token); } + catch (OperationCanceledException) + when (token.IsCancellationRequested) + { + throw; + } catch (OperationCanceledException) { _unreachableNodes.Set(keyOperator.GetNodeHash(node), timestamper.UtcNowOffset); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 8a04efdf4318..d2738f8e9341 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; @@ -17,18 +17,24 @@ public class KademliaNodeSource( IIteratorNodeLookup lookup, IKademliaDiscv4Adapter discv4Adapter, IDiscoveryConfig discoveryConfig, + KademliaConfig kademliaConfig, ILogManager logManager) : IKademliaNodeSource { + private const int ChannelCapacity = 64; + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256XorUtils.MaxDistance); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { if (_logger.IsDebug) _logger.Debug($"Starting discover nodes"); using CancellationTokenSource disposeCts = CancellationTokenSource.CreateLinkedTokenSource(token); CancellationToken discoveryToken = disposeCts.Token; - Channel ch = Channel.CreateBounded(64); - ConcurrentDictionary writtenNodes = new(); + Channel ch = Channel.CreateBounded(ChannelCapacity); + LinkedList recentlyWrittenNodes = []; + Dictionary> writtenNodes = []; + object writtenNodesLock = new(); int duplicated = 0; int total = 0; @@ -60,12 +66,21 @@ async Task DiscoverAsync(PublicKey target) anyFound = true; count++; total++; - if (!writtenNodes.TryAdd(node.IdHash, node.IdHash)) + if (!TryReserveNode(node.IdHash)) { duplicated++; continue; } - await ch.Writer.WriteAsync(node, discoveryToken); + + try + { + await ch.Writer.WriteAsync(node, discoveryToken); + } + catch + { + ReleaseReservedNode(node.IdHash); + throw; + } } if (!anyFound) @@ -135,8 +150,50 @@ async Task DiscoverAsync(PublicKey target) void Handler(object? _, Node addedNode) { - writtenNodes.TryAdd(addedNode.IdHash, addedNode.IdHash); - ch.Writer.TryWrite(addedNode); // Ignore if channel full + if (!TryReserveNode(addedNode.IdHash)) + { + return; + } + + if (ch.Writer.TryWrite(addedNode)) + { + return; + } + + ReleaseReservedNode(addedNode.IdHash); + } + + bool TryReserveNode(ValueHash256 nodeId) + { + lock (writtenNodesLock) + { + if (writtenNodes.ContainsKey(nodeId)) + { + return false; + } + + LinkedListNode listNode = recentlyWrittenNodes.AddLast(nodeId); + writtenNodes.Add(nodeId, listNode); + while (writtenNodes.Count > _recentNodeLimit) + { + LinkedListNode oldestNode = recentlyWrittenNodes.First!; + recentlyWrittenNodes.RemoveFirst(); + writtenNodes.Remove(oldestNode.Value); + } + + return true; + } + } + + void ReleaseReservedNode(ValueHash256 nodeId) + { + lock (writtenNodesLock) + { + if (writtenNodes.Remove(nodeId, out LinkedListNode? listNode)) + { + recentlyWrittenNodes.Remove(listNode); + } + } } } } From 5b7cef2b284ae817a1ecd659e816417af024d90f Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 27 May 2026 21:29:39 +0300 Subject: [PATCH 120/182] Require discv4 bond before node health updates --- .../Discv4/KademliaDiscv4AdapterTests.cs | 43 +++++++++++++++++-- .../Discv4/KademliaDiscv4Adapter.cs | 25 +++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index dbe5902b4b2c..d0b1692f9a8e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -63,6 +63,14 @@ private void ConfigureBondCallback() => Task.Run(() => _adapter.OnIncomingMsg(pong)); }); + private async Task BondReceiver(CancellationToken token) + { + ConfigureBondCallback(); + await _adapter.Ping(_receiver, token); + _msgSender.ClearReceivedCalls(); + _nodeHealthTracker.ClearReceivedCalls(); + } + [SetUp] public void Setup() { @@ -80,7 +88,9 @@ public void Setup() _logManager = LimboLogs.Instance; _timestamper = Substitute.For(); - _timestamper.UnixTime.Returns(new UnixTime(new(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc))); + DateTime now = new(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc); + _timestamper.UtcNow.Returns(now); + _timestamper.UnixTime.Returns(new UnixTime(now)); _msgSender = Substitute.For(); _msgSender.SendMsg(Arg.Any()).Returns(Task.CompletedTask); @@ -288,7 +298,7 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => [CancelAfter(10000)] public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(CancellationToken token) { - ConfigureBondCallback(); + await BondReceiver(token); FindNodeMsg findNodeMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); findNodeMsg = AddReceiverFarAddress(findNodeMsg); @@ -316,11 +326,25 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => m.Nodes.Count == 8)); } + [Test] + [CancelAfter(10000)] + public async Task OnIncomingMsg_find_node_from_unbonded_peer_should_not_update_node_health(CancellationToken token) + { + FindNodeMsg findNodeMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); + findNodeMsg = AddReceiverFarAddress(findNodeMsg); + + await _adapter.OnIncomingMsg(findNodeMsg); + + _nodeHealthTracker.DidNotReceive().OnIncomingMessageFrom(Arg.Is(n => n.Id == _receiver.Id)); + _kademliaMessageReceiver.DidNotReceive().GetKNeighbour(Arg.Any(), Arg.Any()); + await _msgSender.DidNotReceive().SendMsg(Arg.Any()); + } + [Test] [CancelAfter(10000)] public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(CancellationToken token) { - ConfigureBondCallback(); + await BondReceiver(token); EnrRequestMsg enrRequestMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20); enrRequestMsg = AddReceiverFarAddress(enrRequestMsg); @@ -333,5 +357,18 @@ await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && m.NodeRecord.Equals(_selfNodeRecord))); } + + [Test] + [CancelAfter(10000)] + public async Task OnIncomingMsg_enr_request_from_unbonded_peer_should_not_update_node_health(CancellationToken token) + { + EnrRequestMsg enrRequestMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20); + enrRequestMsg = AddReceiverFarAddress(enrRequestMsg); + + await _adapter.OnIncomingMsg(enrRequestMsg); + + _nodeHealthTracker.DidNotReceive().OnIncomingMessageFrom(Arg.Is(n => n.Id == _receiver.Id)); + await _msgSender.DidNotReceive().SendMsg(Arg.Any()); + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index bf38b0a85597..84bd336d2e55 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -209,24 +209,25 @@ public async Task SendEnrRequest(Node receiver, CancellationToke }, token); } - private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg, CancellationToken token) + private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg, CancellationToken token) { if (!session.HasReceivedPong) { if (_logger.IsDebug) _logger.Debug($"Rejecting enr request from unbonded peer {node}"); - return; + return false; } Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); await SendMessage(session, new EnrResponseMsg(node.Address, nodeRecordProvider.Current, Keccak.Compute(requestRlp.Bytes)), token); + return true; } - private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg, CancellationToken token) + private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg, CancellationToken token) { if (!session.HasReceivedPong) { if (_logger.IsDebug) _logger.Debug($"Rejecting findNode request from unbonded peer {node}"); - return; + return false; } PublicKey publicKey = new(msg.SearchedNodeId); @@ -234,7 +235,7 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms if (nodes.Length == 0) { await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), nodes), token); - return; + return true; } for (int i = 0; i < nodes.Length; i += MaxNodesPerNeighborsMsg) @@ -242,6 +243,8 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg ms int batchEnd = Math.Min(i + MaxNodesPerNeighborsMsg, nodes.Length); await SendMessage(session, new NeighborsMsg(node.Address, CalculateExpirationTime(), new ArraySegment(nodes, i, batchEnd - i)), token); } + + return true; } private async Task HandlePing(Node node, NodeSession session, PingMsg ping, CancellationToken token) @@ -285,12 +288,16 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) nodeHealthTracker.Value.OnIncomingMessageFrom(node); break; case MsgType.FindNode: - await HandleFindNode(node, session, (FindNodeMsg)msg, token); - nodeHealthTracker.Value.OnIncomingMessageFrom(node); + if (await HandleFindNode(node, session, (FindNodeMsg)msg, token)) + { + nodeHealthTracker.Value.OnIncomingMessageFrom(node); + } break; case MsgType.EnrRequest: - await HandleEnrRequest(node, session, (EnrRequestMsg)msg, token); - nodeHealthTracker.Value.OnIncomingMessageFrom(node); + if (await HandleEnrRequest(node, session, (EnrRequestMsg)msg, token)) + { + nodeHealthTracker.Value.OnIncomingMessageFrom(node); + } break; // Unsolicited response. From 5a0761f4198c275ac768f5ce5c3f9434aa8e6774 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 28 May 2026 00:58:38 +0300 Subject: [PATCH 121/182] Harden discovery receive paths --- .../DiscoveryMessageSerializerTests.cs | 18 +- .../Discv4/KademliaDiscv4AdapterTests.cs | 19 ++ .../Discv4/KademliaNodeSourceTests.cs | 12 +- .../Discv4/NodeSessionTests.cs | 4 +- .../Discv5/Discv5KademliaAdapterTests.cs | 9 +- .../Discv5/Discv5WireTests.cs | 88 ++++++- .../NettyDiscoveryHandlerTests.cs | 76 +++++- .../Discv4/KademliaDiscv4Adapter.cs | 18 +- .../Discv4/NodeSession.cs | 12 +- .../Discv5/Discv5KademliaAdapter.cs | 75 +++++- .../KademliaDiscoveryApp.cs | 13 + .../NettyDiscoveryHandler.cs | 233 ++++++++++++++---- .../Serializers/DiscoveryMsgSerializerBase.cs | 4 +- .../Serializers/PingMsgSerializer.cs | 13 +- 14 files changed, 499 insertions(+), 95 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs index 226f6394fe34..1e521e9fc4ab 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs @@ -71,7 +71,7 @@ public void PingMessageTest() } [Test] - public void PingMessage_Rejects_Port_Zero() + public void PingMessage_Allows_Endpoint_Port_Zero() { PingMsg message = new(_privateKey.PublicKey, 60 + _timestamper.UnixTime.MillisecondsLong, new IPEndPoint(_farAddress.Address, 0), _nearAddress, @@ -79,7 +79,21 @@ public void PingMessage_Rejects_Port_Zero() { FarAddress = _farAddress }; using DisposableByteBuffer data = _messageSerializationService.ZeroSerialize(message).AsDisposable(); - Assert.Throws(() => _messageSerializationService.Deserialize(data)); + PingMsg deserializedMessage = _messageSerializationService.Deserialize(data); + + Assert.That(deserializedMessage.SourceAddress.Port, Is.Zero); + } + + [Test] + public void PingMessage_UsesUdpPortWhenTcpPortIsZero() + { + string devp2pDiscoveryOnlyPing = + "24fca4f142312eb2c8b850295cf1c7b3dbbcac49c79a4e1dbc84bffde5e3605bd3c81c2d313f3ac8f293ead68f0efc76d78033279923216da1ad5bc0356f0d0e4dab24602f48452ac037d8a7291260c6999fc9f65b85095fb5f6abc4ed1f49020101e104c9847f00000182f2bb80c9847f000001827d6580846a17400086019e6ad1b545"; + + PingMsg ping = _messageSerializationService.Deserialize(Bytes.FromHexString(devp2pDiscoveryOnlyPing)); + + Assert.That(ping.SourceAddress, Is.EqualTo(new IPEndPoint(IPAddress.Loopback, 62139))); + Assert.That(ping.DestinationAddress, Is.EqualTo(new IPEndPoint(IPAddress.Loopback, 32101))); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index d0b1692f9a8e..99c0458f9af8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -340,6 +340,23 @@ public async Task OnIncomingMsg_find_node_from_unbonded_peer_should_not_update_n await _msgSender.DidNotReceive().SendMsg(Arg.Any()); } + [Test] + [CancelAfter(10000)] + public async Task OnIncomingMsg_find_node_from_different_endpoint_should_not_respond(CancellationToken token) + { + await BondReceiver(token); + + IPEndPoint differentEndpoint = new(IPAddress.Parse("192.168.1.3"), _receiver.Address.Port); + FindNodeMsg findNodeMsg = new(differentEndpoint, _timestamper.UnixTime.SecondsLong + 20, _testPublicKey.Bytes); + findNodeMsg = AddReceiverFarAddress(findNodeMsg); + + await _adapter.OnIncomingMsg(findNodeMsg); + + _nodeHealthTracker.DidNotReceive().OnIncomingMessageFrom(Arg.Is(n => n.Id == _receiver.Id)); + _kademliaMessageReceiver.DidNotReceive().GetKNeighbour(Arg.Any(), Arg.Any()); + await _msgSender.DidNotReceive().SendMsg(Arg.Any()); + } + [Test] [CancelAfter(10000)] public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(CancellationToken token) @@ -348,6 +365,7 @@ public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(Can EnrRequestMsg enrRequestMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20); enrRequestMsg = AddReceiverFarAddress(enrRequestMsg); + Hash256 expectedRequestHash = new(enrRequestMsg.Hash!.Value.Span); await _adapter.OnIncomingMsg(enrRequestMsg); @@ -355,6 +373,7 @@ public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(Can await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && + m.RequestKeccak.Equals(expectedRequestHash) && m.NodeRecord.Equals(_selfNodeRecord))); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs index d51cd1116c6c..75f386911841 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs @@ -77,7 +77,7 @@ public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToke { Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); - _nodeSession.OnPongReceived(); + _nodeSession.OnPongReceived(node1.Address); _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1, node2)); @@ -129,7 +129,7 @@ public async Task DiscoverNodes_should_skip_nodes_that_have_tried_ping_recently_ session1.OnPingSent(); // Set up session2 to have received a pong - session2.OnPongReceived(); + session2.OnPongReceived(node2.Address); _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1, node2)); @@ -178,7 +178,7 @@ public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(Cancellat Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); - _nodeSession.OnPongReceived(); + _nodeSession.OnPongReceived(node1.Address); _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node1)); @@ -202,7 +202,7 @@ public async Task DiscoverNodes_should_not_emit_duplicate_nodes(CancellationToke { Node node = new(TestItem.PublicKeyC, "192.168.1.1", 30303); - _nodeSession.OnPongReceived(); + _nodeSession.OnPongReceived(node.Address); _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node, node)); @@ -223,7 +223,7 @@ public async Task DiscoverNodes_should_use_multiple_concurrent_discovery_jobs(Ca Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); - _nodeSession.OnPongReceived(); + _nodeSession.OnPongReceived(node1.Address); // Set up the lookup to return different nodes for different calls int callCount = 0; @@ -254,7 +254,7 @@ public async Task DiscoverNodes_should_stop_background_jobs_when_enumeration_is_ { _discoveryConfig.ConcurrentDiscoveryJob = 1; Node node = new(TestItem.PublicKeyA, "192.168.1.1", 30303); - _nodeSession.OnPongReceived(); + _nodeSession.OnPongReceived(node.Address); _lookup.Lookup(Arg.Any(), Arg.Any()) .Returns(CreateAsyncEnumerable(node)); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs index 909ae06779fc..ee8046c16f90 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSessionTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Net; using Nethermind.Core; using Nethermind.Network.Discovery.Discv4; using Nethermind.Stats; @@ -17,6 +18,7 @@ public class NodeSessionTests private INodeStats _nodeStats = null!; private ManualTimestamper _timestamper = null!; private NodeSession _nodeSession = null!; + private static readonly IPEndPoint TestEndpoint = new(IPAddress.Loopback, 30303); [SetUp] public void Setup() @@ -36,7 +38,7 @@ public void Setup() NodeSession.BondTimeout).SetName(nameof(NodeSession.HasReceivedPing)), new TestCaseData( (Func)(s => s.HasReceivedPong), - (Action)(s => s.OnPongReceived()), + (Action)(s => s.OnPongReceived(TestEndpoint)), NodeSession.BondTimeout).SetName(nameof(NodeSession.HasReceivedPong)), new TestCaseData( (Func)(s => s.HasTriedPingRecently), diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index b1fa2d893b39..b76b97142028 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -73,10 +73,11 @@ public void TryAcceptChallenge_ShouldLimitBurstPerIp() Discv5KademliaAdapter adapter = CreateAdapter(); IPEndPoint endpoint = IPEndPoint.Parse("192.0.2.1:30303"); - Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); - Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); - Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); - Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); + for (int i = 0; i < 16; i++) + { + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.True); + } + Assert.That(adapter.TryAcceptChallenge(endpoint), Is.False); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs index 84899f0440b0..773726fb672f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs @@ -11,6 +11,7 @@ using DotNetty.Transport.Channels.Embedded; using DotNetty.Transport.Channels.Sockets; using Nethermind.Core.Crypto; +using Nethermind.Core.Test; using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.Modules; using Nethermind.Crypto; @@ -91,6 +92,65 @@ public async Task Ping_Rehandshakes_After_RemoteSessionLost() await Task.WhenAll(runA, runRestartedB); } + [Test] + public async Task Ping_Completes_With_HandshakeRecord_WithoutEndpoint() + { + IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); + IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); + TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA, includeEndpointInRecord: false); + TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) + { + Enr = peerB.NodeRecordProvider.Current.EnrString + }; + + using CancellationTokenSource cancellationSource = new(10_000); + Task runA = peerA.Adapter.RunAsync(cancellationSource.Token); + Task runB = peerB.Adapter.RunAsync(cancellationSource.Token); + + Task pingTask = peerA.Adapter.Ping(nodeB, cancellationSource.Token); + await PumpUntilComplete(pingTask, peerA, peerB, cancellationSource.Token); + await pingTask; + + await cancellationSource.CancelAsync(); + await Task.WhenAll(runA, runB); + + peerB.Kademlia.Received().AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyA.PublicKey) && string.IsNullOrEmpty(node.Enr))); + } + + [Test] + public async Task InboundPing_Starts_EndpointCheck_PingBack() + { + IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); + IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); + TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) + { + Enr = peerB.NodeRecordProvider.Current.EnrString + }; + + using CancellationTokenSource cancellationSource = new(10_000); + Task runA = peerA.Adapter.RunAsync(cancellationSource.Token); + Task runB = peerB.Adapter.RunAsync(cancellationSource.Token); + + Task pingTask = peerA.Adapter.Ping(nodeB, cancellationSource.Token); + await PumpUntilComplete(pingTask, peerA, peerB, cancellationSource.Token); + await pingTask; + + await PumpUntil( + () => peerB.Kademlia.ReceivedCallsMatching( + kademlia => kademlia.AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyA.PublicKey))), + requiredNumberOfCalls: 2, + maxNumberOfCalls: int.MaxValue), + peerA, + peerB, + cancellationSource.Token); + + await cancellationSource.CancelAsync(); + await Task.WhenAll(runA, runB); + } + [Test] public async Task FindNeighbours_Returns_Records_At_Requested_Distance() { @@ -132,14 +192,14 @@ public async Task FindNeighbours_Returns_Records_At_Requested_Distance() peerA.Kademlia.Received().AddOrRefresh(Arg.Is(node => node.Id.Equals(TestItem.PrivateKeyC.PublicKey))); } - private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint) + private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, bool includeEndpointInRecord = true) { IKademlia kademlia = Substitute.For>(); NettyDiscoveryV5Handler handler = new(new TestLogManager()); EmbeddedChannel channel = new(); handler.InitializeChannel(channel); - TestNodeRecordProvider nodeRecordProvider = new(privateKey, endpoint); + TestNodeRecordProvider nodeRecordProvider = new(privateKey, endpoint, includeEndpointInRecord); Discv5KademliaAdapter adapter = new( new Lazy>(kademlia), handler, @@ -169,6 +229,19 @@ private static async Task PumpUntilComplete(Task task, TestPeer peerA, TestPeer Pump(peerB, peerA); } + private static async Task PumpUntil(Func condition, TestPeer peerA, TestPeer peerB, CancellationToken token) + { + while (!condition()) + { + Pump(peerA, peerB); + Pump(peerB, peerA); + await Task.Delay(10, token); + } + + Pump(peerA, peerB); + Pump(peerB, peerA); + } + private static void Pump(TestPeer from, TestPeer to) { while (from.Channel.ReadOutbound() is { } packet) @@ -209,13 +282,16 @@ private sealed record TestPeer( private sealed class TestNodeRecordProvider : INodeRecordProvider { - public TestNodeRecordProvider(PrivateKey privateKey, IPEndPoint endpoint) + public TestNodeRecordProvider(PrivateKey privateKey, IPEndPoint endpoint, bool includeEndpoint) { NodeRecord nodeRecord = new(); nodeRecord.SetEntry(IdEntry.Instance); - nodeRecord.SetEntry(new IpEntry(endpoint.Address)); - nodeRecord.SetEntry(new TcpEntry(endpoint.Port)); - nodeRecord.SetEntry(new UdpEntry(endpoint.Port)); + if (includeEndpoint) + { + nodeRecord.SetEntry(new IpEntry(endpoint.Address)); + nodeRecord.SetEntry(new TcpEntry(endpoint.Port)); + nodeRecord.SetEntry(new UdpEntry(endpoint.Port)); + } nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); nodeRecord.EnrSequence = 1; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(nodeRecord); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs index 51fc3b2c890b..14c9216dcaa7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs @@ -161,15 +161,26 @@ public async Task NeighborsSentReceivedTest() await _kademliaAdaptersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); } - private (IKademliaDiscv4Adapter Adapter, NettyDiscoveryHandler Handler, IChannelHandlerContext Ctx, IMessageSerializationService Service) CreateHandler(NodeFilter? nodeFilter = null) + private (IKademliaDiscv4Adapter Adapter, NettyDiscoveryHandler Handler, IChannelHandlerContext Ctx, IMessageSerializationService Service) CreateHandler( + NodeFilter? nodeFilter = null, + int? globalInboundMessageBurst = null, + int? inboundMessageQueueCapacity = null, + int? inboundMessageWorkerCount = null) { IKademliaDiscv4Adapter adapter = Substitute.For(); adapter.OnIncomingMsg(Arg.Any()).Returns(Task.CompletedTask); IMessageSerializationService service = Build.A.SerializationService().WithDiscovery(_privateKey2).TestObject; IChannel channel = Substitute.For(); - NettyDiscoveryHandler handler = nodeFilter is not null - ? new(adapter, channel, service, Timestamper.Default, LimboLogs.Instance, nodeFilter) - : new(adapter, channel, service, Timestamper.Default, LimboLogs.Instance); + NettyDiscoveryHandler handler = new( + adapter, + channel, + service, + Timestamper.Default, + LimboLogs.Instance, + nodeFilter, + globalInboundMessageBurst, + inboundMessageQueueCapacity, + inboundMessageWorkerCount); IChannelHandlerContext ctx = Substitute.For(); return (adapter, handler, ctx, service); } @@ -257,14 +268,67 @@ public async Task DefaultInboundRateLimiter_Drops_Message_AboveBurstLimit() byte[] data = SerializePing(service); - for (int i = 0; i < 5; i++) + for (int i = 0; i < 9; i++) { handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer((byte[])data.Clone()), _address2, _address)); } await SleepWhileWaiting(); - await adapter.Received(4).OnIncomingMsg(Arg.Any()); + await adapter.Received(8).OnIncomingMsg(Arg.Any()); + } + + [Test] + public async Task GlobalInboundRateLimiter_Drops_Messages_AboveBurstLimit() + { + (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(globalInboundMessageBurst: 2); + using SemaphoreSlim called = new(0); + adapter.When(x => x.OnIncomingMsg(Arg.Any())).Do(_ => called.Release()); + + byte[] data = SerializePing(service); + + for (int i = 0; i < 3; i++) + { + IPEndPoint sender = new(IPAddress.Parse($"127.0.1.{i + 1}"), _address2.Port); + handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer((byte[])data.Clone()), sender, _address)); + } + + Assert.That(await called.WaitAsync(TimeSpan.FromSeconds(5)), Is.True); + Assert.That(await called.WaitAsync(TimeSpan.FromSeconds(5)), Is.True); + await Task.Delay(50); + + await adapter.Received(2).OnIncomingMsg(Arg.Any()); + } + + [Test] + public async Task InboundDispatchQueue_Drops_Messages_WhenFull() + { + TaskCompletionSource unblockHandler = new(TaskCreationOptions.RunContinuationsAsynchronously); + (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler( + globalInboundMessageBurst: 64, + inboundMessageQueueCapacity: 1, + inboundMessageWorkerCount: 1); + int received = 0; + adapter.OnIncomingMsg(Arg.Any()).Returns(_ => + { + Interlocked.Increment(ref received); + return unblockHandler.Task; + }); + + byte[] data = SerializePing(service); + + for (int i = 0; i < 16; i++) + { + IPEndPoint sender = new(IPAddress.Parse($"127.0.2.{i + 1}"), _address2.Port); + handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer((byte[])data.Clone()), sender, _address)); + } + + Assert.That(() => Interlocked.CompareExchange(ref received, 0, 0), Is.GreaterThanOrEqualTo(1).After(5000, 10)); + await Task.Delay(100); + unblockHandler.SetResult(); + await Task.Delay(100); + + Assert.That(Interlocked.CompareExchange(ref received, 0, 0), Is.LessThan(16)); } private byte[] SerializePing(IMessageSerializationService service) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 84bd336d2e55..1a97cd33d1a6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -10,7 +10,6 @@ using Nethermind.Logging; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Messages; -using Nethermind.Serialization.Rlp; using Nethermind.Stats; using Nethermind.Stats.Model; using NonBlocking; @@ -177,8 +176,8 @@ public async Task Ping(Node receiver, CancellationToken token) EnrSequence = nodeRecordProvider.Current.EnrSequence // optional and does not seem to be used anywhere. }; session.OnPingSent(); - _ = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, session, msg, token); - session.OnPongReceived(); + PongMsg pong = await CallAndWaitForResponse(MsgType.Pong, new PongMsgHandler(msg), receiver, session, msg, token); + session.OnPongReceived(pong.FarAddress ?? receiver.Address); } public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) @@ -211,20 +210,25 @@ public async Task SendEnrRequest(Node receiver, CancellationToke private async Task HandleEnrRequest(Node node, NodeSession session, EnrRequestMsg msg, CancellationToken token) { - if (!session.HasReceivedPong) + if (!session.HasEndpointProof(node.Address)) { if (_logger.IsDebug) _logger.Debug($"Rejecting enr request from unbonded peer {node}"); return false; } - Rlp requestRlp = Rlp.Encode(Rlp.Encode(msg.ExpirationTime)); - await SendMessage(session, new EnrResponseMsg(node.Address, nodeRecordProvider.Current, Keccak.Compute(requestRlp.Bytes)), token); + if (msg.Hash is not { } requestHash) + { + if (_logger.IsDebug) _logger.Debug($"Rejecting enr request without packet hash from {node}"); + return false; + } + + await SendMessage(session, new EnrResponseMsg(node.Address, nodeRecordProvider.Current, new Hash256(requestHash.Span)), token); return true; } private async Task HandleFindNode(Node node, NodeSession session, FindNodeMsg msg, CancellationToken token) { - if (!session.HasReceivedPong) + if (!session.HasEndpointProof(node.Address)) { if (_logger.IsDebug) _logger.Debug($"Rejecting findNode request from unbonded peer {node}"); return false; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index 30a10933a274..cd908ee34ff9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Net; using Nethermind.Core; using Nethermind.Network.Discovery.Messages; using Nethermind.Stats; @@ -18,15 +19,24 @@ public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) private long _lastPongReceivedTicks; private long _lastPingReceivedTicks; private long _lastPingSentTicks; + private IPEndPoint? _lastPongEndpoint; public bool HasReceivedPing => Volatile.Read(ref _lastPingReceivedTicks) + BondTimeout.Ticks > Timestamper.UtcNow.Ticks; public bool NotTooManyFailure => Volatile.Read(ref _authenticatedRequestFailureCount) <= AuthenticatedRequestFailureLimit; public bool HasReceivedPong => Volatile.Read(ref _lastPongReceivedTicks) + BondTimeout.Ticks > Timestamper.UtcNow.Ticks; public bool HasTriedPingRecently => Volatile.Read(ref _lastPingSentTicks) + PingRetryTimeout.Ticks > Timestamper.UtcNow.Ticks; + public bool HasEndpointProof(IPEndPoint endpoint) => + HasReceivedPong && Volatile.Read(ref _lastPongEndpoint) is { } lastPongEndpoint && lastPongEndpoint.Equals(endpoint); + public void ResetAuthenticatedRequestFailure() => Interlocked.Exchange(ref _authenticatedRequestFailureCount, 0); public void OnAuthenticatedRequestFailure() => Interlocked.Increment(ref _authenticatedRequestFailureCount); - public void OnPongReceived() => Volatile.Write(ref _lastPongReceivedTicks, Timestamper.UtcNow.Ticks); + public void OnPongReceived(IPEndPoint endpoint) + { + Volatile.Write(ref _lastPongEndpoint, endpoint); + Volatile.Write(ref _lastPongReceivedTicks, Timestamper.UtcNow.Ticks); + } + public void OnPingReceived() => Volatile.Write(ref _lastPingReceivedTicks, Timestamper.UtcNow.Ticks); public void RecordStatsForOutgoingMsg(DiscoveryMsg msg) => RecordStatsForMsg(msg, outgoing: true); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index 493171eb8069..f5f811204997 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -34,11 +34,13 @@ public class Discv5KademliaAdapter( private const int MaxPendingRequests = 4_096; private const int MaxResponseHandlers = 1_024; private const int MaxKnownRecords = 16_384; + private const int MaxEndpointChecks = 4_096; private const int MaxNodesResponseMessages = 16; private const int MaxNodesResponseRecords = 64; private const long SentChallengeTtlMilliseconds = 60_000; + private const long EndpointCheckTtlMilliseconds = 60_000; private static readonly TimeSpan ChallengeRateLimitWindow = TimeSpan.FromMilliseconds(100); - private const int ChallengeRateLimitBurstPerIp = 4; + private const int ChallengeRateLimitBurstPerIp = 16; private const int ChallengeRateLimitFilterSize = 8_192; private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); @@ -50,6 +52,7 @@ public class Discv5KademliaAdapter( private readonly BoundedMap _pendingByNonce = new(MaxPendingRequests); private readonly BoundedMap _responseHandlers = new(MaxResponseHandlers); private readonly BoundedMap _knownRecords = new(MaxKnownRecords); + private readonly BoundedMap _endpointChecks = new(MaxEndpointChecks); private readonly NodeFilter[] _challengeRateLimiters = CreateChallengeRateLimiters(); /// @@ -91,6 +94,7 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = public async Task Ping(Node receiver, CancellationToken token) { RegisterKnownRecord(receiver); + ReserveEndpointCheck(receiver); byte[] requestId = CreateRequestId(); Discv5Ping ping = new(requestId, nodeRecordProvider.Current.EnrSequence); PongResponseHandler responseHandler = new(receiver); @@ -310,18 +314,23 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can return; } + NodeRecord? messageRecord = knownRecord; if (nodeRecord is not null) { - if (!IsAcceptableNodeRecord(nodeRecord, nodeId, NodeFilter.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + if (!HasExpectedNodeId(nodeRecord, nodeId)) { return; } - SetKnownRecord(nodeId, nodeRecord); + if (IsAcceptableNodeRecord(nodeRecord, nodeId, NodeFilter.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + { + SetKnownRecord(nodeId, nodeRecord); + messageRecord = nodeRecord; + } } SetSession(new SessionKey(nodeId, endpoint), session); - await HandleMessage(session.RemotePublicKey, endpoint, message, token, nodeRecord ?? knownRecord); + await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); } private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket, byte[] destinationNodeId) @@ -331,6 +340,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket long now = Environment.TickCount64; if (_sentChallenges.TryGetValue(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) { + await discoveryHandler.SendAsync(existingChallenge.Packet, endpoint); return; } @@ -342,7 +352,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket ulong enrSequence = TryGetKnownRecord(nodeId, out NodeRecord? record) ? record.EnrSequence : 0UL; byte[] packet = packetCodec.EncodeWhoAreYou(destinationNodeId, requestPacket.Nonce, enrSequence, out Discv5Challenge challenge); - SetSentChallenge(challengeKey, challenge); + SetSentChallenge(challengeKey, challenge, packet); await discoveryHandler.SendAsync(packet, endpoint); } @@ -366,6 +376,10 @@ await SendResponse( new Discv5Pong(ping.RequestId, nodeRecordProvider.Current.EnrSequence, endpoint.Address, endpoint.Port), token); kademlia.Value.AddOrRefresh(remoteNode); + if (!string.IsNullOrEmpty(remoteNode.Enr)) + { + StartEndpointCheck(remoteNode, token); + } break; case Discv5FindNode findNode: await HandleFindNode(remoteNode, findNode, token); @@ -541,11 +555,14 @@ internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedN => Discv5NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable, out Node? node) && node.Id.Hash.Equals(expectedNodeId); - private void SetSentChallenge(ChallengeKey challengeKey, Discv5Challenge challenge) + internal static bool HasExpectedNodeId(NodeRecord record, Hash256 expectedNodeId) + => record.GetObj(EnrContentKey.SecP256k1)?.Decompress().Hash.Equals(expectedNodeId) == true; + + private void SetSentChallenge(ChallengeKey challengeKey, Discv5Challenge challenge, byte[] packet) { long now = Environment.TickCount64; TryTrimExpiredChallenges(now); - _sentChallenges.Set(challengeKey, new SentChallenge(challenge, now)); + _sentChallenges.Set(challengeKey, new SentChallenge(challenge, packet, now)); } private void TryTrimExpiredChallenges(long now) @@ -689,7 +706,7 @@ private void Trim() private sealed record PendingRequest(Node Receiver, Discv5Message Message); - private readonly record struct SentChallenge(Discv5Challenge Challenge, long CreatedAtMilliseconds); + private readonly record struct SentChallenge(Discv5Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds); private interface IResponseHandler { @@ -794,4 +811,46 @@ private bool MatchesRequestedDistance(Node node, int[] requestedDistances) return false; } } + + private void StartEndpointCheck(Node remoteNode, CancellationToken token) + { + if (!TryReserveEndpointCheck(remoteNode)) + { + return; + } + + _ = RunEndpointCheck(remoteNode, token); + } + + private async Task RunEndpointCheck(Node remoteNode, CancellationToken token) + { + try + { + await Ping(remoteNode, token); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + } + catch (Exception e) + { + if (_logger.IsTrace) _logger.Trace($"Discv5 endpoint check failed for {remoteNode}: {e}"); + } + } + + private void ReserveEndpointCheck(Node remoteNode) + => _endpointChecks.Set(new SessionKey(remoteNode.Id.Hash, remoteNode.Address), Environment.TickCount64); + + private bool TryReserveEndpointCheck(Node remoteNode) + { + SessionKey sessionKey = new(remoteNode.Id.Hash, remoteNode.Address); + long now = Environment.TickCount64; + if (_endpointChecks.TryGetValue(sessionKey, out long startedAt) && + now - startedAt <= EndpointCheckTtlMilliseconds) + { + return false; + } + + _endpointChecks.Set(sessionKey, now); + return true; + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs index 9ee7063c154a..ac2f887ccff0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs @@ -26,6 +26,7 @@ public abstract class KademliaDiscoveryApp( private IKademliaNodeSource? _kademliaNodeSource; private IKademlia? _kademlia; private Task? _runningTask; + private int _activationStarted; protected ILogger Logger { get; } = logger; @@ -36,6 +37,7 @@ public Task StartAsync() try { Initialize(); + TryStartActivation(); return Task.CompletedTask; } catch (Exception e) @@ -131,6 +133,17 @@ protected void OnChannelActivated(object? sender, EventArgs e) return; } + TryStartActivation(); + } + + private void TryStartActivation() + { + if (_stopCts.IsCancellationRequested || + Interlocked.CompareExchange(ref _activationStarted, 1, 0) != 0) + { + return; + } + _runningTask = StartActivationAsync(_stopCts.Token); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs index e257fc692d6e..6185632e056c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs @@ -1,9 +1,13 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; +using System.Threading.Channels; using DotNetty.Buffers; +using DotNetty.Common.Utilities; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; using FastEnumUtility; @@ -23,12 +27,19 @@ public class NettyDiscoveryHandler( IMessageSerializationService? msgSerializationService, ITimestamper? timestamper, ILogManager? logManager, - NodeFilter? inboundMessageFilter = null) : NettyDiscoveryBaseHandler(logManager), IMsgSender + NodeFilter? inboundMessageFilter = null, + int? globalInboundMessageBurst = null, + int? inboundMessageQueueCapacity = null, + int? inboundMessageWorkerCount = null) : NettyDiscoveryBaseHandler(logManager), IMsgSender { private static readonly TimeSpan MaxFutureExpirationOffset = TimeSpan.FromHours(1); private static readonly TimeSpan DefaultInboundMessageWindow = TimeSpan.FromMilliseconds(100); - private const int DefaultInboundMessageBurstPerIp = 4; + private static readonly TimeSpan DefaultGlobalInboundMessageWindow = TimeSpan.FromMilliseconds(100); + private const int DefaultInboundMessageBurstPerIp = 8; private const int DefaultInboundMessageFilterSize = 8_192; + private const int DefaultGlobalInboundMessageBurst = 512; + private const int DefaultInboundMessageQueueCapacity = 1_024; + private const int DefaultInboundMessageWorkerCount = 16; private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); private readonly IDiscoveryMsgListener _discoveryMsgListener = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); private readonly IChannel _channel = channel ?? throw new ArgumentNullException(nameof(channel)); @@ -37,9 +48,30 @@ public class NettyDiscoveryHandler( private readonly NodeFilter[] _inboundMessageFilters = inboundMessageFilter is null ? CreateDefaultInboundMessageFilters() : [inboundMessageFilter]; + private readonly FixedWindowLimiter _globalInboundMessageLimiter = new(Math.Max(1, globalInboundMessageBurst ?? DefaultGlobalInboundMessageBurst), DefaultGlobalInboundMessageWindow); + private readonly Channel _inboundMessages = Channel.CreateBounded( + new BoundedChannelOptions(Math.Max(1, inboundMessageQueueCapacity ?? DefaultInboundMessageQueueCapacity)) + { + SingleReader = false, + SingleWriter = false + }); + private readonly int _inboundMessageWorkerCount = Math.Max(1, inboundMessageWorkerCount ?? DefaultInboundMessageWorkerCount); + private int _dispatchWorkersStarted; public override void ChannelActive(IChannelHandlerContext context) => OnChannelActivated?.Invoke(this, EventArgs.Empty); + public override void ChannelInactive(IChannelHandlerContext context) + { + _inboundMessages.Writer.TryComplete(); + base.ChannelInactive(context); + } + + public override void HandlerRemoved(IChannelHandlerContext context) + { + _inboundMessages.Writer.TryComplete(); + base.HandlerRemoved(context); + } + public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) { //In case of SocketException we log it as debug to avoid noise @@ -100,9 +132,9 @@ public async Task SendMsg(DiscoveryMsg discoveryMsg) Metrics.DiscoveryMessagesSent.Increment(discoveryMsg.MsgType); } - private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg, out bool shouldForward) + private bool TryAcceptPacket(DatagramPacket packet, out MsgType type, out bool shouldForward) { - msg = null; + type = default; shouldForward = true; IByteBuffer content = packet.Content; @@ -119,32 +151,24 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg, out b int readerIndex = content.ReaderIndex; byte msgTypeByte = content.GetByte(readerIndex + 97); - if (FromMsgTypeByte(msgTypeByte) is not { } type) + if (FromMsgTypeByte(msgTypeByte) is not { } resolvedType) { if (_logger.IsDebug) _logger.Debug($"Unsupported message type: {msgTypeByte}, sender: {address}"); return false; } + type = resolvedType; if (_logger.IsTrace) _logger.Trace($"Received message: {type}"); - if (address is IPEndPoint remoteEndpoint && !TryAcceptInbound(remoteEndpoint)) + if (!_globalInboundMessageLimiter.TryAcquire()) { - if (_logger.IsDebug) _logger.Debug($"Rate limiting discovery message {type} from {remoteEndpoint}"); - shouldForward = false; + if (_logger.IsDebug) _logger.Debug($"Rate limiting discovery message globally, type: {type}, sender: {address}"); return false; } - using ArrayPoolDisposableReturn handle = ArrayPoolDisposableReturn.Rent(size, out byte[] msgBytes); - content.GetBytes(readerIndex, msgBytes, 0, size); - - try - { - msg = Deserialize(type, new ArraySegment(msgBytes, 0, size)); - msg.FarAddress = (IPEndPoint)address; - } - catch (Exception e) + if (address is IPEndPoint remoteEndpoint && !TryAcceptInbound(remoteEndpoint)) { - if (_logger.IsDebug) _logger.Debug($"Error during deserialization of the message, type: {type}, sender: {address}, msg: {msgBytes.AsSpan(0, size).ToHexString()}, {e.Message}"); + if (_logger.IsDebug) _logger.Debug($"Rate limiting discovery message {type} from {remoteEndpoint}"); return false; } @@ -153,7 +177,7 @@ private bool TryParseMessage(DatagramPacket packet, out DiscoveryMsg? msg, out b protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket packet) { - if (!TryParseMessage(packet, out DiscoveryMsg? msg, out bool shouldForward) || msg == null) + if (!TryAcceptPacket(packet, out MsgType type, out bool shouldForward)) { if (shouldForward) { @@ -163,33 +187,15 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket return; } - MsgType type = msg.MsgType; EndPoint address = packet.Sender; int size = packet.Content.ReadableBytes; + EnsureDispatchWorkersStarted(); - try + packet.Retain(); + if (!_inboundMessages.Writer.TryWrite(new InboundDiscoveryPacket(ctx, packet, type, address, size))) { - ReportMsgByType(msg, size); - - if (!ValidateMsg(msg, type, address, ctx, packet, size)) - return; - - // Explicitly run it on the default scheduler to prevent something down the line hanging netty task scheduler. - Task.Factory.StartNew( - static state => - { - (IDiscoveryMsgListener discoveryMsgListener, DiscoveryMsg discoveryMsg) = ((IDiscoveryMsgListener, DiscoveryMsg))state!; - discoveryMsgListener.OnIncomingMsg(discoveryMsg); - }, - (_discoveryMsgListener, msg), - CancellationToken.None, - TaskCreationOptions.RunContinuationsAsynchronously, - TaskScheduler.Default - ); - } - catch (Exception e) - { - _logger.DebugError($"Error while processing message, type: {type}, sender: {address}, message: {msg}", e); + ReferenceCountUtil.Release(packet); + if (_logger.IsDebug) _logger.Debug($"Dropping discovery message because inbound dispatch queue is full, type: {type}, sender: {address}"); } } @@ -218,7 +224,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket _ => throw new Exception($"Unsupported messageType: {msg.MsgType}") }; - private bool ValidateMsg(DiscoveryMsg msg, MsgType type, EndPoint address, IChannelHandlerContext ctx, DatagramPacket packet, int size) + private bool ValidateMsg(DiscoveryMsg msg, MsgType type, EndPoint address, DatagramPacket packet, int size) { long timeToExpire = msg.ExpirationTime - _timestamper.UnixTime.SecondsLong; if (timeToExpire < 0) @@ -245,14 +251,14 @@ private bool ValidateMsg(DiscoveryMsg msg, MsgType type, EndPoint address, IChan if (!msg.FarAddress.Equals((IPEndPoint)packet.Sender)) { if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", $"{msg.MsgType} has incorrect far address", size); - if (_logger.IsDebug) _logger.Debug($"Discovery fake IP detected - pretended {msg.FarAddress} but was {ctx.Channel.RemoteAddress}, type: {type}, sender: {address}, message: {msg}"); + if (_logger.IsDebug) _logger.Debug($"Discovery fake IP detected - pretended {msg.FarAddress} but was {packet.Sender}, type: {type}, sender: {address}, message: {msg}"); return false; } if (msg.FarPublicKey is null) { if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", $"{msg.MsgType} has null far public key", size); - if (_logger.IsDebug) _logger.Debug($"Discovery message without a valid signature {msg.FarAddress} but was {ctx.Channel.RemoteAddress}, type: {type}, sender: {address}, message: {msg}"); + if (_logger.IsDebug) _logger.Debug($"Discovery message without a valid signature {msg.FarAddress} but was {packet.Sender}, type: {type}, sender: {address}, message: {msg}"); return false; } @@ -311,5 +317,142 @@ private async Task LogDisconnectFailureAsync(Task disconnectTask) } } + private async Task ProcessInboundMessagesAsync() + { + try + { + await foreach (InboundDiscoveryPacket packet in _inboundMessages.Reader.ReadAllAsync()) + { + try + { + ProcessInboundMessage(packet); + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error($"Error while dispatching discovery message, type: {packet.Type}, sender: {packet.Address}", e); + } + finally + { + ReferenceCountUtil.Release(packet.Packet); + } + } + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error("Error in discovery message dispatch loop", e); + } + } + + private void ProcessInboundMessage(InboundDiscoveryPacket packet) + { + if (!TryDeserialize(packet, out DiscoveryMsg? msg)) + { + ForwardPacket(packet); + return; + } + + ReportMsgByType(msg, packet.Size); + + if (!ValidateMsg(msg, packet.Type, packet.Address, packet.Packet, packet.Size)) + { + ForwardPacket(packet); + return; + } + + // Discv4 request handling can wait for response packets that must be decoded by this same bounded queue. + DispatchMessage(msg); + } + + private void DispatchMessage(DiscoveryMsg msg) + { + Task dispatchTask = _discoveryMsgListener.OnIncomingMsg(msg); + if (!dispatchTask.IsCompletedSuccessfully) + { + _ = ObserveDispatchFailure(dispatchTask, msg); + } + } + + private async Task ObserveDispatchFailure(Task dispatchTask, DiscoveryMsg msg) + { + try + { + await dispatchTask; + } + catch (Exception e) + { + if (_logger.IsError) _logger.Error($"Error while handling discovery message, type: {msg.MsgType}, sender: {msg.FarAddress}", e); + } + } + + private bool TryDeserialize(InboundDiscoveryPacket packet, [NotNullWhen(true)] out DiscoveryMsg? msg) + { + msg = null; + IByteBuffer content = packet.Packet.Content; + int readerIndex = content.ReaderIndex; + using ArrayPoolDisposableReturn handle = ArrayPoolDisposableReturn.Rent(packet.Size, out byte[] msgBytes); + content.GetBytes(readerIndex, msgBytes, 0, packet.Size); + + try + { + msg = Deserialize(packet.Type, new ArraySegment(msgBytes, 0, packet.Size)); + msg.FarAddress = (IPEndPoint)packet.Address; + return true; + } + catch (Exception e) + { + if (_logger.IsDebug) _logger.Debug($"Error during deserialization of the message, type: {packet.Type}, sender: {packet.Address}, msg: {msgBytes.AsSpan(0, packet.Size).ToHexString()}, {e.Message}"); + return false; + } + } + + private static void ForwardPacket(InboundDiscoveryPacket packet) + { + packet.Packet.Content.ResetReaderIndex(); + packet.Context.FireChannelRead(packet.Packet.Retain()); + } + + private void EnsureDispatchWorkersStarted() + { + if (Interlocked.Exchange(ref _dispatchWorkersStarted, 1) != 0) + { + return; + } + + for (int i = 0; i < _inboundMessageWorkerCount; i++) + { + _ = Task.Run(ProcessInboundMessagesAsync); + } + } + + private sealed class FixedWindowLimiter(int maxCount, TimeSpan window) + { + private readonly object _lock = new(); + private long _windowStartTicks = Stopwatch.GetTimestamp(); + private int _count; + + public bool TryAcquire() + { + lock (_lock) + { + long now = Stopwatch.GetTimestamp(); + if (Stopwatch.GetElapsedTime(_windowStartTicks, now) >= window) + { + _windowStartTicks = now; + _count = 0; + } + + if (_count >= maxCount) + { + return false; + } + + _count++; + return true; + } + } + } + + private readonly record struct InboundDiscoveryPacket(IChannelHandlerContext Context, DatagramPacket Packet, MsgType Type, EndPoint Address, int Size); + public event EventHandler? OnChannelActivated; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs index 691eaa0f7d24..0b60788d53bd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs @@ -156,9 +156,9 @@ protected static void PrepareBufferForSerialization(IByteBuffer byteBuffer, int byteBuffer.WriteByte(msgType); } - protected static IPEndPoint GetAddress(ReadOnlySpan ip, int port) + protected static IPEndPoint GetAddress(ReadOnlySpan ip, int port, bool allowZeroPort = false) { - if ((uint)(port - 1) >= ushort.MaxValue) + if (allowZeroPort ? (uint)port > ushort.MaxValue : (uint)(port - 1) >= ushort.MaxValue) { ThrowInvalidPort(port); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs index 23d5f3f31c9f..22a0d8034a8b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs @@ -51,16 +51,15 @@ public PingMsg Deserialize(IByteBuffer msgBytes) ctx.ReadSequenceLength(); ReadOnlySpan sourceAddress = ctx.DecodeByteArraySpan(IpAddressRlpLimit); - // TODO: please note that we decode only one field for port and if the UDP is different from TCP then - // our discovery messages will not be routed correctly (the fix will not be part of this commit) - ctx.DecodeInt(); // UDP port - int tcpPort = ctx.DecodeInt(); // we assume here that UDP and TCP port are same + int sourceUdpPort = ctx.DecodeInt(); + ctx.DecodeInt(); // TCP port - IPEndPoint source = GetAddress(sourceAddress, tcpPort); + IPEndPoint source = GetAddress(sourceAddress, sourceUdpPort, allowZeroPort: true); ctx.ReadSequenceLength(); ReadOnlySpan destinationAddress = ctx.DecodeByteArraySpan(IpAddressRlpLimit); - IPEndPoint destination = GetAddress(destinationAddress, ctx.DecodeInt()); - ctx.DecodeInt(); // UDP port + int destinationUdpPort = ctx.DecodeInt(); + ctx.DecodeInt(); // TCP port + IPEndPoint destination = GetAddress(destinationAddress, destinationUdpPort, allowZeroPort: true); long expireTime = ctx.DecodeLong(); PingMsg msg = new(FarPublicKey, expireTime, source, destination, Mdc.ToArray()) { Version = version }; From ec864a49b17674468acfd7b7d09d7bc22e648701 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 28 May 2026 14:09:36 +0300 Subject: [PATCH 122/182] Reduce discv5 allocation pressure --- .../Discv5/Discv5CodecTests.cs | 60 +++++++ .../Discv5/Discv5KademliaAdapter.cs | 98 +++++++---- .../Discv5/Discv5MessageCodec.cs | 157 ++++++++++++++---- .../Discv5/Discv5Messages.cs | 2 +- .../Discv5/Discv5PacketCodec.cs | 50 +++--- 5 files changed, 277 insertions(+), 90 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs index dbbe29b27a01..3cba54b8e3d9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs @@ -171,6 +171,66 @@ public void MessageCodec_Roundtrips_FindNode() Assert.That(decodedFindNode.Distances, Is.EqualTo(message.Distances)); } + [Test] + public void MessageCodec_Roundtrips_Pong() + { + Discv5Pong message = new([0, 0, 0, 2], 3, IPAddress.Parse("192.0.2.1"), 30303); + + Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + + Assert.That(decoded, Is.InstanceOf()); + Discv5Pong decodedPong = (Discv5Pong)decoded; + Assert.That(decodedPong.RequestId, Is.EqualTo(message.RequestId)); + Assert.That(decodedPong.EnrSequence, Is.EqualTo(message.EnrSequence)); + Assert.That(decodedPong.RecipientIp, Is.EqualTo(message.RecipientIp)); + Assert.That(decodedPong.RecipientPort, Is.EqualTo(message.RecipientPort)); + } + + [Test] + public void MessageCodec_Roundtrips_TalkReq() + { + Discv5TalkReq message = new([0, 0, 0, 3], "eth"u8.ToArray(), [1, 2, 3, 4]); + + Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + + Assert.That(decoded, Is.InstanceOf()); + Discv5TalkReq decodedTalkReq = (Discv5TalkReq)decoded; + Assert.That(decodedTalkReq.RequestId, Is.EqualTo(message.RequestId)); + Assert.That(decodedTalkReq.Protocol, Is.EqualTo(message.Protocol)); + Assert.That(decodedTalkReq.Request, Is.EqualTo(message.Request)); + } + + [Test] + public void MessageCodec_Roundtrips_TalkResp() + { + Discv5TalkResp message = new([0, 0, 0, 4], [5, 6, 7, 8]); + + Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + + Assert.That(decoded, Is.InstanceOf()); + Discv5TalkResp decodedTalkResp = (Discv5TalkResp)decoded; + Assert.That(decodedTalkResp.RequestId, Is.EqualTo(message.RequestId)); + Assert.That(decodedTalkResp.Response, Is.EqualTo(message.Response)); + } + + [Test] + public void MessageCodec_Roundtrips_Nodes_From_NonZero_ArraySegment() + { + NodeRecord skippedRecord = CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); + NodeRecord expectedRecord = CreateNodeRecord(new PrivateKey(GethNodeBPrivateKey)); + NodeRecord[] records = [skippedRecord, expectedRecord]; + Discv5Nodes message = new([0, 0, 0, 5], 1, new ArraySegment(records, 1, 1)); + + Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + + Assert.That(decoded, Is.InstanceOf()); + Discv5Nodes decodedNodes = (Discv5Nodes)decoded; + Assert.That(decodedNodes.RequestId, Is.EqualTo(message.RequestId)); + Assert.That(decodedNodes.Total, Is.EqualTo(message.Total)); + Assert.That(decodedNodes.Records.Count, Is.EqualTo(1)); + Assert.That(decodedNodes.Records[0].EnrString, Is.EqualTo(expectedRecord.EnrString)); + } + [Test] public void MessageCodec_Rejects_Nodes_With_Invalid_Enr() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index f5f811204997..2d88379ec1ce 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -151,7 +152,7 @@ private async Task SendRequest( TimeSpan timeout, CancellationToken token) { - ResponseKey responseKey = new(receiver.Id.Hash, RequestIdToString(request.RequestId), responseType); + ResponseKey responseKey = new(receiver.Id.Hash, RequestIdKey.From(request.RequestId), responseType); _responseHandlers.Set(responseKey, responseHandler); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -183,7 +184,7 @@ private async Task SendRequest( if (TryGetSession(sessionKey, out Discv5Session? session)) { byte[] sessionNonce = session.GetNextNonce(cryptoRandom); - PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceToString(sessionNonce)); + PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceKey.From(sessionNonce)); _pendingByNonce.Set(sessionPendingNonceKey, new PendingRequest(receiver, message)); byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, sessionNonce); try @@ -201,7 +202,7 @@ private async Task SendRequest( byte[] nonce = cryptoRandom.GenerateRandomBytes(Discv5PacketCodec.NonceSize); byte[] encryptionKey = cryptoRandom.GenerateRandomBytes(16); PendingRequest pendingRequest = new(receiver, message); - PendingNonceKey pendingNonceKey = new(receiver.Address, NonceToString(nonce)); + PendingNonceKey pendingNonceKey = new(receiver.Address, NonceKey.From(nonce)); _pendingByNonce.Set(pendingNonceKey, pendingRequest); byte[] initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); @@ -262,7 +263,7 @@ private async Task HandlePacket(UdpReceiveResult udpPacket, CancellationToken to private async Task HandleWhoAreYou(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) { - PendingNonceKey pendingNonceKey = new(endpoint, NonceToString(packet.Nonce)); + PendingNonceKey pendingNonceKey = new(endpoint, NonceKey.From(packet.Nonce)); if (!_pendingByNonce.TryRemove(pendingNonceKey, out PendingRequest? pendingRequest)) { return; @@ -403,7 +404,7 @@ await SendResponse( private bool HandleResponse(Hash256 nodeId, Discv5Message message) { - ResponseKey responseKey = new(nodeId, RequestIdToString(message.RequestId), message.MessageType); + ResponseKey responseKey = new(nodeId, RequestIdKey.From(message.RequestId), message.MessageType); return _responseHandlers.TryGetValue(responseKey, out IResponseHandler? handler) && handler.Handle(message); } @@ -420,15 +421,16 @@ private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, Canc for (int i = 0; i < records.Length; i += MaxEnrsPerNodesMessage) { int count = Math.Min(MaxEnrsPerNodesMessage, records.Length - i); - NodeRecord[] chunk = records.AsSpan(i, count).ToArray(); + ArraySegment chunk = new(records, i, count); await SendResponse(remoteNode, new Discv5Nodes(findNode.RequestId, total, chunk), token); } } private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) { - HashSet seen = []; - List result = []; + HashSet seen = new(MaxFindNodeRecords); + List result = new(MaxFindNodeRecords); + bool allowNonRoutableRelays = NodeFilter.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); bool includedSelf = false; for (int i = 0; i < distances.Length && result.Count < MaxFindNodeRecords; i++) { @@ -449,26 +451,35 @@ private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) continue; } - Node[] nodes = GetNodesAtDistances([distance], requester); - for (int j = 0; j < nodes.Length && result.Count < MaxFindNodeRecords; j++) + AddFindNodeRecordsAtDistance(distance, requester, allowNonRoutableRelays, seen, result); + } + + return [.. result]; + } + + private void AddFindNodeRecordsAtDistance( + int distance, + Node requester, + bool allowNonRoutableRelays, + HashSet seen, + List result) + { + Node[] nodes = kademlia.Value.GetAllAtDistance(distance); + Hash256 requesterHash = requester.IdHash; + for (int i = 0; i < nodes.Length && result.Count < MaxFindNodeRecords; i++) + { + Node node = nodes[i]; + if (node.IdHash.Equals(requesterHash) || string.IsNullOrEmpty(node.Enr) || !seen.Add(node.Id.Hash)) { - Node node = nodes[j]; - if (string.IsNullOrEmpty(node.Enr) || !seen.Add(node.Id.Hash)) - { - continue; - } + continue; + } - NodeRecord? record = GetFindNodeRecord( - node, - allowNonRoutableRelays: NodeFilter.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address)); - if (record is not null) - { - result.Add(record); - } + NodeRecord? record = GetFindNodeRecord(node, allowNonRoutableRelays); + if (record is not null) + { + result.Add(record); } } - - return [.. result]; } private NodeRecord? GetFindNodeRecord(Node node, bool allowNonRoutableRelays) @@ -537,10 +548,6 @@ private byte[] CreateRequestId() return requestId.AsSpan().WithoutLeadingZeros().ToArray(); } - private static string RequestIdToString(byte[] requestId) => Convert.ToHexString(requestId); - - private static string NonceToString(byte[] nonce) => Convert.ToHexString(nonce); - private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGetValue(sessionKey, out session); private void SetSession(SessionKey sessionKey, Discv5Session session) @@ -700,9 +707,38 @@ private void Trim() private readonly record struct ChallengeKey(Hash256 NodeId, IPEndPoint Endpoint); - private readonly record struct PendingNonceKey(IPEndPoint Endpoint, string Nonce); + private readonly record struct PendingNonceKey(IPEndPoint Endpoint, NonceKey Nonce); + + private readonly record struct ResponseKey(Hash256 NodeId, RequestIdKey RequestId, Discv5MessageType MessageType); + + private readonly record struct RequestIdKey(ulong Value, byte Length) + { + public static RequestIdKey From(ReadOnlySpan requestId) + { + ulong value = 0; + for (int i = 0; i < requestId.Length; i++) + { + value = (value << 8) | requestId[i]; + } + + return new RequestIdKey(value, checked((byte)requestId.Length)); + } + } - private readonly record struct ResponseKey(Hash256 NodeId, string RequestId, Discv5MessageType MessageType); + private readonly record struct NonceKey(ulong Prefix, uint Suffix) + { + public static NonceKey From(ReadOnlySpan nonce) + { + if (nonce.Length != Discv5PacketCodec.NonceSize) + { + throw new ArgumentException($"Nonce must be {Discv5PacketCodec.NonceSize} bytes.", nameof(nonce)); + } + + return new NonceKey( + BinaryPrimitives.ReadUInt64BigEndian(nonce[..sizeof(ulong)]), + BinaryPrimitives.ReadUInt32BigEndian(nonce.Slice(sizeof(ulong), sizeof(uint)))); + } + } private sealed record PendingRequest(Node Receiver, Discv5Message Message); @@ -772,7 +808,7 @@ public bool Handle(Discv5Message message) _total ??= nodes.Total; _received++; - for (int i = 0; i < nodes.Records.Length && _nodes.Count < MaxNodesResponseRecords; i++) + for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs index aa72ae9c21ff..9e0c87ff7067 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs @@ -13,30 +13,12 @@ internal static class Discv5MessageCodec public static byte[] Encode(Discv5Message message) { - Rlp data = message switch - { - Discv5Ping ping => Rlp.Encode(Rlp.Encode(ping.RequestId), Rlp.Encode(ping.EnrSequence)), - Discv5Pong pong => Rlp.Encode( - Rlp.Encode(pong.RequestId), - Rlp.Encode(pong.EnrSequence), - Rlp.Encode(pong.RecipientIp.GetAddressBytes()), - Rlp.Encode(pong.RecipientPort)), - Discv5FindNode findNode => Rlp.Encode(Rlp.Encode(findNode.RequestId), Rlp.Encode(findNode.Distances)), - Discv5Nodes nodes => Rlp.Encode( - Rlp.Encode(nodes.RequestId), - Rlp.Encode(nodes.Total), - EncodeNodeRecords(nodes.Records)), - Discv5TalkReq talkReq => Rlp.Encode( - Rlp.Encode(talkReq.RequestId), - Rlp.Encode(talkReq.Protocol), - Rlp.Encode(talkReq.Request)), - Discv5TalkResp talkResp => Rlp.Encode(Rlp.Encode(talkResp.RequestId), Rlp.Encode(talkResp.Response)), - _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") - }; - - byte[] result = new byte[data.Length + 1]; + int contentLength = GetContentLength(message); + byte[] result = new byte[Rlp.LengthOfSequence(contentLength) + 1]; result[0] = (byte)message.MessageType; - data.Bytes.CopyTo(result.AsSpan(1)); + RlpStream stream = new(result) { Position = 1 }; + stream.StartSequence(contentLength); + EncodeContent(stream, message); return result; } @@ -68,15 +50,134 @@ public static Discv5Message Decode(ReadOnlySpan message) return decoded; } - private static Rlp EncodeNodeRecords(NodeRecord[] records) + private static int GetContentLength(Discv5Message message) => message switch + { + Discv5Ping ping => Rlp.LengthOf(ping.RequestId) + Rlp.LengthOf(ping.EnrSequence), + Discv5Pong pong => Rlp.LengthOf(pong.RequestId) + + Rlp.LengthOf(pong.EnrSequence) + + GetAddressRlpLength(pong.RecipientIp) + + Rlp.LengthOf(pong.RecipientPort), + Discv5FindNode findNode => Rlp.LengthOf(findNode.RequestId) + GetDistancesLength(findNode.Distances), + Discv5Nodes nodes => Rlp.LengthOf(nodes.RequestId) + Rlp.LengthOf(nodes.Total) + GetNodeRecordsLength(nodes.Records), + Discv5TalkReq talkReq => Rlp.LengthOf(talkReq.RequestId) + Rlp.LengthOf(talkReq.Protocol) + Rlp.LengthOf(talkReq.Request), + Discv5TalkResp talkResp => Rlp.LengthOf(talkResp.RequestId) + Rlp.LengthOf(talkResp.Response), + _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") + }; + + private static void EncodeContent(RlpStream stream, Discv5Message message) + { + switch (message) + { + case Discv5Ping ping: + stream.Encode(ping.RequestId); + stream.Encode(ping.EnrSequence); + break; + case Discv5Pong pong: + stream.Encode(pong.RequestId); + stream.Encode(pong.EnrSequence); + EncodeAddress(stream, pong.RecipientIp); + stream.Encode(pong.RecipientPort); + break; + case Discv5FindNode findNode: + stream.Encode(findNode.RequestId); + EncodeDistances(stream, findNode.Distances); + break; + case Discv5Nodes nodes: + stream.Encode(nodes.RequestId); + stream.Encode(nodes.Total); + EncodeNodeRecords(stream, nodes.Records); + break; + case Discv5TalkReq talkReq: + stream.Encode(talkReq.RequestId); + stream.Encode(talkReq.Protocol); + stream.Encode(talkReq.Request); + break; + case Discv5TalkResp talkResp: + stream.Encode(talkResp.RequestId); + stream.Encode(talkResp.Response); + break; + default: + throw new RlpException($"Unsupported discv5 message {message.GetType().Name}."); + } + } + + private static int GetDistancesLength(int[] distances) + { + int contentLength = 0; + for (int i = 0; i < distances.Length; i++) + { + contentLength += Rlp.LengthOf(distances[i]); + } + + return Rlp.LengthOfSequence(contentLength); + } + + private static void EncodeDistances(RlpStream stream, int[] distances) + { + int contentLength = 0; + for (int i = 0; i < distances.Length; i++) + { + contentLength += Rlp.LengthOf(distances[i]); + } + + stream.StartSequence(contentLength); + for (int i = 0; i < distances.Length; i++) + { + stream.Encode(distances[i]); + } + } + + private static int GetNodeRecordsLength(IReadOnlyList records) + { + int contentLength = 0; + for (int i = 0; i < records.Count; i++) + { + contentLength += records[i].GetRlpLengthWithSignature(); + } + + return Rlp.LengthOfSequence(contentLength); + } + + private static void EncodeNodeRecords(RlpStream stream, IReadOnlyList records) + { + int contentLength = 0; + for (int i = 0; i < records.Count; i++) + { + contentLength += records[i].GetRlpLengthWithSignature(); + } + + stream.StartSequence(contentLength); + for (int i = 0; i < records.Count; i++) + { + records[i].Encode(stream); + } + } + + private static int GetAddressRlpLength(IPAddress ip) + { + if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetwork) + { + return Rlp.LengthOfByteString(4, 0); + } + + if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetworkV6) + { + return Rlp.LengthOfByteString(16, 0); + } + + return Rlp.LengthOf(ip.GetAddressBytes()); + } + + private static void EncodeAddress(RlpStream stream, IPAddress ip) { - Rlp[] encodedRecords = new Rlp[records.Length]; - for (int i = 0; i < records.Length; i++) + Span bytes = stackalloc byte[16]; + if (ip.TryWriteBytes(bytes, out int bytesWritten)) { - encodedRecords[i] = new Rlp(records[i].ToRlpBytes()); + stream.Encode(bytes[..bytesWritten]); + return; } - return Rlp.Encode(encodedRecords); + stream.Encode(ip.GetAddressBytes()); } private static byte[] DecodeRequestId(ref Rlp.ValueDecoderContext ctx) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs index 1f9844894dfe..b75f1ebd880d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs @@ -36,7 +36,7 @@ internal sealed record Discv5FindNode(byte[] RequestId, int[] Distances) : Discv public override Discv5MessageType MessageType => Discv5MessageType.FindNode; } -internal sealed record Discv5Nodes(byte[] RequestId, int Total, NodeRecord[] Records) : Discv5Message(RequestId) +internal sealed record Discv5Nodes(byte[] RequestId, int Total, IReadOnlyList Records) : Discv5Message(RequestId) { public override Discv5MessageType MessageType => Discv5MessageType.Nodes; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs index f348f96a2f41..fe15d753eb29 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs @@ -21,9 +21,7 @@ internal enum Discv5PacketFlag : byte internal sealed record Discv5Packet( Discv5PacketFlag Flag, - byte[] MaskingIv, byte[] Nonce, - byte[] Header, byte[] AuthData, byte[] Message, byte[] MessageAd) @@ -37,8 +35,6 @@ internal sealed record Discv5Session(PublicKey RemotePublicKey, byte[] ReadKey, { private long _nonceCounter; - public byte[] RemoteNodeId => RemotePublicKey.Hash.BytesToArray(); - public byte[] GetNextNonce(ICryptoRandom random) { byte[] nonce = new byte[Discv5PacketCodec.NonceSize]; @@ -74,19 +70,18 @@ public sealed class Discv5PacketCodec( private readonly PrivateKey _privateKey = nodeKey.Unprotect(); private readonly PublicKey _publicKey = nodeKey.PublicKey; + private readonly byte[] _localNodeId = nodeKey.PublicKey.Hash.BytesToArray(); private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; private readonly ICryptoRandom _cryptoRandom = cryptoRandom; private readonly IEcdsa _ecdsa = ecdsa; - internal byte[] LocalNodeId => _publicKey.Hash.BytesToArray(); - public void Dispose() => _privateKey.Dispose(); internal byte[] EncodeOrdinary(PublicKey destination, byte[] encryptionKey, Discv5Message message, byte[]? nonce = null) { byte[] actualNonce = nonce ?? CreateNonce(); - byte[] authData = LocalNodeId.ToArray(); - return EncodePacket(destination.Hash.BytesToArray(), Discv5PacketFlag.Ordinary, actualNonce, authData, encryptionKey, message); + byte[] authData = _localNodeId; + return EncodePacket(destination.Hash.Bytes, Discv5PacketFlag.Ordinary, actualNonce, authData, encryptionKey, message); } internal byte[] EncodeWhoAreYou(byte[] destinationNodeId, byte[] requestNonce, ulong enrSequence, out Discv5Challenge challenge) @@ -107,20 +102,20 @@ internal byte[] EncodeHandshake(PublicKey destination, Discv5Challenge challenge DeriveKeys( destination, ephemeralKey, - LocalNodeId, + _localNodeId, destination.Hash.Bytes, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); byte[] ephemeralPublicKey = ephemeralKey.CompressedPublicKey.Bytes; - byte[] idSignature = SignIdNonce(challenge.ChallengeData, ephemeralPublicKey, destination.Hash.BytesToArray()); + byte[] idSignature = SignIdNonce(challenge.ChallengeData, ephemeralPublicKey, destination.Hash.Bytes); byte[] record = challenge.EnrSequence < _nodeRecordProvider.Current.EnrSequence ? _nodeRecordProvider.Current.ToRlpBytes() : []; byte[] authData = new byte[HandshakeAuthDataHeadSize + idSignature.Length + ephemeralPublicKey.Length + record.Length]; - LocalNodeId.CopyTo(authData, 0); + _localNodeId.CopyTo(authData, 0); authData[NodeIdSize] = IdSignatureSize; authData[NodeIdSize + 1] = EphemeralPublicKeySize; idSignature.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize)); @@ -128,11 +123,11 @@ internal byte[] EncodeHandshake(PublicKey destination, Discv5Challenge challenge record.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize + idSignature.Length + ephemeralPublicKey.Length)); session = new Discv5Session(destination, recipientKey, initiatorKey); - return EncodePacket(destination.Hash.BytesToArray(), Discv5PacketFlag.Handshake, CreateNonce(), authData, initiatorKey, message); + return EncodePacket(destination.Hash.Bytes, Discv5PacketFlag.Handshake, CreateNonce(), authData, initiatorKey, message); } internal bool TryDecode(ReadOnlySpan packet, out Discv5Packet decoded) - => TryDecode(packet, LocalNodeId, out decoded); + => TryDecode(packet, _localNodeId, out decoded); internal static bool TryDecode(ReadOnlySpan packet, ReadOnlySpan localNodeId, out Discv5Packet decoded) { @@ -142,7 +137,7 @@ internal static bool TryDecode(ReadOnlySpan packet, ReadOnlySpan loc return false; } - byte[] maskingIv = packet[..MaskingIvSize].ToArray(); + ReadOnlySpan maskingIv = packet[..MaskingIvSize]; byte[] staticHeader = AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize)); if (!staticHeader.AsSpan(0, ProtocolId.Length).SequenceEqual(ProtocolId)) { @@ -173,10 +168,10 @@ internal static bool TryDecode(ReadOnlySpan packet, ReadOnlySpan loc byte[] authData = header.AsSpan(StaticHeaderSize, authDataSize).ToArray(); byte[] message = packet[(MaskingIvSize + headerSize)..].ToArray(); byte[] messageAd = new byte[MaskingIvSize + header.Length]; - maskingIv.CopyTo(messageAd, 0); + maskingIv.CopyTo(messageAd); header.CopyTo(messageAd.AsSpan(MaskingIvSize)); - decoded = new Discv5Packet(flag, maskingIv, nonce, header, authData, message, messageAd); + decoded = new Discv5Packet(flag, nonce, authData, message, messageAd); return true; } @@ -259,13 +254,13 @@ internal bool TryDecryptHandshake( return false; } - if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature, challenge.ChallengeData, ephemeralPublicKey.Bytes, LocalNodeId)) + if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature, challenge.ChallengeData, ephemeralPublicKey.Bytes, _localNodeId)) { return false; } PublicKey remotePublicKey = remoteCompressedPublicKey.Decompress(); - DeriveKeys(ephemeralPublicKey, sourceNodeId, LocalNodeId, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); + DeriveKeys(ephemeralPublicKey, sourceNodeId, _localNodeId, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); if (!TryDecryptMessage(packet, initiatorKey, out message)) { @@ -293,7 +288,7 @@ internal static bool TryGetSourceNodeId(Discv5Packet packet, out byte[] sourceNo } private byte[] EncodePacket( - byte[] destinationNodeId, + ReadOnlySpan destinationNodeId, Discv5PacketFlag flag, byte[] nonce, byte[] authData, @@ -302,7 +297,7 @@ private byte[] EncodePacket( => EncodePacket(destinationNodeId, flag, nonce, authData, encryptionKey, message, out _); private byte[] EncodePacket( - byte[] destinationNodeId, + ReadOnlySpan destinationNodeId, Discv5PacketFlag flag, byte[] nonce, byte[] authData, @@ -324,7 +319,7 @@ private byte[] EncodePacket( encryptedMessage = EncryptMessage(encryptionKey, nonce, Discv5MessageCodec.Encode(message), messageAd); } - byte[] maskedHeader = AesCtrTransform(destinationNodeId.AsSpan(0, AesKeySize), maskingIv, header); + byte[] maskedHeader = AesCtrTransform(destinationNodeId[..AesKeySize], maskingIv, header); byte[] packet = new byte[MaskingIvSize + maskedHeader.Length + encryptedMessage.Length]; maskingIv.CopyTo(packet, 0); maskedHeader.CopyTo(packet.AsSpan(MaskingIvSize)); @@ -372,19 +367,14 @@ private static byte[] AesCtrTransform(ReadOnlySpan key, ReadOnlySpan aes.Padding = PaddingMode.None; aes.Key = key.ToArray(); - using ICryptoTransform encryptor = aes.CreateEncryptor(); Span counter = stackalloc byte[MaskingIvSize]; iv.CopyTo(counter); Span keyStream = stackalloc byte[MaskingIvSize]; - byte[] counterBlock = new byte[MaskingIvSize]; - byte[] keyStreamBlock = new byte[MaskingIvSize]; int offset = 0; while (offset < input.Length) { - counter.CopyTo(counterBlock); - encryptor.TransformBlock(counterBlock, 0, counterBlock.Length, keyStreamBlock, 0); - keyStreamBlock.CopyTo(keyStream); + aes.EncryptEcb(counter, keyStream, PaddingMode.None); int blockLength = Math.Min(MaskingIvSize, input.Length - offset); for (int i = 0; i < blockLength; i++) @@ -487,14 +477,14 @@ private static byte[] HkdfExpand(byte[] prk, byte[] info, int length) return result; } - private byte[] SignIdNonce(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + private byte[] SignIdNonce(byte[] challengeData, byte[] ephemeralPublicKey, ReadOnlySpan recipientNodeId) { byte[] signingHash = CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); Signature signature = _ecdsa.Sign(_privateKey, new ValueHash256(signingHash)); return signature.Bytes.ToArray(); } - private bool VerifyIdSignature(CompressedPublicKey signer, byte[] signatureBytes, byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + private bool VerifyIdSignature(CompressedPublicKey signer, byte[] signatureBytes, byte[] challengeData, byte[] ephemeralPublicKey, ReadOnlySpan recipientNodeId) { byte[] signingHash = CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); for (int recoveryId = 0; recoveryId <= 1; recoveryId++) @@ -513,7 +503,7 @@ private bool VerifyIdSignature(CompressedPublicKey signer, byte[] signatureBytes internal static byte[] CalculateIdSignatureHashForTest(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) => CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); - private static byte[] CalculateIdSignatureHash(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + private static byte[] CalculateIdSignatureHash(byte[] challengeData, byte[] ephemeralPublicKey, ReadOnlySpan recipientNodeId) { byte[] signingInput = new byte[IdentityProofText.Length + challengeData.Length + ephemeralPublicKey.Length + recipientNodeId.Length]; IdentityProofText.CopyTo(signingInput, 0); From d0ada32637779f3ab172cc4308a17d54c18036de Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 28 May 2026 17:46:21 +0300 Subject: [PATCH 123/182] Add distance service --- .../Nethermind.Kademlia/DoubleEndedLru.cs | 145 ++++++----- .../FromKeyNodeHashProvider.cs | 5 +- .../Nethermind.Kademlia/Hash256XorUtils.cs | 108 -------- .../Nethermind.Kademlia/IKademlia.cs | 48 ++-- .../Nethermind.Kademlia/IKademliaDistance.cs | 41 +++ .../IKademliaMessageSender.cs | 14 +- .../Nethermind.Kademlia/IKeyOperator.cs | 30 ++- .../Nethermind.Kademlia/ILookupAlgo.cs | 6 +- .../Nethermind.Kademlia/INodeHashProvider.cs | 12 +- .../Nethermind.Kademlia/IRoutingTable.cs | 15 +- src/Nethermind/Nethermind.Kademlia/KBucket.cs | 24 +- .../Nethermind.Kademlia/KBucketTree.cs | 241 ++++++++++-------- .../Nethermind.Kademlia/Kademlia.cs | 76 +++--- .../Nethermind.Kademlia/KademliaFactory.cs | 99 +++++++ .../Nethermind.Kademlia/KademliaHash.cs | 85 ------ .../LookupKNearestNeighbour.cs | 169 ++++++------ .../Nethermind.Kademlia.csproj | 6 +- .../Nethermind.Kademlia/NodeHealthTracker.cs | 112 ++++++-- .../Discv4/IteratorNodeLookupTests.cs | 18 +- .../Discv5/Discv5KademliaAdapterTests.cs | 8 +- .../Discv5/Discv5WireTests.cs | 8 +- ...sts.cs => Hash256KademliaDistanceTests.cs} | 45 ++-- .../Kademlia/IdentityNodeHashProvider.cs | 6 +- .../Kademlia/KBucketTests.cs | 32 +-- .../Kademlia/KBucketTreeTests.cs | 18 +- .../Kademlia/KademliaSimulation.cs | 41 +-- .../Kademlia/KademliaTests.cs | 37 +-- .../Kademlia/LookupKNearestNeighbourTests.cs | 30 +-- .../Kademlia/NodeHealthTrackerTests.cs | 67 ++--- .../Discv4/DiscV4KademliaModule.cs | 7 +- .../Discv4/IteratorNodeLookup.cs | 45 ++-- .../Discv4/KademliaNodeSource.cs | 3 +- .../Discv4/PublicKeyKeyOperator.cs | 6 +- .../Discv5/DiscV5KademliaModule.cs | 5 +- .../Discv5/Discv5KademliaAdapter.cs | 22 +- .../Discv5/Discv5NodeSource.cs | 3 +- .../Kademlia/Hash256KademliaDistance.cs | 140 ++++++++++ .../Kademlia/KademliaModule.cs | 20 +- 38 files changed, 1035 insertions(+), 762 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs create mode 100644 src/Nethermind/Nethermind.Kademlia/IKademliaDistance.cs create mode 100644 src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs delete mode 100644 src/Nethermind/Nethermind.Kademlia/KademliaHash.cs rename src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/{Hash256XorUtilsTests.cs => Hash256KademliaDistanceTests.cs} (70%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 366a0dbbf3b9..51b9fb84c36c 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -1,112 +1,121 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Threading; using NonBlocking; namespace Nethermind.Kademlia; -public class DoubleEndedLru(int capacity) where TNode : notnull +public class DoubleEndedLru(int capacity) + where TNode : notnull + where TKadKey : notnull { - private readonly McsLock _lock = new(); + private readonly object _lock = new(); - private readonly LinkedList<(KademliaHash, TNode)> _queue = new(); - private readonly ConcurrentDictionary> _hashMapping = new(); + private readonly LinkedList<(TKadKey, TNode)> _queue = new(); + private readonly ConcurrentDictionary> _hashMapping = new(); public int Count => _queue.Count; - public BucketAddResult AddOrRefresh(in KademliaHash hash, TNode node) + public BucketAddResult AddOrRefresh(in TKadKey hash, TNode node) { - using McsLock.Disposable _ = _lock.Acquire(); - - if (_hashMapping.TryGetValue(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) - { - _queue.Remove(listNode); - listNode.Value = (hash, node); - _queue.AddFirst(listNode); - return BucketAddResult.Refreshed; - } - - if (_queue.Count >= capacity) + lock (_lock) { - return BucketAddResult.Full; + if (_hashMapping.TryGetValue(hash, out LinkedListNode<(TKadKey, TNode)>? listNode)) + { + _queue.Remove(listNode); + listNode.Value = (hash, node); + _queue.AddFirst(listNode); + return BucketAddResult.Refreshed; + } + + if (_queue.Count >= capacity) + { + return BucketAddResult.Full; + } + + listNode = _queue.AddFirst((hash, node)); + _hashMapping.TryAdd(hash, listNode); + return BucketAddResult.Added; } - - listNode = _queue.AddFirst((hash, node)); - _hashMapping.TryAdd(hash, listNode); - return BucketAddResult.Added; } - public bool TryPopHead(out KademliaHash hash, out TNode? node) + public bool TryPopHead(out TKadKey hash, out TNode? node) { - using McsLock.Disposable _ = _lock.Acquire(); - - LinkedListNode<(KademliaHash, TNode)>? front = _queue.First; - if (front == null) + lock (_lock) { - hash = default; - node = default; - return false; - } - - _queue.Remove(front); - hash = front.Value.Item1; - node = front.Value.Item2; - _hashMapping.TryRemove(front.Value.Item1, out front); + LinkedListNode<(TKadKey, TNode)>? front = _queue.First; + if (front == null) + { + hash = default!; + node = default; + return false; + } + + _queue.Remove(front); + hash = front.Value.Item1; + node = front.Value.Item2; + _hashMapping.TryRemove(front.Value.Item1, out front); - return true; + return true; + } } public bool TryGetLast(out TNode? last) { - using McsLock.Disposable _ = _lock.Acquire(); - - LinkedListNode<(KademliaHash, TNode)>? lastNode = _queue.Last; - if (lastNode == null) + lock (_lock) { - last = default; - return false; + LinkedListNode<(TKadKey, TNode)>? lastNode = _queue.Last; + if (lastNode == null) + { + last = default; + return false; + } + + last = lastNode.Value.Item2; + return true; } - - last = lastNode.Value.Item2; - return true; } - public bool Remove(KademliaHash hash) + public bool Remove(TKadKey hash) { - using McsLock.Disposable _ = _lock.Acquire(); - - if (_hashMapping.TryRemove(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) + lock (_lock) { - _queue.Remove(listNode); - return true; - } + if (_hashMapping.TryRemove(hash, out LinkedListNode<(TKadKey, TNode)>? listNode)) + { + _queue.Remove(listNode); + return true; + } - return false; + return false; + } } public TNode[] GetAll() { - using McsLock.Disposable _ = _lock.Acquire(); - TNode[] result = new TNode[_queue.Count]; - int i = 0; - foreach ((KademliaHash, TNode node) entry in _queue) result[i++] = entry.node; - return result; + lock (_lock) + { + TNode[] result = new TNode[_queue.Count]; + int i = 0; + foreach ((TKadKey, TNode node) entry in _queue) result[i++] = entry.node; + return result; + } } - public (KademliaHash, TNode)[] GetAllWithHash() + public (TKadKey, TNode)[] GetAllWithHash() { - using McsLock.Disposable _ = _lock.Acquire(); - (KademliaHash, TNode)[] result = new (KademliaHash, TNode)[_queue.Count]; - int i = 0; - foreach ((KademliaHash, TNode) entry in _queue) result[i++] = entry; - return result; + lock (_lock) + { + (TKadKey, TNode)[] result = new (TKadKey, TNode)[_queue.Count]; + int i = 0; + foreach ((TKadKey, TNode) entry in _queue) result[i++] = entry; + return result; + } } - public bool Contains(in KademliaHash hash) => _hashMapping.ContainsKey(hash); + public bool Contains(in TKadKey hash) => _hashMapping.ContainsKey(hash); - public TNode? GetByHash(KademliaHash hash) + public TNode? GetByHash(TKadKey hash) { - if (_hashMapping.TryGetValue(hash, out LinkedListNode<(KademliaHash, TNode)>? listNode)) + if (_hashMapping.TryGetValue(hash, out LinkedListNode<(TKadKey, TNode)>? listNode)) { return listNode.Value.Item2; } diff --git a/src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs b/src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs index 8e0e7cd56a64..0e6557adfd30 100644 --- a/src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Kademlia/FromKeyNodeHashProvider.cs @@ -4,7 +4,8 @@ namespace Nethermind.Kademlia; -public class FromKeyNodeHashProvider(IKeyOperator keyOperator) : INodeHashProvider +public class FromKeyNodeHashProvider(IKeyOperator keyOperator) : INodeHashProvider + where TKadKey : notnull { - public KademliaHash GetHash(TNode node) => keyOperator.GetNodeHash(node); + public TKadKey GetHash(TNode node) => keyOperator.GetNodeHash(node); } diff --git a/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs b/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs deleted file mode 100644 index 21a0fd44cf9d..000000000000 --- a/src/Nethermind/Nethermind.Kademlia/Hash256XorUtils.cs +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Numerics; - -namespace Nethermind.Kademlia; - -public static class Hash256XorUtils -{ - public static int CalculateLogDistance(KademliaHash h1, KademliaHash h2) - { - KademliaHash xor = XorDistance(h1, h2); - int zeros = 0; - for (int i = 0; i < 32; i += 1) - { - byte xord = xor.Bytes[i]; - if (xord == 0) - { - zeros += 8; - continue; - } - - int nonZeroPostfix = 1; - while ((xord >>= 1) != 0) - { - nonZeroPostfix++; - } - zeros += 8 - nonZeroPostfix; - - break; - } - return MaxDistance - zeros; - } - - public const int MaxDistance = 256; - - public static int Compare(KademliaHash a, KademliaHash b, KademliaHash c) - { - KademliaHash ac = XorDistance(a, c); - KademliaHash bc = XorDistance(b, c); - return ac.CompareTo(bc); - } - - public static KademliaHash XorDistance(KademliaHash hash1, KademliaHash hash2) - { - ReadOnlySpan hash1Bytes = hash1.Bytes; - ReadOnlySpan hash2Bytes = hash2.Bytes; - Span result = stackalloc byte[KademliaHash.Length]; - - int i = 0; - for (; i <= result.Length - Vector.Count; i += Vector.Count) - { - (new Vector(hash1Bytes[i..]) ^ new Vector(hash2Bytes[i..])).CopyTo(result[i..]); - } - - for (; i < result.Length; i++) - { - result[i] = (byte)(hash1Bytes[i] ^ hash2Bytes[i]); - } - - return KademliaHash.FromBytes(result); - } - - public static KademliaHash GetRandomHashAtDistance(KademliaHash currentHash, int distance) => GetRandomHashAtDistance(currentHash, distance, Random.Shared); - - public static KademliaHash GetRandomHashAtDistance(KademliaHash currentHash, int distance, Random random) - { - if ((uint)distance > MaxDistance) - { - throw new ArgumentOutOfRangeException(nameof(distance), distance, $"Distance must be between 0 and {MaxDistance}."); - } - - // TODO: Just add a min/max range per bucket and randomized between them. - Span randomized = stackalloc byte[KademliaHash.Length]; - random.NextBytes(randomized); - return CopyForRandom(currentHash, randomized, MaxDistance - distance); - } - - private static KademliaHash CopyForRandom(KademliaHash currentHash, Span randomizedHash, int distance) - { - if (distance >= 256) return currentHash; - - currentHash.Bytes[0..(distance / 8)].CopyTo(randomizedHash); - - int remainingBit = distance % 8; - int remainingBitByte = distance / 8; - byte mask = (byte)(~((1 << (8 - remainingBit)) - 1)); - byte randomized = randomizedHash[remainingBitByte]; - byte original = currentHash.Bytes[remainingBitByte]; - randomizedHash[remainingBitByte] = (byte)((original & mask) | (randomized & (~mask))); - - if (distance <= 255) - { - // So it always assume that the next bucket (the closer one) is always populated and therefore, - // the bits here for that distance must not be the same as in currentHash. - int nextBit = distance % 8; - int nextBitByte = distance / 8; - mask = (byte)(1 << (7 - nextBit)); - randomized = randomizedHash[nextBitByte]; - byte opposite = (byte)~(currentHash.Bytes[nextBitByte]); - - byte final = (byte)((opposite & mask) | (randomized & ~(mask))); - randomizedHash[nextBitByte] = final; - } - - return KademliaHash.FromBytes(randomizedHash); - } -} diff --git a/src/Nethermind/Nethermind.Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs index b786b1ae6a9c..c7eeb022b775 100644 --- a/src/Nethermind/Nethermind.Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs @@ -4,52 +4,51 @@ namespace Nethermind.Kademlia; /// -/// Main kademlia interface. High level code is expected to interface with this interface. +/// Provides routing-table maintenance and iterative Kademlia lookup over caller-defined key and node types. /// -/// -/// +/// The protocol-specific lookup key type. +/// The protocol-specific node/contact type. public interface IKademlia { /// - /// Add node to the table. + /// Adds a node to the routing table or refreshes its position when it is already present. /// - /// + /// Node to add or refresh. void AddOrRefresh(TNode node); /// - /// Remove from to the table. + /// Removes a node from the routing table. /// - /// + /// Node to remove. void Remove(TNode node); /// - /// Start timers, refresh and such for maintenance of the table. + /// Runs periodic bootstrap and routing-table refresh until cancelled. /// - /// + /// Cancellation token that stops the maintenance loop. Task Run(CancellationToken token); /// - /// Just do the bootstrap sequence, which is to initiate a lookup on current node id. - /// Also do a refresh on all bucket which is not part of joining strictly speaking. + /// Runs one bootstrap pass and refreshes stale non-empty buckets. /// - /// + /// Cancellation token for the bootstrap pass. Task Bootstrap(CancellationToken token); /// - /// Lookup k nearest neighbour closest to the target hash. This will traverse the network. + /// Looks up nodes closest to by traversing the network. /// - /// - /// - /// + /// Protocol-specific lookup key. + /// Cancellation token for the lookup. + /// Optional result size. Defaults to . Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null); /// - /// Return the K nearest table entry from target. This does not traverse the network. The returned array is not - /// sorted. The routing table may return the exact same array for optimization purpose. + /// Returns the closest routing-table entries to without traversing the network. /// - /// - /// - /// + /// Protocol-specific lookup key. + /// Optional node to exclude from the result. + /// Whether to exclude the local node from the result. + /// The returned array is not sorted and may be an internal routing-table array. TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false); /// @@ -59,18 +58,17 @@ public interface IKademlia TNode[] GetAllAtDistance(int distance); /// - /// Called when a TNode is added to the routing table. + /// Raised when a node is added to the routing table. /// event EventHandler OnNodeAdded; /// - /// Called when a TNode is removed from the routing table. + /// Raised when a node is removed from the routing table. /// event EventHandler OnNodeRemoved; /// - /// Iterate all nodes with no ordering + /// Iterates all nodes currently in the routing table without ordering guarantees. /// - /// IEnumerable IterateNodes(); } diff --git a/src/Nethermind/Nethermind.Kademlia/IKademliaDistance.cs b/src/Nethermind/Nethermind.Kademlia/IKademliaDistance.cs new file mode 100644 index 000000000000..8757f4f8edfd --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/IKademliaDistance.cs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Kademlia; + +/// +/// Defines Kademlia XOR-distance operations for a consumer-owned key type. +/// +/// The key-space value used by the routing table. +public interface IKademliaDistance where TKadKey : notnull +{ + /// + /// The maximum log distance supported by the key space. + /// + int MaxDistance { get; } + + /// + /// The all-zero key for the key space. + /// + TKadKey Zero { get; } + + /// + /// Returns the XOR log distance between and . + /// + int CalculateLogDistance(TKadKey left, TKadKey right); + + /// + /// Compares two keys by XOR distance to . + /// + int Compare(TKadKey left, TKadKey right, TKadKey target); + + /// + /// Returns whether the bit at is set. + /// + bool GetBit(TKadKey key, int index); + + /// + /// Returns a key with the bit at set. + /// + TKadKey SetBit(TKadKey key, int index); +} diff --git a/src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs b/src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs index c19767de888d..94514118f90e 100644 --- a/src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKademliaMessageSender.cs @@ -4,13 +4,19 @@ namespace Nethermind.Kademlia; /// -/// Should be exposed by application to kademlia so that kademlia can send out message. +/// Sends protocol-specific Kademlia requests for the core table. /// -/// -/// +/// The protocol-specific lookup key type. +/// The protocol-specific node/contact type. public interface IKademliaMessageSender { + /// + /// Sends a liveness probe to . + /// Task Ping(TNode receiver, CancellationToken token); + + /// + /// Requests neighbours closest to from . + /// Task FindNeighbours(TNode receiver, TKey target, CancellationToken token); } - diff --git a/src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs b/src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs index 34774eaaf707..04081bdced41 100644 --- a/src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKeyOperator.cs @@ -4,14 +4,30 @@ namespace Nethermind.Kademlia; /// -/// Define operations for and . +/// Maps protocol-specific keys and nodes to the Kademlia key space. /// -/// -/// -public interface IKeyOperator +/// The protocol-specific lookup key type. +/// The protocol-specific node/contact type. +/// The key-space value used by the routing table. +public interface IKeyOperator where TKadKey : notnull { + /// + /// Gets the lookup key represented by . + /// TKey GetKey(TNode node); - KademliaHash GetKeyHash(TKey key); - KademliaHash GetNodeHash(TNode node) => GetKeyHash(GetKey(node)); - TKey CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth); + + /// + /// Hashes a protocol-specific key into the fixed-width Kademlia key space. + /// + TKadKey GetKeyHash(TKey key); + + /// + /// Hashes a protocol-specific node into the fixed-width Kademlia key space. + /// + TKadKey GetNodeHash(TNode node) => GetKeyHash(GetKey(node)); + + /// + /// Creates a random protocol-specific key at the requested log distance from . + /// + TKey CreateRandomKeyAtDistance(TKadKey nodePrefix, int depth); } diff --git a/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs b/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs index 65944e0f390b..293179724db2 100644 --- a/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs +++ b/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs @@ -10,7 +10,9 @@ namespace Nethermind.Kademlia; /// Find closest-k is also used to determine which node should store a particular value which is used by /// store RPC (not implemented). /// -public interface ILookupAlgo +public interface ILookupAlgo + where TNode : notnull + where TKadKey : notnull { /// /// The find neighbour operation here is configurable because the same algorithm is also used for finding @@ -22,7 +24,7 @@ public interface ILookupAlgo /// /// Task Lookup( - KademliaHash targetHash, + TKadKey targetHash, int k, Func> findNeighbourOp, CancellationToken token diff --git a/src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs b/src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs index 8394a758c7db..946f95a370a6 100644 --- a/src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Kademlia/INodeHashProvider.cs @@ -5,10 +5,14 @@ namespace Nethermind.Kademlia; /// -/// Just a convenient interface with only one generic parameter. +/// Maps a node/contact to its Kademlia key-space value. /// -/// -public interface INodeHashProvider +/// The protocol-specific node/contact type. +/// The key-space value used by the routing table. +public interface INodeHashProvider where TKadKey : notnull { - KademliaHash GetHash(TNode node); + /// + /// Gets the Kademlia key-space value for . + /// + TKadKey GetHash(TNode node); } diff --git a/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs index 144b52320462..a087f88ef204 100644 --- a/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs +++ b/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs @@ -3,14 +3,17 @@ namespace Nethermind.Kademlia; -public interface IRoutingTable where TNode : notnull +public interface IRoutingTable + where TNode : notnull + where TKadKey : notnull { - BucketAddResult TryAddOrRefresh(in KademliaHash hash, TNode item, out TNode? toRefresh); - bool Remove(in KademliaHash hash); - TNode[] GetKNearestNeighbour(KademliaHash hash, KademliaHash? exclude = null, bool excludeSelf = false); + BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? toRefresh); + bool Remove(in TKadKey hash); + TNode[] GetKNearestNeighbour(TKadKey hash, bool excludeSelf = false); + TNode[] GetKNearestNeighbourExcluding(TKadKey hash, TKadKey exclude, bool excludeSelf = false); TNode[] GetAllAtDistance(int i); - IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> IterateBuckets(); - TNode? GetByHash(KademliaHash nodeId); + IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> IterateBuckets(); + TNode? GetByHash(TKadKey nodeId); void LogDebugInfo(); event EventHandler? OnNodeAdded; event EventHandler? OnNodeRemoved; diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index a900cf6d3942..9b7f9d17c99e 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -4,11 +4,13 @@ namespace Nethermind.Kademlia; -public class KBucket(int k) where TNode : notnull +public class KBucket(int k) + where TNode : notnull + where TKadKey : notnull { private readonly int _k = k; - private DoubleEndedLru _items = new(k); - private DoubleEndedLru _replacement = new(k); + private DoubleEndedLru _items = new(k); + private DoubleEndedLru _replacement = new(k); public int Count => _items.Count; @@ -21,7 +23,7 @@ public class KBucket(int k) where TNode : notnull /// /// /// - public BucketAddResult TryAddOrRefresh(in KademliaHash hash, TNode item, out TNode? toRefresh) + public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? toRefresh) { TNode? previous = _items.GetByHash(hash); BucketAddResult addResult = _items.AddOrRefresh(hash, item); @@ -45,13 +47,13 @@ public BucketAddResult TryAddOrRefresh(in KademliaHash hash, TNode item, out TNo public TNode[] GetAll() => _cachedArray; - public (KademliaHash, TNode)[] GetAllWithHash() => _items.GetAllWithHash(); + public (TKadKey, TNode)[] GetAllWithHash() => _items.GetAllWithHash(); - public bool RemoveAndReplace(in KademliaHash hash) + public bool RemoveAndReplace(in TKadKey hash) { if (!_items.Remove(hash)) return false; - if (_replacement.TryPopHead(out KademliaHash replacementHash, out TNode? replacement)) + if (_replacement.TryPopHead(out TKadKey replacementHash, out TNode? replacement)) { _items.AddOrRefresh(replacementHash, replacement!); } @@ -62,14 +64,14 @@ public bool RemoveAndReplace(in KademliaHash hash) public void Clear() { - _items = new DoubleEndedLru(_k); - _replacement = new DoubleEndedLru(_k); + _items = new DoubleEndedLru(_k); + _replacement = new DoubleEndedLru(_k); _cachedArray = _items.GetAll(); } - public bool ContainsNode(in KademliaHash hash) => _items.Contains(hash); + public bool ContainsNode(in TKadKey hash) => _items.Contains(hash); - public TNode? GetByHash(KademliaHash hash) => _items.GetByHash(hash); + public TNode? GetByHash(TKadKey hash) => _items.GetByHash(hash); private static bool ShouldUpdateCachedArray(TNode? previous, TNode item) { diff --git a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index f9f56a78dd32..a0380f9a4eca 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -2,79 +2,92 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Text; -using Nethermind.Core.Threading; -using Nethermind.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Nethermind.Kademlia; -public class KBucketTree : IRoutingTable where TNode : notnull +public class KBucketTree : IRoutingTable + where TNode : notnull + where TKadKey : notnull { - private class TreeNode(int k, KademliaHash prefix) + private class TreeNode(int k, TKadKey prefix) { - public KBucket Bucket { get; } = new KBucket(k); + public KBucket Bucket { get; } = new KBucket(k); public TreeNode? Left { get; set; } public TreeNode? Right { get; set; } - public KademliaHash Prefix { get; } = prefix; + public TKadKey Prefix { get; } = prefix; public bool IsLeaf => Left == null && Right == null; } private readonly TreeNode _root; private readonly int _b; private readonly int _k; - private readonly KademliaHash _currentNodeHash; + private readonly TKadKey _currentNodeHash; + private readonly IKademliaDistance _distance; private readonly ILogger _logger; - // TODO: Double check and probably make lockless - private readonly McsLock _lock = new(); + private readonly object _lock = new(); - public KBucketTree(KademliaConfig config, INodeHashProvider nodeHashProvider, ILogManager logManager) + public KBucketTree( + KademliaConfig config, + INodeHashProvider nodeHashProvider, + IKademliaDistance distance, + ILoggerFactory? loggerFactory = null) { _k = config.KSize; _b = config.Beta; + _distance = distance; _currentNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); - _root = new TreeNode(config.KSize, KademliaHash.Zero); - _logger = logManager.GetClassLogger>(); - if (_logger.IsDebug) _logger.Debug($"Initialized KBucketTree with k={_k}, currentNodeId={_currentNodeHash}"); + _root = new TreeNode(config.KSize, distance.Zero); + _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Initialized KBucketTree with k={K}, currentNodeId={CurrentNodeId}", _k, _currentNodeHash); + } } - public BucketAddResult TryAddOrRefresh(in KademliaHash nodeHash, TNode node, out TNode? toRefresh) + public BucketAddResult TryAddOrRefresh(in TKadKey nodeHash, TNode node, out TNode? toRefresh) { BucketAddResult resp; bool fireAdded; - using (_lock.Acquire()) + lock (_lock) { - if (_logger.IsDebug) _logger.Debug($"Adding node {node} with XOR distance {Hash256XorUtils.XorDistance(_currentNodeHash, nodeHash)}"); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Adding node {Node} with XOR distance {Distance}", node, _distance.CalculateLogDistance(_currentNodeHash, nodeHash)); + } TreeNode current = _root; // As in, what would be the depth of the node assuming all branch on the traversal is populated. - int logDistance = Hash256XorUtils.MaxDistance - Hash256XorUtils.CalculateLogDistance(_currentNodeHash, nodeHash); + int logDistance = _distance.MaxDistance - _distance.CalculateLogDistance(_currentNodeHash, nodeHash); int depth = 0; while (true) { if (current.IsLeaf) { - if (_logger.IsTrace) _logger.Trace($"Reached leaf node at depth {depth}"); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Reached leaf node at depth {Depth}", depth); resp = current.Bucket.TryAddOrRefresh(nodeHash, node, out toRefresh); fireAdded = resp == BucketAddResult.Added; if (resp is BucketAddResult.Added or BucketAddResult.Refreshed) { - if (_logger.IsDebug) _logger.Debug($"Successfully added/refreshed node {node} in bucket at depth {depth}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Successfully added/refreshed node {Node} in bucket at depth {Depth}", node, depth); break; } if (resp == BucketAddResult.Full && ShouldSplit(depth, logDistance)) { - if (_logger.IsTrace) _logger.Trace($"Splitting bucket at depth {depth}"); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Splitting bucket at depth {Depth}", depth); SplitBucket(depth, current); continue; } - if (_logger.IsDebug) _logger.Debug($"Failed to add node {nodeHash} {node}. Bucket at depth {depth} is full. {_k} {current.Bucket.Count}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Failed to add node {Hash} {Node}. Bucket at depth {Depth} is full. {K} {Count}", nodeHash, node, depth, _k, current.Bucket.Count); break; } - bool goRight = GetBit(nodeHash, depth); - if (_logger.IsTrace) _logger.Trace($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); + bool goRight = _distance.GetBit(nodeHash, depth); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Traversing {Direction} at depth {Depth}", goRight ? "right" : "left", depth); current = goRight ? current.Right! : current.Left!; depth++; @@ -85,13 +98,15 @@ public BucketAddResult TryAddOrRefresh(in KademliaHash nodeHash, TNode node, out return resp; } - public TNode? GetByHash(KademliaHash hash) + public TNode? GetByHash(TKadKey hash) { - using McsLock.Disposable _ = _lock.Acquire(); - return GetBucketForHash(hash).GetByHash(hash); + lock (_lock) + { + return GetBucketForHash(hash).GetByHash(hash); + } } - private KBucket GetBucketForHash(KademliaHash nodeHash) + private KBucket GetBucketForHash(TKadKey nodeHash) { TreeNode current = _root; int depth = 0; @@ -99,12 +114,12 @@ private KBucket GetBucketForHash(KademliaHash nodeHash) { if (current.IsLeaf) { - if (_logger.IsDebug) _logger.Debug($"Reached leaf node at depth {depth}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Reached leaf node at depth {Depth}", depth); return current.Bucket; } - bool goRight = GetBit(nodeHash, depth); - if (_logger.IsDebug) _logger.Debug($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); + bool goRight = _distance.GetBit(nodeHash, depth); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Traversing {Direction} at depth {Depth}", goRight ? "right" : "left", depth); current = goRight ? current.Right! : current.Left!; depth++; @@ -113,43 +128,41 @@ private KBucket GetBucketForHash(KademliaHash nodeHash) private bool ShouldSplit(int depth, int targetLogDistance) { - bool shouldSplit = depth < 256 && targetLogDistance + _b >= depth; - if (_logger.IsDebug) _logger.Debug($"ShouldSplit at depth {depth}: {shouldSplit}"); + bool shouldSplit = depth < _distance.MaxDistance && targetLogDistance + _b >= depth; + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("ShouldSplit at depth {Depth}: {ShouldSplit}", depth, shouldSplit); return shouldSplit; } private void SplitBucket(int depth, TreeNode node) { node.Left = new TreeNode(_k, node.Prefix); - byte[] rightPrefixBytes = node.Prefix.Bytes.ToArray(); - rightPrefixBytes[depth / 8] |= (byte)(1 << (7 - (depth % 8))); - node.Right = new TreeNode(_k, KademliaHash.FromBytes(rightPrefixBytes)); + node.Right = new TreeNode(_k, _distance.SetBit(node.Prefix, depth)); - if (_logger.IsDebug) _logger.Debug($"Created children at depth {depth + 1}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Created children at depth {Depth}", depth + 1); // Iterate from oldest to newest so the new buckets preserve original LRU order. - (KademliaHash, TNode)[] items = node.Bucket.GetAllWithHash(); + (TKadKey, TNode)[] items = node.Bucket.GetAllWithHash(); for (int i = items.Length - 1; i >= 0; i--) { - (KademliaHash itemHash, TNode value) = items[i]; - TreeNode? targetNode = GetBit(itemHash, depth) ? node.Right : node.Left; + (TKadKey itemHash, TNode value) = items[i]; + TreeNode? targetNode = _distance.GetBit(itemHash, depth) ? node.Right : node.Left; targetNode.Bucket.TryAddOrRefresh(itemHash, value, out _); - if (_logger.IsDebug) _logger.Debug($"Moved item ({itemHash}, {value}) to {(GetBit(itemHash, depth) ? "right" : "left")} child"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Moved item ({Hash}, {Value}) to {Direction} child", itemHash, value, _distance.GetBit(itemHash, depth) ? "right" : "left"); } node.Bucket.Clear(); - if (_logger.IsDebug) _logger.Debug($"Finished splitting bucket. Left count: {node.Left.Bucket.Count}, Right count: {node.Right.Bucket.Count}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Finished splitting bucket. Left count: {LeftCount}, Right count: {RightCount}", node.Left.Bucket.Count, node.Right.Bucket.Count); } - public bool Remove(in KademliaHash nodeHash) + public bool Remove(in TKadKey nodeHash) { bool removed; TNode? removedNode; - using (_lock.Acquire()) + lock (_lock) { - if (_logger.IsDebug) _logger.Debug($"Attempting to remove node {nodeHash} with hash {nodeHash}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Attempting to remove node {NodeHash}", nodeHash); - KBucket bucket = GetBucketForHash(nodeHash); + KBucket bucket = GetBucketForHash(nodeHash); removedNode = bucket.GetByHash(nodeHash); removed = bucket.RemoveAndReplace(nodeHash); } @@ -160,25 +173,26 @@ public bool Remove(in KademliaHash nodeHash) public TNode[] GetAllAtDistance(int distance) { - using McsLock.Disposable _ = _lock.Acquire(); - - if (_logger.IsDebug) _logger.Debug($"Getting all nodes at distance {distance}"); - List result = []; - GetAllAtDistanceRecursive(_root, 0, distance, result); - if (_logger.IsDebug) _logger.Debug($"Found {result.Count} nodes at distance {distance}"); - return [.. result]; + lock (_lock) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Getting all nodes at distance {Distance}", distance); + List result = []; + GetAllAtDistanceRecursive(_root, 0, distance, result); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Found {Count} nodes at distance {Distance}", result.Count, distance); + return [.. result]; + } } private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, List result) { - int targetDepth = Hash256XorUtils.MaxDistance - distance; + int targetDepth = _distance.MaxDistance - distance; if (node.IsLeaf) { if (depth <= targetDepth) { - foreach ((KademliaHash hash, TNode item) in node.Bucket.GetAllWithHash()) + foreach ((TKadKey hash, TNode item) in node.Bucket.GetAllWithHash()) { - if (Hash256XorUtils.CalculateLogDistance(hash, _currentNodeHash) == distance) + if (_distance.CalculateLogDistance(hash, _currentNodeHash) == distance) { result.Add(item); } @@ -193,7 +207,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L { if (depth < targetDepth) { - bool goRight = GetBit(_currentNodeHash, depth); + bool goRight = _distance.GetBit(_currentNodeHash, depth); if (goRight) { GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); @@ -205,7 +219,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L } else if (depth == targetDepth) { - bool goRight = GetBit(_currentNodeHash, depth); + bool goRight = _distance.GetBit(_currentNodeHash, depth); // Note: We go the opposite direction here, as the same direction would have a distance + 1 if (goRight) { @@ -224,15 +238,16 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L } } - public IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> IterateBuckets() + public IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> IterateBuckets() { - using McsLock.Disposable _ = _lock.Acquire(); - - // Well, it need to ToArray, otherwise the lock does not really do anything. - return DoIterateBucketRandomHashes(_root, 0).ToArray(); + lock (_lock) + { + // Well, it need to ToArray, otherwise the lock does not really do anything. + return DoIterateBucketRandomHashes(_root, 0).ToArray(); + } } - private IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> DoIterateBucketRandomHashes(TreeNode node, int depth) + private IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> DoIterateBucketRandomHashes(TreeNode node, int depth) { if (node.IsLeaf) { @@ -240,30 +255,30 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L } else { - foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) + foreach ((TKadKey Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) { yield return bucketInfo; } - foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) + foreach ((TKadKey Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) { yield return bucketInfo; } } } - private IEnumerable<(KademliaHash, TNode)> IterateNeighbour(KademliaHash hash) + private IEnumerable<(TKadKey, TNode)> IterateNeighbour(TKadKey hash) { foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(_root, 0, hash)) { - foreach ((KademliaHash, TNode) entry in treeNode.Bucket.GetAllWithHash()) + foreach ((TKadKey, TNode) entry in treeNode.Bucket.GetAllWithHash()) { yield return entry; } } } - private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNode, int depth, KademliaHash target) + private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNode, int depth, TKadKey target) { if (currentNode.IsLeaf) { @@ -271,7 +286,7 @@ private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNod } else { - if (GetBit(target, depth)) + if (_distance.GetBit(target, depth)) { foreach (TreeNode treeNode in IterateNodeFromClosestToTarget(currentNode.Right!, depth + 1, target)) { @@ -298,46 +313,44 @@ private IEnumerable IterateNodeFromClosestToTarget(TreeNode currentNod } } - public TNode[] GetKNearestNeighbour(KademliaHash hash, KademliaHash? exclude, bool excludeSelf) - { - using McsLock.Disposable _ = _lock.Acquire(); + public TNode[] GetKNearestNeighbour(TKadKey hash, bool excludeSelf = false) => GetKNearestNeighbour(hash, default!, false, excludeSelf); - KBucket firstBucket = GetBucketForHash(hash); - bool shouldNotContainExcludedNode = exclude == null || !firstBucket.ContainsNode(exclude.Value); - bool shouldNotContainSelf = !excludeSelf || !firstBucket.ContainsNode(_currentNodeHash); + public TNode[] GetKNearestNeighbourExcluding(TKadKey hash, TKadKey exclude, bool excludeSelf = false) => GetKNearestNeighbour(hash, exclude, true, excludeSelf); - if (shouldNotContainExcludedNode && shouldNotContainSelf) + private TNode[] GetKNearestNeighbour(TKadKey hash, TKadKey exclude, bool hasExclude, bool excludeSelf) + { + lock (_lock) { - TNode[] nodes = firstBucket.GetAll(); - if (nodes.Length == _k) + KBucket firstBucket = GetBucketForHash(hash); + bool shouldNotContainExcludedNode = !hasExclude || !firstBucket.ContainsNode(exclude); + bool shouldNotContainSelf = !excludeSelf || !firstBucket.ContainsNode(_currentNodeHash); + + if (shouldNotContainExcludedNode && shouldNotContainSelf) { - // Fast path. In theory, most of the time, this would be the taken path, where no array - // concatenation or creation is needed. - return nodes; + TNode[] nodes = firstBucket.GetAll(); + if (nodes.Length == _k) + { + // Fast path. In theory, most of the time, this would be the taken path, where no array + // concatenation or creation is needed. + return nodes; + } } - } - TNode[] resultArr = new TNode[_k]; - int count = 0; - foreach ((KademliaHash itemHash, TNode item) in IterateNeighbour(hash)) - { - if (exclude != null && itemHash == exclude.Value) continue; - if (excludeSelf && itemHash == _currentNodeHash) continue; - resultArr[count++] = item; - if (count == _k) break; - } - - if (count == _k) return resultArr; - TNode[] truncated = new TNode[count]; - Array.Copy(resultArr, truncated, count); - return truncated; - } + TNode[] resultArr = new TNode[_k]; + int count = 0; + foreach ((TKadKey itemHash, TNode item) in IterateNeighbour(hash)) + { + if (hasExclude && EqualityComparer.Default.Equals(itemHash, exclude)) continue; + if (excludeSelf && EqualityComparer.Default.Equals(itemHash, _currentNodeHash)) continue; + resultArr[count++] = item; + if (count == _k) break; + } - private bool GetBit(KademliaHash hash, int index) - { - int byteIndex = index / 8; - int bitIndex = index % 8; - return (hash.Bytes[byteIndex] & (1 << (7 - bitIndex))) != 0; + if (count == _k) return resultArr; + TNode[] truncated = new TNode[count]; + Array.Copy(resultArr, truncated, count); + return truncated; + } } private void LogTreeStructureRecursive(TreeNode node, string indent, bool last, int depth, StringBuilder sb) @@ -391,25 +404,31 @@ void TraverseTree(TreeNode node, int depth) TraverseTree(_root, 0); - if (_logger.IsDebug) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.Debug($"Tree Statistics:\n" + - $"Total Nodes: {totalNodes}\n" + - $"Total Buckets: {totalBuckets}\n" + - $"Max Depth: {maxDepth}\n" + - $"Total Items: {totalItems}\n" + - $"Average Items per Bucket: {(double)totalItems / totalBuckets:F2}"); + _logger.LogDebug( + "Tree Statistics: Total Nodes: {TotalNodes}, Total Buckets: {TotalBuckets}, Max Depth: {MaxDepth}, Total Items: {TotalItems}, Average Items per Bucket: {AverageItemsPerBucket:F2}", + totalNodes, + totalBuckets, + maxDepth, + totalItems, + (double)totalItems / totalBuckets); } } + private void LogTreeStructure() { + if (!_logger.IsEnabled(LogLevel.Debug)) return; + StringBuilder sb = new(); LogTreeStructureRecursive(_root, "", true, 0, sb); - _logger.Info($"Current Tree Structure:\n{sb}"); + _logger.LogDebug("Current Tree Structure:{NewLine}{Tree}", Environment.NewLine, sb); } public void LogDebugInfo() { + if (!_logger.IsEnabled(LogLevel.Debug)) return; + LogTreeStatistics(); LogTreeStructure(); } @@ -422,7 +441,7 @@ public int Size get { int total = 0; - foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) in IterateBuckets()) + foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in IterateBuckets()) { total += Bucket.Count; } diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index ff88b26e6e84..030642aa0545 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -2,46 +2,54 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics; -using Nethermind.Core; -using Nethermind.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Nethermind.Kademlia; -public class Kademlia : IKademlia where TNode : notnull +/// +/// Maintains a Kademlia routing table and runs network lookups through caller-provided message transport. +/// +public class Kademlia : IKademlia + where TNode : notnull + where TKadKey : notnull { private readonly IKademliaMessageSender _kademliaMessageSender; - private readonly IKeyOperator _keyOperator; - private readonly IRoutingTable _routingTable; - private readonly ILookupAlgo _lookupAlgo; + private readonly IKeyOperator _keyOperator; + private readonly IRoutingTable _routingTable; + private readonly ILookupAlgo _lookupAlgo; private readonly INodeHealthTracker _nodeHealthTracker; private readonly ILogger _logger; private readonly TNode _currentNodeId; - private readonly KademliaHash _currentNodeIdAsHash; + private readonly TKadKey _currentNodeIdAsHash; private readonly int _kSize; private readonly TimeSpan _refreshInterval; private readonly TimeSpan _bucketRefreshInterval; private readonly IReadOnlyList _bootNodes; - private readonly ITimestamper _timestamper; - private readonly Dictionary _lastBucketRefreshTicks = []; + private readonly TimeProvider _timeProvider; + private readonly Dictionary _lastBucketRefreshTicks = []; private readonly object _lastBucketRefreshLock = new(); + /// + /// Creates a Kademlia table over the supplied routing, lookup, health, and transport abstractions. + /// public Kademlia( - IKeyOperator keyOperator, + IKeyOperator keyOperator, IKademliaMessageSender sender, - IRoutingTable routingTable, - ILookupAlgo lookupAlgo, - ILogManager logManager, + IRoutingTable routingTable, + ILookupAlgo lookupAlgo, INodeHealthTracker nodeHealthTracker, - ITimestamper timestamper, - KademliaConfig config) + KademliaConfig config, + ILoggerFactory? loggerFactory = null, + TimeProvider? timeProvider = null) { _keyOperator = keyOperator; _kademliaMessageSender = sender; _routingTable = routingTable; _lookupAlgo = lookupAlgo; _nodeHealthTracker = nodeHealthTracker; - _logger = logManager.GetClassLogger>(); + _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); _currentNodeId = config.CurrentNodeId; _currentNodeIdAsHash = _keyOperator.GetNodeHash(_currentNodeId); @@ -49,7 +57,7 @@ public Kademlia( _refreshInterval = config.RefreshInterval; _bucketRefreshInterval = config.BucketRefreshInterval; _bootNodes = config.BootNodes; - _timestamper = timestamper; + _timeProvider = timeProvider ?? TimeProvider.System; AddOrRefresh(_currentNodeId); for (int i = 0; i < _bootNodes.Count; i++) @@ -66,7 +74,7 @@ public Kademlia( public TNode[] GetAllAtDistance(int i) => _routingTable.GetAllAtDistance(i); - private bool SameAsSelf(TNode node) => _keyOperator.GetNodeHash(node) == _currentNodeIdAsHash; + private bool SameAsSelf(TNode node) => EqualityComparer.Default.Equals(_keyOperator.GetNodeHash(node), _currentNodeIdAsHash); public Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) => _lookupAlgo.Lookup( _keyOperator.GetKeyHash(key), @@ -75,7 +83,7 @@ public Task LookupNodesClosest(TKey key, CancellationToken token, int? { if (SameAsSelf(nextNode)) { - KademliaHash keyHash = _keyOperator.GetKeyHash(key); + TKadKey keyHash = _keyOperator.GetKeyHash(key); return _routingTable.GetKNearestNeighbour(keyHash); } return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); @@ -97,7 +105,7 @@ public async Task Run(CancellationToken token) } catch (Exception e) { - if (_logger.IsError) _logger.Error("Bootstrap iteration failed.", e); + _logger.LogError(e, "Bootstrap iteration failed."); } await Task.Delay(_refreshInterval, token); @@ -125,11 +133,11 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } catch (Exception e) { - if (_logger.IsDebug) _logger.Debug($"Bootnode ping failed for {node}. {e}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug(e, "Bootnode ping failed for {Node}.", node); } }); - if (_logger.IsInfo) _logger.Info($"Online bootnodes: {onlineBootNodes}"); + _logger.LogInformation("Online bootnodes: {OnlineBootNodes}", onlineBootNodes); TKey currentNodeIdAsKey = _keyOperator.GetKey(_currentNodeId); await LookupNodesClosest(currentNodeIdAsKey, token); @@ -138,7 +146,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => // Refresh stale non-empty buckets one by one. A refresh means to do a k-nearest node lookup for a random hash // for that particular bucket. - foreach ((KademliaHash Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) + foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { if (!ShouldRefreshBucket(Prefix, Bucket)) continue; @@ -146,18 +154,18 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => await LookupNodesClosest(keyToLookup, token); } - if (_logger.IsDebug) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.Debug($"Bootstrap completed. Took {sw}."); + _logger.LogDebug("Bootstrap completed. Took {Elapsed}.", sw.Elapsed); _routingTable.LogDebugInfo(); } } - private bool ShouldRefreshBucket(KademliaHash prefix, KBucket bucket) + private bool ShouldRefreshBucket(TKadKey prefix, KBucket bucket) { if (bucket.Count == 0) return false; - long nowTicks = _timestamper.UtcNow.Ticks; + long nowTicks = _timeProvider.GetUtcNow().Ticks; lock (_lastBucketRefreshLock) { if (_lastBucketRefreshTicks.TryGetValue(prefix, out long lastRefreshTicks) && @@ -173,10 +181,14 @@ private bool ShouldRefreshBucket(KademliaHash prefix, KBucket bucket) public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false) { - KademliaHash? excludeHash = null; - if (excluding != null) excludeHash = _keyOperator.GetNodeHash(excluding); - KademliaHash hash = _keyOperator.GetKeyHash(target); - return _routingTable.GetKNearestNeighbour(hash, excludeHash, excludeSelf); + TKadKey hash = _keyOperator.GetKeyHash(target); + if (excluding is null) + { + return _routingTable.GetKNearestNeighbour(hash, excludeSelf); + } + + TKadKey excludeHash = _keyOperator.GetNodeHash(excluding); + return _routingTable.GetKNearestNeighbourExcluding(hash, excludeHash, excludeSelf); } public event EventHandler OnNodeAdded @@ -193,7 +205,7 @@ public event EventHandler OnNodeRemoved public IEnumerable IterateNodes() { - foreach ((KademliaHash _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) + foreach ((TKadKey _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) { foreach (TNode node in Bucket.GetAll()) { diff --git a/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs b/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs new file mode 100644 index 000000000000..6c887d2fe854 --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Microsoft.Extensions.Logging; + +namespace Nethermind.Kademlia; + +/// +/// Creates the default Kademlia routing table, lookup algorithm, health tracker, and facade. +/// +public static class KademliaFactory +{ + /// + /// Creates the default Kademlia component graph for consumers that do not use a dependency-injection container. + /// + /// Maps nodes and lookup keys to the Kademlia key space. + /// Compares and manipulates values in the Kademlia key space. + /// Sends protocol-specific ping and find-neighbour requests. + /// Kademlia table and maintenance settings. + /// Optional logger factory. When omitted, logging is disabled. + /// Optional time provider used for bucket refresh scheduling. + public static KademliaComponents Create( + IKeyOperator keyOperator, + IKademliaDistance distance, + IKademliaMessageSender sender, + KademliaConfig config, + ILoggerFactory? loggerFactory = null, + TimeProvider? timeProvider = null) + where TNode : notnull + where TKadKey : notnull + { + ArgumentNullException.ThrowIfNull(keyOperator); + ArgumentNullException.ThrowIfNull(distance); + ArgumentNullException.ThrowIfNull(sender); + ArgumentNullException.ThrowIfNull(config); + + FromKeyNodeHashProvider nodeHashProvider = new(keyOperator); + KBucketTree routingTable = new(config, nodeHashProvider, distance, loggerFactory); + NodeHealthTracker nodeHealthTracker = new(config, routingTable, nodeHashProvider, sender, loggerFactory); + LookupKNearestNeighbour lookup = new(routingTable, nodeHashProvider, distance, nodeHealthTracker, config, loggerFactory); + Kademlia kademlia = new( + keyOperator, + sender, + routingTable, + lookup, + nodeHealthTracker, + config, + loggerFactory, + timeProvider); + + return new KademliaComponents( + kademlia, + routingTable, + lookup, + nodeHashProvider, + nodeHealthTracker); + } +} + +/// +/// Owns a Kademlia instance and the default components created for it. +/// +public sealed class KademliaComponents( + Kademlia kademlia, + IRoutingTable routingTable, + ILookupAlgo lookup, + INodeHashProvider nodeHashProvider, + NodeHealthTracker nodeHealthTracker) : IDisposable + where TNode : notnull + where TKadKey : notnull +{ + /// + /// The high-level Kademlia facade. + /// + public Kademlia Kademlia { get; } = kademlia; + + /// + /// The routing table used by . + /// + public IRoutingTable RoutingTable { get; } = routingTable; + + /// + /// The iterative closest-node lookup algorithm used by . + /// + public ILookupAlgo Lookup { get; } = lookup; + + /// + /// Maps nodes to their Kademlia hash. + /// + public INodeHashProvider NodeHashProvider { get; } = nodeHashProvider; + + /// + /// Tracks liveness and evicts failed nodes. + /// + public NodeHealthTracker NodeHealthTracker { get; } = nodeHealthTracker; + + /// + public void Dispose() => NodeHealthTracker.Dispose(); +} diff --git a/src/Nethermind/Nethermind.Kademlia/KademliaHash.cs b/src/Nethermind/Nethermind.Kademlia/KademliaHash.cs deleted file mode 100644 index 3e1d7740043e..000000000000 --- a/src/Nethermind/Nethermind.Kademlia/KademliaHash.cs +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; - -namespace Nethermind.Kademlia; - -/// -/// Fixed-width 256-bit identifier used by the Kademlia routing table and XOR-distance operations. -/// -public readonly struct KademliaHash : IComparable, IEquatable -{ - private readonly ValueHash256 _value; - - /// - /// Number of bytes in a Kademlia hash. - /// - public const int Length = 32; - - /// - /// The all-zero hash value. - /// - public static KademliaHash Zero { get; } = new(new ValueHash256()); - - /// - /// Creates a hash from a hexadecimal string. - /// - /// A 32-byte hexadecimal string, with or without the 0x prefix. - public KademliaHash(string hex) - : this(new ValueHash256(hex)) - { - } - - private KademliaHash(ValueHash256 value) => _value = value; - - /// - /// Gets the hash bytes. - /// - public ReadOnlySpan Bytes => _value.BytesAsSpan; - - /// - /// Creates a hash from exactly 32 bytes. - /// - /// The bytes to copy into the hash. - /// Thrown when is not 32 bytes long. - public static KademliaHash FromBytes(ReadOnlySpan bytes) - { - if (bytes.Length != Length) - { - throw new ArgumentException($"Kademlia hash must be {Length} bytes.", nameof(bytes)); - } - - return new KademliaHash(new ValueHash256(bytes)); - } - - /// - /// Copies the hash into a new byte array. - /// - public byte[] ToArray() => _value.Bytes.ToArray(); - - /// - public int CompareTo(KademliaHash other) => _value.CompareTo(other._value); - - /// - public bool Equals(KademliaHash other) => _value == other._value; - - /// - public override bool Equals(object? obj) => obj is KademliaHash other && Equals(other); - - /// - public override int GetHashCode() => _value.GetHashCode(); - - /// - public override string ToString() => _value.ToString(); - - /// - /// Returns whether two hashes contain the same bytes. - /// - public static bool operator ==(KademliaHash left, KademliaHash right) => left.Equals(right); - - /// - /// Returns whether two hashes contain different bytes. - /// - public static bool operator !=(KademliaHash left, KademliaHash right) => !left.Equals(right); -} diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 7c6e6e018ef7..1f0e94ba0abe 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics.CodeAnalysis; -using Nethermind.Core.Threading; -using Nethermind.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using NonBlocking; namespace Nethermind.Kademlia; @@ -16,47 +16,53 @@ namespace Nethermind.Kademlia; /// earlier as it converge to the content faster, but take more query for findnodes due to a more strict stop /// condition. /// -public class LookupKNearestNeighbour( - IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, +public class LookupKNearestNeighbour( + IRoutingTable routingTable, + INodeHashProvider nodeHashProvider, + IKademliaDistance distance, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILogManager logManager) : ILookupAlgo where TNode : notnull + ILoggerFactory? loggerFactory = null) : ILookupAlgo + where TNode : notnull + where TKadKey : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimeout; - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); public async Task Lookup( - KademliaHash targetHash, + TKadKey targetHash, int k, Func> findNeighbourOp, CancellationToken token ) { - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Initiate lookup for hash {TargetHash}", targetHash); + } using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); token = cts.Token; - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); - IComparer comparer = Comparer.Create((h1, h2) => - Hash256XorUtils.Compare(h1, h2, targetHash)); - IComparer comparerReverse = Comparer.Create((h1, h2) => - Hash256XorUtils.Compare(h2, h1, targetHash)); + IComparer comparer = Comparer.Create((h1, h2) => + distance.Compare(h1, h2, targetHash)); + IComparer comparerReverse = Comparer.Create((h1, h2) => + distance.Compare(h2, h1, targetHash)); - McsLock queueLock = new(); + object queueLock = new(); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(KademliaHash, TNode), KademliaHash> bestSeen = new(comparer); + PriorityQueue<(TKadKey, TNode), TKadKey> bestSeen = new(comparer); // Ordered by highest distance. Added on result. Get popped as result. - PriorityQueue<(KademliaHash, TNode), KademliaHash> finalResult = new(comparerReverse); + PriorityQueue<(TKadKey, TNode), TKadKey> finalResult = new(comparerReverse); - foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, default)) + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) { - KademliaHash nodeHash = nodeHashProvider.GetHash(node); + TKadKey nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); bestSeen.Enqueue((nodeHash, node), nodeHash); } @@ -72,7 +78,7 @@ CancellationToken token while (!Volatile.Read(ref finished)) { token.ThrowIfCancellationRequested(); - if (!TryGetNodeToQuery(out (KademliaHash hash, TNode node)? toQuery)) + if (!TryGetNodeToQuery(out (TKadKey hash, TNode node)? toQuery)) { if (queryingTask > 0) { @@ -82,7 +88,7 @@ CancellationToken token } // No node to query and running query. - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Stopping lookup. No node to query."); break; } @@ -90,7 +96,7 @@ CancellationToken token { if (ShouldStopDueToNoBetterResult(out int round)) { - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Stopping lookup. No better result."); break; } @@ -156,64 +162,66 @@ CancellationToken token catch (Exception e) { nodeHealthTracker.OnRequestFailed(node); - if (_logger.IsWarn) _logger.Warn($"Find neighbour op failed. {e}"); - if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); + _logger.LogWarning(e, "Find neighbour op failed."); return (node, null); } } - bool TryGetNodeToQuery([NotNullWhen(true)] out (KademliaHash, TNode)? toQuery) + bool TryGetNodeToQuery([NotNullWhen(true)] out (TKadKey, TNode)? toQuery) { - using McsLock.Disposable _ = queueLock.Acquire(); - if (bestSeen.Count == 0) + lock (queueLock) { - toQuery = default; - // No more node to query. - // Note: its possible that there are other worker currently which may add to bestSeen. - return false; - } + if (bestSeen.Count == 0) + { + toQuery = default; + // No more node to query. + // Note: its possible that there are other worker currently which may add to bestSeen. + return false; + } - Interlocked.Increment(ref queryingTask); - toQuery = bestSeen.Dequeue(); - return true; + Interlocked.Increment(ref queryingTask); + toQuery = bestSeen.Dequeue(); + return true; + } } - void ProcessResult(KademliaHash hash, TNode toQuery, (TNode, TNode[]? neighbours)? valueTuple, int round) + void ProcessResult(TKadKey hash, TNode toQuery, (TNode, TNode[]? neighbours)? valueTuple, int round) { - using McsLock.Disposable _ = queueLock.Acquire(); - - finalResult.Enqueue((hash, toQuery), hash); - while (finalResult.Count > k) + lock (queueLock) { - finalResult.Dequeue(); - } - - TNode[]? neighbours = valueTuple?.neighbours; - if (neighbours == null) return; + finalResult.Enqueue((hash, toQuery), hash); + while (finalResult.Count > k) + { + finalResult.Dequeue(); + } - foreach (TNode neighbour in neighbours) - { - KademliaHash neighbourHash = nodeHashProvider.GetHash(neighbour); + TNode[]? neighbours = valueTuple?.neighbours; + if (neighbours == null) return; - // Already queried, we ignore - if (queried.ContainsKey(neighbourHash)) continue; + foreach (TNode neighbour in neighbours) + { + TKadKey neighbourHash = nodeHashProvider.GetHash(neighbour); - // When seen already dont record - if (!seen.TryAdd(neighbourHash, neighbour)) continue; + // Already queried, we ignore + if (queried.ContainsKey(neighbourHash)) continue; - bestSeen.Enqueue((neighbourHash, neighbour), neighbourHash); + // When seen already dont record + if (!seen.TryAdd(neighbourHash, neighbour)) continue; - if (closestNodeRound < round) - { - if (finalResult.Count < k) - { - closestNodeRound = round; - } + bestSeen.Enqueue((neighbourHash, neighbour), neighbourHash); - // If the worst item in final result is worst that this neighbour, update closes node round - if (finalResult.TryPeek(out (KademliaHash hash, TNode node) worstResult, out KademliaHash _) && comparer.Compare(neighbourHash, worstResult.hash) < 0) + if (closestNodeRound < round) { - closestNodeRound = round; + if (finalResult.Count < k) + { + closestNodeRound = round; + } + + // If the worst item in final result is worst that this neighbour, update closes node round + if (finalResult.TryPeek(out (TKadKey hash, TNode node) worstResult, out _) && comparer.Compare(neighbourHash, worstResult.hash) < 0) + { + closestNodeRound = round; + } } } } @@ -221,28 +229,31 @@ void ProcessResult(KademliaHash hash, TNode toQuery, (TNode, TNode[]? neighbours TNode[] CompileResult() { - using McsLock.Disposable _ = queueLock.Acquire(); - if (finalResult.Count > k) finalResult.Dequeue(); - return [.. finalResult.UnorderedItems.Select((kv) => kv.Element.Item2)]; + lock (queueLock) + { + if (finalResult.Count > k) finalResult.Dequeue(); + return [.. finalResult.UnorderedItems.Select((kv) => kv.Element.Item2)]; + } } bool ShouldStopDueToNoBetterResult(out int round) { - using McsLock.Disposable _ = queueLock.Acquire(); - - round = Interlocked.Increment(ref currentRound); - if (finalResult.Count >= k && round - closestNodeRound >= (config.Alpha * 2)) + lock (queueLock) { - // No closer node for more than or equal to _alpha*2 round. - // Assume exit condition - // Why not just _alpha? - // Because there could be currently running work that may increase closestNodeRound. - // So including this worker, assume no more - if (_logger.IsTrace) _logger.Trace($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); - return true; - } + round = Interlocked.Increment(ref currentRound); + if (finalResult.Count >= k && round - closestNodeRound >= (config.Alpha * 2)) + { + // No closer node for more than or equal to _alpha*2 round. + // Assume exit condition + // Why not just _alpha? + // Because there could be currently running work that may increase closestNodeRound. + // So including this worker, assume no more + if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("No more closer node. Round: {Round}, closestNodeRound {ClosestNodeRound}", round, closestNodeRound); + return true; + } - return false; + return false; + } } } } diff --git a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj index 7a5c0ed94a8d..f4087527233a 100644 --- a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj +++ b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj @@ -6,11 +6,7 @@ - - - - - + diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index 327dd90b7b80..a54c8b40c5e8 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -1,36 +1,41 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Caching; -using Nethermind.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using NonBlocking; namespace Nethermind.Kademlia; -public class NodeHealthTracker( +/// +/// Tracks node liveness signals and evicts peers that repeatedly fail Kademlia requests. +/// +public class NodeHealthTracker( KademliaConfig config, - IRoutingTable routingTable, - INodeHashProvider nodeHashProvider, + IRoutingTable routingTable, + INodeHashProvider nodeHashProvider, IKademliaMessageSender kademliaMessageSender, - ILogManager logManager -) : INodeHealthTracker, IDisposable where TNode : notnull + ILoggerFactory? loggerFactory = null +) : INodeHealthTracker, IDisposable + where TNode : notnull + where TKadKey : notnull { - private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly ILogger _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); - private readonly ConcurrentDictionary _isRefreshing = new(); - private readonly ConcurrentDictionary _refreshTasks = new(); - private readonly LruCache _peerFailures = new(1024, "peer failure"); - private readonly KademliaHash _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); + private readonly ConcurrentDictionary _isRefreshing = new(); + private readonly ConcurrentDictionary _refreshTasks = new(); + private readonly PeerFailureCache _peerFailures = new(1024); + private readonly TKadKey _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); private readonly TimeSpan _refreshPingTimeout = config.RefreshPingTimeout; private readonly CancellationTokenSource _refreshCancellation = new(); private int _disposed; - private bool SameAsSelf(TNode node) => nodeHashProvider.GetHash(node) == _currentNodeIdAsHash; + private bool SameAsSelf(TNode node) => EqualityComparer.Default.Equals(nodeHashProvider.GetHash(node), _currentNodeIdAsHash); private void TryRefresh(TNode toRefresh) { - KademliaHash nodeHash = nodeHashProvider.GetHash(toRefresh); + TKadKey nodeHash = nodeHashProvider.GetHash(toRefresh); if (_isRefreshing.TryAdd(nodeHash, true)) { if (Volatile.Read(ref _disposed) != 0) @@ -43,7 +48,7 @@ private void TryRefresh(TNode toRefresh) } } - private async Task RefreshAsync(TNode toRefresh, KademliaHash nodeHash, CancellationToken token) + private async Task RefreshAsync(TNode toRefresh, TKadKey nodeHash, CancellationToken token) { try { @@ -73,7 +78,7 @@ private async Task RefreshAsync(TNode toRefresh, KademliaHash nodeHash, Cancella catch (Exception e) { OnRequestFailed(toRefresh); - if (_logger.IsDebug) _logger.Debug($"Error while refreshing node {toRefresh}, {e}"); + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug(e, "Error while refreshing node {Node}.", toRefresh); } if (_isRefreshing.TryRemove(nodeHash, out _)) @@ -121,7 +126,7 @@ public void OnIncomingMessageFrom(TNode node) /// public void OnRequestFailed(TNode node) { - KademliaHash hash = nodeHashProvider.GetHash(node); + TKadKey hash = nodeHashProvider.GetHash(node); if (!_peerFailures.TryGet(hash, out int currentFailure)) { _peerFailures.Set(hash, 1); @@ -177,7 +182,10 @@ public void Dispose() catch (AggregateException e) { completed = true; - if (!HasOnlyCancellationExceptions(e) && _logger.IsDebug) _logger.Debug($"Error while disposing node health tracker. {e}"); + if (!HasOnlyCancellationExceptions(e)) + { + _logger.LogDebug(e, "Error while disposing node health tracker."); + } } if (completed) @@ -198,4 +206,72 @@ private static bool HasOnlyCancellationExceptions(AggregateException e) return true; } + + private sealed class PeerFailureCache(int capacity) + { + private readonly object _lock = new(); + private readonly Dictionary OrderNode)> _values = []; + private readonly LinkedList _order = []; + + public bool TryGet(TKadKey hash, out int failureCount) + { + lock (_lock) + { + if (!_values.TryGetValue(hash, out (int FailureCount, LinkedListNode OrderNode) entry)) + { + failureCount = 0; + return false; + } + + _order.Remove(entry.OrderNode); + _order.AddLast(entry.OrderNode); + failureCount = entry.FailureCount; + return true; + } + } + + public void Set(TKadKey hash, int failureCount) + { + lock (_lock) + { + if (_values.TryGetValue(hash, out (int FailureCount, LinkedListNode OrderNode) entry)) + { + _order.Remove(entry.OrderNode); + _order.AddLast(entry.OrderNode); + _values[hash] = (failureCount, entry.OrderNode); + return; + } + + LinkedListNode orderNode = _order.AddLast(hash); + _values[hash] = (failureCount, orderNode); + Trim(); + } + } + + public void Delete(TKadKey hash) + { + lock (_lock) + { + if (_values.Remove(hash, out (int FailureCount, LinkedListNode OrderNode) entry)) + { + _order.Remove(entry.OrderNode); + } + } + } + + private void Trim() + { + while (_values.Count > capacity) + { + LinkedListNode? oldest = _order.First; + if (oldest is null) + { + return; + } + + _order.RemoveFirst(); + _values.Remove(oldest.Value); + } + } + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs index fccc21a89a1b..6fb29efc603b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs @@ -11,6 +11,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Kademlia; using Nethermind.Stats.Model; using NSubstitute; @@ -25,8 +26,8 @@ public class IteratorNodeLookupTests private static readonly Node InitialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); private static readonly Node NeighbourNode = new(TestItem.PublicKeyD, "192.168.1.4", 30303); - private IRoutingTable _routingTable = null!; - private IteratorNodeLookup _lookup = null!; + private IRoutingTable _routingTable = null!; + private IteratorNodeLookup _lookup = null!; private IKademliaMessageSender _msgSender = null!; private Node _currentNode = null!; private PublicKey _targetKey = null!; @@ -37,22 +38,23 @@ public void Setup() _currentNode = new(TestItem.PublicKeyA, "192.168.1.1", 30303); _targetKey = TestItem.PublicKeyB; - _routingTable = Substitute.For>(); + _routingTable = Substitute.For>(); KademliaConfig kademliaConfig = new() { CurrentNodeId = _currentNode }; _msgSender = Substitute.For>(); ILogManager logManager = Substitute.For(); - _lookup = new IteratorNodeLookup( + _lookup = new IteratorNodeLookup( _routingTable, kademliaConfig, _msgSender, new PublicKeyKeyOperator(), + Hash256KademliaDistance.Instance, new ManualTimestamper(new DateTime(2025, 5, 13, 21, 0, 0, DateTimeKind.Utc)), logManager); } private void RoutingTableReturns(params Node[] nodes) => - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) + _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) .Returns(nodes); private void FindNeighboursReturns(Node from, params Node[] result) => @@ -80,8 +82,8 @@ public async Task Lookup_should_return_nodes_from_routing_table(CancellationToke Assert.That(result, Is.EquivalentTo(expectedNodes)); _routingTable.Received(1).GetKNearestNeighbour( - Arg.Is(h => h == TargetHash), - Arg.Any()); + Arg.Is(h => h == TargetHash), + Arg.Any()); } [Test] @@ -206,6 +208,6 @@ public async Task Lookup_should_not_return_duplicate_nodes(CancellationToken tok Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); } - private KademliaHash TargetHash => KademliaHash.FromBytes(_targetKey.Hash.Bytes); + private Hash256 TargetHash => _targetKey.Hash; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index b76b97142028..0daef3414a0b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -9,6 +9,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; using NSubstitute; @@ -186,6 +187,7 @@ public void BoundedMap_ShouldRemoveInsertionOrderEntriesOnRemove() null!, new DiscoveryConfig(), new CryptoRandom(), + Hash256KademliaDistance.Instance, LimboLogs.Instance); private static Node CreateNode(PublicKey publicKey, int hostSuffix) => @@ -206,9 +208,7 @@ private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) private static Discv5KademliaAdapter.NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) { PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); - int distance = Hash256XorUtils.CalculateLogDistance( - KademliaHash.FromBytes(receiver.Id.Hash.Bytes), - KademliaHash.FromBytes(nodeId.Hash.Bytes)); - return new Discv5KademliaAdapter.NodesResponseHandler(receiver, [distance]); + int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); + return new Discv5KademliaAdapter.NodesResponseHandler(receiver, [distance], Hash256KademliaDistance.Instance); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs index 773726fb672f..f4f0f24b6144 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs @@ -19,6 +19,7 @@ using Nethermind.Logging; using Nethermind.Network.Enr; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; using NSubstitute; @@ -211,6 +212,7 @@ private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, b nodeRecordProvider, new DiscoveryConfig(), new CryptoRandom(), + Hash256KademliaDistance.Instance, LimboLogs.Instance); return new TestPeer(adapter, handler, channel, kademlia, nodeRecordProvider, endpoint); @@ -254,9 +256,7 @@ private static void Pump(TestPeer from, TestPeer to) private static int[] GetLookupDistances(Node receiver, PublicKey target) { - KademliaHash receiverHash = KademliaHash.FromBytes(receiver.Id.Hash.Bytes); - KademliaHash targetHash = KademliaHash.FromBytes(target.Hash.Bytes); - int distance = Hash256XorUtils.CalculateLogDistance(receiverHash, targetHash); + int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, target.Hash); List distances = [distance]; if (distance > 0) @@ -264,7 +264,7 @@ private static int[] GetLookupDistances(Node receiver, PublicKey target) distances.Add(distance - 1); } - if (distance < Hash256XorUtils.MaxDistance) + if (distance < Hash256KademliaDistance.Instance.MaxDistance) { distances.Add(distance + 1); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256KademliaDistanceTests.cs similarity index 70% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256KademliaDistanceTests.cs index 42d282a8081d..233ce116d68c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256XorUtilsTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Hash256KademliaDistanceTests.cs @@ -2,13 +2,15 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using Nethermind.Kademlia; +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Kademlia; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; -public class Hash256XorUtilsTests +public class Hash256KademliaDistanceTests { + private static readonly Hash256KademliaDistance Distance = Hash256KademliaDistance.Instance; [TestCase("0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", @@ -51,24 +53,24 @@ public class Hash256XorUtilsTests "0x000000000000000000000000000000000000000000000000000000000001000f", 17)] public void TestDistance(string hash1, string hash2, string xosString, int expectedDistance) { - KademliaHash xor = Hash256XorUtils.XorDistance(new(hash1), new(hash2)); + Hash256 xor = XorDistance(new(hash1), new(hash2)); Assert.That(xor.ToString(), Is.EqualTo(xosString.ToLower())); - Assert.That(Hash256XorUtils.CalculateLogDistance(new(hash1), new(hash2)), Is.EqualTo(expectedDistance)); - Assert.That(Hash256XorUtils.CalculateLogDistance(new(hash2), new(hash1)), Is.EqualTo(expectedDistance)); + Assert.That(Distance.CalculateLogDistance(new Hash256(hash1), new Hash256(hash2)), Is.EqualTo(expectedDistance)); + Assert.That(Distance.CalculateLogDistance(new Hash256(hash2), new Hash256(hash1)), Is.EqualTo(expectedDistance)); } [Test] public void TestGetRandomHash() { Random rand = new(0); - Span randomizedBytes = stackalloc byte[KademliaHash.Length]; + Span randomizedBytes = stackalloc byte[Hash256.Size]; rand.NextBytes(randomizedBytes); - KademliaHash randomized = KademliaHash.FromBytes(randomizedBytes); + Hash256 randomized = new(randomizedBytes); void TestForDistance(int distance) { - KademliaHash randHash = Hash256XorUtils.GetRandomHashAtDistance(randomized, distance, rand); - Assert.That(Hash256XorUtils.CalculateLogDistance(randomized, randHash), Is.EqualTo(distance)); + Hash256 randHash = Distance.GetRandomHashAtDistance(randomized, distance, rand); + Assert.That(Distance.CalculateLogDistance(randomized, randHash), Is.EqualTo(distance)); } for (int i = 0; i <= 256; i++) @@ -86,18 +88,31 @@ void TestForDistance(int distance) [TestCase(257)] public void GetRandomHashAtDistance_ShouldRejectInvalidDistance(int distance) { - KademliaHash hash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + Hash256 hash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); - Assert.That(() => Hash256XorUtils.GetRandomHashAtDistance(hash, distance, new Random(0)), Throws.InstanceOf()); + Assert.That(() => Distance.GetRandomHashAtDistance(hash, distance, new Random(0)), Throws.InstanceOf()); } [TestCase] public void TestDistanceCompare() { - KademliaHash h1 = new("0x0010000000000000000000000000000000000000000000000000000000000000"); - KademliaHash h2 = new("0x0110000000000000000000000000000000000000000000000000000000000000"); - KademliaHash h3 = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + Hash256 h1 = new("0x0010000000000000000000000000000000000000000000000000000000000000"); + Hash256 h2 = new("0x0110000000000000000000000000000000000000000000000000000000000000"); + Hash256 h3 = new("0x0000000000000000000000000000000000000000000000000000000000000000"); - Assert.That(Hash256XorUtils.Compare(h1, h2, h3), Is.LessThan(0)); + Assert.That(Distance.Compare(h1, h2, h3), Is.LessThan(0)); + } + + private static Hash256 XorDistance(Hash256 left, Hash256 right) + { + Span result = stackalloc byte[Hash256.Size]; + ReadOnlySpan leftBytes = left.Bytes; + ReadOnlySpan rightBytes = right.Bytes; + for (int i = 0; i < result.Length; i++) + { + result[i] = (byte)(leftBytes[i] ^ rightBytes[i]); + } + + return new Hash256(result); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs index 8229005a101a..d2a14bbf0267 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs @@ -6,11 +6,11 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; -internal sealed class IdentityNodeHashProvider : INodeHashProvider +internal sealed class IdentityNodeHashProvider : INodeHashProvider { public static readonly IdentityNodeHashProvider Instance = new(); - public static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + public static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - public KademliaHash GetHash(ValueHash256 node) => ToKademliaHash(node); + public Hash256 GetHash(ValueHash256 node) => ToHash(node); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index 4f0a83135963..919b045c37a6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -13,48 +13,48 @@ public class KBucketTests [Test] public void TryAddOrRefresh_ShouldLimitToK() { - KBucket bucket = new(5); + KBucket bucket = new(5); ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); + bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); } // Again foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); + bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); } Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(toAdd[..5].ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (ToKademliaHash(it), it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (ToHash(it), it)).ToHashSet())); foreach (ValueHash256 valueHash256 in toAdd[..5]) { - Assert.That(bucket.ContainsNode(ToKademliaHash(valueHash256)), Is.True); - Assert.That(bucket.GetByHash(ToKademliaHash(valueHash256)), Is.EqualTo(valueHash256)); + Assert.That(bucket.ContainsNode(ToHash(valueHash256)), Is.True); + Assert.That(bucket.GetByHash(ToHash(valueHash256)), Is.EqualTo(valueHash256)); } } [Test] public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() { - KBucket bucket = new(5); + KBucket bucket = new(5); ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); + bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); } ValueHash256[] nodes = bucket.GetAll(); foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); + bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); } Assert.That(bucket.GetAll(), Is.SameAs(nodes)); @@ -63,8 +63,8 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() [Test] public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNewInstance() { - KBucket bucket = new(5); - KademliaHash hash = KademliaHash.FromBytes(ValueKeccak.Compute("node").BytesAsSpan); + KBucket bucket = new(5); + Hash256 hash = ToHash(ValueKeccak.Compute("node")); bucket.TryAddOrRefresh(hash, "old", out _); bucket.TryAddOrRefresh(hash, "new", out _); @@ -77,21 +77,21 @@ public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNe [Test] public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() { - KBucket bucket = new(5); + KBucket bucket = new(5); ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); foreach (ValueHash256 valueHash256 in toAdd) { - bucket.TryAddOrRefresh(ToKademliaHash(valueHash256), valueHash256, out _); + bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); } - bucket.RemoveAndReplace(ToKademliaHash(toAdd[0])); + bucket.RemoveAndReplace(ToHash(toAdd[0])); ValueHash256[] expected = [.. toAdd[1..5], toAdd[9]]; Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(expected.ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (ToKademliaHash(it), it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (ToHash(it), it)).ToHashSet())); } - private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs index f86518d7e08e..1d90d1d1805d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs @@ -5,7 +5,7 @@ using System.Linq; using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -14,23 +14,23 @@ public class KBucketTreeTests { private static readonly ValueHash256 SelfHash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); - private static KBucketTree CreateTree(int k = 4, int beta = 0) => new( + private static KBucketTree CreateTree(int k = 4, int beta = 0) => new( new KademliaConfig { CurrentNodeId = SelfHash, KSize = k, Beta = beta }, IdentityNodeHashProvider.Instance, - LimboLogs.Instance); + Hash256KademliaDistance.Instance); - private static void Add(KBucketTree tree, ValueHash256 hash) => - tree.TryAddOrRefresh(IdentityNodeHashProvider.ToKademliaHash(hash), hash, out _); + private static void Add(KBucketTree tree, ValueHash256 hash) => + tree.TryAddOrRefresh(IdentityNodeHashProvider.ToHash(hash), hash, out _); private static ValueHash256 HashAtDistance(int distance, byte tag) => - ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(IdentityNodeHashProvider.ToKademliaHash(SelfHash), distance, new Random(tag))); + ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(IdentityNodeHashProvider.ToHash(SelfHash), distance, new Random(tag))); - private static ValueHash256 ToValueHash(KademliaHash hash) => new(hash.Bytes); + private static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; [Test] public void Split_should_preserve_lru_order_in_child_buckets() { - KBucketTree tree = CreateTree(k: 2, beta: 0); + KBucketTree tree = CreateTree(k: 2, beta: 0); ValueHash256 left0 = HashAtDistance(255, 0x10); ValueHash256 left1 = HashAtDistance(255, 0x11); @@ -49,7 +49,7 @@ public void Split_should_preserve_lru_order_in_child_buckets() [Test] public void GetAllAtDistance_should_include_nodes_in_deeper_split_buckets() { - KBucketTree tree = CreateTree(k: 2, beta: 4); + KBucketTree tree = CreateTree(k: 2, beta: 4); ValueHash256 deep1 = HashAtDistance(252, 0x40); ValueHash256 deep2 = HashAtDistance(252, 0x41); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index 17168acb4d2a..b9dd2dd3969e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -49,9 +49,9 @@ public async Task TestBootstrap() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Kademlia node1 = fabric.CreateNode(node1Hash); - Kademlia node2 = fabric.CreateNode(node2Hash); - Kademlia node3 = fabric.CreateNode(node3Hash); + Nethermind.Kademlia.Kademlia node1 = fabric.CreateNode(node1Hash); + Nethermind.Kademlia.Kademlia node2 = fabric.CreateNode(node2Hash); + Nethermind.Kademlia.Kademlia node3 = fabric.CreateNode(node3Hash); Assert.That(node1.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray(), Is.EquivalentTo(new[] { node1Hash })); @@ -85,7 +85,7 @@ public async Task TestKNearestNeighbour() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Kademlia node1 = fabric.CreateNode(node1Hash); + Nethermind.Kademlia.Kademlia node1 = fabric.CreateNode(node1Hash); Assert.That( (await node1.LookupNodesClosest(node1Hash, cts.Token)) @@ -93,7 +93,7 @@ public async Task TestKNearestNeighbour() .ToArray(), Is.EquivalentTo(new[] { node1Hash })); - Kademlia node2 = fabric.CreateNode(node2Hash); + Nethermind.Kademlia.Kademlia node2 = fabric.CreateNode(node2Hash); fabric.CreateNode(node3Hash); node1.AddOrRefresh(new TestNode(node2Hash)); @@ -118,13 +118,13 @@ public async Task SimulateLargeKNearestNeighbour() TestFabric fabric = CreateFabric(); Random rand = new(0); ValueHash256 mainNodeHash = RandomKeccak(rand); - Kademlia mainNode = fabric.CreateNode(mainNodeHash); + Nethermind.Kademlia.Kademlia mainNode = fabric.CreateNode(mainNodeHash); List nodeIds = []; for (int i = 0; i < nodeCount; i++) { ValueHash256 nodeHash = RandomKeccak(rand); - Kademlia kad = fabric.CreateNode(nodeHash); + Nethermind.Kademlia.Kademlia kad = fabric.CreateNode(nodeHash); kad.AddOrRefresh(new TestNode(mainNodeHash)); nodeIds.Add(nodeHash); } @@ -147,7 +147,7 @@ public async Task SimulateLargeKNearestNeighbour() { TestNode[] nodesClosest = await mainNode.LookupNodesClosest(targetNode, cts.Token); HashSet expectedNodeClosestK = nodeIds - .Order(Comparer.Create((n1, n2) => Hash256XorUtils.Compare(ToKademliaHash(n1), ToKademliaHash(n2), ToKademliaHash(targetNode)))) + .Order(Comparer.Create((n1, n2) => Hash256KademliaDistance.Instance.Compare(ToHash(n1), ToHash(n2), ToHash(targetNode)))) .Take(_config.KSize) .ToHashSet(); @@ -185,20 +185,20 @@ private static ValueHash256 RandomKeccak(Random rand) return val; } - private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - private static ValueHash256 ToValueHash(KademliaHash hash) => new(hash.Bytes); + private static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; - private class ValueHashNodeHashProvider : IKeyOperator + private class ValueHashNodeHashProvider : IKeyOperator { public ValueHash256 GetKey(TestNode node) => node.Hash; - public KademliaHash GetKeyHash(ValueHash256 key) => ToKademliaHash(key); + public Hash256 GetKeyHash(ValueHash256 key) => ToHash(key); - public ValueHash256 CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth) => - ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth)); + public ValueHash256 CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) => + ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(nodePrefix, depth)); - public KademliaHash GetHash(ValueHash256 key) => ToKademliaHash(key); + public Hash256 GetHash(ValueHash256 key) => ToHash(key); } private class TestFabric(KademliaConfig config) @@ -226,16 +226,17 @@ private bool TryGetReceiver(TestNode receiverHash, out ReceiverForNode contentKa return false; } - public Kademlia CreateNode(ValueHash256 nodeID) + public Nethermind.Kademlia.Kademlia CreateNode(ValueHash256 nodeID) { TestNode nodeIDTestNode = new(nodeID); ContainerBuilder builder = new(); builder - .AddModule(new KademliaModule()) + .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Error)) .AddSingleton(new ManualTimestamper(new DateTime(2025, 5, 13, 21, 0, 0, DateTimeKind.Utc))) - .AddSingleton>(_nodeHashProvider) + .AddSingleton>(Hash256KademliaDistance.Instance) + .AddSingleton>(_nodeHashProvider) .AddSingleton(new KademliaConfig { CurrentNodeId = nodeIDTestNode, @@ -246,13 +247,13 @@ public Kademlia CreateNode(ValueHash256 nodeID) }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton() - .AddSingleton>(); + .AddSingleton>(); IContainer container = builder.Build(); _nodes[nodeID] = container; - return container.Resolve>(); + return container.Resolve>(); } private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 6c7d46fc1122..5729e9ca76ed 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -20,22 +20,23 @@ public class KademliaTests { private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); - private Kademlia CreateKad(KademliaConfig config) => + private Nethermind.Kademlia.Kademlia CreateKad(KademliaConfig config) => new ContainerBuilder() - .AddModule(new KademliaModule()) + .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Trace)) .AddSingleton(new ManualTimestamper(new System.DateTime(2025, 5, 13, 21, 0, 0, System.DateTimeKind.Utc))) - .AddSingleton>(new ValueHashNodeHashProvider()) + .AddSingleton>(Hash256KademliaDistance.Instance) + .AddSingleton>(new ValueHashNodeHashProvider()) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) - .AddSingleton>() + .AddSingleton>() .Build() - .Resolve>(); + .Resolve>(); [Test] public void TestNewNodeAdded() { - Kademlia kad = CreateKad(new KademliaConfig + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig { KSize = 5, Beta = 0, @@ -55,7 +56,7 @@ public void TestNewNodeAdded() [Test] public void TestNodeRemoved() { - Kademlia kad = CreateKad(new KademliaConfig + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig { KSize = 5, Beta = 0, @@ -79,7 +80,7 @@ public void TestNodeRemoved() public void ShouldSeedBootnodes() { ValueHash256 bootNode = ValueKeccak.Compute("bootnode"); - Kademlia kad = CreateKad(new KademliaConfig + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig { KSize = 5, Beta = 0, @@ -97,7 +98,7 @@ public async Task TestTooManyNode() .Ping(Arg.Any(), Arg.Any()) .Returns(pingSource.Task); - Kademlia kad = CreateKad(new KademliaConfig + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig { KSize = 5, Beta = 0, @@ -127,7 +128,7 @@ public void TestGetKNeighbours() .Ping(Arg.Any(), Arg.Any()) .Returns(pingSource.Task); - Kademlia kad = CreateKad(new KademliaConfig + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig { CurrentNodeId = ValueKeccak.Compute("something"), KSize = 5, @@ -163,7 +164,7 @@ public async Task TestTooManyNodeWithAcceleratedLookup() .Ping(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); - Kademlia kad = CreateKad(new KademliaConfig + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig { KSize = 5, Beta = 1, @@ -196,20 +197,20 @@ public async Task TestTooManyNodeWithAcceleratedLookup() Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(testHashes[10..].ToHashSet())); } - private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - private static ValueHash256 ToValueHash(KademliaHash hash) => new(hash.Bytes); + private static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; private static ValueHash256 RandomValueHashAtDistance(ValueHash256 currentHash, int distance) => - ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(ToKademliaHash(currentHash), distance)); + ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(ToHash(currentHash), distance)); - private class ValueHashNodeHashProvider : IKeyOperator + private class ValueHashNodeHashProvider : IKeyOperator { public ValueHash256 GetKey(ValueHash256 node) => node; - public KademliaHash GetKeyHash(ValueHash256 key) => ToKademliaHash(key); + public Hash256 GetKeyHash(ValueHash256 key) => ToHash(key); - public ValueHash256 CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth) => - ToValueHash(Hash256XorUtils.GetRandomHashAtDistance(nodePrefix, depth)); + public ValueHash256 CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) => + ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(nodePrefix, depth)); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 3e2974ea4a75..a86144e55197 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Logging; +using Nethermind.Network.Discovery.Kademlia; using NSubstitute; using NUnit.Framework; @@ -22,16 +22,17 @@ public class LookupKNearestNeighbourTests private static readonly ValueHash256 N1 = new("0x4400000000000000000000000000000000000000000000000000000000000000"); private static readonly ValueHash256 N2 = new("0x5500000000000000000000000000000000000000000000000000000000000000"); - private static (LookupKNearestNeighbour Lookup, IRoutingTable Routing, INodeHealthTracker Health) CreateLookup(int alpha, TimeSpan hardTimeout, ValueHash256[] seeds) + private static (LookupKNearestNeighbour Lookup, IRoutingTable Routing, INodeHealthTracker Health) CreateLookup(int alpha, TimeSpan hardTimeout, ValueHash256[] seeds) { - IRoutingTable routing = Substitute.For>(); - routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); + IRoutingTable routing = Substitute.For>(); + routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); INodeHealthTracker health = Substitute.For>(); - LookupKNearestNeighbour lookup = new( + LookupKNearestNeighbour lookup = new( routing, IdentityNodeHashProvider.Instance, + Hash256KademliaDistance.Instance, health, new KademliaConfig { @@ -39,8 +40,7 @@ private static (LookupKNearestNeighbour Lookup, IRou Alpha = alpha, KSize = 8, LookupFindNeighbourHardTimeout = hardTimeout, - }, - LimboLogs.Instance); + }); return (lookup, routing, health); } @@ -50,13 +50,13 @@ private static (LookupKNearestNeighbour Lookup, IRou [CancelAfter(10000)] public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, CancellationToken token) { - (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = CreateLookup(alpha, TimeSpan.FromSeconds(30), [Seed1]); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); Task task = lookup.Lookup( - IdentityNodeHashProvider.ToKademliaHash(Seed1), + IdentityNodeHashProvider.ToHash(Seed1), 8, async (_, t) => { @@ -75,11 +75,11 @@ public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, Ca [CancelAfter(10000)] public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(CancellationToken token) { - (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = CreateLookup(1, TimeSpan.FromMilliseconds(50), [Seed1]); _ = await lookup.Lookup( - IdentityNodeHashProvider.ToKademliaHash(Seed1), + IdentityNodeHashProvider.ToHash(Seed1), 8, async (_, t) => { @@ -96,7 +96,7 @@ public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(Ca [CancelAfter(10000)] public async Task Lookup_should_return_results_with_different_alpha(int alpha, CancellationToken token) { - (LookupKNearestNeighbour lookup, _, _) = + (LookupKNearestNeighbour lookup, _, _) = CreateLookup(alpha, TimeSpan.FromSeconds(10), [Seed1, Seed2, Seed3]); Dictionary neighbours = new() @@ -107,7 +107,7 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C }; ValueHash256[] result = await lookup.Lookup( - IdentityNodeHashProvider.ToKademliaHash(Self), + IdentityNodeHashProvider.ToHash(Self), 8, (node, _) => Task.FromResult(neighbours.GetValueOrDefault(node, [])), token); @@ -119,12 +119,12 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C [CancelAfter(10000)] public async Task Lookup_should_drain_cancelled_workers_before_returning(CancellationToken token) { - (LookupKNearestNeighbour lookup, _, _) = + (LookupKNearestNeighbour lookup, _, _) = CreateLookup(2, TimeSpan.FromSeconds(10), [Seed1, Seed2, Seed3, N1]); TaskCompletionSource cancelledWorkerDrained = new(TaskCreationOptions.RunContinuationsAsynchronously); _ = await lookup.Lookup( - IdentityNodeHashProvider.ToKademliaHash(Self), + IdentityNodeHashProvider.ToHash(Self), 1, async (node, findToken) => { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index 38bc3a8d4a40..14167488ee4c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Logging; using NSubstitute; using NUnit.Framework; @@ -19,14 +18,14 @@ public class NodeHealthTrackerTests private const string Remote = "remote"; private const string Stale = "stale"; - private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( + private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( string? toRefresh = null, int failureThreshold = 5, TimeSpan? refreshPingTimeout = null, - IKademliaMessageSender? sender = null) + IKademliaMessageSender? sender = null) { RoutingTableStub routing = new() { ToRefresh = toRefresh ?? string.Empty }; - sender ??= Substitute.For>(); + sender ??= Substitute.For>(); KademliaConfig config = new() { CurrentNodeId = Self, @@ -34,24 +33,23 @@ private static (NodeHealthTracker Tracker, RoutingTableStu }; if (refreshPingTimeout is { } timeout) config.RefreshPingTimeout = timeout; - NodeHealthTracker tracker = new( + NodeHealthTracker tracker = new( config, routing, StringNodeHashProvider.Instance, - sender, - LimboLogs.Instance); + sender); return (tracker, routing, sender); } [Test] public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSelectsSelf() { - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); tracker.OnIncomingMessageFrom(Remote); Assert.That(routing.AddCalls, Has.Count.EqualTo(2)); - Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(ToKademliaHash(ValueKeccak.Compute(Self)))); + Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(ToHash(ValueKeccak.Compute(Self)))); Assert.That(routing.AddCalls[1].Node, Is.EqualTo(Self)); } @@ -59,18 +57,18 @@ public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSe [CancelAfter(10000)] public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(CancellationToken token) { - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()) .Returns(Task.FromException(new OperationCanceledException())); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, refreshPingTimeout: TimeSpan.FromMilliseconds(50), sender: sender); tracker.OnIncomingMessageFrom(Remote); - KademliaHash staleHash = ToKademliaHash(ValueKeccak.Compute(Stale)); + Hash256 staleHash = ToHash(ValueKeccak.Compute(Stale)); await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(staleHash), token); } @@ -78,16 +76,16 @@ public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(Cancellation [CancelAfter(10000)] public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken token) { - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()).Returns(Task.CompletedTask); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, sender: sender); tracker.OnIncomingMessageFrom(Remote); - KademliaHash staleHash = ToKademliaHash(ValueKeccak.Compute(Stale)); + Hash256 staleHash = ToHash(ValueKeccak.Compute(Stale)); await AssertEventuallyAsync(() => routing.HasAddedNode(staleHash), token); Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); } @@ -98,7 +96,7 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(Cancellat { TaskCompletionSource pingStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); TaskCompletionSource pingCancelled = new(TaskCreationOptions.RunContinuationsAsynchronously); - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()).Returns(async call => { CancellationToken pingToken = call.Arg(); @@ -114,7 +112,7 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(Cancellat } }); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, refreshPingTimeout: TimeSpan.FromSeconds(10), sender: sender); @@ -125,20 +123,20 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(Cancellat tracker.Dispose(); await pingCancelled.Task.WaitAsync(token); - Assert.That(routing.RemoveCalls, Does.Not.Contain(ToKademliaHash(ValueKeccak.Compute(Stale)))); + Assert.That(routing.RemoveCalls, Does.Not.Contain(ToHash(ValueKeccak.Compute(Stale)))); } [Test] public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() { - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); tracker.OnRequestFailed(Remote); tracker.OnRequestFailed(Remote); tracker.OnRequestFailed(Remote); Assert.That(routing.RemoveCalls, Has.Count.EqualTo(1)); - Assert.That(routing.RemoveCalls[0], Is.EqualTo(ToKademliaHash(ValueKeccak.Compute(Remote)))); + Assert.That(routing.RemoveCalls[0], Is.EqualTo(ToHash(ValueKeccak.Compute(Remote)))); } private static async Task AssertEventuallyAsync(Func condition, CancellationToken token) @@ -151,24 +149,24 @@ private static async Task AssertEventuallyAsync(Func condition, Cancellati Assert.Fail("Condition not met within timeout."); } - private static KademliaHash ToKademliaHash(ValueHash256 hash) => KademliaHash.FromBytes(hash.BytesAsSpan); + private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - private sealed class StringNodeHashProvider : INodeHashProvider + private sealed class StringNodeHashProvider : INodeHashProvider { public static readonly StringNodeHashProvider Instance = new(); - public KademliaHash GetHash(string node) => ToKademliaHash(ValueKeccak.Compute(node)); + public Hash256 GetHash(string node) => ToHash(ValueKeccak.Compute(node)); } - private sealed class RoutingTableStub : IRoutingTable + private sealed class RoutingTableStub : IRoutingTable { public string ToRefresh { get; init; } = string.Empty; - public List<(KademliaHash Hash, string Node)> AddCalls { get; } = []; + public List<(Hash256 Hash, string Node)> AddCalls { get; } = []; - public List RemoveCalls { get; } = []; + public List RemoveCalls { get; } = []; - public BucketAddResult TryAddOrRefresh(in KademliaHash hash, string item, out string? toRefresh) + public BucketAddResult TryAddOrRefresh(in Hash256 hash, string item, out string? toRefresh) { bool isFirstAdd; lock (AddCalls) @@ -187,11 +185,11 @@ public BucketAddResult TryAddOrRefresh(in KademliaHash hash, string item, out st return BucketAddResult.Refreshed; } - public bool HasAddedNode(KademliaHash hash) + public bool HasAddedNode(Hash256 hash) { lock (AddCalls) { - foreach ((KademliaHash h, string _) in AddCalls) + foreach ((Hash256 h, string _) in AddCalls) { if (h == hash) return true; } @@ -199,21 +197,24 @@ public bool HasAddedNode(KademliaHash hash) return false; } - public bool Remove(in KademliaHash hash) + public bool Remove(in Hash256 hash) { lock (RemoveCalls) RemoveCalls.Add(hash); return true; } - public string[] GetKNearestNeighbour(KademliaHash hash, KademliaHash? exclude = null, bool excludeSelf = false) => + public string[] GetKNearestNeighbour(Hash256 hash, bool excludeSelf = false) => + throw new NotSupportedException(); + + public string[] GetKNearestNeighbourExcluding(Hash256 hash, Hash256 exclude, bool excludeSelf = false) => throw new NotSupportedException(); public string[] GetAllAtDistance(int i) => throw new NotSupportedException(); - public IEnumerable<(KademliaHash Prefix, int Distance, KBucket Bucket)> IterateBuckets() => + public IEnumerable<(Hash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() => throw new NotSupportedException(); - public string? GetByHash(KademliaHash nodeId) => throw new NotSupportedException(); + public string? GetByHash(Hash256 nodeId) => throw new NotSupportedException(); public void LogDebugInfo() => throw new NotSupportedException(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 4ff8c9397b9d..764ec95348be 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery.Discv4; /// -/// Specify the discv4 kademlia components. Mainly provide transport for . +/// Specify the discv4 kademlia components. Mainly provide transport for . /// Because kademlia can and probably will be reused outside of discv4, this module is meant to be added within a child /// lifecycle in to prevent unexpected conflict. /// @@ -31,9 +31,10 @@ protected override void Load(ContainerBuilder builder) => builder .AddSingleton() // Register the main kademlia module and integration - .AddModule(new KademliaModule()) + .AddModule(new KademliaModule()) .Bind, IKademliaDiscv4Adapter>() - .AddSingleton, PublicKeyKeyOperator>() + .AddSingleton>(Hash256KademliaDistance.Instance) + .AddSingleton, PublicKeyKeyOperator>() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), // It actually only need masterNode. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs index 15cba82b7331..1e156c34561b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs @@ -19,19 +19,22 @@ namespace Nethermind.Network.Discovery.Discv4; /// is to reach all node. The lookup is not parallelized as it is expected to be parallelized at a higher level with /// each worker having different target to look into. /// -public class IteratorNodeLookup( - IRoutingTable routingTable, +public class IteratorNodeLookup( + IRoutingTable routingTable, KademliaConfig kademliaConfig, IKademliaMessageSender msgSender, - IKeyOperator keyOperator, + IKeyOperator keyOperator, + IKademliaDistance distance, ITimestamper timestamper, - ILogManager logManager) : IIteratorNodeLookup where TNode : notnull + ILogManager logManager) : IIteratorNodeLookup + where TNode : notnull + where TKadKey : notnull { - private readonly ILogger _logger = logManager.GetClassLogger>(); - private readonly KademliaHash _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); + private readonly ILogger _logger = logManager.GetClassLogger>(); + private readonly TKadKey _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); // Small lru of unreachable nodes, prevent retrying. Pretty effective, although does not improve discovery overall. - private readonly LruCache _unreachableNodes = new(256, ""); + private readonly LruCache _unreachableNodes = new(256, ""); // The maximum round per lookup. Higher means that it will 'see' deeper into the network, but come at a latency // cost of trying many node for increasingly lower new node. @@ -41,51 +44,53 @@ public class IteratorNodeLookup( private const int MaxNonProgressingRound = 3; private const int MinResult = 128; - private bool SameAsSelf(TNode node) => keyOperator.GetNodeHash(node) == _currentNodeIdAsHash; + private bool SameAsSelf(TNode node) => EqualityComparer.Default.Equals(keyOperator.GetNodeHash(node), _currentNodeIdAsHash); public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation] CancellationToken token) { - KademliaHash targetHash = keyOperator.GetKeyHash(target); + TKadKey targetHash = keyOperator.GetKeyHash(target); if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); using AutoCancelTokenSource cts = token.CreateChildTokenSource(); token = cts.Token; - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); + ConcurrentDictionary queried = new(); + ConcurrentDictionary seen = new(); - IComparer comparer = Comparer.Create((h1, h2) => - Hash256XorUtils.Compare(h1, h2, targetHash)); + IComparer comparer = Comparer.Create((h1, h2) => + distance.Compare(h1, h2, targetHash)); // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(KademliaHash, TNode), KademliaHash> queryQueue = new(comparer); + PriorityQueue<(TKadKey, TNode), TKadKey> queryQueue = new(comparer); // Used to determine if the worker should stop - KademliaHash bestNodeId = KademliaHash.Zero; + TKadKey bestNodeId = distance.Zero; + bool hasBestNodeId = false; int closestNodeRound = 0; int currentRound = 0; int totalResult = 0; // Check internal table first - foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash, null)) + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) { - KademliaHash nodeHash = keyOperator.GetNodeHash(node); + TKadKey nodeHash = keyOperator.GetNodeHash(node); seen.TryAdd(nodeHash, node); queryQueue.Enqueue((nodeHash, node), nodeHash); yield return node; - if (bestNodeId == KademliaHash.Zero || comparer.Compare(nodeHash, bestNodeId) < 0) + if (!hasBestNodeId || comparer.Compare(nodeHash, bestNodeId) < 0) { bestNodeId = nodeHash; + hasBestNodeId = true; } } while (true) { token.ThrowIfCancellationRequested(); - if (!queryQueue.TryDequeue(out (KademliaHash hash, TNode node) toQuery, out KademliaHash hash256)) + if (!queryQueue.TryDequeue(out (TKadKey hash, TNode node) toQuery, out _)) { // No node to query and running query. if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); @@ -108,7 +113,7 @@ public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation int seenIgnored = 0; foreach (TNode neighbour in neighbours!) { - KademliaHash neighbourHash = keyOperator.GetNodeHash(neighbour); + TKadKey neighbourHash = keyOperator.GetNodeHash(neighbour); // Already queried, we ignore if (queried.ContainsKey(neighbourHash)) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index d2738f8e9341..fe091fb6255a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -8,6 +8,7 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv4; @@ -24,7 +25,7 @@ public class KademliaNodeSource( private const int ChannelCapacity = 64; private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256XorUtils.MaxDistance); + private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256KademliaDistance.Instance.MaxDistance); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs index 13d9fd2e6066..2316d9db04ba 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs @@ -7,11 +7,11 @@ namespace Nethermind.Network.Discovery.Discv4; -public class PublicKeyKeyOperator : IKeyOperator +public class PublicKeyKeyOperator : IKeyOperator { public PublicKey GetKey(Node node) => node.Id; - public KademliaHash GetKeyHash(PublicKey key) => KademliaHash.FromBytes(key.Hash.Bytes); + public Hash256 GetKeyHash(PublicKey key) => key.Hash; /// /// Creates a random discv4 lookup target. @@ -21,7 +21,7 @@ public class PublicKeyKeyOperator : IKeyOperator /// Constructing a public key whose Keccak hash lands in that prefix is not practical, so this uses a random /// 64-byte target and treats discv4 bucket refresh as best-effort sampling. /// - public PublicKey CreateRandomKeyAtDistance(KademliaHash nodePrefix, int depth) + public PublicKey CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) { Span randomBytes = new byte[64]; Random.Shared.NextBytes(randomBytes); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs index 6da5b1f976c8..79c2989a73a7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs @@ -22,8 +22,9 @@ protected override void Load(ContainerBuilder builder) => builder .Bind, IDiscv5KademliaAdapter>() .AddSingleton() .AddSingleton() - .AddModule(new KademliaModule()) - .AddSingleton, PublicKeyKeyOperator>() + .AddModule(new KademliaModule()) + .AddSingleton>(Hash256KademliaDistance.Instance) + .AddSingleton, PublicKeyKeyOperator>() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() { CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index 2d88379ec1ce..5e76705fc946 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -26,6 +26,7 @@ public class Discv5KademliaAdapter( INodeRecordProvider nodeRecordProvider, IDiscoveryConfig discoveryConfig, ICryptoRandom cryptoRandom, + IKademliaDistance distance, ILogManager logManager) : IDiscv5KademliaAdapter { private const int MaxFindNodeRecords = 16; @@ -46,6 +47,7 @@ public class Discv5KademliaAdapter( private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); + private readonly IKademliaDistance _distance = distance; private readonly ILogger _logger = logManager.GetClassLogger(); private readonly BoundedMap _sessions = new(MaxSessions); private readonly BoundedMap _sentChallenges = new(MaxSentChallenges); @@ -67,9 +69,9 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = foreach (int distance in distances) { - if (distance < 0 || distance > Hash256XorUtils.MaxDistance) + if (distance < 0 || distance > _distance.MaxDistance) { - throw new ArgumentOutOfRangeException(nameof(distances), distance, $"Distance must be between 0 and {Hash256XorUtils.MaxDistance}."); + throw new ArgumentOutOfRangeException(nameof(distances), distance, $"Distance must be between 0 and {_distance.MaxDistance}."); } Node[] nodes = kademlia.Value.GetAllAtDistance(distance); @@ -111,7 +113,7 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel int[] distances = GetLookupDistances(receiver, target); byte[] requestId = CreateRequestId(); Discv5FindNode findNode = new(requestId, distances); - NodesResponseHandler responseHandler = new(receiver, distances); + NodesResponseHandler responseHandler = new(receiver, distances, _distance); await SendRequest(receiver, findNode, Discv5MessageType.Nodes, responseHandler, _findNodeTimeout, token); Node[] nodes = responseHandler.GetNodes(); @@ -435,7 +437,7 @@ private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) for (int i = 0; i < distances.Length && result.Count < MaxFindNodeRecords; i++) { int distance = distances[i]; - if (distance < 0 || distance > Hash256XorUtils.MaxDistance) + if (distance < 0 || distance > _distance.MaxDistance) { continue; } @@ -524,9 +526,7 @@ private void RegisterKnownRecord(Node node) private int[] GetLookupDistances(Node receiver, PublicKey target) { - KademliaHash receiverHash = KademliaHash.FromBytes(receiver.Id.Hash.Bytes); - KademliaHash targetHash = KademliaHash.FromBytes(target.Hash.Bytes); - int distance = Hash256XorUtils.CalculateLogDistance(receiverHash, targetHash); + int distance = _distance.CalculateLogDistance(receiver.Id.Hash, target.Hash); List distances = [distance]; if (distance > 0) @@ -534,7 +534,7 @@ private int[] GetLookupDistances(Node receiver, PublicKey target) distances.Add(distance - 1); } - if (distance < Hash256XorUtils.MaxDistance) + if (distance < _distance.MaxDistance) { distances.Add(distance + 1); } @@ -770,7 +770,7 @@ public bool Handle(Discv5Message message) } } - internal sealed class NodesResponseHandler(Node receiver, int[] requestedDistances) : IResponseHandler + internal sealed class NodesResponseHandler(Node receiver, int[] requestedDistances, IKademliaDistance distanceCalculator) : IResponseHandler { private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly List _nodes = []; @@ -833,9 +833,7 @@ public bool Handle(Discv5Message message) private bool MatchesRequestedDistance(Node node, int[] requestedDistances) { - KademliaHash receiverHash = KademliaHash.FromBytes(receiver.Id.Hash.Bytes); - KademliaHash nodeHash = KademliaHash.FromBytes(node.Id.Hash.Bytes); - int distance = Hash256XorUtils.CalculateLogDistance(receiverHash, nodeHash); + int distance = distanceCalculator.CalculateLogDistance(receiver.Id.Hash, node.Id.Hash); for (int i = 0; i < requestedDistances.Length; i++) { if (requestedDistances[i] == distance) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs index 196111706092..4edd894a3d3d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs @@ -8,6 +8,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv5; @@ -22,7 +23,7 @@ public class Discv5NodeSource( private readonly ILogger _logger = logManager.GetClassLogger(); private readonly Hash256 _currentNodeHash = kademliaConfig.CurrentNodeId.IdHash; - private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256XorUtils.MaxDistance); + private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256KademliaDistance.Instance.MaxDistance); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs new file mode 100644 index 000000000000..822bf4be22c2 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; + +namespace Nethermind.Network.Discovery.Kademlia; + +/// +/// Kademlia XOR-distance operations for Nethermind's 256-bit hash type. +/// +public sealed class Hash256KademliaDistance : IKademliaDistance +{ + /// + /// Shared stateless instance. + /// + public static Hash256KademliaDistance Instance { get; } = new(); + + /// + public int MaxDistance => Hash256.Size * 8; + + /// + public Hash256 Zero => Hash256.Zero; + + /// + public int CalculateLogDistance(Hash256 left, Hash256 right) + { + ReadOnlySpan leftBytes = left.Bytes; + ReadOnlySpan rightBytes = right.Bytes; + int zeros = 0; + + for (int i = 0; i < Hash256.Size; i++) + { + byte xor = (byte)(leftBytes[i] ^ rightBytes[i]); + if (xor == 0) + { + zeros += 8; + continue; + } + + int nonZeroPostfix = 1; + while ((xor >>= 1) != 0) + { + nonZeroPostfix++; + } + + zeros += 8 - nonZeroPostfix; + break; + } + + return MaxDistance - zeros; + } + + /// + public int Compare(Hash256 left, Hash256 right, Hash256 target) + { + ReadOnlySpan leftBytes = left.Bytes; + ReadOnlySpan rightBytes = right.Bytes; + ReadOnlySpan targetBytes = target.Bytes; + + for (int i = 0; i < Hash256.Size; i++) + { + byte leftDistance = (byte)(leftBytes[i] ^ targetBytes[i]); + byte rightDistance = (byte)(rightBytes[i] ^ targetBytes[i]); + int compared = leftDistance.CompareTo(rightDistance); + if (compared != 0) + { + return compared; + } + } + + return 0; + } + + /// + public bool GetBit(Hash256 key, int index) + { + int byteIndex = index / 8; + int bitIndex = index % 8; + return (key.Bytes[byteIndex] & (1 << (7 - bitIndex))) != 0; + } + + /// + public Hash256 SetBit(Hash256 key, int index) + { + byte[] bytes = key.Bytes.ToArray(); + bytes[index / 8] |= (byte)(1 << (7 - (index % 8))); + return new Hash256(bytes); + } + + /// + /// Creates a random 256-bit key at the requested XOR log distance from . + /// + public Hash256 GetRandomHashAtDistance(Hash256 currentHash, int distance) => + GetRandomHashAtDistance(currentHash, distance, Random.Shared); + + /// + /// Creates a random 256-bit key at the requested XOR log distance from . + /// + public Hash256 GetRandomHashAtDistance(Hash256 currentHash, int distance, Random random) + { + if ((uint)distance > MaxDistance) + { + throw new ArgumentOutOfRangeException(nameof(distance), distance, $"Distance must be between 0 and {MaxDistance}."); + } + + Span randomized = stackalloc byte[Hash256.Size]; + random.NextBytes(randomized); + return CopyForRandom(currentHash, randomized, MaxDistance - distance); + } + + private Hash256 CopyForRandom(Hash256 currentHash, Span randomizedHash, int distance) + { + if (distance >= MaxDistance) + { + return currentHash; + } + + currentHash.Bytes[..(distance / 8)].CopyTo(randomizedHash); + + int remainingBit = distance % 8; + int remainingBitByte = distance / 8; + byte mask = (byte)(~((1 << (8 - remainingBit)) - 1)); + byte randomized = randomizedHash[remainingBitByte]; + byte original = currentHash.Bytes[remainingBitByte]; + randomizedHash[remainingBitByte] = (byte)((original & mask) | (randomized & ~mask)); + + if (distance <= MaxDistance - 1) + { + int nextBit = distance % 8; + int nextBitByte = distance / 8; + mask = (byte)(1 << (7 - nextBit)); + randomized = randomizedHash[nextBitByte]; + byte opposite = (byte)~currentHash.Bytes[nextBitByte]; + randomizedHash[nextBitByte] = (byte)((opposite & mask) | (randomized & ~mask)); + } + + return new Hash256(randomizedHash); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index ed4252e4d30c..d57621569da5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -12,7 +12,8 @@ namespace Nethermind.Network.Discovery.Kademlia; /// A kademlia module. /// Application is expected to expose a /// - -/// - +/// - +/// - /// - /// for the table bootstrap and maintenance to function. /// Call to start the table. @@ -23,18 +24,21 @@ namespace Nethermind.Network.Discovery.Kademlia; /// /// Key is the type that represent the target or hash. /// Type of the node. -public class KademliaModule : Module where TNode : notnull +/// Type of the key-space value used by the routing table. +public class KademliaModule : Module + where TNode : notnull + where TKadKey : notnull { protected override void Load(ContainerBuilder builder) { base.Load(builder); builder - .AddSingleton, Kademlia>() - .AddSingleton, LookupKNearestNeighbour>() - .AddSingleton, FromKeyNodeHashProvider>() - .AddSingleton, KBucketTree>() - .AddSingleton, IteratorNodeLookup>() - .AddSingleton, NodeHealthTracker>(); + .AddSingleton, Kademlia>() + .AddSingleton, LookupKNearestNeighbour>() + .AddSingleton, FromKeyNodeHashProvider>() + .AddSingleton, KBucketTree>() + .AddSingleton, IteratorNodeLookup>() + .AddSingleton, NodeHealthTracker>(); } } From a60aac28ea457e02d2e0b82b6c64d75c915a97be Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 28 May 2026 21:14:29 +0300 Subject: [PATCH 124/182] Reduce discv5 message allocations --- .../Discv5/Discv5CodecTests.cs | 39 +-- .../Discv5/Discv5KademliaAdapterTests.cs | 3 +- .../Discv5/Discv5KademliaAdapter.cs | 104 ++++---- .../Discv5/Discv5MessageCodec.cs | 231 ++++++++++++------ .../Discv5/Discv5Messages.cs | 52 ---- .../Discv5/Discv5PacketCodec.cs | 23 +- .../Discv5/Messages/Discv5Distances.cs | 103 ++++++++ .../Discv5/Messages/Discv5FindNode.cs | 22 ++ .../Discv5/Messages/Discv5Message.cs | 30 +++ .../Discv5/Messages/Discv5MessageBuffer.cs | 26 ++ .../Discv5/Messages/Discv5MessageType.cs | 14 ++ .../Discv5/Messages/Discv5Nodes.cs | 27 ++ .../Discv5/Messages/Discv5Ping.cs | 20 ++ .../Discv5/Messages/Discv5Pong.cs | 30 +++ .../Discv5/Messages/Discv5RequestId.cs | 44 ++++ .../Discv5/Messages/Discv5TalkReq.cs | 25 ++ .../Discv5/Messages/Discv5TalkResp.cs | 20 ++ .../Nethermind.Network.Enr/EnrContentEntry.cs | 31 +++ .../Nethermind.Network.Enr/EthEntry.cs | 10 + .../Nethermind.Network.Enr/IdEntry.cs | 2 + .../Nethermind.Network.Enr/Ip6Entry.cs | 7 + .../Nethermind.Network.Enr/IpEntry.cs | 7 + .../Nethermind.Network.Enr/NodeRecord.cs | 33 ++- .../Nethermind.Network.Enr/SecP256k1Entry.cs | 2 + .../Nethermind.Network.Enr/Tcp6Entry.cs | 2 + .../Nethermind.Network.Enr/TcpEntry.cs | 2 + .../Nethermind.Network.Enr/Udp6Entry.cs | 2 + .../Nethermind.Network.Enr/UdpEntry.cs | 2 + 28 files changed, 713 insertions(+), 200 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs index 3cba54b8e3d9..20192bc0ec14 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Crypto; +using Nethermind.Core.Collections; using Nethermind.Core.Extensions; using Nethermind.Core.Test.Modules; using Nethermind.Crypto; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; using NUnit.Framework; @@ -76,8 +78,9 @@ public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() Assert.That(decrypted, Is.True); Assert.That(message, Is.InstanceOf()); Discv5Ping ping = (Discv5Ping)message; - Assert.That(ping.RequestId, Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); Assert.That(ping.EnrSequence, Is.EqualTo(2)); + message.Dispose(); } [Test] @@ -153,17 +156,19 @@ public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( Assert.That(session.ReadKey.ToHexString(true), Is.EqualTo(expectedReadKeyHex)); Assert.That(message, Is.InstanceOf()); Discv5Ping ping = (Discv5Ping)message; - Assert.That(ping.RequestId, Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); Assert.That(ping.EnrSequence, Is.EqualTo(1)); Assert.That(nodeRecord is not null, Is.EqualTo(includesRecord)); + message.Dispose(); } [Test] public void MessageCodec_Roundtrips_FindNode() { - Discv5FindNode message = new([0, 0, 0, 1], [255, 254, 256]); + using Discv5FindNode message = new([0, 0, 0, 1], [255, 254, 256]); - Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); + using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); Assert.That(decoded, Is.InstanceOf()); Discv5FindNode decodedFindNode = (Discv5FindNode)decoded; @@ -174,9 +179,10 @@ public void MessageCodec_Roundtrips_FindNode() [Test] public void MessageCodec_Roundtrips_Pong() { - Discv5Pong message = new([0, 0, 0, 2], 3, IPAddress.Parse("192.0.2.1"), 30303); + using Discv5Pong message = new([0, 0, 0, 2], 3, IPAddress.Parse("192.0.2.1"), 30303); - Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); + using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); Assert.That(decoded, Is.InstanceOf()); Discv5Pong decodedPong = (Discv5Pong)decoded; @@ -189,28 +195,30 @@ public void MessageCodec_Roundtrips_Pong() [Test] public void MessageCodec_Roundtrips_TalkReq() { - Discv5TalkReq message = new([0, 0, 0, 3], "eth"u8.ToArray(), [1, 2, 3, 4]); + using Discv5TalkReq message = new([0, 0, 0, 3], "eth"u8.ToArray(), new byte[] { 1, 2, 3, 4 }); - Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); + using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); Assert.That(decoded, Is.InstanceOf()); Discv5TalkReq decodedTalkReq = (Discv5TalkReq)decoded; Assert.That(decodedTalkReq.RequestId, Is.EqualTo(message.RequestId)); - Assert.That(decodedTalkReq.Protocol, Is.EqualTo(message.Protocol)); - Assert.That(decodedTalkReq.Request, Is.EqualTo(message.Request)); + Assert.That(decodedTalkReq.Protocol.ToArray(), Is.EqualTo(message.Protocol.ToArray())); + Assert.That(decodedTalkReq.Request.ToArray(), Is.EqualTo(message.Request.ToArray())); } [Test] public void MessageCodec_Roundtrips_TalkResp() { - Discv5TalkResp message = new([0, 0, 0, 4], [5, 6, 7, 8]); + using Discv5TalkResp message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); - Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); + using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); Assert.That(decoded, Is.InstanceOf()); Discv5TalkResp decodedTalkResp = (Discv5TalkResp)decoded; Assert.That(decodedTalkResp.RequestId, Is.EqualTo(message.RequestId)); - Assert.That(decodedTalkResp.Response, Is.EqualTo(message.Response)); + Assert.That(decodedTalkResp.Response.ToArray(), Is.EqualTo(message.Response.ToArray())); } [Test] @@ -219,9 +227,10 @@ public void MessageCodec_Roundtrips_Nodes_From_NonZero_ArraySegment() NodeRecord skippedRecord = CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); NodeRecord expectedRecord = CreateNodeRecord(new PrivateKey(GethNodeBPrivateKey)); NodeRecord[] records = [skippedRecord, expectedRecord]; - Discv5Nodes message = new([0, 0, 0, 5], 1, new ArraySegment(records, 1, 1)); + using Discv5Nodes message = new([0, 0, 0, 5], 1, new ArraySegment(records, 1, 1)); - Discv5Message decoded = Discv5MessageCodec.Decode(Discv5MessageCodec.Encode(message)); + using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); + using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); Assert.That(decoded, Is.InstanceOf()); Discv5Nodes decodedNodes = (Discv5Nodes)decoded; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index 0daef3414a0b..767e7c133ceb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -9,6 +9,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; @@ -209,6 +210,6 @@ private static Discv5KademliaAdapter.NodesResponseHandler CreateNodesResponseHan { PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); - return new Discv5KademliaAdapter.NodesResponseHandler(receiver, [distance], Hash256KademliaDistance.Instance); + return new Discv5KademliaAdapter.NodesResponseHandler(receiver, new Discv5Distances([distance]), Hash256KademliaDistance.Instance); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index 5e76705fc946..684e19bdd099 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -6,11 +6,11 @@ using System.Net; using System.Net.Sockets; using Nethermind.Core.Crypto; -using Nethermind.Core.Extensions; using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network; +using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; using Nethermind.Stats.Model; @@ -98,8 +98,7 @@ public async Task Ping(Node receiver, CancellationToken token) { RegisterKnownRecord(receiver); ReserveEndpointCheck(receiver); - byte[] requestId = CreateRequestId(); - Discv5Ping ping = new(requestId, nodeRecordProvider.Current.EnrSequence); + using Discv5Ping ping = new(CreateRequestId(), nodeRecordProvider.Current.EnrSequence); PongResponseHandler responseHandler = new(receiver); await SendRequest(receiver, ping, Discv5MessageType.Pong, responseHandler, _pingTimeout, token); @@ -110,9 +109,8 @@ public async Task Ping(Node receiver, CancellationToken token) public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) { RegisterKnownRecord(receiver); - int[] distances = GetLookupDistances(receiver, target); - byte[] requestId = CreateRequestId(); - Discv5FindNode findNode = new(requestId, distances); + Discv5Distances distances = GetLookupDistances(receiver, target); + using Discv5FindNode findNode = new(CreateRequestId(), distances); NodesResponseHandler responseHandler = new(receiver, distances, _distance); await SendRequest(receiver, findNode, Discv5MessageType.Nodes, responseHandler, _findNodeTimeout, token); @@ -154,7 +152,7 @@ private async Task SendRequest( TimeSpan timeout, CancellationToken token) { - ResponseKey responseKey = new(receiver.Id.Hash, RequestIdKey.From(request.RequestId), responseType); + ResponseKey responseKey = new(receiver.Id.Hash, request.RequestId, responseType); _responseHandlers.Set(responseKey, responseHandler); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -293,7 +291,14 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Discv5Packet packet, Canc return; } - await HandleMessage(session.RemotePublicKey, endpoint, message, token); + try + { + await HandleMessage(session.RemotePublicKey, endpoint, message, token); + } + finally + { + message.Dispose(); + } } private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) @@ -333,7 +338,14 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can } SetSession(new SessionKey(nodeId, endpoint), session); - await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); + try + { + await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); + } + finally + { + message.Dispose(); + } } private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket, byte[] destinationNodeId) @@ -374,10 +386,11 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, switch (message) { case Discv5Ping ping: - await SendResponse( - remoteNode, - new Discv5Pong(ping.RequestId, nodeRecordProvider.Current.EnrSequence, endpoint.Address, endpoint.Port), - token); + using (Discv5Pong pong = new(ping.RequestId, nodeRecordProvider.Current.EnrSequence, endpoint.Address, endpoint.Port)) + { + await SendResponse(remoteNode, pong, token); + } + kademlia.Value.AddOrRefresh(remoteNode); if (!string.IsNullOrEmpty(remoteNode.Enr)) { @@ -389,7 +402,11 @@ await SendResponse( kademlia.Value.AddOrRefresh(remoteNode); break; case Discv5TalkReq talkReq: - await SendResponse(remoteNode, new Discv5TalkResp(talkReq.RequestId, []), token); + using (Discv5TalkResp talkResp = new(talkReq.RequestId, ReadOnlyMemory.Empty)) + { + await SendResponse(remoteNode, talkResp, token); + } + break; } } @@ -406,7 +423,7 @@ await SendResponse( private bool HandleResponse(Hash256 nodeId, Discv5Message message) { - ResponseKey responseKey = new(nodeId, RequestIdKey.From(message.RequestId), message.MessageType); + ResponseKey responseKey = new(nodeId, message.RequestId, message.MessageType); return _responseHandlers.TryGetValue(responseKey, out IResponseHandler? handler) && handler.Handle(message); } @@ -415,7 +432,8 @@ private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, Canc NodeRecord[] records = GetFindNodeRecords(findNode.Distances, remoteNode); if (records.Length == 0) { - await SendResponse(remoteNode, new Discv5Nodes(findNode.RequestId, 1, []), token); + using Discv5Nodes emptyResponse = new(findNode.RequestId, 1, []); + await SendResponse(remoteNode, emptyResponse, token); return; } @@ -424,17 +442,18 @@ private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, Canc { int count = Math.Min(MaxEnrsPerNodesMessage, records.Length - i); ArraySegment chunk = new(records, i, count); - await SendResponse(remoteNode, new Discv5Nodes(findNode.RequestId, total, chunk), token); + using Discv5Nodes nodes = new(findNode.RequestId, total, chunk); + await SendResponse(remoteNode, nodes, token); } } - private NodeRecord[] GetFindNodeRecords(int[] distances, Node requester) + private NodeRecord[] GetFindNodeRecords(Discv5Distances distances, Node requester) { HashSet seen = new(MaxFindNodeRecords); List result = new(MaxFindNodeRecords); bool allowNonRoutableRelays = NodeFilter.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); bool includedSelf = false; - for (int i = 0; i < distances.Length && result.Count < MaxFindNodeRecords; i++) + for (int i = 0; i < distances.Count && result.Count < MaxFindNodeRecords; i++) { int distance = distances[i]; if (distance < 0 || distance > _distance.MaxDistance) @@ -524,28 +543,37 @@ private void RegisterKnownRecord(Node node) } } - private int[] GetLookupDistances(Node receiver, PublicKey target) + private Discv5Distances GetLookupDistances(Node receiver, PublicKey target) { int distance = _distance.CalculateLogDistance(receiver.Id.Hash, target.Hash); - List distances = [distance]; + Span distances = stackalloc int[3]; + distances[0] = distance; + int count = 1; if (distance > 0) { - distances.Add(distance - 1); + distances[count++] = distance - 1; } if (distance < _distance.MaxDistance) { - distances.Add(distance + 1); + distances[count++] = distance + 1; } - return [.. distances]; + return new Discv5Distances(distances[..count]); } - private byte[] CreateRequestId() + private Discv5RequestId CreateRequestId() { - byte[] requestId = cryptoRandom.GenerateRandomBytes(sizeof(ulong)); - return requestId.AsSpan().WithoutLeadingZeros().ToArray(); + Span requestId = stackalloc byte[sizeof(ulong)]; + cryptoRandom.GenerateRandomBytes(requestId); + int start = 0; + while (start < requestId.Length && requestId[start] == 0) + { + start++; + } + + return Discv5RequestId.From(requestId[start..]); } private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGetValue(sessionKey, out session); @@ -709,21 +737,7 @@ private void Trim() private readonly record struct PendingNonceKey(IPEndPoint Endpoint, NonceKey Nonce); - private readonly record struct ResponseKey(Hash256 NodeId, RequestIdKey RequestId, Discv5MessageType MessageType); - - private readonly record struct RequestIdKey(ulong Value, byte Length) - { - public static RequestIdKey From(ReadOnlySpan requestId) - { - ulong value = 0; - for (int i = 0; i < requestId.Length; i++) - { - value = (value << 8) | requestId[i]; - } - - return new RequestIdKey(value, checked((byte)requestId.Length)); - } - } + private readonly record struct ResponseKey(Hash256 NodeId, Discv5RequestId RequestId, Discv5MessageType MessageType); private readonly record struct NonceKey(ulong Prefix, uint Suffix) { @@ -770,7 +784,7 @@ public bool Handle(Discv5Message message) } } - internal sealed class NodesResponseHandler(Node receiver, int[] requestedDistances, IKademliaDistance distanceCalculator) : IResponseHandler + internal sealed class NodesResponseHandler(Node receiver, Discv5Distances requestedDistances, IKademliaDistance distanceCalculator) : IResponseHandler { private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly List _nodes = []; @@ -831,10 +845,10 @@ public bool Handle(Discv5Message message) public Node[] GetNodes() => [.. _nodes]; - private bool MatchesRequestedDistance(Node node, int[] requestedDistances) + private bool MatchesRequestedDistance(Node node, Discv5Distances requestedDistances) { int distance = distanceCalculator.CalculateLogDistance(receiver.Id.Hash, node.Id.Hash); - for (int i = 0; i < requestedDistances.Length; i++) + for (int i = 0; i < requestedDistances.Count; i++) { if (requestedDistances[i] == distance) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs index 9e0c87ff7067..f75d6466ab0f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Net; +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; @@ -9,102 +11,152 @@ namespace Nethermind.Network.Discovery.Discv5; internal static class Discv5MessageCodec { - private const int MaxRequestIdLength = 8; - - public static byte[] Encode(Discv5Message message) + public static ArrayPoolSpan Encode(Discv5Message message) { int contentLength = GetContentLength(message); - byte[] result = new byte[Rlp.LengthOfSequence(contentLength) + 1]; - result[0] = (byte)message.MessageType; - RlpStream stream = new(result) { Position = 1 }; - stream.StartSequence(contentLength); - EncodeContent(stream, message); + ArrayPoolSpan result = new(Rlp.LengthOfSequence(contentLength) + 1); + try + { + Span resultSpan = result; + int position = 0; + resultSpan[position++] = (byte)message.MessageType; + position = Rlp.StartSequence(resultSpan, position, contentLength); + EncodeContent(resultSpan, ref position, message); + } + catch + { + result.Dispose(); + throw; + } + return result; } public static Discv5Message Decode(ReadOnlySpan message) + => Decode(message, default, null); + + public static Discv5Message Decode(ReadOnlyMemory message, IDisposable owner) + => Decode(message.Span, message, owner); + + private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory ownedMessage, IDisposable? owner) { if (message.IsEmpty) { + owner?.Dispose(); throw new RlpException("Empty discv5 message."); } - Discv5MessageType messageType = (Discv5MessageType)message[0]; - Rlp.ValueDecoderContext ctx = new(message[1..]); - int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + Discv5Message? decoded = null; + try + { + Discv5MessageType messageType = (Discv5MessageType)message[0]; + Rlp.ValueDecoderContext ctx = new(message[1..]); + int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + + Discv5RequestId requestId = DecodeRequestId(ref ctx); + decoded = messageType switch + { + Discv5MessageType.Ping => new Discv5Ping(requestId, ctx.DecodeULong(), owner), + Discv5MessageType.Pong => DecodePong(requestId, ref ctx, owner), + Discv5MessageType.FindNode => new Discv5FindNode(requestId, DecodeDistances(ref ctx), owner), + Discv5MessageType.Nodes => DecodeNodes(requestId, ref ctx, owner), + Discv5MessageType.TalkReq => new Discv5TalkReq( + requestId, + DecodeByteMemory(ref ctx, ownedMessage), + DecodeByteMemory(ref ctx, ownedMessage), + owner), + Discv5MessageType.TalkResp => new Discv5TalkResp(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner), + _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") + }; - byte[] requestId = DecodeRequestId(ref ctx); - Discv5Message decoded = messageType switch + ctx.Check(checkPosition); + ctx.CheckEnd(); + return decoded; + } + catch { - Discv5MessageType.Ping => new Discv5Ping(requestId, ctx.DecodeULong()), - Discv5MessageType.Pong => DecodePong(requestId, ref ctx), - Discv5MessageType.FindNode => new Discv5FindNode(requestId, DecodeDistances(ref ctx)), - Discv5MessageType.Nodes => DecodeNodes(requestId, ref ctx), - Discv5MessageType.TalkReq => new Discv5TalkReq(requestId, ctx.DecodeByteArray(), ctx.DecodeByteArray()), - Discv5MessageType.TalkResp => new Discv5TalkResp(requestId, ctx.DecodeByteArray()), - _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") - }; + if (decoded is not null) + { + decoded.Dispose(); + } + else + { + owner?.Dispose(); + } - ctx.Check(checkPosition); - ctx.CheckEnd(); - return decoded; + throw; + } } private static int GetContentLength(Discv5Message message) => message switch { - Discv5Ping ping => Rlp.LengthOf(ping.RequestId) + Rlp.LengthOf(ping.EnrSequence), - Discv5Pong pong => Rlp.LengthOf(pong.RequestId) + + Discv5Ping ping => GetRequestIdLength(ping.RequestId) + Rlp.LengthOf(ping.EnrSequence), + Discv5Pong pong => GetRequestIdLength(pong.RequestId) + Rlp.LengthOf(pong.EnrSequence) + GetAddressRlpLength(pong.RecipientIp) + Rlp.LengthOf(pong.RecipientPort), - Discv5FindNode findNode => Rlp.LengthOf(findNode.RequestId) + GetDistancesLength(findNode.Distances), - Discv5Nodes nodes => Rlp.LengthOf(nodes.RequestId) + Rlp.LengthOf(nodes.Total) + GetNodeRecordsLength(nodes.Records), - Discv5TalkReq talkReq => Rlp.LengthOf(talkReq.RequestId) + Rlp.LengthOf(talkReq.Protocol) + Rlp.LengthOf(talkReq.Request), - Discv5TalkResp talkResp => Rlp.LengthOf(talkResp.RequestId) + Rlp.LengthOf(talkResp.Response), + Discv5FindNode findNode => GetRequestIdLength(findNode.RequestId) + GetDistancesLength(findNode.Distances), + Discv5Nodes nodes => GetRequestIdLength(nodes.RequestId) + Rlp.LengthOf(nodes.Total) + GetNodeRecordsLength(nodes.Records), + Discv5TalkReq talkReq => GetRequestIdLength(talkReq.RequestId) + Rlp.LengthOf(talkReq.Protocol.Span) + Rlp.LengthOf(talkReq.Request.Span), + Discv5TalkResp talkResp => GetRequestIdLength(talkResp.RequestId) + Rlp.LengthOf(talkResp.Response.Span), _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") }; - private static void EncodeContent(RlpStream stream, Discv5Message message) + private static void EncodeContent(Span buffer, ref int position, Discv5Message message) { switch (message) { case Discv5Ping ping: - stream.Encode(ping.RequestId); - stream.Encode(ping.EnrSequence); + EncodeRequestId(buffer, ref position, ping.RequestId); + Encode(buffer, ref position, ping.EnrSequence); break; case Discv5Pong pong: - stream.Encode(pong.RequestId); - stream.Encode(pong.EnrSequence); - EncodeAddress(stream, pong.RecipientIp); - stream.Encode(pong.RecipientPort); + EncodeRequestId(buffer, ref position, pong.RequestId); + Encode(buffer, ref position, pong.EnrSequence); + EncodeAddress(buffer, ref position, pong.RecipientIp); + Encode(buffer, ref position, pong.RecipientPort); break; case Discv5FindNode findNode: - stream.Encode(findNode.RequestId); - EncodeDistances(stream, findNode.Distances); + EncodeRequestId(buffer, ref position, findNode.RequestId); + EncodeDistances(buffer, ref position, findNode.Distances); break; case Discv5Nodes nodes: - stream.Encode(nodes.RequestId); - stream.Encode(nodes.Total); - EncodeNodeRecords(stream, nodes.Records); + EncodeRequestId(buffer, ref position, nodes.RequestId); + Encode(buffer, ref position, nodes.Total); + EncodeNodeRecords(buffer, ref position, nodes.Records); break; case Discv5TalkReq talkReq: - stream.Encode(talkReq.RequestId); - stream.Encode(talkReq.Protocol); - stream.Encode(talkReq.Request); + EncodeRequestId(buffer, ref position, talkReq.RequestId); + position = Rlp.Encode(buffer, position, talkReq.Protocol.Span); + position = Rlp.Encode(buffer, position, talkReq.Request.Span); break; case Discv5TalkResp talkResp: - stream.Encode(talkResp.RequestId); - stream.Encode(talkResp.Response); + EncodeRequestId(buffer, ref position, talkResp.RequestId); + position = Rlp.Encode(buffer, position, talkResp.Response.Span); break; default: throw new RlpException($"Unsupported discv5 message {message.GetType().Name}."); } } - private static int GetDistancesLength(int[] distances) + private static int GetRequestIdLength(Discv5RequestId requestId) + { + Span bytes = stackalloc byte[Discv5RequestId.MaxLength]; + requestId.CopyTo(bytes); + return Rlp.LengthOf(bytes[..requestId.Length]); + } + + private static void EncodeRequestId(Span buffer, ref int position, Discv5RequestId requestId) + { + Span bytes = stackalloc byte[Discv5RequestId.MaxLength]; + requestId.CopyTo(bytes); + position = Rlp.Encode(buffer, position, bytes[..requestId.Length]); + } + + private static int GetDistancesLength(Discv5Distances distances) { int contentLength = 0; - for (int i = 0; i < distances.Length; i++) + for (int i = 0; i < distances.Count; i++) { contentLength += Rlp.LengthOf(distances[i]); } @@ -112,18 +164,18 @@ private static int GetDistancesLength(int[] distances) return Rlp.LengthOfSequence(contentLength); } - private static void EncodeDistances(RlpStream stream, int[] distances) + private static void EncodeDistances(Span buffer, ref int position, Discv5Distances distances) { int contentLength = 0; - for (int i = 0; i < distances.Length; i++) + for (int i = 0; i < distances.Count; i++) { contentLength += Rlp.LengthOf(distances[i]); } - stream.StartSequence(contentLength); - for (int i = 0; i < distances.Length; i++) + position = Rlp.StartSequence(buffer, position, contentLength); + for (int i = 0; i < distances.Count; i++) { - stream.Encode(distances[i]); + Encode(buffer, ref position, distances[i]); } } @@ -138,7 +190,7 @@ private static int GetNodeRecordsLength(IReadOnlyList records) return Rlp.LengthOfSequence(contentLength); } - private static void EncodeNodeRecords(RlpStream stream, IReadOnlyList records) + private static void EncodeNodeRecords(Span buffer, ref int position, IReadOnlyList records) { int contentLength = 0; for (int i = 0; i < records.Count; i++) @@ -146,10 +198,10 @@ private static void EncodeNodeRecords(RlpStream stream, IReadOnlyList buffer, ref int position, IPAddress ip) { Span bytes = stackalloc byte[16]; if (ip.TryWriteBytes(bytes, out int bytesWritten)) { - stream.Encode(bytes[..bytesWritten]); + position = Rlp.Encode(buffer, position, bytes[..bytesWritten]); return; } - stream.Encode(ip.GetAddressBytes()); + position = Rlp.Encode(buffer, position, ip.GetAddressBytes()); } - private static byte[] DecodeRequestId(ref Rlp.ValueDecoderContext ctx) + private static void Encode(Span buffer, ref int position, ulong value) + => position += Rlp.Encode(value, buffer[position..]).Length; + + private static void Encode(Span buffer, ref int position, int value) + => position += Rlp.Encode((long)value, buffer[position..]).Length; + + private static Discv5RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) { - byte[] requestId = ctx.DecodeByteArray(); - if (requestId.Length > MaxRequestIdLength) + ReadOnlySpan requestId = ctx.DecodeByteArraySpan(); + if (requestId.Length > Discv5RequestId.MaxLength) { - throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {MaxRequestIdLength}."); + throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {Discv5RequestId.MaxLength}."); } - return requestId; + return Discv5RequestId.From(requestId); } - private static Discv5Pong DecodePong(byte[] requestId, ref Rlp.ValueDecoderContext ctx) + private static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) + { + ReadOnlySpan value = ctx.DecodeByteArraySpan(); + if (ownedMessage.IsEmpty) + { + return value.ToArray(); + } + + return ownedMessage.Slice(1 + ctx.Position - value.Length, value.Length); + } + + private static Discv5Pong DecodePong(Discv5RequestId requestId, ref Rlp.ValueDecoderContext ctx, IDisposable? owner) { ulong enrSequence = ctx.DecodeULong(); - IPAddress recipientIp = new(ctx.DecodeByteArray()); + IPAddress recipientIp = new(ctx.DecodeByteArraySpan()); int recipientPort = ctx.DecodePositiveInt(); - return new Discv5Pong(requestId, enrSequence, recipientIp, recipientPort); + return new Discv5Pong(requestId, enrSequence, recipientIp, recipientPort, owner); } - private static int[] DecodeDistances(ref Rlp.ValueDecoderContext ctx) + private static Discv5Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) { int checkPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(checkPosition); - int[] distances = new int[count]; - for (int i = 0; i < count; i++) + Discv5Distances distances = new(count); + try { - distances[i] = ctx.DecodePositiveInt(); - } + for (int i = 0; i < count; i++) + { + distances.Set(i, ctx.DecodePositiveInt()); + } - ctx.Check(checkPosition); - return distances; + ctx.Check(checkPosition); + return distances; + } + catch + { + distances.Dispose(); + throw; + } } - private static Discv5Nodes DecodeNodes(byte[] requestId, ref Rlp.ValueDecoderContext ctx) + private static Discv5Nodes DecodeNodes(Discv5RequestId requestId, ref Rlp.ValueDecoderContext ctx, IDisposable? owner) { int total = ctx.DecodePositiveInt(); int checkPosition = ctx.ReadSequenceLength() + ctx.Position; @@ -227,6 +304,6 @@ private static Discv5Nodes DecodeNodes(byte[] requestId, ref Rlp.ValueDecoderCon } ctx.Check(checkPosition); - return new Discv5Nodes(requestId, total, records); + return new Discv5Nodes(requestId, total, records, owner); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs deleted file mode 100644 index b75f1ebd880d..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5Messages.cs +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Net; -using Nethermind.Network.Enr; - -namespace Nethermind.Network.Discovery.Discv5; - -internal enum Discv5MessageType : byte -{ - Ping = 0x01, - Pong = 0x02, - FindNode = 0x03, - Nodes = 0x04, - TalkReq = 0x05, - TalkResp = 0x06 -} - -internal abstract record Discv5Message(byte[] RequestId) -{ - public abstract Discv5MessageType MessageType { get; } -} - -internal sealed record Discv5Ping(byte[] RequestId, ulong EnrSequence) : Discv5Message(RequestId) -{ - public override Discv5MessageType MessageType => Discv5MessageType.Ping; -} - -internal sealed record Discv5Pong(byte[] RequestId, ulong EnrSequence, IPAddress RecipientIp, int RecipientPort) : Discv5Message(RequestId) -{ - public override Discv5MessageType MessageType => Discv5MessageType.Pong; -} - -internal sealed record Discv5FindNode(byte[] RequestId, int[] Distances) : Discv5Message(RequestId) -{ - public override Discv5MessageType MessageType => Discv5MessageType.FindNode; -} - -internal sealed record Discv5Nodes(byte[] RequestId, int Total, IReadOnlyList Records) : Discv5Message(RequestId) -{ - public override Discv5MessageType MessageType => Discv5MessageType.Nodes; -} - -internal sealed record Discv5TalkReq(byte[] RequestId, byte[] Protocol, byte[] Request) : Discv5Message(RequestId) -{ - public override Discv5MessageType MessageType => Discv5MessageType.TalkReq; -} - -internal sealed record Discv5TalkResp(byte[] RequestId, byte[] Response) : Discv5Message(RequestId) -{ - public override Discv5MessageType MessageType => Discv5MessageType.TalkResp; -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs index fe15d753eb29..3c79c43d1742 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs @@ -5,8 +5,10 @@ using System.Security.Cryptography; using System.Text; using Autofac.Features.AttributeFilters; +using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Crypto; +using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; @@ -186,7 +188,7 @@ internal static bool TryDecryptMessageForTest(Discv5Packet packet, byte[] encryp return false; } - byte[] plaintext = new byte[packet.Message.Length - AesGcmTagSize]; + Discv5MessageBuffer plaintext = new(packet.Message.Length - AesGcmTagSize); try { using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); @@ -194,16 +196,22 @@ internal static bool TryDecryptMessageForTest(Discv5Packet packet, byte[] encryp packet.Nonce, packet.Message.AsSpan(0, plaintext.Length), packet.Message.AsSpan(plaintext.Length, AesGcmTagSize), - plaintext, + plaintext.Span, packet.MessageAd); + + message = Discv5MessageCodec.Decode(plaintext.Memory, plaintext); + return true; } catch (CryptographicException) { + plaintext.Dispose(); return false; } - - message = Discv5MessageCodec.Decode(plaintext); - return true; + catch + { + plaintext.Dispose(); + throw; + } } internal Discv5Challenge DecodeWhoAreYou(Discv5Packet packet) @@ -316,7 +324,8 @@ private byte[] EncodePacket( { ArgumentNullException.ThrowIfNull(encryptionKey); - encryptedMessage = EncryptMessage(encryptionKey, nonce, Discv5MessageCodec.Encode(message), messageAd); + using ArrayPoolSpan encodedMessage = Discv5MessageCodec.Encode(message); + encryptedMessage = EncryptMessage(encryptionKey, nonce, encodedMessage, messageAd); } byte[] maskedHeader = AesCtrTransform(destinationNodeId[..AesKeySize], maskingIv, header); @@ -346,7 +355,7 @@ private static byte[] CreateHeader(Discv5PacketFlag flag, byte[] nonce, byte[] a private byte[] CreateNonce() => _cryptoRandom.GenerateRandomBytes(NonceSize); - private static byte[] EncryptMessage(byte[] encryptionKey, byte[] nonce, byte[] plaintext, byte[] messageAd) + private static byte[] EncryptMessage(byte[] encryptionKey, byte[] nonce, ReadOnlySpan plaintext, byte[] messageAd) { byte[] encrypted = new byte[plaintext.Length + AesGcmTagSize]; using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs new file mode 100644 index 000000000000..34776893e8cc --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections; +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed class Discv5Distances : IReadOnlyList, IDisposable +{ + private const int InlineCapacity = 3; + + private int[]? _rented; + private int _first; + private int _second; + private int _third; + + public Discv5Distances(ReadOnlySpan distances) + : this(distances.Length) + { + for (int i = 0; i < distances.Length; i++) + { + Set(i, distances[i]); + } + } + + internal Discv5Distances(int count) + { + Count = count; + if (count > InlineCapacity) + { + _rented = SafeArrayPool.Shared.Rent(count); + } + } + + public int Count { get; } + + public int this[int index] + { + get + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count, nameof(index)); + + if (_rented is not null) + { + return _rented[index]; + } + + return index switch + { + 0 => _first, + 1 => _second, + 2 => _third, + _ => throw new ArgumentOutOfRangeException(nameof(index)) + }; + } + } + + public void Dispose() + { + if (_rented is not null) + { + SafeArrayPool.Shared.Return(_rented); + _rented = null; + } + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal void Set(int index, int value) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count, nameof(index)); + + if (_rented is not null) + { + _rented[index] = value; + return; + } + + switch (index) + { + case 0: + _first = value; + return; + case 1: + _second = value; + return; + case 2: + _third = value; + return; + default: + throw new ArgumentOutOfRangeException(nameof(index)); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs new file mode 100644 index 000000000000..dda24d1efaf6 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record Discv5FindNode : Discv5Message +{ + public Discv5FindNode(ReadOnlySpan requestId, ReadOnlySpan distances) + : this(Discv5RequestId.From(requestId), new Discv5Distances(distances)) + { + } + + public Discv5FindNode(Discv5RequestId requestId, Discv5Distances distances, IDisposable? owner = null) + : base(requestId, owner) + => Distances = distances; + + public override Discv5MessageType MessageType => Discv5MessageType.FindNode; + + public Discv5Distances Distances { get; } + + protected override void DisposeCore() => Distances.Dispose(); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs new file mode 100644 index 000000000000..2db7e0e4e97c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal abstract record Discv5Message : IDisposable +{ + public abstract Discv5MessageType MessageType { get; } + + public Discv5RequestId RequestId { get; } + + private IDisposable? _owner; + + protected Discv5Message(Discv5RequestId requestId, IDisposable? owner = null) + { + RequestId = requestId; + _owner = owner; + } + + public void Dispose() + { + DisposeCore(); + _owner?.Dispose(); + _owner = null; + } + + protected virtual void DisposeCore() + { + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs new file mode 100644 index 000000000000..f60c5f63ca04 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed class Discv5MessageBuffer(int length) : IDisposable +{ + private byte[]? _buffer = SafeArrayPool.Shared.Rent(length); + + public Span Span => _buffer.AsSpan(0, Length); + + public ReadOnlyMemory Memory => _buffer.AsMemory(0, Length); + + public int Length { get; } = length; + + public void Dispose() + { + if (_buffer is not null) + { + SafeArrayPool.Shared.Return(_buffer); + _buffer = null; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs new file mode 100644 index 000000000000..c5ff03e54339 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal enum Discv5MessageType : byte +{ + Ping = 0x01, + Pong = 0x02, + FindNode = 0x03, + Nodes = 0x04, + TalkReq = 0x05, + TalkResp = 0x06 +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs new file mode 100644 index 000000000000..f65594c3d327 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Enr; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record Discv5Nodes : Discv5Message +{ + public Discv5Nodes(ReadOnlySpan requestId, int total, IReadOnlyList records) + : this(Discv5RequestId.From(requestId), total, records) + { + } + + public Discv5Nodes(Discv5RequestId requestId, int total, IReadOnlyList records, IDisposable? owner = null) + : base(requestId, owner) + { + Total = total; + Records = records; + } + + public override Discv5MessageType MessageType => Discv5MessageType.Nodes; + + public int Total { get; } + + public IReadOnlyList Records { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs new file mode 100644 index 000000000000..3c6a2deee457 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record Discv5Ping : Discv5Message +{ + public Discv5Ping(ReadOnlySpan requestId, ulong enrSequence) + : this(Discv5RequestId.From(requestId), enrSequence) + { + } + + public Discv5Ping(Discv5RequestId requestId, ulong enrSequence, IDisposable? owner = null) + : base(requestId, owner) + => EnrSequence = enrSequence; + + public override Discv5MessageType MessageType => Discv5MessageType.Ping; + + public ulong EnrSequence { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs new file mode 100644 index 000000000000..cfc6d4b77b75 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record Discv5Pong : Discv5Message +{ + public Discv5Pong(ReadOnlySpan requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort) + : this(Discv5RequestId.From(requestId), enrSequence, recipientIp, recipientPort) + { + } + + public Discv5Pong(Discv5RequestId requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort, IDisposable? owner = null) + : base(requestId, owner) + { + EnrSequence = enrSequence; + RecipientIp = recipientIp; + RecipientPort = recipientPort; + } + + public override Discv5MessageType MessageType => Discv5MessageType.Pong; + + public ulong EnrSequence { get; } + + public IPAddress RecipientIp { get; } + + public int RecipientPort { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs new file mode 100644 index 000000000000..dda784144386 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal readonly record struct Discv5RequestId(ulong Value, byte Length) +{ + public const int MaxLength = sizeof(ulong); + + public static Discv5RequestId From(ReadOnlySpan requestId) + { + if (requestId.Length > MaxLength) + { + throw new ArgumentOutOfRangeException(nameof(requestId), requestId.Length, $"discv5 request-id length exceeds {MaxLength}."); + } + + ulong value = 0; + for (int i = 0; i < requestId.Length; i++) + { + value = (value << 8) | requestId[i]; + } + + return new Discv5RequestId(value, checked((byte)requestId.Length)); + } + + public void CopyTo(Span destination) + { + ArgumentOutOfRangeException.ThrowIfLessThan(destination.Length, Length, nameof(destination)); + + ulong value = Value; + for (int i = Length - 1; i >= 0; i--) + { + destination[i] = (byte)value; + value >>= 8; + } + } + + public byte[] ToArray() + { + byte[] bytes = new byte[Length]; + CopyTo(bytes); + return bytes; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs new file mode 100644 index 000000000000..21383b51451e --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record Discv5TalkReq : Discv5Message +{ + public Discv5TalkReq(ReadOnlySpan requestId, ReadOnlyMemory protocol, ReadOnlyMemory request) + : this(Discv5RequestId.From(requestId), protocol, request) + { + } + + public Discv5TalkReq(Discv5RequestId requestId, ReadOnlyMemory protocol, ReadOnlyMemory request, IDisposable? owner = null) + : base(requestId, owner) + { + Protocol = protocol; + Request = request; + } + + public override Discv5MessageType MessageType => Discv5MessageType.TalkReq; + + public ReadOnlyMemory Protocol { get; } + + public ReadOnlyMemory Request { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs new file mode 100644 index 000000000000..25e6f6f04d1b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record Discv5TalkResp : Discv5Message +{ + public Discv5TalkResp(ReadOnlySpan requestId, ReadOnlyMemory response) + : this(Discv5RequestId.From(requestId), response) + { + } + + public Discv5TalkResp(Discv5RequestId requestId, ReadOnlyMemory response, IDisposable? owner = null) + : base(requestId, owner) + => Response = response; + + public override Discv5MessageType MessageType => Discv5MessageType.TalkResp; + + public ReadOnlyMemory Response { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs index fa769a3383b2..739052b94067 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Diagnostics; +using System.Text; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Enr @@ -30,9 +32,38 @@ public void Encode(RlpStream rlpStream) EncodeValue(rlpStream); } + /// + /// Encodes the entry into a span-backed buffer. + /// + public void Encode(Span buffer, ref int position) + { + position = EncodeAscii(buffer, position, Key); + EncodeValue(buffer, ref position); + } + protected abstract void EncodeValue(RlpStream rlpStream); + protected abstract void EncodeValue(Span buffer, ref int position); + public override int GetHashCode() => Key.GetHashCode(); + + private static int EncodeAscii(Span buffer, int position, string value) + { + if (string.IsNullOrEmpty(value)) + { + return Rlp.Encode(buffer, position, ReadOnlySpan.Empty); + } + + int byteCount = Encoding.ASCII.GetByteCount(value); + if (byteCount <= 128) + { + Span bytes = stackalloc byte[byteCount]; + Encoding.ASCII.GetBytes(value.AsSpan(), bytes); + return Rlp.Encode(buffer, position, bytes); + } + + return Rlp.Encode(buffer, position, Encoding.ASCII.GetBytes(value)); + } } /// diff --git a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs index 058a328df499..8e659cb9c1c4 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs @@ -25,4 +25,14 @@ protected override void EncodeValue(RlpStream rlpStream) rlpStream.Encode(Value.ForkHash); rlpStream.Encode(Value.NextBlock); } + + protected override void EncodeValue(Span buffer, ref int position) + { + // I am just guessing this one + int contentLength = 5 + Rlp.LengthOf(Value.NextBlock); + position = Rlp.StartSequence(buffer, position, contentLength + 1); + position = Rlp.StartSequence(buffer, position, contentLength); + position = Rlp.Encode(buffer, position, Value.ForkHash); + position += Rlp.Encode((ulong)Value.NextBlock, buffer[position..]).Length; + } } diff --git a/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs b/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs index 5331e152f880..e1389566506c 100644 --- a/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs @@ -19,4 +19,6 @@ private IdEntry() : base("v4") { } protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode("v4"); + + protected override void EncodeValue(Span buffer, ref int position) => position = Rlp.Encode(buffer, position, "v4"u8); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs index 822f7549fbf3..e8c819996107 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs @@ -21,4 +21,11 @@ protected override void EncodeValue(RlpStream rlpStream) Value.MapToIPv6().TryWriteBytes(bytes, out int _); rlpStream.Encode(bytes); } + + protected override void EncodeValue(Span buffer, ref int position) + { + Span bytes = stackalloc byte[16]; + Value.MapToIPv6().TryWriteBytes(bytes, out int _); + position = Rlp.Encode(buffer, position, bytes); + } } diff --git a/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs index 3fdc90f9d81d..a27dd3a72aa6 100644 --- a/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs @@ -21,4 +21,11 @@ protected override void EncodeValue(RlpStream rlpStream) Value.MapToIPv4().TryWriteBytes(bytes, out int _); rlpStream.Encode(bytes); } + + protected override void EncodeValue(Span buffer, ref int position) + { + Span bytes = stackalloc byte[4]; + Value.MapToIPv4().TryWriteBytes(bytes, out int _); + position = Rlp.Encode(buffer, position, bytes); + } } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index a16bfab9e230..350d5199d02a 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -260,9 +260,10 @@ public byte[] ToRlpBytes() } int rlpLength = GetRlpLengthWithSignature(); - RlpStream rlpStream = new(rlpLength); - Encode(rlpStream); - return rlpStream.Data.ToArray()!; + byte[] bytes = GC.AllocateUninitializedArray(rlpLength); + int position = 0; + Encode(bytes, ref position); + return bytes; } /// @@ -289,6 +290,32 @@ public void Encode(RlpStream rlpStream) } } + /// + /// Applies Rlp([signature, seq, k, v, ...]) into a span. + /// + /// The destination span. + /// The current write position. + public void Encode(Span buffer, ref int position) + { + if (OriginalRlp is not null) + { + OriginalRlp.CopyTo(buffer[position..]); + position += OriginalRlp.Length; + return; + } + + RequireSignature(); + + int contentLength = GetContentLengthWithSignature(); + position = Rlp.StartSequence(buffer, position, contentLength); + position = Rlp.Encode(buffer, position, Signature!.Bytes); + position += Rlp.Encode(EnrSequence, buffer[position..]).Length; // a different sequence here (not RLP sequence) + foreach ((_, EnrContentEntry contentEntry) in Entries) + { + contentEntry.Encode(buffer, ref position); + } + } + private string CreateEnrString() { RequireSignature(); diff --git a/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs b/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs index 6bfccdba1acc..10fcea67d52e 100644 --- a/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs @@ -16,4 +16,6 @@ public class SecP256k1Entry(CompressedPublicKey publicKey) : EnrContentEntry CompressedPublicKey.LengthInBytes + 1; protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value.Bytes); + + protected override void EncodeValue(Span buffer, ref int position) => position = Rlp.Encode(buffer, position, Value.Bytes); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs index 85ab40b277f2..bff1e15a2f91 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs @@ -15,4 +15,6 @@ public class Tcp6Entry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); + + protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; } diff --git a/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs index f6fe7a769b83..699cfcd7191c 100644 --- a/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs @@ -15,4 +15,6 @@ public class TcpEntry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); + + protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; } diff --git a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs index 864a7efadc33..d3c95bac01c1 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs @@ -15,4 +15,6 @@ public class Udp6Entry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); + + protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; } diff --git a/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs index 25973be48bf6..83e3c03060d1 100644 --- a/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs @@ -15,4 +15,6 @@ public class UdpEntry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); + + protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; } From 5fde7944fdfd977e6d4d3219542f109384a13fdd Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 29 May 2026 15:54:32 +0300 Subject: [PATCH 125/182] Fix PR validation failures --- .../DiscoveryApp.cs | 4 +- .../Discv4/KademliaNodeSource.cs | 1 - .../Discv5/DiscoveryV5App.cs | 4 +- .../Discv5/Discv5KademliaAdapter.cs | 1 - .../Discv5/Discv5NodeSource.cs | 1 - .../Nethermind.Network.Enr/EnrContentEntry.cs | 1 - .../NodeRecordSigner.cs | 66 +++++++++---------- .../Module/MainProcessingContextTests.cs | 12 ++-- 8 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs index a2ee6839370b..f763aab6bec4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs @@ -3,12 +3,10 @@ using Autofac; using Autofac.Features.AttributeFilters; -using DotNetty.Handlers.Logging; using DotNetty.Transport.Channels; using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; -using Nethermind.Core.ServiceStopper; using Nethermind.Crypto; using Nethermind.Logging; using Nethermind.Network.Config; @@ -118,7 +116,7 @@ public override void InitializeChannel(IChannel channel) _discoveryHandler.OnChannelActivated += OnChannelActivated; channel.Pipeline - .AddLast(new LoggingHandler(LogLevel.INFO)) + .AddLast(new DotNetty.Handlers.Logging.LoggingHandler(LogLevel.INFO)) .AddLast(_discoveryHandler); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index fe091fb6255a..2e7585e1377e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index a324a8f1baa7..da351cc88af1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.CompilerServices; @@ -12,7 +11,6 @@ using Nethermind.Config; using Nethermind.Core; using Nethermind.Core.Crypto; -using Nethermind.Core.ServiceStopper; using Nethermind.Crypto; using Nethermind.Db; using Nethermind.Kademlia; @@ -287,7 +285,7 @@ private static bool IsSpecialUseAddress(IPAddress ipAddress) private static bool IsSpecialUseIPv4(ReadOnlySpan bytes) { - uint v4 = BinaryPrimitives.ReadUInt32BigEndian(bytes); + uint v4 = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(bytes); byte a = (byte)(v4 >> 24); byte b = (byte)(v4 >> 16); byte c = (byte)(v4 >> 8); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index 684e19bdd099..f269064cadd0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -9,7 +9,6 @@ using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs index 4edd894a3d3d..7dd42e2f4315 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading.Channels; using Nethermind.Core.Crypto; diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs index 739052b94067..9d5765e33778 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System; using System.Diagnostics; using System.Text; using Nethermind.Serialization.Rlp; diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index dfa4894e43c5..03dfe31cbd80 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -90,19 +90,19 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) nodeRecord.SetEntry(IdEntry.Instance); break; case 2 when key.SequenceEqual(EnrContentKey.IpU8): - { - ReadOnlySpan ipBytes = ctx.DecodeByteArraySpan(); - IPAddress address = new(ipBytes); - nodeRecord.SetEntry(new IpEntry(address)); - break; - } + { + ReadOnlySpan ipBytes = ctx.DecodeByteArraySpan(); + IPAddress address = new(ipBytes); + nodeRecord.SetEntry(new IpEntry(address)); + break; + } case 3 when key.SequenceEqual(EnrContentKey.Ip6U8): - { - ReadOnlySpan ipBytes = ctx.DecodeByteArraySpan(); - IPAddress address = new(ipBytes); - nodeRecord.SetEntry(new Ip6Entry(address)); - break; - } + { + ReadOnlySpan ipBytes = ctx.DecodeByteArraySpan(); + IPAddress address = new(ipBytes); + nodeRecord.SetEntry(new Ip6Entry(address)); + break; + } case 3 when key.SequenceEqual(EnrContentKey.EthU8): _ = ctx.ReadSequenceLength(); _ = ctx.ReadSequenceLength(); @@ -111,29 +111,29 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) nodeRecord.SetEntry(new EthEntry(forkHash, nextBlock)); break; case 3 when key.SequenceEqual(EnrContentKey.TcpU8): - { - int tcpPort = ctx.DecodePositiveInt(); - nodeRecord.SetEntry(new TcpEntry(tcpPort)); - break; - } + { + int tcpPort = ctx.DecodePositiveInt(); + nodeRecord.SetEntry(new TcpEntry(tcpPort)); + break; + } case 4 when key.SequenceEqual(EnrContentKey.Tcp6U8): - { - int tcpPort = ctx.DecodePositiveInt(); - nodeRecord.SetEntry(new Tcp6Entry(tcpPort)); - break; - } + { + int tcpPort = ctx.DecodePositiveInt(); + nodeRecord.SetEntry(new Tcp6Entry(tcpPort)); + break; + } case 3 when key.SequenceEqual(EnrContentKey.UdpU8): - { - int udpPort = ctx.DecodePositiveInt(); - nodeRecord.SetEntry(new UdpEntry(udpPort)); - break; - } + { + int udpPort = ctx.DecodePositiveInt(); + nodeRecord.SetEntry(new UdpEntry(udpPort)); + break; + } case 4 when key.SequenceEqual(EnrContentKey.Udp6U8): - { - int udpPort = ctx.DecodePositiveInt(); - nodeRecord.SetEntry(new Udp6Entry(udpPort)); - break; - } + { + int udpPort = ctx.DecodePositiveInt(); + nodeRecord.SetEntry(new Udp6Entry(udpPort)); + break; + } case 9 when key.SequenceEqual(EnrContentKey.SecP256k1U8): ReadOnlySpan keyBytes = ctx.DecodeByteArraySpan(); CompressedPublicKey reportedKey = new(keyBytes); @@ -145,7 +145,7 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) ctx.SkipItem(); nodeRecord.Snap = true; break; - } + } } ctx.Check(checkPosition); diff --git a/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs b/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs index 44e153dee55c..13e3b0400eac 100644 --- a/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs @@ -11,6 +11,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.Container; using Nethermind.Core.Test.Modules; +using Nethermind.Crypto; using Nethermind.Evm; using Nethermind.Evm.State; using Nethermind.Specs.Forks; @@ -24,11 +25,14 @@ public class MainProcessingContextTests [CancelAfter(10000)] public async Task Test_TransactionProcessed_EventIsFired(CancellationToken cancellationToken) { + using PrivateKey privateKeyA = new("010102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"); + using PrivateKey privateKeyB = new("020102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"); + await using IContainer ctx = new ContainerBuilder() .AddModule(new TestNethermindModule(Cancun.Instance)) .WithGenesisPostProcessor((_, state) => { - state.AddToBalanceAndCreateIfNotExists(TestItem.AddressA, 10.Ether, Osaka.Instance); + state.AddToBalanceAndCreateIfNotExists(privateKeyA.Address, 10.Ether, Osaka.Instance); }) .Build(); @@ -40,13 +44,13 @@ public async Task Test_TransactionProcessed_EventIsFired(CancellationToken cance await ctx.Resolve().AddBlockAndWaitForHead(false, cancellationToken, Build.A.Transaction .WithGasLimit(100_000) - .WithSenderAddress(TestItem.AddressA) + .WithSenderAddress(privateKeyA.Address) .WithCode(Prepare.EvmCode .ForInitOf(Prepare.EvmCode - .PushData(TestItem.PrivateKeyB.Address) + .PushData(privateKeyB.Address) .Done) .Done) - .Signed(TestItem.PrivateKeyA) + .Signed(privateKeyA) .TestObject); Assert.That(totalTransactionProcessed, Is.EqualTo(1)); From 8ad3ff7d3865b3599b164013d92d9f7c17d1e9d5 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 1 Jun 2026 15:09:46 +0300 Subject: [PATCH 126/182] Refactor more --- .../Caching/LruCacheTests.cs | 12 + .../Nethermind.Core/Caching/LruCache.cs | 27 +- .../Modules/DiscoveryModule.cs | 5 +- .../Steps/InitializeNetwork.cs | 1 + .../Nethermind.Kademlia/KBucketTree.cs | 59 ++-- .../Nethermind.Kademlia/Kademlia.cs | 19 +- .../Nethermind.Kademlia/KademliaFactory.cs | 14 +- .../LookupKNearestNeighbour.cs | 19 +- .../Nethermind.Kademlia.csproj | 5 +- .../Nethermind.Kademlia/NodeHealthTracker.cs | 11 +- .../MicrosoftLoggerExtensions.cs | 13 +- .../Nethermind.Logging.Microsoft.csproj | 4 + .../NethermindLoggerFactory.cs | 37 ++- .../DiscoveryMessageSerializerTests.cs | 4 +- .../DiscoveryPersistenceManagerTests.cs | 2 +- .../{ => Discv4}/EIP8DiscoveryTests.cs | 4 +- .../NeighbourMsgHandlerTests.cs | 6 +- .../Discv4/KademliaDiscv4AdapterTests.cs | 2 +- .../NettyDiscoveryHandlerTests.cs | 4 +- .../NodeSourceToDiscV4FeederTests.cs | 3 +- .../Discv5/Discv5KademliaAdapterTests.cs | 66 ----- .../Handlers/NodesResponseHandlerTests.cs | 77 ++++++ .../IteratorNodeLookupTests.cs | 3 +- .../NettyDiscoveryV5HandlerTests.cs | 14 + .../CompositeDiscoveryApp.cs | 1 + .../Discv4/DiscV4KademliaModule.cs | 13 +- .../{ => Discv4}/DiscoveryApp.cs | 6 +- .../DiscoveryPersistenceManager.cs | 5 +- .../{ => Handlers}/EnrResponseHandler.cs | 4 +- .../Discv4/{ => Handlers}/IMessageHandler.cs | 4 +- .../Discv4/{ => Handlers}/ITaskCompleter.cs | 2 +- .../{ => Handlers}/NeighbourMsgHandler.cs | 4 +- .../Discv4/{ => Handlers}/PongMsgHandler.cs | 4 +- .../{ => Discv4}/IDiscoveryMsgListener.cs | 4 +- .../Discv4/IKademliaDiscv4Adapter.cs | 2 +- .../{ => Discv4}/IMsgSender.cs | 4 +- .../Discv4/KademliaDiscv4Adapter.cs | 3 +- .../Discv4/KademliaNodeSource.cs | 45 +-- .../{ => Discv4}/Messages/DiscoveryMsg.cs | 2 +- .../{ => Discv4}/Messages/EnrRequestMsg.cs | 2 +- .../{ => Discv4}/Messages/EnrResponseMsg.cs | 2 +- .../{ => Discv4}/Messages/FindNodeMsg.cs | 2 +- .../{ => Discv4}/Messages/INodeIdResolver.cs | 2 +- .../{ => Discv4}/Messages/NeighborsMsg.cs | 2 +- .../{ => Discv4}/Messages/NodeIdResolver.cs | 2 +- .../{ => Discv4}/Messages/PingMsg.cs | 2 +- .../{ => Discv4}/Messages/PongMsg.cs | 2 +- .../{ => Discv4}/NettyDiscoveryHandler.cs | 4 +- .../Discv4/NodeSession.cs | 2 +- .../{ => Discv4}/NodeSourceToDiscV4Feeder.cs | 2 +- .../Serializers/DiscoveryMsgSerializerBase.cs | 4 +- .../Serializers/EnrRequestMsgSerializer.cs | 4 +- .../Serializers/EnrResponseMsgSerializer.cs | 4 +- .../Serializers/FindNodeMsgSerializer.cs | 4 +- .../Serializers/NeighborsMsgSerializer.cs | 4 +- .../Serializers/PingMsgSerializer.cs | 4 +- .../Serializers/PongMsgSerializer.cs | 4 +- .../Discv5/DiscV5KademliaModule.cs | 13 +- .../Discv5/DiscoveryV5App.cs | 115 +------- .../Discv5/Discv5AdapterState.cs | 37 +++ .../Discv5/Discv5KademliaAdapter.cs | 259 ++---------------- .../Discv5/Discv5NodeSource.cs | 44 +-- .../Discv5/Handlers/IResponseHandler.cs | 32 +++ .../Discv5/Handlers/NodesResponseHandler.cs | 85 ++++++ .../Discv5/Handlers/PongResponseHandler.cs | 21 ++ .../Discv5/NettyDiscoveryV5Handler.cs | 16 +- .../DiscoveryKademliaConfigFactory.cs | 24 ++ .../IKademliaNodeSource.cs | 2 +- .../IteratorNodeLookup.cs | 2 +- .../Kademlia/KademliaModule.cs | 1 - .../PublicKeyKeyOperator.cs | 2 +- .../Kademlia/RecentNodeFilter.cs | 45 +++ .../KademliaDiscoveryApp.cs | 2 +- .../Builders/SerializationBuilder.cs | 4 +- .../NodeFilterTests.cs | 31 ++- .../{ => Discv4}/Messages/MsgType.cs | 2 +- .../IP/IPAddressExtensions.cs | 13 +- .../Nethermind.Network/IPAddressClassifier.cs | 197 +++++++++++++ src/Nethermind/Nethermind.Network/Metrics.cs | 2 +- .../Nethermind.Network/NodeFilter.cs | 75 +---- .../Module/NetworkModuleTest.cs | 1 + .../Nethermind.Runner.csproj | 1 + src/Nethermind/Nethermind.Runner/Program.cs | 2 +- .../Nethermind.Runner/packages.lock.json | 18 +- .../Nethermind.Shutter.csproj | 1 + .../Nethermind.Shutter/ShutterP2P.cs | 2 +- .../Discovery/XdcDiscoveryTests.cs | 3 +- .../Discovery/XdcDiscoveryApp.cs | 1 + .../Discovery/XdcNettyDiscoveryHandler.cs | 3 +- .../Discovery/XdcPingMsgSerializer.cs | 4 +- src/Nethermind/Nethermind.Xdc/XdcModule.cs | 3 +- 91 files changed, 861 insertions(+), 768 deletions(-) rename src/Nethermind/{Nethermind.Network.Discovery/Discv5 => Nethermind.Logging.Microsoft}/NethermindLoggerFactory.cs (67%) rename src/Nethermind/Nethermind.Network.Discovery.Test/{ => Discv4}/DiscoveryMessageSerializerTests.cs (99%) rename src/Nethermind/Nethermind.Network.Discovery.Test/{ => Discv4}/DiscoveryPersistenceManagerTests.cs (99%) rename src/Nethermind/Nethermind.Network.Discovery.Test/{ => Discv4}/EIP8DiscoveryTests.cs (98%) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/{ => Handlers}/NeighbourMsgHandlerTests.cs (94%) rename src/Nethermind/Nethermind.Network.Discovery.Test/{ => Discv4}/NettyDiscoveryHandlerTests.cs (99%) rename src/Nethermind/Nethermind.Network.Discovery.Test/{ => Discv4}/NodeSourceToDiscV4FeederTests.cs (94%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs rename src/Nethermind/Nethermind.Network.Discovery.Test/{Discv4 => Kademlia}/IteratorNodeLookupTests.cs (98%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/DiscoveryApp.cs (98%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/DiscoveryPersistenceManager.cs (98%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Handlers}/EnrResponseHandler.cs (84%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Handlers}/IMessageHandler.cs (62%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Handlers}/ITaskCompleter.cs (79%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Handlers}/NeighbourMsgHandler.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Handlers}/PongMsgHandler.cs (82%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/IDiscoveryMsgListener.cs (65%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/IMsgSender.cs (64%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/DiscoveryMsg.cs (95%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/EnrRequestMsg.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/EnrResponseMsg.cs (94%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/FindNodeMsg.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/INodeIdResolver.cs (82%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/NeighborsMsg.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/NodeIdResolver.cs (89%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/PingMsg.cs (96%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Messages/PongMsg.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/NettyDiscoveryHandler.cs (99%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/NodeSourceToDiscV4Feeder.cs (95%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/DiscoveryMsgSerializerBase.cs (98%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/EnrRequestMsgSerializer.cs (94%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/EnrResponseMsgSerializer.cs (95%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/FindNodeMsgSerializer.cs (94%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/NeighborsMsgSerializer.cs (97%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/PingMsgSerializer.cs (97%) rename src/Nethermind/Nethermind.Network.Discovery/{ => Discv4}/Serializers/PongMsgSerializer.cs (96%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs rename src/Nethermind/Nethermind.Network.Discovery/{Discv4 => Kademlia}/IKademliaNodeSource.cs (92%) rename src/Nethermind/Nethermind.Network.Discovery/{Discv4 => Kademlia}/IteratorNodeLookup.cs (99%) rename src/Nethermind/Nethermind.Network.Discovery/{Discv4 => Kademlia}/PublicKeyKeyOperator.cs (95%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs rename src/Nethermind/Nethermind.Network/Discovery/{ => Discv4}/Messages/MsgType.cs (80%) create mode 100644 src/Nethermind/Nethermind.Network/IPAddressClassifier.cs diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index 2c5850808157..d0d492d0c22c 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -235,6 +235,18 @@ public void Can_delete() Assert.That(cache.Delete(_addresses[0]), Is.False); } + [Test] + public void Can_remove_and_return_value() + { + LruCache cache = new(Capacity, "test"); + cache.Set(_addresses[0], _accounts[0]); + + Assert.That(cache.TryRemove(_addresses[0], out Account? removed), Is.True); + Assert.That(removed, Is.EqualTo(_accounts[0])); + Assert.That(cache.TryRemove(_addresses[0], out removed), Is.False); + Assert.That(removed, Is.Null); + } + [Test] public void Clear_should_free_all_capacity() { diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 56f195b7551c..7747e245da7d 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -149,18 +149,41 @@ public bool Delete(TKey key) return DeleteNoLock(key); } + /// + /// Deletes a cached value and returns it when the key is present. + /// + public bool TryRemove(TKey key, [MaybeNullWhen(false)] out TValue value) + { + using McsLock.Disposable lockRelease = _lock.Acquire(); + + if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) + { + value = node.Value.Value; + RemoveNoLock(key, node); + return true; + } + + value = default; + return false; + } + private bool DeleteNoLock(TKey key) { if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) { - LinkedListNode.Remove(ref _leastRecentlyUsed, node); - _cacheMap.Remove(key); + RemoveNoLock(key, node); return true; } return false; } + private void RemoveNoLock(TKey key, LinkedListNode node) + { + LinkedListNode.Remove(ref _leastRecentlyUsed, node); + _cacheMap.Remove(key); + } + public bool Contains(TKey key) { using McsLock.Disposable lockRelease = _lock.Acquire(); diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index b4c762325627..44337eb342b1 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -11,9 +11,10 @@ using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Discovery; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv5; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.Serializers; +using Nethermind.Network.Discovery.Discv4.Messages; +using Nethermind.Network.Discovery.Discv4.Serializers; using Nethermind.Network.Dns; using Nethermind.Network.Enr; using Nethermind.Network.StaticNodes; diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index fb021929d4b9..ca88941fe6bc 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -16,6 +16,7 @@ using Nethermind.Network.Config; using Nethermind.Network.Contract.P2P; using Nethermind.Network.Discovery; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.P2P.ProtocolHandlers; using Nethermind.Stats; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index 8785c8312519..3c42e35a2cb9 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Text; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -33,17 +32,17 @@ public KBucketTree( KademliaConfig config, INodeHashProvider nodeHashProvider, IKademliaDistance distance, - ILoggerFactory? loggerFactory = null) + ILogManager? logManager = null) { _k = config.KSize; _b = config.Beta; _distance = distance; _currentNodeHash = nodeHashProvider.GetHash(config.CurrentNodeId); _root = new TreeNode(config.KSize, distance.Zero); - _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); - if (_logger.IsEnabled(LogLevel.Debug)) + _logger = (logManager ?? NullLogManager.Instance).GetClassLogger>(); + if (_logger.IsDebug) { - _logger.LogDebug("Initialized KBucketTree with k={K}, currentNodeId={CurrentNodeId}", _k, _currentNodeHash); + _logger.Debug($"Initialized KBucketTree with k={_k}, currentNodeId={_currentNodeHash}"); } } @@ -53,9 +52,9 @@ public BucketAddResult TryAddOrRefresh(in TKadKey nodeHash, TNode node, out TNod bool fireAdded; lock (_lock) { - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsDebug) { - _logger.LogDebug("Adding node {Node} with XOR distance {Distance}", node, _distance.CalculateLogDistance(_currentNodeHash, nodeHash)); + _logger.Debug($"Adding node {node} with XOR distance {_distance.CalculateLogDistance(_currentNodeHash, nodeHash)}"); } TreeNode current = _root; @@ -66,28 +65,28 @@ public BucketAddResult TryAddOrRefresh(in TKadKey nodeHash, TNode node, out TNod { if (current.IsLeaf) { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Reached leaf node at depth {Depth}", depth); + if (_logger.IsTrace) _logger.Trace($"Reached leaf node at depth {depth}"); resp = current.Bucket.TryAddOrRefresh(nodeHash, node, out toRefresh); fireAdded = resp == BucketAddResult.Added; if (resp is BucketAddResult.Added or BucketAddResult.Refreshed) { - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Successfully added/refreshed node {Node} in bucket at depth {Depth}", node, depth); + if (_logger.IsDebug) _logger.Debug($"Successfully added/refreshed node {node} in bucket at depth {depth}"); break; } if (resp == BucketAddResult.Full && ShouldSplit(depth, logDistance)) { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Splitting bucket at depth {Depth}", depth); + if (_logger.IsTrace) _logger.Trace($"Splitting bucket at depth {depth}"); SplitBucket(depth, current); continue; } - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Failed to add node {Hash} {Node}. Bucket at depth {Depth} is full. {K} {Count}", nodeHash, node, depth, _k, current.Bucket.Count); + if (_logger.IsDebug) _logger.Debug($"Failed to add node {nodeHash} {node}. Bucket at depth {depth} is full. {_k} {current.Bucket.Count}"); break; } bool goRight = _distance.GetBit(nodeHash, depth); - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Traversing {Direction} at depth {Depth}", goRight ? "right" : "left", depth); + if (_logger.IsTrace) _logger.Trace($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); current = goRight ? current.Right! : current.Left!; depth++; @@ -114,12 +113,12 @@ private KBucket GetBucketForHash(TKadKey nodeHash) { if (current.IsLeaf) { - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Reached leaf node at depth {Depth}", depth); + if (_logger.IsDebug) _logger.Debug($"Reached leaf node at depth {depth}"); return current.Bucket; } bool goRight = _distance.GetBit(nodeHash, depth); - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Traversing {Direction} at depth {Depth}", goRight ? "right" : "left", depth); + if (_logger.IsDebug) _logger.Debug($"Traversing {(goRight ? "right" : "left")} at depth {depth}"); current = goRight ? current.Right! : current.Left!; depth++; @@ -129,7 +128,7 @@ private KBucket GetBucketForHash(TKadKey nodeHash) private bool ShouldSplit(int depth, int targetLogDistance) { bool shouldSplit = depth < _distance.MaxDistance && targetLogDistance + _b >= depth; - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("ShouldSplit at depth {Depth}: {ShouldSplit}", depth, shouldSplit); + if (_logger.IsDebug) _logger.Debug($"ShouldSplit at depth {depth}: {shouldSplit}"); return shouldSplit; } @@ -138,7 +137,7 @@ private void SplitBucket(int depth, TreeNode node) node.Left = new TreeNode(_k, node.Prefix); node.Right = new TreeNode(_k, _distance.SetBit(node.Prefix, depth)); - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Created children at depth {Depth}", depth + 1); + if (_logger.IsDebug) _logger.Debug($"Created children at depth {depth + 1}"); // Iterate from oldest to newest so the new buckets preserve original LRU order. (TKadKey, TNode)[] items = node.Bucket.GetAllWithHash(); @@ -147,11 +146,11 @@ private void SplitBucket(int depth, TreeNode node) (TKadKey itemHash, TNode value) = items[i]; TreeNode? targetNode = _distance.GetBit(itemHash, depth) ? node.Right : node.Left; targetNode.Bucket.TryAddOrRefresh(itemHash, value, out _); - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Moved item ({Hash}, {Value}) to {Direction} child", itemHash, value, _distance.GetBit(itemHash, depth) ? "right" : "left"); + if (_logger.IsDebug) _logger.Debug($"Moved item ({itemHash}, {value}) to {(_distance.GetBit(itemHash, depth) ? "right" : "left")} child"); } node.Bucket.Clear(); - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Finished splitting bucket. Left count: {LeftCount}, Right count: {RightCount}", node.Left.Bucket.Count, node.Right.Bucket.Count); + if (_logger.IsDebug) _logger.Debug($"Finished splitting bucket. Left count: {node.Left.Bucket.Count}, Right count: {node.Right.Bucket.Count}"); } public bool Remove(in TKadKey nodeHash) @@ -160,7 +159,7 @@ public bool Remove(in TKadKey nodeHash) TNode? removedNode; lock (_lock) { - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Attempting to remove node {NodeHash}", nodeHash); + if (_logger.IsDebug) _logger.Debug($"Attempting to remove node {nodeHash}"); KBucket bucket = GetBucketForHash(nodeHash); removedNode = bucket.GetByHash(nodeHash); @@ -175,10 +174,10 @@ public TNode[] GetAllAtDistance(int distance) { lock (_lock) { - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Getting all nodes at distance {Distance}", distance); + if (_logger.IsDebug) _logger.Debug($"Getting all nodes at distance {distance}"); List result = []; GetAllAtDistanceRecursive(_root, 0, distance, result); - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Found {Count} nodes at distance {Distance}", result.Count, distance); + if (_logger.IsDebug) _logger.Debug($"Found {result.Count} nodes at distance {distance}"); return [.. result]; } } @@ -380,7 +379,7 @@ private void LogTreeStructureRecursive(TreeNode node, string indent, bool last, private void LogTreeStatistics() { - if (!_logger.IsEnabled(LogLevel.Debug)) return; + if (!_logger.IsDebug) return; int totalNodes = 0; int totalBuckets = 0; @@ -406,27 +405,21 @@ void TraverseTree(TreeNode node, int depth) TraverseTree(_root, 0); - _logger.LogDebug( - "Tree Statistics: Total Nodes: {TotalNodes}, Total Buckets: {TotalBuckets}, Max Depth: {MaxDepth}, Total Items: {TotalItems}, Average Items per Bucket: {AverageItemsPerBucket:F2}", - totalNodes, - totalBuckets, - maxDepth, - totalItems, - (double)totalItems / totalBuckets); + _logger.Debug($"Tree Statistics: Total Nodes: {totalNodes}, Total Buckets: {totalBuckets}, Max Depth: {maxDepth}, Total Items: {totalItems}, Average Items per Bucket: {(double)totalItems / totalBuckets:F2}"); } private void LogTreeStructure() { - if (!_logger.IsEnabled(LogLevel.Trace)) return; + if (!_logger.IsTrace) return; StringBuilder sb = new(); LogTreeStructureRecursive(_root, "", true, 0, sb); - _logger.LogTrace("Current Tree Structure:{NewLine}{Tree}", Environment.NewLine, sb); + _logger.Trace($"Current Tree Structure:{Environment.NewLine}{sb}"); } public void LogDebugInfo() { - if (!_logger.IsEnabled(LogLevel.Debug)) return; + if (!_logger.IsDebug) return; LogTreeStatistics(); LogTreeStructure(); diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index c4934d59c6f0..5022fe4b1f4e 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -41,7 +40,7 @@ public Kademlia( ILookupAlgo lookupAlgo, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILoggerFactory? loggerFactory = null, + ILogManager? logManager = null, TimeProvider? timeProvider = null) { _keyOperator = keyOperator; @@ -49,7 +48,7 @@ public Kademlia( _routingTable = routingTable; _lookupAlgo = lookupAlgo; _nodeHealthTracker = nodeHealthTracker; - _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); + _logger = (logManager ?? NullLogManager.Instance).GetClassLogger>(); _currentNodeId = config.CurrentNodeId; _currentNodeIdAsHash = _keyOperator.GetNodeHash(_currentNodeId); @@ -105,7 +104,7 @@ public async Task Run(CancellationToken token) } catch (Exception e) { - _logger.LogError(e, "Bootstrap iteration failed."); + if (_logger.IsError) _logger.Error("Bootstrap iteration failed.", e); } await Task.Delay(_refreshInterval, token); @@ -133,13 +132,13 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } catch (Exception e) { - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug(e, "Bootnode ping failed for {Node}.", node); + if (_logger.IsDebug) _logger.Debug($"Bootnode ping failed for {node}: {e}"); } }); - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsDebug) { - _logger.LogDebug("Online bootnodes: {OnlineBootNodes}", onlineBootNodes); + _logger.Debug($"Online bootnodes: {onlineBootNodes}"); } TKey currentNodeIdAsKey = _keyOperator.GetKey(_currentNodeId); @@ -157,9 +156,9 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => await LookupNodesClosest(keyToLookup, token); } - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsDebug) { - _logger.LogDebug("Bootstrap completed. Took {Elapsed}.", sw.Elapsed); + _logger.Debug($"Bootstrap completed. Took {sw.Elapsed}."); _routingTable.LogDebugInfo(); } } diff --git a/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs b/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs index 6c887d2fe854..df9baea8a7dd 100644 --- a/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs +++ b/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Microsoft.Extensions.Logging; +using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -17,14 +17,14 @@ public static class KademliaFactory /// Compares and manipulates values in the Kademlia key space. /// Sends protocol-specific ping and find-neighbour requests. /// Kademlia table and maintenance settings. - /// Optional logger factory. When omitted, logging is disabled. + /// Optional log manager. When omitted, logging is disabled. /// Optional time provider used for bucket refresh scheduling. public static KademliaComponents Create( IKeyOperator keyOperator, IKademliaDistance distance, IKademliaMessageSender sender, KademliaConfig config, - ILoggerFactory? loggerFactory = null, + ILogManager? logManager = null, TimeProvider? timeProvider = null) where TNode : notnull where TKadKey : notnull @@ -35,9 +35,9 @@ public static KademliaComponents Create nodeHashProvider = new(keyOperator); - KBucketTree routingTable = new(config, nodeHashProvider, distance, loggerFactory); - NodeHealthTracker nodeHealthTracker = new(config, routingTable, nodeHashProvider, sender, loggerFactory); - LookupKNearestNeighbour lookup = new(routingTable, nodeHashProvider, distance, nodeHealthTracker, config, loggerFactory); + KBucketTree routingTable = new(config, nodeHashProvider, distance, logManager); + NodeHealthTracker nodeHealthTracker = new(config, routingTable, nodeHashProvider, sender, logManager); + LookupKNearestNeighbour lookup = new(routingTable, nodeHashProvider, distance, nodeHealthTracker, config, logManager); Kademlia kademlia = new( keyOperator, sender, @@ -45,7 +45,7 @@ public static KademliaComponents Create( diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 1f0e94ba0abe..23bdb8574f7f 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using Nethermind.Logging; using NonBlocking; namespace Nethermind.Kademlia; @@ -22,12 +21,12 @@ public class LookupKNearestNeighbour( IKademliaDistance distance, INodeHealthTracker nodeHealthTracker, KademliaConfig config, - ILoggerFactory? loggerFactory = null) : ILookupAlgo + ILogManager? logManager = null) : ILookupAlgo where TNode : notnull where TKadKey : notnull { private readonly TimeSpan _findNeighbourHardTimeout = config.LookupFindNeighbourHardTimeout; - private readonly ILogger _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); + private readonly ILogger _logger = (logManager ?? NullLogManager.Instance).GetClassLogger>(); public async Task Lookup( TKadKey targetHash, @@ -36,9 +35,9 @@ public async Task Lookup( CancellationToken token ) { - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsDebug) { - _logger.LogDebug("Initiate lookup for hash {TargetHash}", targetHash); + _logger.Debug($"Initiate lookup for hash {targetHash}"); } using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -88,7 +87,7 @@ CancellationToken token } // No node to query and running query. - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Stopping lookup. No node to query."); + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); break; } @@ -96,7 +95,7 @@ CancellationToken token { if (ShouldStopDueToNoBetterResult(out int round)) { - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("Stopping lookup. No better result."); + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); break; } @@ -162,7 +161,7 @@ CancellationToken token catch (Exception e) { nodeHealthTracker.OnRequestFailed(node); - _logger.LogWarning(e, "Find neighbour op failed."); + if (_logger.IsWarn) _logger.Warn($"Find neighbour op failed: {e}"); return (node, null); } } @@ -248,7 +247,7 @@ bool ShouldStopDueToNoBetterResult(out int round) // Why not just _alpha? // Because there could be currently running work that may increase closestNodeRound. // So including this worker, assume no more - if (_logger.IsEnabled(LogLevel.Trace)) _logger.LogTrace("No more closer node. Round: {Round}, closestNodeRound {ClosestNodeRound}", round, closestNodeRound); + if (_logger.IsTrace) _logger.Trace($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); return true; } diff --git a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj index f4087527233a..e5ce246513b6 100644 --- a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj +++ b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj @@ -6,8 +6,11 @@ - + + + + diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index a54c8b40c5e8..d830249fae18 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -1,8 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using Nethermind.Logging; using NonBlocking; namespace Nethermind.Kademlia; @@ -15,12 +14,12 @@ public class NodeHealthTracker( IRoutingTable routingTable, INodeHashProvider nodeHashProvider, IKademliaMessageSender kademliaMessageSender, - ILoggerFactory? loggerFactory = null + ILogManager? logManager = null ) : INodeHealthTracker, IDisposable where TNode : notnull where TKadKey : notnull { - private readonly ILogger _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger>(); + private readonly ILogger _logger = (logManager ?? NullLogManager.Instance).GetClassLogger>(); private readonly ConcurrentDictionary _isRefreshing = new(); private readonly ConcurrentDictionary _refreshTasks = new(); @@ -78,7 +77,7 @@ private async Task RefreshAsync(TNode toRefresh, TKadKey nodeHash, CancellationT catch (Exception e) { OnRequestFailed(toRefresh); - if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug(e, "Error while refreshing node {Node}.", toRefresh); + if (_logger.IsDebug) _logger.Debug($"Error while refreshing node {toRefresh}: {e}"); } if (_isRefreshing.TryRemove(nodeHash, out _)) @@ -184,7 +183,7 @@ public void Dispose() completed = true; if (!HasOnlyCancellationExceptions(e)) { - _logger.LogDebug(e, "Error while disposing node health tracker."); + if (_logger.IsDebug) _logger.Debug($"Error while disposing node health tracker: {e}"); } } diff --git a/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs b/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs index e8e2e50b6486..634a3cc57e81 100644 --- a/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs +++ b/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs @@ -1,20 +1,21 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Microsoft.Extensions.Logging; +using MsILogger = global::Microsoft.Extensions.Logging.ILogger; +using MsLogLevel = global::Microsoft.Extensions.Logging.LogLevel; namespace Nethermind.Logging.Microsoft { public static class MicrosoftLoggerExtensions { - public static bool IsError(this ILogger logger) => logger.IsEnabled(LogLevel.Error); + public static bool IsError(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Error); - public static bool IsWarn(this ILogger logger) => logger.IsEnabled(LogLevel.Warning); + public static bool IsWarn(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Warning); - public static bool IsInfo(this ILogger logger) => logger.IsEnabled(LogLevel.Information); + public static bool IsInfo(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Information); - public static bool IsDebug(this ILogger logger) => logger.IsEnabled(LogLevel.Debug); + public static bool IsDebug(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Debug); - public static bool IsTrace(this ILogger logger) => logger.IsEnabled(LogLevel.Trace); + public static bool IsTrace(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Trace); } } diff --git a/src/Nethermind/Nethermind.Logging.Microsoft/Nethermind.Logging.Microsoft.csproj b/src/Nethermind/Nethermind.Logging.Microsoft/Nethermind.Logging.Microsoft.csproj index 05492bb49b61..72bbdf828bc0 100644 --- a/src/Nethermind/Nethermind.Logging.Microsoft/Nethermind.Logging.Microsoft.csproj +++ b/src/Nethermind/Nethermind.Logging.Microsoft/Nethermind.Logging.Microsoft.csproj @@ -8,4 +8,8 @@ + + + + diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NethermindLoggerFactory.cs b/src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs similarity index 67% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/NethermindLoggerFactory.cs rename to src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs index 33d86362ce18..6cc2ac1f4f4f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NethermindLoggerFactory.cs +++ b/src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs @@ -1,21 +1,24 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Microsoft.Extensions.Logging; -using Nethermind.Logging; -using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; +using System; +using MsEventId = global::Microsoft.Extensions.Logging.EventId; +using MsILogger = global::Microsoft.Extensions.Logging.ILogger; +using MsILoggerFactory = global::Microsoft.Extensions.Logging.ILoggerFactory; +using MsILoggerProvider = global::Microsoft.Extensions.Logging.ILoggerProvider; +using MsLogLevel = global::Microsoft.Extensions.Logging.LogLevel; -namespace Nethermind.Network.Discovery.Discv5; +namespace Nethermind.Logging.Microsoft; -public sealed class NethermindLoggerFactory(ILogManager logManager, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : ILoggerFactory +public sealed class NethermindLoggerFactory(ILogManager logManager, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : MsILoggerFactory { - public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) => new NethermindLogger(logManager.GetLogger(categoryName), lowerLogLevel, maxLogLevel); + public MsILogger CreateLogger(string categoryName) => new NethermindLogger(logManager.GetLogger(categoryName), lowerLogLevel, maxLogLevel); public void Dispose() { } - public void AddProvider(ILoggerProvider provider) { } + public void AddProvider(MsILoggerProvider provider) { } - class NethermindLogger(Logging.ILogger logger, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : Microsoft.Extensions.Logging.ILogger + private sealed class NethermindLogger(ILogger logger, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : MsILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; @@ -37,8 +40,8 @@ public bool IsEnabled(MsLogLevel logLevel) }; } - public void Log(MsLogLevel logLevel, EventId eventId, - TState state, Exception? exception, Func formatter) + public void Log(MsLogLevel logLevel, MsEventId eventId, + TState state, Exception? exception, Func formatter) { if (lowerLogLevel) { @@ -50,30 +53,40 @@ public void Log(MsLogLevel logLevel, EventId eventId, case MsLogLevel.Critical: case MsLogLevel.Error: if (logger.IsError) + { logger.Error(formatter(state, exception)); + } break; case MsLogLevel.Warning: if (logger.IsWarn) + { logger.Warn(formatter(state, exception)); + } break; case MsLogLevel.Information: if (logger.IsInfo) + { logger.Info(formatter(state, exception)); + } break; case MsLogLevel.Debug: if (logger.IsDebug) + { logger.Debug(formatter(state, exception)); + } break; case MsLogLevel.Trace: if (logger.IsTrace) + { logger.Trace(formatter(state, exception)); + } break; } } private static MsLogLevel LowerLogLevel(MsLogLevel logLevel, MsLogLevel? maxLogLevel) { - // DotNetty outputs Trace level data at Info + // DotNetty outputs Trace level data at Info. MsLogLevel loweredLogLevel = logLevel switch { MsLogLevel.Critical => MsLogLevel.Error, diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs similarity index 99% rename from src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs index 1e521e9fc4ab..1a071449fc4b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs @@ -10,7 +10,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Enr; using Nethermind.Network.Test; using Nethermind.Network.Test.Builders; @@ -18,7 +18,7 @@ using Nethermind.Serialization.Rlp; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test; +namespace Nethermind.Network.Discovery.Test.Discv4; [Parallelizable(ParallelScope.Self)] public class DiscoveryMessageSerializerTests diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs similarity index 99% rename from src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs index 6a36e565d82e..6b7648487531 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs @@ -17,7 +17,7 @@ using NSubstitute; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test +namespace Nethermind.Network.Discovery.Test.Discv4 { [Parallelizable(ParallelScope.Self)] public class DiscoveryPersistenceManagerTests diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/EIP8DiscoveryTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/EIP8DiscoveryTests.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery.Test/EIP8DiscoveryTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/EIP8DiscoveryTests.cs index c3f93ca82ced..31a3be33bc11 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/EIP8DiscoveryTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/EIP8DiscoveryTests.cs @@ -4,11 +4,11 @@ using Nethermind.Core.Extensions; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Test.Builders; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test +namespace Nethermind.Network.Discovery.Test.Discv4 { [Parallelizable(ParallelScope.Self)] [TestFixture] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Handlers/NeighbourMsgHandlerTests.cs similarity index 94% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Handlers/NeighbourMsgHandlerTests.cs index ffc3ad57160c..45dff06b9024 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NeighbourMsgHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Handlers/NeighbourMsgHandlerTests.cs @@ -6,12 +6,12 @@ using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; -using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Handlers; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test.Discv4 +namespace Nethermind.Network.Discovery.Test.Discv4.Handlers { [Parallelizable(ParallelScope.Self)] [TestFixture] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs index 99c0458f9af8..6912ecf2de2b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs @@ -16,7 +16,7 @@ using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; using Nethermind.Stats; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs similarity index 99% rename from src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs index 14c9216dcaa7..2affb0dbfb77 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs @@ -20,14 +20,14 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Test.Builders; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test +namespace Nethermind.Network.Discovery.Test.Discv4 { [Parallelizable(ParallelScope.None)] // Some test check for global metric [TestFixture] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeSourceToDiscV4FeederTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs similarity index 94% rename from src/Nethermind/Nethermind.Network.Discovery.Test/NodeSourceToDiscV4FeederTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs index 910b78bfaed7..74caf75896c5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NodeSourceToDiscV4FeederTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs @@ -5,12 +5,13 @@ using System.Threading.Tasks; using Nethermind.Config; using Nethermind.Core.Test.Builders; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Test; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test; +namespace Nethermind.Network.Discovery.Test.Discv4; public class NodeSourceToDiscV4FeederTests { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs index 767e7c133ceb..587ec04ce14a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs @@ -9,7 +9,6 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5; -using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; @@ -83,42 +82,6 @@ public void TryAcceptChallenge_ShouldLimitBurstPerIp() Assert.That(adapter.TryAcceptChallenge(endpoint), Is.False); } - [Test] - public void NodesResponseHandler_ShouldRejectNonRoutableRecordFromPublicReceiver() - { - Node receiver = new(TestItem.PublicKeyA, "8.8.8.8", 30303); - NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); - Discv5KademliaAdapter.NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); - - handler.Handle(new Discv5Nodes([1], 1, [loopbackRecord])); - - Assert.That(handler.GetNodes(), Is.Empty); - } - - [Test] - public void NodesResponseHandler_ShouldAcceptNonRoutableRecordFromNonRoutableReceiver() - { - Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); - NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); - Discv5KademliaAdapter.NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); - - handler.Handle(new Discv5Nodes([1], 1, [loopbackRecord])); - - Assert.That(handler.GetNodes(), Has.Length.EqualTo(1)); - } - - [Test] - public void NodesResponseHandler_ShouldRejectSpecialUseRecordFromNonRoutableReceiver() - { - Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); - NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); - Discv5KademliaAdapter.NodesResponseHandler handler = CreateNodesResponseHandler(receiver, documentationRecord); - - handler.Handle(new Discv5Nodes([1], 1, [documentationRecord])); - - Assert.That(handler.GetNodes(), Is.Empty); - } - [Test] public void IsAcceptableNodeRecord_ShouldRejectSpecialUseRecord() { @@ -158,29 +121,6 @@ public void IsAcceptableNodeRecord_ShouldAllowNonRoutableWhenRequested() Is.True); } - [Test] - public void BoundedMap_ShouldRemoveInsertionOrderEntriesOnRemove() - { - Discv5KademliaAdapter.BoundedMap map = new(2); - map.Set(1, "a"); - map.Set(2, "b"); - - Assert.That(map.TryRemove(1, out string? removed), Is.True); - Assert.That(removed, Is.EqualTo("a")); - - map.Set(3, "c"); - map.Set(4, "d"); - - using (Assert.EnterMultipleScope()) - { - Assert.That(map.Snapshot(), Has.Length.EqualTo(2)); - Assert.That(map.TryGetValue(1, out _), Is.False); - Assert.That(map.TryGetValue(2, out _), Is.False); - Assert.That(map.TryGetValue(3, out _), Is.True); - Assert.That(map.TryGetValue(4, out _), Is.True); - } - } - private Discv5KademliaAdapter CreateAdapter() => new( new Lazy>(_kademlia), null!, @@ -206,10 +146,4 @@ private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) return enr; } - private static Discv5KademliaAdapter.NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) - { - PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); - int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); - return new Discv5KademliaAdapter.NodesResponseHandler(receiver, new Discv5Distances([distance]), Hash256KademliaDistance.Instance); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs new file mode 100644 index 000000000000..f55fe1e9497a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv5.Handlers; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Enr; +using Nethermind.Stats.Model; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv5.Handlers; + +public class NodesResponseHandlerTests +{ + [Test] + public void ShouldRejectNonRoutableRecordFromPublicReceiver() + { + Node receiver = new(TestItem.PublicKeyA, "8.8.8.8", 30303); + NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); + + using Discv5Nodes nodes = new([1], 1, [loopbackRecord]); + handler.Handle(nodes); + + Assert.That(handler.GetNodes(), Is.Empty); + } + + [Test] + public void ShouldAcceptNonRoutableRecordFromNonRoutableReceiver() + { + Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); + NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); + + using Discv5Nodes nodes = new([1], 1, [loopbackRecord]); + handler.Handle(nodes); + + Assert.That(handler.GetNodes(), Has.Length.EqualTo(1)); + } + + [Test] + public void ShouldRejectSpecialUseRecordFromNonRoutableReceiver() + { + Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); + NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); + NodesResponseHandler handler = CreateNodesResponseHandler(receiver, documentationRecord); + + using Discv5Nodes nodes = new([1], 1, [documentationRecord]); + handler.Handle(nodes); + + Assert.That(handler.GetNodes(), Is.Empty); + } + + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new UdpEntry(30303)); + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + return enr; + } + + private static NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) + { + PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); + int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); + return new NodesResponseHandler(receiver, new Discv5Distances([distance]), Hash256KademliaDistance.Instance); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs index 6fb29efc603b..ecf7b3c5997e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/IteratorNodeLookupTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs @@ -10,14 +10,13 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Kademlia; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test.Discv4 +namespace Nethermind.Network.Discovery.Test.Kademlia { [Parallelizable(ParallelScope.Self)] [TestFixture] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index 5591120b912c..e392f86d183a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -101,5 +101,19 @@ public async Task SkipsMessagesOfInvalidSize(int size) Assert.That(enumerator.Current.Buffer, Is.EqualTo(data)); Assert.That(await enumerator.MoveNextAsync(), Is.False); } + + [Test] + public async Task ChannelInactiveStopsReader() + { + using CancellationTokenSource cancellationSource = new(10_000); + IAsyncEnumerator enumerator = _handler + .ReadMessagesAsync(cancellationSource.Token) + .GetAsyncEnumerator(cancellationSource.Token); + ValueTask readTask = enumerator.MoveNextAsync(); + + _handler.ChannelInactive(Substitute.For()); + + Assert.That(await readTask.AsTask().WaitAsync(cancellationSource.Token), Is.False); + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index a76f18a70356..5853ff9a2f97 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -10,6 +10,7 @@ using Nethermind.Core.ServiceStopper; using Nethermind.Logging; using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv5; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs index 764ec95348be..186fa48636e8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs @@ -35,17 +35,6 @@ protected override void Load(ContainerBuilder builder) => builder .Bind, IKademliaDiscv4Adapter>() .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() - .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() - { - CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), // It actually only need masterNode. - KSize = discoveryConfig.BucketSize, - Alpha = discoveryConfig.Concurrency, - Beta = discoveryConfig.BitsPerHop, - - LookupFindNeighbourHardTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), // TODO: This seems very low. - RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), - RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), - BootNodes = bootNodes - }) + .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)) ; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs index f763aab6bec4..eedc0aad5380 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs @@ -8,14 +8,14 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Crypto; +using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Discv4; -using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; using LogLevel = DotNetty.Handlers.Logging.LogLevel; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public class DiscoveryApp : KademliaDiscoveryApp { diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs index 670a66e22c5d..273b2039ad64 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs @@ -5,13 +5,12 @@ using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Db; -using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Kademlia; +using Nethermind.Logging; using Nethermind.Stats; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; /// /// Manages persistence operations for the discovery process, including loading nodes from storage diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/EnrResponseHandler.cs similarity index 84% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/EnrResponseHandler.cs index a9f01cbacd19..687de022465c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/EnrResponseHandler.cs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Extensions; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Handlers; public class EnrResponseHandler(EnrRequestMsg request) : ITaskCompleter { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/IMessageHandler.cs similarity index 62% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/IMessageHandler.cs index e3be64a4b8cc..94449b9e08a8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMessageHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/IMessageHandler.cs @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Handlers; internal interface IMessageHandler { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/ITaskCompleter.cs similarity index 79% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/ITaskCompleter.cs index ddc9dc6a2192..ff60a0d18f5e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/ITaskCompleter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/ITaskCompleter.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Handlers; internal interface ITaskCompleter : IMessageHandler { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/NeighbourMsgHandler.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/NeighbourMsgHandler.cs index 08170d544ffe..52f7f1a8dce0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/NeighbourMsgHandler.cs @@ -1,10 +1,10 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Handlers; public class NeighbourMsgHandler(int k) : ITaskCompleter { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/PongMsgHandler.cs similarity index 82% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/PongMsgHandler.cs index 5751cad3769f..34d7a864fcb7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PongMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/PongMsgHandler.cs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Extensions; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Handlers; public class PongMsgHandler(PingMsg ping) : ITaskCompleter { diff --git a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IDiscoveryMsgListener.cs similarity index 65% rename from src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/IDiscoveryMsgListener.cs index 70db39491f26..6c188aee5f12 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/IDiscoveryMsgListener.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IDiscoveryMsgListener.cs @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public interface IDiscoveryMsgListener { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs index f8fdf0b36d93..2067c8925545 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs @@ -3,7 +3,7 @@ using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv4; diff --git a/src/Nethermind/Nethermind.Network.Discovery/IMsgSender.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMsgSender.cs similarity index 64% rename from src/Nethermind/Nethermind.Network.Discovery/IMsgSender.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/IMsgSender.cs index 1235dea5fef4..89eafc905cc0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/IMsgSender.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/IMsgSender.cs @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public interface IMsgSender { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs index 1a97cd33d1a6..53e327dc989e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs @@ -9,7 +9,8 @@ using Nethermind.Core.Utils; using Nethermind.Logging; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Handlers; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats; using Nethermind.Stats.Model; using NonBlocking; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs index 2e7585e1377e..42e6dfbb9cd1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs @@ -32,9 +32,7 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance using CancellationTokenSource disposeCts = CancellationTokenSource.CreateLinkedTokenSource(token); CancellationToken discoveryToken = disposeCts.Token; Channel ch = Channel.CreateBounded(ChannelCapacity); - LinkedList recentlyWrittenNodes = []; - Dictionary> writtenNodes = []; - object writtenNodesLock = new(); + RecentNodeFilter recentlyWrittenNodes = new(_recentNodeLimit); int duplicated = 0; int total = 0; @@ -66,7 +64,7 @@ async Task DiscoverAsync(PublicKey target) anyFound = true; count++; total++; - if (!TryReserveNode(node.IdHash)) + if (!recentlyWrittenNodes.TryReserve(node.IdHash)) { duplicated++; continue; @@ -78,7 +76,7 @@ async Task DiscoverAsync(PublicKey target) } catch { - ReleaseReservedNode(node.IdHash); + recentlyWrittenNodes.Release(node.IdHash); throw; } } @@ -150,7 +148,7 @@ async Task DiscoverAsync(PublicKey target) void Handler(object? _, Node addedNode) { - if (!TryReserveNode(addedNode.IdHash)) + if (!recentlyWrittenNodes.TryReserve(addedNode.IdHash)) { return; } @@ -160,40 +158,7 @@ void Handler(object? _, Node addedNode) return; } - ReleaseReservedNode(addedNode.IdHash); - } - - bool TryReserveNode(ValueHash256 nodeId) - { - lock (writtenNodesLock) - { - if (writtenNodes.ContainsKey(nodeId)) - { - return false; - } - - LinkedListNode listNode = recentlyWrittenNodes.AddLast(nodeId); - writtenNodes.Add(nodeId, listNode); - while (writtenNodes.Count > _recentNodeLimit) - { - LinkedListNode oldestNode = recentlyWrittenNodes.First!; - recentlyWrittenNodes.RemoveFirst(); - writtenNodes.Remove(oldestNode.Value); - } - - return true; - } - } - - void ReleaseReservedNode(ValueHash256 nodeId) - { - lock (writtenNodesLock) - { - if (writtenNodes.Remove(nodeId, out LinkedListNode? listNode)) - { - recentlyWrittenNodes.Remove(listNode); - } - } + recentlyWrittenNodes.Release(addedNode.IdHash); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/DiscoveryMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/DiscoveryMsg.cs similarity index 95% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/DiscoveryMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/DiscoveryMsg.cs index e7f4855e4c6f..3cb8159130ac 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/DiscoveryMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/DiscoveryMsg.cs @@ -4,7 +4,7 @@ using System.Net; using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public abstract class DiscoveryMsg : MessageBase { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs index 6ce00eef3e32..a1fbfc111756 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrRequestMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs @@ -4,7 +4,7 @@ using System.Net; using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; /// /// https://eips.ethereum.org/EIPS/eip-868 diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrResponseMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs similarity index 94% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/EnrResponseMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs index e22c68b15b44..294618356a22 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/EnrResponseMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs @@ -5,7 +5,7 @@ using Nethermind.Core.Crypto; using Nethermind.Network.Enr; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; /// /// https://eips.ethereum.org/EIPS/eip-868 diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/FindNodeMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/FindNodeMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs index cc4ff14a10fc..fbf17bcee903 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/FindNodeMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs @@ -5,7 +5,7 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public class FindNodeMsg : DiscoveryMsg { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/INodeIdResolver.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/INodeIdResolver.cs similarity index 82% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/INodeIdResolver.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/INodeIdResolver.cs index 802a887ca36d..2a165405e871 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/INodeIdResolver.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/INodeIdResolver.cs @@ -3,7 +3,7 @@ using Nethermind.Core.Crypto; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public interface INodeIdResolver { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs index 72d9a65b3f02..eb080a5cdcce 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/NeighborsMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs @@ -5,7 +5,7 @@ using Nethermind.Core.Crypto; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public class NeighborsMsg : DiscoveryMsg { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/NodeIdResolver.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs similarity index 89% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/NodeIdResolver.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs index 72d13fffbc82..a0178a2d9335 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/NodeIdResolver.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs @@ -4,7 +4,7 @@ using Nethermind.Core.Crypto; using Nethermind.Crypto; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public class NodeIdResolver(IEcdsa ecdsa) : INodeIdResolver { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs similarity index 96% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs index 08cfd353b141..da0e37ba6159 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/PingMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs @@ -5,7 +5,7 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public class PingMsg : DiscoveryMsg { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Messages/PongMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Messages/PongMsg.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs index e02c08f6bec4..6de0b77113b7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Messages/PongMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs @@ -5,7 +5,7 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public class PongMsg : DiscoveryMsg { diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs similarity index 99% rename from src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs index 6185632e056c..64344e324ca6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs @@ -16,10 +16,10 @@ using Nethermind.Core.Collections; using Nethermind.Core.Extensions; using Nethermind.Logging; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using ILogger = Nethermind.Logging.ILogger; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public class NettyDiscoveryHandler( IDiscoveryMsgListener? discoveryManager, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index cd908ee34ff9..0a583e4c32c8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -3,7 +3,7 @@ using System.Net; using Nethermind.Core; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodeSourceToDiscV4Feeder.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs similarity index 95% rename from src/Nethermind/Nethermind.Network.Discovery/NodeSourceToDiscV4Feeder.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs index 52b021e1cdf1..e94298761ec3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NodeSourceToDiscV4Feeder.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs @@ -5,7 +5,7 @@ using Nethermind.Config; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery; +namespace Nethermind.Network.Discovery.Discv4; public class NodeSourceToDiscV4Feeder([KeyFilter(NodeSourceToDiscV4Feeder.SourceKey)] INodeSource nodeSource, IDiscoveryApp discoveryApp, IProcessExitSource exitSource, int maxNodes = 50) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs index 0b60788d53bd..4869ce6cfa11 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/DiscoveryMsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs @@ -8,10 +8,10 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public abstract class DiscoveryMsgSerializerBase(IEcdsa ecdsa, IPrivateKeyGenerator nodeKey, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs similarity index 94% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs index cca1af0b8cb9..5b0eb032adef 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrRequestMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs @@ -5,10 +5,10 @@ using DotNetty.Buffers; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public class EnrRequestMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrResponseMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs similarity index 95% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrResponseMsgSerializer.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs index 94d8f91132d8..4cafc69140ba 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/EnrResponseMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs @@ -6,11 +6,11 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public class EnrResponseMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs similarity index 94% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/FindNodeMsgSerializer.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs index 5eed89a7bcd9..3bb7867432fc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs @@ -5,10 +5,10 @@ using DotNetty.Buffers; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public class FindNodeMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs index c3d747506622..558fafc54706 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/NeighborsMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs @@ -6,11 +6,11 @@ using DotNetty.Buffers; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public class NeighborsMsgSerializer( IEcdsa ecdsa, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs index 22a0d8034a8b..7390ae65cbb6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs @@ -6,10 +6,10 @@ using DotNetty.Buffers; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public class PingMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs similarity index 96% rename from src/Nethermind/Nethermind.Network.Discovery/Serializers/PongMsgSerializer.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs index 6a2f21e93349..51b277a28214 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs @@ -5,10 +5,10 @@ using DotNetty.Buffers; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; -namespace Nethermind.Network.Discovery.Serializers; +namespace Nethermind.Network.Discovery.Discv4.Serializers; public class PongMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs index 79c2989a73a7..19e096ed35c2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs @@ -5,7 +5,6 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; @@ -25,15 +24,5 @@ protected override void Load(ContainerBuilder builder) => builder .AddModule(new KademliaModule()) .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() - .AddSingleton, IDiscoveryConfig>((discoveryConfig) => new KademliaConfig() - { - CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), - KSize = discoveryConfig.BucketSize, - Alpha = discoveryConfig.Concurrency, - Beta = discoveryConfig.BitsPerHop, - LookupFindNeighbourHardTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), - RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), - RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), - BootNodes = bootNodes - }); + .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index da351cc88af1..bc60c759be8e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -16,7 +16,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; @@ -195,45 +195,13 @@ private static string[] GetDefaultDiscv5Bootnodes() => internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? node) { - node = null; - - PublicKey? key = GetPublicKeyFromEnr(enr); - if (key is null) - { - if (Logger.IsTrace) Logger.Trace("Enr declined, unable to extract public key."); - return false; - } - - (IPAddress? ip, int? discoveryPort) = Discv5NodeRecordConverter.GetDiscoveryEndpoint(enr); - if (ip is null) - { - if (Logger.IsTrace) Logger.Trace("Enr declined, no IP."); - return false; - } - - if (discoveryPort is null) - { - if (Logger.IsTrace) Logger.Trace("Enr declined, no discovery UDP port."); - return false; - } - - if (!IsDiscoveryAddressAcceptable(ip, _allowNonRoutableEnrs)) - { - if (Logger.IsTrace) Logger.Trace($"Enr declined, non-routable IP {ip}."); - return false; - } - - if ((uint)discoveryPort.Value > ushort.MaxValue || discoveryPort.Value == 0) + if (Discv5NodeRecordConverter.TryGetNodeFromEnr(enr, _allowNonRoutableEnrs, out node)) { - if (Logger.IsTrace) Logger.Trace($"Enr declined, invalid discovery UDP port {discoveryPort.Value}."); - return false; + return true; } - node = new Node(key, ip.ToString(), discoveryPort.Value) - { - Enr = enr.EnrString - }; - return true; + if (Logger.IsTrace) Logger.Trace("Enr declined, unable to extract a usable discv5 node endpoint."); + return false; } private static PublicKey? GetPublicKeyFromEnr(NodeRecord enr) => @@ -246,91 +214,26 @@ internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allo return false; } - if (ipAddress.IsIPv6Multicast || NodeFilter.IsIPv4Multicast(ipAddress)) + if (IPAddressClassifier.IsMulticast(ipAddress)) { return false; } - if (IsSpecialUseAddress(ipAddress)) + if (IPAddressClassifier.IsSpecialUseAddress(ipAddress)) { return false; } - return allowNonRoutable || !NodeFilter.IsLoopbackOrPrivateOrLinkLocal(ipAddress); + return allowNonRoutable || !IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(ipAddress); } internal static bool IsDiscoveryAddressRoutable(IPAddress ipAddress) => IsDiscoveryAddressAcceptable(ipAddress, allowNonRoutable: false); - private static bool IsSpecialUseAddress(IPAddress ipAddress) - { - Span bytes = stackalloc byte[16]; - if (!ipAddress.TryWriteBytes(bytes, out int written)) - { - return true; - } - - if (written == 4) - { - return IsSpecialUseIPv4(bytes[..4]); - } - - if (IsIPv4MappedIPv6(bytes)) - { - return IsSpecialUseIPv4(bytes[12..]); - } - - return IsSpecialUseIPv6(bytes); - } - - private static bool IsSpecialUseIPv4(ReadOnlySpan bytes) - { - uint v4 = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(bytes); - byte a = (byte)(v4 >> 24); - byte b = (byte)(v4 >> 16); - byte c = (byte)(v4 >> 8); - - return a == 0 // 0.0.0.0/8 - || a == 192 && b == 0 && c is 0 or 2 // 192.0.0.0/24, 192.0.2.0/24 - || a == 192 && b == 31 && c == 196 // 192.31.196.0/24 - || a == 192 && b == 52 && c == 193 // 192.52.193.0/24 - || a == 192 && b == 88 && c == 99 // 192.88.99.0/24 - || a == 192 && b == 175 && c == 48 // 192.175.48.0/24 - || a == 198 && b is 18 or 19 // 198.18.0.0/15 - || a == 198 && b == 51 && c == 100 // 198.51.100.0/24 - || a == 203 && b == 0 && c == 113 // 203.0.113.0/24 - || a >= 224; // 224.0.0.0/4, 240.0.0.0/4 - } - - private static bool IsSpecialUseIPv6(ReadOnlySpan bytes) - => bytes[0] == 0x00 && bytes[1] == 0x64 && bytes[2] == 0xff && bytes[3] == 0x9b && IsZero(bytes[4..12]) // 64:ff9b::/96 - || bytes[0] == 0x00 && bytes[1] == 0x64 && bytes[2] == 0xff && bytes[3] == 0x9b && bytes[4] == 0x00 && bytes[5] == 0x01 // 64:ff9b:1::/48 - || bytes[0] == 0x01 && IsZero(bytes[1..8]) // 100::/64 - || bytes[0] == 0x20 && bytes[1] == 0x01 && (bytes[2] & 0xfe) == 0x00 // 2001::/23 - || bytes[0] == 0x20 && bytes[1] == 0x01 && bytes[2] == 0x0d && bytes[3] == 0xb8 // 2001:db8::/32 - || bytes[0] == 0x20 && bytes[1] == 0x02 // 2002::/16 - || bytes[0] == 0x3f && bytes[1] == 0xff && (bytes[2] & 0xf0) == 0x00; // 3fff::/20 - - private static bool IsIPv4MappedIPv6(ReadOnlySpan bytes) - => IsZero(bytes[..10]) && bytes[10] == 0xff && bytes[11] == 0xff; - - private static bool IsZero(ReadOnlySpan bytes) - { - for (int i = 0; i < bytes.Length; i++) - { - if (bytes[i] != 0) - { - return false; - } - } - - return true; - } - private static bool ShouldAcceptNonRoutableEnrs(IPAddress externalIp) => !IPAddress.Any.Equals(externalIp) && !IPAddress.None.Equals(externalIp) - && NodeFilter.IsLoopbackOrPrivateOrLinkLocal(externalIp); + && IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(externalIp); internal static bool TryEnqueueNewEnr(Queue nodesToCheck, HashSet seenNodes, NodeRecord enr) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs new file mode 100644 index 000000000000..7a6b959074d6 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Buffers.Binary; +using System.Net; +using Nethermind.Core.Crypto; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5; + +internal readonly record struct SessionKey(Hash256 NodeId, IPEndPoint Endpoint); + +internal readonly record struct ChallengeKey(Hash256 NodeId, IPEndPoint Endpoint); + +internal readonly record struct PendingNonceKey(IPEndPoint Endpoint, NonceKey Nonce); + +internal readonly record struct ResponseKey(Hash256 NodeId, Discv5RequestId RequestId, Discv5MessageType MessageType); + +internal readonly record struct NonceKey(ulong Prefix, uint Suffix) +{ + public static NonceKey From(ReadOnlySpan nonce) + { + if (nonce.Length != Discv5PacketCodec.NonceSize) + { + throw new ArgumentException($"Nonce must be {Discv5PacketCodec.NonceSize} bytes.", nameof(nonce)); + } + + return new NonceKey( + BinaryPrimitives.ReadUInt64BigEndian(nonce[..sizeof(ulong)]), + BinaryPrimitives.ReadUInt32BigEndian(nonce.Slice(sizeof(ulong), sizeof(uint)))); + } +} + +internal sealed record PendingRequest(Node Receiver, Discv5Message Message); + +internal readonly record struct SentChallenge(Discv5Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs index f269064cadd0..137f0e94a030 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs @@ -1,13 +1,14 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; +using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Crypto; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv5.Handlers; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; @@ -36,8 +37,6 @@ public class Discv5KademliaAdapter( private const int MaxResponseHandlers = 1_024; private const int MaxKnownRecords = 16_384; private const int MaxEndpointChecks = 4_096; - private const int MaxNodesResponseMessages = 16; - private const int MaxNodesResponseRecords = 64; private const long SentChallengeTtlMilliseconds = 60_000; private const long EndpointCheckTtlMilliseconds = 60_000; private static readonly TimeSpan ChallengeRateLimitWindow = TimeSpan.FromMilliseconds(100); @@ -48,13 +47,13 @@ public class Discv5KademliaAdapter( private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); private readonly IKademliaDistance _distance = distance; private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly BoundedMap _sessions = new(MaxSessions); - private readonly BoundedMap _sentChallenges = new(MaxSentChallenges); + private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions"); + private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); private long _lastSentChallengeTrimMilliseconds; - private readonly BoundedMap _pendingByNonce = new(MaxPendingRequests); - private readonly BoundedMap _responseHandlers = new(MaxResponseHandlers); - private readonly BoundedMap _knownRecords = new(MaxKnownRecords); - private readonly BoundedMap _endpointChecks = new(MaxEndpointChecks); + private readonly LruCache _pendingByNonce = new(MaxPendingRequests, "discv5 pending requests"); + private readonly LruCache _responseHandlers = new(MaxResponseHandlers, "discv5 response handlers"); + private readonly LruCache _knownRecords = new(MaxKnownRecords, "discv5 known records"); + private readonly LruCache _endpointChecks = new(MaxEndpointChecks, "discv5 endpoint checks"); private readonly NodeFilter[] _challengeRateLimiters = CreateChallengeRateLimiters(); /// @@ -100,7 +99,7 @@ public async Task Ping(Node receiver, CancellationToken token) using Discv5Ping ping = new(CreateRequestId(), nodeRecordProvider.Current.EnrSequence); PongResponseHandler responseHandler = new(receiver); - await SendRequest(receiver, ping, Discv5MessageType.Pong, responseHandler, _pingTimeout, token); + await SendRequest(receiver, ping, responseHandler, _pingTimeout, token); kademlia.Value.AddOrRefresh(receiver); } @@ -112,7 +111,7 @@ public async Task FindNeighbours(Node receiver, PublicKey target, Cancel using Discv5FindNode findNode = new(CreateRequestId(), distances); NodesResponseHandler responseHandler = new(receiver, distances, _distance); - await SendRequest(receiver, findNode, Discv5MessageType.Nodes, responseHandler, _findNodeTimeout, token); + await SendRequest(receiver, findNode, responseHandler, _findNodeTimeout, token); Node[] nodes = responseHandler.GetNodes(); for (int i = 0; i < nodes.Length; i++) { @@ -143,15 +142,15 @@ public async Task RunAsync(CancellationToken token) /// public ValueTask DisposeAsync() => ValueTask.CompletedTask; - private async Task SendRequest( + private async Task SendRequest( Node receiver, Discv5Message request, - Discv5MessageType responseType, - IResponseHandler responseHandler, + IResponseHandler responseHandler, TimeSpan timeout, CancellationToken token) + where TResponse : Discv5Message { - ResponseKey responseKey = new(receiver.Id.Hash, request.RequestId, responseType); + ResponseKey responseKey = new(receiver.Id.Hash, request.RequestId, responseHandler.MessageType); _responseHandlers.Set(responseKey, responseHandler); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -329,7 +328,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can return; } - if (IsAcceptableNodeRecord(nodeRecord, nodeId, NodeFilter.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) { SetKnownRecord(nodeId, nodeRecord); messageRecord = nodeRecord; @@ -352,7 +351,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket Hash256 nodeId = new(destinationNodeId); ChallengeKey challengeKey = new(nodeId, endpoint); long now = Environment.TickCount64; - if (_sentChallenges.TryGetValue(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) + if (_sentChallenges.TryGet(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) { await discoveryHandler.SendAsync(existingChallenge.Packet, endpoint); return; @@ -417,13 +416,13 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, return nodeRecord.EnrString; } - return _knownRecords.TryGetValue(nodeId, out NodeRecord? knownRecord) ? knownRecord.EnrString : null; + return _knownRecords.TryGet(nodeId, out NodeRecord? knownRecord) ? knownRecord.EnrString : null; } private bool HandleResponse(Hash256 nodeId, Discv5Message message) { ResponseKey responseKey = new(nodeId, message.RequestId, message.MessageType); - return _responseHandlers.TryGetValue(responseKey, out IResponseHandler? handler) && handler.Handle(message); + return _responseHandlers.TryGet(responseKey, out IResponseHandler? handler) && handler.Handle(message); } private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, CancellationToken token) @@ -450,7 +449,7 @@ private NodeRecord[] GetFindNodeRecords(Discv5Distances distances, Node requeste { HashSet seen = new(MaxFindNodeRecords); List result = new(MaxFindNodeRecords); - bool allowNonRoutableRelays = NodeFilter.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); + bool allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); bool includedSelf = false; for (int i = 0; i < distances.Count && result.Count < MaxFindNodeRecords; i++) { @@ -531,7 +530,7 @@ private void RegisterKnownRecord(Node node) try { NodeRecord record = NodeRecord.FromEnrString(node.Enr); - if (IsAcceptableNodeRecord(record, node.Id.Hash, NodeFilter.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address))) + if (IsAcceptableNodeRecord(record, node.Id.Hash, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address))) { SetKnownRecord(node.Id.Hash, record); } @@ -575,12 +574,12 @@ private Discv5RequestId CreateRequestId() return Discv5RequestId.From(requestId[start..]); } - private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGetValue(sessionKey, out session); + private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGet(sessionKey, out session); private void SetSession(SessionKey sessionKey, Discv5Session session) => _sessions.Set(sessionKey, session); - private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGetValue(nodeId, out record); + private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGet(nodeId, out record); private void SetKnownRecord(Hash256 nodeId, NodeRecord record) => _knownRecords.Set(nodeId, record); @@ -613,7 +612,7 @@ private void TryTrimExpiredChallenges(long now) private void TrimExpiredChallenges(long now) { - foreach (KeyValuePair kv in _sentChallenges.Snapshot()) + foreach (KeyValuePair kv in _sentChallenges.ToArray()) { if (IsExpired(kv.Value, now)) { @@ -649,216 +648,6 @@ private static NodeFilter[] CreateChallengeRateLimiters() return filters; } - internal sealed class BoundedMap(int maxCount) - where TKey : notnull - where TValue : notnull - { - private readonly object _lock = new(); - private readonly Dictionary _items = []; - private readonly LinkedList _insertionOrder = []; - private readonly Dictionary> _insertionNodes = []; - - public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) - { - lock (_lock) - { - if (_items.TryGetValue(key, out TValue? found)) - { - value = found; - return true; - } - - value = default; - return false; - } - } - - public void Set(TKey key, TValue value) - { - lock (_lock) - { - if (_items.ContainsKey(key)) - { - _items[key] = value; - return; - } - - _items.Add(key, value); - _insertionNodes.Add(key, _insertionOrder.AddLast(key)); - Trim(); - } - } - - public bool TryRemove(TKey key, [NotNullWhen(true)] out TValue? value) - { - lock (_lock) - { - if (!_items.TryGetValue(key, out TValue? found)) - { - value = default; - return false; - } - - _items.Remove(key); - if (_insertionNodes.Remove(key, out LinkedListNode? node)) - { - _insertionOrder.Remove(node); - } - - value = found; - return true; - } - } - - public KeyValuePair[] Snapshot() - { - lock (_lock) - { - return [.. _items]; - } - } - - private void Trim() - { - while (_items.Count > maxCount) - { - LinkedListNode oldest = _insertionOrder.First!; - _insertionOrder.RemoveFirst(); - _insertionNodes.Remove(oldest.Value); - _items.Remove(oldest.Value); - } - } - } - - private readonly record struct SessionKey(Hash256 NodeId, IPEndPoint Endpoint); - - private readonly record struct ChallengeKey(Hash256 NodeId, IPEndPoint Endpoint); - - private readonly record struct PendingNonceKey(IPEndPoint Endpoint, NonceKey Nonce); - - private readonly record struct ResponseKey(Hash256 NodeId, Discv5RequestId RequestId, Discv5MessageType MessageType); - - private readonly record struct NonceKey(ulong Prefix, uint Suffix) - { - public static NonceKey From(ReadOnlySpan nonce) - { - if (nonce.Length != Discv5PacketCodec.NonceSize) - { - throw new ArgumentException($"Nonce must be {Discv5PacketCodec.NonceSize} bytes.", nameof(nonce)); - } - - return new NonceKey( - BinaryPrimitives.ReadUInt64BigEndian(nonce[..sizeof(ulong)]), - BinaryPrimitives.ReadUInt32BigEndian(nonce.Slice(sizeof(ulong), sizeof(uint)))); - } - } - - private sealed record PendingRequest(Node Receiver, Discv5Message Message); - - private readonly record struct SentChallenge(Discv5Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds); - - private interface IResponseHandler - { - Task Task { get; } - - bool Handle(Discv5Message message); - } - - private sealed class PongResponseHandler(Node receiver) : IResponseHandler - { - private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - - public Task Task => _completion.Task; - - public bool Handle(Discv5Message message) - { - if (message is not Discv5Pong pong) - { - return false; - } - - receiver.ValidatedProtocol = true; - _completion.TrySetResult(); - return true; - } - } - - internal sealed class NodesResponseHandler(Node receiver, Discv5Distances requestedDistances, IKademliaDistance distanceCalculator) : IResponseHandler - { - private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly List _nodes = []; - private readonly HashSet _seenNodeIds = []; - private readonly bool _allowNonRoutableRelays = NodeFilter.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); - private int? _total; - private int _received; - - public Task Task => _completion.Task; - - public bool Handle(Discv5Message message) - { - if (message is not Discv5Nodes nodes) - { - return false; - } - - if (_completion.Task.IsCompleted) - { - return true; - } - - if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) - { - _completion.TrySetResult(); - return true; - } - - if (_total is not null && _total.Value != nodes.Total) - { - _completion.TrySetResult(); - return true; - } - - _total ??= nodes.Total; - _received++; - - for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) - { - NodeRecord record = nodes.Records[i]; - if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || - !_seenNodeIds.Add(node.Id.Hash) || - !MatchesRequestedDistance(node, requestedDistances)) - { - continue; - } - - _nodes.Add(node); - } - - if (_received >= _total || _nodes.Count >= MaxNodesResponseRecords) - { - _completion.TrySetResult(); - } - - return true; - } - - public Node[] GetNodes() => [.. _nodes]; - - private bool MatchesRequestedDistance(Node node, Discv5Distances requestedDistances) - { - int distance = distanceCalculator.CalculateLogDistance(receiver.Id.Hash, node.Id.Hash); - for (int i = 0; i < requestedDistances.Count; i++) - { - if (requestedDistances[i] == distance) - { - return true; - } - } - - return false; - } - } - private void StartEndpointCheck(Node remoteNode, CancellationToken token) { if (!TryReserveEndpointCheck(remoteNode)) @@ -891,7 +680,7 @@ private bool TryReserveEndpointCheck(Node remoteNode) { SessionKey sessionKey = new(remoteNode.Id.Hash, remoteNode.Address); long now = Environment.TickCount64; - if (_endpointChecks.TryGetValue(sessionKey, out long startedAt) && + if (_endpointChecks.TryGet(sessionKey, out long startedAt) && now - startedAt <= EndpointCheckTtlMilliseconds) { return false; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs index 7dd42e2f4315..80581eb4cc51 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs @@ -6,7 +6,6 @@ using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; @@ -29,14 +28,12 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance if (_logger.IsDebug) _logger.Debug("Starting discv5 node source"); Channel channel = Channel.CreateBounded(ChannelCapacity); - LinkedList recentlyWrittenNodes = []; - Dictionary> writtenNodes = []; - object writtenNodesLock = new(); + RecentNodeFilter recentlyWrittenNodes = new(_recentNodeLimit); int initialNodes = 0; foreach (Node node in kademlia.IterateNodes()) { - if (!IsExcluded(node) && TryReserveNode(node.IdHash)) + if (!IsExcluded(node) && recentlyWrittenNodes.TryReserve(node.IdHash)) { initialNodes++; yield return node; @@ -60,7 +57,7 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance void Handler(object? _, Node node) { - if (IsExcluded(node) || !TryReserveNode(node.IdHash)) + if (IsExcluded(node) || !recentlyWrittenNodes.TryReserve(node.IdHash)) { return; } @@ -71,45 +68,12 @@ void Handler(object? _, Node node) return; } - ReleaseReservedNode(node.IdHash); + recentlyWrittenNodes.Release(node.IdHash); if (_logger.IsTrace) { _logger.Trace($"Discv5 node source queue is full, dropping discovered node {node:s}."); } } - - bool TryReserveNode(Hash256 nodeId) - { - lock (writtenNodesLock) - { - if (writtenNodes.ContainsKey(nodeId)) - { - return false; - } - - LinkedListNode listNode = recentlyWrittenNodes.AddLast(nodeId); - writtenNodes.Add(nodeId, listNode); - while (writtenNodes.Count > _recentNodeLimit) - { - LinkedListNode oldestNode = recentlyWrittenNodes.First!; - recentlyWrittenNodes.RemoveFirst(); - writtenNodes.Remove(oldestNode.Value); - } - - return true; - } - } - - void ReleaseReservedNode(Hash256 nodeId) - { - lock (writtenNodesLock) - { - if (writtenNodes.Remove(nodeId, out LinkedListNode? listNode)) - { - recentlyWrittenNodes.Remove(listNode); - } - } - } } private bool IsExcluded(Node node) => node.IsBootnode || node.IdHash.Equals(_currentNodeHash); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs new file mode 100644 index 000000000000..36aefc4adc84 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Discv5.Messages; + +namespace Nethermind.Network.Discovery.Discv5.Handlers; + +internal interface IResponseHandler +{ + Task Task { get; } + + Discv5MessageType MessageType { get; } + + bool Handle(Discv5Message message); +} + +internal interface IResponseHandler : IResponseHandler where TMessage : Discv5Message +{ + bool Handle(TMessage message); +} + +internal abstract class ResponseHandler(Discv5MessageType messageType) : IResponseHandler + where TMessage : Discv5Message +{ + public abstract Task Task { get; } + + public Discv5MessageType MessageType { get; } = messageType; + + public bool Handle(Discv5Message message) => message is TMessage typedMessage && Handle(typedMessage); + + public abstract bool Handle(TMessage message); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs new file mode 100644 index 000000000000..d443a40dd465 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Enr; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5.Handlers; + +internal sealed class NodesResponseHandler(Node receiver, Discv5Distances requestedDistances, IKademliaDistance distanceCalculator) + : ResponseHandler(Discv5MessageType.Nodes) +{ + private const int MaxNodesResponseMessages = 16; + private const int MaxNodesResponseRecords = 64; + + private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly List _nodes = []; + private readonly HashSet _seenNodeIds = []; + private readonly bool _allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); + private int? _total; + private int _received; + + public override Task Task => _completion.Task; + + public override bool Handle(Discv5Nodes nodes) + { + if (_completion.Task.IsCompleted) + { + return true; + } + + if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) + { + _completion.TrySetResult(); + return true; + } + + if (_total is not null && _total.Value != nodes.Total) + { + _completion.TrySetResult(); + return true; + } + + _total ??= nodes.Total; + _received++; + + for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) + { + NodeRecord record = nodes.Records[i]; + if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || + !_seenNodeIds.Add(node.Id.Hash) || + !MatchesRequestedDistance(node, requestedDistances)) + { + continue; + } + + _nodes.Add(node); + } + + if (_received >= _total || _nodes.Count >= MaxNodesResponseRecords) + { + _completion.TrySetResult(); + } + + return true; + } + + public Node[] GetNodes() => [.. _nodes]; + + private bool MatchesRequestedDistance(Node node, Discv5Distances requestedDistances) + { + int distance = distanceCalculator.CalculateLogDistance(receiver.Id.Hash, node.Id.Hash); + for (int i = 0; i < requestedDistances.Count; i++) + { + if (requestedDistances[i] == distance) + { + return true; + } + } + + return false; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs new file mode 100644 index 000000000000..52742098ebb9 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Discv5.Handlers; + +internal sealed class PongResponseHandler(Node receiver) : ResponseHandler(Discv5MessageType.Pong) +{ + private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public override Task Task => _completion.Task; + + public override bool Handle(Discv5Pong message) + { + receiver.ValidatedProtocol = true; + _completion.TrySetResult(); + return true; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 7bd8a349dfa3..9395f966606b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -31,6 +31,18 @@ public class NettyDiscoveryV5Handler(ILogManager loggerManager) : NettyDiscovery public override void ChannelActive(IChannelHandlerContext context) => OnChannelActivated?.Invoke(this, EventArgs.Empty); + public override void ChannelInactive(IChannelHandlerContext context) + { + Close(); + base.ChannelInactive(context); + } + + public override void HandlerRemoved(IChannelHandlerContext context) + { + Close(); + base.HandlerRemoved(context); + } + protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket msg) { msg.Retain(); @@ -42,7 +54,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket } ReferenceCountUtil.Release(queuedPacket); - if (_logger.IsDebug) + if (_logger.IsWarn) { _logger.Warn("Skipping discovery v5 message as inbound buffer is full"); } @@ -50,7 +62,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket public async Task SendAsync(byte[] data, IPEndPoint destination) { - if (_nettyChannel == null) throw new("Channel for discovery v5 is not initialized"); + if (_nettyChannel is null) throw new("Channel for discovery v5 is not initialized"); DatagramPacket packet = new(Unpooled.WrappedBuffer(data), destination); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs new file mode 100644 index 000000000000..d375a425b77e --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Kademlia; + +internal static class DiscoveryKademliaConfigFactory +{ + public static KademliaConfig Create(PublicKey masterNode, IReadOnlyList bootNodes, IDiscoveryConfig discoveryConfig) + => new() + { + CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), + KSize = discoveryConfig.BucketSize, + Alpha = discoveryConfig.Concurrency, + Beta = discoveryConfig.BitsPerHop, + LookupFindNeighbourHardTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout), + RefreshPingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout), + RefreshInterval = TimeSpan.FromMilliseconds(discoveryConfig.DiscoveryInterval), + BootNodes = bootNodes + }; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaNodeSource.cs similarity index 92% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaNodeSource.cs index 0ee9c86d7102..9925fc8d113b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IKademliaNodeSource.cs @@ -3,7 +3,7 @@ using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Kademlia; /// /// Interface for discovering nodes in a Kademlia distributed hash table network. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs similarity index 99% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs index 1e156c34561b..f9ea43a7b0a8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs @@ -10,7 +10,7 @@ using Nethermind.Logging; using NonBlocking; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Kademlia; /// /// Special lookup made specially for node discovery as the standard lookup is too slow or unnecessarily parallelized. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index d57621569da5..267d6d69c599 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -4,7 +4,6 @@ using Autofac; using Nethermind.Core; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; namespace Nethermind.Network.Discovery.Kademlia; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs similarity index 95% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs index 2316d9db04ba..8f398e6efcaa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs @@ -5,7 +5,7 @@ using Nethermind.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Kademlia; public class PublicKeyKeyOperator : IKeyOperator { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs new file mode 100644 index 000000000000..a33d3184cecc --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Kademlia; + +internal sealed class RecentNodeFilter(int maxCount) + where TKey : notnull +{ + private readonly LinkedList _recentNodes = []; + private readonly Dictionary> _nodes = []; + private readonly object _lock = new(); + + public bool TryReserve(TKey nodeId) + { + lock (_lock) + { + if (_nodes.ContainsKey(nodeId)) + { + return false; + } + + LinkedListNode listNode = _recentNodes.AddLast(nodeId); + _nodes.Add(nodeId, listNode); + while (_nodes.Count > maxCount) + { + LinkedListNode oldestNode = _recentNodes.First!; + _recentNodes.RemoveFirst(); + _nodes.Remove(oldestNode.Value); + } + + return true; + } + } + + public void Release(TKey nodeId) + { + lock (_lock) + { + if (_nodes.Remove(nodeId, out LinkedListNode? listNode)) + { + _recentNodes.Remove(listNode); + } + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs index ac2f887ccff0..d3a580df83ba 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs @@ -9,7 +9,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery; diff --git a/src/Nethermind/Nethermind.Network.Test/Builders/SerializationBuilder.cs b/src/Nethermind/Nethermind.Network.Test/Builders/SerializationBuilder.cs index 96efaf2edf86..df31da53ffcb 100644 --- a/src/Nethermind/Nethermind.Network.Test/Builders/SerializationBuilder.cs +++ b/src/Nethermind/Nethermind.Network.Test/Builders/SerializationBuilder.cs @@ -6,8 +6,8 @@ using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.Serializers; +using Nethermind.Network.Discovery.Discv4.Messages; +using Nethermind.Network.Discovery.Discv4.Serializers; using Nethermind.Network.P2P.Subprotocols.Eth.V62.Messages; using Nethermind.Network.P2P.Subprotocols.Eth.V63.Messages; using Nethermind.Network.P2P.Subprotocols.Eth.V65.Messages; diff --git a/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs b/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs index 4f6063328703..b03276c646b6 100644 --- a/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs @@ -227,7 +227,36 @@ public void IpSubnetKey_AreInSameSubnet(string a, string b, bool expected) => As [TestCase("fe80::1", true, Description = "IPv6 link-local")] [TestCase("8.8.8.8", false, Description = "Public IPv4")] [TestCase("2001:4860:4860::8888", false, Description = "Public IPv6")] - public void IpSubnetKey_IsLoopbackOrPrivateOrLinkLocal(string address, bool expected) => Assert.That(NodeFilter.IpSubnetKey.IsLoopbackOrPrivateOrLinkLocal(IPAddress.Parse(address)), Is.EqualTo(expected)); + public void IPAddressClassifier_IsLoopbackOrPrivateOrLinkLocal(string address, bool expected) => Assert.That(IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(IPAddress.Parse(address)), Is.EqualTo(expected)); + + [TestCase("0.1.2.3", true, Description = "IPv4 this-network")] + [TestCase("192.0.0.1", true, Description = "IPv4 IETF protocol assignments")] + [TestCase("192.0.2.1", true, Description = "IPv4 documentation TEST-NET-1")] + [TestCase("192.31.196.1", true, Description = "IPv4 AS112")] + [TestCase("192.52.193.1", true, Description = "IPv4 AMT")] + [TestCase("198.18.0.1", true, Description = "IPv4 benchmarking")] + [TestCase("192.175.48.1", true, Description = "IPv4 direct delegation AS112")] + [TestCase("198.51.100.1", true, Description = "IPv4 documentation TEST-NET-2")] + [TestCase("203.0.113.1", true, Description = "IPv4 documentation TEST-NET-3")] + [TestCase("224.0.0.1", true, Description = "IPv4 multicast")] + [TestCase("240.0.0.1", true, Description = "IPv4 reserved")] + [TestCase("::ffff:224.0.0.1", true, Description = "IPv4-mapped multicast")] + [TestCase("64:ff9b::1", true, Description = "IPv6 IPv4/IPv6 translation")] + [TestCase("64:ff9b:1::1", true, Description = "IPv6 local-use translation")] + [TestCase("100::1", true, Description = "IPv6 discard-only")] + [TestCase("2001::1", true, Description = "IPv6 IETF protocol assignments")] + [TestCase("2001:db8::1", true, Description = "IPv6 documentation")] + [TestCase("2002::1", true, Description = "IPv6 6to4")] + [TestCase("3fff::1", true, Description = "IPv6 documentation")] + [TestCase("8.8.8.8", false, Description = "Public IPv4")] + [TestCase("2001:4860:4860::8888", false, Description = "Public IPv6")] + public void IPAddressClassifier_IsSpecialUseAddress(string address, bool expected) => Assert.That(IPAddressClassifier.IsSpecialUseAddress(IPAddress.Parse(address)), Is.EqualTo(expected)); + + [TestCase("224.0.0.1", true, Description = "IPv4 multicast")] + [TestCase("ff02::1", true, Description = "IPv6 multicast")] + [TestCase("8.8.8.8", false, Description = "Public IPv4")] + [TestCase("2001:4860:4860::8888", false, Description = "Public IPv6")] + public void IPAddressClassifier_IsMulticast(string address, bool expected) => Assert.That(IPAddressClassifier.IsMulticast(IPAddress.Parse(address)), Is.EqualTo(expected)); [TestCase("192.168.1.10", "192.168.1.20", "203.0.113.1", false, Description = "Private addresses use exact keying")] [TestCase("203.0.113.1", "203.0.113.50", "198.51.100.1", true, Description = "Public addresses in same /24 use subnet bucketing")] diff --git a/src/Nethermind/Nethermind.Network/Discovery/Messages/MsgType.cs b/src/Nethermind/Nethermind.Network/Discovery/Discv4/Messages/MsgType.cs similarity index 80% rename from src/Nethermind/Nethermind.Network/Discovery/Messages/MsgType.cs rename to src/Nethermind/Nethermind.Network/Discovery/Discv4/Messages/MsgType.cs index 41ac1fc7532c..5c1c62c31408 100644 --- a/src/Nethermind/Nethermind.Network/Discovery/Messages/MsgType.cs +++ b/src/Nethermind/Nethermind.Network/Discovery/Discv4/Messages/MsgType.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Messages; +namespace Nethermind.Network.Discovery.Discv4.Messages; public enum MsgType { diff --git a/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs b/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs index a05e4a1dbfd4..0705c45a9036 100644 --- a/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs +++ b/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs @@ -8,20 +8,11 @@ namespace Nethermind.Network.IP public static class IPAddressExtensions { /// - /// An extension method to determine if an IP address is internal, as specified in RFC1918 + /// An extension method to determine if an IP address is internal or otherwise local to the node. /// /// The IP address that will be tested /// Returns true if the IP is internal, false if it is external public static bool IsInternal(this IPAddress toTest) - { - byte[] bytes = toTest.GetAddressBytes(); - return bytes[0] switch - { - 10 => true, - 172 => bytes[1] < 32 && bytes[1] >= 16, - 192 => bytes[1] == 168, - _ => false, - }; - } + => IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(toTest); } } diff --git a/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs b/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs new file mode 100644 index 000000000000..58149ac1e177 --- /dev/null +++ b/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Buffers.Binary; +using System.Net; +using System.Runtime.CompilerServices; + +namespace Nethermind.Network; + +/// +/// Classifies IP addresses into reusable networking categories used by peer and discovery filtering. +/// +public static class IPAddressClassifier +{ + internal enum ParsedIPAddressFamily : byte { IPv4 = 4, IPv6 = 6 } + + internal readonly struct ParsedIPAddress(ParsedIPAddressFamily family, uint v4, ulong hi, ulong lo) + { + public readonly ParsedIPAddressFamily Family = family; + public readonly uint V4 = v4; + public readonly ulong Hi = hi; + public readonly ulong Lo = lo; + } + + /// + /// Returns true for loopback, private, link-local, CGNAT, and IPv6 ULA addresses. + /// + public static bool IsLoopbackOrPrivateOrLinkLocal(IPAddress ipAddress) + => IsLoopbackOrPrivateOrLinkLocal(Parse(ipAddress)); + + /// + /// Returns true for IPv4 or IPv6 multicast addresses. + /// + public static bool IsMulticast(IPAddress ipAddress) + { + ParsedIPAddress parsed = Parse(ipAddress); + return parsed.Family == ParsedIPAddressFamily.IPv4 + ? IsIPv4Multicast(parsed.V4) + : IsIPv6Multicast(parsed.Hi); + } + + /// + /// Returns true for IPv4 multicast addresses. + /// + public static bool IsIPv4Multicast(IPAddress ipAddress) + { + ParsedIPAddress parsed = Parse(ipAddress); + return parsed.Family == ParsedIPAddressFamily.IPv4 && IsIPv4Multicast(parsed.V4); + } + + /// + /// Returns true for special-use addresses that should not be accepted as routable peers. + /// + /// + /// This intentionally does not include loopback, private, link-local, CGNAT, or IPv6 ULA ranges; + /// callers that support private deployments can decide whether to accept those separately. + /// + public static bool IsSpecialUseAddress(IPAddress ipAddress) + { + ParsedIPAddress parsed = Parse(ipAddress); + return parsed.Family == ParsedIPAddressFamily.IPv4 + ? IsIPv4SpecialUseAddress(parsed.V4) + : IsIPv6SpecialUseAddress(parsed.Hi, parsed.Lo); + } + + internal static ParsedIPAddress Parse(IPAddress ipAddress) + { + Span bytes = stackalloc byte[16]; + if (!ipAddress.TryWriteBytes(bytes, out int written)) + { + throw new ArgumentException("Invalid IPAddress.", nameof(ipAddress)); + } + + switch (written) + { + case 4: + return new ParsedIPAddress( + ParsedIPAddressFamily.IPv4, + BinaryPrimitives.ReadUInt32BigEndian(bytes), + hi: 0, + lo: 0); + case 16: + { + ulong hi = BinaryPrimitives.ReadUInt64BigEndian(bytes); + + // Fast-path IPv4-mapped IPv6 (::ffff:a.b.c.d) - treat as IPv4. + if (hi == 0) + { + uint mid = BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(8, 4)); + if (mid == 0x0000_FFFFu) + { + return new ParsedIPAddress( + ParsedIPAddressFamily.IPv4, + BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(12, 4)), + hi: 0, + lo: 0); + } + } + + return new ParsedIPAddress( + ParsedIPAddressFamily.IPv6, + v4: 0, + hi, + BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8))); + } + default: + throw new ArgumentException("Unsupported address length.", nameof(ipAddress)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsLoopbackOrPrivateOrLinkLocal(ParsedIPAddress parsed) + => parsed.Family == ParsedIPAddressFamily.IPv4 + ? IsIPv4LoopbackOrPrivateOrLinkLocal(parsed.V4) + : IsIPv6LoopbackOrPrivateOrLinkLocal(parsed.Hi, parsed.Lo); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsIPv4Multicast(uint v4) + => (byte)(v4 >> 24) is >= 224 and <= 239; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsIPv4LoopbackOrPrivateOrLinkLocal(uint v4) + { + byte a = (byte)(v4 >> 24); + byte b = (byte)(v4 >> 16); + + return a == 127 // Loopback: 127.0.0.0/8 + || a == 10 // RFC1918: 10.0.0.0/8 + || a == 172 && (uint)(b - 16) <= 15u // RFC1918: 172.16.0.0/12 + || a == 192 && b == 168 // RFC1918: 192.168.0.0/16 + || a == 169 && b == 254 // IPv4 link-local: 169.254.0.0/16 + || a == 100 && (b & 0xC0) == 0x40; // CGNAT: 100.64.0.0/10 + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsIPv6LoopbackOrPrivateOrLinkLocal(ulong hi, ulong lo) + { + if (hi == 0 && lo == 1) + { + return true; + } + + byte first = (byte)(hi >> 56); + byte second = (byte)(hi >> 48); + + return (first & 0xFE) == 0xFC // ULA: fc00::/7 + || first == 0xFE && (second & 0xC0) == 0x80; // IPv6 link-local: fe80::/10 + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIPv4SpecialUseAddress(uint v4) + { + byte a = (byte)(v4 >> 24); + byte b = (byte)(v4 >> 16); + byte c = (byte)(v4 >> 8); + + return a == 0 // 0.0.0.0/8 + || a == 192 && b == 0 && c is 0 or 2 // 192.0.0.0/24, 192.0.2.0/24 + || a == 192 && b == 31 && c == 196 // 192.31.196.0/24 + || a == 192 && b == 52 && c == 193 // 192.52.193.0/24 + || a == 192 && b == 88 && c == 99 // 192.88.99.0/24 + || a == 192 && b == 175 && c == 48 // 192.175.48.0/24 + || a == 198 && b is 18 or 19 // 198.18.0.0/15 + || a == 198 && b == 51 && c == 100 // 198.51.100.0/24 + || a == 203 && b == 0 && c == 113 // 203.0.113.0/24 + || a >= 224; // 224.0.0.0/4, 240.0.0.0/4 + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIPv6SpecialUseAddress(ulong hi, ulong lo) + { + byte b0 = (byte)(hi >> 56); + byte b1 = (byte)(hi >> 48); + byte b2 = (byte)(hi >> 40); + byte b3 = (byte)(hi >> 32); + byte b4 = (byte)(hi >> 24); + byte b5 = (byte)(hi >> 16); + + return b0 == 0x00 && b1 == 0x64 && b2 == 0xff && b3 == 0x9b && IsZeroFromByte4To11(hi, lo) // 64:ff9b::/96 + || b0 == 0x00 && b1 == 0x64 && b2 == 0xff && b3 == 0x9b && b4 == 0x00 && b5 == 0x01 // 64:ff9b:1::/48 + || b0 == 0x01 && (hi & 0x00FF_FFFF_FFFF_FFFFUL) == 0 // 100::/64 + || b0 == 0x20 && b1 == 0x01 && (b2 & 0xfe) == 0x00 // 2001::/23 + || b0 == 0x20 && b1 == 0x01 && b2 == 0x0d && b3 == 0xb8 // 2001:db8::/32 + || b0 == 0x20 && b1 == 0x02 // 2002::/16 + || b0 == 0x3f && b1 == 0xff && (b2 & 0xf0) == 0x00; // 3fff::/20 + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIPv6Multicast(ulong hi) + => (byte)(hi >> 56) == 0xff; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsZeroFromByte4To11(ulong hi, ulong lo) + => (hi & 0x0000_0000_FFFF_FFFFUL) == 0 + && (lo & 0xFFFF_FFFF_0000_0000UL) == 0; +} diff --git a/src/Nethermind/Nethermind.Network/Metrics.cs b/src/Nethermind/Nethermind.Network/Metrics.cs index 6cd555d27095..9208aa732312 100644 --- a/src/Nethermind/Nethermind.Network/Metrics.cs +++ b/src/Nethermind/Nethermind.Network/Metrics.cs @@ -5,7 +5,7 @@ using System.ComponentModel; using System.Runtime.Serialization; using Nethermind.Core.Attributes; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.P2P; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network/NodeFilter.cs b/src/Nethermind/Nethermind.Network/NodeFilter.cs index f9d69c1ef0d3..f5067b1d55b3 100644 --- a/src/Nethermind/Nethermind.Network/NodeFilter.cs +++ b/src/Nethermind/Nethermind.Network/NodeFilter.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Buffers.Binary; using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -55,13 +54,10 @@ public static NodeFilter CreateExact(int size, TimeSpan timeout) => new(size, exactMatchOnly: true, currentIp: null, (long)timeout.TotalMilliseconds); public static bool IsLoopbackOrPrivateOrLinkLocal(IPAddress ipAddress) - => IpSubnetKey.IsLoopbackOrPrivateOrLinkLocal(ipAddress); + => IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(ipAddress); public static bool IsIPv4Multicast(IPAddress ipAddress) - { - byte[] bytes = ipAddress.GetAddressBytes(); - return bytes.Length == 4 && bytes[0] is >= 224 and <= 239; - } + => IPAddressClassifier.IsIPv4Multicast(ipAddress); /// /// Checks whether should be accepted. @@ -336,41 +332,11 @@ private static ushort MakeMeta(IpFamily family, byte prefixBits) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IpFamily ReadAddress(IPAddress ip, out uint v4, out ulong hi, out ulong lo) { - Span bytes = stackalloc byte[16]; - if (!ip.TryWriteBytes(bytes, out int written)) - throw new ArgumentException("Invalid IPAddress.", nameof(ip)); - - switch (written) - { - case 4: - v4 = BinaryPrimitives.ReadUInt32BigEndian(bytes); - hi = 0; - lo = 0; - return IpFamily.IPv4; - case 16: - { - hi = BinaryPrimitives.ReadUInt64BigEndian(bytes); - - // Fast-path IPv4-mapped IPv6 (::ffff:a.b.c.d) - treat as IPv4. - if (hi == 0) - { - uint mid = BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(8, 4)); - if (mid == 0x0000_FFFFu) - { - v4 = BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(12, 4)); - hi = 0; - lo = 0; - return IpFamily.IPv4; - } - } - - v4 = 0; - lo = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8)); - return IpFamily.IPv6; - } - default: - throw new ArgumentException("Unsupported address length.", nameof(ip)); - } + IPAddressClassifier.ParsedIPAddress parsed = IPAddressClassifier.Parse(ip); + v4 = parsed.V4; + hi = parsed.Hi; + lo = parsed.Lo; + return parsed.Family == IPAddressClassifier.ParsedIPAddressFamily.IPv4 ? IpFamily.IPv4 : IpFamily.IPv6; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -416,29 +382,8 @@ private static void MaskV6Trusted(ref ulong hi, ref ulong lo, byte prefixBits) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsLoopbackOrPrivateOrLinkLocal(IpFamily family, uint v4, ulong hi, ulong lo) - { - if (family == IpFamily.IPv4) - { - byte a = (byte)(v4 >> 24); - byte b = (byte)(v4 >> 16); - - return a == 127 // Loopback: 127.0.0.0/8 - || a == 10 // RFC1918: 10.0.0.0/8 - || a == 172 && (uint)(b - 16) <= 15u // RFC1918: 172.16.0.0/12 - || a == 192 && b == 168 // RFC1918: 192.168.0.0/16 - || a == 169 && b == 254 // IPv4 link-local: 169.254.0.0/16 - || a == 100 && (b & 0xC0) == 0x40; // CGNAT: 100.64.0.0/10 - } - - // IPv6 loopback: ::1 - if (hi == 0 && lo == 1) - return true; - - byte first = (byte)(hi >> 56); - byte second = (byte)(hi >> 48); - - return (first & 0xFE) == 0xFC // ULA: fc00::/7 - || first == 0xFE && (second & 0xC0) == 0x80; // IPv6 link-local: fe80::/10 - } + => family == IpFamily.IPv4 + ? IPAddressClassifier.IsIPv4LoopbackOrPrivateOrLinkLocal(v4) + : IPAddressClassifier.IsIPv6LoopbackOrPrivateOrLinkLocal(hi, lo); } } diff --git a/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs b/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs index bb313d952687..0dd492de3439 100644 --- a/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs +++ b/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs @@ -22,6 +22,7 @@ using Nethermind.Network.Config; using Nethermind.Network.Contract.P2P; using Nethermind.Network.Discovery; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.P2P; using Nethermind.Network.P2P.Messages; using Nethermind.Network.P2P.Subprotocols.Eth.V71; diff --git a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj index bdf774b26166..46e1faaf6280 100644 --- a/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj +++ b/src/Nethermind/Nethermind.Runner/Nethermind.Runner.csproj @@ -64,6 +64,7 @@ + diff --git a/src/Nethermind/Nethermind.Runner/Program.cs b/src/Nethermind/Nethermind.Runner/Program.cs index 2b4b35fbdd68..3548b38b0b25 100644 --- a/src/Nethermind/Nethermind.Runner/Program.cs +++ b/src/Nethermind/Nethermind.Runner/Program.cs @@ -24,6 +24,7 @@ using Nethermind.Init.Snapshot; using Nethermind.KeyStore.Config; using Nethermind.Logging; +using Nethermind.Logging.Microsoft; using Nethermind.Logging.NLog; using Nethermind.Runner; using Nethermind.Runner.Ethereum; @@ -38,7 +39,6 @@ using NullLogger = Nethermind.Logging.NullLogger; using DotNettyLoggerFactory = DotNetty.Common.Internal.Logging.InternalLoggerFactory; using Testably.Abstractions; -using Nethermind.Network.Discovery.Discv5; #if !DEBUG using DotNettyLeakDetector = DotNetty.Common.ResourceLeakDetector; #endif diff --git a/src/Nethermind/Nethermind.Runner/packages.lock.json b/src/Nethermind/Nethermind.Runner/packages.lock.json index 693dadf1440d..c839ff077656 100644 --- a/src/Nethermind/Nethermind.Runner/packages.lock.json +++ b/src/Nethermind/Nethermind.Runner/packages.lock.json @@ -731,7 +731,6 @@ "Nethermind.Consensus": "[1.39.0-unstable, )", "Nethermind.Core": "[1.39.0-unstable, )", "Nethermind.JsonRpc": "[1.39.0-unstable, )", - "Nethermind.Merkleization": "[1.39.0-unstable, )", "Nethermind.Serialization.Rlp": "[1.39.0-unstable, )", "Nethermind.Serialization.Ssz": "[1.39.0-unstable, )", "Nethermind.State": "[1.39.0-unstable, )", @@ -749,7 +748,6 @@ "Nethermind.Era1": "[1.39.0-unstable, )", "Nethermind.History": "[1.39.0-unstable, )", "Nethermind.JsonRpc": "[1.39.0-unstable, )", - "Nethermind.Merkleization": "[1.39.0-unstable, )", "Nethermind.Serialization.Rlp": "[1.39.0-unstable, )", "Nethermind.Serialization.Ssz": "[1.39.0-unstable, )", "Nethermind.State": "[1.39.0-unstable, )", @@ -904,7 +902,6 @@ "nethermind.kademlia": { "type": "Project", "dependencies": { - "Nethermind.Core": "[1.39.0-unstable, )", "Nethermind.Logging": "[1.39.0-unstable, )", "NonBlocking": "[2.1.2, )" } @@ -922,6 +919,12 @@ "nethermind.logging": { "type": "Project" }, + "nethermind.logging.microsoft": { + "type": "Project", + "dependencies": { + "Nethermind.Logging": "[1.39.0-unstable, )" + } + }, "nethermind.logging.nlog": { "type": "Project", "dependencies": { @@ -947,16 +950,9 @@ "type": "Project", "dependencies": { "Nethermind.Api": "[1.39.0-unstable, )", - "Nethermind.Merkleization": "[1.39.0-unstable, )", "Nethermind.Serialization.Ssz": "[1.39.0-unstable, )" } }, - "nethermind.merkleization": { - "type": "Project", - "dependencies": { - "Nethermind.Numerics.Int256": "[1.5.0, )" - } - }, "nethermind.monitoring": { "type": "Project", "dependencies": { @@ -1087,8 +1083,8 @@ "Nethermind.Init": "[1.39.0-unstable, )", "Nethermind.Libp2p": "[1.0.0-preview.45, )", "Nethermind.Libp2p.Protocols.PubsubPeerDiscovery": "[1.0.0-preview.45, )", + "Nethermind.Logging.Microsoft": "[1.39.0-unstable, )", "Nethermind.Merge.Plugin": "[1.39.0-unstable, )", - "Nethermind.Merkleization": "[1.39.0-unstable, )", "Nethermind.Network.Discovery": "[1.39.0-unstable, )", "Nethermind.Serialization.Ssz": "[1.39.0-unstable, )", "Nethermind.Specs": "[1.39.0-unstable, )" diff --git a/src/Nethermind/Nethermind.Shutter/Nethermind.Shutter.csproj b/src/Nethermind/Nethermind.Shutter/Nethermind.Shutter.csproj index 6a548d6660a3..ccb26df5ecf6 100644 --- a/src/Nethermind/Nethermind.Shutter/Nethermind.Shutter.csproj +++ b/src/Nethermind/Nethermind.Shutter/Nethermind.Shutter.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Nethermind/Nethermind.Shutter/ShutterP2P.cs b/src/Nethermind/Nethermind.Shutter/ShutterP2P.cs index 6f96e73234a7..f55e8e2537cd 100644 --- a/src/Nethermind/Nethermind.Shutter/ShutterP2P.cs +++ b/src/Nethermind/Nethermind.Shutter/ShutterP2P.cs @@ -22,8 +22,8 @@ using System.Net; using Microsoft.Extensions.Logging; using Nethermind.Core; +using Nethermind.Logging.Microsoft; using System.Collections.Generic; -using Nethermind.Network.Discovery.Discv5; namespace Nethermind.Shutter; diff --git a/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs b/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs index 1cf6879650a1..f3252a82666e 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs @@ -8,7 +8,8 @@ using Nethermind.Crypto; using Nethermind.Network; using Nethermind.Network.Discovery; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Test; using Nethermind.Xdc.Discovery; using NSubstitute; diff --git a/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs b/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs index 893ff1f50345..eb921f78342d 100644 --- a/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs @@ -8,6 +8,7 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery; +using Nethermind.Network.Discovery.Discv4; namespace Nethermind.Xdc.Discovery; diff --git a/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs index 482610066118..e55f29c9d063 100644 --- a/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs @@ -6,7 +6,8 @@ using Nethermind.Logging; using Nethermind.Network; using Nethermind.Network.Discovery; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Messages; namespace Nethermind.Xdc.Discovery; diff --git a/src/Nethermind/Nethermind.Xdc/Discovery/XdcPingMsgSerializer.cs b/src/Nethermind/Nethermind.Xdc/Discovery/XdcPingMsgSerializer.cs index 09caeac25854..969ee6e0ba11 100644 --- a/src/Nethermind/Nethermind.Xdc/Discovery/XdcPingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Xdc/Discovery/XdcPingMsgSerializer.cs @@ -3,8 +3,8 @@ using Autofac.Features.AttributeFilters; using Nethermind.Crypto; -using Nethermind.Network.Discovery.Messages; -using Nethermind.Network.Discovery.Serializers; +using Nethermind.Network.Discovery.Discv4.Messages; +using Nethermind.Network.Discovery.Discv4.Serializers; namespace Nethermind.Xdc.Discovery; diff --git a/src/Nethermind/Nethermind.Xdc/XdcModule.cs b/src/Nethermind/Nethermind.Xdc/XdcModule.cs index 870773b11b2d..ce01ac59266a 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcModule.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcModule.cs @@ -21,7 +21,8 @@ using Nethermind.Init.Modules; using Nethermind.Network; using Nethermind.Network.Discovery; -using Nethermind.Network.Discovery.Messages; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; using Nethermind.Specs.ChainSpecStyle; using Nethermind.Synchronization; From c644dd4161f7003e868d97405494c74ee556240b Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 1 Jun 2026 17:20:24 +0300 Subject: [PATCH 127/182] Rename --- .../{Discv5CodecTests.cs => CodecTests.cs} | 146 ++-- .../Handlers/NodesResponseHandlerTests.cs | 8 +- ...dapterTests.cs => KademliaAdapterTests.cs} | 18 +- ...5NodeSourceTests.cs => NodeSourceTests.cs} | 4 +- .../{Discv5WireTests.cs => WireTests.cs} | 9 +- ...{Discv5AdapterState.cs => AdapterState.cs} | 9 +- .../Discv5/DiscoveryV5App.cs | 34 +- .../Discv5/Discv5PacketCodec.cs | 563 --------------- .../Discv5/Handlers/IResponseHandler.cs | 6 +- .../Discv5/Handlers/NodesResponseHandler.cs | 10 +- .../Discv5/Handlers/PongResponseHandler.cs | 4 +- ...KademliaAdapter.cs => IKademliaAdapter.cs} | 2 +- ...5KademliaAdapter.cs => KademliaAdapter.cs} | 140 ++-- ...cV5KademliaModule.cs => KademliaModule.cs} | 11 +- ...{Discv5MessageCodec.cs => MessageCodec.cs} | 96 +-- .../Discv5/Messages/Discv5FindNode.cs | 22 - .../Discv5/Messages/Discv5Message.cs | 24 +- .../Discv5/Messages/Discv5MessageBuffer.cs | 26 - .../Discv5/Messages/Discv5Nodes.cs | 27 - .../Discv5/Messages/Discv5Ping.cs | 20 - .../Discv5/Messages/Discv5Pong.cs | 30 - .../Discv5/Messages/Discv5TalkReq.cs | 25 - .../Discv5/Messages/Discv5TalkResp.cs | 20 - .../{Discv5Distances.cs => Distances.cs} | 6 +- .../Discv5/Messages/FindNodeMsg.cs | 24 + .../{Discv5MessageType.cs => MessageType.cs} | 2 +- .../Discv5/Messages/NodesMsg.cs | 28 + .../Discv5/Messages/PingMsg.cs | 22 + .../Discv5/Messages/PongMsg.cs | 31 + .../{Discv5RequestId.cs => RequestId.cs} | 6 +- .../Discv5/Messages/TalkReqMsg.cs | 31 + .../Discv5/Messages/TalkRespMsg.cs | 24 + ...ordConverter.cs => NodeRecordConverter.cs} | 2 +- .../{Discv5NodeSource.cs => NodeSource.cs} | 4 +- .../Discv5/Packets/Challenge.cs | 6 + .../Discv5/Packets/Packet.cs | 39 ++ .../Discv5/Packets/PacketCodec.cs | 651 ++++++++++++++++++ .../Discv5/Packets/PacketFlag.cs | 11 + .../Discv5/Packets/Session.cs | 24 + .../KademliaDiscoveryApp.cs | 5 +- 40 files changed, 1181 insertions(+), 989 deletions(-) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/{Discv5CodecTests.cs => CodecTests.cs} (66%) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/{Discv5KademliaAdapterTests.cs => KademliaAdapterTests.cs} (89%) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/{Discv5NodeSourceTests.cs => NodeSourceTests.cs} (96%) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/{Discv5WireTests.cs => WireTests.cs} (98%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{Discv5AdapterState.cs => AdapterState.cs} (70%) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{IDiscv5KademliaAdapter.cs => IKademliaAdapter.cs} (88%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{Discv5KademliaAdapter.cs => KademliaAdapter.cs} (82%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{DiscV5KademliaModule.cs => KademliaModule.cs} (71%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{Discv5MessageCodec.cs => MessageCodec.cs} (74%) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs rename src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/{Discv5Distances.cs => Distances.cs} (92%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs rename src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/{Discv5MessageType.cs => MessageType.cs} (87%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs rename src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/{Discv5RequestId.cs => RequestId.cs} (82%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{Discv5NodeRecordConverter.cs => NodeRecordConverter.cs} (97%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{Discv5NodeSource.cs => NodeSource.cs} (98%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Packet.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketFlag.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs similarity index 66% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 20192bc0ec14..405f472f5671 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -8,6 +8,7 @@ using Nethermind.Crypto; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; using NUnit.Framework; @@ -16,7 +17,7 @@ namespace Nethermind.Network.Discovery.Test.Discv5; -public class Discv5CodecTests +public class CodecTests { private static readonly byte[] NodeAId = Bytes.FromHexString("0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb"); private static readonly byte[] NodeBId = Bytes.FromHexString("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9"); @@ -42,7 +43,7 @@ public void KeyDerivation_Matches_Devp2p_Vector() byte[] secret = SecP256k1Agreement.AgreeCompressed(destinationPublicKey, ephemeralPrivateKey); byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); - (byte[] initiatorKey, byte[] recipientKey) = Discv5PacketCodec.DeriveKeysForTest(secret, NodeAId, NodeBId, challengeData); + (byte[] initiatorKey, byte[] recipientKey) = PacketCodec.DeriveKeysForTest(secret, NodeAId, NodeBId, challengeData); Assert.That(initiatorKey.ToHexString(true), Is.EqualTo("0xdccc82d81bd610f4f76d3ebe97a40571")); Assert.That(recipientKey.ToHexString(true), Is.EqualTo("0xac74bb8773749920b0d3a8881c173ec5")); @@ -54,7 +55,7 @@ public void IdNonceSignature_Matches_Devp2p_Vector() PrivateKey staticKey = new("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736"); byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); byte[] ephemeralPublicKey = Bytes.FromHexString("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231"); - byte[] signingHash = Discv5PacketCodec.CalculateIdSignatureHashForTest(challengeData, ephemeralPublicKey, NodeBId); + byte[] signingHash = PacketCodec.CalculateIdSignatureHashForTest(challengeData, ephemeralPublicKey, NodeBId); Signature signature = new Ecdsa().Sign(staticKey, new ValueHash256(signingHash)); @@ -69,18 +70,21 @@ public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() "ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + "4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); - bool decoded = Discv5PacketCodec.TryDecode(packetBytes, NodeBId, out Discv5Packet packet); - bool decrypted = Discv5PacketCodec.TryDecryptMessageForTest(packet, new byte[16], out Discv5Message message); - - Assert.That(decoded, Is.True); - Assert.That(packet.Flag, Is.EqualTo(Discv5PacketFlag.Ordinary)); - Assert.That(packet.AuthData, Is.EqualTo(NodeAId)); - Assert.That(decrypted, Is.True); - Assert.That(message, Is.InstanceOf()); - Discv5Ping ping = (Discv5Ping)message; - Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); - Assert.That(ping.EnrSequence, Is.EqualTo(2)); - message.Dispose(); + bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); + using (packet) + { + bool decrypted = PacketCodec.TryDecryptMessageForTest(packet, new byte[16], out Discv5Message message); + + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(PacketFlag.Ordinary)); + Assert.That(packet.AuthData.ToArray(), Is.EqualTo(NodeAId)); + Assert.That(decrypted, Is.True); + Assert.That(message, Is.InstanceOf()); + PingMsg ping = (PingMsg)message; + Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + Assert.That(ping.EnrSequence, Is.EqualTo(2)); + message.Dispose(); + } } [Test] @@ -91,16 +95,19 @@ public void PacketCodec_Decodes_WhoAreYou_GoEthereum_Vector() "1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d"); byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); - bool decoded = Discv5PacketCodec.TryDecode(packetBytes, NodeBId, out Discv5Packet packet); - Discv5PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); - Discv5Challenge challenge = codec.DecodeWhoAreYou(packet); - - Assert.That(decoded, Is.True); - Assert.That(packet.Flag, Is.EqualTo(Discv5PacketFlag.WhoAreYou)); - Assert.That(challenge.RequestNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c")); - Assert.That(challenge.IdNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c0d0e0f10")); - Assert.That(challenge.EnrSequence, Is.Zero); - Assert.That(challenge.ChallengeData, Is.EqualTo(challengeData)); + bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); + using (packet) + { + PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + Challenge challenge = codec.DecodeWhoAreYou(packet); + + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(PacketFlag.WhoAreYou)); + Assert.That(challenge.RequestNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c")); + Assert.That(challenge.IdNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c0d0e0f10")); + Assert.That(challenge.EnrSequence, Is.Zero); + Assert.That(challenge.ChallengeData, Is.EqualTo(challengeData)); + } } [TestCase( @@ -139,39 +146,42 @@ public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( bool includesRecord) { byte[] packetBytes = Bytes.FromHexString(packetHex); - Discv5Challenge challenge = new( + Challenge challenge = new( Bytes.FromHexString("0x0102030405060708090a0b0c"), Bytes.FromHexString("0x0102030405060708090a0b0c0d0e0f10"), challengeEnrSequence, Bytes.FromHexString(challengeDataHex)); - Discv5PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); NodeRecord? knownRecord = includesRecord ? null : CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); - bool decoded = Discv5PacketCodec.TryDecode(packetBytes, NodeBId, out Discv5Packet packet); - bool decrypted = codec.TryDecryptHandshake(packet, challenge, knownRecord, out Discv5Session session, out Discv5Message message, out NodeRecord? nodeRecord); - - Assert.That(decoded, Is.True); - Assert.That(packet.Flag, Is.EqualTo(Discv5PacketFlag.Handshake)); - Assert.That(decrypted, Is.True); - Assert.That(session.ReadKey.ToHexString(true), Is.EqualTo(expectedReadKeyHex)); - Assert.That(message, Is.InstanceOf()); - Discv5Ping ping = (Discv5Ping)message; - Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); - Assert.That(ping.EnrSequence, Is.EqualTo(1)); - Assert.That(nodeRecord is not null, Is.EqualTo(includesRecord)); - message.Dispose(); + bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); + using (packet) + { + bool decrypted = codec.TryDecryptHandshake(packet, challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord); + + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(PacketFlag.Handshake)); + Assert.That(decrypted, Is.True); + Assert.That(session.ReadKey.ToHexString(true), Is.EqualTo(expectedReadKeyHex)); + Assert.That(message, Is.InstanceOf()); + PingMsg ping = (PingMsg)message; + Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + Assert.That(ping.EnrSequence, Is.EqualTo(1)); + Assert.That(nodeRecord is not null, Is.EqualTo(includesRecord)); + message.Dispose(); + } } [Test] public void MessageCodec_Roundtrips_FindNode() { - using Discv5FindNode message = new([0, 0, 0, 1], [255, 254, 256]); + using FindNodeMsg message = new([0, 0, 0, 1], [255, 254, 256]); - using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); - using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); + using ArrayPoolSpan encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded); - Assert.That(decoded, Is.InstanceOf()); - Discv5FindNode decodedFindNode = (Discv5FindNode)decoded; + Assert.That(decoded, Is.InstanceOf()); + FindNodeMsg decodedFindNode = (FindNodeMsg)decoded; Assert.That(decodedFindNode.RequestId, Is.EqualTo(message.RequestId)); Assert.That(decodedFindNode.Distances, Is.EqualTo(message.Distances)); } @@ -179,13 +189,13 @@ public void MessageCodec_Roundtrips_FindNode() [Test] public void MessageCodec_Roundtrips_Pong() { - using Discv5Pong message = new([0, 0, 0, 2], 3, IPAddress.Parse("192.0.2.1"), 30303); + using PongMsg message = new([0, 0, 0, 2], 3, IPAddress.Parse("192.0.2.1"), 30303); - using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); - using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); + using ArrayPoolSpan encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded); - Assert.That(decoded, Is.InstanceOf()); - Discv5Pong decodedPong = (Discv5Pong)decoded; + Assert.That(decoded, Is.InstanceOf()); + PongMsg decodedPong = (PongMsg)decoded; Assert.That(decodedPong.RequestId, Is.EqualTo(message.RequestId)); Assert.That(decodedPong.EnrSequence, Is.EqualTo(message.EnrSequence)); Assert.That(decodedPong.RecipientIp, Is.EqualTo(message.RecipientIp)); @@ -195,13 +205,13 @@ public void MessageCodec_Roundtrips_Pong() [Test] public void MessageCodec_Roundtrips_TalkReq() { - using Discv5TalkReq message = new([0, 0, 0, 3], "eth"u8.ToArray(), new byte[] { 1, 2, 3, 4 }); + using TalkReqMsg message = new([0, 0, 0, 3], "eth"u8.ToArray(), new byte[] { 1, 2, 3, 4 }); - using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); - using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); + using ArrayPoolSpan encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded); - Assert.That(decoded, Is.InstanceOf()); - Discv5TalkReq decodedTalkReq = (Discv5TalkReq)decoded; + Assert.That(decoded, Is.InstanceOf()); + TalkReqMsg decodedTalkReq = (TalkReqMsg)decoded; Assert.That(decodedTalkReq.RequestId, Is.EqualTo(message.RequestId)); Assert.That(decodedTalkReq.Protocol.ToArray(), Is.EqualTo(message.Protocol.ToArray())); Assert.That(decodedTalkReq.Request.ToArray(), Is.EqualTo(message.Request.ToArray())); @@ -210,13 +220,13 @@ public void MessageCodec_Roundtrips_TalkReq() [Test] public void MessageCodec_Roundtrips_TalkResp() { - using Discv5TalkResp message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); + using TalkRespMsg message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); - using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); - using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); + using ArrayPoolSpan encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded); - Assert.That(decoded, Is.InstanceOf()); - Discv5TalkResp decodedTalkResp = (Discv5TalkResp)decoded; + Assert.That(decoded, Is.InstanceOf()); + TalkRespMsg decodedTalkResp = (TalkRespMsg)decoded; Assert.That(decodedTalkResp.RequestId, Is.EqualTo(message.RequestId)); Assert.That(decodedTalkResp.Response.ToArray(), Is.EqualTo(message.Response.ToArray())); } @@ -227,13 +237,13 @@ public void MessageCodec_Roundtrips_Nodes_From_NonZero_ArraySegment() NodeRecord skippedRecord = CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); NodeRecord expectedRecord = CreateNodeRecord(new PrivateKey(GethNodeBPrivateKey)); NodeRecord[] records = [skippedRecord, expectedRecord]; - using Discv5Nodes message = new([0, 0, 0, 5], 1, new ArraySegment(records, 1, 1)); + using NodesMsg message = new([0, 0, 0, 5], 1, new ArraySegment(records, 1, 1)); - using ArrayPoolSpan encoded = Discv5MessageCodec.Encode(message); - using Discv5Message decoded = Discv5MessageCodec.Decode(encoded); + using ArrayPoolSpan encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded); - Assert.That(decoded, Is.InstanceOf()); - Discv5Nodes decodedNodes = (Discv5Nodes)decoded; + Assert.That(decoded, Is.InstanceOf()); + NodesMsg decodedNodes = (NodesMsg)decoded; Assert.That(decodedNodes.RequestId, Is.EqualTo(message.RequestId)); Assert.That(decodedNodes.Total, Is.EqualTo(message.Total)); Assert.That(decodedNodes.Records.Count, Is.EqualTo(1)); @@ -253,13 +263,13 @@ public void MessageCodec_Rejects_Nodes_With_Invalid_Enr() Rlp.Encode(1), Rlp.Encode(new Rlp(invalidRecord))); byte[] message = new byte[data.Length + 1]; - message[0] = (byte)Discv5MessageType.Nodes; + message[0] = (byte)MessageType.Nodes; data.Bytes.CopyTo(message.AsSpan(1)); - Assert.That(() => Discv5MessageCodec.Decode(message), Throws.TypeOf()); + Assert.That(() => MessageCodec.Decode(message), Throws.TypeOf()); } - private static Discv5PacketCodec CreateCodec(PrivateKey privateKey) + private static PacketCodec CreateCodec(PrivateKey privateKey) => new( new InsecureProtectedPrivateKey(privateKey), new TestNodeRecordProvider(privateKey), diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index f55fe1e9497a..cd4cbfe62497 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -24,7 +24,7 @@ public void ShouldRejectNonRoutableRecordFromPublicReceiver() NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); - using Discv5Nodes nodes = new([1], 1, [loopbackRecord]); + using NodesMsg nodes = new([1], 1, [loopbackRecord]); handler.Handle(nodes); Assert.That(handler.GetNodes(), Is.Empty); @@ -37,7 +37,7 @@ public void ShouldAcceptNonRoutableRecordFromNonRoutableReceiver() NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); - using Discv5Nodes nodes = new([1], 1, [loopbackRecord]); + using NodesMsg nodes = new([1], 1, [loopbackRecord]); handler.Handle(nodes); Assert.That(handler.GetNodes(), Has.Length.EqualTo(1)); @@ -50,7 +50,7 @@ public void ShouldRejectSpecialUseRecordFromNonRoutableReceiver() NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); NodesResponseHandler handler = CreateNodesResponseHandler(receiver, documentationRecord); - using Discv5Nodes nodes = new([1], 1, [documentationRecord]); + using NodesMsg nodes = new([1], 1, [documentationRecord]); handler.Handle(nodes); Assert.That(handler.GetNodes(), Is.Empty); @@ -72,6 +72,6 @@ private static NodesResponseHandler CreateNodesResponseHandler(Node receiver, No { PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); - return new NodesResponseHandler(receiver, new Discv5Distances([distance]), Hash256KademliaDistance.Instance); + return new NodesResponseHandler(receiver, new Distances([distance]), Hash256KademliaDistance.Instance); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs similarity index 89% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 587ec04ce14a..4c19d571e4f4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -17,7 +17,7 @@ namespace Nethermind.Network.Discovery.Test.Discv5; -public class Discv5KademliaAdapterTests +public class KademliaAdapterTests { private IKademlia _kademlia = null!; @@ -35,7 +35,7 @@ public void GetNodesAtDistances_ShouldMapEachDistanceToKademliaTable() _kademlia.GetAllAtDistance(11).Returns([nodeB, nodeC]); _kademlia.ClearReceivedCalls(); - Discv5KademliaAdapter adapter = CreateAdapter(); + KademliaAdapter adapter = CreateAdapter(); Node[] result = adapter.GetNodesAtDistances([10, 11]); @@ -52,7 +52,7 @@ public void GetNodesAtDistances_ShouldExcludeRequester() _kademlia.GetAllAtDistance(10).Returns([requester, returned]); - Discv5KademliaAdapter adapter = CreateAdapter(); + KademliaAdapter adapter = CreateAdapter(); Node[] result = adapter.GetNodesAtDistances([10], requester); @@ -63,7 +63,7 @@ public void GetNodesAtDistances_ShouldExcludeRequester() [TestCase(257)] public void GetNodesAtDistances_ShouldRejectInvalidDistance(int distance) { - Discv5KademliaAdapter adapter = CreateAdapter(); + KademliaAdapter adapter = CreateAdapter(); Assert.Throws(() => adapter.GetNodesAtDistances([distance])); } @@ -71,7 +71,7 @@ public void GetNodesAtDistances_ShouldRejectInvalidDistance(int distance) [Test] public void TryAcceptChallenge_ShouldLimitBurstPerIp() { - Discv5KademliaAdapter adapter = CreateAdapter(); + KademliaAdapter adapter = CreateAdapter(); IPEndPoint endpoint = IPEndPoint.Parse("192.0.2.1:30303"); for (int i = 0; i < 16; i++) @@ -88,7 +88,7 @@ public void IsAcceptableNodeRecord_ShouldRejectSpecialUseRecord() NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); Assert.That( - Discv5KademliaAdapter.IsAcceptableNodeRecord( + KademliaAdapter.IsAcceptableNodeRecord( documentationRecord, TestItem.PrivateKeyB.PublicKey.Hash, allowNonRoutable: true), @@ -101,7 +101,7 @@ public void IsAcceptableNodeRecord_ShouldRejectNodeIdMismatch() NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.8.8")); Assert.That( - Discv5KademliaAdapter.IsAcceptableNodeRecord( + KademliaAdapter.IsAcceptableNodeRecord( record, TestItem.PrivateKeyA.PublicKey.Hash, allowNonRoutable: false), @@ -114,14 +114,14 @@ public void IsAcceptableNodeRecord_ShouldAllowNonRoutableWhenRequested() NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); Assert.That( - Discv5KademliaAdapter.IsAcceptableNodeRecord( + KademliaAdapter.IsAcceptableNodeRecord( loopbackRecord, TestItem.PrivateKeyB.PublicKey.Hash, allowNonRoutable: true), Is.True); } - private Discv5KademliaAdapter CreateAdapter() => new( + private KademliaAdapter CreateAdapter() => new( new Lazy>(_kademlia), null!, null!, diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs similarity index 96% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index f7e12414204b..920812ba2380 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -16,7 +16,7 @@ namespace Nethermind.Network.Discovery.Test.Discv5; -public class Discv5NodeSourceTests +public class NodeSourceTests { [Test] [CancelAfter(10000)] @@ -24,7 +24,7 @@ public async Task DiscoverNodes_ShouldNotRetainDroppedNodesInRecentDedupe(Cancel { IKademlia kademlia = Substitute.For>(); kademlia.IterateNodes().Returns(Array.Empty()); - Discv5NodeSource source = new( + NodeSource source = new( kademlia, new KademliaConfig { CurrentNodeId = CreateNode(0) }, LimboLogs.Instance); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index f4f0f24b6144..900e56ba3646 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Discv5WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -19,6 +19,7 @@ using Nethermind.Logging; using Nethermind.Network.Enr; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; @@ -27,7 +28,7 @@ namespace Nethermind.Network.Discovery.Test.Discv5; -public class Discv5WireTests +public class WireTests { [Test] public async Task Ping_Completes_After_WhoAreYou_Handshake() @@ -201,10 +202,10 @@ private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, b handler.InitializeChannel(channel); TestNodeRecordProvider nodeRecordProvider = new(privateKey, endpoint, includeEndpointInRecord); - Discv5KademliaAdapter adapter = new( + KademliaAdapter adapter = new( new Lazy>(kademlia), handler, - new Discv5PacketCodec( + new PacketCodec( new InsecureProtectedPrivateKey(privateKey), nodeRecordProvider, new CryptoRandom(), @@ -273,7 +274,7 @@ private static int[] GetLookupDistances(Node receiver, PublicKey target) } private sealed record TestPeer( - Discv5KademliaAdapter Adapter, + KademliaAdapter Adapter, NettyDiscoveryV5Handler Handler, EmbeddedChannel Channel, IKademlia Kademlia, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/AdapterState.cs similarity index 70% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/AdapterState.cs index 7a6b959074d6..49b41030fc19 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5AdapterState.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/AdapterState.cs @@ -5,6 +5,7 @@ using System.Net; using Nethermind.Core.Crypto; using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv5; @@ -15,15 +16,15 @@ namespace Nethermind.Network.Discovery.Discv5; internal readonly record struct PendingNonceKey(IPEndPoint Endpoint, NonceKey Nonce); -internal readonly record struct ResponseKey(Hash256 NodeId, Discv5RequestId RequestId, Discv5MessageType MessageType); +internal readonly record struct ResponseKey(Hash256 NodeId, RequestId RequestId, MessageType MessageType); internal readonly record struct NonceKey(ulong Prefix, uint Suffix) { public static NonceKey From(ReadOnlySpan nonce) { - if (nonce.Length != Discv5PacketCodec.NonceSize) + if (nonce.Length != PacketCodec.NonceSize) { - throw new ArgumentException($"Nonce must be {Discv5PacketCodec.NonceSize} bytes.", nameof(nonce)); + throw new ArgumentException($"Nonce must be {PacketCodec.NonceSize} bytes.", nameof(nonce)); } return new NonceKey( @@ -34,4 +35,4 @@ public static NonceKey From(ReadOnlySpan nonce) internal sealed record PendingRequest(Node Receiver, Discv5Message Message); -internal readonly record struct SentChallenge(Discv5Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds); +internal readonly record struct SentChallenge(Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index bc60c759be8e..ef01b3e539e4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -32,9 +32,9 @@ public sealed class DiscoveryV5App : KademliaDiscoveryApp private readonly IDb _discoveryDb; private readonly IDb _legacyDiscoveryDb; private readonly bool _allowNonRoutableEnrs; - private readonly IDiscv5KademliaAdapter _discv5Adapter; + private readonly IKademliaAdapter _adapter; private readonly Func _discoveryHandlerFactory; - private readonly ILifetimeScope _discv5Services; + private readonly ILifetimeScope _services; private NettyDiscoveryV5Handler? _discoveryHandler; @@ -48,7 +48,7 @@ public DiscoveryV5App( [KeyFilter(DbNames.DiscoveryNodes)] IDb legacyDiscoveryDb, IProcessExitSource processExitSource, ILogManager logManager, - Action? configureDiscv5Services = null) + Action? configureServices = null) : base("discv5", networkConfig, processExitSource, logManager.GetClassLogger()) { _discoveryDb = discoveryDb; @@ -58,26 +58,26 @@ public DiscoveryV5App( List bootNodes = CreateBootNodes(networkConfig, discoveryConfig); ITimestamper timestamper = rootScope.ResolveOptional() ?? Timestamper.Default; - _discv5Services = rootScope.BeginLifetimeScope(builder => + _services = rootScope.BeginLifetimeScope(builder => { builder.RegisterInstance(discoveryConfig).As(); builder.RegisterInstance(timestamper).As(); builder - .AddModule(new DiscV5KademliaModule(nodeKey.PublicKey, bootNodes)) - .AddSingleton(); + .AddModule(new KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddSingleton(); - configureDiscv5Services?.Invoke(builder); + configureServices?.Invoke(builder); }); - DiscV5Services services = _discv5Services.Resolve(); - _discv5Adapter = services.Discv5Adapter; + Services services = _services.Resolve(); + _adapter = services.Adapter; _discoveryHandlerFactory = services.NettyDiscoveryHandlerFactory; UseKademliaServices(services.NodeSource, services.Kademlia); } - private record DiscV5Services( + private record Services( IKademliaNodeSource NodeSource, - IDiscv5KademliaAdapter Discv5Adapter, + IKademliaAdapter Adapter, IKademlia Kademlia, Func NettyDiscoveryHandlerFactory ) @@ -100,7 +100,7 @@ internal List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConf if (discoveryConfig.UseDefaultDiscv5Bootnodes) { - string[] defaultBootnodes = GetDefaultDiscv5Bootnodes(); + string[] defaultBootnodes = GetDefaultBootnodes(); for (int i = 0; i < defaultBootnodes.Length; i++) { defaultStats.Record(AddBootNode(bootNodes, seen, NodeRecord.FromEnrString(defaultBootnodes[i]))); @@ -190,12 +190,12 @@ private BootNodeAddResult AddBootNode(List bootNodes, HashSet see return BootNodeAddResult.Added; } - private static string[] GetDefaultDiscv5Bootnodes() => + private static string[] GetDefaultBootnodes() => JsonSerializer.Deserialize(typeof(DiscoveryV5App).Assembly.GetManifestResourceStream("Nethermind.Network.Discovery.Discv5.discv5-bootnodes.json")!) ?? []; internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? node) { - if (Discv5NodeRecordConverter.TryGetNodeFromEnr(enr, _allowNonRoutableEnrs, out node)) + if (NodeRecordConverter.TryGetNodeFromEnr(enr, _allowNonRoutableEnrs, out node)) { return true; } @@ -342,13 +342,13 @@ protected override void DetachEventHandlers() } protected override Task RunDiscoveryAsync(CancellationToken cancellationToken) => - Task.WhenAll(_discv5Adapter.RunAsync(cancellationToken), Kademlia.Run(cancellationToken)); + Task.WhenAll(_adapter.RunAsync(cancellationToken), Kademlia.Run(cancellationToken)); protected override async Task StopAsyncCore() { PersistKnownEnrs(); - await _discv5Adapter.DisposeAsync(); + await _adapter.DisposeAsync(); _discoveryHandler?.Close(); } @@ -387,7 +387,7 @@ private void PersistKnownEnrs() } } - protected override ValueTask DisposeAsyncCore() => _discv5Services.DisposeAsync(); + protected override ValueTask DisposeAsyncCore() => _services.DisposeAsync(); private enum BootNodeAddResult { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs deleted file mode 100644 index 3c79c43d1742..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5PacketCodec.cs +++ /dev/null @@ -1,563 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Buffers.Binary; -using System.Security.Cryptography; -using System.Text; -using Autofac.Features.AttributeFilters; -using Nethermind.Core.Collections; -using Nethermind.Core.Crypto; -using Nethermind.Crypto; -using Nethermind.Network.Discovery.Discv5.Messages; -using Nethermind.Network.Enr; -using Nethermind.Serialization.Rlp; - -namespace Nethermind.Network.Discovery.Discv5; - -internal enum Discv5PacketFlag : byte -{ - Ordinary = 0, - WhoAreYou = 1, - Handshake = 2 -} - -internal sealed record Discv5Packet( - Discv5PacketFlag Flag, - byte[] Nonce, - byte[] AuthData, - byte[] Message, - byte[] MessageAd) -{ - public byte[] ChallengeData => MessageAd; -} - -internal sealed record Discv5Challenge(byte[] RequestNonce, byte[] IdNonce, ulong EnrSequence, byte[] ChallengeData); - -internal sealed record Discv5Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] WriteKey) -{ - private long _nonceCounter; - - public byte[] GetNextNonce(ICryptoRandom random) - { - byte[] nonce = new byte[Discv5PacketCodec.NonceSize]; - BinaryPrimitives.WriteUInt32BigEndian(nonce, unchecked((uint)Interlocked.Increment(ref _nonceCounter))); - random.GenerateRandomBytes(nonce.AsSpan(sizeof(uint))); - return nonce; - } -} - -public sealed class Discv5PacketCodec( - [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, - INodeRecordProvider nodeRecordProvider, - ICryptoRandom cryptoRandom, - IEcdsa ecdsa) : IDisposable -{ - public const int NonceSize = 12; - - private const int MaskingIvSize = 16; - private const int StaticHeaderSize = 23; - private const int NodeIdSize = 32; - private const int WhoAreYouAuthDataSize = 24; - private const int IdNonceSize = 16; - private const int AesKeySize = 16; - private const int AesGcmTagSize = 16; - private const int Version = 1; - private const int IdSignatureSize = 64; - private const int EphemeralPublicKeySize = 33; - private const int HandshakeAuthDataHeadSize = NodeIdSize + 2; - - private static readonly byte[] ProtocolId = "discv5"u8.ToArray(); - private static readonly byte[] KeyAgreementInfoPrefix = Encoding.ASCII.GetBytes("discovery v5 key agreement"); - private static readonly byte[] IdentityProofText = Encoding.ASCII.GetBytes("discovery v5 identity proof"); - - private readonly PrivateKey _privateKey = nodeKey.Unprotect(); - private readonly PublicKey _publicKey = nodeKey.PublicKey; - private readonly byte[] _localNodeId = nodeKey.PublicKey.Hash.BytesToArray(); - private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; - private readonly ICryptoRandom _cryptoRandom = cryptoRandom; - private readonly IEcdsa _ecdsa = ecdsa; - - public void Dispose() => _privateKey.Dispose(); - - internal byte[] EncodeOrdinary(PublicKey destination, byte[] encryptionKey, Discv5Message message, byte[]? nonce = null) - { - byte[] actualNonce = nonce ?? CreateNonce(); - byte[] authData = _localNodeId; - return EncodePacket(destination.Hash.Bytes, Discv5PacketFlag.Ordinary, actualNonce, authData, encryptionKey, message); - } - - internal byte[] EncodeWhoAreYou(byte[] destinationNodeId, byte[] requestNonce, ulong enrSequence, out Discv5Challenge challenge) - { - byte[] idNonce = _cryptoRandom.GenerateRandomBytes(IdNonceSize); - byte[] authData = new byte[WhoAreYouAuthDataSize]; - idNonce.CopyTo(authData, 0); - BinaryPrimitives.WriteUInt64BigEndian(authData.AsSpan(IdNonceSize), enrSequence); - - byte[] packet = EncodePacket(destinationNodeId, Discv5PacketFlag.WhoAreYou, requestNonce, authData, null, null, out byte[] challengeData); - challenge = new Discv5Challenge(requestNonce.ToArray(), idNonce, enrSequence, challengeData); - return packet; - } - - internal byte[] EncodeHandshake(PublicKey destination, Discv5Challenge challenge, Discv5Message message, out Discv5Session session) - { - using PrivateKey ephemeralKey = new PrivateKeyGenerator(_cryptoRandom).Generate(); - DeriveKeys( - destination, - ephemeralKey, - _localNodeId, - destination.Hash.Bytes, - challenge.ChallengeData, - out byte[] initiatorKey, - out byte[] recipientKey); - - byte[] ephemeralPublicKey = ephemeralKey.CompressedPublicKey.Bytes; - byte[] idSignature = SignIdNonce(challenge.ChallengeData, ephemeralPublicKey, destination.Hash.Bytes); - byte[] record = challenge.EnrSequence < _nodeRecordProvider.Current.EnrSequence - ? _nodeRecordProvider.Current.ToRlpBytes() - : []; - - byte[] authData = new byte[HandshakeAuthDataHeadSize + idSignature.Length + ephemeralPublicKey.Length + record.Length]; - _localNodeId.CopyTo(authData, 0); - authData[NodeIdSize] = IdSignatureSize; - authData[NodeIdSize + 1] = EphemeralPublicKeySize; - idSignature.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize)); - ephemeralPublicKey.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize + idSignature.Length)); - record.CopyTo(authData.AsSpan(HandshakeAuthDataHeadSize + idSignature.Length + ephemeralPublicKey.Length)); - - session = new Discv5Session(destination, recipientKey, initiatorKey); - return EncodePacket(destination.Hash.Bytes, Discv5PacketFlag.Handshake, CreateNonce(), authData, initiatorKey, message); - } - - internal bool TryDecode(ReadOnlySpan packet, out Discv5Packet decoded) - => TryDecode(packet, _localNodeId, out decoded); - - internal static bool TryDecode(ReadOnlySpan packet, ReadOnlySpan localNodeId, out Discv5Packet decoded) - { - decoded = null!; - if (packet.Length < MaskingIvSize + StaticHeaderSize) - { - return false; - } - - ReadOnlySpan maskingIv = packet[..MaskingIvSize]; - byte[] staticHeader = AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize)); - if (!staticHeader.AsSpan(0, ProtocolId.Length).SequenceEqual(ProtocolId)) - { - return false; - } - - int authDataSize = BinaryPrimitives.ReadUInt16BigEndian(staticHeader.AsSpan(StaticHeaderSize - sizeof(ushort))); - int headerSize = StaticHeaderSize + authDataSize; - if (packet.Length < MaskingIvSize + headerSize) - { - return false; - } - - byte[] header = AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, headerSize)); - if (!header.AsSpan(0, ProtocolId.Length).SequenceEqual(ProtocolId)) - { - return false; - } - - int version = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(ProtocolId.Length)); - if (version != Version) - { - return false; - } - - Discv5PacketFlag flag = (Discv5PacketFlag)header[ProtocolId.Length + sizeof(ushort)]; - byte[] nonce = header.AsSpan(ProtocolId.Length + sizeof(ushort) + sizeof(byte), NonceSize).ToArray(); - byte[] authData = header.AsSpan(StaticHeaderSize, authDataSize).ToArray(); - byte[] message = packet[(MaskingIvSize + headerSize)..].ToArray(); - byte[] messageAd = new byte[MaskingIvSize + header.Length]; - maskingIv.CopyTo(messageAd); - header.CopyTo(messageAd.AsSpan(MaskingIvSize)); - - decoded = new Discv5Packet(flag, nonce, authData, message, messageAd); - return true; - } - - internal bool TryDecryptMessage(Discv5Packet packet, byte[] encryptionKey, out Discv5Message message) - => TryDecryptMessageForTest(packet, encryptionKey, out message); - - internal static bool TryDecryptMessageForTest(Discv5Packet packet, byte[] encryptionKey, out Discv5Message message) - { - message = null!; - if (packet.Message.Length < AesGcmTagSize) - { - return false; - } - - Discv5MessageBuffer plaintext = new(packet.Message.Length - AesGcmTagSize); - try - { - using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); - aesGcm.Decrypt( - packet.Nonce, - packet.Message.AsSpan(0, plaintext.Length), - packet.Message.AsSpan(plaintext.Length, AesGcmTagSize), - plaintext.Span, - packet.MessageAd); - - message = Discv5MessageCodec.Decode(plaintext.Memory, plaintext); - return true; - } - catch (CryptographicException) - { - plaintext.Dispose(); - return false; - } - catch - { - plaintext.Dispose(); - throw; - } - } - - internal Discv5Challenge DecodeWhoAreYou(Discv5Packet packet) - { - if (packet.AuthData.Length != WhoAreYouAuthDataSize) - { - throw new RlpException("Invalid WHOAREYOU authdata length."); - } - - byte[] idNonce = packet.AuthData.AsSpan(0, IdNonceSize).ToArray(); - ulong enrSequence = BinaryPrimitives.ReadUInt64BigEndian(packet.AuthData.AsSpan(IdNonceSize)); - return new Discv5Challenge(packet.Nonce, idNonce, enrSequence, packet.ChallengeData); - } - - internal bool TryDecryptHandshake( - Discv5Packet packet, - Discv5Challenge challenge, - NodeRecord? knownRecord, - out Discv5Session session, - out Discv5Message message, - out NodeRecord? nodeRecord) - { - session = null!; - message = null!; - nodeRecord = null; - - if (!TryReadHandshakeAuthData(packet.AuthData, out byte[] sourceNodeId, out byte[] idSignature, out CompressedPublicKey ephemeralPublicKey, out byte[] recordBytes)) - { - return false; - } - - if (recordBytes.Length > 0) - { - try - { - nodeRecord = NodeRecord.FromBytes(recordBytes); - } - catch (Exception) - { - return false; - } - } - - NodeRecord? record = nodeRecord ?? knownRecord; - CompressedPublicKey? remoteCompressedPublicKey = record?.GetObj(EnrContentKey.SecP256k1); - if (remoteCompressedPublicKey is null || !remoteCompressedPublicKey.Decompress().Hash.Bytes.SequenceEqual(sourceNodeId)) - { - return false; - } - - if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature, challenge.ChallengeData, ephemeralPublicKey.Bytes, _localNodeId)) - { - return false; - } - - PublicKey remotePublicKey = remoteCompressedPublicKey.Decompress(); - DeriveKeys(ephemeralPublicKey, sourceNodeId, _localNodeId, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); - - if (!TryDecryptMessage(packet, initiatorKey, out message)) - { - return false; - } - - session = new Discv5Session(remotePublicKey, initiatorKey, recipientKey); - return true; - } - - internal static bool TryGetSourceNodeId(Discv5Packet packet, out byte[] sourceNodeId) - { - sourceNodeId = []; - switch (packet.Flag) - { - case Discv5PacketFlag.Ordinary when packet.AuthData.Length == NodeIdSize: - sourceNodeId = packet.AuthData.ToArray(); - return true; - case Discv5PacketFlag.Handshake when packet.AuthData.Length >= HandshakeAuthDataHeadSize: - sourceNodeId = packet.AuthData.AsSpan(0, NodeIdSize).ToArray(); - return true; - default: - return false; - } - } - - private byte[] EncodePacket( - ReadOnlySpan destinationNodeId, - Discv5PacketFlag flag, - byte[] nonce, - byte[] authData, - byte[]? encryptionKey, - Discv5Message? message) - => EncodePacket(destinationNodeId, flag, nonce, authData, encryptionKey, message, out _); - - private byte[] EncodePacket( - ReadOnlySpan destinationNodeId, - Discv5PacketFlag flag, - byte[] nonce, - byte[] authData, - byte[]? encryptionKey, - Discv5Message? message, - out byte[] messageAd) - { - byte[] maskingIv = _cryptoRandom.GenerateRandomBytes(MaskingIvSize); - byte[] header = CreateHeader(flag, nonce, authData); - messageAd = new byte[MaskingIvSize + header.Length]; - maskingIv.CopyTo(messageAd, 0); - header.CopyTo(messageAd.AsSpan(MaskingIvSize)); - - byte[] encryptedMessage = []; - if (message is not null) - { - ArgumentNullException.ThrowIfNull(encryptionKey); - - using ArrayPoolSpan encodedMessage = Discv5MessageCodec.Encode(message); - encryptedMessage = EncryptMessage(encryptionKey, nonce, encodedMessage, messageAd); - } - - byte[] maskedHeader = AesCtrTransform(destinationNodeId[..AesKeySize], maskingIv, header); - byte[] packet = new byte[MaskingIvSize + maskedHeader.Length + encryptedMessage.Length]; - maskingIv.CopyTo(packet, 0); - maskedHeader.CopyTo(packet.AsSpan(MaskingIvSize)); - encryptedMessage.CopyTo(packet.AsSpan(MaskingIvSize + maskedHeader.Length)); - return packet; - } - - private static byte[] CreateHeader(Discv5PacketFlag flag, byte[] nonce, byte[] authData) - { - if (nonce.Length != NonceSize) - { - throw new ArgumentException($"Nonce must be {NonceSize} bytes.", nameof(nonce)); - } - - byte[] header = new byte[StaticHeaderSize + authData.Length]; - ProtocolId.CopyTo(header, 0); - BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(ProtocolId.Length), Version); - header[ProtocolId.Length + sizeof(ushort)] = (byte)flag; - nonce.CopyTo(header.AsSpan(ProtocolId.Length + sizeof(ushort) + sizeof(byte))); - BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(StaticHeaderSize - sizeof(ushort)), checked((ushort)authData.Length)); - authData.CopyTo(header.AsSpan(StaticHeaderSize)); - return header; - } - - private byte[] CreateNonce() => _cryptoRandom.GenerateRandomBytes(NonceSize); - - private static byte[] EncryptMessage(byte[] encryptionKey, byte[] nonce, ReadOnlySpan plaintext, byte[] messageAd) - { - byte[] encrypted = new byte[plaintext.Length + AesGcmTagSize]; - using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); - aesGcm.Encrypt( - nonce, - plaintext, - encrypted.AsSpan(0, plaintext.Length), - encrypted.AsSpan(plaintext.Length, AesGcmTagSize), - messageAd); - return encrypted; - } - - private static byte[] AesCtrTransform(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan input) - { - byte[] output = new byte[input.Length]; - using Aes aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - aes.Key = key.ToArray(); - - Span counter = stackalloc byte[MaskingIvSize]; - iv.CopyTo(counter); - Span keyStream = stackalloc byte[MaskingIvSize]; - - int offset = 0; - while (offset < input.Length) - { - aes.EncryptEcb(counter, keyStream, PaddingMode.None); - - int blockLength = Math.Min(MaskingIvSize, input.Length - offset); - for (int i = 0; i < blockLength; i++) - { - output[offset + i] = (byte)(input[offset + i] ^ keyStream[i]); - } - - IncrementCounter(counter); - offset += blockLength; - } - - return output; - } - - private static void IncrementCounter(Span counter) - { - for (int i = counter.Length - 1; i >= 0; i--) - { - counter[i]++; - if (counter[i] != 0) - { - return; - } - } - } - - private static void DeriveKeys( - PublicKey remotePublicKey, - PrivateKey ephemeralPrivateKey, - ReadOnlySpan initiatorNodeId, - ReadOnlySpan recipientNodeId, - byte[] challengeData, - out byte[] initiatorKey, - out byte[] recipientKey) - { - byte[] secret = SecP256k1Agreement.AgreeCompressed(remotePublicKey, ephemeralPrivateKey); - DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); - } - - private void DeriveKeys( - CompressedPublicKey ephemeralPublicKey, - ReadOnlySpan initiatorNodeId, - ReadOnlySpan recipientNodeId, - byte[] challengeData, - out byte[] initiatorKey, - out byte[] recipientKey) - { - byte[] secret = SecP256k1Agreement.AgreeCompressed(ephemeralPublicKey, _privateKey); - DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); - } - - private static void DeriveKeys( - byte[] secret, - ReadOnlySpan initiatorNodeId, - ReadOnlySpan recipientNodeId, - byte[] challengeData, - out byte[] initiatorKey, - out byte[] recipientKey) - { - byte[] prk = HMACSHA256.HashData(challengeData, secret); - byte[] info = new byte[KeyAgreementInfoPrefix.Length + NodeIdSize + NodeIdSize]; - KeyAgreementInfoPrefix.CopyTo(info, 0); - initiatorNodeId.CopyTo(info.AsSpan(KeyAgreementInfoPrefix.Length)); - recipientNodeId.CopyTo(info.AsSpan(KeyAgreementInfoPrefix.Length + NodeIdSize)); - - byte[] keyData = HkdfExpand(prk, info, AesKeySize * 2); - initiatorKey = keyData[..AesKeySize]; - recipientKey = keyData[AesKeySize..]; - } - - internal static (byte[] InitiatorKey, byte[] RecipientKey) DeriveKeysForTest( - byte[] secret, - byte[] initiatorNodeId, - byte[] recipientNodeId, - byte[] challengeData) - { - DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out byte[] initiatorKey, out byte[] recipientKey); - return (initiatorKey, recipientKey); - } - - private static byte[] HkdfExpand(byte[] prk, byte[] info, int length) - { - byte[] result = new byte[length]; - byte[] previous = []; - int offset = 0; - byte counter = 1; - using HMACSHA256 hmac = new(prk); - while (offset < length) - { - byte[] input = new byte[previous.Length + info.Length + 1]; - previous.CopyTo(input, 0); - info.CopyTo(input.AsSpan(previous.Length)); - input[^1] = counter++; - previous = hmac.ComputeHash(input); - int copyLength = Math.Min(previous.Length, length - offset); - previous.AsSpan(0, copyLength).CopyTo(result.AsSpan(offset)); - offset += copyLength; - } - - return result; - } - - private byte[] SignIdNonce(byte[] challengeData, byte[] ephemeralPublicKey, ReadOnlySpan recipientNodeId) - { - byte[] signingHash = CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); - Signature signature = _ecdsa.Sign(_privateKey, new ValueHash256(signingHash)); - return signature.Bytes.ToArray(); - } - - private bool VerifyIdSignature(CompressedPublicKey signer, byte[] signatureBytes, byte[] challengeData, byte[] ephemeralPublicKey, ReadOnlySpan recipientNodeId) - { - byte[] signingHash = CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); - for (int recoveryId = 0; recoveryId <= 1; recoveryId++) - { - Signature signature = new(signatureBytes, recoveryId); - CompressedPublicKey? recovered = _ecdsa.RecoverCompressedPublicKey(signature, new ValueHash256(signingHash)); - if (signer.Equals(recovered)) - { - return true; - } - } - - return false; - } - - internal static byte[] CalculateIdSignatureHashForTest(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) - => CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId); - - private static byte[] CalculateIdSignatureHash(byte[] challengeData, byte[] ephemeralPublicKey, ReadOnlySpan recipientNodeId) - { - byte[] signingInput = new byte[IdentityProofText.Length + challengeData.Length + ephemeralPublicKey.Length + recipientNodeId.Length]; - IdentityProofText.CopyTo(signingInput, 0); - challengeData.CopyTo(signingInput.AsSpan(IdentityProofText.Length)); - ephemeralPublicKey.CopyTo(signingInput.AsSpan(IdentityProofText.Length + challengeData.Length)); - recipientNodeId.CopyTo(signingInput.AsSpan(IdentityProofText.Length + challengeData.Length + ephemeralPublicKey.Length)); - return SHA256.HashData(signingInput); - } - - private static bool TryReadHandshakeAuthData( - byte[] authData, - out byte[] sourceNodeId, - out byte[] idSignature, - out CompressedPublicKey ephemeralPublicKey, - out byte[] record) - { - sourceNodeId = []; - idSignature = []; - ephemeralPublicKey = null!; - record = []; - - if (authData.Length < HandshakeAuthDataHeadSize) - { - return false; - } - - sourceNodeId = authData.AsSpan(0, NodeIdSize).ToArray(); - int signatureSize = authData[NodeIdSize]; - int ephemeralKeySize = authData[NodeIdSize + 1]; - if (signatureSize != IdSignatureSize || ephemeralKeySize != EphemeralPublicKeySize) - { - return false; - } - - int signatureOffset = HandshakeAuthDataHeadSize; - int ephemeralKeyOffset = signatureOffset + signatureSize; - int recordOffset = ephemeralKeyOffset + ephemeralKeySize; - if (authData.Length < recordOffset) - { - return false; - } - - idSignature = authData.AsSpan(signatureOffset, signatureSize).ToArray(); - ephemeralPublicKey = new CompressedPublicKey(authData.AsSpan(ephemeralKeyOffset, ephemeralKeySize)); - record = authData.AsSpan(recordOffset).ToArray(); - return true; - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs index 36aefc4adc84..f5642454bd9c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs @@ -9,7 +9,7 @@ internal interface IResponseHandler { Task Task { get; } - Discv5MessageType MessageType { get; } + MessageType MessageType { get; } bool Handle(Discv5Message message); } @@ -19,12 +19,12 @@ internal interface IResponseHandler : IResponseHandler where TMessa bool Handle(TMessage message); } -internal abstract class ResponseHandler(Discv5MessageType messageType) : IResponseHandler +internal abstract class ResponseHandler(MessageType messageType) : IResponseHandler where TMessage : Discv5Message { public abstract Task Task { get; } - public Discv5MessageType MessageType { get; } = messageType; + public MessageType MessageType { get; } = messageType; public bool Handle(Discv5Message message) => message is TMessage typedMessage && Handle(typedMessage); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs index d443a40dd465..61d15c593f8b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs @@ -9,8 +9,8 @@ namespace Nethermind.Network.Discovery.Discv5.Handlers; -internal sealed class NodesResponseHandler(Node receiver, Discv5Distances requestedDistances, IKademliaDistance distanceCalculator) - : ResponseHandler(Discv5MessageType.Nodes) +internal sealed class NodesResponseHandler(Node receiver, Distances requestedDistances, IKademliaDistance distanceCalculator) + : ResponseHandler(MessageType.Nodes) { private const int MaxNodesResponseMessages = 16; private const int MaxNodesResponseRecords = 64; @@ -24,7 +24,7 @@ internal sealed class NodesResponseHandler(Node receiver, Discv5Distances reques public override Task Task => _completion.Task; - public override bool Handle(Discv5Nodes nodes) + public override bool Handle(NodesMsg nodes) { if (_completion.Task.IsCompleted) { @@ -49,7 +49,7 @@ public override bool Handle(Discv5Nodes nodes) for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; - if (!Discv5NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || + if (!NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || !_seenNodeIds.Add(node.Id.Hash) || !MatchesRequestedDistance(node, requestedDistances)) { @@ -69,7 +69,7 @@ public override bool Handle(Discv5Nodes nodes) public Node[] GetNodes() => [.. _nodes]; - private bool MatchesRequestedDistance(Node node, Discv5Distances requestedDistances) + private bool MatchesRequestedDistance(Node node, Distances requestedDistances) { int distance = distanceCalculator.CalculateLogDistance(receiver.Id.Hash, node.Id.Hash); for (int i = 0; i < requestedDistances.Count; i++) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs index 52742098ebb9..f7101f38a9d3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs @@ -6,13 +6,13 @@ namespace Nethermind.Network.Discovery.Discv5.Handlers; -internal sealed class PongResponseHandler(Node receiver) : ResponseHandler(Discv5MessageType.Pong) +internal sealed class PongResponseHandler(Node receiver) : ResponseHandler(MessageType.Pong) { private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); public override Task Task => _completion.Task; - public override bool Handle(Discv5Pong message) + public override bool Handle(PongMsg message) { receiver.ValidatedProtocol = true; _completion.TrySetResult(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/IKademliaAdapter.cs similarity index 88% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/IKademliaAdapter.cs index 6bbd3df997c6..061074a10e49 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/IDiscv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/IKademliaAdapter.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv5; /// /// Adapts discv5 distance-based FINDNODE requests to the protocol-specific Kademlia routing table. /// -public interface IDiscv5KademliaAdapter : IKademliaMessageSender, IAsyncDisposable +public interface IKademliaAdapter : IKademliaMessageSender, IAsyncDisposable { /// /// Gets known nodes at the requested log distances from the local node. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaAdapter.cs similarity index 82% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaAdapter.cs index 137f0e94a030..496fa92bdb97 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaAdapter.cs @@ -9,9 +9,10 @@ using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Discv5.Handlers; -using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Enr; +using Nethermind.Logging; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv5; @@ -19,15 +20,15 @@ namespace Nethermind.Network.Discovery.Discv5; /// /// Maps discv5 FINDNODE distance requests onto the protocol-specific Kademlia table. /// -public class Discv5KademliaAdapter( +public class KademliaAdapter( Lazy> kademlia, NettyDiscoveryV5Handler discoveryHandler, - Discv5PacketCodec packetCodec, + PacketCodec packetCodec, INodeRecordProvider nodeRecordProvider, IDiscoveryConfig discoveryConfig, ICryptoRandom cryptoRandom, IKademliaDistance distance, - ILogManager logManager) : IDiscv5KademliaAdapter + ILogManager logManager) : IKademliaAdapter { private const int MaxFindNodeRecords = 16; private const int MaxEnrsPerNodesMessage = 3; @@ -46,8 +47,8 @@ public class Discv5KademliaAdapter( private readonly TimeSpan _pingTimeout = TimeSpan.FromMilliseconds(discoveryConfig.PingTimeout); private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); private readonly IKademliaDistance _distance = distance; - private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions"); + private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions"); private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); private long _lastSentChallengeTrimMilliseconds; private readonly LruCache _pendingByNonce = new(MaxPendingRequests, "discv5 pending requests"); @@ -96,7 +97,7 @@ public async Task Ping(Node receiver, CancellationToken token) { RegisterKnownRecord(receiver); ReserveEndpointCheck(receiver); - using Discv5Ping ping = new(CreateRequestId(), nodeRecordProvider.Current.EnrSequence); + using PingMsg ping = new(CreateRequestId(), nodeRecordProvider.Current.EnrSequence); PongResponseHandler responseHandler = new(receiver); await SendRequest(receiver, ping, responseHandler, _pingTimeout, token); @@ -107,8 +108,8 @@ public async Task Ping(Node receiver, CancellationToken token) public async Task FindNeighbours(Node receiver, PublicKey target, CancellationToken token) { RegisterKnownRecord(receiver); - Discv5Distances distances = GetLookupDistances(receiver, target); - using Discv5FindNode findNode = new(CreateRequestId(), distances); + Distances distances = GetLookupDistances(receiver, target); + using FindNodeMsg findNode = new(CreateRequestId(), distances); NodesResponseHandler responseHandler = new(receiver, distances, _distance); await SendRequest(receiver, findNode, responseHandler, _findNodeTimeout, token); @@ -179,9 +180,10 @@ private async Task SendRequest( private async Task SendMessage(Node receiver, Discv5Message message) { SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); - if (TryGetSession(sessionKey, out Discv5Session? session)) + if (TryGetSession(sessionKey, out Session? session)) { - byte[] sessionNonce = session.GetNextNonce(cryptoRandom); + Span sessionNonce = stackalloc byte[PacketCodec.NonceSize]; + session.WriteNextNonce(cryptoRandom, sessionNonce); PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceKey.From(sessionNonce)); _pendingByNonce.Set(sessionPendingNonceKey, new PendingRequest(receiver, message)); byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, sessionNonce); @@ -197,8 +199,10 @@ private async Task SendRequest( } } - byte[] nonce = cryptoRandom.GenerateRandomBytes(Discv5PacketCodec.NonceSize); - byte[] encryptionKey = cryptoRandom.GenerateRandomBytes(16); + Span nonce = stackalloc byte[PacketCodec.NonceSize]; + cryptoRandom.GenerateRandomBytes(nonce); + Span encryptionKey = stackalloc byte[16]; + cryptoRandom.GenerateRandomBytes(encryptionKey); PendingRequest pendingRequest = new(receiver, message); PendingNonceKey pendingNonceKey = new(receiver.Address, NonceKey.From(nonce)); _pendingByNonce.Set(pendingNonceKey, pendingRequest); @@ -219,73 +223,77 @@ private async Task SendRequest( private async Task SendResponse(Node receiver, Discv5Message message, CancellationToken token) { SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); - if (!TryGetSession(sessionKey, out Discv5Session? session)) + if (!TryGetSession(sessionKey, out Session? session)) { return; } - byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, session.GetNextNonce(cryptoRandom)); + Span nonce = stackalloc byte[PacketCodec.NonceSize]; + session.WriteNextNonce(cryptoRandom, nonce); + byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, nonce); await discoveryHandler.SendAsync(packet, receiver.Address); } private async Task HandlePacket(UdpReceiveResult udpPacket, CancellationToken token) { - if (!packetCodec.TryDecode(udpPacket.Buffer, out Discv5Packet packet)) + if (!packetCodec.TryDecode(udpPacket.Buffer, out Packet packet)) { return; } - try + using (packet) { - switch (packet.Flag) + try { - case Discv5PacketFlag.WhoAreYou: - await HandleWhoAreYou(udpPacket.RemoteEndPoint, packet, token); - break; - case Discv5PacketFlag.Ordinary: - await HandleOrdinary(udpPacket.RemoteEndPoint, packet, token); - break; - case Discv5PacketFlag.Handshake: - await HandleHandshake(udpPacket.RemoteEndPoint, packet, token); - break; + switch (packet.Flag) + { + case PacketFlag.WhoAreYou: + await HandleWhoAreYou(udpPacket.RemoteEndPoint, packet, token); + break; + case PacketFlag.Ordinary: + await HandleOrdinary(udpPacket.RemoteEndPoint, packet, token); + break; + case PacketFlag.Handshake: + await HandleHandshake(udpPacket.RemoteEndPoint, packet, token); + break; + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + } + catch (Exception e) + { + if (_logger.IsDebug) _logger.Debug($"Error handling discv5 packet from {udpPacket.RemoteEndPoint}: {e}"); } - } - catch (OperationCanceledException) when (token.IsCancellationRequested) - { - } - catch (Exception e) - { - if (_logger.IsDebug) _logger.Debug($"Error handling discv5 packet from {udpPacket.RemoteEndPoint}: {e}"); } } - private async Task HandleWhoAreYou(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) + private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, CancellationToken token) { - PendingNonceKey pendingNonceKey = new(endpoint, NonceKey.From(packet.Nonce)); + PendingNonceKey pendingNonceKey = new(endpoint, NonceKey.From(packet.Nonce.Span)); if (!_pendingByNonce.TryRemove(pendingNonceKey, out PendingRequest? pendingRequest)) { return; } - Discv5Challenge challenge = packetCodec.DecodeWhoAreYou(packet); - byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Discv5Session session); + Challenge challenge = packetCodec.DecodeWhoAreYou(packet); + byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Session session); SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash, endpoint), session); await discoveryHandler.SendAsync(handshakePacket, endpoint); } - private async Task HandleOrdinary(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) + private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!Discv5PacketCodec.TryGetSourceNodeId(packet, out byte[] sourceNodeId)) + if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256 nodeId)) { return; } - Hash256 nodeId = new(sourceNodeId); SessionKey sessionKey = new(nodeId, endpoint); - if (!TryGetSession(sessionKey, out Discv5Session? session) || + if (!TryGetSession(sessionKey, out Session? session) || !packetCodec.TryDecryptMessage(packet, session.ReadKey, out Discv5Message message)) { - await SendWhoAreYou(endpoint, packet, sourceNodeId); + await SendWhoAreYou(endpoint, packet, nodeId); return; } @@ -299,14 +307,13 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Discv5Packet packet, Canc } } - private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, CancellationToken token) + private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!Discv5PacketCodec.TryGetSourceNodeId(packet, out byte[] sourceNodeId)) + if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256 nodeId)) { return; } - Hash256 nodeId = new(sourceNodeId); ChallengeKey challengeKey = new(nodeId, endpoint); if (!_sentChallenges.TryRemove(challengeKey, out SentChallenge sentChallenge) || IsExpired(sentChallenge, Environment.TickCount64)) @@ -315,7 +322,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can } TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); - if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Discv5Session session, out Discv5Message message, out NodeRecord? nodeRecord)) + if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) { return; } @@ -346,9 +353,8 @@ private async Task HandleHandshake(IPEndPoint endpoint, Discv5Packet packet, Can } } - private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket, byte[] destinationNodeId) + private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Hash256 nodeId) { - Hash256 nodeId = new(destinationNodeId); ChallengeKey challengeKey = new(nodeId, endpoint); long now = Environment.TickCount64; if (_sentChallenges.TryGet(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) @@ -364,7 +370,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Discv5Packet requestPacket } ulong enrSequence = TryGetKnownRecord(nodeId, out NodeRecord? record) ? record.EnrSequence : 0UL; - byte[] packet = packetCodec.EncodeWhoAreYou(destinationNodeId, requestPacket.Nonce, enrSequence, out Discv5Challenge challenge); + byte[] packet = packetCodec.EncodeWhoAreYou(nodeId.Bytes, requestPacket.Nonce.Span, enrSequence, out Challenge challenge); SetSentChallenge(challengeKey, challenge, packet); await discoveryHandler.SendAsync(packet, endpoint); } @@ -383,8 +389,8 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, switch (message) { - case Discv5Ping ping: - using (Discv5Pong pong = new(ping.RequestId, nodeRecordProvider.Current.EnrSequence, endpoint.Address, endpoint.Port)) + case PingMsg ping: + using (PongMsg pong = new(ping.RequestId, nodeRecordProvider.Current.EnrSequence, endpoint.Address, endpoint.Port)) { await SendResponse(remoteNode, pong, token); } @@ -395,12 +401,12 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, StartEndpointCheck(remoteNode, token); } break; - case Discv5FindNode findNode: + case FindNodeMsg findNode: await HandleFindNode(remoteNode, findNode, token); kademlia.Value.AddOrRefresh(remoteNode); break; - case Discv5TalkReq talkReq: - using (Discv5TalkResp talkResp = new(talkReq.RequestId, ReadOnlyMemory.Empty)) + case TalkReqMsg talkReq: + using (TalkRespMsg talkResp = new(talkReq.RequestId, ReadOnlyMemory.Empty)) { await SendResponse(remoteNode, talkResp, token); } @@ -425,12 +431,12 @@ private bool HandleResponse(Hash256 nodeId, Discv5Message message) return _responseHandlers.TryGet(responseKey, out IResponseHandler? handler) && handler.Handle(message); } - private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, CancellationToken token) + private async Task HandleFindNode(Node remoteNode, FindNodeMsg findNode, CancellationToken token) { NodeRecord[] records = GetFindNodeRecords(findNode.Distances, remoteNode); if (records.Length == 0) { - using Discv5Nodes emptyResponse = new(findNode.RequestId, 1, []); + using NodesMsg emptyResponse = new(findNode.RequestId, 1, []); await SendResponse(remoteNode, emptyResponse, token); return; } @@ -440,12 +446,12 @@ private async Task HandleFindNode(Node remoteNode, Discv5FindNode findNode, Canc { int count = Math.Min(MaxEnrsPerNodesMessage, records.Length - i); ArraySegment chunk = new(records, i, count); - using Discv5Nodes nodes = new(findNode.RequestId, total, chunk); + using NodesMsg nodes = new(findNode.RequestId, total, chunk); await SendResponse(remoteNode, nodes, token); } } - private NodeRecord[] GetFindNodeRecords(Discv5Distances distances, Node requester) + private NodeRecord[] GetFindNodeRecords(Distances distances, Node requester) { HashSet seen = new(MaxFindNodeRecords); List result = new(MaxFindNodeRecords); @@ -541,7 +547,7 @@ private void RegisterKnownRecord(Node node) } } - private Discv5Distances GetLookupDistances(Node receiver, PublicKey target) + private Distances GetLookupDistances(Node receiver, PublicKey target) { int distance = _distance.CalculateLogDistance(receiver.Id.Hash, target.Hash); @@ -558,10 +564,10 @@ private Discv5Distances GetLookupDistances(Node receiver, PublicKey target) distances[count++] = distance + 1; } - return new Discv5Distances(distances[..count]); + return new Distances(distances[..count]); } - private Discv5RequestId CreateRequestId() + private RequestId CreateRequestId() { Span requestId = stackalloc byte[sizeof(ulong)]; cryptoRandom.GenerateRandomBytes(requestId); @@ -571,12 +577,12 @@ private Discv5RequestId CreateRequestId() start++; } - return Discv5RequestId.From(requestId[start..]); + return RequestId.From(requestId[start..]); } - private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Discv5Session? session) => _sessions.TryGet(sessionKey, out session); + private bool TryGetSession(SessionKey sessionKey, [NotNullWhen(true)] out Session? session) => _sessions.TryGet(sessionKey, out session); - private void SetSession(SessionKey sessionKey, Discv5Session session) + private void SetSession(SessionKey sessionKey, Session session) => _sessions.Set(sessionKey, session); private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGet(nodeId, out record); @@ -585,13 +591,13 @@ private void SetKnownRecord(Hash256 nodeId, NodeRecord record) => _knownRecords.Set(nodeId, record); internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable) - => Discv5NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable, out Node? node) && + => NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable, out Node? node) && node.Id.Hash.Equals(expectedNodeId); internal static bool HasExpectedNodeId(NodeRecord record, Hash256 expectedNodeId) => record.GetObj(EnrContentKey.SecP256k1)?.Decompress().Hash.Equals(expectedNodeId) == true; - private void SetSentChallenge(ChallengeKey challengeKey, Discv5Challenge challenge, byte[] packet) + private void SetSentChallenge(ChallengeKey challengeKey, Challenge challenge, byte[] packet) { long now = Environment.TickCount64; TryTrimExpiredChallenges(now); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaModule.cs similarity index 71% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaModule.cs index 19e096ed35c2..d9540ae3ed48 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscV5KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaModule.cs @@ -6,6 +6,7 @@ using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv5; @@ -13,14 +14,14 @@ namespace Nethermind.Network.Discovery.Discv5; /// /// Specifies the protocol-specific Kademlia services used by discv5. /// -public class DiscV5KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module +public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module { protected override void Load(ContainerBuilder builder) => builder - .AddSingleton() - .AddSingleton() - .Bind, IDiscv5KademliaAdapter>() + .AddSingleton() + .AddSingleton() + .Bind, IKademliaAdapter>() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddModule(new KademliaModule()) .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs similarity index 74% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs index f75d6466ab0f..75c1e94e0455 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Discv5; -internal static class Discv5MessageCodec +internal static class MessageCodec { public static ArrayPoolSpan Encode(Discv5Message message) { @@ -35,37 +35,37 @@ public static ArrayPoolSpan Encode(Discv5Message message) public static Discv5Message Decode(ReadOnlySpan message) => Decode(message, default, null); - public static Discv5Message Decode(ReadOnlyMemory message, IDisposable owner) + public static Discv5Message Decode(ReadOnlyMemory message, ArrayPoolSpan owner) => Decode(message.Span, message, owner); - private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory ownedMessage, IDisposable? owner) + private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { if (message.IsEmpty) { - owner?.Dispose(); + DisposeOwner(owner); throw new RlpException("Empty discv5 message."); } Discv5Message? decoded = null; try { - Discv5MessageType messageType = (Discv5MessageType)message[0]; + MessageType messageType = (MessageType)message[0]; Rlp.ValueDecoderContext ctx = new(message[1..]); int checkPosition = ctx.ReadSequenceLength() + ctx.Position; - Discv5RequestId requestId = DecodeRequestId(ref ctx); + RequestId requestId = DecodeRequestId(ref ctx); decoded = messageType switch { - Discv5MessageType.Ping => new Discv5Ping(requestId, ctx.DecodeULong(), owner), - Discv5MessageType.Pong => DecodePong(requestId, ref ctx, owner), - Discv5MessageType.FindNode => new Discv5FindNode(requestId, DecodeDistances(ref ctx), owner), - Discv5MessageType.Nodes => DecodeNodes(requestId, ref ctx, owner), - Discv5MessageType.TalkReq => new Discv5TalkReq( + MessageType.Ping => new PingMsg(requestId, ctx.DecodeULong(), owner), + MessageType.Pong => DecodePong(requestId, ref ctx, owner), + MessageType.FindNode => new FindNodeMsg(requestId, DecodeDistances(ref ctx), owner), + MessageType.Nodes => DecodeNodes(requestId, ref ctx, owner), + MessageType.TalkReq => new TalkReqMsg( requestId, DecodeByteMemory(ref ctx, ownedMessage), DecodeByteMemory(ref ctx, ownedMessage), owner), - Discv5MessageType.TalkResp => new Discv5TalkResp(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner), + MessageType.TalkResp => new TalkRespMsg(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner), _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") }; @@ -81,24 +81,32 @@ private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory? owner) + { + if (owner is { } ownerValue) + { + ownerValue.Dispose(); + } + } + private static int GetContentLength(Discv5Message message) => message switch { - Discv5Ping ping => GetRequestIdLength(ping.RequestId) + Rlp.LengthOf(ping.EnrSequence), - Discv5Pong pong => GetRequestIdLength(pong.RequestId) + + PingMsg ping => GetRequestIdLength(ping.RequestId) + Rlp.LengthOf(ping.EnrSequence), + PongMsg pong => GetRequestIdLength(pong.RequestId) + Rlp.LengthOf(pong.EnrSequence) + GetAddressRlpLength(pong.RecipientIp) + Rlp.LengthOf(pong.RecipientPort), - Discv5FindNode findNode => GetRequestIdLength(findNode.RequestId) + GetDistancesLength(findNode.Distances), - Discv5Nodes nodes => GetRequestIdLength(nodes.RequestId) + Rlp.LengthOf(nodes.Total) + GetNodeRecordsLength(nodes.Records), - Discv5TalkReq talkReq => GetRequestIdLength(talkReq.RequestId) + Rlp.LengthOf(talkReq.Protocol.Span) + Rlp.LengthOf(talkReq.Request.Span), - Discv5TalkResp talkResp => GetRequestIdLength(talkResp.RequestId) + Rlp.LengthOf(talkResp.Response.Span), + FindNodeMsg findNode => GetRequestIdLength(findNode.RequestId) + GetDistancesLength(findNode.Distances), + NodesMsg nodes => GetRequestIdLength(nodes.RequestId) + Rlp.LengthOf(nodes.Total) + GetNodeRecordsLength(nodes.Records), + TalkReqMsg talkReq => GetRequestIdLength(talkReq.RequestId) + Rlp.LengthOf(talkReq.Protocol) + Rlp.LengthOf(talkReq.Request), + TalkRespMsg talkResp => GetRequestIdLength(talkResp.RequestId) + Rlp.LengthOf(talkResp.Response), _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") }; @@ -106,54 +114,54 @@ private static void EncodeContent(Span buffer, ref int position, Discv5Mes { switch (message) { - case Discv5Ping ping: + case PingMsg ping: EncodeRequestId(buffer, ref position, ping.RequestId); Encode(buffer, ref position, ping.EnrSequence); break; - case Discv5Pong pong: + case PongMsg pong: EncodeRequestId(buffer, ref position, pong.RequestId); Encode(buffer, ref position, pong.EnrSequence); EncodeAddress(buffer, ref position, pong.RecipientIp); Encode(buffer, ref position, pong.RecipientPort); break; - case Discv5FindNode findNode: + case FindNodeMsg findNode: EncodeRequestId(buffer, ref position, findNode.RequestId); EncodeDistances(buffer, ref position, findNode.Distances); break; - case Discv5Nodes nodes: + case NodesMsg nodes: EncodeRequestId(buffer, ref position, nodes.RequestId); Encode(buffer, ref position, nodes.Total); EncodeNodeRecords(buffer, ref position, nodes.Records); break; - case Discv5TalkReq talkReq: + case TalkReqMsg talkReq: EncodeRequestId(buffer, ref position, talkReq.RequestId); - position = Rlp.Encode(buffer, position, talkReq.Protocol.Span); - position = Rlp.Encode(buffer, position, talkReq.Request.Span); + position = Rlp.Encode(buffer, position, talkReq.Protocol); + position = Rlp.Encode(buffer, position, talkReq.Request); break; - case Discv5TalkResp talkResp: + case TalkRespMsg talkResp: EncodeRequestId(buffer, ref position, talkResp.RequestId); - position = Rlp.Encode(buffer, position, talkResp.Response.Span); + position = Rlp.Encode(buffer, position, talkResp.Response); break; default: throw new RlpException($"Unsupported discv5 message {message.GetType().Name}."); } } - private static int GetRequestIdLength(Discv5RequestId requestId) + private static int GetRequestIdLength(RequestId requestId) { - Span bytes = stackalloc byte[Discv5RequestId.MaxLength]; + Span bytes = stackalloc byte[RequestId.MaxLength]; requestId.CopyTo(bytes); return Rlp.LengthOf(bytes[..requestId.Length]); } - private static void EncodeRequestId(Span buffer, ref int position, Discv5RequestId requestId) + private static void EncodeRequestId(Span buffer, ref int position, RequestId requestId) { - Span bytes = stackalloc byte[Discv5RequestId.MaxLength]; + Span bytes = stackalloc byte[RequestId.MaxLength]; requestId.CopyTo(bytes); position = Rlp.Encode(buffer, position, bytes[..requestId.Length]); } - private static int GetDistancesLength(Discv5Distances distances) + private static int GetDistancesLength(Distances distances) { int contentLength = 0; for (int i = 0; i < distances.Count; i++) @@ -164,7 +172,7 @@ private static int GetDistancesLength(Discv5Distances distances) return Rlp.LengthOfSequence(contentLength); } - private static void EncodeDistances(Span buffer, ref int position, Discv5Distances distances) + private static void EncodeDistances(Span buffer, ref int position, Distances distances) { int contentLength = 0; for (int i = 0; i < distances.Count; i++) @@ -238,15 +246,15 @@ private static void Encode(Span buffer, ref int position, ulong value) private static void Encode(Span buffer, ref int position, int value) => position += Rlp.Encode((long)value, buffer[position..]).Length; - private static Discv5RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) + private static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) { ReadOnlySpan requestId = ctx.DecodeByteArraySpan(); - if (requestId.Length > Discv5RequestId.MaxLength) + if (requestId.Length > RequestId.MaxLength) { - throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {Discv5RequestId.MaxLength}."); + throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {RequestId.MaxLength}."); } - return Discv5RequestId.From(requestId); + return RequestId.From(requestId); } private static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) @@ -260,19 +268,19 @@ private static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext return ownedMessage.Slice(1 + ctx.Position - value.Length, value.Length); } - private static Discv5Pong DecodePong(Discv5RequestId requestId, ref Rlp.ValueDecoderContext ctx, IDisposable? owner) + private static PongMsg DecodePong(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) { ulong enrSequence = ctx.DecodeULong(); IPAddress recipientIp = new(ctx.DecodeByteArraySpan()); int recipientPort = ctx.DecodePositiveInt(); - return new Discv5Pong(requestId, enrSequence, recipientIp, recipientPort, owner); + return new PongMsg(requestId, enrSequence, recipientIp, recipientPort, owner); } - private static Discv5Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) + private static Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) { int checkPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(checkPosition); - Discv5Distances distances = new(count); + Distances distances = new(count); try { for (int i = 0; i < count; i++) @@ -290,7 +298,7 @@ private static Discv5Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) } } - private static Discv5Nodes DecodeNodes(Discv5RequestId requestId, ref Rlp.ValueDecoderContext ctx, IDisposable? owner) + private static NodesMsg DecodeNodes(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) { int total = ctx.DecodePositiveInt(); int checkPosition = ctx.ReadSequenceLength() + ctx.Position; @@ -304,6 +312,6 @@ private static Discv5Nodes DecodeNodes(Discv5RequestId requestId, ref Rlp.ValueD } ctx.Check(checkPosition); - return new Discv5Nodes(requestId, total, records, owner); + return new NodesMsg(requestId, total, records, owner); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs deleted file mode 100644 index dda24d1efaf6..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5FindNode.cs +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed record Discv5FindNode : Discv5Message -{ - public Discv5FindNode(ReadOnlySpan requestId, ReadOnlySpan distances) - : this(Discv5RequestId.From(requestId), new Discv5Distances(distances)) - { - } - - public Discv5FindNode(Discv5RequestId requestId, Discv5Distances distances, IDisposable? owner = null) - : base(requestId, owner) - => Distances = distances; - - public override Discv5MessageType MessageType => Discv5MessageType.FindNode; - - public Discv5Distances Distances { get; } - - protected override void DisposeCore() => Distances.Dispose(); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs index 2db7e0e4e97c..43a16477477e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs @@ -1,27 +1,37 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Core.Collections; + namespace Nethermind.Network.Discovery.Discv5.Messages; internal abstract record Discv5Message : IDisposable { - public abstract Discv5MessageType MessageType { get; } + public abstract MessageType MessageType { get; } - public Discv5RequestId RequestId { get; } + public RequestId RequestId { get; } - private IDisposable? _owner; + private ArrayPoolSpan _owner; + private bool _hasOwner; - protected Discv5Message(Discv5RequestId requestId, IDisposable? owner = null) + protected Discv5Message(RequestId requestId, ArrayPoolSpan? owner = null) { RequestId = requestId; - _owner = owner; + if (owner is { } ownerValue) + { + _owner = ownerValue; + _hasOwner = true; + } } public void Dispose() { DisposeCore(); - _owner?.Dispose(); - _owner = null; + if (_hasOwner) + { + _owner.Dispose(); + _hasOwner = false; + } } protected virtual void DisposeCore() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs deleted file mode 100644 index f60c5f63ca04..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageBuffer.cs +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Collections; - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed class Discv5MessageBuffer(int length) : IDisposable -{ - private byte[]? _buffer = SafeArrayPool.Shared.Rent(length); - - public Span Span => _buffer.AsSpan(0, Length); - - public ReadOnlyMemory Memory => _buffer.AsMemory(0, Length); - - public int Length { get; } = length; - - public void Dispose() - { - if (_buffer is not null) - { - SafeArrayPool.Shared.Return(_buffer); - _buffer = null; - } - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs deleted file mode 100644 index f65594c3d327..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Nodes.cs +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Network.Enr; - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed record Discv5Nodes : Discv5Message -{ - public Discv5Nodes(ReadOnlySpan requestId, int total, IReadOnlyList records) - : this(Discv5RequestId.From(requestId), total, records) - { - } - - public Discv5Nodes(Discv5RequestId requestId, int total, IReadOnlyList records, IDisposable? owner = null) - : base(requestId, owner) - { - Total = total; - Records = records; - } - - public override Discv5MessageType MessageType => Discv5MessageType.Nodes; - - public int Total { get; } - - public IReadOnlyList Records { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs deleted file mode 100644 index 3c6a2deee457..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Ping.cs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed record Discv5Ping : Discv5Message -{ - public Discv5Ping(ReadOnlySpan requestId, ulong enrSequence) - : this(Discv5RequestId.From(requestId), enrSequence) - { - } - - public Discv5Ping(Discv5RequestId requestId, ulong enrSequence, IDisposable? owner = null) - : base(requestId, owner) - => EnrSequence = enrSequence; - - public override Discv5MessageType MessageType => Discv5MessageType.Ping; - - public ulong EnrSequence { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs deleted file mode 100644 index cfc6d4b77b75..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Pong.cs +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Net; - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed record Discv5Pong : Discv5Message -{ - public Discv5Pong(ReadOnlySpan requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort) - : this(Discv5RequestId.From(requestId), enrSequence, recipientIp, recipientPort) - { - } - - public Discv5Pong(Discv5RequestId requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort, IDisposable? owner = null) - : base(requestId, owner) - { - EnrSequence = enrSequence; - RecipientIp = recipientIp; - RecipientPort = recipientPort; - } - - public override Discv5MessageType MessageType => Discv5MessageType.Pong; - - public ulong EnrSequence { get; } - - public IPAddress RecipientIp { get; } - - public int RecipientPort { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs deleted file mode 100644 index 21383b51451e..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkReq.cs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed record Discv5TalkReq : Discv5Message -{ - public Discv5TalkReq(ReadOnlySpan requestId, ReadOnlyMemory protocol, ReadOnlyMemory request) - : this(Discv5RequestId.From(requestId), protocol, request) - { - } - - public Discv5TalkReq(Discv5RequestId requestId, ReadOnlyMemory protocol, ReadOnlyMemory request, IDisposable? owner = null) - : base(requestId, owner) - { - Protocol = protocol; - Request = request; - } - - public override Discv5MessageType MessageType => Discv5MessageType.TalkReq; - - public ReadOnlyMemory Protocol { get; } - - public ReadOnlyMemory Request { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs deleted file mode 100644 index 25e6f6f04d1b..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5TalkResp.cs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Network.Discovery.Discv5.Messages; - -internal sealed record Discv5TalkResp : Discv5Message -{ - public Discv5TalkResp(ReadOnlySpan requestId, ReadOnlyMemory response) - : this(Discv5RequestId.From(requestId), response) - { - } - - public Discv5TalkResp(Discv5RequestId requestId, ReadOnlyMemory response, IDisposable? owner = null) - : base(requestId, owner) - => Response = response; - - public override Discv5MessageType MessageType => Discv5MessageType.TalkResp; - - public ReadOnlyMemory Response { get; } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs similarity index 92% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs index 34776893e8cc..800d6c9215a1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Distances.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs @@ -6,7 +6,7 @@ namespace Nethermind.Network.Discovery.Discv5.Messages; -internal sealed class Discv5Distances : IReadOnlyList, IDisposable +internal sealed class Distances : IReadOnlyList, IDisposable { private const int InlineCapacity = 3; @@ -15,7 +15,7 @@ internal sealed class Discv5Distances : IReadOnlyList, IDisposable private int _second; private int _third; - public Discv5Distances(ReadOnlySpan distances) + public Distances(ReadOnlySpan distances) : this(distances.Length) { for (int i = 0; i < distances.Length; i++) @@ -24,7 +24,7 @@ public Discv5Distances(ReadOnlySpan distances) } } - internal Discv5Distances(int count) + internal Distances(int count) { Count = count; if (count > InlineCapacity) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs new file mode 100644 index 000000000000..41c86db54ca6 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record FindNodeMsg : Discv5Message +{ + public FindNodeMsg(ReadOnlySpan requestId, ReadOnlySpan distances) + : this(RequestId.From(requestId), new Distances(distances)) + { + } + + public FindNodeMsg(RequestId requestId, Distances distances, ArrayPoolSpan? owner = null) + : base(requestId, owner) + => Distances = distances; + + public override MessageType MessageType => MessageType.FindNode; + + public Distances Distances { get; } + + protected override void DisposeCore() => Distances.Dispose(); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/MessageType.cs similarity index 87% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/MessageType.cs index c5ff03e54339..d8059c7fb58b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5MessageType.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/MessageType.cs @@ -3,7 +3,7 @@ namespace Nethermind.Network.Discovery.Discv5.Messages; -internal enum Discv5MessageType : byte +internal enum MessageType : byte { Ping = 0x01, Pong = 0x02, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs new file mode 100644 index 000000000000..85f42a634654 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; +using Nethermind.Network.Enr; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record NodesMsg : Discv5Message +{ + public NodesMsg(ReadOnlySpan requestId, int total, IReadOnlyList records) + : this(RequestId.From(requestId), total, records) + { + } + + public NodesMsg(RequestId requestId, int total, IReadOnlyList records, ArrayPoolSpan? owner = null) + : base(requestId, owner) + { + Total = total; + Records = records; + } + + public override MessageType MessageType => MessageType.Nodes; + + public int Total { get; } + + public IReadOnlyList Records { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs new file mode 100644 index 000000000000..fc2fbc4a0c56 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record PingMsg : Discv5Message +{ + public PingMsg(ReadOnlySpan requestId, ulong enrSequence) + : this(RequestId.From(requestId), enrSequence) + { + } + + public PingMsg(RequestId requestId, ulong enrSequence, ArrayPoolSpan? owner = null) + : base(requestId, owner) + => EnrSequence = enrSequence; + + public override MessageType MessageType => MessageType.Ping; + + public ulong EnrSequence { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs new file mode 100644 index 000000000000..967c27ce5e79 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record PongMsg : Discv5Message +{ + public PongMsg(ReadOnlySpan requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort) + : this(RequestId.From(requestId), enrSequence, recipientIp, recipientPort) + { + } + + public PongMsg(RequestId requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort, ArrayPoolSpan? owner = null) + : base(requestId, owner) + { + EnrSequence = enrSequence; + RecipientIp = recipientIp; + RecipientPort = recipientPort; + } + + public override MessageType MessageType => MessageType.Pong; + + public ulong EnrSequence { get; } + + public IPAddress RecipientIp { get; } + + public int RecipientPort { get; } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs similarity index 82% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs index dda784144386..8130544c6e23 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5RequestId.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs @@ -3,11 +3,11 @@ namespace Nethermind.Network.Discovery.Discv5.Messages; -internal readonly record struct Discv5RequestId(ulong Value, byte Length) +internal readonly record struct RequestId(ulong Value, byte Length) { public const int MaxLength = sizeof(ulong); - public static Discv5RequestId From(ReadOnlySpan requestId) + public static RequestId From(ReadOnlySpan requestId) { if (requestId.Length > MaxLength) { @@ -20,7 +20,7 @@ public static Discv5RequestId From(ReadOnlySpan requestId) value = (value << 8) | requestId[i]; } - return new Discv5RequestId(value, checked((byte)requestId.Length)); + return new RequestId(value, checked((byte)requestId.Length)); } public void CopyTo(Span destination) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs new file mode 100644 index 000000000000..7755225ebf0a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record TalkReqMsg : Discv5Message +{ + public TalkReqMsg(ReadOnlySpan requestId, ReadOnlyMemory protocol, ReadOnlyMemory request) + : this(RequestId.From(requestId), protocol, request) + { + } + + public TalkReqMsg(RequestId requestId, ReadOnlyMemory protocol, ReadOnlyMemory request, ArrayPoolSpan? owner = null) + : base(requestId, owner) + { + _protocol = protocol; + _request = request; + } + + public override MessageType MessageType => MessageType.TalkReq; + + private readonly ReadOnlyMemory _protocol; + + private readonly ReadOnlyMemory _request; + + internal ReadOnlySpan Protocol => _protocol.Span; + + internal ReadOnlySpan Request => _request.Span; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs new file mode 100644 index 000000000000..0e47e591e64c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Messages; + +internal sealed record TalkRespMsg : Discv5Message +{ + public TalkRespMsg(ReadOnlySpan requestId, ReadOnlyMemory response) + : this(RequestId.From(requestId), response) + { + } + + public TalkRespMsg(RequestId requestId, ReadOnlyMemory response, ArrayPoolSpan? owner = null) + : base(requestId, owner) + => _response = response; + + public override MessageType MessageType => MessageType.TalkResp; + + private readonly ReadOnlyMemory _response; + + internal ReadOnlySpan Response => _response.Span; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs index 01e8bd2abb4b..101766f30f4c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeRecordConverter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Discv5; -internal static class Discv5NodeRecordConverter +internal static class NodeRecordConverter { public static bool TryGetNodeFromEnr(NodeRecord enr, bool allowNonRoutable, [NotNullWhen(true)] out Node? node) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeSource.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeSource.cs index 80581eb4cc51..46e6677ea805 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Discv5NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeSource.cs @@ -11,7 +11,7 @@ namespace Nethermind.Network.Discovery.Discv5; -public class Discv5NodeSource( +public class NodeSource( IKademlia kademlia, KademliaConfig kademliaConfig, ILogManager logManager) @@ -19,7 +19,7 @@ public class Discv5NodeSource( { private const int ChannelCapacity = 64; - private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly ILogger _logger = logManager.GetClassLogger(); private readonly Hash256 _currentNodeHash = kademliaConfig.CurrentNodeId.IdHash; private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256KademliaDistance.Instance.MaxDistance); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs new file mode 100644 index 000000000000..1aded8f9422d --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Packets; + +internal sealed record Challenge(byte[] RequestNonce, byte[] IdNonce, ulong EnrSequence, byte[] ChallengeData); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Packet.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Packet.cs new file mode 100644 index 000000000000..458e409d8958 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Packet.cs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5.Packets; + +internal readonly struct Packet( + PacketFlag flag, + ReadOnlyMemory nonce, + ReadOnlyMemory authData, + ReadOnlyMemory message, + byte[] messageAdBuffer, + int messageAdLength) : IDisposable +{ + private readonly byte[]? _messageAdBuffer = messageAdBuffer; + + public PacketFlag Flag { get; } = flag; + + public ReadOnlyMemory Nonce { get; } = nonce; + + public ReadOnlyMemory AuthData { get; } = authData; + + public ReadOnlyMemory Message { get; } = message; + + public ReadOnlyMemory MessageAd { get; } = messageAdBuffer.AsMemory(0, messageAdLength); + + public ReadOnlyMemory ChallengeData => MessageAd; + + public void Dispose() + { + if (_messageAdBuffer is null) + { + return; + } + + SafeArrayPool.Shared.Return(_messageAdBuffer); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs new file mode 100644 index 000000000000..dc8c0a6f38ef --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -0,0 +1,651 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Buffers.Binary; +using System.Security.Cryptography; +using Autofac.Features.AttributeFilters; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Crypto; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Packets; + +public sealed class PacketCodec( + [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + INodeRecordProvider nodeRecordProvider, + ICryptoRandom cryptoRandom, + IEcdsa ecdsa) : IDisposable +{ + public const int NonceSize = 12; + + private const int MaskingIvSize = 16; + private const int StaticHeaderSize = 23; + private const int NodeIdSize = 32; + private const int WhoAreYouAuthDataSize = 24; + private const int IdNonceSize = 16; + private const int AesKeySize = 16; + private const int AesGcmTagSize = 16; + private const int Version = 1; + private const int IdSignatureSize = 64; + private const int EphemeralPublicKeySize = 33; + private const int HandshakeAuthDataHeadSize = NodeIdSize + 2; + private const int MaxStackPacketBufferSize = 512; + + private static ReadOnlySpan ProtocolId => "discv5"u8; + private static ReadOnlySpan KeyAgreementInfoPrefix => "discovery v5 key agreement"u8; + private static ReadOnlySpan IdentityProofText => "discovery v5 identity proof"u8; + + private readonly PrivateKey _privateKey = nodeKey.Unprotect(); + private readonly PublicKey _publicKey = nodeKey.PublicKey; + private readonly byte[] _localNodeId = nodeKey.PublicKey.Hash.BytesToArray(); + private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; + private readonly ICryptoRandom _cryptoRandom = cryptoRandom; + private readonly IEcdsa _ecdsa = ecdsa; + + public void Dispose() => _privateKey.Dispose(); + + internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encryptionKey, Discv5Message message) + { + Span generatedNonce = stackalloc byte[NonceSize]; + _cryptoRandom.GenerateRandomBytes(generatedNonce); + return EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, generatedNonce, _localNodeId, encryptionKey, message); + } + + internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encryptionKey, Discv5Message message, ReadOnlySpan nonce) + => EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, nonce, _localNodeId, encryptionKey, message); + + internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySpan requestNonce, ulong enrSequence, out Challenge challenge) + { + byte[] idNonce = _cryptoRandom.GenerateRandomBytes(IdNonceSize); + Span authData = stackalloc byte[WhoAreYouAuthDataSize]; + idNonce.CopyTo(authData); + BinaryPrimitives.WriteUInt64BigEndian(authData[IdNonceSize..], enrSequence); + + byte[] packet = EncodePacket(destinationNodeId, PacketFlag.WhoAreYou, requestNonce, authData, default, null, out byte[] challengeData); + challenge = new Challenge(requestNonce.ToArray(), idNonce, enrSequence, challengeData); + return packet; + } + + internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Discv5Message message, out Session session) + { + using PrivateKey ephemeralKey = new PrivateKeyGenerator(_cryptoRandom).Generate(); + DeriveKeys( + destination, + ephemeralKey, + _localNodeId, + destination.Hash.Bytes, + challenge.ChallengeData, + out byte[] initiatorKey, + out byte[] recipientKey); + + byte[] ephemeralPublicKey = ephemeralKey.CompressedPublicKey.Bytes; + byte[] record = challenge.EnrSequence < _nodeRecordProvider.Current.EnrSequence + ? _nodeRecordProvider.Current.ToRlpBytes() + : []; + + int authDataLength = HandshakeAuthDataHeadSize + IdSignatureSize + EphemeralPublicKeySize + record.Length; + byte[]? rentedAuthData = null; + Span authData = authDataLength <= MaxStackPacketBufferSize + ? stackalloc byte[authDataLength] + : (rentedAuthData = SafeArrayPool.Shared.Rent(authDataLength)).AsSpan(0, authDataLength); + + try + { + _localNodeId.CopyTo(authData); + authData[NodeIdSize] = IdSignatureSize; + authData[NodeIdSize + 1] = EphemeralPublicKeySize; + SignIdNonce(challenge.ChallengeData, ephemeralPublicKey, destination.Hash.Bytes, authData.Slice(HandshakeAuthDataHeadSize, IdSignatureSize)); + ephemeralPublicKey.CopyTo(authData[(HandshakeAuthDataHeadSize + IdSignatureSize)..]); + record.CopyTo(authData[(HandshakeAuthDataHeadSize + IdSignatureSize + EphemeralPublicKeySize)..]); + + session = new Session(destination, recipientKey, initiatorKey); + Span nonce = stackalloc byte[NonceSize]; + _cryptoRandom.GenerateRandomBytes(nonce); + return EncodePacket(destination.Hash.Bytes, PacketFlag.Handshake, nonce, authData, initiatorKey, message); + } + finally + { + if (rentedAuthData is not null) + { + SafeArrayPool.Shared.Return(rentedAuthData); + } + } + } + + internal bool TryDecode(byte[] packet, out Packet decoded) + => TryDecode(packet.AsMemory(), _localNodeId, out decoded); + + internal static bool TryDecode(byte[] packet, ReadOnlySpan localNodeId, out Packet decoded) + => TryDecode(packet.AsMemory(), localNodeId, out decoded); + + internal static bool TryDecode(ReadOnlyMemory packetMemory, ReadOnlySpan localNodeId, out Packet decoded) + { + decoded = default; + ReadOnlySpan packet = packetMemory.Span; + if (packet.Length < MaskingIvSize + StaticHeaderSize) + { + return false; + } + + ReadOnlySpan maskingIv = packet[..MaskingIvSize]; + Span staticHeader = stackalloc byte[StaticHeaderSize]; + AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize), staticHeader); + ReadOnlySpan protocolId = ProtocolId; + if (!staticHeader[..protocolId.Length].SequenceEqual(protocolId)) + { + return false; + } + + int authDataSize = BinaryPrimitives.ReadUInt16BigEndian(staticHeader[(StaticHeaderSize - sizeof(ushort))..]); + int headerSize = StaticHeaderSize + authDataSize; + if (packet.Length < MaskingIvSize + headerSize) + { + return false; + } + + int messageAdLength = MaskingIvSize + headerSize; + byte[] messageAdBuffer = SafeArrayPool.Shared.Rent(messageAdLength); + Span messageAd = messageAdBuffer.AsSpan(0, messageAdLength); + maskingIv.CopyTo(messageAd); + Span header = messageAd[MaskingIvSize..]; + + try + { + AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, headerSize), header); + if (!header[..protocolId.Length].SequenceEqual(protocolId)) + { + SafeArrayPool.Shared.Return(messageAdBuffer); + return false; + } + + int version = BinaryPrimitives.ReadUInt16BigEndian(header[protocolId.Length..]); + if (version != Version) + { + SafeArrayPool.Shared.Return(messageAdBuffer); + return false; + } + + int nonceOffset = MaskingIvSize + protocolId.Length + sizeof(ushort) + sizeof(byte); + int authDataOffset = MaskingIvSize + StaticHeaderSize; + PacketFlag flag = (PacketFlag)header[protocolId.Length + sizeof(ushort)]; + decoded = new Packet( + flag, + messageAdBuffer.AsMemory(nonceOffset, NonceSize), + messageAdBuffer.AsMemory(authDataOffset, authDataSize), + packetMemory.Slice(MaskingIvSize + headerSize), + messageAdBuffer, + messageAdLength); + return true; + } + catch + { + SafeArrayPool.Shared.Return(messageAdBuffer); + throw; + } + } + + internal bool TryDecryptMessage(Packet packet, ReadOnlySpan encryptionKey, out Discv5Message message) + => TryDecryptMessageForTest(packet, encryptionKey, out message); + + internal static bool TryDecryptMessageForTest(Packet packet, ReadOnlySpan encryptionKey, out Discv5Message message) + { + message = null!; + ReadOnlySpan encryptedMessage = packet.Message.Span; + if (packet.Message.Length < AesGcmTagSize) + { + return false; + } + + ArrayPoolSpan plaintext = new(packet.Message.Length - AesGcmTagSize); + bool ownerTransferred = false; + try + { + using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); + aesGcm.Decrypt( + packet.Nonce.Span, + encryptedMessage[..plaintext.Length], + encryptedMessage.Slice(plaintext.Length, AesGcmTagSize), + plaintext, + packet.MessageAd.Span); + + ownerTransferred = true; + message = MessageCodec.Decode(plaintext.AsReadOnlyMemory(), plaintext); + return true; + } + catch (CryptographicException) + { + if (!ownerTransferred) + { + plaintext.Dispose(); + } + + return false; + } + catch + { + if (!ownerTransferred) + { + plaintext.Dispose(); + } + + throw; + } + } + + internal Challenge DecodeWhoAreYou(Packet packet) + { + if (packet.AuthData.Length != WhoAreYouAuthDataSize) + { + throw new RlpException("Invalid WHOAREYOU authdata length."); + } + + byte[] idNonce = packet.AuthData.Span[..IdNonceSize].ToArray(); + ulong enrSequence = BinaryPrimitives.ReadUInt64BigEndian(packet.AuthData.Span[IdNonceSize..]); + return new Challenge(packet.Nonce.ToArray(), idNonce, enrSequence, packet.ChallengeData.ToArray()); + } + + internal bool TryDecryptHandshake( + Packet packet, + Challenge challenge, + NodeRecord? knownRecord, + out Session session, + out Discv5Message message, + out NodeRecord? nodeRecord) + { + session = null!; + message = null!; + nodeRecord = null; + + if (!TryReadHandshakeAuthData(packet.AuthData, out Hash256 sourceNodeId, out ReadOnlyMemory idSignature, out CompressedPublicKey ephemeralPublicKey, out ReadOnlyMemory recordBytes)) + { + return false; + } + + if (recordBytes.Length > 0) + { + try + { + nodeRecord = NodeRecord.FromBytes(recordBytes.Span); + } + catch (Exception) + { + return false; + } + } + + NodeRecord? record = nodeRecord ?? knownRecord; + CompressedPublicKey? remoteCompressedPublicKey = record?.GetObj(EnrContentKey.SecP256k1); + if (remoteCompressedPublicKey is null) + { + return false; + } + + PublicKey remotePublicKey = remoteCompressedPublicKey.Decompress(); + if (!remotePublicKey.Hash.Equals(sourceNodeId)) + { + return false; + } + + if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature.Span, challenge.ChallengeData, ephemeralPublicKey.Bytes, _localNodeId)) + { + return false; + } + + DeriveKeys(ephemeralPublicKey, sourceNodeId.Bytes, _localNodeId, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); + + if (!TryDecryptMessage(packet, initiatorKey, out message)) + { + return false; + } + + session = new Session(remotePublicKey, initiatorKey, recipientKey); + return true; + } + + internal static bool TryGetSourceNodeId(Packet packet, out Hash256 sourceNodeId) + { + sourceNodeId = null!; + switch (packet.Flag) + { + case PacketFlag.Ordinary when packet.AuthData.Length == NodeIdSize: + sourceNodeId = new Hash256(packet.AuthData.Span); + return true; + case PacketFlag.Handshake when packet.AuthData.Length >= HandshakeAuthDataHeadSize: + sourceNodeId = new Hash256(packet.AuthData.Span[..NodeIdSize]); + return true; + default: + return false; + } + } + + private byte[] EncodePacket( + ReadOnlySpan destinationNodeId, + PacketFlag flag, + ReadOnlySpan nonce, + ReadOnlySpan authData, + ReadOnlySpan encryptionKey, + Discv5Message? message) + => EncodePacket(destinationNodeId, flag, nonce, authData, encryptionKey, message, out _); + + private byte[] EncodePacket( + ReadOnlySpan destinationNodeId, + PacketFlag flag, + ReadOnlySpan nonce, + ReadOnlySpan authData, + ReadOnlySpan encryptionKey, + Discv5Message? message, + out byte[] messageAd) + { + if (message is null) + { + return EncodePacketCore(destinationNodeId, flag, nonce, authData, default, default, out messageAd); + } + + using ArrayPoolSpan encodedMessage = MessageCodec.Encode(message); + return EncodePacketCore(destinationNodeId, flag, nonce, authData, encryptionKey, encodedMessage, out messageAd); + } + + private byte[] EncodePacketCore( + ReadOnlySpan destinationNodeId, + PacketFlag flag, + ReadOnlySpan nonce, + ReadOnlySpan authData, + ReadOnlySpan encryptionKey, + ReadOnlySpan plaintext, + out byte[] messageAd) + { + int headerLength = StaticHeaderSize + authData.Length; + int messageAdLength = MaskingIvSize + headerLength; + int encryptedMessageLength = plaintext.IsEmpty ? 0 : plaintext.Length + AesGcmTagSize; + + byte[] packet = new byte[MaskingIvSize + headerLength + encryptedMessageLength]; + Span maskingIv = packet.AsSpan(0, MaskingIvSize); + _cryptoRandom.GenerateRandomBytes(maskingIv); + + byte[]? rentedMessageAd = null; + Span messageAdBuffer = messageAdLength <= MaxStackPacketBufferSize + ? stackalloc byte[messageAdLength] + : (rentedMessageAd = SafeArrayPool.Shared.Rent(messageAdLength)).AsSpan(0, messageAdLength); + + try + { + maskingIv.CopyTo(messageAdBuffer); + WriteHeader(messageAdBuffer[MaskingIvSize..], flag, nonce, authData); + + if (!plaintext.IsEmpty) + { + EncryptMessage( + encryptionKey, + nonce, + plaintext, + messageAdBuffer, + packet.AsSpan(MaskingIvSize + headerLength, encryptedMessageLength)); + } + + AesCtrTransform(destinationNodeId[..AesKeySize], maskingIv, messageAdBuffer.Slice(MaskingIvSize, headerLength), packet.AsSpan(MaskingIvSize, headerLength)); + messageAd = plaintext.IsEmpty + ? messageAdBuffer.ToArray() + : []; + return packet; + } + finally + { + if (rentedMessageAd is not null) + { + SafeArrayPool.Shared.Return(rentedMessageAd); + } + } + } + + private static void WriteHeader(Span header, PacketFlag flag, ReadOnlySpan nonce, ReadOnlySpan authData) + { + if (nonce.Length != NonceSize) + { + throw new ArgumentException($"Nonce must be {NonceSize} bytes.", nameof(nonce)); + } + + ReadOnlySpan protocolId = ProtocolId; + protocolId.CopyTo(header); + BinaryPrimitives.WriteUInt16BigEndian(header[protocolId.Length..], Version); + header[protocolId.Length + sizeof(ushort)] = (byte)flag; + nonce.CopyTo(header.Slice(protocolId.Length + sizeof(ushort) + sizeof(byte), NonceSize)); + BinaryPrimitives.WriteUInt16BigEndian(header[(StaticHeaderSize - sizeof(ushort))..], checked((ushort)authData.Length)); + authData.CopyTo(header[StaticHeaderSize..]); + } + + private static void EncryptMessage( + ReadOnlySpan encryptionKey, + ReadOnlySpan nonce, + ReadOnlySpan plaintext, + ReadOnlySpan messageAd, + Span encrypted) + { + using AesGcm aesGcm = new(encryptionKey, AesGcmTagSize); + aesGcm.Encrypt( + nonce, + plaintext, + encrypted[..plaintext.Length], + encrypted.Slice(plaintext.Length, AesGcmTagSize), + messageAd); + } + + private static void AesCtrTransform(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan input, Span output) + { + if (output.Length < input.Length) + { + throw new ArgumentException("Output span must be at least as long as input.", nameof(output)); + } + + using Aes aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.SetKey(key); + + Span counter = stackalloc byte[MaskingIvSize]; + iv.CopyTo(counter); + Span keyStream = stackalloc byte[MaskingIvSize]; + + int offset = 0; + while (offset < input.Length) + { + aes.EncryptEcb(counter, keyStream, PaddingMode.None); + + int blockLength = Math.Min(MaskingIvSize, input.Length - offset); + for (int i = 0; i < blockLength; i++) + { + output[offset + i] = (byte)(input[offset + i] ^ keyStream[i]); + } + + IncrementCounter(counter); + offset += blockLength; + } + } + + private static void IncrementCounter(Span counter) + { + for (int i = counter.Length - 1; i >= 0; i--) + { + counter[i]++; + if (counter[i] != 0) + { + return; + } + } + } + + private static void DeriveKeys( + PublicKey remotePublicKey, + PrivateKey ephemeralPrivateKey, + ReadOnlySpan initiatorNodeId, + ReadOnlySpan recipientNodeId, + ReadOnlySpan challengeData, + out byte[] initiatorKey, + out byte[] recipientKey) + { + byte[] secret = SecP256k1Agreement.AgreeCompressed(remotePublicKey, ephemeralPrivateKey); + DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); + } + + private void DeriveKeys( + CompressedPublicKey ephemeralPublicKey, + ReadOnlySpan initiatorNodeId, + ReadOnlySpan recipientNodeId, + ReadOnlySpan challengeData, + out byte[] initiatorKey, + out byte[] recipientKey) + { + byte[] secret = SecP256k1Agreement.AgreeCompressed(ephemeralPublicKey, _privateKey); + DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); + } + + private static void DeriveKeys( + byte[] secret, + ReadOnlySpan initiatorNodeId, + ReadOnlySpan recipientNodeId, + ReadOnlySpan challengeData, + out byte[] initiatorKey, + out byte[] recipientKey) + { + Span prk = stackalloc byte[SHA256.HashSizeInBytes]; + HMACSHA256.HashData(challengeData, secret, prk); + + ReadOnlySpan infoPrefix = KeyAgreementInfoPrefix; + Span info = stackalloc byte[infoPrefix.Length + NodeIdSize + NodeIdSize]; + infoPrefix.CopyTo(info); + initiatorNodeId.CopyTo(info[infoPrefix.Length..]); + recipientNodeId.CopyTo(info[(infoPrefix.Length + NodeIdSize)..]); + + Span hkdfInput = stackalloc byte[info.Length + 1]; + info.CopyTo(hkdfInput); + hkdfInput[^1] = 1; + + Span keyData = stackalloc byte[AesKeySize * 2]; + HMACSHA256.HashData(prk, hkdfInput, keyData); + initiatorKey = keyData[..AesKeySize].ToArray(); + recipientKey = keyData[AesKeySize..].ToArray(); + } + + internal static (byte[] InitiatorKey, byte[] RecipientKey) DeriveKeysForTest( + byte[] secret, + byte[] initiatorNodeId, + byte[] recipientNodeId, + byte[] challengeData) + { + DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out byte[] initiatorKey, out byte[] recipientKey); + return (initiatorKey, recipientKey); + } + + private void SignIdNonce( + ReadOnlySpan challengeData, + ReadOnlySpan ephemeralPublicKey, + ReadOnlySpan recipientNodeId, + Span destination) + { + Span signingHash = stackalloc byte[SHA256.HashSizeInBytes]; + CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId, signingHash); + Signature signature = _ecdsa.Sign(_privateKey, new ValueHash256(signingHash)); + signature.Bytes[..IdSignatureSize].CopyTo(destination); + } + + private bool VerifyIdSignature( + CompressedPublicKey signer, + ReadOnlySpan signatureBytes, + ReadOnlySpan challengeData, + ReadOnlySpan ephemeralPublicKey, + ReadOnlySpan recipientNodeId) + { + Span signingHash = stackalloc byte[SHA256.HashSizeInBytes]; + CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId, signingHash); + for (int recoveryId = 0; recoveryId <= 1; recoveryId++) + { + Signature signature = new(signatureBytes, recoveryId); + CompressedPublicKey? recovered = _ecdsa.RecoverCompressedPublicKey(signature, new ValueHash256(signingHash)); + if (signer.Equals(recovered)) + { + return true; + } + } + + return false; + } + + internal static byte[] CalculateIdSignatureHashForTest(byte[] challengeData, byte[] ephemeralPublicKey, byte[] recipientNodeId) + { + byte[] signingHash = new byte[SHA256.HashSizeInBytes]; + CalculateIdSignatureHash(challengeData, ephemeralPublicKey, recipientNodeId, signingHash); + return signingHash; + } + + private static void CalculateIdSignatureHash( + ReadOnlySpan challengeData, + ReadOnlySpan ephemeralPublicKey, + ReadOnlySpan recipientNodeId, + Span destination) + { + ReadOnlySpan identityProofText = IdentityProofText; + int signingInputLength = identityProofText.Length + challengeData.Length + ephemeralPublicKey.Length + recipientNodeId.Length; + byte[]? rentedSigningInput = null; + Span signingInput = signingInputLength <= MaxStackPacketBufferSize + ? stackalloc byte[signingInputLength] + : (rentedSigningInput = SafeArrayPool.Shared.Rent(signingInputLength)).AsSpan(0, signingInputLength); + + try + { + identityProofText.CopyTo(signingInput); + challengeData.CopyTo(signingInput[identityProofText.Length..]); + ephemeralPublicKey.CopyTo(signingInput[(identityProofText.Length + challengeData.Length)..]); + recipientNodeId.CopyTo(signingInput[(identityProofText.Length + challengeData.Length + ephemeralPublicKey.Length)..]); + SHA256.HashData(signingInput, destination); + } + finally + { + if (rentedSigningInput is not null) + { + SafeArrayPool.Shared.Return(rentedSigningInput); + } + } + } + + private static bool TryReadHandshakeAuthData( + ReadOnlyMemory authDataMemory, + out Hash256 sourceNodeId, + out ReadOnlyMemory idSignature, + out CompressedPublicKey ephemeralPublicKey, + out ReadOnlyMemory record) + { + sourceNodeId = null!; + idSignature = ReadOnlyMemory.Empty; + ephemeralPublicKey = null!; + record = ReadOnlyMemory.Empty; + + ReadOnlySpan authData = authDataMemory.Span; + if (authData.Length < HandshakeAuthDataHeadSize) + { + return false; + } + + sourceNodeId = new Hash256(authData[..NodeIdSize]); + int signatureSize = authData[NodeIdSize]; + int ephemeralKeySize = authData[NodeIdSize + 1]; + if (signatureSize != IdSignatureSize || ephemeralKeySize != EphemeralPublicKeySize) + { + return false; + } + + int signatureOffset = HandshakeAuthDataHeadSize; + int ephemeralKeyOffset = signatureOffset + signatureSize; + int recordOffset = ephemeralKeyOffset + ephemeralKeySize; + if (authData.Length < recordOffset) + { + return false; + } + + idSignature = authDataMemory.Slice(signatureOffset, signatureSize); + ephemeralPublicKey = new CompressedPublicKey(authData.Slice(ephemeralKeyOffset, ephemeralKeySize)); + record = authDataMemory[recordOffset..]; + return true; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketFlag.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketFlag.cs new file mode 100644 index 000000000000..d6b6b2dbcf5c --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketFlag.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Network.Discovery.Discv5.Packets; + +internal enum PacketFlag : byte +{ + Ordinary = 0, + WhoAreYou = 1, + Handshake = 2 +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs new file mode 100644 index 000000000000..404656b35bff --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Buffers.Binary; +using Nethermind.Core.Crypto; +using Nethermind.Crypto; + +namespace Nethermind.Network.Discovery.Discv5.Packets; + +internal sealed record Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] WriteKey) +{ + private long _nonceCounter; + + public void WriteNextNonce(ICryptoRandom random, Span nonce) + { + if (nonce.Length != PacketCodec.NonceSize) + { + throw new ArgumentException($"Nonce must be {PacketCodec.NonceSize} bytes.", nameof(nonce)); + } + + BinaryPrimitives.WriteUInt32BigEndian(nonce, unchecked((uint)Interlocked.Increment(ref _nonceCounter))); + random.GenerateRandomBytes(nonce[sizeof(uint)..]); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs index d3a580df83ba..d5c5b4c2d1f6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs @@ -116,10 +116,7 @@ protected void UseKademliaServices(IKademliaNodeSource kademliaNodeSource, IKade protected virtual void Initialize() { - if (Logger.IsDebug) - { - Logger.Debug($"Discovery : udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); - } + if (Logger.IsDebug) Logger.Debug($"Discovery : udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); ThisNodeInfo.AddInfo("Discovery :", $"udp://{_networkConfig.ExternalIp}:{_networkConfig.DiscoveryPort}"); } From 148c82a814b9c34c960a7f72ce633342e5ac9031 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 1 Jun 2026 19:41:51 +0300 Subject: [PATCH 128/182] Refactor more --- .../Discv5/CodecTests.cs | 13 +- .../Handlers/NodesResponseHandlerTests.cs | 2 +- .../Discv5/KademliaAdapterTests.cs | 2 +- .../Discv5/NodeSourceTests.cs | 2 +- .../Discv5/WireTests.cs | 1 + .../NettyDiscoveryV5HandlerTests.cs | 12 +- .../Discv5/DiscoveryV5App.cs | 4 +- .../Discv5/{ => Kademlia}/AdapterState.cs | 4 +- .../Handlers/IResponseHandler.cs | 2 +- .../Handlers/NodesResponseHandler.cs | 2 +- .../Handlers/PongResponseHandler.cs | 2 +- .../Discv5/{ => Kademlia}/IKademliaAdapter.cs | 2 +- .../Discv5/{ => Kademlia}/KademliaAdapter.cs | 56 ++-- .../Discv5/{ => Kademlia}/KademliaModule.cs | 4 +- .../Discv5/{ => Kademlia}/NodeSource.cs | 2 +- .../Discv5/MessageCodec.cs | 262 ++++-------------- .../Discv5/NettyDiscoveryV5Handler.cs | 35 ++- .../Discv5/Packets/PacketCodec.cs | 20 +- .../Discv5/PooledUdpReceiveResult.cs | 25 ++ .../Serializers/FindNodeMsgSerializer.cs | 71 +++++ .../Discv5/Serializers/MsgSerializerBase.cs | 60 ++++ .../Discv5/Serializers/NodesMsgSerializer.cs | 70 +++++ .../Discv5/Serializers/PingMsgSerializer.cs | 23 ++ .../Discv5/Serializers/PongMsgSerializer.cs | 61 ++++ .../Serializers/TalkReqMsgSerializer.cs | 24 ++ .../Serializers/TalkRespMsgSerializer.cs | 23 ++ .../Nethermind.Network.Enr/EnrContentEntry.cs | 7 + .../Nethermind.Network.Enr/EthEntry.cs | 2 +- .../Nethermind.Network.Enr/NodeRecord.cs | 4 +- .../Nethermind.Network.Enr/Tcp6Entry.cs | 2 +- .../Nethermind.Network.Enr/TcpEntry.cs | 2 +- .../Nethermind.Network.Enr/Udp6Entry.cs | 2 +- .../Nethermind.Network.Enr/UdpEntry.cs | 2 +- 33 files changed, 538 insertions(+), 267 deletions(-) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/AdapterState.cs (89%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/Handlers/IResponseHandler.cs (92%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/Handlers/NodesResponseHandler.cs (97%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/Handlers/PongResponseHandler.cs (90%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/IKademliaAdapter.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/KademliaAdapter.cs (93%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/KademliaModule.cs (95%) rename src/Nethermind/Nethermind.Network.Discovery/Discv5/{ => Kademlia}/NodeSource.cs (97%) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/PooledUdpReceiveResult.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 405f472f5671..0907b9b7123d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -208,7 +208,7 @@ public void MessageCodec_Roundtrips_TalkReq() using TalkReqMsg message = new([0, 0, 0, 3], "eth"u8.ToArray(), new byte[] { 1, 2, 3, 4 }); using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.Decode(encoded); + using Discv5Message decoded = MessageCodec.DecodeCopied(encoded); Assert.That(decoded, Is.InstanceOf()); TalkReqMsg decodedTalkReq = (TalkReqMsg)decoded; @@ -223,7 +223,7 @@ public void MessageCodec_Roundtrips_TalkResp() using TalkRespMsg message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.Decode(encoded); + using Discv5Message decoded = MessageCodec.DecodeCopied(encoded); Assert.That(decoded, Is.InstanceOf()); TalkRespMsg decodedTalkResp = (TalkRespMsg)decoded; @@ -231,6 +231,15 @@ public void MessageCodec_Roundtrips_TalkResp() Assert.That(decodedTalkResp.Response.ToArray(), Is.EqualTo(message.Response.ToArray())); } + [Test] + public void MessageCodec_Requires_Owned_Memory_For_Talk_Messages() + { + using TalkRespMsg message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); + using ArrayPoolSpan encoded = MessageCodec.Encode(message); + + Assert.That(() => MessageCodec.Decode(encoded), Throws.TypeOf()); + } + [Test] public void MessageCodec_Roundtrips_Nodes_From_NonZero_ArraySegment() { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index cd4cbfe62497..9235c1846431 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -6,7 +6,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv5.Handlers; +using Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 4c19d571e4f4..32b74c76799e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -8,7 +8,7 @@ using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index 920812ba2380..f00281764caa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -9,7 +9,7 @@ using Nethermind.Core.Test.Builders; using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index 4551210b5003..497fc020ac05 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -19,6 +19,7 @@ using Nethermind.Logging; using Nethermind.Network.Enr; using Nethermind.Network.Discovery.Discv5; +using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Serialization.Rlp; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index e392f86d183a..0bad6cb76447 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -59,7 +59,7 @@ public async Task ForwardsReceivedMessageToReader() IPEndPoint to = IPEndPoint.Parse("127.0.0.1:10001"); using CancellationTokenSource cancellationSource = new(10_000); - IAsyncEnumerator enumerator = _handler + await using IAsyncEnumerator enumerator = _handler .ReadMessagesAsync(cancellationSource.Token) .GetAsyncEnumerator(cancellationSource.Token); ValueTask readTask = enumerator.MoveNextAsync(); @@ -69,9 +69,9 @@ public async Task ForwardsReceivedMessageToReader() _handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer(data), from, to)); Assert.That(await readTask, Is.True); - UdpReceiveResult forwardedPacket = enumerator.Current; + PooledUdpReceiveResult forwardedPacket = enumerator.Current; - Assert.That(forwardedPacket.Buffer, Is.EqualTo(data)); + Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(from)); } @@ -85,7 +85,7 @@ public async Task SkipsMessagesOfInvalidSize(int size) IPEndPoint to = IPEndPoint.Parse("127.0.0.1:10001"); using CancellationTokenSource cancellationSource = new(10_000); - IAsyncEnumerator enumerator = _handler + await using IAsyncEnumerator enumerator = _handler .ReadMessagesAsync(cancellationSource.Token) .GetAsyncEnumerator(cancellationSource.Token); ValueTask readTask = enumerator.MoveNextAsync(); @@ -98,7 +98,7 @@ public async Task SkipsMessagesOfInvalidSize(int size) _handler.Close(); Assert.That(await readTask, Is.True); - Assert.That(enumerator.Current.Buffer, Is.EqualTo(data)); + Assert.That(enumerator.Current.Buffer.ToArray(), Is.EqualTo(data)); Assert.That(await enumerator.MoveNextAsync(), Is.False); } @@ -106,7 +106,7 @@ public async Task SkipsMessagesOfInvalidSize(int size) public async Task ChannelInactiveStopsReader() { using CancellationTokenSource cancellationSource = new(10_000); - IAsyncEnumerator enumerator = _handler + await using IAsyncEnumerator enumerator = _handler .ReadMessagesAsync(cancellationSource.Token) .GetAsyncEnumerator(cancellationSource.Token); ValueTask readTask = enumerator.MoveNextAsync(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index ef01b3e539e4..6164e106b749 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -16,9 +16,11 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; +using Discv5KademliaModule = Nethermind.Network.Discovery.Discv5.Kademlia.KademliaModule; [assembly: InternalsVisibleTo("Nethermind.Network.Discovery.Test")] @@ -63,7 +65,7 @@ public DiscoveryV5App( builder.RegisterInstance(discoveryConfig).As(); builder.RegisterInstance(timestamper).As(); builder - .AddModule(new KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddModule(new Discv5KademliaModule(nodeKey.PublicKey, bootNodes)) .AddSingleton(); configureServices?.Invoke(builder); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs similarity index 89% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/AdapterState.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs index 49b41030fc19..decc8fab1ebc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/AdapterState.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs @@ -8,7 +8,7 @@ using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5; +namespace Nethermind.Network.Discovery.Discv5.Kademlia; internal readonly record struct SessionKey(Hash256 NodeId, IPEndPoint Endpoint); @@ -18,6 +18,8 @@ namespace Nethermind.Network.Discovery.Discv5; internal readonly record struct ResponseKey(Hash256 NodeId, RequestId RequestId, MessageType MessageType); +internal readonly record struct SentChallengeExpiry(ChallengeKey Key, long CreatedAtMilliseconds); + internal readonly record struct NonceKey(ulong Prefix, uint Suffix) { public static NonceKey From(ReadOnlySpan nonce) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/IResponseHandler.cs similarity index 92% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/IResponseHandler.cs index f5642454bd9c..f90eb60f6a21 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/IResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/IResponseHandler.cs @@ -3,7 +3,7 @@ using Nethermind.Network.Discovery.Discv5.Messages; -namespace Nethermind.Network.Discovery.Discv5.Handlers; +namespace Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; internal interface IResponseHandler { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 61d15c593f8b..79730f0b387a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -7,7 +7,7 @@ using Nethermind.Network.Enr; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5.Handlers; +namespace Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; internal sealed class NodesResponseHandler(Node receiver, Distances requestedDistances, IKademliaDistance distanceCalculator) : ResponseHandler(MessageType.Nodes) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/PongResponseHandler.cs similarity index 90% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/PongResponseHandler.cs index f7101f38a9d3..4efa8a489b45 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Handlers/PongResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/PongResponseHandler.cs @@ -4,7 +4,7 @@ using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5.Handlers; +namespace Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; internal sealed class PongResponseHandler(Node receiver) : ResponseHandler(MessageType.Pong) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/IKademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IKademliaAdapter.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/IKademliaAdapter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IKademliaAdapter.cs index 061074a10e49..051863c52ca1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/IKademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IKademliaAdapter.cs @@ -5,7 +5,7 @@ using Nethermind.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5; +namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Adapts discv5 distance-based FINDNODE requests to the protocol-specific Kademlia routing table. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs similarity index 93% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaAdapter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 9ebd710b907e..26717bdc7112 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -8,14 +8,14 @@ using Nethermind.Core.Crypto; using Nethermind.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv5.Handlers; +using Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Enr; using Nethermind.Logging; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5; +namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Maps discv5 FINDNODE distance requests onto the protocol-specific Kademlia table. @@ -50,6 +50,8 @@ public class KademliaAdapter( private readonly ILogger _logger = logManager.GetClassLogger(); private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions"); private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); + private readonly Queue _sentChallengeExpiries = new(); + private readonly object _sentChallengeExpiriesLock = new(); private long _lastSentChallengeTrimMilliseconds; private readonly LruCache _pendingByNonce = new(MaxPendingRequests, "discv5 pending requests"); private readonly LruCache _responseHandlers = new(MaxResponseHandlers, "discv5 response handlers"); @@ -135,7 +137,7 @@ public async Task RunAsync(CancellationToken token) { try { - await foreach (UdpReceiveResult result in discoveryHandler.ReadMessagesAsync(token)) + await foreach (PooledUdpReceiveResult result in discoveryHandler.ReadMessagesAsync(token)) { await HandlePacket(result, token); } @@ -244,7 +246,7 @@ private async Task SendResponse(Node receiver, Discv5Message message, Cancellati await discoveryHandler.SendAsync(packet, receiver.Address); } - private async Task HandlePacket(UdpReceiveResult udpPacket, CancellationToken token) + private async Task HandlePacket(PooledUdpReceiveResult udpPacket, CancellationToken token) { if (!packetCodec.TryDecode(udpPacket.Buffer, out Packet packet)) { @@ -294,7 +296,7 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256 nodeId)) + if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) { return; } @@ -319,7 +321,7 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256 nodeId)) + if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) { return; } @@ -337,24 +339,24 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat return; } - NodeRecord? messageRecord = knownRecord; - if (nodeRecord is not null) + try { - if (!HasExpectedNodeId(nodeRecord, nodeId)) + NodeRecord? messageRecord = knownRecord; + if (nodeRecord is not null) { - return; - } + if (!HasExpectedNodeId(nodeRecord, nodeId)) + { + return; + } - if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) - { - SetKnownRecord(nodeId, nodeRecord); - messageRecord = nodeRecord; + if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + { + SetKnownRecord(nodeId, nodeRecord); + messageRecord = nodeRecord; + } } - } - SetSession(new SessionKey(nodeId, endpoint), session); - try - { + SetSession(new SessionKey(nodeId, endpoint), session); await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); } finally @@ -612,6 +614,10 @@ private void SetSentChallenge(ChallengeKey challengeKey, Challenge challenge, by long now = Environment.TickCount64; TryTrimExpiredChallenges(now); _sentChallenges.Set(challengeKey, new SentChallenge(challenge, packet, now)); + lock (_sentChallengeExpiriesLock) + { + _sentChallengeExpiries.Enqueue(new SentChallengeExpiry(challengeKey, now)); + } } private void TryTrimExpiredChallenges(long now) @@ -628,11 +634,17 @@ private void TryTrimExpiredChallenges(long now) private void TrimExpiredChallenges(long now) { - foreach (KeyValuePair kv in _sentChallenges.ToArray()) + lock (_sentChallengeExpiriesLock) { - if (IsExpired(kv.Value, now)) + while (_sentChallengeExpiries.TryPeek(out SentChallengeExpiry expiry) && + now - expiry.CreatedAtMilliseconds > SentChallengeTtlMilliseconds) { - _sentChallenges.TryRemove(kv.Key, out _); + _sentChallengeExpiries.Dequeue(); + if (_sentChallenges.TryGet(expiry.Key, out SentChallenge challenge) && + challenge.CreatedAtMilliseconds == expiry.CreatedAtMilliseconds) + { + _sentChallenges.TryRemove(expiry.Key, out _); + } } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs similarity index 95% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaModule.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs index d9540ae3ed48..af9c577a3423 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs @@ -5,11 +5,11 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Discovery.Discv5.Packets; +using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5; +namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Specifies the protocol-specific Kademlia services used by discv5. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeSource.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index 46e6677ea805..25d86b8e7dcb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -9,7 +9,7 @@ using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv5; +namespace Nethermind.Network.Discovery.Discv5.Kademlia; public class NodeSource( IKademlia kademlia, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs index 75c1e94e0455..80771be9386c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs @@ -1,16 +1,22 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Net; using Nethermind.Core.Collections; using Nethermind.Network.Discovery.Discv5.Messages; -using Nethermind.Network.Enr; +using Nethermind.Network.Discovery.Discv5.Serializers; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Discovery.Discv5; internal static class MessageCodec { + private static readonly PingMsgSerializer PingSerializer = new(); + private static readonly PongMsgSerializer PongSerializer = new(); + private static readonly FindNodeMsgSerializer FindNodeSerializer = new(); + private static readonly NodesMsgSerializer NodesSerializer = new(); + private static readonly TalkReqMsgSerializer TalkReqSerializer = new(); + private static readonly TalkRespMsgSerializer TalkRespSerializer = new(); + public static ArrayPoolSpan Encode(Discv5Message message) { int contentLength = GetContentLength(message); @@ -33,11 +39,33 @@ public static ArrayPoolSpan Encode(Discv5Message message) } public static Discv5Message Decode(ReadOnlySpan message) - => Decode(message, default, null); + { + if (NeedsOwnedMessage(message)) + { + throw new RlpException("discv5 TALK messages require owned message memory. Use DecodeOwned or DecodeCopied."); + } - public static Discv5Message Decode(ReadOnlyMemory message, ArrayPoolSpan owner) + return Decode(message, default, null); + } + + public static Discv5Message DecodeOwned(ReadOnlyMemory message, ArrayPoolSpan owner) => Decode(message.Span, message, owner); + public static Discv5Message DecodeCopied(ReadOnlySpan message) + { + ArrayPoolSpan owner = new(message.Length); + try + { + message.CopyTo(owner); + return DecodeOwned(owner.AsReadOnlyMemory(), owner); + } + catch + { + owner.Dispose(); + throw; + } + } + private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { if (message.IsEmpty) @@ -53,19 +81,15 @@ private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory new PingMsg(requestId, ctx.DecodeULong(), owner), - MessageType.Pong => DecodePong(requestId, ref ctx, owner), - MessageType.FindNode => new FindNodeMsg(requestId, DecodeDistances(ref ctx), owner), - MessageType.Nodes => DecodeNodes(requestId, ref ctx, owner), - MessageType.TalkReq => new TalkReqMsg( - requestId, - DecodeByteMemory(ref ctx, ownedMessage), - DecodeByteMemory(ref ctx, ownedMessage), - owner), - MessageType.TalkResp => new TalkRespMsg(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner), + MessageType.Ping => PingSerializer.Deserialize(requestId, ref ctx, owner), + MessageType.Pong => PongSerializer.Deserialize(requestId, ref ctx, owner), + MessageType.FindNode => FindNodeSerializer.Deserialize(requestId, ref ctx, owner), + MessageType.Nodes => NodesSerializer.Deserialize(requestId, ref ctx, owner), + MessageType.TalkReq => TalkReqSerializer.Deserialize(requestId, ref ctx, ownedMessage, owner), + MessageType.TalkResp => TalkRespSerializer.Deserialize(requestId, ref ctx, ownedMessage, owner), _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") }; @@ -96,17 +120,17 @@ private static void DisposeOwner(ArrayPoolSpan? owner) } } + private static bool NeedsOwnedMessage(ReadOnlySpan message) + => !message.IsEmpty && (MessageType)message[0] is MessageType.TalkReq or MessageType.TalkResp; + private static int GetContentLength(Discv5Message message) => message switch { - PingMsg ping => GetRequestIdLength(ping.RequestId) + Rlp.LengthOf(ping.EnrSequence), - PongMsg pong => GetRequestIdLength(pong.RequestId) + - Rlp.LengthOf(pong.EnrSequence) + - GetAddressRlpLength(pong.RecipientIp) + - Rlp.LengthOf(pong.RecipientPort), - FindNodeMsg findNode => GetRequestIdLength(findNode.RequestId) + GetDistancesLength(findNode.Distances), - NodesMsg nodes => GetRequestIdLength(nodes.RequestId) + Rlp.LengthOf(nodes.Total) + GetNodeRecordsLength(nodes.Records), - TalkReqMsg talkReq => GetRequestIdLength(talkReq.RequestId) + Rlp.LengthOf(talkReq.Protocol) + Rlp.LengthOf(talkReq.Request), - TalkRespMsg talkResp => GetRequestIdLength(talkResp.RequestId) + Rlp.LengthOf(talkResp.Response), + PingMsg ping => PingSerializer.GetContentLength(ping), + PongMsg pong => PongSerializer.GetContentLength(pong), + FindNodeMsg findNode => FindNodeSerializer.GetContentLength(findNode), + NodesMsg nodes => NodesSerializer.GetContentLength(nodes), + TalkReqMsg talkReq => TalkReqSerializer.GetContentLength(talkReq), + TalkRespMsg talkResp => TalkRespSerializer.GetContentLength(talkResp), _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") }; @@ -115,203 +139,25 @@ private static void EncodeContent(Span buffer, ref int position, Discv5Mes switch (message) { case PingMsg ping: - EncodeRequestId(buffer, ref position, ping.RequestId); - Encode(buffer, ref position, ping.EnrSequence); + PingSerializer.Serialize(buffer, ref position, ping); break; case PongMsg pong: - EncodeRequestId(buffer, ref position, pong.RequestId); - Encode(buffer, ref position, pong.EnrSequence); - EncodeAddress(buffer, ref position, pong.RecipientIp); - Encode(buffer, ref position, pong.RecipientPort); + PongSerializer.Serialize(buffer, ref position, pong); break; case FindNodeMsg findNode: - EncodeRequestId(buffer, ref position, findNode.RequestId); - EncodeDistances(buffer, ref position, findNode.Distances); + FindNodeSerializer.Serialize(buffer, ref position, findNode); break; case NodesMsg nodes: - EncodeRequestId(buffer, ref position, nodes.RequestId); - Encode(buffer, ref position, nodes.Total); - EncodeNodeRecords(buffer, ref position, nodes.Records); + NodesSerializer.Serialize(buffer, ref position, nodes); break; case TalkReqMsg talkReq: - EncodeRequestId(buffer, ref position, talkReq.RequestId); - position = Rlp.Encode(buffer, position, talkReq.Protocol); - position = Rlp.Encode(buffer, position, talkReq.Request); + TalkReqSerializer.Serialize(buffer, ref position, talkReq); break; case TalkRespMsg talkResp: - EncodeRequestId(buffer, ref position, talkResp.RequestId); - position = Rlp.Encode(buffer, position, talkResp.Response); + TalkRespSerializer.Serialize(buffer, ref position, talkResp); break; default: throw new RlpException($"Unsupported discv5 message {message.GetType().Name}."); } } - - private static int GetRequestIdLength(RequestId requestId) - { - Span bytes = stackalloc byte[RequestId.MaxLength]; - requestId.CopyTo(bytes); - return Rlp.LengthOf(bytes[..requestId.Length]); - } - - private static void EncodeRequestId(Span buffer, ref int position, RequestId requestId) - { - Span bytes = stackalloc byte[RequestId.MaxLength]; - requestId.CopyTo(bytes); - position = Rlp.Encode(buffer, position, bytes[..requestId.Length]); - } - - private static int GetDistancesLength(Distances distances) - { - int contentLength = 0; - for (int i = 0; i < distances.Count; i++) - { - contentLength += Rlp.LengthOf(distances[i]); - } - - return Rlp.LengthOfSequence(contentLength); - } - - private static void EncodeDistances(Span buffer, ref int position, Distances distances) - { - int contentLength = 0; - for (int i = 0; i < distances.Count; i++) - { - contentLength += Rlp.LengthOf(distances[i]); - } - - position = Rlp.StartSequence(buffer, position, contentLength); - for (int i = 0; i < distances.Count; i++) - { - Encode(buffer, ref position, distances[i]); - } - } - - private static int GetNodeRecordsLength(IReadOnlyList records) - { - int contentLength = 0; - for (int i = 0; i < records.Count; i++) - { - contentLength += records[i].GetRlpLengthWithSignature(); - } - - return Rlp.LengthOfSequence(contentLength); - } - - private static void EncodeNodeRecords(Span buffer, ref int position, IReadOnlyList records) - { - int contentLength = 0; - for (int i = 0; i < records.Count; i++) - { - contentLength += records[i].GetRlpLengthWithSignature(); - } - - position = Rlp.StartSequence(buffer, position, contentLength); - for (int i = 0; i < records.Count; i++) - { - records[i].Encode(buffer, ref position); - } - } - - private static int GetAddressRlpLength(IPAddress ip) - { - if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetwork) - { - return Rlp.LengthOfByteString(4, 0); - } - - if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetworkV6) - { - return Rlp.LengthOfByteString(16, 0); - } - - return Rlp.LengthOf(ip.GetAddressBytes()); - } - - private static void EncodeAddress(Span buffer, ref int position, IPAddress ip) - { - Span bytes = stackalloc byte[16]; - if (ip.TryWriteBytes(bytes, out int bytesWritten)) - { - position = Rlp.Encode(buffer, position, bytes[..bytesWritten]); - return; - } - - position = Rlp.Encode(buffer, position, ip.GetAddressBytes()); - } - - private static void Encode(Span buffer, ref int position, ulong value) - => position += Rlp.Encode(value, buffer[position..]).Length; - - private static void Encode(Span buffer, ref int position, int value) - => position += Rlp.Encode((long)value, buffer[position..]).Length; - - private static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) - { - ReadOnlySpan requestId = ctx.DecodeByteArraySpan(); - if (requestId.Length > RequestId.MaxLength) - { - throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {RequestId.MaxLength}."); - } - - return RequestId.From(requestId); - } - - private static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) - { - ReadOnlySpan value = ctx.DecodeByteArraySpan(); - if (ownedMessage.IsEmpty) - { - return value.ToArray(); - } - - return ownedMessage.Slice(1 + ctx.Position - value.Length, value.Length); - } - - private static PongMsg DecodePong(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) - { - ulong enrSequence = ctx.DecodeULong(); - IPAddress recipientIp = new(ctx.DecodeByteArraySpan()); - int recipientPort = ctx.DecodePositiveInt(); - return new PongMsg(requestId, enrSequence, recipientIp, recipientPort, owner); - } - - private static Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) - { - int checkPosition = ctx.ReadSequenceLength() + ctx.Position; - int count = ctx.PeekNumberOfItemsRemaining(checkPosition); - Distances distances = new(count); - try - { - for (int i = 0; i < count; i++) - { - distances.Set(i, ctx.DecodePositiveInt()); - } - - ctx.Check(checkPosition); - return distances; - } - catch - { - distances.Dispose(); - throw; - } - } - - private static NodesMsg DecodeNodes(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) - { - int total = ctx.DecodePositiveInt(); - int checkPosition = ctx.ReadSequenceLength() + ctx.Position; - int count = ctx.PeekNumberOfItemsRemaining(checkPosition); - NodeRecord[] records = new NodeRecord[count]; - for (int i = 0; i < count; i++) - { - ReadOnlySpan record = ctx.PeekNextItem(); - records[i] = NodeRecord.FromBytes(record); - ctx.SkipItem(); - } - - ctx.Check(checkPosition); - return new NodesMsg(requestId, total, records, owner); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 9395f966606b..402ad32a68d5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -9,8 +9,8 @@ using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Sockets; using Microsoft.Extensions.DependencyInjection; +using Nethermind.Core.Collections; using Nethermind.Logging; -using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Discovery.Discv5; @@ -77,19 +77,28 @@ public async Task SendAsync(byte[] data, IPEndPoint destination) } } - public async IAsyncEnumerable ReadMessagesAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token = default) + internal async IAsyncEnumerable ReadMessagesAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token = default) { Interlocked.Increment(ref _activeReaders); try { await foreach (DatagramPacket packet in _inboundQueue.Reader.ReadAllAsync(token)) { + PooledUdpReceiveResult receiveResult = default; + bool hasReceiveResult = false; try { - yield return new UdpReceiveResult(packet.Content.ReadAllBytesAsArray(), (IPEndPoint)packet.Sender); + receiveResult = CreateReceiveResult(packet); + hasReceiveResult = true; + yield return receiveResult; } finally { + if (hasReceiveResult) + { + receiveResult.Dispose(); + } + ReferenceCountUtil.Release(packet); } } @@ -101,6 +110,26 @@ public async IAsyncEnumerable ReadMessagesAsync([System.Runtim } } + private static PooledUdpReceiveResult CreateReceiveResult(DatagramPacket packet) + { + ArrayPoolSpan buffer = new(packet.Content.ReadableBytes); + try + { + Span bytes = buffer; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = packet.Content.ReadByte(); + } + + return new PooledUdpReceiveResult((IPEndPoint)packet.Sender, buffer); + } + catch + { + buffer.Dispose(); + throw; + } + } + public Task ListenAsync(CancellationToken token = default) => Task.CompletedTask; public void Close() { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index dc8c0a6f38ef..fba71f32fd2d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using Autofac.Features.AttributeFilters; using Nethermind.Core.Collections; @@ -118,6 +119,9 @@ internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Disc internal bool TryDecode(byte[] packet, out Packet decoded) => TryDecode(packet.AsMemory(), _localNodeId, out decoded); + internal bool TryDecode(ReadOnlyMemory packet, out Packet decoded) + => TryDecode(packet, _localNodeId, out decoded); + internal static bool TryDecode(byte[] packet, ReadOnlySpan localNodeId, out Packet decoded) => TryDecode(packet.AsMemory(), localNodeId, out decoded); @@ -212,7 +216,7 @@ internal static bool TryDecryptMessageForTest(Packet packet, ReadOnlySpan packet.MessageAd.Span); ownerTransferred = true; - message = MessageCodec.Decode(plaintext.AsReadOnlyMemory(), plaintext); + message = MessageCodec.DecodeOwned(plaintext.AsReadOnlyMemory(), plaintext); return true; } catch (CryptographicException) @@ -259,7 +263,7 @@ internal bool TryDecryptHandshake( message = null!; nodeRecord = null; - if (!TryReadHandshakeAuthData(packet.AuthData, out Hash256 sourceNodeId, out ReadOnlyMemory idSignature, out CompressedPublicKey ephemeralPublicKey, out ReadOnlyMemory recordBytes)) + if (!TryReadHandshakeAuthData(packet.AuthData, out Hash256? sourceNodeId, out ReadOnlyMemory idSignature, out CompressedPublicKey? ephemeralPublicKey, out ReadOnlyMemory recordBytes)) { return false; } @@ -305,9 +309,9 @@ internal bool TryDecryptHandshake( return true; } - internal static bool TryGetSourceNodeId(Packet packet, out Hash256 sourceNodeId) + internal static bool TryGetSourceNodeId(Packet packet, [NotNullWhen(true)] out Hash256? sourceNodeId) { - sourceNodeId = null!; + sourceNodeId = null; switch (packet.Flag) { case PacketFlag.Ordinary when packet.AuthData.Length == NodeIdSize: @@ -611,14 +615,14 @@ private static void CalculateIdSignatureHash( private static bool TryReadHandshakeAuthData( ReadOnlyMemory authDataMemory, - out Hash256 sourceNodeId, + [NotNullWhen(true)] out Hash256? sourceNodeId, out ReadOnlyMemory idSignature, - out CompressedPublicKey ephemeralPublicKey, + [NotNullWhen(true)] out CompressedPublicKey? ephemeralPublicKey, out ReadOnlyMemory record) { - sourceNodeId = null!; + sourceNodeId = null; idSignature = ReadOnlyMemory.Empty; - ephemeralPublicKey = null!; + ephemeralPublicKey = null; record = ReadOnlyMemory.Empty; ReadOnlySpan authData = authDataMemory.Span; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/PooledUdpReceiveResult.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/PooledUdpReceiveResult.cs new file mode 100644 index 000000000000..8bfd6a51c55a --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/PooledUdpReceiveResult.cs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Core.Collections; + +namespace Nethermind.Network.Discovery.Discv5; + +internal readonly struct PooledUdpReceiveResult(IPEndPoint remoteEndPoint, ArrayPoolSpan buffer) +{ + private readonly bool _hasBuffer = true; + private readonly ArrayPoolSpan _buffer = buffer; + + public ReadOnlyMemory Buffer => _buffer.AsReadOnlyMemory(); + + public IPEndPoint RemoteEndPoint { get; } = remoteEndPoint; + + internal void Dispose() + { + if (_hasBuffer) + { + _buffer.Dispose(); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs new file mode 100644 index 000000000000..a20127152e88 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal sealed class FindNodeMsgSerializer : MsgSerializerBase +{ + public int GetContentLength(FindNodeMsg msg) + => GetRequestIdLength(msg.RequestId) + GetDistancesLength(msg.Distances); + + public void Serialize(Span buffer, ref int position, FindNodeMsg msg) + { + EncodeRequestId(buffer, ref position, msg.RequestId); + EncodeDistances(buffer, ref position, msg.Distances); + } + + public FindNodeMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + => new(requestId, DecodeDistances(ref ctx), owner); + + private static int GetDistancesLength(Distances distances) + { + int contentLength = 0; + for (int i = 0; i < distances.Count; i++) + { + contentLength += Rlp.LengthOf(distances[i]); + } + + return Rlp.LengthOfSequence(contentLength); + } + + private static void EncodeDistances(Span buffer, ref int position, Distances distances) + { + int contentLength = 0; + for (int i = 0; i < distances.Count; i++) + { + contentLength += Rlp.LengthOf(distances[i]); + } + + position = Rlp.StartSequence(buffer, position, contentLength); + for (int i = 0; i < distances.Count; i++) + { + Encode(buffer, ref position, distances[i]); + } + } + + private static Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) + { + int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + int count = ctx.PeekNumberOfItemsRemaining(checkPosition); + Distances distances = new(count); + try + { + for (int i = 0; i < count; i++) + { + distances.Set(i, ctx.DecodePositiveInt()); + } + + ctx.Check(checkPosition); + return distances; + } + catch + { + distances.Dispose(); + throw; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs new file mode 100644 index 000000000000..60cd6bf1b619 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal abstract class MsgSerializerBase +{ + internal static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) + { + ReadOnlySpan requestId = ctx.DecodeByteArraySpan(); + if (requestId.Length > RequestId.MaxLength) + { + throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {RequestId.MaxLength}."); + } + + return RequestId.From(requestId); + } + + protected static int GetRequestIdLength(RequestId requestId) + { + Span bytes = stackalloc byte[RequestId.MaxLength]; + requestId.CopyTo(bytes); + return Rlp.LengthOf(bytes[..requestId.Length]); + } + + protected static void EncodeRequestId(Span buffer, ref int position, RequestId requestId) + { + Span bytes = stackalloc byte[RequestId.MaxLength]; + requestId.CopyTo(bytes); + position = Rlp.Encode(buffer, position, bytes[..requestId.Length]); + } + + protected static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) + { + ReadOnlySpan value = ctx.DecodeByteArraySpan(); + if (ownedMessage.IsEmpty) + { + throw new RlpException("discv5 byte fields require owned message memory."); + } + + return ownedMessage.Slice(1 + ctx.Position - value.Length, value.Length); + } + + protected static void Encode(Span buffer, ref int position, ulong value) + { + int length = Rlp.LengthOf(value); + Rlp.Encode(value, buffer.Slice(position, length)); + position += length; + } + + protected static void Encode(Span buffer, ref int position, int value) + { + int length = Rlp.LengthOf(value); + Rlp.Encode((long)value, buffer.Slice(position, length)); + position += length; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs new file mode 100644 index 000000000000..ff991a0fc436 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal sealed class NodesMsgSerializer : MsgSerializerBase +{ + public int GetContentLength(NodesMsg msg) + => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Total) + GetNodeRecordsLength(msg.Records); + + public void Serialize(Span buffer, ref int position, NodesMsg msg) + { + EncodeRequestId(buffer, ref position, msg.RequestId); + Encode(buffer, ref position, msg.Total); + EncodeNodeRecords(buffer, ref position, msg.Records); + } + + public NodesMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + { + int total = ctx.DecodePositiveInt(); + return new NodesMsg(requestId, total, DecodeNodeRecords(ref ctx), owner); + } + + private static int GetNodeRecordsLength(IReadOnlyList records) + { + int contentLength = 0; + for (int i = 0; i < records.Count; i++) + { + contentLength += records[i].GetRlpLengthWithSignature(); + } + + return Rlp.LengthOfSequence(contentLength); + } + + private static void EncodeNodeRecords(Span buffer, ref int position, IReadOnlyList records) + { + int contentLength = 0; + for (int i = 0; i < records.Count; i++) + { + contentLength += records[i].GetRlpLengthWithSignature(); + } + + position = Rlp.StartSequence(buffer, position, contentLength); + for (int i = 0; i < records.Count; i++) + { + records[i].Encode(buffer, ref position); + } + } + + private static NodeRecord[] DecodeNodeRecords(ref Rlp.ValueDecoderContext ctx) + { + int checkPosition = ctx.ReadSequenceLength() + ctx.Position; + int count = ctx.PeekNumberOfItemsRemaining(checkPosition); + NodeRecord[] records = new NodeRecord[count]; + for (int i = 0; i < count; i++) + { + ReadOnlySpan record = ctx.PeekNextItem(); + records[i] = NodeRecord.FromBytes(record); + ctx.SkipItem(); + } + + ctx.Check(checkPosition); + return records; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs new file mode 100644 index 000000000000..d68061133e3b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal sealed class PingMsgSerializer : MsgSerializerBase +{ + public int GetContentLength(PingMsg msg) + => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.EnrSequence); + + public void Serialize(Span buffer, ref int position, PingMsg msg) + { + EncodeRequestId(buffer, ref position, msg.RequestId); + Encode(buffer, ref position, msg.EnrSequence); + } + + public PingMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + => new(requestId, ctx.DecodeULong(), owner); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs new file mode 100644 index 000000000000..b755bfe5b0c2 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal sealed class PongMsgSerializer : MsgSerializerBase +{ + public int GetContentLength(PongMsg msg) + => GetRequestIdLength(msg.RequestId) + + Rlp.LengthOf(msg.EnrSequence) + + GetAddressRlpLength(msg.RecipientIp) + + Rlp.LengthOf(msg.RecipientPort); + + public void Serialize(Span buffer, ref int position, PongMsg msg) + { + EncodeRequestId(buffer, ref position, msg.RequestId); + Encode(buffer, ref position, msg.EnrSequence); + EncodeAddress(buffer, ref position, msg.RecipientIp); + Encode(buffer, ref position, msg.RecipientPort); + } + + public PongMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + { + ulong enrSequence = ctx.DecodeULong(); + IPAddress recipientIp = new(ctx.DecodeByteArraySpan()); + int recipientPort = ctx.DecodePositiveInt(); + return new PongMsg(requestId, enrSequence, recipientIp, recipientPort, owner); + } + + private static int GetAddressRlpLength(IPAddress ip) + { + if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetwork) + { + return Rlp.LengthOfByteString(4, 0); + } + + if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetworkV6) + { + return Rlp.LengthOfByteString(16, 0); + } + + return Rlp.LengthOf(ip.GetAddressBytes()); + } + + private static void EncodeAddress(Span buffer, ref int position, IPAddress ip) + { + Span bytes = stackalloc byte[16]; + if (ip.TryWriteBytes(bytes, out int bytesWritten)) + { + position = Rlp.Encode(buffer, position, bytes[..bytesWritten]); + return; + } + + position = Rlp.Encode(buffer, position, ip.GetAddressBytes()); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs new file mode 100644 index 000000000000..5a3dbab1f39d --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal sealed class TalkReqMsgSerializer : MsgSerializerBase +{ + public int GetContentLength(TalkReqMsg msg) + => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Protocol) + Rlp.LengthOf(msg.Request); + + public void Serialize(Span buffer, ref int position, TalkReqMsg msg) + { + EncodeRequestId(buffer, ref position, msg.RequestId); + position = Rlp.Encode(buffer, position, msg.Protocol); + position = Rlp.Encode(buffer, position, msg.Request); + } + + public TalkReqMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + => new(requestId, DecodeByteMemory(ref ctx, ownedMessage), DecodeByteMemory(ref ctx, ownedMessage), owner); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs new file mode 100644 index 000000000000..a5f71dcfa384 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Collections; +using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Discv5.Serializers; + +internal sealed class TalkRespMsgSerializer : MsgSerializerBase +{ + public int GetContentLength(TalkRespMsg msg) + => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Response); + + public void Serialize(Span buffer, ref int position, TalkRespMsg msg) + { + EncodeRequestId(buffer, ref position, msg.RequestId); + position = Rlp.Encode(buffer, position, msg.Response); + } + + public TalkRespMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + => new(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner); +} diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs index 9d5765e33778..491759e4345d 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs @@ -44,6 +44,13 @@ public void Encode(Span buffer, ref int position) protected abstract void EncodeValue(Span buffer, ref int position); + protected static void EncodeInteger(Span buffer, ref int position, long value) + { + int length = Rlp.LengthOf(value); + Rlp.Encode(value, buffer.Slice(position, length)); + position += length; + } + public override int GetHashCode() => Key.GetHashCode(); private static int EncodeAscii(Span buffer, int position, string value) diff --git a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs index 8e659cb9c1c4..6d07bf329c78 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs @@ -33,6 +33,6 @@ protected override void EncodeValue(Span buffer, ref int position) position = Rlp.StartSequence(buffer, position, contentLength + 1); position = Rlp.StartSequence(buffer, position, contentLength); position = Rlp.Encode(buffer, position, Value.ForkHash); - position += Rlp.Encode((ulong)Value.NextBlock, buffer[position..]).Length; + EncodeInteger(buffer, ref position, Value.NextBlock); } } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 350d5199d02a..acd5aa4c52e8 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -309,7 +309,9 @@ public void Encode(Span buffer, ref int position) int contentLength = GetContentLengthWithSignature(); position = Rlp.StartSequence(buffer, position, contentLength); position = Rlp.Encode(buffer, position, Signature!.Bytes); - position += Rlp.Encode(EnrSequence, buffer[position..]).Length; // a different sequence here (not RLP sequence) + int sequenceLength = Rlp.LengthOf(EnrSequence); + Rlp.Encode(EnrSequence, buffer.Slice(position, sequenceLength)); // a different sequence here (not RLP sequence) + position += sequenceLength; foreach ((_, EnrContentEntry contentEntry) in Entries) { contentEntry.Encode(buffer, ref position); diff --git a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs index bff1e15a2f91..b3c93dea1a89 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs @@ -16,5 +16,5 @@ public class Tcp6Entry(int portNumber) : EnrContentEntry(portNumber) protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; + protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } diff --git a/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs index 699cfcd7191c..016effb79c2f 100644 --- a/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs @@ -16,5 +16,5 @@ public class TcpEntry(int portNumber) : EnrContentEntry(portNumber) protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; + protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs index d3c95bac01c1..35ec3c14c415 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs @@ -16,5 +16,5 @@ public class Udp6Entry(int portNumber) : EnrContentEntry(portNumber) protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; + protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } diff --git a/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs index 83e3c03060d1..db70dc07e42d 100644 --- a/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs @@ -16,5 +16,5 @@ public class UdpEntry(int portNumber) : EnrContentEntry(portNumber) protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - protected override void EncodeValue(Span buffer, ref int position) => position += Rlp.Encode((ulong)(long)Value, buffer[position..]).Length; + protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } From c257aed93df5e7a4084df8733696dde023b69dde Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 13:48:09 +0300 Subject: [PATCH 129/182] More moves and cleanup --- .../DiscoveryPersistenceManagerTests.cs | 9 +++--- .../Handlers/NeighbourMsgHandlerTests.cs | 4 +-- .../KademliaAdapterTests.cs} | 11 +++---- .../NodeSourceTests.cs} | 15 +++++----- .../Discv4/NettyDiscoveryHandlerTests.cs | 27 +++++++++-------- .../Discv5/CodecTests.cs | 25 ++++++++-------- .../Discv4/DiscoveryApp.cs | 7 +++-- .../Discv4/DiscoveryPersistenceManager.cs | 5 ++-- .../Handlers/EnrResponseHandler.cs | 3 +- .../Handlers/IMessageHandler.cs | 2 +- .../{ => Kademlia}/Handlers/ITaskCompleter.cs | 4 ++- .../Handlers/NeighbourMsgHandler.cs | 3 +- .../{ => Kademlia}/Handlers/PongMsgHandler.cs | 3 +- .../IKademliaAdapter.cs} | 5 ++-- .../KademliaAdapter.cs} | 11 +++---- .../KademliaModule.cs} | 13 ++++---- .../NodeSource.cs} | 9 +++--- .../Discv4/NettyDiscoveryHandler.cs | 25 ++++------------ .../Discv5/MessageCodec.cs | 30 +++++++++---------- .../Discv5/NettyDiscoveryV5Handler.cs | 28 +++-------------- .../Discv5/Packets/PacketCodec.cs | 4 +-- .../Serializers/FindNodeMsgSerializer.cs | 12 ++++---- .../Discv5/Serializers/MsgSerializerBase.cs | 18 +++-------- .../Discv5/Serializers/NodesMsgSerializer.cs | 14 ++++----- .../Discv5/Serializers/PingMsgSerializer.cs | 6 ++-- .../Discv5/Serializers/PongMsgSerializer.cs | 16 +++++----- .../Serializers/TalkReqMsgSerializer.cs | 8 ++--- .../Serializers/TalkRespMsgSerializer.cs | 6 ++-- .../NettyDiscoveryBaseHandler.cs | 27 ++++++++++++++++- 29 files changed, 171 insertions(+), 179 deletions(-) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/{ => Kademlia}/Handlers/NeighbourMsgHandlerTests.cs (95%) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/{KademliaDiscv4AdapterTests.cs => Kademlia/KademliaAdapterTests.cs} (98%) rename src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/{KademliaNodeSourceTests.cs => Kademlia/NodeSourceTests.cs} (97%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Kademlia}/Handlers/EnrResponseHandler.cs (87%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Kademlia}/Handlers/IMessageHandler.cs (77%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Kademlia}/Handlers/ITaskCompleter.cs (68%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Kademlia}/Handlers/NeighbourMsgHandler.cs (94%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{ => Kademlia}/Handlers/PongMsgHandler.cs (86%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{IKademliaDiscv4Adapter.cs => Kademlia/IKademliaAdapter.cs} (84%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{KademliaDiscv4Adapter.cs => Kademlia/KademliaAdapter.cs} (98%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{DiscV4KademliaModule.cs => Kademlia/KademliaModule.cs} (80%) rename src/Nethermind/Nethermind.Network.Discovery/Discv4/{KademliaNodeSource.cs => Kademlia/NodeSource.cs} (96%) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs index 089017a1c7ea..de1ecbe41a4f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs @@ -10,8 +10,9 @@ using Nethermind.Core.Test.Builders; using Nethermind.Db; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; @@ -28,7 +29,7 @@ public class DiscoveryPersistenceManagerTests private MemDb _discoveryDb = null!; private INetworkStorage _networkStorage = null!; private INodeStatsManager _nodeStatsManager = null!; - private IKademliaDiscv4Adapter _discv4Adapter = null!; + private IKademliaAdapter _discv4Adapter = null!; private IDiscoveryConfig _discoveryConfig = null!; private ILogManager _logManager = null!; private IKademlia _kademlia = null!; @@ -42,7 +43,7 @@ public void Setup() _discoveryDb = new MemDb(); _networkStorage = new NetworkStorage(_discoveryDb, LimboLogs.Instance); _nodeStatsManager = Substitute.For(); - _discv4Adapter = Substitute.For(); + _discv4Adapter = Substitute.For(); _discoveryConfig = new DiscoveryConfig() { DiscoveryPersistenceInterval = 100, @@ -68,7 +69,7 @@ public async Task Teardown() _discoveryConfig, _logManager); - private static Task PingReceived(IKademliaDiscv4Adapter adapter, NetworkNode node, int times = 1) => + private static Task PingReceived(IKademliaAdapter adapter, NetworkNode node, int times = 1) => adapter.Received(times).Ping( Arg.Is(n => n.Id.Equals(node.NodeId)), Arg.Any()); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Handlers/NeighbourMsgHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs similarity index 95% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Handlers/NeighbourMsgHandlerTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs index 45dff06b9024..5a98d94085cb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Handlers/NeighbourMsgHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs @@ -6,12 +6,12 @@ using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; -using Nethermind.Network.Discovery.Discv4.Handlers; +using Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test.Discv4.Handlers +namespace Nethermind.Network.Discovery.Test.Discv4.Kademlia.Handlers { [Parallelizable(ParallelScope.Self)] [TestFixture] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs index fdc1c13f2f0f..491f90d815fa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaDiscv4AdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs @@ -13,9 +13,10 @@ using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Logging; +using Nethermind.Kademlia; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; @@ -24,11 +25,11 @@ using NSubstitute; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test.Discv4 +namespace Nethermind.Network.Discovery.Test.Discv4.Kademlia { [Parallelizable(ParallelScope.Self)] [TestFixture] - public class KademliaDiscv4AdapterTests + public class KademliaAdapterTests { public enum NoResponseRequest { @@ -37,7 +38,7 @@ public enum NoResponseRequest SendEnrRequest } - private IKademliaDiscv4Adapter _adapter = null!; + private IKademliaAdapter _adapter = null!; private IKademlia _kademliaMessageReceiver = null!; private INodeHealthTracker _nodeHealthTracker = null!; @@ -111,7 +112,7 @@ public void Setup() _nodeStatsManager = Substitute.For(); _nodeStatsManager.GetOrAdd(Arg.Any()).Returns(Substitute.For()); - _adapter = new KademliaDiscv4Adapter( + _adapter = new KademliaAdapter( new Lazy>(() => _kademliaMessageReceiver), new Lazy>(() => _nodeHealthTracker), new DiscoveryConfig diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs similarity index 97% rename from src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs rename to src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs index ad317fe51e3c..7370426a2dde 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/KademliaNodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs @@ -12,23 +12,24 @@ using Nethermind.Core.Test.Builders; using Nethermind.Core.Utils; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; -namespace Nethermind.Network.Discovery.Test.Discv4 +namespace Nethermind.Network.Discovery.Test.Discv4.Kademlia { [Parallelizable(ParallelScope.Self)] [TestFixture] - public class KademliaNodeSourceTests + public class NodeSourceTests { private TestKademlia _kademlia = null!; private IIteratorNodeLookup _lookup = null!; - private IKademliaDiscv4Adapter _discv4Adapter = null!; - private KademliaNodeSource _nodeSource = null!; + private IKademliaAdapter _discv4Adapter = null!; + private NodeSource _nodeSource = null!; private NodeSession _nodeSession = null!; private INodeStats _nodeStats = null!; private ManualTimestamper _timestamper = null!; @@ -40,7 +41,7 @@ public void Setup() { _kademlia = new(); _lookup = Substitute.For>(); - _discv4Adapter = Substitute.For(); + _discv4Adapter = Substitute.For(); _discoveryConfig = new DiscoveryConfig { @@ -59,7 +60,7 @@ public void Setup() _nodeSession = new(_nodeStats, _timestamper); _discv4Adapter.GetSession(Arg.Any()).Returns(_nodeSession); - _nodeSource = new KademliaNodeSource( + _nodeSource = new NodeSource( _kademlia, _lookup, _discv4Adapter, diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs index 2affb0dbfb77..46e1ae3efa9a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs @@ -20,6 +20,7 @@ using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Test.Builders; using Nethermind.Serialization.Rlp; @@ -37,7 +38,7 @@ public class NettyDiscoveryHandlerTests private readonly PrivateKey _privateKey2 = new("3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"); private List _channels = []; private List _discoveryHandlers = []; - private List _kademliaAdaptersMocks = []; + private List _kademliaAdaptersMocks = []; private readonly IPEndPoint _address = new(IPAddress.Loopback, 10001); private readonly IPEndPoint _address2 = new(IPAddress.Loopback, 10002); private int _channelActivatedCounter; @@ -50,11 +51,11 @@ public async Task Initialize() _discoveryHandlers = []; _kademliaAdaptersMocks = []; _channelActivatedCounter = 0; - IKademliaDiscv4Adapter? kademliaAdapterMock = Substitute.For(); + IKademliaAdapter? kademliaAdapterMock = Substitute.For(); kademliaAdapterMock.OnIncomingMsg(Arg.Any()).Returns(Task.CompletedTask); IMessageSerializationService? messageSerializationService = Build.A.SerializationService().WithDiscovery(_privateKey).TestObject; - IKademliaDiscv4Adapter? kademliaAdapterMock2 = Substitute.For(); + IKademliaAdapter? kademliaAdapterMock2 = Substitute.For(); kademliaAdapterMock2.OnIncomingMsg(Arg.Any()).Returns(Task.CompletedTask); IMessageSerializationService? messageSerializationService2 = Build.A.SerializationService().WithDiscovery(_privateKey).TestObject; @@ -161,13 +162,13 @@ public async Task NeighborsSentReceivedTest() await _kademliaAdaptersMocks[0].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Neighbors)); } - private (IKademliaDiscv4Adapter Adapter, NettyDiscoveryHandler Handler, IChannelHandlerContext Ctx, IMessageSerializationService Service) CreateHandler( + private (IKademliaAdapter Adapter, NettyDiscoveryHandler Handler, IChannelHandlerContext Ctx, IMessageSerializationService Service) CreateHandler( NodeFilter? nodeFilter = null, int? globalInboundMessageBurst = null, int? inboundMessageQueueCapacity = null, int? inboundMessageWorkerCount = null) { - IKademliaDiscv4Adapter adapter = Substitute.For(); + IKademliaAdapter adapter = Substitute.For(); adapter.OnIncomingMsg(Arg.Any()).Returns(Task.CompletedTask); IMessageSerializationService service = Build.A.SerializationService().WithDiscovery(_privateKey2).TestObject; IChannel channel = Substitute.For(); @@ -188,7 +189,7 @@ public async Task NeighborsSentReceivedTest() [Test] public void UndersizedPacketIsNotForwardedToDiscoveryManager() { - (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService _) = CreateHandler(); + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService _) = CreateHandler(); byte[] data = new byte[50]; IPEndPoint from = IPEndPoint.Parse("127.0.0.1:10000"); @@ -231,7 +232,7 @@ public async Task FarFutureMessagesAreRejected() [Test] public async Task RateLimitedMessagesAreIgnored() { - (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(NodeFilter.CreateExact(16, TimeSpan.FromMinutes(1))); + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(NodeFilter.CreateExact(16, TimeSpan.FromMinutes(1))); using SemaphoreSlim called = new(0); adapter.When(x => x.OnIncomingMsg(Arg.Any())).Do(_ => called.Release()); @@ -249,7 +250,7 @@ public async Task RateLimitedMessagesAreIgnored() [Test] public async Task DefaultInboundRateLimiter_Allows_ShortBurstFromSameIp() { - (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(); + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(); byte[] data = SerializePing(service); @@ -264,7 +265,7 @@ public async Task DefaultInboundRateLimiter_Allows_ShortBurstFromSameIp() [Test] public async Task DefaultInboundRateLimiter_Drops_Message_AboveBurstLimit() { - (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(); + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(); byte[] data = SerializePing(service); @@ -281,7 +282,7 @@ public async Task DefaultInboundRateLimiter_Drops_Message_AboveBurstLimit() [Test] public async Task GlobalInboundRateLimiter_Drops_Messages_AboveBurstLimit() { - (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(globalInboundMessageBurst: 2); + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(globalInboundMessageBurst: 2); using SemaphoreSlim called = new(0); adapter.When(x => x.OnIncomingMsg(Arg.Any())).Do(_ => called.Release()); @@ -304,7 +305,7 @@ public async Task GlobalInboundRateLimiter_Drops_Messages_AboveBurstLimit() public async Task InboundDispatchQueue_Drops_Messages_WhenFull() { TaskCompletionSource unblockHandler = new(TaskCreationOptions.RunContinuationsAsynchronously); - (IKademliaDiscv4Adapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler( + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler( globalInboundMessageBurst: 64, inboundMessageQueueCapacity: 1, inboundMessageWorkerCount: 1); @@ -344,7 +345,7 @@ private byte[] SerializePing(IMessageSerializationService service) return data; } - private async Task StartUdpChannel(string address, int port, IKademliaDiscv4Adapter kademliaAdapter, IMessageSerializationService service) + private async Task StartUdpChannel(string address, int port, IKademliaAdapter kademliaAdapter, IMessageSerializationService service) { MultithreadEventLoopGroup group = new(1); @@ -357,7 +358,7 @@ private async Task StartUdpChannel(string address, int port, IKademliaDiscv4Adap _channels.Add(await bootstrap.BindAsync(IPAddress.Parse(address), port)); } - private void InitializeChannel(IDatagramChannel channel, IKademliaDiscv4Adapter kademliaAdapter, IMessageSerializationService service) + private void InitializeChannel(IDatagramChannel channel, IKademliaAdapter kademliaAdapter, IMessageSerializationService service) { NettyDiscoveryHandler handler = new(kademliaAdapter, channel, service, new Timestamper(), LimboLogs.Instance); handler.OnChannelActivated += (_, _) => diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 0907b9b7123d..1efe1f2ca61e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Crypto; -using Nethermind.Core.Collections; using Nethermind.Core.Extensions; using Nethermind.Core.Test.Modules; using Nethermind.Crypto; @@ -177,8 +176,8 @@ public void MessageCodec_Roundtrips_FindNode() { using FindNodeMsg message = new([0, 0, 0, 1], [255, 254, 256]); - using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.Decode(encoded); + using NettyRlpStream encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded.AsSpan()); Assert.That(decoded, Is.InstanceOf()); FindNodeMsg decodedFindNode = (FindNodeMsg)decoded; @@ -191,8 +190,8 @@ public void MessageCodec_Roundtrips_Pong() { using PongMsg message = new([0, 0, 0, 2], 3, IPAddress.Parse("192.0.2.1"), 30303); - using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.Decode(encoded); + using NettyRlpStream encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded.AsSpan()); Assert.That(decoded, Is.InstanceOf()); PongMsg decodedPong = (PongMsg)decoded; @@ -207,8 +206,8 @@ public void MessageCodec_Roundtrips_TalkReq() { using TalkReqMsg message = new([0, 0, 0, 3], "eth"u8.ToArray(), new byte[] { 1, 2, 3, 4 }); - using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.DecodeCopied(encoded); + using NettyRlpStream encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.DecodeCopied(encoded.AsSpan()); Assert.That(decoded, Is.InstanceOf()); TalkReqMsg decodedTalkReq = (TalkReqMsg)decoded; @@ -222,8 +221,8 @@ public void MessageCodec_Roundtrips_TalkResp() { using TalkRespMsg message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); - using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.DecodeCopied(encoded); + using NettyRlpStream encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.DecodeCopied(encoded.AsSpan()); Assert.That(decoded, Is.InstanceOf()); TalkRespMsg decodedTalkResp = (TalkRespMsg)decoded; @@ -235,9 +234,9 @@ public void MessageCodec_Roundtrips_TalkResp() public void MessageCodec_Requires_Owned_Memory_For_Talk_Messages() { using TalkRespMsg message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); - using ArrayPoolSpan encoded = MessageCodec.Encode(message); + using NettyRlpStream encoded = MessageCodec.Encode(message); - Assert.That(() => MessageCodec.Decode(encoded), Throws.TypeOf()); + Assert.That(() => MessageCodec.Decode(encoded.AsSpan()), Throws.TypeOf()); } [Test] @@ -248,8 +247,8 @@ public void MessageCodec_Roundtrips_Nodes_From_NonZero_ArraySegment() NodeRecord[] records = [skippedRecord, expectedRecord]; using NodesMsg message = new([0, 0, 0, 5], 1, new ArraySegment(records, 1, 1)); - using ArrayPoolSpan encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.Decode(encoded); + using NettyRlpStream encoded = MessageCodec.Encode(message); + using Discv5Message decoded = MessageCodec.Decode(encoded.AsSpan()); Assert.That(decoded, Is.InstanceOf()); NodesMsg decodedNodes = (NodesMsg)decoded; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs index eedc0aad5380..bfa29de8d1a2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs @@ -11,6 +11,7 @@ using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; +using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; using LogLevel = DotNetty.Handlers.Logging.LogLevel; @@ -20,7 +21,7 @@ namespace Nethermind.Network.Discovery.Discv4; public class DiscoveryApp : KademliaDiscoveryApp { private readonly DiscoveryPersistenceManager _persistenceManager; - private readonly IKademliaDiscv4Adapter _discv4Adapter; + private readonly IKademliaAdapter _discv4Adapter; private readonly Func _discoveryHandlerFactory; private readonly ILifetimeScope _discv4Services; @@ -65,7 +66,7 @@ public DiscoveryApp( (builder) => { builder - .AddModule(new DiscV4KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddModule(new KademliaModule(nodeKey.PublicKey, bootNodes)) .AddSingleton(); configureDiscv4Services?.Invoke(builder); @@ -84,7 +85,7 @@ public DiscoveryApp( private record DiscV4Services( IKademliaNodeSource NodeSource, DiscoveryPersistenceManager PersistenceManager, - IKademliaDiscv4Adapter Discv4Adapter, + IKademliaAdapter Discv4Adapter, IKademlia Kademlia, Func NettyDiscoveryHandlerFactory ) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs index 098684e4bed0..85a3f19ba58e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs @@ -7,6 +7,7 @@ using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; @@ -28,14 +29,14 @@ namespace Nethermind.Network.Discovery.Discv4; public class DiscoveryPersistenceManager( [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage discoveryStorage, INodeStatsManager nodeStatsManager, - IKademliaDiscv4Adapter discv4Adapter, + IKademliaAdapter discv4Adapter, IKademlia kademlia, IDiscoveryConfig discoveryConfig, ILogManager logManager) { private readonly INetworkStorage _discoveryStorage = discoveryStorage; private readonly INodeStatsManager _nodeStatsManager = nodeStatsManager; - private readonly IKademliaDiscv4Adapter _discv4Adapter = discv4Adapter; + private readonly IKademliaAdapter _discv4Adapter = discv4Adapter; private readonly IKademlia _kademlia = kademlia; private readonly ILogger _logger = logManager.GetClassLogger(); private readonly int _persistenceInterval = discoveryConfig.DiscoveryPersistenceInterval; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs similarity index 87% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/EnrResponseHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs index 80131224d432..a93a9d4f4541 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Extensions; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery.Discv4.Handlers; +namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; public class EnrResponseHandler(EnrRequestMsg request) : ITaskCompleter { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/IMessageHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/IMessageHandler.cs similarity index 77% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/IMessageHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/IMessageHandler.cs index 94449b9e08a8..d96ead657f9b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/IMessageHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/IMessageHandler.cs @@ -3,7 +3,7 @@ using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery.Discv4.Handlers; +namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; internal interface IMessageHandler { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/ITaskCompleter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs similarity index 68% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/ITaskCompleter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs index 038ca226b2b5..80147f0b8709 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/ITaskCompleter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -namespace Nethermind.Network.Discovery.Discv4.Handlers; +using Nethermind.Network.Discovery.Discv4; + +namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; internal interface ITaskCompleter : IMessageHandler { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs similarity index 94% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/NeighbourMsgHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs index 059d5a617a84..fa564ee36add 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs @@ -1,10 +1,11 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4.Handlers; +namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; public class NeighbourMsgHandler(int k) : ITaskCompleter { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs similarity index 86% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/PongMsgHandler.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs index c1bbad44a2be..ec3af65d9d8b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Handlers/PongMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Extensions; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; -namespace Nethermind.Network.Discovery.Discv4.Handlers; +namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; public class PongMsgHandler(PingMsg ping) : ITaskCompleter { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs similarity index 84% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs index 178f54ce3289..abbe178997eb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/IKademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs @@ -3,15 +3,16 @@ using Nethermind.Core.Crypto; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Kademlia; /// /// Interfaces between and discv4. Largely handles the transport and session handling. /// -public interface IKademliaDiscv4Adapter : IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable +public interface IKademliaAdapter : IKademliaMessageSender, IDiscoveryMsgListener, IAsyncDisposable { /// /// Gets or sets the message sender used to send discovery messages. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs similarity index 98% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index 0d7df6c41c55..5e7ee60f0406 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaDiscv4Adapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -7,15 +7,16 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4.Handlers; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats; using Nethermind.Stats.Model; using NonBlocking; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Kademlia; -public class KademliaDiscv4Adapter( +public class KademliaAdapter( Lazy> kademlia, // Cyclic dependency Lazy> nodeHealthTracker, IDiscoveryConfig discoveryConfig, @@ -25,7 +26,7 @@ public class KademliaDiscv4Adapter( ITimestamper timestamper, IProcessExitSource processExitSource, ILogManager logManager -) : IKademliaDiscv4Adapter +) : IKademliaAdapter { private const int MaxNodesPerNeighborsMsg = 12; @@ -35,7 +36,7 @@ ILogManager logManager private readonly TimeSpan _expirationTime = TimeSpan.FromMilliseconds(discoveryConfig.MessageExpiryTime); private readonly TimeSpan _waitAfterPongDelay = TimeSpan.FromMilliseconds(discoveryConfig.BondWaitTime); - private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly ILogger _logger = logManager.GetClassLogger(); private readonly RateLimiter _outboundRateLimiter = new(discoveryConfig.MaxOutgoingMessagePerSecond); public IMsgSender? MsgSender { get; set; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs similarity index 80% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index 186fa48636e8..b6379129cf2d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscV4KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -5,10 +5,11 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Kademlia; /// /// Specify the discv4 kademlia components. Mainly provide transport for . @@ -17,22 +18,22 @@ namespace Nethermind.Network.Discovery.Discv4; /// /// /// -public class DiscV4KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module +public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module { protected override void Load(ContainerBuilder builder) => builder .AddSingleton() // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. - .AddSingleton() + .AddSingleton() // Some transport wiring. - .AddSingleton() - .Bind() + .AddSingleton() + .Bind() .AddSingleton() // Register the main kademlia module and integration .AddModule(new KademliaModule()) - .Bind, IKademliaDiscv4Adapter>() + .Bind, IKademliaAdapter>() .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs similarity index 96% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs rename to src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index 23e473307578..2beb79e96e2d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/KademliaNodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -7,15 +7,16 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Discv4.Kademlia; -public class KademliaNodeSource( +public class NodeSource( IKademlia kademlia, IIteratorNodeLookup lookup, - IKademliaDiscv4Adapter discv4Adapter, + IKademliaAdapter discv4Adapter, IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, ILogManager logManager) @@ -23,7 +24,7 @@ public class KademliaNodeSource( { private const int ChannelCapacity = 64; - private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly ILogger _logger = logManager.GetClassLogger(); private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256KademliaDistance.Instance.MaxDistance); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs index 64344e324ca6..87550751c00f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs @@ -30,7 +30,7 @@ public class NettyDiscoveryHandler( NodeFilter? inboundMessageFilter = null, int? globalInboundMessageBurst = null, int? inboundMessageQueueCapacity = null, - int? inboundMessageWorkerCount = null) : NettyDiscoveryBaseHandler(logManager), IMsgSender + int? inboundMessageWorkerCount = null) : NettyDiscoveryBaseHandler(logManager, channel ?? throw new ArgumentNullException(nameof(channel))), IMsgSender { private static readonly TimeSpan MaxFutureExpirationOffset = TimeSpan.FromHours(1); private static readonly TimeSpan DefaultInboundMessageWindow = TimeSpan.FromMilliseconds(100); @@ -42,14 +42,13 @@ public class NettyDiscoveryHandler( private const int DefaultInboundMessageWorkerCount = 16; private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); private readonly IDiscoveryMsgListener _discoveryMsgListener = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); - private readonly IChannel _channel = channel ?? throw new ArgumentNullException(nameof(channel)); private readonly IMessageSerializationService _msgSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); private readonly ITimestamper _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); private readonly NodeFilter[] _inboundMessageFilters = inboundMessageFilter is null ? CreateDefaultInboundMessageFilters() : [inboundMessageFilter]; private readonly FixedWindowLimiter _globalInboundMessageLimiter = new(Math.Max(1, globalInboundMessageBurst ?? DefaultGlobalInboundMessageBurst), DefaultGlobalInboundMessageWindow); - private readonly Channel _inboundMessages = Channel.CreateBounded( + private readonly Channel _inboundMessages = System.Threading.Channels.Channel.CreateBounded( new BoundedChannelOptions(Math.Max(1, inboundMessageQueueCapacity ?? DefaultInboundMessageQueueCapacity)) { SingleReader = false, @@ -58,19 +57,7 @@ public class NettyDiscoveryHandler( private readonly int _inboundMessageWorkerCount = Math.Max(1, inboundMessageWorkerCount ?? DefaultInboundMessageWorkerCount); private int _dispatchWorkersStarted; - public override void ChannelActive(IChannelHandlerContext context) => OnChannelActivated?.Invoke(this, EventArgs.Empty); - - public override void ChannelInactive(IChannelHandlerContext context) - { - _inboundMessages.Writer.TryComplete(); - base.ChannelInactive(context); - } - - public override void HandlerRemoved(IChannelHandlerContext context) - { - _inboundMessages.Writer.TryComplete(); - base.HandlerRemoved(context); - } + protected override void CloseInbound() => _inboundMessages.Writer.TryComplete(); public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) { @@ -95,7 +82,7 @@ public async Task SendMsg(DiscoveryMsg discoveryMsg) try { if (_logger.IsTrace) _logger.Trace($"Sending message: {discoveryMsg}"); - msgBuffer = Serialize(discoveryMsg, _channel.Allocator); + msgBuffer = Serialize(discoveryMsg, Channel.Allocator); } catch (Exception e) { @@ -121,7 +108,7 @@ public async Task SendMsg(DiscoveryMsg discoveryMsg) IAddressedEnvelope packet = new DatagramPacket(msgBuffer, discoveryMsg.FarAddress); try { - await _channel.WriteAndFlushAsync(packet); + await Channel.WriteAndFlushAsync(packet); } catch (Exception e) { @@ -453,6 +440,4 @@ public bool TryAcquire() } private readonly record struct InboundDiscoveryPacket(IChannelHandlerContext Context, DatagramPacket Packet, MsgType Type, EndPoint Address, int Size); - - public event EventHandler? OnChannelActivated; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs index 80771be9386c..1269265b7168 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs @@ -17,25 +17,23 @@ internal static class MessageCodec private static readonly TalkReqMsgSerializer TalkReqSerializer = new(); private static readonly TalkRespMsgSerializer TalkRespSerializer = new(); - public static ArrayPoolSpan Encode(Discv5Message message) + public static NettyRlpStream Encode(Discv5Message message) { int contentLength = GetContentLength(message); - ArrayPoolSpan result = new(Rlp.LengthOfSequence(contentLength) + 1); + NettyRlpStream stream = new(NethermindBuffers.Default.Buffer(Rlp.LengthOfSequence(contentLength) + 1)); try { - Span resultSpan = result; - int position = 0; - resultSpan[position++] = (byte)message.MessageType; - position = Rlp.StartSequence(resultSpan, position, contentLength); - EncodeContent(resultSpan, ref position, message); + stream.WriteByte((byte)message.MessageType); + stream.StartSequence(contentLength); + EncodeContent(stream, message); } catch { - result.Dispose(); + stream.Dispose(); throw; } - return result; + return stream; } public static Discv5Message Decode(ReadOnlySpan message) @@ -134,27 +132,27 @@ private static bool NeedsOwnedMessage(ReadOnlySpan message) _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") }; - private static void EncodeContent(Span buffer, ref int position, Discv5Message message) + private static void EncodeContent(NettyRlpStream stream, Discv5Message message) { switch (message) { case PingMsg ping: - PingSerializer.Serialize(buffer, ref position, ping); + PingSerializer.Serialize(stream, ping); break; case PongMsg pong: - PongSerializer.Serialize(buffer, ref position, pong); + PongSerializer.Serialize(stream, pong); break; case FindNodeMsg findNode: - FindNodeSerializer.Serialize(buffer, ref position, findNode); + FindNodeSerializer.Serialize(stream, findNode); break; case NodesMsg nodes: - NodesSerializer.Serialize(buffer, ref position, nodes); + NodesSerializer.Serialize(stream, nodes); break; case TalkReqMsg talkReq: - TalkReqSerializer.Serialize(buffer, ref position, talkReq); + TalkReqSerializer.Serialize(stream, talkReq); break; case TalkRespMsg talkResp: - TalkRespSerializer.Serialize(buffer, ref position, talkResp); + TalkRespSerializer.Serialize(stream, talkResp); break; default: throw new RlpException($"Unsupported discv5 message {message.GetType().Name}."); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 402ad32a68d5..9137aee10e54 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -17,31 +17,16 @@ namespace Nethermind.Network.Discovery.Discv5; /// /// DotNetty UDP bridge used by the native discv5 implementation. /// -public class NettyDiscoveryV5Handler(ILogManager loggerManager) : NettyDiscoveryBaseHandler(loggerManager) +public class NettyDiscoveryV5Handler(ILogManager loggerManager, IChannel? channel = null) : NettyDiscoveryBaseHandler(loggerManager, channel) { private const int MaxMessagesBuffered = 1024; private readonly ILogger _logger = loggerManager.GetClassLogger(); - private readonly Channel _inboundQueue = Channel.CreateBounded(MaxMessagesBuffered); + private readonly Channel _inboundQueue = System.Threading.Channels.Channel.CreateBounded(MaxMessagesBuffered); - private IChannel? _nettyChannel; private int _activeReaders; - public void InitializeChannel(IChannel channel) => _nettyChannel = channel; - - public override void ChannelActive(IChannelHandlerContext context) => OnChannelActivated?.Invoke(this, EventArgs.Empty); - - public override void ChannelInactive(IChannelHandlerContext context) - { - Close(); - base.ChannelInactive(context); - } - - public override void HandlerRemoved(IChannelHandlerContext context) - { - Close(); - base.HandlerRemoved(context); - } + protected override void CloseInbound() => Close(); protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket msg) { @@ -62,13 +47,11 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket public async Task SendAsync(byte[] data, IPEndPoint destination) { - if (_nettyChannel is null) throw new("Channel for discovery v5 is not initialized"); - DatagramPacket packet = new(Unpooled.WrappedBuffer(data), destination); try { - await _nettyChannel.WriteAndFlushAsync(packet); + await Channel.WriteAndFlushAsync(packet); } catch (SocketException exception) { @@ -130,7 +113,6 @@ private static PooledUdpReceiveResult CreateReceiveResult(DatagramPacket packet) } } - public Task ListenAsync(CancellationToken token = default) => Task.CompletedTask; public void Close() { _inboundQueue.Writer.TryComplete(); @@ -149,6 +131,4 @@ private void ReleaseQueuedPackets() } public static void Register(IServiceCollection services) => services.AddSingleton(); - - public event EventHandler? OnChannelActivated; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index fba71f32fd2d..0c537be21a27 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -348,8 +348,8 @@ private byte[] EncodePacket( return EncodePacketCore(destinationNodeId, flag, nonce, authData, default, default, out messageAd); } - using ArrayPoolSpan encodedMessage = MessageCodec.Encode(message); - return EncodePacketCore(destinationNodeId, flag, nonce, authData, encryptionKey, encodedMessage, out messageAd); + using NettyRlpStream encodedMessage = MessageCodec.Encode(message); + return EncodePacketCore(destinationNodeId, flag, nonce, authData, encryptionKey, encodedMessage.AsSpan(), out messageAd); } private byte[] EncodePacketCore( diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs index a20127152e88..a2d4a9dcd3ad 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs @@ -12,10 +12,10 @@ internal sealed class FindNodeMsgSerializer : MsgSerializerBase public int GetContentLength(FindNodeMsg msg) => GetRequestIdLength(msg.RequestId) + GetDistancesLength(msg.Distances); - public void Serialize(Span buffer, ref int position, FindNodeMsg msg) + public void Serialize(NettyRlpStream stream, FindNodeMsg msg) { - EncodeRequestId(buffer, ref position, msg.RequestId); - EncodeDistances(buffer, ref position, msg.Distances); + EncodeRequestId(stream, msg.RequestId); + EncodeDistances(stream, msg.Distances); } public FindNodeMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) @@ -32,7 +32,7 @@ private static int GetDistancesLength(Distances distances) return Rlp.LengthOfSequence(contentLength); } - private static void EncodeDistances(Span buffer, ref int position, Distances distances) + private static void EncodeDistances(NettyRlpStream stream, Distances distances) { int contentLength = 0; for (int i = 0; i < distances.Count; i++) @@ -40,10 +40,10 @@ private static void EncodeDistances(Span buffer, ref int position, Distanc contentLength += Rlp.LengthOf(distances[i]); } - position = Rlp.StartSequence(buffer, position, contentLength); + stream.StartSequence(contentLength); for (int i = 0; i < distances.Count; i++) { - Encode(buffer, ref position, distances[i]); + Encode(stream, distances[i]); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs index 60cd6bf1b619..6b738f738fa3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs @@ -26,11 +26,11 @@ protected static int GetRequestIdLength(RequestId requestId) return Rlp.LengthOf(bytes[..requestId.Length]); } - protected static void EncodeRequestId(Span buffer, ref int position, RequestId requestId) + protected static void EncodeRequestId(NettyRlpStream stream, RequestId requestId) { Span bytes = stackalloc byte[RequestId.MaxLength]; requestId.CopyTo(bytes); - position = Rlp.Encode(buffer, position, bytes[..requestId.Length]); + stream.Encode(bytes[..requestId.Length]); } protected static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) @@ -44,17 +44,7 @@ protected static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderConte return ownedMessage.Slice(1 + ctx.Position - value.Length, value.Length); } - protected static void Encode(Span buffer, ref int position, ulong value) - { - int length = Rlp.LengthOf(value); - Rlp.Encode(value, buffer.Slice(position, length)); - position += length; - } + protected static void Encode(NettyRlpStream stream, ulong value) => stream.Encode(value); - protected static void Encode(Span buffer, ref int position, int value) - { - int length = Rlp.LengthOf(value); - Rlp.Encode((long)value, buffer.Slice(position, length)); - position += length; - } + protected static void Encode(NettyRlpStream stream, int value) => stream.Encode(value); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index ff991a0fc436..3377ed2a371b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -13,11 +13,11 @@ internal sealed class NodesMsgSerializer : MsgSerializerBase public int GetContentLength(NodesMsg msg) => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Total) + GetNodeRecordsLength(msg.Records); - public void Serialize(Span buffer, ref int position, NodesMsg msg) + public void Serialize(NettyRlpStream stream, NodesMsg msg) { - EncodeRequestId(buffer, ref position, msg.RequestId); - Encode(buffer, ref position, msg.Total); - EncodeNodeRecords(buffer, ref position, msg.Records); + EncodeRequestId(stream, msg.RequestId); + Encode(stream, msg.Total); + EncodeNodeRecords(stream, msg.Records); } public NodesMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) @@ -37,7 +37,7 @@ private static int GetNodeRecordsLength(IReadOnlyList records) return Rlp.LengthOfSequence(contentLength); } - private static void EncodeNodeRecords(Span buffer, ref int position, IReadOnlyList records) + private static void EncodeNodeRecords(NettyRlpStream stream, IReadOnlyList records) { int contentLength = 0; for (int i = 0; i < records.Count; i++) @@ -45,10 +45,10 @@ private static void EncodeNodeRecords(Span buffer, ref int position, IRead contentLength += records[i].GetRlpLengthWithSignature(); } - position = Rlp.StartSequence(buffer, position, contentLength); + stream.StartSequence(contentLength); for (int i = 0; i < records.Count; i++) { - records[i].Encode(buffer, ref position); + records[i].Encode(stream); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs index d68061133e3b..c021b5fb6cf4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs @@ -12,10 +12,10 @@ internal sealed class PingMsgSerializer : MsgSerializerBase public int GetContentLength(PingMsg msg) => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.EnrSequence); - public void Serialize(Span buffer, ref int position, PingMsg msg) + public void Serialize(NettyRlpStream stream, PingMsg msg) { - EncodeRequestId(buffer, ref position, msg.RequestId); - Encode(buffer, ref position, msg.EnrSequence); + EncodeRequestId(stream, msg.RequestId); + Encode(stream, msg.EnrSequence); } public PingMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs index b755bfe5b0c2..49d12c207767 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -16,12 +16,12 @@ public int GetContentLength(PongMsg msg) GetAddressRlpLength(msg.RecipientIp) + Rlp.LengthOf(msg.RecipientPort); - public void Serialize(Span buffer, ref int position, PongMsg msg) + public void Serialize(NettyRlpStream stream, PongMsg msg) { - EncodeRequestId(buffer, ref position, msg.RequestId); - Encode(buffer, ref position, msg.EnrSequence); - EncodeAddress(buffer, ref position, msg.RecipientIp); - Encode(buffer, ref position, msg.RecipientPort); + EncodeRequestId(stream, msg.RequestId); + Encode(stream, msg.EnrSequence); + EncodeAddress(stream, msg.RecipientIp); + Encode(stream, msg.RecipientPort); } public PongMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) @@ -47,15 +47,15 @@ private static int GetAddressRlpLength(IPAddress ip) return Rlp.LengthOf(ip.GetAddressBytes()); } - private static void EncodeAddress(Span buffer, ref int position, IPAddress ip) + private static void EncodeAddress(NettyRlpStream stream, IPAddress ip) { Span bytes = stackalloc byte[16]; if (ip.TryWriteBytes(bytes, out int bytesWritten)) { - position = Rlp.Encode(buffer, position, bytes[..bytesWritten]); + stream.Encode(bytes[..bytesWritten]); return; } - position = Rlp.Encode(buffer, position, ip.GetAddressBytes()); + stream.Encode(ip.GetAddressBytes()); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs index 5a3dbab1f39d..a7aa88f36804 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs @@ -12,11 +12,11 @@ internal sealed class TalkReqMsgSerializer : MsgSerializerBase public int GetContentLength(TalkReqMsg msg) => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Protocol) + Rlp.LengthOf(msg.Request); - public void Serialize(Span buffer, ref int position, TalkReqMsg msg) + public void Serialize(NettyRlpStream stream, TalkReqMsg msg) { - EncodeRequestId(buffer, ref position, msg.RequestId); - position = Rlp.Encode(buffer, position, msg.Protocol); - position = Rlp.Encode(buffer, position, msg.Request); + EncodeRequestId(stream, msg.RequestId); + stream.Encode(msg.Protocol); + stream.Encode(msg.Request); } public TalkReqMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs index a5f71dcfa384..f50bf06cc110 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs @@ -12,10 +12,10 @@ internal sealed class TalkRespMsgSerializer : MsgSerializerBase public int GetContentLength(TalkRespMsg msg) => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Response); - public void Serialize(Span buffer, ref int position, TalkRespMsg msg) + public void Serialize(NettyRlpStream stream, TalkRespMsg msg) { - EncodeRequestId(buffer, ref position, msg.RequestId); - position = Rlp.Encode(buffer, position, msg.Response); + EncodeRequestId(stream, msg.RequestId); + stream.Encode(msg.Response); } public TalkRespMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) diff --git a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryBaseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryBaseHandler.cs index d733d4fe6a17..1c9967ab9534 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryBaseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NettyDiscoveryBaseHandler.cs @@ -8,14 +8,33 @@ namespace Nethermind.Network.Discovery; -public abstract class NettyDiscoveryBaseHandler(ILogManager? logManager) : SimpleChannelInboundHandler +public abstract class NettyDiscoveryBaseHandler(ILogManager? logManager, IChannel? channel = null) : SimpleChannelInboundHandler { private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); + private IChannel? _channel = channel; // https://github.com/ethereum/devp2p/blob/master/discv4.md#wire-protocol // https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#udp-communication protected const int MaxPacketSize = 1280; + protected IChannel Channel => _channel ?? throw new InvalidOperationException("Discovery channel is not initialized."); + + public void InitializeChannel(IChannel channel) => _channel = channel; + + public override void ChannelActive(IChannelHandlerContext context) => OnChannelActivated?.Invoke(this, EventArgs.Empty); + + public override void ChannelInactive(IChannelHandlerContext context) + { + CloseInbound(); + base.ChannelInactive(context); + } + + public override void HandlerRemoved(IChannelHandlerContext context) + { + CloseInbound(); + base.HandlerRemoved(context); + } + public override void ChannelRead(IChannelHandlerContext ctx, object msg) { if (msg is DatagramPacket packet && AcceptInboundMessage(packet) && !ValidatePacket(packet)) @@ -39,4 +58,10 @@ protected bool ValidatePacket(DatagramPacket packet) return true; } + + protected virtual void CloseInbound() + { + } + + public event EventHandler? OnChannelActivated; } From 7a4cb6cb8d6f8a3ecfd364397b2b560fcefeb34f Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 14:36:11 +0300 Subject: [PATCH 130/182] Add discv5 trace logging --- .../NettyDiscoveryV5HandlerTests.cs | 25 ++++++++++ .../Discv5/Kademlia/KademliaAdapter.cs | 48 +++++++++++++++++++ .../Discv5/NettyDiscoveryV5Handler.cs | 9 +++- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index 0bad6cb76447..e5af230a743a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -75,6 +75,31 @@ public async Task ForwardsReceivedMessageToReader() Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(from)); } + [Test] + public async Task MapsIpv4MappedIpv6SenderToIpv4() + { + byte[] data = [1, 2, 3]; + IPEndPoint from = new(IPAddress.Parse("::ffff:127.0.0.1"), 10000); + IPEndPoint expectedFrom = IPEndPoint.Parse("127.0.0.1:10000"); + IPEndPoint to = IPEndPoint.Parse("127.0.0.1:10001"); + + using CancellationTokenSource cancellationSource = new(10_000); + await using IAsyncEnumerator enumerator = _handler + .ReadMessagesAsync(cancellationSource.Token) + .GetAsyncEnumerator(cancellationSource.Token); + ValueTask readTask = enumerator.MoveNextAsync(); + + IChannelHandlerContext ctx = Substitute.For(); + + _handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer(data), from, to)); + + Assert.That(await readTask, Is.True); + PooledUdpReceiveResult forwardedPacket = enumerator.Current; + + Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); + Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(expectedFrom)); + } + [TestCase(0)] [TestCase(1280 + 1)] public async Task SkipsMessagesOfInvalidSize(int size) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 26717bdc7112..a631d6f70f5d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -102,11 +102,14 @@ public async Task Ping(Node receiver, CancellationToken token) using PingMsg ping = new(CreateRequestId(), nodeRecordProvider.Current.EnrSequence); PongResponseHandler responseHandler = new(receiver); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 PING {ping.RequestId} to {receiver:s}."); if (!await SendRequest(receiver, ping, responseHandler, _pingTimeout, token)) { + if (_logger.IsTrace) _logger.Trace($"Discv5 PING {ping.RequestId} to {receiver:s} timed out."); return false; } + if (_logger.IsTrace) _logger.Trace($"Discv5 PING {ping.RequestId} to {receiver:s} succeeded."); kademlia.Value.AddOrRefresh(receiver); return true; } @@ -119,12 +122,15 @@ public async Task Ping(Node receiver, CancellationToken token) using FindNodeMsg findNode = new(CreateRequestId(), distances); NodesResponseHandler responseHandler = new(receiver, distances, _distance); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 FINDNODE {findNode.RequestId} to {receiver:s}, distances: {FormatDistances(distances)}."); if (!await SendRequest(receiver, findNode, responseHandler, _findNodeTimeout, token)) { + if (_logger.IsTrace) _logger.Trace($"Discv5 FINDNODE {findNode.RequestId} to {receiver:s} timed out."); return null; } Node[] nodes = responseHandler.GetNodes(); + if (_logger.IsTrace) _logger.Trace($"Discv5 FINDNODE {findNode.RequestId} to {receiver:s} returned {nodes.Length} nodes."); for (int i = 0; i < nodes.Length; i++) { kademlia.Value.AddOrRefresh(nodes[i]); @@ -177,6 +183,7 @@ private async Task SendRequest( } catch (OperationCanceledException) when (!token.IsCancellationRequested && timeoutCts.IsCancellationRequested) { + if (_logger.IsTrace) _logger.Trace($"Discv5 request {request.MessageType} {request.RequestId} to {receiver:s} timed out after {timeout}."); return false; } finally @@ -201,6 +208,7 @@ private async Task SendRequest( byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, sessionNonce); try { + if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} with existing session, bytes: {packet.Length}."); await discoveryHandler.SendAsync(packet, receiver.Address); return sessionPendingNonceKey; } @@ -222,6 +230,7 @@ private async Task SendRequest( byte[] initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); try { + if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} without session, bytes: {initialPacket.Length}."); await discoveryHandler.SendAsync(initialPacket, receiver.Address); return pendingNonceKey; } @@ -243,6 +252,7 @@ private async Task SendResponse(Node receiver, Discv5Message message, Cancellati Span nonce = stackalloc byte[PacketCodec.NonceSize]; session.WriteNextNonce(cryptoRandom, nonce); byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, nonce); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 response {message.MessageType} {message.RequestId} to {receiver:s}, bytes: {packet.Length}."); await discoveryHandler.SendAsync(packet, receiver.Address); } @@ -250,11 +260,13 @@ private async Task HandlePacket(PooledUdpReceiveResult udpPacket, CancellationTo { if (!packetCodec.TryDecode(udpPacket.Buffer, out Packet packet)) { + if (_logger.IsTrace) _logger.Trace($"Dropping undecodable discv5 packet from {udpPacket.RemoteEndPoint}, bytes: {udpPacket.Buffer.Length}."); return; } using (packet) { + if (_logger.IsTrace) _logger.Trace($"Received discv5 {packet.Flag} packet from {udpPacket.RemoteEndPoint}, bytes: {udpPacket.Buffer.Length}."); try { switch (packet.Flag) @@ -285,12 +297,14 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat PendingNonceKey pendingNonceKey = new(endpoint, NonceKey.From(packet.Nonce.Span)); if (!_pendingByNonce.TryRemove(pendingNonceKey, out PendingRequest? pendingRequest)) { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 WHOAREYOU from {endpoint}; no pending request for nonce."); return; } Challenge challenge = packetCodec.DecodeWhoAreYou(packet); byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Session session); SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash, endpoint), session); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 HANDSHAKE for {pendingRequest.Message.MessageType} {pendingRequest.Message.RequestId} to {endpoint}, bytes: {handshakePacket.Length}, requested ENR seq: {challenge.EnrSequence}."); await discoveryHandler.SendAsync(handshakePacket, endpoint); } @@ -298,6 +312,7 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati { if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 ordinary packet from {endpoint}; source node id missing."); return; } @@ -305,12 +320,14 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati if (!TryGetSession(sessionKey, out Session? session) || !packetCodec.TryDecryptMessage(packet, session.ReadKey, out Discv5Message message)) { + if (_logger.IsTrace) _logger.Trace($"Discv5 ordinary packet from {endpoint} could not be decrypted with an existing session; sending WHOAREYOU."); await SendWhoAreYou(endpoint, packet, nodeId); return; } try { + if (_logger.IsTrace) _logger.Trace($"Received discv5 message {message.MessageType} {message.RequestId} from {endpoint}."); await HandleMessage(session.RemotePublicKey, endpoint, message, token); } finally @@ -323,6 +340,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat { if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; source node id missing."); return; } @@ -330,12 +348,14 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat if (!_sentChallenges.TryRemove(challengeKey, out SentChallenge sentChallenge) || IsExpired(sentChallenge, Environment.TickCount64)) { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; matching challenge missing or expired."); return; } TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) { + if (_logger.IsTrace) _logger.Trace($"Unable to decrypt discv5 handshake packet from {endpoint}."); return; } @@ -346,6 +366,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat { if (!HasExpectedNodeId(nodeRecord, nodeId)) { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake ENR from {endpoint}; ENR node id does not match packet source."); return; } @@ -357,6 +378,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat } SetSession(new SessionKey(nodeId, endpoint), session); + if (_logger.IsTrace) _logger.Trace($"Received discv5 handshake message {message.MessageType} {message.RequestId} from {endpoint}, ENR included: {nodeRecord is not null}."); await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); } finally @@ -371,6 +393,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Hash long now = Environment.TickCount64; if (_sentChallenges.TryGet(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) { + if (_logger.IsTrace) _logger.Trace($"Resending discv5 WHOAREYOU challenge to {endpoint}."); await discoveryHandler.SendAsync(existingChallenge.Packet, endpoint); return; } @@ -384,6 +407,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Hash ulong enrSequence = TryGetKnownRecord(nodeId, out NodeRecord? record) ? record.EnrSequence : 0UL; byte[] packet = packetCodec.EncodeWhoAreYou(nodeId.Bytes, requestPacket.Nonce.Span, enrSequence, out Challenge challenge); SetSentChallenge(challengeKey, challenge, packet); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 WHOAREYOU challenge to {endpoint}, known ENR seq: {enrSequence}, bytes: {packet.Length}."); await discoveryHandler.SendAsync(packet, endpoint); } @@ -395,10 +419,12 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, }; if (HandleResponse(remotePublicKey.Hash, message)) { + if (_logger.IsTrace) _logger.Trace($"Handled discv5 response {message.MessageType} {message.RequestId} from {endpoint}."); kademlia.Value.AddOrRefresh(remoteNode); return; } + if (_logger.IsTrace) _logger.Trace($"Handling discv5 request {message.MessageType} {message.RequestId} from {endpoint}."); switch (message) { case PingMsg ping: @@ -579,6 +605,28 @@ private Distances GetLookupDistances(Node receiver, PublicKey target) return new Distances(distances[..count]); } + private static string FormatDistances(Distances distances) + { + Span chars = stackalloc char[16]; + int position = 0; + for (int i = 0; i < distances.Count; i++) + { + if (i > 0) + { + chars[position++] = ','; + } + + if (!distances[i].TryFormat(chars[position..], out int written)) + { + return string.Join(",", distances); + } + + position += written; + } + + return chars[..position].ToString(); + } + private RequestId CreateRequestId() { Span requestId = stackalloc byte[sizeof(ulong)]; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 9137aee10e54..3ea75eb42bc4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -35,6 +35,7 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket if (_inboundQueue.Writer.TryWrite(queuedPacket)) { + if (_logger.IsTrace) _logger.Trace($"Queued discv5 UDP packet from {NormalizeEndpoint((IPEndPoint)msg.Sender)}, bytes: {msg.Content.ReadableBytes}."); return; } @@ -51,6 +52,7 @@ public async Task SendAsync(byte[] data, IPEndPoint destination) try { + if (_logger.IsTrace) _logger.Trace($"Sending discv5 UDP packet to {destination}, bytes: {data.Length}."); await Channel.WriteAndFlushAsync(packet); } catch (SocketException exception) @@ -104,7 +106,7 @@ private static PooledUdpReceiveResult CreateReceiveResult(DatagramPacket packet) bytes[i] = packet.Content.ReadByte(); } - return new PooledUdpReceiveResult((IPEndPoint)packet.Sender, buffer); + return new PooledUdpReceiveResult(NormalizeEndpoint((IPEndPoint)packet.Sender), buffer); } catch { @@ -113,6 +115,11 @@ private static PooledUdpReceiveResult CreateReceiveResult(DatagramPacket packet) } } + private static IPEndPoint NormalizeEndpoint(IPEndPoint endpoint) + => endpoint.Address.IsIPv4MappedToIPv6 + ? new IPEndPoint(endpoint.Address.MapToIPv4(), endpoint.Port) + : endpoint; + public void Close() { _inboundQueue.Writer.TryComplete(); From d3252bddb18b84cd520407b92ad0f670cfc7e0c7 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 14:57:40 +0300 Subject: [PATCH 131/182] Fix PR CI failures --- .../Nethermind.Init/Steps/InitializeNetwork.cs | 1 - .../Discv4/Kademlia/NodeSourceTests.cs | 9 +++++++-- .../Discv5/Handlers/NodesResponseHandlerTests.cs | 1 - .../NettyDiscoveryV5HandlerTests.cs | 1 - .../Discv4/Kademlia/Handlers/EnrResponseHandler.cs | 1 - .../Discv4/Kademlia/Handlers/ITaskCompleter.cs | 2 -- .../Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs | 1 - .../Discv4/Kademlia/Handlers/PongMsgHandler.cs | 1 - .../Discv4/Kademlia/IKademliaAdapter.cs | 1 - .../Discv4/Kademlia/KademliaAdapter.cs | 1 - .../Discv4/Kademlia/KademliaModule.cs | 1 - .../Discv4/Kademlia/NodeSource.cs | 1 - .../Discv5/Kademlia/KademliaAdapter.cs | 1 - .../Nethermind.Runner.Test/Module/NetworkModuleTest.cs | 1 - .../Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs | 1 - .../Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs | 1 - src/Nethermind/Nethermind.Xdc/XdcModule.cs | 1 - 17 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index ca88941fe6bc..ac01474a7b45 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -15,7 +15,6 @@ using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Contract.P2P; -using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.P2P.ProtocolHandlers; using Nethermind.Stats; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs index 7370426a2dde..3f8a31c4d7e4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs @@ -100,6 +100,7 @@ public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToke [CancelAfter(10000)] public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(CancellationToken token) { + _discoveryConfig.ConcurrentDiscoveryJob = 1; Node node = new(TestItem.PublicKeyA, "192.168.1.1", 30303); _discv4Adapter.Ping(node, Arg.Any()) .Returns(true); @@ -111,7 +112,7 @@ public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(Ca await enumerator.MoveNextAsync(); // Assert - Verify that ping was called - await _discv4Adapter.Received(2).Ping( + await _discv4Adapter.Received(1).Ping( Arg.Is(n => n == node), Arg.Any()); } @@ -152,6 +153,7 @@ await _discv4Adapter.DidNotReceive().Ping( [CancelAfter(10000)] public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken token) { + _discoveryConfig.ConcurrentDiscoveryJob = 1; Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); @@ -169,9 +171,12 @@ public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken tok await enumerator.MoveNextAsync(); Assert.That(enumerator.Current, Is.EqualTo(node2)); - await _discv4Adapter.Received(2).Ping( + await _discv4Adapter.Received(1).Ping( Arg.Is(n => n == node1), Arg.Any()); + await _discv4Adapter.Received(1).Ping( + Arg.Is(n => n == node2), + Arg.Any()); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index 9235c1846431..330b9139d40a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -5,7 +5,6 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; -using Nethermind.Kademlia; using Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Kademlia; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index e5af230a743a..adb1122983df 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using DotNetty.Buffers; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs index a93a9d4f4541..d7353c6764dc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Extensions; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs index 80147f0b8709..b281dd05a219 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/ITaskCompleter.cs @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Network.Discovery.Discv4; - namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; internal interface ITaskCompleter : IMessageHandler diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs index fa564ee36add..696c97a63417 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs index ec3af65d9d8b..4a696249c9a3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Extensions; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs index abbe178997eb..39855cf7957b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/IKademliaAdapter.cs @@ -3,7 +3,6 @@ using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index 5e7ee60f0406..9499f11546c2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -7,7 +7,6 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Stats; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index b6379129cf2d..cbf1b869c236 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -5,7 +5,6 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index 2beb79e96e2d..f5f07f68a497 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -7,7 +7,6 @@ using Nethermind.Core.Crypto; using Nethermind.Logging; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index a631d6f70f5d..0d54b6b3db0f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Sockets; using Nethermind.Core.Caching; using Nethermind.Core.Crypto; using Nethermind.Crypto; diff --git a/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs b/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs index 0dd492de3439..4f5a82d14fdf 100644 --- a/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs +++ b/src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs @@ -21,7 +21,6 @@ using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Contract.P2P; -using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.P2P; using Nethermind.Network.P2P.Messages; diff --git a/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs b/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs index f3252a82666e..645fa1e491ce 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Discovery/XdcDiscoveryTests.cs @@ -7,7 +7,6 @@ using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Network; -using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Network.Test; diff --git a/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs index e55f29c9d063..e98274162baf 100644 --- a/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Xdc/Discovery/XdcNettyDiscoveryHandler.cs @@ -5,7 +5,6 @@ using Nethermind.Core; using Nethermind.Logging; using Nethermind.Network; -using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; diff --git a/src/Nethermind/Nethermind.Xdc/XdcModule.cs b/src/Nethermind/Nethermind.Xdc/XdcModule.cs index ce01ac59266a..d21a2c4a9905 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcModule.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcModule.cs @@ -20,7 +20,6 @@ using Nethermind.Evm.TransactionProcessing; using Nethermind.Init.Modules; using Nethermind.Network; -using Nethermind.Network.Discovery; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Messages; using Nethermind.Serialization.Rlp; From 51374a1ea5c503b60d02bf8f9bc6cf9424181595 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 16:53:11 +0300 Subject: [PATCH 132/182] Fix and cut --- .../Discv5/WireTests.cs | 24 +------- .../Discv5/Kademlia/KademliaAdapter.cs | 2 +- .../Discv5/Packets/PacketCodec.cs | 61 ++++++++++++++----- .../Discv5/Serializers/NodesMsgSerializer.cs | 7 ++- .../Nethermind.Network.Enr/EnrContentEntry.cs | 37 ----------- .../Nethermind.Network.Enr/EthEntry.cs | 10 --- .../Nethermind.Network.Enr/IdEntry.cs | 2 - .../Nethermind.Network.Enr/Ip6Entry.cs | 7 --- .../Nethermind.Network.Enr/IpEntry.cs | 7 --- .../Nethermind.Network.Enr/NodeRecord.cs | 52 ++++++---------- .../Nethermind.Network.Enr/SecP256k1Entry.cs | 2 - .../Nethermind.Network.Enr/Tcp6Entry.cs | 2 - .../Nethermind.Network.Enr/TcpEntry.cs | 2 - .../Nethermind.Network.Enr/Udp6Entry.cs | 2 - .../Nethermind.Network.Enr/UdpEntry.cs | 2 - 15 files changed, 71 insertions(+), 148 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index 497fc020ac05..deceb31d9773 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -20,6 +19,7 @@ using Nethermind.Network.Enr; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Discovery.Discv5.Kademlia; +using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Serialization.Rlp; @@ -171,8 +171,8 @@ public async Task FindNeighbours_Returns_Records_At_Requested_Distance() { Enr = peerC.NodeRecordProvider.Current.EnrString }; - int[] requestedDistances = GetLookupDistances(nodeB, TestItem.PrivateKeyC.PublicKey); - for (int i = 0; i < requestedDistances.Length; i++) + using Distances requestedDistances = peerA.Adapter.GetLookupDistances(nodeB, TestItem.PrivateKeyC.PublicKey); + for (int i = 0; i < requestedDistances.Count; i++) { peerB.Kademlia.GetAllAtDistance(requestedDistances[i]).Returns([]); } @@ -257,24 +257,6 @@ private static void Pump(TestPeer from, TestPeer to) } } - private static int[] GetLookupDistances(Node receiver, PublicKey target) - { - int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, target.Hash); - - List distances = [distance]; - if (distance > 0) - { - distances.Add(distance - 1); - } - - if (distance < Hash256KademliaDistance.Instance.MaxDistance) - { - distances.Add(distance + 1); - } - - return [.. distances]; - } - private sealed record TestPeer( KademliaAdapter Adapter, NettyDiscoveryV5Handler Handler, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 0d54b6b3db0f..99086e9ef1b9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -584,7 +584,7 @@ private void RegisterKnownRecord(Node node) } } - private Distances GetLookupDistances(Node receiver, PublicKey target) + internal Distances GetLookupDistances(Node receiver, PublicKey target) { int distance = _distance.CalculateLogDistance(receiver.Id.Hash, target.Hash); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 0c537be21a27..4b5114ce46e2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -45,14 +45,13 @@ public sealed class PacketCodec( private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; private readonly ICryptoRandom _cryptoRandom = cryptoRandom; private readonly IEcdsa _ecdsa = ecdsa; + private readonly Aes _decodeMaskingAes = CreateMaskingAes(nodeKey.PublicKey.Hash.Bytes[..AesKeySize]); + private readonly object _decodeMaskingAesLock = new(); - public void Dispose() => _privateKey.Dispose(); - - internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encryptionKey, Discv5Message message) + public void Dispose() { - Span generatedNonce = stackalloc byte[NonceSize]; - _cryptoRandom.GenerateRandomBytes(generatedNonce); - return EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, generatedNonce, _localNodeId, encryptionKey, message); + _decodeMaskingAes.Dispose(); + _privateKey.Dispose(); } internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encryptionKey, Discv5Message message, ReadOnlySpan nonce) @@ -117,15 +116,21 @@ internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Disc } internal bool TryDecode(byte[] packet, out Packet decoded) - => TryDecode(packet.AsMemory(), _localNodeId, out decoded); + => TryDecode(packet.AsMemory(), _decodeMaskingAes, _decodeMaskingAesLock, out decoded); internal bool TryDecode(ReadOnlyMemory packet, out Packet decoded) - => TryDecode(packet, _localNodeId, out decoded); + => TryDecode(packet, _decodeMaskingAes, _decodeMaskingAesLock, out decoded); internal static bool TryDecode(byte[] packet, ReadOnlySpan localNodeId, out Packet decoded) => TryDecode(packet.AsMemory(), localNodeId, out decoded); internal static bool TryDecode(ReadOnlyMemory packetMemory, ReadOnlySpan localNodeId, out Packet decoded) + { + using Aes localNodeMaskingAes = CreateMaskingAes(localNodeId[..AesKeySize]); + return TryDecode(packetMemory, localNodeMaskingAes, null, out decoded); + } + + private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMaskingAes, object? aesLock, out Packet decoded) { decoded = default; ReadOnlySpan packet = packetMemory.Span; @@ -136,7 +141,7 @@ internal static bool TryDecode(ReadOnlyMemory packetMemory, ReadOnlySpan maskingIv = packet[..MaskingIvSize]; Span staticHeader = stackalloc byte[StaticHeaderSize]; - AesCtrTransform(localNodeId[..AesKeySize], maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize), staticHeader); + AesCtrTransform(localNodeMaskingAes, aesLock, maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize), staticHeader); ReadOnlySpan protocolId = ProtocolId; if (!staticHeader[..protocolId.Length].SequenceEqual(protocolId)) { @@ -158,7 +163,7 @@ internal static bool TryDecode(ReadOnlyMemory packetMemory, ReadOnlySpan.Shared.Return(messageAdBuffer); @@ -272,7 +277,7 @@ internal bool TryDecryptHandshake( { try { - nodeRecord = NodeRecord.FromBytes(recordBytes.Span); + nodeRecord = NodeRecord.FromBytes(recordBytes.Span, _ecdsa); } catch (Exception) { @@ -437,17 +442,32 @@ private static void EncryptMessage( } private static void AesCtrTransform(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan input, Span output) + { + using Aes aes = CreateMaskingAes(key); + AesCtrTransform(aes, null, iv, input, output); + } + + private static void AesCtrTransform(Aes aes, object? aesLock, ReadOnlySpan iv, ReadOnlySpan input, Span output) + { + if (aesLock is null) + { + AesCtrTransform(aes, iv, input, output); + return; + } + + lock (aesLock) + { + AesCtrTransform(aes, iv, input, output); + } + } + + private static void AesCtrTransform(Aes aes, ReadOnlySpan iv, ReadOnlySpan input, Span output) { if (output.Length < input.Length) { throw new ArgumentException("Output span must be at least as long as input.", nameof(output)); } - using Aes aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - aes.SetKey(key); - Span counter = stackalloc byte[MaskingIvSize]; iv.CopyTo(counter); Span keyStream = stackalloc byte[MaskingIvSize]; @@ -468,6 +488,15 @@ private static void AesCtrTransform(ReadOnlySpan key, ReadOnlySpan i } } + private static Aes CreateMaskingAes(ReadOnlySpan key) + { + Aes aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.SetKey(key); + return aes; + } + private static void IncrementCounter(Span counter) { for (int i = counter.Length - 1; i >= 0; i--) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index 3377ed2a371b..51631fb500a4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Collections; +using Nethermind.Crypto; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; @@ -10,6 +11,8 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; internal sealed class NodesMsgSerializer : MsgSerializerBase { + private readonly IEcdsa _ecdsa = new Ecdsa(); + public int GetContentLength(NodesMsg msg) => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Total) + GetNodeRecordsLength(msg.Records); @@ -52,7 +55,7 @@ private static void EncodeNodeRecords(NettyRlpStream stream, IReadOnlyList record = ctx.PeekNextItem(); - records[i] = NodeRecord.FromBytes(record); + records[i] = NodeRecord.FromBytes(record, _ecdsa); ctx.SkipItem(); } diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs index 491759e4345d..fa769a3383b2 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentEntry.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics; -using System.Text; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Enr @@ -31,45 +30,9 @@ public void Encode(RlpStream rlpStream) EncodeValue(rlpStream); } - /// - /// Encodes the entry into a span-backed buffer. - /// - public void Encode(Span buffer, ref int position) - { - position = EncodeAscii(buffer, position, Key); - EncodeValue(buffer, ref position); - } - protected abstract void EncodeValue(RlpStream rlpStream); - protected abstract void EncodeValue(Span buffer, ref int position); - - protected static void EncodeInteger(Span buffer, ref int position, long value) - { - int length = Rlp.LengthOf(value); - Rlp.Encode(value, buffer.Slice(position, length)); - position += length; - } - public override int GetHashCode() => Key.GetHashCode(); - - private static int EncodeAscii(Span buffer, int position, string value) - { - if (string.IsNullOrEmpty(value)) - { - return Rlp.Encode(buffer, position, ReadOnlySpan.Empty); - } - - int byteCount = Encoding.ASCII.GetByteCount(value); - if (byteCount <= 128) - { - Span bytes = stackalloc byte[byteCount]; - Encoding.ASCII.GetBytes(value.AsSpan(), bytes); - return Rlp.Encode(buffer, position, bytes); - } - - return Rlp.Encode(buffer, position, Encoding.ASCII.GetBytes(value)); - } } /// diff --git a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs index 6d07bf329c78..058a328df499 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs @@ -25,14 +25,4 @@ protected override void EncodeValue(RlpStream rlpStream) rlpStream.Encode(Value.ForkHash); rlpStream.Encode(Value.NextBlock); } - - protected override void EncodeValue(Span buffer, ref int position) - { - // I am just guessing this one - int contentLength = 5 + Rlp.LengthOf(Value.NextBlock); - position = Rlp.StartSequence(buffer, position, contentLength + 1); - position = Rlp.StartSequence(buffer, position, contentLength); - position = Rlp.Encode(buffer, position, Value.ForkHash); - EncodeInteger(buffer, ref position, Value.NextBlock); - } } diff --git a/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs b/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs index e1389566506c..5331e152f880 100644 --- a/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/IdEntry.cs @@ -19,6 +19,4 @@ private IdEntry() : base("v4") { } protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode("v4"); - - protected override void EncodeValue(Span buffer, ref int position) => position = Rlp.Encode(buffer, position, "v4"u8); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs index e8c819996107..822f7549fbf3 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Ip6Entry.cs @@ -21,11 +21,4 @@ protected override void EncodeValue(RlpStream rlpStream) Value.MapToIPv6().TryWriteBytes(bytes, out int _); rlpStream.Encode(bytes); } - - protected override void EncodeValue(Span buffer, ref int position) - { - Span bytes = stackalloc byte[16]; - Value.MapToIPv6().TryWriteBytes(bytes, out int _); - position = Rlp.Encode(buffer, position, bytes); - } } diff --git a/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs index a27dd3a72aa6..3fdc90f9d81d 100644 --- a/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/IpEntry.cs @@ -21,11 +21,4 @@ protected override void EncodeValue(RlpStream rlpStream) Value.MapToIPv4().TryWriteBytes(bytes, out int _); rlpStream.Encode(bytes); } - - protected override void EncodeValue(Span buffer, ref int position) - { - Span bytes = stackalloc byte[4]; - Value.MapToIPv4().TryWriteBytes(bytes, out int _); - position = Rlp.Encode(buffer, position, bytes); - } } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index acd5aa4c52e8..ae5c6da48be6 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -14,6 +14,8 @@ namespace Nethermind.Network.Enr; /// public class NodeRecord { + private static readonly IEcdsa DefaultEcdsa = new Ecdsa(); + private ulong _enrSequence; private string? _enrString; @@ -129,14 +131,22 @@ public static NodeRecord FromEnrString(string enrString) } public static NodeRecord FromBytes(ReadOnlySpan bytes) - => FromBytes(bytes.ToArray()); + => FromBytes(bytes, DefaultEcdsa); public static NodeRecord FromBytes(byte[] bytes) + => FromBytes(bytes.AsSpan(), DefaultEcdsa); + + public static NodeRecord FromBytes(byte[] bytes, IEcdsa ecdsa) + => FromBytes(bytes.AsSpan(), ecdsa); + + public static NodeRecord FromBytes(ReadOnlySpan bytes, IEcdsa ecdsa) { - NodeRecordSigner signer = new(new Ecdsa()); - RlpStream stream = new(bytes); - NodeRecord nodeRecord = signer.Deserialize(stream); - if (stream.Position != stream.Length) + ArgumentNullException.ThrowIfNull(ecdsa); + + NodeRecordSigner signer = new(ecdsa); + Rlp.ValueDecoderContext ctx = new(bytes); + NodeRecord nodeRecord = signer.Deserialize(ref ctx); + if (ctx.Position != bytes.Length) { throw new RlpException("Unexpected trailing bytes in ENR."); } @@ -261,8 +271,8 @@ public byte[] ToRlpBytes() int rlpLength = GetRlpLengthWithSignature(); byte[] bytes = GC.AllocateUninitializedArray(rlpLength); - int position = 0; - Encode(bytes, ref position); + RlpStream rlpStream = new(bytes); + Encode(rlpStream); return bytes; } @@ -290,34 +300,6 @@ public void Encode(RlpStream rlpStream) } } - /// - /// Applies Rlp([signature, seq, k, v, ...]) into a span. - /// - /// The destination span. - /// The current write position. - public void Encode(Span buffer, ref int position) - { - if (OriginalRlp is not null) - { - OriginalRlp.CopyTo(buffer[position..]); - position += OriginalRlp.Length; - return; - } - - RequireSignature(); - - int contentLength = GetContentLengthWithSignature(); - position = Rlp.StartSequence(buffer, position, contentLength); - position = Rlp.Encode(buffer, position, Signature!.Bytes); - int sequenceLength = Rlp.LengthOf(EnrSequence); - Rlp.Encode(EnrSequence, buffer.Slice(position, sequenceLength)); // a different sequence here (not RLP sequence) - position += sequenceLength; - foreach ((_, EnrContentEntry contentEntry) in Entries) - { - contentEntry.Encode(buffer, ref position); - } - } - private string CreateEnrString() { RequireSignature(); diff --git a/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs b/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs index 10fcea67d52e..6bfccdba1acc 100644 --- a/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/SecP256k1Entry.cs @@ -16,6 +16,4 @@ public class SecP256k1Entry(CompressedPublicKey publicKey) : EnrContentEntry CompressedPublicKey.LengthInBytes + 1; protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value.Bytes); - - protected override void EncodeValue(Span buffer, ref int position) => position = Rlp.Encode(buffer, position, Value.Bytes); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs index b3c93dea1a89..85ab40b277f2 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Tcp6Entry.cs @@ -15,6 +15,4 @@ public class Tcp6Entry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - - protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } diff --git a/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs index 016effb79c2f..f6fe7a769b83 100644 --- a/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/TcpEntry.cs @@ -15,6 +15,4 @@ public class TcpEntry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - - protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } diff --git a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs index 35ec3c14c415..864a7efadc33 100644 --- a/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/Udp6Entry.cs @@ -15,6 +15,4 @@ public class Udp6Entry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - - protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } diff --git a/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs b/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs index db70dc07e42d..25973be48bf6 100644 --- a/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/UdpEntry.cs @@ -15,6 +15,4 @@ public class UdpEntry(int portNumber) : EnrContentEntry(portNumber) protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); - - protected override void EncodeValue(Span buffer, ref int position) => EncodeInteger(buffer, ref position, Value); } From 7c37920da72cb9e2e007d44e2a251307905c91ab Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 18:24:52 +0300 Subject: [PATCH 133/182] Use native compressed ECDH --- .../Nethermind.Crypto/PrivateKey.cs | 24 +++++ .../Nethermind.Crypto/SecP256k1Agreement.cs | 45 --------- .../Nethermind.Crypto/SecP256k1Ecdh.cs | 97 +++++++++++++++++++ .../Discv5/CodecTests.cs | 6 +- .../Discv5/Packets/PacketCodec.cs | 8 +- .../Nethermind.Runner/configs/hoodi.json | 17 +--- .../configs/hoodi_archive.json | 17 +--- 7 files changed, 130 insertions(+), 84 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs create mode 100644 src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs diff --git a/src/Nethermind/Nethermind.Crypto/PrivateKey.cs b/src/Nethermind/Nethermind.Crypto/PrivateKey.cs index 8baf43de123b..db0a18dfdaaa 100644 --- a/src/Nethermind/Nethermind.Crypto/PrivateKey.cs +++ b/src/Nethermind/Nethermind.Crypto/PrivateKey.cs @@ -52,6 +52,30 @@ public PrivateKey(byte[] keyBytes) public Address Address => PublicKey.Address; + /// + /// Computes the compressed secp256k1 ECDH shared EC point for this private key and a remote public key. + /// + /// The remote public key. + /// The 33-byte compressed ECDH shared EC point. + public byte[] GetCompressedSharedPoint(PublicKey publicKey) + { + ArgumentNullException.ThrowIfNull(publicKey); + + return SecP256k1Ecdh.GetCompressedSharedPoint(publicKey.PrefixedBytes, KeyBytes); + } + + /// + /// Computes the compressed secp256k1 ECDH shared EC point for this private key and a remote compressed public key. + /// + /// The remote compressed public key. + /// The 33-byte compressed ECDH shared EC point. + public byte[] GetCompressedSharedPoint(CompressedPublicKey publicKey) + { + ArgumentNullException.ThrowIfNull(publicKey); + + return SecP256k1Ecdh.GetCompressedSharedPoint(publicKey.Bytes, KeyBytes); + } + private bool Equals(PrivateKey other) => Bytes.AreEqual(KeyBytes, other.KeyBytes); public override bool Equals(object obj) diff --git a/src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs b/src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs deleted file mode 100644 index 88d3b1df0e56..000000000000 --- a/src/Nethermind/Nethermind.Crypto/SecP256k1Agreement.cs +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using System; -using Org.BouncyCastle.Math; -using Org.BouncyCastle.Math.EC; - -namespace Nethermind.Crypto; - -/// -/// secp256k1 key-agreement helpers for protocols that need the serialized shared EC point. -/// -public static class SecP256k1Agreement -{ - /// - /// Computes the compressed shared EC point for ECDH. - /// - public static byte[] AgreeCompressed(PublicKey publicKey, PrivateKey privateKey) - { - ArgumentNullException.ThrowIfNull(publicKey); - ArgumentNullException.ThrowIfNull(privateKey); - - ECPoint point = BouncyCrypto.DomainParameters.Curve.DecodePoint(publicKey.PrefixedBytes); - return AgreeCompressed(point, privateKey); - } - - /// - /// Computes the compressed shared EC point for ECDH. - /// - public static byte[] AgreeCompressed(CompressedPublicKey publicKey, PrivateKey privateKey) - { - ArgumentNullException.ThrowIfNull(publicKey); - ArgumentNullException.ThrowIfNull(privateKey); - - ECPoint point = BouncyCrypto.DomainParameters.Curve.DecodePoint(publicKey.Bytes); - return AgreeCompressed(point, privateKey); - } - - private static byte[] AgreeCompressed(ECPoint point, PrivateKey privateKey) - { - BigInteger privateScalar = new(1, privateKey.KeyBytes); - return point.Multiply(privateScalar).Normalize().GetEncoded(compressed: true); - } -} diff --git a/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs b/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs new file mode 100644 index 000000000000..c3b36a412e30 --- /dev/null +++ b/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Runtime.InteropServices; +using Nethermind.Core.Crypto; + +namespace Nethermind.Crypto; + +// The public SecP256k1.Ecdh API returns the default ECDH secret, while discv5 needs the compressed shared EC point. +internal static unsafe class SecP256k1Ecdh +{ + private const uint ContextNone = 1; + private const int ParsedPublicKeyLength = 64; + private const int CoordinateLength = 32; + + private static readonly EcdhHashFunction CompressedPointHashFunction = WriteCompressedPoint; + private static readonly SecP256k1ContextHandle Context = new(); + + internal static byte[] GetCompressedSharedPoint(ReadOnlySpan publicKey, ReadOnlySpan privateKey) + { + if (Context.IsInvalid) + { + throw new InvalidOperationException("Failed to create secp256k1 context."); + } + + Span parsedPublicKey = stackalloc byte[ParsedPublicKeyLength]; + Span output = stackalloc byte[CompressedPublicKey.LengthInBytes]; + + fixed (byte* publicKeyPtr = publicKey) + fixed (byte* privateKeyPtr = privateKey) + fixed (byte* parsedPublicKeyPtr = parsedPublicKey) + fixed (byte* outputPtr = output) + { + if (secp256k1_ec_pubkey_parse(Context, parsedPublicKeyPtr, publicKeyPtr, (nuint)publicKey.Length) != 1) + { + throw new ArgumentException("Invalid secp256k1 public key.", nameof(publicKey)); + } + + if (secp256k1_ecdh(Context, outputPtr, parsedPublicKeyPtr, privateKeyPtr, CompressedPointHashFunction, IntPtr.Zero) != 1) + { + throw new ArgumentException("Invalid secp256k1 private key.", nameof(privateKey)); + } + } + + return output.ToArray(); + } + + // The unmanaged callback receives libsecp256k1-owned 32-byte coordinates for the shared point and writes + // the compressed EC point form required by the devp2p discv5 key agreement. + private static int WriteCompressedPoint(byte* output, byte* x32, byte* y32, IntPtr data) + { + output[0] = (byte)(2 | (y32[CoordinateLength - 1] & 1)); + new ReadOnlySpan(x32, CoordinateLength).CopyTo(new Span(output + 1, CoordinateLength)); + return 1; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int EcdhHashFunction(byte* output, byte* x32, byte* y32, IntPtr data); + + [DllImport("secp256k1", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr secp256k1_context_create(uint flags); + + [DllImport("secp256k1", CallingConvention = CallingConvention.Cdecl)] + private static extern void secp256k1_context_destroy(IntPtr context); + + [DllImport("secp256k1", CallingConvention = CallingConvention.Cdecl)] + private static extern int secp256k1_ec_pubkey_parse( + SecP256k1ContextHandle context, + byte* publicKey, + byte* input, + nuint inputLength); + + [DllImport("secp256k1", CallingConvention = CallingConvention.Cdecl)] + private static extern int secp256k1_ecdh( + SecP256k1ContextHandle context, + byte* output, + byte* publicKey, + byte* privateKey, + EcdhHashFunction hashFunction, + IntPtr data); + + private sealed class SecP256k1ContextHandle : SafeHandle + { + public SecP256k1ContextHandle() + : base(IntPtr.Zero, ownsHandle: true) + => handle = secp256k1_context_create(ContextNone); + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + secp256k1_context_destroy(handle); + return true; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 1efe1f2ca61e..416ccfe5f976 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -29,9 +29,9 @@ public void CompressedAgreement_Matches_Devp2p_Vector() CompressedPublicKey publicKey = new("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231"); PrivateKey privateKey = new("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736"); - byte[] sharedSecret = SecP256k1Agreement.AgreeCompressed(publicKey, privateKey); + byte[] sharedPoint = privateKey.GetCompressedSharedPoint(publicKey); - Assert.That(sharedSecret.ToHexString(true), Is.EqualTo("0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e")); + Assert.That(sharedPoint.ToHexString(true), Is.EqualTo("0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e")); } [Test] @@ -39,7 +39,7 @@ public void KeyDerivation_Matches_Devp2p_Vector() { CompressedPublicKey destinationPublicKey = new("0x0317931e6e0840220642f230037d285d122bc59063221ef3226b1f403ddc69ca91"); PrivateKey ephemeralPrivateKey = new("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736"); - byte[] secret = SecP256k1Agreement.AgreeCompressed(destinationPublicKey, ephemeralPrivateKey); + byte[] secret = ephemeralPrivateKey.GetCompressedSharedPoint(destinationPublicKey); byte[] challengeData = Bytes.FromHexString("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000"); (byte[] initiatorKey, byte[] recipientKey) = PacketCodec.DeriveKeysForTest(secret, NodeAId, NodeBId, challengeData); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 4b5114ce46e2..2a6954ee653d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -518,8 +518,8 @@ private static void DeriveKeys( out byte[] initiatorKey, out byte[] recipientKey) { - byte[] secret = SecP256k1Agreement.AgreeCompressed(remotePublicKey, ephemeralPrivateKey); - DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); + byte[] sharedPoint = ephemeralPrivateKey.GetCompressedSharedPoint(remotePublicKey); + DeriveKeys(sharedPoint, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); } private void DeriveKeys( @@ -530,8 +530,8 @@ private void DeriveKeys( out byte[] initiatorKey, out byte[] recipientKey) { - byte[] secret = SecP256k1Agreement.AgreeCompressed(ephemeralPublicKey, _privateKey); - DeriveKeys(secret, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); + byte[] sharedPoint = _privateKey.GetCompressedSharedPoint(ephemeralPublicKey); + DeriveKeys(sharedPoint, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); } private static void DeriveKeys( diff --git a/src/Nethermind/Nethermind.Runner/configs/hoodi.json b/src/Nethermind/Nethermind.Runner/configs/hoodi.json index 264955a274db..6d1f4871a158 100644 --- a/src/Nethermind/Nethermind.Runner/configs/hoodi.json +++ b/src/Nethermind/Nethermind.Runner/configs/hoodi.json @@ -33,21 +33,6 @@ "Enabled": true }, "Discovery": { - "DiscoveryVersion": "V5", - "UseDefaultDiscv5Bootnodes": false, - "Bootnodes": [ - "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", - "enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303", - "enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303", - "enr:-Mq4QLkmuSwbGBUph1r7iHopzRpdqE-gcm5LNZfcE-6T37OCZbRHi22bXZkaqnZ6XdIyEDTelnkmMEQB8w6NbnJUt9GGAZWaowaYh2F0dG5ldHOIABgAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhNEmfKCEcXVpY4IyyIlzZWNwMjU2azGhA0hGa4jZJZYQAS-z6ZFK-m4GCFnWS8wfjO0bpSQn6hyEiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo", - "enr:-Ku4QLVumWTwyOUVS4ajqq8ZuZz2ik6t3Gtq0Ozxqecj0qNZWpMnudcvTs-4jrlwYRQMQwBS8Pvtmu4ZPP2Lx3i2t7YBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhNEmfKCJc2VjcDI1NmsxoQLdRlI8aCa_ELwTJhVN8k7km7IDc3pYu-FMYBs5_FiigIN1ZHCCIyk", - "enr:-LK4QAYuLujoiaqCAs0-qNWj9oFws1B4iy-Hff1bRB7wpQCYSS-IIMxLWCn7sWloTJzC1SiH8Y7lMQ5I36ynGV1ASj4Eh2F0dG5ldHOIYAAAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQOmI5MlAu3f5WEThAYOqoygpS2wYn0XS5NV2aYq7T0a04N0Y3CCIyiDdWRwgiMo", - "enr:-Ku4QIC89sMC0o-irosD4_23lJJ4qCGOvdUz7SmoShWx0k6AaxCFTKviEHa-sa7-EzsiXpDp0qP0xzX6nKdXJX3X-IQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQK_m0f1DzDc9Cjrspm36zuRa7072HSiMGYWLsKiVSbP34N1ZHCCIyk", - "enr:-Ku4QNkWjw5tNzo8DtWqKm7CnDdIq_y7xppD6c1EZSwjB8rMOkSFA1wJPLoKrq5UvA7wcxIotH6Usx3PAugEN2JMncIBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbHuBeJc2VjcDI1NmsxoQP3FwrhFYB60djwRjAoOjttq6du94DtkQuaN99wvgqaIYN1ZHCCIyk", - "enr:-OS4QMJGE13xEROqvKN1xnnt7U-noc51VXyM6wFMuL9LMhQDfo1p1dF_zFdS4OsnXz_vIYk-nQWnqJMWRDKvkSK6_CwDh2F0dG5ldHOIAAAAADAAAACGY2xpZW502IpMaWdodGhvdXNljDcuMC4wLWJldGEuM4RldGgykNLxmX9gAAkQAAgAAAAAAACCaWSCdjSCaXCEhse4F4RxdWljgiMqiXNlY3AyNTZrMaECef77P8k5l3PC_raLw42OAzdXfxeQ-58BJriNaqiRGJSIc3luY25ldHMAg3RjcIIjKIN1ZHCCIyg", - "enr:-LK4QDwhXMitMbC8xRiNL-XGMhRyMSOnxej-zGifjv9Nm5G8EF285phTU-CAsMHRRefZimNI7eNpAluijMQP7NDC8kEMh2F0dG5ldHOIAAAAAAAABgCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAOIT_SJc2VjcDI1NmsxoQMoHWNL4MAvh6YpQeM2SUjhUrLIPsAVPB8nyxbmckC6KIN0Y3CCIyiDdWRwgiMo", - "enr:-LK4QPYl2HnMPQ7b1es6Nf_tFYkyya5bj9IqAKOEj2cmoqVkN8ANbJJJK40MX4kciL7pZszPHw6vLNyeC-O3HUrLQv8Mh2F0dG5ldHOIAAAAAAAAAMCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAMYRG-Jc2VjcDI1NmsxoQPQ35tjr6q1qUqwAnegQmYQyfqxC_6437CObkZneI9n34N0Y3CCIyiDdWRwgiMo", - "enr:-KG4QKRSUi4IOAIK_xt5ERrwW_J47wmNCLWFh7Jo0hFE69drZsiZ5Pb5CEcM_njFTTLlIR6SCf67HTcSV1g6hCXdhWkCgmlkgnY0gmlwhLkvrBODaXA2kCoGxcAWAAAYAAAAAAAAABCJc2VjcDI1NmsxoQPU7g2jQGTz8BYbB2vLTb39S_PrcZAehwMM0b3bWsM5rIN1ZHCCIyiEdWRwNoIjKA" - ] + "DiscoveryVersion": "V5" } } diff --git a/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json b/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json index 5742d3b372ff..63631f796b1d 100644 --- a/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json +++ b/src/Nethermind/Nethermind.Runner/configs/hoodi_archive.json @@ -34,21 +34,6 @@ "Enabled": true }, "Discovery": { - "DiscoveryVersion": "V5", - "UseDefaultDiscv5Bootnodes": false, - "Bootnodes": [ - "enode://2112dd3839dd752813d4df7f40936f06829fc54c0e051a93967c26e5f5d27d99d886b57b4ffcc3c475e930ec9e79c56ef1dbb7d86ca5ee83a9d2ccf36e5c240c@134.209.138.84:30303", - "enode://60203fcb3524e07c5df60a14ae1c9c5b24023ea5d47463dfae051d2c9f3219f309657537576090ca0ae641f73d419f53d8e8000d7a464319d4784acd7d2abc41@209.38.124.160:30303", - "enode://8ae4a48101b2299597341263da0deb47cc38aa4d3ef4b7430b897d49bfa10eb1ccfe1655679b1ed46928ef177fbf21b86837bd724400196c508427a6f41602cd@134.199.184.23:30303", - "enr:-Mq4QLkmuSwbGBUph1r7iHopzRpdqE-gcm5LNZfcE-6T37OCZbRHi22bXZkaqnZ6XdIyEDTelnkmMEQB8w6NbnJUt9GGAZWaowaYh2F0dG5ldHOIABgAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhNEmfKCEcXVpY4IyyIlzZWNwMjU2azGhA0hGa4jZJZYQAS-z6ZFK-m4GCFnWS8wfjO0bpSQn6hyEiHN5bmNuZXRzAIN0Y3CCIyiDdWRwgiMo", - "enr:-Ku4QLVumWTwyOUVS4ajqq8ZuZz2ik6t3Gtq0Ozxqecj0qNZWpMnudcvTs-4jrlwYRQMQwBS8Pvtmu4ZPP2Lx3i2t7YBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhNEmfKCJc2VjcDI1NmsxoQLdRlI8aCa_ELwTJhVN8k7km7IDc3pYu-FMYBs5_FiigIN1ZHCCIyk", - "enr:-LK4QAYuLujoiaqCAs0-qNWj9oFws1B4iy-Hff1bRB7wpQCYSS-IIMxLWCn7sWloTJzC1SiH8Y7lMQ5I36ynGV1ASj4Eh2F0dG5ldHOIYAAAAAAAAACEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQOmI5MlAu3f5WEThAYOqoygpS2wYn0XS5NV2aYq7T0a04N0Y3CCIyiDdWRwgiMo", - "enr:-Ku4QIC89sMC0o-irosD4_23lJJ4qCGOvdUz7SmoShWx0k6AaxCFTKviEHa-sa7-EzsiXpDp0qP0xzX6nKdXJX3X-IQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbRilSJc2VjcDI1NmsxoQK_m0f1DzDc9Cjrspm36zuRa7072HSiMGYWLsKiVSbP34N1ZHCCIyk", - "enr:-Ku4QNkWjw5tNzo8DtWqKm7CnDdIq_y7xppD6c1EZSwjB8rMOkSFA1wJPLoKrq5UvA7wcxIotH6Usx3PAugEN2JMncIBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBd9cEGEAAJEP__________gmlkgnY0gmlwhIbHuBeJc2VjcDI1NmsxoQP3FwrhFYB60djwRjAoOjttq6du94DtkQuaN99wvgqaIYN1ZHCCIyk", - "enr:-OS4QMJGE13xEROqvKN1xnnt7U-noc51VXyM6wFMuL9LMhQDfo1p1dF_zFdS4OsnXz_vIYk-nQWnqJMWRDKvkSK6_CwDh2F0dG5ldHOIAAAAADAAAACGY2xpZW502IpMaWdodGhvdXNljDcuMC4wLWJldGEuM4RldGgykNLxmX9gAAkQAAgAAAAAAACCaWSCdjSCaXCEhse4F4RxdWljgiMqiXNlY3AyNTZrMaECef77P8k5l3PC_raLw42OAzdXfxeQ-58BJriNaqiRGJSIc3luY25ldHMAg3RjcIIjKIN1ZHCCIyg", - "enr:-LK4QDwhXMitMbC8xRiNL-XGMhRyMSOnxej-zGifjv9Nm5G8EF285phTU-CAsMHRRefZimNI7eNpAluijMQP7NDC8kEMh2F0dG5ldHOIAAAAAAAABgCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAOIT_SJc2VjcDI1NmsxoQMoHWNL4MAvh6YpQeM2SUjhUrLIPsAVPB8nyxbmckC6KIN0Y3CCIyiDdWRwgiMo", - "enr:-LK4QPYl2HnMPQ7b1es6Nf_tFYkyya5bj9IqAKOEj2cmoqVkN8ANbJJJK40MX4kciL7pZszPHw6vLNyeC-O3HUrLQv8Mh2F0dG5ldHOIAAAAAAAAAMCEZXRoMpDS8Zl_YAAJEAAIAAAAAAAAgmlkgnY0gmlwhAMYRG-Jc2VjcDI1NmsxoQPQ35tjr6q1qUqwAnegQmYQyfqxC_6437CObkZneI9n34N0Y3CCIyiDdWRwgiMo", - "enr:-KG4QKRSUi4IOAIK_xt5ERrwW_J47wmNCLWFh7Jo0hFE69drZsiZ5Pb5CEcM_njFTTLlIR6SCf67HTcSV1g6hCXdhWkCgmlkgnY0gmlwhLkvrBODaXA2kCoGxcAWAAAYAAAAAAAAABCJc2VjcDI1NmsxoQPU7g2jQGTz8BYbB2vLTb39S_PrcZAehwMM0b3bWsM5rIN1ZHCCIyiEdWRwNoIjKA" - ] + "DiscoveryVersion": "V5" } } From 6781fe9778250baef1693395aeb2f67d89022e05 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 18:58:15 +0300 Subject: [PATCH 134/182] Stabilize discovery E2E test --- .../Modules/PseudoNethermindRunner.cs | 2 +- .../E2EDiscoveryTests.cs | 77 +++++++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindRunner.cs b/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindRunner.cs index 6f74bb97939e..b2f21482e8f6 100644 --- a/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindRunner.cs +++ b/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindRunner.cs @@ -83,7 +83,7 @@ public async Task StartDiscovery(CancellationToken cancellationToken) await ctx.Resolve().InitAsync(); _discoveryApp = ctx.Resolve(); - _ = _discoveryApp.StartAsync(); // Bootstrap is not blocking by default + await _discoveryApp.StartAsync(); _peerPool = ctx.Resolve(); _peerPool.Start(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs index 5663488a311f..d5448ea02af8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/E2EDiscoveryTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Autofac; @@ -26,11 +25,10 @@ namespace Nethermind.Network.Discovery.Test; [TestFixture(DiscoveryVersion.V5)] public class E2EDiscoveryTests(DiscoveryVersion discoveryVersion) { - private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(20); + private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan PeerDiscoveryTimeout = TimeSpan.FromSeconds(45); + private static readonly TimeSpan PeerDiscoveryPollInterval = TimeSpan.FromMilliseconds(100); - /// - /// Common code for all node - /// private IContainer CreateNode(PrivateKey nodeKey, IEnode? bootEnode = null) { ConfigProvider configProvider = new(); @@ -50,6 +48,7 @@ private IContainer CreateNode(PrivateKey nodeKey, IEnode? bootEnode = null) networkConfig.P2PPort = port; IDiscoveryConfig discoveryConfig = configProvider.GetConfig(); discoveryConfig.DiscoveryVersion = discoveryVersion; + discoveryConfig.UseDefaultDiscv5Bootnodes = false; return new ContainerBuilder() .AddModule(new PseudoNethermindModule(spec, configProvider, new TestLogManager())) @@ -67,7 +66,7 @@ private IContainer CreateNode(PrivateKey nodeKey, IEnode? bootEnode = null) [Parallelizable(ParallelScope.None)] public async Task TestDiscovery() { - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource().ThatCancelAfter(TestTimeout); + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource().ThatCancelAfter(TestTimeout); await using IContainer boot = CreateNode(TestItem.PrivateKeys[0]); IEnode bootEnode = boot.Resolve(); @@ -78,21 +77,73 @@ public async Task TestDiscovery() IContainer[] nodes = [boot, node1, node2, node3, node4]; - HashSet nodeKeys = nodes.Select(ctx => ctx.Resolve().PublicKey).ToHashSet(); + HashSet nodeKeys = GetNodeKeys(nodes); foreach (IContainer node in nodes) { await node.Resolve().StartDiscovery(cancellationTokenSource.Token); } - foreach (IContainer node in nodes) + Task[] waitTasks = new Task[nodes.Length]; + for (int i = 0; i < nodes.Length; i++) + { + waitTasks[i] = AssertPeerPoolContainsExpectedNodes(nodes[i], nodeKeys, cancellationTokenSource.Token); + } + + await Task.WhenAll(waitTasks); + } + + private static HashSet GetNodeKeys(IContainer[] nodes) + { + HashSet nodeKeys = []; + for (int i = 0; i < nodes.Length; i++) + { + nodeKeys.Add(nodes[i].Resolve().PublicKey); + } + + return nodeKeys; + } + + private static async Task AssertPeerPoolContainsExpectedNodes(IContainer node, HashSet nodeKeys, CancellationToken cancellationToken) + { + IPeerPool pool = node.Resolve(); + PublicKey localKey = node.Resolve().PublicKey; + HashSet expectedKeys = [.. nodeKeys]; + expectedKeys.Remove(localKey); + + using CancellationTokenSource timeoutCts = new(PeerDiscoveryTimeout); + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + CancellationToken linkedToken = linkedCts.Token; + + while (!linkedToken.IsCancellationRequested) { - IPeerPool pool = node.Resolve(); - HashSet expectedKeys = [.. nodeKeys]; - expectedKeys.Remove(node.Resolve().PublicKey); + HashSet actualKeys = GetPeerKeys(pool); + if (actualKeys.SetEquals(expectedKeys)) + { + return; + } - Assert.That(() => pool.Peers.Select(static kvp => kvp.Value.Node.Id).ToHashSet(), - Is.EquivalentTo(expectedKeys).After(15000, 100)); + try + { + await Task.Delay(PeerDiscoveryPollInterval, linkedToken); + } + catch (OperationCanceledException) when (linkedToken.IsCancellationRequested) + { + break; + } } + + Assert.That(GetPeerKeys(pool), Is.EquivalentTo(expectedKeys), $"Node {localKey} did not discover all peers before {PeerDiscoveryTimeout}."); + } + + private static HashSet GetPeerKeys(IPeerPool pool) + { + HashSet peerKeys = []; + foreach (KeyValuePair peer in pool.Peers) + { + peerKeys.Add(peer.Value.Node.Id); + } + + return peerKeys; } } From 88096d4a3286be4ed6c45124e8d3dc9c6fb06a34 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 2 Jun 2026 22:39:11 +0300 Subject: [PATCH 135/182] Fix review --- .../Discv5/CodecTests.cs | 12 ++++++-- .../Discv5/Serializers/NodesMsgSerializer.cs | 29 ++++++++++++++++++- .../NodeRecordSignerTests.cs | 10 +++++++ .../NodeRecordSigner.cs | 10 +++---- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 416ccfe5f976..1404d1cb47dc 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -259,8 +259,9 @@ public void MessageCodec_Roundtrips_Nodes_From_NonZero_ArraySegment() } [Test] - public void MessageCodec_Rejects_Nodes_With_Invalid_Enr() + public void MessageCodec_Skips_Invalid_Enrs_In_Nodes() { + NodeRecord expectedRecord = CreateNodeRecord(new PrivateKey(GethNodeBPrivateKey)); byte[] invalidRecord = new byte[304]; invalidRecord[0] = 0xf9; invalidRecord[1] = 0x01; @@ -269,12 +270,17 @@ public void MessageCodec_Rejects_Nodes_With_Invalid_Enr() Rlp data = Rlp.Encode( Rlp.Encode(new byte[] { 1 }), Rlp.Encode(1), - Rlp.Encode(new Rlp(invalidRecord))); + Rlp.Encode(new Rlp(invalidRecord), new Rlp(expectedRecord.ToRlpBytes()), new Rlp(invalidRecord))); byte[] message = new byte[data.Length + 1]; message[0] = (byte)MessageType.Nodes; data.Bytes.CopyTo(message.AsSpan(1)); - Assert.That(() => MessageCodec.Decode(message), Throws.TypeOf()); + using Discv5Message decoded = MessageCodec.Decode(message); + + Assert.That(decoded, Is.InstanceOf()); + NodesMsg nodes = (NodesMsg)decoded; + Assert.That(nodes.Records.Count, Is.EqualTo(1)); + Assert.That(nodes.Records[0].EnrString, Is.EqualTo(expectedRecord.EnrString)); } private static PacketCodec CreateCodec(PrivateKey privateKey) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index 51631fb500a4..e6371c453f5b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics.CodeAnalysis; using Nethermind.Core.Collections; using Nethermind.Crypto; using Nethermind.Network.Discovery.Discv5.Messages; @@ -60,14 +61,40 @@ private NodeRecord[] DecodeNodeRecords(ref Rlp.ValueDecoderContext ctx) int checkPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(checkPosition); NodeRecord[] records = new NodeRecord[count]; + int recordCount = 0; for (int i = 0; i < count; i++) { ReadOnlySpan record = ctx.PeekNextItem(); - records[i] = NodeRecord.FromBytes(record, _ecdsa); ctx.SkipItem(); + if (TryDecodeNodeRecord(record, out NodeRecord? nodeRecord)) + { + records[recordCount++] = nodeRecord; + } } ctx.Check(checkPosition); + if (recordCount != count) + { + Array.Resize(ref records, recordCount); + } + return records; } + + private bool TryDecodeNodeRecord(ReadOnlySpan record, [NotNullWhen(true)] out NodeRecord? nodeRecord) + { + try + { + nodeRecord = NodeRecord.FromBytes(record, _ecdsa); + return true; + } + catch (Exception e) when (IsMalformedNodeRecordException(e)) + { + nodeRecord = null; + return false; + } + } + + private static bool IsMalformedNodeRecordException(Exception exception) + => exception is RlpException or ArgumentException or InvalidOperationException or FormatException; } diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index 7ffbc55d2430..1d623469c637 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -192,6 +192,16 @@ public void FromBytes_throws_when_record_has_trailing_bytes() Assert.That(() => NodeRecord.FromBytes(recordWithTrailingBytes), Throws.TypeOf()); } + [Test] + public void FromBytes_throws_rlp_exception_when_signature_cannot_recover() + { + byte[] publicKey = new byte[CompressedPublicKey.LengthInBytes]; + RlpStream rlpStream = CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), + (EnrContentKey.SecP256k1, stream => stream.Encode(publicKey), Rlp.LengthOf(publicKey))); + + Assert.That(() => NodeRecord.FromBytes(rlpStream.Data), Throws.TypeOf()); + } [Test] public void Cannot_verify_when_signature_missing() diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index 03dfe31cbd80..18cf3ed9bdaf 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -203,15 +203,15 @@ public bool Verify(NodeRecord nodeRecord) contentHash = nodeRecord.ContentHash; } - CompressedPublicKey publicKeyA = - _ecdsa.RecoverCompressedPublicKey(nodeRecord.Signature!, in contentHash)!; + CompressedPublicKey? publicKeyA = + _ecdsa.RecoverCompressedPublicKey(nodeRecord.Signature!, in contentHash); Signature sigB = new(nodeRecord.Signature!.Bytes, 1); - CompressedPublicKey publicKeyB = - _ecdsa.RecoverCompressedPublicKey(sigB, in contentHash)!; + CompressedPublicKey? publicKeyB = + _ecdsa.RecoverCompressedPublicKey(sigB, in contentHash); CompressedPublicKey? reportedKey = nodeRecord.GetObj(EnrContentKey.SecP256k1); - return publicKeyA.Equals(reportedKey) || publicKeyB.Equals(reportedKey); + return publicKeyA?.Equals(reportedKey) == true || publicKeyB?.Equals(reportedKey) == true; } } From 7b234314896105e282fc0b520938bb6d1bc3014c Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 15:54:07 +0300 Subject: [PATCH 136/182] Harden discv5 packet dispatch --- .../NettyDiscoveryV5HandlerTests.cs | 33 ++++++++++++++++--- .../Discv5/DiscoveryV5App.cs | 18 +++++----- .../Discv5/Kademlia/KademliaAdapter.cs | 21 +++++++++++- .../Discv5/NettyDiscoveryV5Handler.cs | 17 +++------- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index adb1122983df..c4ad99b21bbd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -70,8 +70,15 @@ public async Task ForwardsReceivedMessageToReader() Assert.That(await readTask, Is.True); PooledUdpReceiveResult forwardedPacket = enumerator.Current; - Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); - Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(from)); + try + { + Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); + Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(from)); + } + finally + { + forwardedPacket.Dispose(); + } } [Test] @@ -95,8 +102,15 @@ public async Task MapsIpv4MappedIpv6SenderToIpv4() Assert.That(await readTask, Is.True); PooledUdpReceiveResult forwardedPacket = enumerator.Current; - Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); - Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(expectedFrom)); + try + { + Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); + Assert.That(forwardedPacket.RemoteEndPoint, Is.EqualTo(expectedFrom)); + } + finally + { + forwardedPacket.Dispose(); + } } [TestCase(0)] @@ -122,7 +136,16 @@ public async Task SkipsMessagesOfInvalidSize(int size) _handler.Close(); Assert.That(await readTask, Is.True); - Assert.That(enumerator.Current.Buffer.ToArray(), Is.EqualTo(data)); + PooledUdpReceiveResult forwardedPacket = enumerator.Current; + try + { + Assert.That(forwardedPacket.Buffer.ToArray(), Is.EqualTo(data)); + } + finally + { + forwardedPacket.Dispose(); + } + Assert.That(await enumerator.MoveNextAsync(), Is.False); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 6164e106b749..8a2b361bffa6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -281,19 +281,19 @@ internal List LoadStoredEnrs() continue; } - if (enrs.Count is 0) + PublicKey? publicKey = GetPublicKeyFromEnr(enr); + if (publicKey is null) { - migrateBatch = _discoveryDb.StartWriteBatch(); - deleteBatch = _legacyDiscoveryDb.StartWriteBatch(); + deleteBatch ??= _legacyDiscoveryDb.StartWriteBatch(); + deleteBatch[kv.Key] = null; + continue; } + migrateBatch ??= _discoveryDb.StartWriteBatch(); + deleteBatch ??= _legacyDiscoveryDb.StartWriteBatch(); enrs.Add(enr); - PublicKey? publicKey = GetPublicKeyFromEnr(enr); - if (publicKey is not null) - { - migrateBatch![publicKey.Hash.Bytes] = kv.Value; - deleteBatch![kv.Key] = null; - } + migrateBatch[publicKey.Hash.Bytes] = kv.Value; + deleteBatch[kv.Key] = null; } } finally diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 99086e9ef1b9..a9f0d0045dff 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -37,6 +37,7 @@ public class KademliaAdapter( private const int MaxResponseHandlers = 1_024; private const int MaxKnownRecords = 16_384; private const int MaxEndpointChecks = 4_096; + private const int PacketWorkerCount = 4; private const long SentChallengeTtlMilliseconds = 60_000; private const long EndpointCheckTtlMilliseconds = 60_000; private static readonly TimeSpan ChallengeRateLimitWindow = TimeSpan.FromMilliseconds(100); @@ -139,12 +140,30 @@ public async Task Ping(Node receiver, CancellationToken token) } public async Task RunAsync(CancellationToken token) + { + Task[] workers = new Task[PacketWorkerCount]; + for (int i = 0; i < workers.Length; i++) + { + workers[i] = RunPacketWorkerAsync(token); + } + + await Task.WhenAll(workers); + } + + private async Task RunPacketWorkerAsync(CancellationToken token) { try { await foreach (PooledUdpReceiveResult result in discoveryHandler.ReadMessagesAsync(token)) { - await HandlePacket(result, token); + try + { + await HandlePacket(result, token); + } + finally + { + result.Dispose(); + } } } catch (OperationCanceledException) when (token.IsCancellationRequested) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 3ea75eb42bc4..a5de07eadb88 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -69,29 +69,22 @@ internal async IAsyncEnumerable ReadMessagesAsync([Syste { await foreach (DatagramPacket packet in _inboundQueue.Reader.ReadAllAsync(token)) { - PooledUdpReceiveResult receiveResult = default; - bool hasReceiveResult = false; try { - receiveResult = CreateReceiveResult(packet); - hasReceiveResult = true; - yield return receiveResult; + yield return CreateReceiveResult(packet); } finally { - if (hasReceiveResult) - { - receiveResult.Dispose(); - } - ReferenceCountUtil.Release(packet); } } } finally { - Interlocked.Decrement(ref _activeReaders); - ReleaseQueuedPackets(); + if (Interlocked.Decrement(ref _activeReaders) == 0) + { + ReleaseQueuedPackets(); + } } } From 25a00d6dfbcf55ae6bda57b11d1e2875d6f73bd3 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 15:56:59 +0300 Subject: [PATCH 137/182] Restore Kademlia distance SIMD --- .../Kademlia/Hash256KademliaDistance.cs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs index 822bf4be22c2..d79fc760f1b9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Numerics; using Nethermind.Core.Crypto; using Nethermind.Kademlia; @@ -25,13 +26,13 @@ public sealed class Hash256KademliaDistance : IKademliaDistance /// public int CalculateLogDistance(Hash256 left, Hash256 right) { - ReadOnlySpan leftBytes = left.Bytes; - ReadOnlySpan rightBytes = right.Bytes; + Span xorDistance = stackalloc byte[Hash256.Size]; + XorDistance(left.Bytes, right.Bytes, xorDistance); int zeros = 0; for (int i = 0; i < Hash256.Size; i++) { - byte xor = (byte)(leftBytes[i] ^ rightBytes[i]); + byte xor = xorDistance[i]; if (xor == 0) { zeros += 8; @@ -54,22 +55,13 @@ public int CalculateLogDistance(Hash256 left, Hash256 right) /// public int Compare(Hash256 left, Hash256 right, Hash256 target) { - ReadOnlySpan leftBytes = left.Bytes; - ReadOnlySpan rightBytes = right.Bytes; + Span leftDistance = stackalloc byte[Hash256.Size]; + Span rightDistance = stackalloc byte[Hash256.Size]; ReadOnlySpan targetBytes = target.Bytes; + XorDistance(left.Bytes, targetBytes, leftDistance); + XorDistance(right.Bytes, targetBytes, rightDistance); - for (int i = 0; i < Hash256.Size; i++) - { - byte leftDistance = (byte)(leftBytes[i] ^ targetBytes[i]); - byte rightDistance = (byte)(rightBytes[i] ^ targetBytes[i]); - int compared = leftDistance.CompareTo(rightDistance); - if (compared != 0) - { - return compared; - } - } - - return 0; + return leftDistance.SequenceCompareTo(rightDistance); } /// @@ -83,7 +75,8 @@ public bool GetBit(Hash256 key, int index) /// public Hash256 SetBit(Hash256 key, int index) { - byte[] bytes = key.Bytes.ToArray(); + Span bytes = stackalloc byte[Hash256.Size]; + key.Bytes.CopyTo(bytes); bytes[index / 8] |= (byte)(1 << (7 - (index % 8))); return new Hash256(bytes); } @@ -137,4 +130,18 @@ private Hash256 CopyForRandom(Hash256 currentHash, Span randomizedHash, in return new Hash256(randomizedHash); } + + private static void XorDistance(ReadOnlySpan left, ReadOnlySpan right, Span destination) + { + int i = 0; + for (; i <= destination.Length - Vector.Count; i += Vector.Count) + { + (new Vector(left[i..]) ^ new Vector(right[i..])).CopyTo(destination[i..]); + } + + for (; i < destination.Length; i++) + { + destination[i] = (byte)(left[i] ^ right[i]); + } + } } From 8bfcf7f86a14aa2005fb2b61a35a3f59ca316f57 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 15:57:22 +0300 Subject: [PATCH 138/182] Remove unused Kademlia factory --- .../Nethermind.Kademlia/KademliaFactory.cs | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs diff --git a/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs b/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs deleted file mode 100644 index df9baea8a7dd..000000000000 --- a/src/Nethermind/Nethermind.Kademlia/KademliaFactory.cs +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Logging; - -namespace Nethermind.Kademlia; - -/// -/// Creates the default Kademlia routing table, lookup algorithm, health tracker, and facade. -/// -public static class KademliaFactory -{ - /// - /// Creates the default Kademlia component graph for consumers that do not use a dependency-injection container. - /// - /// Maps nodes and lookup keys to the Kademlia key space. - /// Compares and manipulates values in the Kademlia key space. - /// Sends protocol-specific ping and find-neighbour requests. - /// Kademlia table and maintenance settings. - /// Optional log manager. When omitted, logging is disabled. - /// Optional time provider used for bucket refresh scheduling. - public static KademliaComponents Create( - IKeyOperator keyOperator, - IKademliaDistance distance, - IKademliaMessageSender sender, - KademliaConfig config, - ILogManager? logManager = null, - TimeProvider? timeProvider = null) - where TNode : notnull - where TKadKey : notnull - { - ArgumentNullException.ThrowIfNull(keyOperator); - ArgumentNullException.ThrowIfNull(distance); - ArgumentNullException.ThrowIfNull(sender); - ArgumentNullException.ThrowIfNull(config); - - FromKeyNodeHashProvider nodeHashProvider = new(keyOperator); - KBucketTree routingTable = new(config, nodeHashProvider, distance, logManager); - NodeHealthTracker nodeHealthTracker = new(config, routingTable, nodeHashProvider, sender, logManager); - LookupKNearestNeighbour lookup = new(routingTable, nodeHashProvider, distance, nodeHealthTracker, config, logManager); - Kademlia kademlia = new( - keyOperator, - sender, - routingTable, - lookup, - nodeHealthTracker, - config, - logManager, - timeProvider); - - return new KademliaComponents( - kademlia, - routingTable, - lookup, - nodeHashProvider, - nodeHealthTracker); - } -} - -/// -/// Owns a Kademlia instance and the default components created for it. -/// -public sealed class KademliaComponents( - Kademlia kademlia, - IRoutingTable routingTable, - ILookupAlgo lookup, - INodeHashProvider nodeHashProvider, - NodeHealthTracker nodeHealthTracker) : IDisposable - where TNode : notnull - where TKadKey : notnull -{ - /// - /// The high-level Kademlia facade. - /// - public Kademlia Kademlia { get; } = kademlia; - - /// - /// The routing table used by . - /// - public IRoutingTable RoutingTable { get; } = routingTable; - - /// - /// The iterative closest-node lookup algorithm used by . - /// - public ILookupAlgo Lookup { get; } = lookup; - - /// - /// Maps nodes to their Kademlia hash. - /// - public INodeHashProvider NodeHashProvider { get; } = nodeHashProvider; - - /// - /// Tracks liveness and evicts failed nodes. - /// - public NodeHealthTracker NodeHealthTracker { get; } = nodeHealthTracker; - - /// - public void Dispose() => NodeHealthTracker.Dispose(); -} From 4826b7654282811290a011b40ba38bb38e5c5c12 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 16:10:18 +0300 Subject: [PATCH 139/182] Reduce discovery duplication --- .../Handlers/NodesResponseHandlerTests.cs | 42 ++++--------------- .../Kademlia/KademliaSimulation.cs | 18 +------- .../Kademlia/KademliaTests.cs | 15 ++----- .../Kademlia/ValueHashKeyOperator.cs | 23 ++++++++++ .../AddressBurstLimiter.cs | 41 ++++++++++++++++++ .../Discv4/Kademlia/KademliaModule.cs | 12 ++---- .../Discv4/NettyDiscoveryHandler.cs | 36 ++++------------ .../Serializers/DiscoveryMsgSerializerBase.cs | 9 ++-- .../Discv5/Kademlia/KademliaAdapter.cs | 27 ++---------- .../Discv5/Kademlia/KademliaModule.cs | 10 ++--- .../Discv5/Serializers/PongMsgSerializer.cs | 31 ++------------ .../Kademlia/DiscoveryKademliaModuleBase.cs | 26 ++++++++++++ .../Serializers/IPAddressRlp.cs | 31 ++++++++++++++ 13 files changed, 159 insertions(+), 162 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/ValueHashKeyOperator.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/AddressBurstLimiter.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index 330b9139d40a..0957a08ed8a3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -16,43 +16,19 @@ namespace Nethermind.Network.Discovery.Test.Discv5.Handlers; public class NodesResponseHandlerTests { - [Test] - public void ShouldRejectNonRoutableRecordFromPublicReceiver() + [TestCase("8.8.8.8", "127.0.0.1", 0)] + [TestCase("127.0.0.1", "127.0.0.1", 1)] + [TestCase("127.0.0.1", "192.0.2.1", 0)] + public void ShouldFilterRecordByReceiverAndRecordAddress(string receiverIp, string recordIp, int expectedCount) { - Node receiver = new(TestItem.PublicKeyA, "8.8.8.8", 30303); - NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); - NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); + Node receiver = new(TestItem.PublicKeyA, receiverIp, 30303); + NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse(recordIp)); + NodesResponseHandler handler = CreateNodesResponseHandler(receiver, record); - using NodesMsg nodes = new([1], 1, [loopbackRecord]); + using NodesMsg nodes = new([1], 1, [record]); handler.Handle(nodes); - Assert.That(handler.GetNodes(), Is.Empty); - } - - [Test] - public void ShouldAcceptNonRoutableRecordFromNonRoutableReceiver() - { - Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); - NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); - NodesResponseHandler handler = CreateNodesResponseHandler(receiver, loopbackRecord); - - using NodesMsg nodes = new([1], 1, [loopbackRecord]); - handler.Handle(nodes); - - Assert.That(handler.GetNodes(), Has.Length.EqualTo(1)); - } - - [Test] - public void ShouldRejectSpecialUseRecordFromNonRoutableReceiver() - { - Node receiver = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 30303); - NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); - NodesResponseHandler handler = CreateNodesResponseHandler(receiver, documentationRecord); - - using NodesMsg nodes = new([1], 1, [documentationRecord]); - handler.Handle(nodes); - - Assert.That(handler.GetNodes(), Is.Empty); + Assert.That(handler.GetNodes(), Has.Length.EqualTo(expectedCount)); } private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index c52dbf7ddef2..f6316bc1d4a3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -185,21 +185,7 @@ private static ValueHash256 RandomKeccak(Random rand) return val; } - private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - - private static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; - - private class ValueHashNodeHashProvider : IKeyOperator - { - public ValueHash256 GetKey(TestNode node) => node.Hash; - - public Hash256 GetKeyHash(ValueHash256 key) => ToHash(key); - - public ValueHash256 CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) => - ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(nodePrefix, depth)); - - public Hash256 GetHash(ValueHash256 key) => ToHash(key); - } + private static Hash256 ToHash(ValueHash256 hash) => ValueHashKeyOperator.ToHash(hash); private class TestFabric(KademliaConfig config) { @@ -211,7 +197,7 @@ private class TestFabric(KademliaConfig config) public bool SimulateLatency { get; set; } = false; internal ConcurrentDictionary _nodes = new(); - readonly ValueHashNodeHashProvider _nodeHashProvider = new(); + private readonly ValueHashKeyOperator _nodeHashProvider = new(static node => node.Hash); private readonly Random _random = new(0); private bool TryGetReceiver(TestNode receiverHash, out ReceiverForNode contentKademliaMessageReceiver) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 52f9e6a11af4..2da078c83ab5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -26,7 +26,7 @@ private Nethermind.Kademlia.Kademlia Create .AddSingleton(new TestLogManager(LogLevel.Trace)) .AddSingleton(new ManualTimestamper(new System.DateTime(2025, 5, 13, 21, 0, 0, System.DateTimeKind.Utc))) .AddSingleton>(Hash256KademliaDistance.Instance) - .AddSingleton>(new ValueHashNodeHashProvider()) + .AddSingleton>(new ValueHashKeyOperator(static node => node)) .AddSingleton(config) .AddSingleton(_kademliaMessageSender) .AddSingleton>() @@ -197,20 +197,11 @@ public async Task TestTooManyNodeWithAcceleratedLookup() Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(testHashes[10..].ToHashSet())); } - private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); + private static Hash256 ToHash(ValueHash256 hash) => ValueHashKeyOperator.ToHash(hash); - private static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; + private static ValueHash256 ToValueHash(Hash256 hash) => ValueHashKeyOperator.ToValueHash(hash); private static ValueHash256 RandomValueHashAtDistance(ValueHash256 currentHash, int distance) => ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(ToHash(currentHash), distance)); - private class ValueHashNodeHashProvider : IKeyOperator - { - public ValueHash256 GetKey(ValueHash256 node) => node; - - public Hash256 GetKeyHash(ValueHash256 key) => ToHash(key); - - public ValueHash256 CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) => - ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(nodePrefix, depth)); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/ValueHashKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/ValueHashKeyOperator.cs new file mode 100644 index 000000000000..70936ace0655 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/ValueHashKeyOperator.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Kademlia; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +internal sealed class ValueHashKeyOperator(Func getKey) : IKeyOperator +{ + public static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); + + public static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; + + public ValueHash256 GetKey(TNode node) => getKey(node); + + public Hash256 GetKeyHash(ValueHash256 key) => ToHash(key); + + public ValueHash256 CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) + => ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(nodePrefix, depth)); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/AddressBurstLimiter.cs b/src/Nethermind/Nethermind.Network.Discovery/AddressBurstLimiter.cs new file mode 100644 index 000000000000..2768ed561a81 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/AddressBurstLimiter.cs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; + +namespace Nethermind.Network.Discovery; + +internal sealed class AddressBurstLimiter +{ + private readonly NodeFilter[] _filters; + + public AddressBurstLimiter(int burstPerAddress, int filterSize, TimeSpan window) + { + _filters = new NodeFilter[Math.Max(1, burstPerAddress)]; + for (int i = 0; i < _filters.Length; i++) + { + _filters[i] = NodeFilter.CreateExact(Math.Max(1, filterSize), window); + } + } + + public AddressBurstLimiter(NodeFilter filter) + { + ArgumentNullException.ThrowIfNull(filter); + + _filters = [filter]; + } + + public bool TryAccept(IPAddress address) + { + NodeFilter[] filters = _filters; + for (int i = 0; i < filters.Length; i++) + { + if (filters[i].TryAccept(address, exactOnly: true)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index cbf1b869c236..89a7b17a7452 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -17,9 +17,9 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; /// /// /// -public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module +public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) { - protected override void Load(ContainerBuilder builder) => builder + protected override void RegisterProtocolServices(ContainerBuilder builder) => builder .AddSingleton() // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. @@ -28,13 +28,7 @@ protected override void Load(ContainerBuilder builder) => builder // Some transport wiring. .AddSingleton() .Bind() - .AddSingleton() - - // Register the main kademlia module and integration - .AddModule(new KademliaModule()) .Bind, IKademliaAdapter>() - .AddSingleton>(Hash256KademliaDistance.Instance) - .AddSingleton, PublicKeyKeyOperator>() - .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)) + .AddSingleton() ; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs index 87550751c00f..cfe77b2760d2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs @@ -39,14 +39,14 @@ public class NettyDiscoveryHandler( private const int DefaultInboundMessageFilterSize = 8_192; private const int DefaultGlobalInboundMessageBurst = 512; private const int DefaultInboundMessageQueueCapacity = 1_024; - private const int DefaultInboundMessageWorkerCount = 16; + private const int DefaultInboundMessageWorkerCount = 4; private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); private readonly IDiscoveryMsgListener _discoveryMsgListener = discoveryManager ?? throw new ArgumentNullException(nameof(discoveryManager)); private readonly IMessageSerializationService _msgSerializationService = msgSerializationService ?? throw new ArgumentNullException(nameof(msgSerializationService)); private readonly ITimestamper _timestamper = timestamper ?? throw new ArgumentNullException(nameof(timestamper)); - private readonly NodeFilter[] _inboundMessageFilters = inboundMessageFilter is null - ? CreateDefaultInboundMessageFilters() - : [inboundMessageFilter]; + private readonly AddressBurstLimiter _inboundMessageLimiter = inboundMessageFilter is null + ? new(DefaultInboundMessageBurstPerIp, DefaultInboundMessageFilterSize, DefaultInboundMessageWindow) + : new(inboundMessageFilter); private readonly FixedWindowLimiter _globalInboundMessageLimiter = new(Math.Max(1, globalInboundMessageBurst ?? DefaultGlobalInboundMessageBurst), DefaultGlobalInboundMessageWindow); private readonly Channel _inboundMessages = System.Threading.Channels.Channel.CreateBounded( new BoundedChannelOptions(Math.Max(1, inboundMessageQueueCapacity ?? DefaultInboundMessageQueueCapacity)) @@ -265,32 +265,10 @@ private static void ReportMsgByType(DiscoveryMsg msg, int size) Metrics.DiscoveryMessagesReceived.Increment(msg.MsgType); } + // Allow a small burst from the same IP so split Neighbors and other valid + // multi-packet exchanges are not dropped before signature verification. private bool TryAcceptInbound(IPEndPoint remoteEndpoint) - { - // Allow a small burst from the same IP so split Neighbors and other valid - // multi-packet exchanges are not dropped before signature verification. - NodeFilter[] inboundMessageFilters = _inboundMessageFilters; - for (int i = 0; i < inboundMessageFilters.Length; i++) - { - if (inboundMessageFilters[i].TryAccept(remoteEndpoint.Address, exactOnly: true)) - { - return true; - } - } - - return false; - } - - private static NodeFilter[] CreateDefaultInboundMessageFilters() - { - NodeFilter[] filters = new NodeFilter[DefaultInboundMessageBurstPerIp]; - for (int i = 0; i < filters.Length; i++) - { - filters[i] = NodeFilter.CreateExact(DefaultInboundMessageFilterSize, DefaultInboundMessageWindow); - } - - return filters; - } + => _inboundMessageLimiter.TryAccept(remoteEndpoint.Address); private async Task LogDisconnectFailureAsync(Task disconnectTask) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs index 4869ce6cfa11..29009e4cd5c8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs @@ -9,6 +9,7 @@ using Nethermind.Core.Extensions; using Nethermind.Crypto; using Nethermind.Network.Discovery.Discv4.Messages; +using Nethermind.Network.Discovery.Serializers; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Discovery.Discv4.Serializers; @@ -112,7 +113,7 @@ protected void AddSignatureAndMdc(IByteBuffer byteBuffer, int dataLength) protected static void Encode(RlpStream stream, IPEndPoint address, int length) { stream.StartSequence(length); - stream.Encode(address.Address.GetAddressBytes()); + IPAddressRlp.Encode(stream, address.Address); //tcp port stream.Encode(address.Port); //udp port @@ -121,7 +122,7 @@ protected static void Encode(RlpStream stream, IPEndPoint address, int length) protected static int GetIPEndPointLength(IPEndPoint address) { - int length = Rlp.LengthOf(address.Address.GetAddressBytes()); + int length = IPAddressRlp.GetLength(address.Address); length += Rlp.LengthOf(address.Port); length += Rlp.LengthOf(address.Port); return length; @@ -131,7 +132,7 @@ protected static void SerializeNode(RlpStream stream, IPEndPoint address, byte[] { int length = GetLengthSerializeNode(address, id); stream.StartSequence(length); - stream.Encode(address.Address.GetAddressBytes()); + IPAddressRlp.Encode(stream, address.Address); //tcp port stream.Encode(address.Port); //udp port @@ -141,7 +142,7 @@ protected static void SerializeNode(RlpStream stream, IPEndPoint address, byte[] protected static int GetLengthSerializeNode(IPEndPoint address, byte[] id) { - int length = Rlp.LengthOf(address.Address.GetAddressBytes()); + int length = IPAddressRlp.GetLength(address.Address); length += Rlp.LengthOf(address.Port); length += Rlp.LengthOf(address.Port); length += Rlp.LengthOf(id); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index a9f0d0045dff..f922ac688afe 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -20,7 +20,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// Maps discv5 FINDNODE distance requests onto the protocol-specific Kademlia table. /// public class KademliaAdapter( - Lazy> kademlia, + Lazy> kademlia, // Cyclic dependency: Kademlia uses this adapter as its message sender. NettyDiscoveryV5Handler discoveryHandler, PacketCodec packetCodec, INodeRecordProvider nodeRecordProvider, @@ -57,7 +57,7 @@ public class KademliaAdapter( private readonly LruCache _responseHandlers = new(MaxResponseHandlers, "discv5 response handlers"); private readonly LruCache _knownRecords = new(MaxKnownRecords, "discv5 known records"); private readonly LruCache _endpointChecks = new(MaxEndpointChecks, "discv5 endpoint checks"); - private readonly NodeFilter[] _challengeRateLimiters = CreateChallengeRateLimiters(); + private readonly AddressBurstLimiter _challengeRateLimiter = new(ChallengeRateLimitBurstPerIp, ChallengeRateLimitFilterSize, ChallengeRateLimitWindow); /// public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = null) @@ -719,28 +719,7 @@ private static bool IsExpired(SentChallenge challenge, long now) => now - challenge.CreatedAtMilliseconds > SentChallengeTtlMilliseconds; internal bool TryAcceptChallenge(IPEndPoint endpoint) - { - for (int i = 0; i < _challengeRateLimiters.Length; i++) - { - if (_challengeRateLimiters[i].TryAccept(endpoint.Address, exactOnly: true)) - { - return true; - } - } - - return false; - } - - private static NodeFilter[] CreateChallengeRateLimiters() - { - NodeFilter[] filters = new NodeFilter[ChallengeRateLimitBurstPerIp]; - for (int i = 0; i < filters.Length; i++) - { - filters[i] = NodeFilter.CreateExact(ChallengeRateLimitFilterSize, ChallengeRateLimitWindow); - } - - return filters; - } + => _challengeRateLimiter.TryAccept(endpoint.Address); private void StartEndpointCheck(Node remoteNode, CancellationToken token) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs index af9c577a3423..843fb207a6a4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs @@ -14,16 +14,12 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Specifies the protocol-specific Kademlia services used by discv5. /// -public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : Module +public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) { - protected override void Load(ContainerBuilder builder) => builder + protected override void RegisterProtocolServices(ContainerBuilder builder) => builder .AddSingleton() .AddSingleton() .Bind, IKademliaAdapter>() .AddSingleton() - .AddSingleton() - .AddModule(new KademliaModule()) - .AddSingleton>(Hash256KademliaDistance.Instance) - .AddSingleton, PublicKeyKeyOperator>() - .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)); + .AddSingleton(); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs index 49d12c207767..0472ff8e1ae6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -4,6 +4,7 @@ using System.Net; using Nethermind.Core.Collections; using Nethermind.Network.Discovery.Discv5.Messages; +using Nethermind.Network.Discovery.Serializers; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Discovery.Discv5.Serializers; @@ -13,14 +14,14 @@ internal sealed class PongMsgSerializer : MsgSerializerBase public int GetContentLength(PongMsg msg) => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.EnrSequence) + - GetAddressRlpLength(msg.RecipientIp) + + IPAddressRlp.GetLength(msg.RecipientIp) + Rlp.LengthOf(msg.RecipientPort); public void Serialize(NettyRlpStream stream, PongMsg msg) { EncodeRequestId(stream, msg.RequestId); Encode(stream, msg.EnrSequence); - EncodeAddress(stream, msg.RecipientIp); + IPAddressRlp.Encode(stream, msg.RecipientIp); Encode(stream, msg.RecipientPort); } @@ -32,30 +33,4 @@ public PongMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, return new PongMsg(requestId, enrSequence, recipientIp, recipientPort, owner); } - private static int GetAddressRlpLength(IPAddress ip) - { - if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetwork) - { - return Rlp.LengthOfByteString(4, 0); - } - - if (ip.AddressFamily is System.Net.Sockets.AddressFamily.InterNetworkV6) - { - return Rlp.LengthOfByteString(16, 0); - } - - return Rlp.LengthOf(ip.GetAddressBytes()); - } - - private static void EncodeAddress(NettyRlpStream stream, IPAddress ip) - { - Span bytes = stackalloc byte[16]; - if (ip.TryWriteBytes(bytes, out int bytesWritten)) - { - stream.Encode(bytes[..bytesWritten]); - return; - } - - stream.Encode(ip.GetAddressBytes()); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs new file mode 100644 index 000000000000..6e7c4b9b2bd8 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Autofac; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Kademlia; +using Nethermind.Stats.Model; + +namespace Nethermind.Network.Discovery.Kademlia; + +public abstract class DiscoveryKademliaModuleBase(PublicKey masterNode, IReadOnlyList bootNodes) : Module +{ + protected override void Load(ContainerBuilder builder) + { + RegisterProtocolServices(builder); + + builder + .AddModule(new KademliaModule()) + .AddSingleton>(Hash256KademliaDistance.Instance) + .AddSingleton, PublicKeyKeyOperator>() + .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)); + } + + protected abstract void RegisterProtocolServices(ContainerBuilder builder); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs new file mode 100644 index 000000000000..f9c14a9f9dec --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; +using System.Net.Sockets; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Serializers; + +internal static class IPAddressRlp +{ + public static int GetLength(IPAddress ip) + => ip.AddressFamily switch + { + AddressFamily.InterNetwork => Rlp.LengthOfByteString(4, 0), + AddressFamily.InterNetworkV6 => Rlp.LengthOfByteString(16, 0), + _ => Rlp.LengthOf(ip.GetAddressBytes()) + }; + + public static void Encode(RlpStream stream, IPAddress ip) + { + Span bytes = stackalloc byte[16]; + if (ip.TryWriteBytes(bytes, out int bytesWritten)) + { + stream.Encode(bytes[..bytesWritten]); + return; + } + + stream.Encode(ip.GetAddressBytes()); + } +} From 464af4bb837724aa50c93aaa80279e3029db5202 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 16:21:43 +0300 Subject: [PATCH 140/182] Harden Kademlia cleanup --- .../Caching/LruCacheTests.cs | 39 ++++++ .../Nethermind.Core/Caching/LruCache.cs | 46 ++++++-- .../Nethermind.Kademlia/Kademlia.cs | 49 +++++++- .../Nethermind.Kademlia/NodeHealthTracker.cs | 6 +- .../Discv5/Kademlia/AdapterState.cs | 5 +- .../Discv5/Kademlia/KademliaAdapter.cs | 111 ++++++++++++------ .../Discv5/Packets/Challenge.cs | 12 +- .../Discv5/Packets/Session.cs | 45 ++++++- 8 files changed, 254 insertions(+), 59 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index d0d492d0c22c..c46bffbb3202 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -247,6 +247,45 @@ public void Can_remove_and_return_value() Assert.That(removed, Is.Null); } + [Test] + public void Eviction_callback_is_called_when_capacity_replaces_oldest() + { + int evicted = 0; + LruCache cache = new(2, "test", value => evicted = value); + + cache.Set(1, 10); + cache.Set(2, 20); + cache.Set(3, 30); + + Assert.That(evicted, Is.EqualTo(10)); + } + + [Test] + public void Eviction_callback_is_called_when_existing_value_is_replaced() + { + int evicted = 0; + LruCache cache = new(2, "test", value => evicted = value); + + cache.Set(1, 10); + cache.Set(1, 11); + + Assert.That(evicted, Is.EqualTo(10)); + Assert.That(cache.Get(1), Is.EqualTo(11)); + } + + [Test] + public void TryRemove_returns_value_without_calling_eviction_callback() + { + int evicted = 0; + LruCache cache = new(2, "test", value => evicted = value); + cache.Set(1, 10); + + Assert.That(cache.TryRemove(1, out int removed), Is.True); + + Assert.That(removed, Is.EqualTo(10)); + Assert.That(evicted, Is.Zero); + } + [Test] public void Clear_should_free_all_capacity() { diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 7747e245da7d..8bf66c6cf8e5 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -16,21 +16,23 @@ public sealed class LruCache : ICache where TKey : n private readonly Dictionary> _cacheMap; private readonly McsLock _lock = new(); private readonly string _name; + private readonly Action? _onEvict; private LinkedListNode? _leastRecentlyUsed; - public LruCache(int maxCapacity, int startCapacity, string name) + public LruCache(int maxCapacity, int startCapacity, string name, Action? onEvict = null) { ArgumentOutOfRangeException.ThrowIfLessThan(maxCapacity, 1); _name = name; _maxCapacity = maxCapacity; + _onEvict = onEvict; _cacheMap = typeof(TKey) == typeof(byte[]) ? new Dictionary>((IEqualityComparer)Bytes.EqualityComparer) : new Dictionary>(startCapacity); // do not initialize it at the full capacity } - public LruCache(int maxCapacity, string name) - : this(maxCapacity, 0, name) + public LruCache(int maxCapacity, string name, Action? onEvict = null) + : this(maxCapacity, 0, name, onEvict) { } @@ -38,6 +40,7 @@ public void Clear() { using McsLock.Disposable lockRelease = _lock.Acquire(); + NotifyEvictedValues(); _leastRecentlyUsed = null; _cacheMap.Clear(); } @@ -123,6 +126,7 @@ public bool Set(TKey key, TValue val) if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) { + NotifyEvicted(node.Value.Value); node.Value.Value = val; LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); return false; @@ -159,7 +163,7 @@ public bool TryRemove(TKey key, [MaybeNullWhen(false)] out TValue value) if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) { value = node.Value.Value; - RemoveNoLock(key, node); + RemoveNoLock(key, node, notifyEviction: false); return true; } @@ -171,15 +175,20 @@ private bool DeleteNoLock(TKey key) { if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) { - RemoveNoLock(key, node); + RemoveNoLock(key, node, notifyEviction: true); return true; } return false; } - private void RemoveNoLock(TKey key, LinkedListNode node) + private void RemoveNoLock(TKey key, LinkedListNode node, bool notifyEviction) { + if (notifyEviction) + { + NotifyEvicted(node.Value.Value); + } + LinkedListNode.Remove(ref _leastRecentlyUsed, node); _cacheMap.Remove(key); } @@ -228,17 +237,40 @@ private void Replace(TKey key, TValue value) ThrowInvalidOperationException(); } - _cacheMap.Remove(node!.Value.Key); + TValue evictedValue = node!.Value.Value; + _cacheMap.Remove(node.Value.Key); node.Value = new(key, value); LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); _cacheMap.Add(key, node); + NotifyEvicted(evictedValue); [DoesNotReturn] static void ThrowInvalidOperationException() => throw new InvalidOperationException( $"{nameof(LruCache)} called {nameof(Replace)} when empty."); } + private void NotifyEvictedValues() + { + if (_onEvict is null) + { + return; + } + + foreach (KeyValuePair> kvp in _cacheMap) + { + _onEvict(kvp.Value.Value.Value); + } + } + + private void NotifyEvicted(TValue value) + { + if (_onEvict is not null && value is not null) + { + _onEvict(value); + } + } + private struct LruCacheItem(TKey k, TValue v) { public readonly TKey Key = k; diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index 1324dca6825b..e68a68c62ce0 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -75,20 +75,24 @@ public Kademlia( private bool SameAsSelf(TNode node) => EqualityComparer.Default.Equals(_keyOperator.GetNodeHash(node), _currentNodeIdAsHash); - public Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) => _lookupAlgo.Lookup( - _keyOperator.GetKeyHash(key), + public Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null) + { + TKadKey keyHash = _keyOperator.GetKeyHash(key); + return _lookupAlgo.Lookup( + keyHash, k ?? _kSize, async (nextNode, token) => { if (SameAsSelf(nextNode)) { - TKadKey keyHash = _keyOperator.GetKeyHash(key); return _routingTable.GetKNearestNeighbour(keyHash); } + return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); }, token ); + } public async Task Run(CancellationToken token) { @@ -148,16 +152,20 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => token.ThrowIfCancellationRequested(); - // Refresh stale non-empty buckets one by one. A refresh means to do a k-nearest node lookup for a random hash - // for that particular bucket. + // Refresh stale non-empty buckets one by one. Protocols whose wire lookup target cannot be synthesized from a + // bucket prefix may return a best-effort random lookup key here; discv4 public keys are one example. + HashSet activeBucketPrefixes = []; foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { + activeBucketPrefixes.Add(Prefix); if (!ShouldRefreshBucket(Prefix, Bucket)) continue; TKey? keyToLookup = _keyOperator.CreateRandomKeyAtDistance(Prefix, Distance); await LookupNodesClosest(keyToLookup, token); } + PruneLastBucketRefreshTicks(activeBucketPrefixes); + if (_logger.IsDebug) { _logger.Debug($"Bootstrap completed. Took {sw.Elapsed}."); @@ -183,6 +191,37 @@ private bool ShouldRefreshBucket(TKadKey prefix, KBucket bucket) } } + private void PruneLastBucketRefreshTicks(HashSet activeBucketPrefixes) + { + lock (_lastBucketRefreshLock) + { + if (_lastBucketRefreshTicks.Count <= activeBucketPrefixes.Count) + { + return; + } + + List? stalePrefixes = null; + foreach (TKadKey prefix in _lastBucketRefreshTicks.Keys) + { + if (!activeBucketPrefixes.Contains(prefix)) + { + stalePrefixes ??= []; + stalePrefixes.Add(prefix); + } + } + + if (stalePrefixes is null) + { + return; + } + + for (int i = 0; i < stalePrefixes.Count; i++) + { + _lastBucketRefreshTicks.Remove(stalePrefixes[i]); + } + } + } + public TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false) { TKadKey hash = _keyOperator.GetKeyHash(target); diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index 97f84b75b5fa..c06fa1fcc06d 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -258,11 +258,7 @@ private void Trim() { while (_values.Count > capacity) { - LinkedListNode? oldest = _order.First; - if (oldest is null) - { - return; - } + LinkedListNode oldest = _order.First!; _order.RemoveFirst(); _values.Remove(oldest.Value); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs index decc8fab1ebc..adbe9e65d646 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs @@ -37,4 +37,7 @@ public static NonceKey From(ReadOnlySpan nonce) internal sealed record PendingRequest(Node Receiver, Discv5Message Message); -internal readonly record struct SentChallenge(Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds); +internal readonly record struct SentChallenge(Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds) : IDisposable +{ + public void Dispose() => Challenge.Dispose(); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index f922ac688afe..7e68e0a77103 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -48,8 +48,8 @@ public class KademliaAdapter( private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); private readonly IKademliaDistance _distance = distance; private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions"); - private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); + private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions", static session => session.Dispose()); + private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges", static sentChallenge => sentChallenge.Dispose()); private readonly Queue _sentChallengeExpiries = new(); private readonly object _sentChallengeExpiriesLock = new(); private long _lastSentChallengeTrimMilliseconds; @@ -219,27 +219,35 @@ private async Task SendRequest( SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); if (TryGetSession(sessionKey, out Session? session)) { - Span sessionNonce = stackalloc byte[PacketCodec.NonceSize]; - session.WriteNextNonce(cryptoRandom, sessionNonce); - PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceKey.From(sessionNonce)); - _pendingByNonce.Set(sessionPendingNonceKey, new PendingRequest(receiver, message)); - byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, sessionNonce); - try + Span writeKey = stackalloc byte[Session.KeySize]; + if (!session.TryCopyWriteKey(writeKey)) { - if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} with existing session, bytes: {packet.Length}."); - await discoveryHandler.SendAsync(packet, receiver.Address); - return sessionPendingNonceKey; + _sessions.TryRemove(sessionKey, out _); } - catch + else { - _pendingByNonce.TryRemove(sessionPendingNonceKey, out _); - throw; + Span sessionNonce = stackalloc byte[PacketCodec.NonceSize]; + session.WriteNextNonce(cryptoRandom, sessionNonce); + PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceKey.From(sessionNonce)); + _pendingByNonce.Set(sessionPendingNonceKey, new PendingRequest(receiver, message)); + byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, writeKey, message, sessionNonce); + try + { + if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} with existing session, bytes: {packet.Length}."); + await discoveryHandler.SendAsync(packet, receiver.Address); + return sessionPendingNonceKey; + } + catch + { + _pendingByNonce.TryRemove(sessionPendingNonceKey, out _); + throw; + } } } Span nonce = stackalloc byte[PacketCodec.NonceSize]; cryptoRandom.GenerateRandomBytes(nonce); - Span encryptionKey = stackalloc byte[16]; + Span encryptionKey = stackalloc byte[Session.KeySize]; cryptoRandom.GenerateRandomBytes(encryptionKey); PendingRequest pendingRequest = new(receiver, message); PendingNonceKey pendingNonceKey = new(receiver.Address, NonceKey.From(nonce)); @@ -267,9 +275,16 @@ private async Task SendResponse(Node receiver, Discv5Message message, Cancellati return; } + Span writeKey = stackalloc byte[Session.KeySize]; + if (!session.TryCopyWriteKey(writeKey)) + { + _sessions.TryRemove(sessionKey, out _); + return; + } + Span nonce = stackalloc byte[PacketCodec.NonceSize]; session.WriteNextNonce(cryptoRandom, nonce); - byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, session.WriteKey, message, nonce); + byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, writeKey, message, nonce); if (_logger.IsTrace) _logger.Trace($"Sending discv5 response {message.MessageType} {message.RequestId} to {receiver:s}, bytes: {packet.Length}."); await discoveryHandler.SendAsync(packet, receiver.Address); } @@ -335,8 +350,10 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati } SessionKey sessionKey = new(nodeId, endpoint); + Span readKey = stackalloc byte[Session.KeySize]; if (!TryGetSession(sessionKey, out Session? session) || - !packetCodec.TryDecryptMessage(packet, session.ReadKey, out Discv5Message message)) + !session.TryCopyReadKey(readKey) || + !packetCodec.TryDecryptMessage(packet, readKey, out Discv5Message message)) { if (_logger.IsTrace) _logger.Trace($"Discv5 ordinary packet from {endpoint} could not be decrypted with an existing session; sending WHOAREYOU."); await SendWhoAreYou(endpoint, packet, nodeId); @@ -363,45 +380,58 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat } ChallengeKey challengeKey = new(nodeId, endpoint); - if (!_sentChallenges.TryRemove(challengeKey, out SentChallenge sentChallenge) || - IsExpired(sentChallenge, Environment.TickCount64)) + if (!_sentChallenges.TryRemove(challengeKey, out SentChallenge sentChallenge)) { - if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; matching challenge missing or expired."); + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; matching challenge missing."); return; } - TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); - if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) + if (IsExpired(sentChallenge, Environment.TickCount64)) { - if (_logger.IsTrace) _logger.Trace($"Unable to decrypt discv5 handshake packet from {endpoint}."); + sentChallenge.Dispose(); + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; matching challenge expired."); return; } try { - NodeRecord? messageRecord = knownRecord; - if (nodeRecord is not null) + TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); + if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) { - if (!HasExpectedNodeId(nodeRecord, nodeId)) - { - if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake ENR from {endpoint}; ENR node id does not match packet source."); - return; - } + if (_logger.IsTrace) _logger.Trace($"Unable to decrypt discv5 handshake packet from {endpoint}."); + return; + } - if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + try + { + NodeRecord? messageRecord = knownRecord; + if (nodeRecord is not null) { - SetKnownRecord(nodeId, nodeRecord); - messageRecord = nodeRecord; + if (!HasExpectedNodeId(nodeRecord, nodeId)) + { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake ENR from {endpoint}; ENR node id does not match packet source."); + return; + } + + if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + { + SetKnownRecord(nodeId, nodeRecord); + messageRecord = nodeRecord; + } } - } - SetSession(new SessionKey(nodeId, endpoint), session); - if (_logger.IsTrace) _logger.Trace($"Received discv5 handshake message {message.MessageType} {message.RequestId} from {endpoint}, ENR included: {nodeRecord is not null}."); - await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); + SetSession(new SessionKey(nodeId, endpoint), session); + if (_logger.IsTrace) _logger.Trace($"Received discv5 handshake message {message.MessageType} {message.RequestId} from {endpoint}, ENR included: {nodeRecord is not null}."); + await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); + } + finally + { + message.Dispose(); + } } finally { - message.Dispose(); + sentChallenge.Dispose(); } } @@ -709,7 +739,10 @@ private void TrimExpiredChallenges(long now) if (_sentChallenges.TryGet(expiry.Key, out SentChallenge challenge) && challenge.CreatedAtMilliseconds == expiry.CreatedAtMilliseconds) { - _sentChallenges.TryRemove(expiry.Key, out _); + if (_sentChallenges.TryRemove(expiry.Key, out SentChallenge removed)) + { + removed.Dispose(); + } } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs index 1aded8f9422d..0cf8d9a34ea7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs @@ -1,6 +1,16 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Security.Cryptography; + namespace Nethermind.Network.Discovery.Discv5.Packets; -internal sealed record Challenge(byte[] RequestNonce, byte[] IdNonce, ulong EnrSequence, byte[] ChallengeData); +internal sealed record Challenge(byte[] RequestNonce, byte[] IdNonce, ulong EnrSequence, byte[] ChallengeData) : IDisposable +{ + public void Dispose() + { + CryptographicOperations.ZeroMemory(RequestNonce); + CryptographicOperations.ZeroMemory(IdNonce); + CryptographicOperations.ZeroMemory(ChallengeData); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs index 404656b35bff..65f4dd70c3c5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs @@ -2,14 +2,19 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Buffers.Binary; +using System.Security.Cryptography; using Nethermind.Core.Crypto; using Nethermind.Crypto; namespace Nethermind.Network.Discovery.Discv5.Packets; -internal sealed record Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] WriteKey) +internal sealed record Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] WriteKey) : IDisposable { + public const int KeySize = 16; + + private readonly object _lock = new(); private long _nonceCounter; + private bool _disposed; public void WriteNextNonce(ICryptoRandom random, Span nonce) { @@ -21,4 +26,42 @@ public void WriteNextNonce(ICryptoRandom random, Span nonce) BinaryPrimitives.WriteUInt32BigEndian(nonce, unchecked((uint)Interlocked.Increment(ref _nonceCounter))); random.GenerateRandomBytes(nonce[sizeof(uint)..]); } + + public bool TryCopyReadKey(Span destination) => TryCopyKey(ReadKey, destination); + + public bool TryCopyWriteKey(Span destination) => TryCopyKey(WriteKey, destination); + + public void Dispose() + { + lock (_lock) + { + if (_disposed) + { + return; + } + + _disposed = true; + CryptographicOperations.ZeroMemory(ReadKey); + CryptographicOperations.ZeroMemory(WriteKey); + } + } + + private bool TryCopyKey(byte[] key, Span destination) + { + if (destination.Length != KeySize) + { + throw new ArgumentException($"Key destination must be {KeySize} bytes.", nameof(destination)); + } + + lock (_lock) + { + if (_disposed) + { + return false; + } + + key.CopyTo(destination); + return true; + } + } } From 274df2d996f6e390d3b09208fb31b1c7f2133166 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 16:24:17 +0300 Subject: [PATCH 141/182] Tidy Kademlia tests --- .../Handlers/NeighbourMsgHandlerTests.cs | 25 +++++---- .../Kademlia/KBucketTests.cs | 53 ++++++++----------- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs index 5a98d94085cb..0ef611cfe384 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/Handlers/NeighbourMsgHandlerTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Nethermind.Core.Crypto; @@ -30,13 +31,17 @@ public void Setup() _expirationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60; // 60 seconds in the future } - [Test] - public async Task When_TotalNodesLessThanK_ThenDontFinish_UntilTimeout() + [TestCaseSource(nameof(TimeoutCases))] + public async Task When_TotalNodesDoNotCompleteImmediately_ThenCompleteAfterTimeout(int nodeCount, bool[] expectedHandleResults) { - ArraySegment nodes = CreateNodes(5); + ArraySegment nodes = CreateNodes(nodeCount); NeighborsMsg msg = new(_farAddress, _expirationTime, nodes); - Assert.That(_handler.Handle(msg), Is.True); + for (int i = 0; i < expectedHandleResults.Length; i++) + { + Assert.That(_handler.Handle(msg), Is.EqualTo(expectedHandleResults[i])); + } + Assert.That(_handler.TaskCompletionSource.Task.IsCompleted, Is.False); await _handler.TaskCompletionSource.Task; @@ -54,16 +59,10 @@ public void When_TotalNodesLessEqualToK_ThenFinishImmediately() Assert.That(_handler.TaskCompletionSource.Task.IsCompleted, Is.True); } - [Test] - public async Task When_TotalNodesDoesNotAddUp_DontTakeMessage() + private static IEnumerable TimeoutCases() { - ArraySegment nodes = CreateNodes(10); - NeighborsMsg msg = new(_farAddress, _expirationTime, nodes); - - Assert.That(_handler.Handle(msg), Is.True); - Assert.That(_handler.Handle(msg), Is.False); - Assert.That(_handler.TaskCompletionSource.Task.IsCompleted, Is.False); - await _handler.TaskCompletionSource.Task; + yield return new TestCaseData(5, new[] { true }).SetName("FewerThanK"); + yield return new TestCaseData(10, new[] { true, false }).SetName("SecondMessageWouldOverflowK"); } private ArraySegment CreateNodes(int count, int startIndex = 0) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index 919b045c37a6..293165146ee8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -13,20 +13,10 @@ public class KBucketTests [Test] public void TryAddOrRefresh_ShouldLimitToK() { - KBucket bucket = new(5); - - ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); - - foreach (ValueHash256 valueHash256 in toAdd) - { - bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); - } + (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); // Again - foreach (ValueHash256 valueHash256 in toAdd) - { - bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); - } + AddNodes(bucket, toAdd); Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(toAdd[..5].ToHashSet())); Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (ToHash(it), it)).ToHashSet())); @@ -41,21 +31,11 @@ public void TryAddOrRefresh_ShouldLimitToK() [Test] public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() { - KBucket bucket = new(5); - - ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); - - foreach (ValueHash256 valueHash256 in toAdd) - { - bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); - } + (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); ValueHash256[] nodes = bucket.GetAll(); - foreach (ValueHash256 valueHash256 in toAdd) - { - bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); - } + AddNodes(bucket, toAdd); Assert.That(bucket.GetAll(), Is.SameAs(nodes)); } @@ -77,14 +57,7 @@ public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNe [Test] public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() { - KBucket bucket = new(5); - - ValueHash256[] toAdd = Enumerable.Range(0, 10).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); - - foreach (ValueHash256 valueHash256 in toAdd) - { - bucket.TryAddOrRefresh(ToHash(valueHash256), valueHash256, out _); - } + (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); bucket.RemoveAndReplace(ToHash(toAdd[0])); @@ -94,4 +67,20 @@ public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() } private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); + + private static (KBucket Bucket, ValueHash256[] Nodes) BuildFullBucket() + { + KBucket bucket = new(5); + ValueHash256[] nodes = Enumerable.Range(0, 10).Select(static k => ValueKeccak.Compute(k.ToString())).ToArray(); + AddNodes(bucket, nodes); + return (bucket, nodes); + } + + private static void AddNodes(KBucket bucket, ValueHash256[] nodes) + { + foreach (ValueHash256 node in nodes) + { + bucket.TryAddOrRefresh(ToHash(node), node, out _); + } + } } From ecac6ad0dc28892b3345bb4b12ade2eb808b270f Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 16:27:05 +0300 Subject: [PATCH 142/182] Add async node health disposal --- .../Nethermind.Kademlia/NodeHealthTracker.cs | 80 ++++++++++++++----- .../Kademlia/NodeHealthTrackerTests.cs | 14 +++- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index c06fa1fcc06d..53a5601fcbc6 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -15,7 +15,7 @@ public class NodeHealthTracker( INodeHashProvider nodeHashProvider, IKademliaMessageSender kademliaMessageSender, ILogManager? logManager = null -) : INodeHealthTracker, IDisposable +) : INodeHealthTracker, IDisposable, IAsyncDisposable where TNode : notnull where TKadKey : notnull { @@ -139,10 +139,65 @@ public void OnRequestFailed(TNode node) } public void Dispose() + { + Task[] refreshTasks = CancelAndGetRefreshTasks(); + if (refreshTasks.Length == 0) return; + + bool completed = false; + try + { + completed = Task.WaitAll(refreshTasks, _refreshPingTimeout + TimeSpan.FromMilliseconds(500)); + } + catch (AggregateException e) + { + completed = true; + if (!HasOnlyCancellationExceptions(e)) + { + if (_logger.IsDebug) _logger.Debug($"Error while disposing node health tracker: {e}"); + } + } + + if (completed) + { + _refreshCancellation.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + Task[] refreshTasks = CancelAndGetRefreshTasks(); + if (refreshTasks.Length == 0) return; + + bool completed = false; + try + { + await Task.WhenAll(refreshTasks).WaitAsync(_refreshPingTimeout + TimeSpan.FromMilliseconds(500)); + completed = true; + } + catch (TimeoutException) + { + } + catch (OperationCanceledException) + { + completed = true; + } + catch (Exception e) + { + completed = true; + if (_logger.IsDebug) _logger.Debug($"Error while disposing node health tracker: {e}"); + } + + if (completed) + { + _refreshCancellation.Dispose(); + } + } + + private Task[] CancelAndGetRefreshTasks() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { - return; + return []; } _refreshCancellation.Cancel(); @@ -161,7 +216,7 @@ public void Dispose() if (refreshTaskCount == 0) { _refreshCancellation.Dispose(); - return; + return []; } if (refreshTaskCount != refreshTasks.Length) @@ -169,24 +224,7 @@ public void Dispose() Array.Resize(ref refreshTasks, refreshTaskCount); } - bool completed = false; - try - { - completed = Task.WaitAll(refreshTasks, _refreshPingTimeout + TimeSpan.FromMilliseconds(500)); - } - catch (AggregateException e) - { - completed = true; - if (!HasOnlyCancellationExceptions(e)) - { - if (_logger.IsDebug) _logger.Debug($"Error while disposing node health tracker: {e}"); - } - } - - if (completed) - { - _refreshCancellation.Dispose(); - } + return refreshTasks; } private static bool HasOnlyCancellationExceptions(AggregateException e) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index ae9e7e0d07c6..836bcb891329 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -89,9 +89,10 @@ public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken t Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); } - [Test] + [TestCase(false)] + [TestCase(true)] [CancelAfter(10000)] - public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(CancellationToken token) + public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(bool asyncDispose, CancellationToken token) { TaskCompletionSource pingStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); TaskCompletionSource pingCancelled = new(TaskCreationOptions.RunContinuationsAsynchronously); @@ -120,7 +121,14 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(Cancellat tracker.OnIncomingMessageFrom(Remote); await pingStarted.Task.WaitAsync(token); - tracker.Dispose(); + if (asyncDispose) + { + await tracker.DisposeAsync(); + } + else + { + tracker.Dispose(); + } await pingCancelled.Task.WaitAsync(token); Assert.That(routing.RemoveCalls, Does.Not.Contain(ToHash(ValueKeccak.Compute(Stale)))); From 6283f39b4b7546a8c435f836e7531b292fc041e8 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 3 Jun 2026 21:41:22 +0300 Subject: [PATCH 143/182] More unification --- .../Nethermind.Config/NetworkNode.cs | 4 +- .../Modules/TestEnvironmentModule.cs | 1 - .../Modules/DiscoveryModule.cs | 1 - .../DiscoveryV5AppTests.cs | 139 ++---------- .../DiscoveryPersistenceManagerTests.cs | 59 ++++- .../Discv5/CodecTests.cs | 34 ++- .../Discv4/Kademlia/KademliaModule.cs | 2 - .../Discv5/DiscoveryV5App.cs | 203 +++++------------- .../Kademlia/Handlers/NodesResponseHandler.cs | 3 +- .../Discv5/Kademlia/KademliaAdapter.cs | 5 +- .../Discv5/NodeRecordConverter.cs | 55 ----- .../Discv5/Packets/PacketCodec.cs | 66 +++--- .../Kademlia/DiscoveryKademliaModuleBase.cs | 1 + .../DiscoveryPersistenceManager.cs | 39 ++-- .../Nethermind.Network.Enr/NodeRecord.cs | 45 ++++ .../Nethermind.Network.Stats/Model/Node.cs | 31 +++ .../Nethermind.Network.Stats.csproj | 1 + .../NetworkNodeDecoderTests.cs | 43 ++++ .../Nethermind.Network/NetworkNodeDecoder.cs | 58 ++++- .../DatabasePurgerTests.cs | 2 +- .../Nethermind.Runner/DatabasePurger.cs | 3 +- .../Nethermind.Runner/packages.lock.json | 3 +- 22 files changed, 399 insertions(+), 399 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs rename src/Nethermind/Nethermind.Network.Discovery/{Discv4 => Kademlia}/DiscoveryPersistenceManager.cs (79%) diff --git a/src/Nethermind/Nethermind.Config/NetworkNode.cs b/src/Nethermind/Nethermind.Config/NetworkNode.cs index 5a4c654444e3..4ac8a6e68f64 100644 --- a/src/Nethermind/Nethermind.Config/NetworkNode.cs +++ b/src/Nethermind/Nethermind.Config/NetworkNode.cs @@ -89,8 +89,8 @@ public NetworkNode(PublicKey publicKey, string ip, int port, long reputation = 0 public PublicKey NodeId => IsEnode ? Enode.PublicKey : GetEnrPublicKey(); public string Host => IsEnode ? Enode.HostIp.ToString() : HostIp.ToString(); - public IPAddress HostIp => IsEnode ? Enode.HostIp : Enr!.GetObj(EnrContentKey.Ip) ?? IPAddress.None; - public int Port => IsEnode ? Enode.Port : Enr!.GetValue(EnrContentKey.Tcp) ?? 0; + public IPAddress HostIp => IsEnode ? Enode.HostIp : Enr!.DiscoveryIp ?? IPAddress.None; + public int Port => IsEnode ? Enode.Port : Enr!.DiscoveryPort ?? 0; public long Reputation { get; set; } private PublicKey GetEnrPublicKey() diff --git a/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs b/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs index c8de30f052df..f32866867331 100644 --- a/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs +++ b/src/Nethermind/Nethermind.Core.Test/Modules/TestEnvironmentModule.cs @@ -43,7 +43,6 @@ protected override void Load(ContainerBuilder builder) // These two don't use the DB provider .AddKeyedSingleton(DbNames.PeersDb, (_) => new MemDb()) .AddKeyedSingleton(DbNames.DiscoveryNodes, (_) => new MemDb()) - .AddKeyedSingleton(DbNames.DiscoveryV5Nodes, (_) => new MemDb()) .AddSingleton(networkConfig => new LocalChannelFactory(networkGroup ?? nameof(TestEnvironmentModule), networkConfig)) .AddSingleton(NodeFilter.AcceptAll) // Disable inbound rate limiting for in-memory channels diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 44337eb342b1..0157537ab986 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -110,7 +110,6 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddNetworkStorage(DbNames.DiscoveryNodes, DbNames.DiscoveryNodes) - .AddNetworkStorage(DbNames.DiscoveryV5Nodes, DbNames.DiscoveryV5Nodes) ; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 877d12d988ed..e63b636c6f31 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -11,10 +11,11 @@ using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Logging; +using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Enr; -using Nethermind.Serialization.Rlp; +using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -31,18 +32,15 @@ namespace Nethermind.Network.Discovery.Test; public class DiscoveryV5AppTests { private MemDb _discoveryDb = null!; - private MemDb _legacyDiscoveryDb = null!; private DiscoveryV5App _discoveryV5App = null!; private readonly List _containers = []; - [OneTimeSetUp] - public void OneTimeSetup() => Rlp.RegisterDecoder(typeof(NetworkNode), new NetworkNodeDecoder()); - [SetUp] public void Setup() { + NetworkNodeDecoder.Init(); + _discoveryDb = new MemDb(); - _legacyDiscoveryDb = new MemDb(); _discoveryV5App = CreateDiscoveryV5App(IPAddress.Parse("8.8.8.8")); } @@ -63,6 +61,8 @@ private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp, Action(IProtectedPrivateKey.NodeKey); builder.RegisterInstance(ecdsa).As().As(); builder.RegisterInstance(new CryptoRandom()).As(); + builder.RegisterInstance(new NetworkStorage(_discoveryDb, LimboLogs.Instance)).Keyed(DbNames.DiscoveryNodes); + builder.RegisterInstance(Substitute.For()).As(); builder.RegisterType().As().WithAttributeFiltering().SingleInstance(); IContainer container = builder.Build(); _containers.Add(container); @@ -73,8 +73,6 @@ private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp, Action loadedEnrs = _discoveryV5App.LoadStoredEnrs(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(loadedEnrs.Count, Is.EqualTo(2), "Should get all records"); - Assert.That(_legacyDiscoveryDb.Count, Is.EqualTo(0), "Legacy DB should be empty"); - Assert.That(_discoveryDb.Count, Is.EqualTo(2), "DB should contain all items migrated"); - } - } - - [Test] - public void Should_Stop_Migration_From_V4_DB() - { - NetworkNode enode1 = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 1, 1); - _legacyDiscoveryDb[enode1.NodeId.Bytes] = Rlp.Encode(enode1).Bytes; - - NetworkNode enode2 = new(TestItem.PublicKeyB, IPAddress.Loopback.ToString(), 1, 1); - _legacyDiscoveryDb[enode2.NodeId.Bytes] = Rlp.Encode(enode2).Bytes; - - List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(loadedEnrs.Count, Is.EqualTo(0), "Should not load any nodes if legacy DB contains enodes"); - Assert.That(_legacyDiscoveryDb.Count, Is.EqualTo(2), "Legacy DB should not be changed"); - Assert.That(_discoveryDb.Count, Is.EqualTo(0), "DB should not load any records"); - } - } - - [Test] - public void Should_Skip_Malformed_Legacy_Records_And_Migrate_Valid_Ones() - { - NetworkNode enode = new(TestItem.PublicKeyA, IPAddress.Loopback.ToString(), 1, 1); - _legacyDiscoveryDb[enode.NodeId.Bytes] = Rlp.Encode(enode).Bytes; - - PrivateKey validPrivateKey = TestItem.PrivateKeyB; - NodeRecord validEnr = CreateTestEnr(validPrivateKey); - _legacyDiscoveryDb[validPrivateKey.PublicKey.Hash.Bytes] = validEnr.ToRlpBytes(); - - List loadedEnrs = _discoveryV5App.LoadStoredEnrs(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(loadedEnrs, Has.Count.EqualTo(1)); - Assert.That(loadedEnrs[0].EnrString, Is.EqualTo(validEnr.EnrString)); - Assert.That(_legacyDiscoveryDb, Has.Count.EqualTo(1), "Malformed legacy records should remain untouched"); - Assert.That(_discoveryDb, Has.Count.EqualTo(1), "Valid records should still be migrated"); - } - } - [Test] public void Should_Reject_Private_Ip_Enr() { NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Loopback); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.False); Assert.That(node, Is.Null); @@ -220,7 +156,7 @@ public void Should_Accept_Private_Ip_Enr_On_Private_Deployment() DiscoveryV5App privateDiscoveryApp = CreateDiscoveryV5App(IPAddress.Loopback); NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Loopback); - bool result = privateDiscoveryApp.TryGetNodeFromEnr(enr, out Node? node); + bool result = privateDiscoveryApp.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.True); Assert.That(node, Is.Not.Null); @@ -247,7 +183,7 @@ public void Should_Reject_Special_Use_Ip_Enr(string ip) { NodeRecord enr = CreateEnrForAddress(TestItem.PrivateKeyA, IPAddress.Parse(ip)); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.False); Assert.That(node, Is.Null); @@ -260,7 +196,7 @@ public void Should_Reject_Special_Use_Ip_Enr_On_Private_Deployment(string ip) DiscoveryV5App privateDiscoveryApp = CreateDiscoveryV5App(IPAddress.Loopback); NodeRecord enr = CreateEnrForAddress(TestItem.PrivateKeyA, IPAddress.Parse(ip)); - bool result = privateDiscoveryApp.TryGetNodeFromEnr(enr, out Node? node); + bool result = privateDiscoveryApp.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.False); Assert.That(node, Is.Null); @@ -271,7 +207,7 @@ public void Should_Accept_Public_Ip_Enr() { NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.True); Assert.That(node, Is.Not.Null); @@ -357,7 +293,7 @@ public void Should_Use_Udp_Port_From_Enr() { NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), port: 30303, udpPort: 30304); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.True); Assert.That(node, Is.Not.Null); @@ -369,7 +305,7 @@ public void Should_Reject_Tcp_Only_Enr() { NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), includeTcp: true, includeUdp: false); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.False); Assert.That(node, Is.Null); @@ -380,7 +316,7 @@ public void Should_Accept_Ipv6_Enr() { NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.True); Assert.That(node, Is.Not.Null); @@ -393,7 +329,7 @@ public void Should_Accept_Ipv6_Enr_With_Default_Udp_Port() { NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001, useUdp6: false); - bool result = _discoveryV5App.TryGetNodeFromEnr(enr, out Node? node); + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); Assert.That(result, Is.True); Assert.That(node, Is.Not.Null); @@ -423,49 +359,4 @@ public void Should_Use_Udp_Port_From_Configured_Enr_Bootnode() Assert.That(bootNodes[0].Enr, Is.EqualTo(enr.EnrString)); } } - - [Test] - public void TryEnqueueNewEnr_Should_Deduplicate() - { - Queue queue = new(); - HashSet seenNodes = []; - NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); - - Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, enr), Is.True); - Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, enr), Is.False); - Assert.That(queue.Count, Is.EqualTo(1)); - } - - [Test] - public void TryEnqueueNewEnr_Should_Respect_Tracked_Cap() - { - Queue queue = new(); - HashSet seenNodes = []; - for (int i = 0; i < DiscoveryV5App.MaxTrackedEnrsPerWalk; i++) - { - seenNodes.Add(new NodeRecord()); - } - - NodeRecord candidate = CreateTestEnr(TestItem.PrivateKeyB, IPAddress.Parse("1.1.1.1"), port: 30304); - - Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, candidate), Is.False); - Assert.That(queue.Count, Is.EqualTo(0)); - } - - [Test] - public void TryEnqueueNewEnr_Should_Respect_Pending_Cap() - { - Queue queue = new(); - NodeRecord existing = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8")); - for (int i = 0; i < DiscoveryV5App.MaxPendingEnrsPerWalk; i++) - { - queue.Enqueue(existing); - } - - HashSet seenNodes = []; - NodeRecord candidate = CreateTestEnr(TestItem.PrivateKeyB, IPAddress.Parse("1.1.1.1"), port: 30304); - - Assert.That(DiscoveryV5App.TryEnqueueNewEnr(queue, seenNodes, candidate), Is.False); - Assert.That(queue.Count, Is.EqualTo(DiscoveryV5App.MaxPendingEnrsPerWalk)); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs index de1ecbe41a4f..a24f762f529b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs @@ -3,16 +3,19 @@ using System; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; using Nethermind.Db; using Nethermind.Logging; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Kademlia; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Enr; using Nethermind.Stats; using Nethermind.Stats.Model; using NSubstitute; @@ -177,5 +180,59 @@ public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() Assert.That(_discoveryDb.Count, Is.EqualTo(nodes.Length)); } + + [Test] + public async Task RunDiscoveryPersistenceCommit_Should_Preserve_Enr_In_Common_Storage() + { + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), 30303, 30304); + Node node = new(TestItem.PrivateKeyA.PublicKey, "8.8.8.8", 30304) + { + Enr = enr.EnrString + }; + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(10)); + + _kademlia.IterateNodes().Returns([node]); + + _ = _persistenceManager.RunDiscoveryPersistenceCommit(cts.Token); + + while (_discoveryDb.Count == 0) + { + cts.Token.ThrowIfCancellationRequested(); + await Task.Delay(10, cts.Token); + } + + await cts.CancelAsync(); + + NetworkStorage reloadedStorage = new(_discoveryDb, LimboLogs.Instance); + NetworkNode[] persistedNodes = reloadedStorage.GetPersistedNodes(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(persistedNodes, Has.Length.EqualTo(1)); + NetworkNode persistedNode = persistedNodes[0]; + NodeRecord? persistedEnr = persistedNode.Enr; + Assert.That(persistedNode.IsEnr, Is.True); + Assert.That(persistedEnr, Is.Not.Null); + Assert.That(persistedEnr!.EnrString, Is.EqualTo(enr.EnrString)); + Assert.That(persistedNode.NodeId, Is.EqualTo(TestItem.PrivateKeyA.PublicKey)); + Assert.That(persistedNode.Host, Is.EqualTo("8.8.8.8")); + Assert.That(persistedNode.Port, Is.EqualTo(30304)); + } + } + + private static NodeRecord CreateTestEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new TcpEntry(tcpPort)); + enr.SetEntry(new UdpEntry(udpPort)); + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + + return enr; + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 1404d1cb47dc..fa8c89c01106 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -13,6 +13,7 @@ using NUnit.Framework; using System; using System.Net; +using System.Threading.Tasks; namespace Nethermind.Network.Discovery.Test.Discv5; @@ -64,10 +65,7 @@ public void IdNonceSignature_Matches_Devp2p_Vector() [Test] public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() { - byte[] packetBytes = Bytes.FromHexString( - "0x00000000000000000000000000000000088b3d4342774649325f313964a39e55" + - "ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + - "4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); + byte[] packetBytes = CreateDevp2pPingPacketBytes(); bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); using (packet) @@ -86,6 +84,24 @@ public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() } } + [Test] + public void PacketCodec_Decodes_Packets_Concurrently() + { + byte[] packetBytes = CreateDevp2pPingPacketBytes(); + using PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + + Parallel.For(0, 128, (int _) => + { + bool decoded = codec.TryDecode(packetBytes, out Packet packet); + using (packet) + { + Assert.That(decoded, Is.True); + Assert.That(packet.Flag, Is.EqualTo(PacketFlag.Ordinary)); + Assert.That(packet.AuthData.ToArray(), Is.EqualTo(NodeAId)); + } + }); + } + [Test] public void PacketCodec_Decodes_WhoAreYou_GoEthereum_Vector() { @@ -97,7 +113,7 @@ public void PacketCodec_Decodes_WhoAreYou_GoEthereum_Vector() bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); using (packet) { - PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + using PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); Challenge challenge = codec.DecodeWhoAreYou(packet); Assert.That(decoded, Is.True); @@ -150,7 +166,7 @@ public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( Bytes.FromHexString("0x0102030405060708090a0b0c0d0e0f10"), challengeEnrSequence, Bytes.FromHexString(challengeDataHex)); - PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); + using PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); NodeRecord? knownRecord = includesRecord ? null : CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); @@ -290,6 +306,12 @@ private static PacketCodec CreateCodec(PrivateKey privateKey) new CryptoRandom(), new EthereumEcdsa(0)); + private static byte[] CreateDevp2pPingPacketBytes() + => Bytes.FromHexString( + "0x00000000000000000000000000000000088b3d4342774649325f313964a39e55" + + "ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + + "4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); + private static NodeRecord CreateNodeRecord(PrivateKey privateKey) { NodeRecord nodeRecord = new(); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index 89a7b17a7452..6e81bb26971f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -20,8 +20,6 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder - .AddSingleton() - // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. .AddSingleton() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 8a2b361bffa6..28fe16ae0ab0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -12,12 +12,11 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Crypto; -using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Config; -using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; using Discv5KademliaModule = Nethermind.Network.Discovery.Discv5.Kademlia.KademliaModule; @@ -28,15 +27,11 @@ namespace Nethermind.Network.Discovery.Discv5; public sealed class DiscoveryV5App : KademliaDiscoveryApp { - internal const int MaxPendingEnrsPerWalk = 4_096; - internal const int MaxTrackedEnrsPerWalk = MaxPendingEnrsPerWalk * 2; - - private readonly IDb _discoveryDb; - private readonly IDb _legacyDiscoveryDb; private readonly bool _allowNonRoutableEnrs; - private readonly IKademliaAdapter _adapter; + private readonly DiscoveryPersistenceManager _persistenceManager; + private readonly IKademliaAdapter _discv5Adapter; private readonly Func _discoveryHandlerFactory; - private readonly ILifetimeScope _services; + private readonly ILifetimeScope _discv5Services; private NettyDiscoveryV5Handler? _discoveryHandler; @@ -46,40 +41,41 @@ public DiscoveryV5App( IIPResolver ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, - [KeyFilter(DbNames.DiscoveryV5Nodes)] IDb discoveryDb, - [KeyFilter(DbNames.DiscoveryNodes)] IDb legacyDiscoveryDb, IProcessExitSource processExitSource, ILogManager logManager, - Action? configureServices = null) + Action? configureDiscv5Services = null) : base("discv5", networkConfig, processExitSource, logManager.GetClassLogger()) { - _discoveryDb = discoveryDb; - _legacyDiscoveryDb = legacyDiscoveryDb; _allowNonRoutableEnrs = ShouldAcceptNonRoutableEnrs(ipResolver.ExternalIp); List bootNodes = CreateBootNodes(networkConfig, discoveryConfig); ITimestamper timestamper = rootScope.ResolveOptional() ?? Timestamper.Default; - _services = rootScope.BeginLifetimeScope(builder => + _discv5Services = rootScope.BeginLifetimeScope(builder => { builder.RegisterInstance(discoveryConfig).As(); builder.RegisterInstance(timestamper).As(); builder .AddModule(new Discv5KademliaModule(nodeKey.PublicKey, bootNodes)) - .AddSingleton(); + .AddSingleton(); - configureServices?.Invoke(builder); + configureDiscv5Services?.Invoke(builder); }); - Services services = _services.Resolve(); - _adapter = services.Adapter; + DiscV5Services services = _discv5Services.Resolve(); + _persistenceManager = services.PersistenceManager; + _discv5Adapter = services.Discv5Adapter; _discoveryHandlerFactory = services.NettyDiscoveryHandlerFactory; UseKademliaServices(services.NodeSource, services.Kademlia); } - private record Services( + /// + /// Just a small class to make resolve easier + /// + private record DiscV5Services( IKademliaNodeSource NodeSource, - IKademliaAdapter Adapter, + DiscoveryPersistenceManager PersistenceManager, + IKademliaAdapter Discv5Adapter, IKademlia Kademlia, Func NettyDiscoveryHandlerFactory ) @@ -92,7 +88,6 @@ internal List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConf HashSet seen = []; BootNodeStats configuredStats = new(); BootNodeStats defaultStats = new(); - BootNodeStats storedStats = new(); NetworkNode[] configuredBootnodes = networkConfig.Bootnodes; for (int i = 0; i < configuredBootnodes.Length; i++) @@ -109,15 +104,9 @@ internal List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConf } } - List storedEnrs = LoadStoredEnrs(); - for (int i = 0; i < storedEnrs.Count; i++) - { - storedStats.Record(AddBootNode(bootNodes, seen, storedEnrs[i])); - } - if (Logger.IsInfo) { - Logger.Info($"Discv5 bootnodes accepted: {bootNodes.Count} ({configuredStats.Added}/{configuredStats.Total} configured, {defaultStats.Added}/{defaultStats.Total} default, {storedStats.Added}/{storedStats.Total} stored, duplicates: {configuredStats.Duplicates + defaultStats.Duplicates + storedStats.Duplicates}, skipped: {configuredStats.Skipped + defaultStats.Skipped + storedStats.Skipped}, use default discv5 bootnodes: {discoveryConfig.UseDefaultDiscv5Bootnodes})."); + Logger.Info($"Discv5 bootnodes accepted: {bootNodes.Count} ({configuredStats.Added}/{configuredStats.Total} configured, {defaultStats.Added}/{defaultStats.Total} default, duplicates: {configuredStats.Duplicates + defaultStats.Duplicates}, skipped: {configuredStats.Skipped + defaultStats.Skipped}, use default discv5 bootnodes: {discoveryConfig.UseDefaultDiscv5Bootnodes})."); } if (bootNodes.Count == 0 && Logger.IsWarn) @@ -138,7 +127,7 @@ public override void AddNodeToDiscovery(Node node) try { NodeRecord record = NodeRecord.FromEnrString(node.Enr); - if (!TryGetNodeFromEnr(record, out Node? enrNode)) + if (!TryGetAcceptableNodeFromEnr(record, out Node? enrNode)) { return; } @@ -170,7 +159,7 @@ private BootNodeAddResult AddBootNode(List bootNodes, HashSet see private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, NodeRecord nodeRecord) { - if (TryGetNodeFromEnr(nodeRecord, out Node? node)) + if (TryGetAcceptableNodeFromEnr(nodeRecord, out Node? node)) { return AddBootNode(bootNodes, seen, node); } @@ -195,20 +184,19 @@ private BootNodeAddResult AddBootNode(List bootNodes, HashSet see private static string[] GetDefaultBootnodes() => JsonSerializer.Deserialize(typeof(DiscoveryV5App).Assembly.GetManifestResourceStream("Nethermind.Network.Discovery.Discv5.discv5-bootnodes.json")!) ?? []; - internal bool TryGetNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? node) + internal bool TryGetAcceptableNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? node) { - if (NodeRecordConverter.TryGetNodeFromEnr(enr, _allowNonRoutableEnrs, out node)) + if (Node.TryFromEnr(enr, out Node? enrNode) && IsDiscoveryAddressAcceptable(enrNode.Address.Address, _allowNonRoutableEnrs)) { + node = enrNode; return true; } + node = null; if (Logger.IsTrace) Logger.Trace("Enr declined, unable to extract a usable discv5 node endpoint."); return false; } - private static PublicKey? GetPublicKeyFromEnr(NodeRecord enr) => - enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); - internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allowNonRoutable) { if (IPAddress.Any.Equals(ipAddress) || IPAddress.IPv6Any.Equals(ipAddress) || IPAddress.Broadcast.Equals(ipAddress)) @@ -237,89 +225,6 @@ private static bool ShouldAcceptNonRoutableEnrs(IPAddress externalIp) && !IPAddress.None.Equals(externalIp) && IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(externalIp); - internal static bool TryEnqueueNewEnr(Queue nodesToCheck, HashSet seenNodes, NodeRecord enr) - { - if (seenNodes.Count >= MaxTrackedEnrsPerWalk || nodesToCheck.Count >= MaxPendingEnrsPerWalk || !seenNodes.Add(enr)) - { - return false; - } - - nodesToCheck.Enqueue(enr); - return true; - } - - internal List LoadStoredEnrs() - { - List enrs = []; - foreach (byte[] enrBytes in _discoveryDb.GetAllValues()) - { - if (TryLoadStoredEnr(enrBytes, out NodeRecord? enr)) - { - enrs.Add(enr); - } - } - - if (enrs.Count is not 0) - { - return enrs; - } - - IWriteBatch? migrateBatch = null; - IWriteBatch? deleteBatch = null; - - try - { - foreach (KeyValuePair kv in _legacyDiscoveryDb.GetAll()) - { - if (kv.Value is null) - { - continue; - } - - if (!TryLoadStoredEnr(kv.Value, out NodeRecord? enr)) - { - continue; - } - - PublicKey? publicKey = GetPublicKeyFromEnr(enr); - if (publicKey is null) - { - deleteBatch ??= _legacyDiscoveryDb.StartWriteBatch(); - deleteBatch[kv.Key] = null; - continue; - } - - migrateBatch ??= _discoveryDb.StartWriteBatch(); - deleteBatch ??= _legacyDiscoveryDb.StartWriteBatch(); - enrs.Add(enr); - migrateBatch[publicKey.Hash.Bytes] = kv.Value; - deleteBatch[kv.Key] = null; - } - } - finally - { - migrateBatch?.Dispose(); - deleteBatch?.Dispose(); - } - - return enrs; - } - - private bool TryLoadStoredEnr(byte[] enrBytes, [NotNullWhen(true)] out NodeRecord? enr) - { - try - { - enr = NodeRecord.FromBytes(enrBytes); - return true; - } - catch (Exception e) - { - enr = null; - if (Logger.IsDebug) Logger.Debug($"Skipping stored discv5 ENR that cannot be decoded: {e}"); - return false; - } - } - public override void InitializeChannel(IChannel channel) { _discoveryHandler = _discoveryHandlerFactory(); @@ -343,53 +248,41 @@ protected override void DetachEventHandlers() } } - protected override Task RunDiscoveryAsync(CancellationToken cancellationToken) => - Task.WhenAll(_adapter.RunAsync(cancellationToken), Kademlia.Run(cancellationToken)); - - protected override async Task StopAsyncCore() - { - PersistKnownEnrs(); - - await _adapter.DisposeAsync(); - _discoveryHandler?.Close(); - } - - private void PersistKnownEnrs() + protected override async Task RunDiscoveryAsync(CancellationToken cancellationToken) { - _discoveryDb.Clear(); + Task adapterTask = _discv5Adapter.RunAsync(cancellationToken); + Task? persistenceTask = null; - IWriteBatch? batch = null; try { - foreach (Node node in Kademlia.IterateNodes()) - { - if (string.IsNullOrEmpty(node.Enr)) - { - continue; - } - - NodeRecord enr; - try - { - enr = NodeRecord.FromEnrString(node.Enr); - } - catch (Exception e) - { - if (Logger.IsDebug) Logger.Debug($"Skipping malformed discv5 ENR while persisting {node}: {e}"); - continue; - } + await _persistenceManager.LoadPersistedNodes(cancellationToken); - batch ??= _discoveryDb.StartWriteBatch(); - batch[node.IdHash.Bytes] = enr.ToRlpBytes(); - } + persistenceTask = _persistenceManager.RunDiscoveryPersistenceCommit(cancellationToken); + await Kademlia.Run(cancellationToken); } finally { - batch?.Dispose(); + try + { + if (persistenceTask is not null) + { + await persistenceTask; + } + } + finally + { + await adapterTask; + } } } - protected override ValueTask DisposeAsyncCore() => _services.DisposeAsync(); + protected override async Task StopAsyncCore() + { + await _discv5Adapter.DisposeAsync(); + _discoveryHandler?.Close(); + } + + protected override ValueTask DisposeAsyncCore() => _discv5Services.DisposeAsync(); private enum BootNodeAddResult { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 79730f0b387a..5ac118ca941d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -49,7 +49,8 @@ public override bool Handle(NodesMsg nodes) for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; - if (!NodeRecordConverter.TryGetNodeFromEnr(record, _allowNonRoutableRelays, out Node? node) || + if (!Node.TryFromEnr(record, out Node? node) || + !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || !_seenNodeIds.Add(node.Id.Hash) || !MatchesRequestedDistance(node, requestedDistances)) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 7e68e0a77103..27f01a1c72d9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -699,8 +699,9 @@ private void SetKnownRecord(Hash256 nodeId, NodeRecord record) => _knownRecords.Set(nodeId, record); internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable) - => NodeRecordConverter.TryGetNodeFromEnr(record, allowNonRoutable, out Node? node) && - node.Id.Hash.Equals(expectedNodeId); + => Node.TryFromEnr(record, out Node? node) && + node.Id.Hash.Equals(expectedNodeId) && + DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, allowNonRoutable); internal static bool HasExpectedNodeId(NodeRecord record, Hash256 expectedNodeId) => record.GetObj(EnrContentKey.SecP256k1)?.Decompress().Hash.Equals(expectedNodeId) == true; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs deleted file mode 100644 index 101766f30f4c..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NodeRecordConverter.cs +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Diagnostics.CodeAnalysis; -using System.Net; -using Nethermind.Core.Crypto; -using Nethermind.Network.Enr; -using Nethermind.Stats.Model; - -namespace Nethermind.Network.Discovery.Discv5; - -internal static class NodeRecordConverter -{ - public static bool TryGetNodeFromEnr(NodeRecord enr, bool allowNonRoutable, [NotNullWhen(true)] out Node? node) - { - node = null; - - PublicKey? key = enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); - (IPAddress? ip, int? discoveryPort) = GetDiscoveryEndpoint(enr); - if (key is null || ip is null || discoveryPort is null) - { - return false; - } - - if (!DiscoveryV5App.IsDiscoveryAddressAcceptable(ip, allowNonRoutable)) - { - return false; - } - - if ((uint)discoveryPort.Value > ushort.MaxValue || discoveryPort.Value == 0) - { - return false; - } - - node = new Node(key, ip.ToString(), discoveryPort.Value) - { - Enr = enr.EnrString - }; - return true; - } - - internal static (IPAddress? Ip, int? Port) GetDiscoveryEndpoint(NodeRecord enr) - { - IPAddress? ip = enr.GetObj(EnrContentKey.Ip); - int? udp = enr.GetValue(EnrContentKey.Udp); - if (ip is not null && udp is not null) - { - return (ip, udp); - } - - IPAddress? ip6 = enr.GetObj(EnrContentKey.Ip6); - int? udp6 = enr.GetValue(EnrContentKey.Udp6) ?? udp; - return ip6 is not null && udp6 is not null ? (ip6, udp6) : (null, null); - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 2a6954ee653d..40743f242529 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using Autofac.Features.AttributeFilters; +using Microsoft.Extensions.ObjectPool; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Crypto; @@ -34,6 +35,7 @@ public sealed class PacketCodec( private const int EphemeralPublicKeySize = 33; private const int HandshakeAuthDataHeadSize = NodeIdSize + 2; private const int MaxStackPacketBufferSize = 512; + private const int MaxRetainedDecodeMaskingAes = 32; private static ReadOnlySpan ProtocolId => "discv5"u8; private static ReadOnlySpan KeyAgreementInfoPrefix => "discovery v5 key agreement"u8; @@ -45,12 +47,11 @@ public sealed class PacketCodec( private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; private readonly ICryptoRandom _cryptoRandom = cryptoRandom; private readonly IEcdsa _ecdsa = ecdsa; - private readonly Aes _decodeMaskingAes = CreateMaskingAes(nodeKey.PublicKey.Hash.Bytes[..AesKeySize]); - private readonly object _decodeMaskingAesLock = new(); + private readonly ObjectPool _decodeMaskingAesPool = CreateDecodeMaskingAesPool(nodeKey.PublicKey.Hash.Bytes[..AesKeySize]); public void Dispose() { - _decodeMaskingAes.Dispose(); + (_decodeMaskingAesPool as IDisposable)?.Dispose(); _privateKey.Dispose(); } @@ -116,10 +117,20 @@ internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Disc } internal bool TryDecode(byte[] packet, out Packet decoded) - => TryDecode(packet.AsMemory(), _decodeMaskingAes, _decodeMaskingAesLock, out decoded); + => TryDecode(packet.AsMemory(), out decoded); internal bool TryDecode(ReadOnlyMemory packet, out Packet decoded) - => TryDecode(packet, _decodeMaskingAes, _decodeMaskingAesLock, out decoded); + { + Aes localNodeMaskingAes = _decodeMaskingAesPool.Get(); + try + { + return TryDecode(packet, localNodeMaskingAes, out decoded); + } + finally + { + _decodeMaskingAesPool.Return(localNodeMaskingAes); + } + } internal static bool TryDecode(byte[] packet, ReadOnlySpan localNodeId, out Packet decoded) => TryDecode(packet.AsMemory(), localNodeId, out decoded); @@ -127,10 +138,10 @@ internal static bool TryDecode(byte[] packet, ReadOnlySpan localNodeId, ou internal static bool TryDecode(ReadOnlyMemory packetMemory, ReadOnlySpan localNodeId, out Packet decoded) { using Aes localNodeMaskingAes = CreateMaskingAes(localNodeId[..AesKeySize]); - return TryDecode(packetMemory, localNodeMaskingAes, null, out decoded); + return TryDecode(packetMemory, localNodeMaskingAes, out decoded); } - private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMaskingAes, object? aesLock, out Packet decoded) + private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMaskingAes, out Packet decoded) { decoded = default; ReadOnlySpan packet = packetMemory.Span; @@ -141,7 +152,7 @@ private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMa ReadOnlySpan maskingIv = packet[..MaskingIvSize]; Span staticHeader = stackalloc byte[StaticHeaderSize]; - AesCtrTransform(localNodeMaskingAes, aesLock, maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize), staticHeader); + AesCtrTransform(localNodeMaskingAes, maskingIv, packet.Slice(MaskingIvSize, StaticHeaderSize), staticHeader); ReadOnlySpan protocolId = ProtocolId; if (!staticHeader[..protocolId.Length].SequenceEqual(protocolId)) { @@ -163,7 +174,7 @@ private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMa try { - AesCtrTransform(localNodeMaskingAes, aesLock, maskingIv, packet.Slice(MaskingIvSize, headerSize), header); + AesCtrTransform(localNodeMaskingAes, maskingIv, packet.Slice(MaskingIvSize, headerSize), header); if (!header[..protocolId.Length].SequenceEqual(protocolId)) { SafeArrayPool.Shared.Return(messageAdBuffer); @@ -444,21 +455,7 @@ private static void EncryptMessage( private static void AesCtrTransform(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan input, Span output) { using Aes aes = CreateMaskingAes(key); - AesCtrTransform(aes, null, iv, input, output); - } - - private static void AesCtrTransform(Aes aes, object? aesLock, ReadOnlySpan iv, ReadOnlySpan input, Span output) - { - if (aesLock is null) - { - AesCtrTransform(aes, iv, input, output); - return; - } - - lock (aesLock) - { - AesCtrTransform(aes, iv, input, output); - } + AesCtrTransform(aes, iv, input, output); } private static void AesCtrTransform(Aes aes, ReadOnlySpan iv, ReadOnlySpan input, Span output) @@ -497,6 +494,27 @@ private static Aes CreateMaskingAes(ReadOnlySpan key) return aes; } + private static ObjectPool CreateDecodeMaskingAesPool(ReadOnlySpan key) + { + DefaultObjectPoolProvider provider = new() + { + MaximumRetained = Math.Min(Environment.ProcessorCount, MaxRetainedDecodeMaskingAes) + }; + + return provider.Create(new DecodeMaskingAesPolicy(key)); + } + + private sealed class DecodeMaskingAesPolicy : IPooledObjectPolicy + { + private readonly byte[] _key; + + public DecodeMaskingAesPolicy(ReadOnlySpan key) => _key = key.ToArray(); + + public Aes Create() => CreateMaskingAes(_key); + + public bool Return(Aes obj) => true; + } + private static void IncrementCounter(Span counter) { for (int i = counter.Length - 1; i >= 0; i--) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs index 6e7c4b9b2bd8..4b65d0f203df 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs @@ -19,6 +19,7 @@ protected override void Load(ContainerBuilder builder) .AddModule(new KademliaModule()) .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() + .AddSingleton() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs similarity index 79% rename from src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs rename to src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs index 85a3f19ba58e..153f3844c375 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs @@ -7,11 +7,10 @@ using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Stats; using Nethermind.Stats.Model; -namespace Nethermind.Network.Discovery.Discv4; +namespace Nethermind.Network.Discovery.Kademlia; /// /// Manages persistence operations for the discovery process, including loading nodes from storage @@ -22,21 +21,22 @@ namespace Nethermind.Network.Discovery.Discv4; /// /// The network storage for persisting discovery nodes. /// Manager for node statistics. -/// Adapter for Discv4 protocol communication. +/// Protocol-specific Kademlia message sender. +/// Kademlia table whose live nodes should be persisted. /// Configuration for the discovery process. /// Log manager for logging events. /// Thrown if any required parameter is null. public class DiscoveryPersistenceManager( [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage discoveryStorage, INodeStatsManager nodeStatsManager, - IKademliaAdapter discv4Adapter, + IKademliaMessageSender messageSender, IKademlia kademlia, IDiscoveryConfig discoveryConfig, ILogManager logManager) { private readonly INetworkStorage _discoveryStorage = discoveryStorage; private readonly INodeStatsManager _nodeStatsManager = nodeStatsManager; - private readonly IKademliaAdapter _discv4Adapter = discv4Adapter; + private readonly IKademliaMessageSender _messageSender = messageSender; private readonly IKademlia _kademlia = kademlia; private readonly ILogger _logger = logManager.GetClassLogger(); private readonly int _persistenceInterval = discoveryConfig.DiscoveryPersistenceInterval; @@ -56,11 +56,11 @@ public async Task LoadPersistedNodes(CancellationToken cancellationToken) Node node; try { - node = new Node(networkNode.NodeId, networkNode.Host, networkNode.Port); + node = new Node(networkNode); } catch (Exception e) { - _logger.DebugError($"Peer could not be loaded for {networkNode.NodeId}@{networkNode.Host}:{networkNode.Port}. {e}"); + _logger.DebugError($"Peer could not be loaded for persisted node {networkNode}. {e}"); continue; } @@ -69,7 +69,7 @@ public async Task LoadPersistedNodes(CancellationToken cancellationToken) { // Reputation must be set before Ping so the routing table has the correct reputation when the Pong is received. _nodeStatsManager.GetOrAdd(node).CurrentPersistedNodeReputation = networkNode.Reputation; - if (!await _discv4Adapter.Ping(node, cancellationToken)) + if (!await _messageSender.Ping(node, cancellationToken)) { continue; } @@ -107,12 +107,15 @@ public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationTo { try { - _discoveryStorage.StartBatch(); - - _discoveryStorage.UpdateNodes(_kademlia - .IterateNodes() - .Select(x => new NetworkNode(x.Id, x.Host, x.Port, _nodeStatsManager.GetNewPersistedReputation(x)))); + List nodes = []; + foreach (Node node in _kademlia.IterateNodes()) + { + long reputation = _nodeStatsManager.GetNewPersistedReputation(node); + nodes.Add(CreatePersistedNode(node, reputation)); + } + _discoveryStorage.StartBatch(); + _discoveryStorage.UpdateNodes(nodes); _discoveryStorage.Commit(); } catch (Exception ex) @@ -121,4 +124,14 @@ public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationTo } } } + + private static NetworkNode CreatePersistedNode(Node node, long reputation) + { + if (!string.IsNullOrEmpty(node.Enr)) + { + return new NetworkNode(node.Enr) { Reputation = reputation }; + } + + return new NetworkNode(node.Id, node.Host, node.Port, reputation); + } } diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index ae5c6da48be6..3275834e3f2f 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -5,6 +5,7 @@ using Nethermind.Core.Extensions; using Nethermind.Crypto; using Nethermind.Serialization.Rlp; +using System.Net; using Convert = System.Convert; namespace Nethermind.Network.Enr; @@ -110,6 +111,50 @@ public Signature? Signature public NodeRecord() => SetEntry(IdEntry.Instance); + /// + /// Gets the IP address advertised for discovery traffic. + /// + /// + /// IPv4 is preferred when both ip and udp are present. Otherwise IPv6 is returned when ip6 + /// is present. + /// + public IPAddress? DiscoveryIp + { + get + { + IPAddress? ip = GetObj(EnrContentKey.Ip); + if (ip is not null && GetValue(EnrContentKey.Udp) is not null) + { + return ip; + } + + return GetObj(EnrContentKey.Ip6); + } + } + + /// + /// Gets the UDP port advertised for discovery traffic. + /// + /// + /// For IPv6, udp6 is preferred and udp is used as the EIP-778 fallback. + /// + public int? DiscoveryPort + { + get + { + IPAddress? ip = GetObj(EnrContentKey.Ip); + int? udp = GetValue(EnrContentKey.Udp); + if (ip is not null && udp is not null) + { + return udp; + } + + return GetObj(EnrContentKey.Ip6) is not null + ? GetValue(EnrContentKey.Udp6) ?? udp + : null; + } + } + public static NodeRecord FromEnrString(string enrString) { const string prefix = "enr:"; diff --git a/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs b/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs index 55d91caf798c..4751786aa9ca 100644 --- a/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs +++ b/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Text.RegularExpressions; using FastEnumUtility; using Nethermind.Config; using Nethermind.Core.Crypto; +using Nethermind.Network.Enr; namespace Nethermind.Stats.Model { @@ -81,6 +83,35 @@ public string ClientId public Node(NetworkNode networkNode, bool isStatic = false) : this(networkNode.NodeId, networkNode.Host, networkNode.Port, isStatic) { + if (networkNode.IsEnr) + { + Enr = networkNode.Enr.EnrString; + } + } + + /// + /// Tries to create a node from an Ethereum Node Record with a secp256k1 key and discovery endpoint. + /// + /// The Ethereum Node Record to read. + /// The node created from the record when the record contains a usable discovery endpoint. + /// when a node could be created; otherwise . + public static bool TryFromEnr(NodeRecord enr, [MaybeNullWhen(false)] out Node node) + { + node = null; + + PublicKey key = enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); + IPAddress ip = enr.DiscoveryIp; + int? discoveryPort = enr.DiscoveryPort; + if (key is null || ip is null || discoveryPort is null || discoveryPort.Value == 0 || (uint)discoveryPort.Value > ushort.MaxValue) + { + return false; + } + + node = new Node(key, new IPEndPoint(ip, discoveryPort.Value)) + { + Enr = enr.EnrString + }; + return true; } public Node(PublicKey id, string host, int port, bool isStatic = false) diff --git a/src/Nethermind/Nethermind.Network.Stats/Nethermind.Network.Stats.csproj b/src/Nethermind/Nethermind.Network.Stats/Nethermind.Network.Stats.csproj index 2f0d60944a8e..b77c105e8985 100644 --- a/src/Nethermind/Nethermind.Network.Stats/Nethermind.Network.Stats.csproj +++ b/src/Nethermind/Nethermind.Network.Stats/Nethermind.Network.Stats.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs index c58e4928bb5e..f1475cb0a32f 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs @@ -5,8 +5,11 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Network.Enr; using Nethermind.Serialization.Rlp; using NUnit.Framework; +using System.Net; namespace Nethermind.Network.Test { @@ -69,5 +72,45 @@ public void Negative_port_just_in_case_for_resilience() Assert.That(decoded.Reputation, Is.EqualTo(node.Reputation)); } + [Test] + public void Can_do_enr_roundtrip() + { + NetworkNodeDecoder networkNodeDecoder = new(); + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), 30303, 30304); + NetworkNode node = new(enr.EnrString) + { + Reputation = 100L + }; + + Rlp encoded = Rlp.Encode(node); + Rlp.ValueDecoderContext context = encoded.Bytes.AsRlpValueContext(); + NetworkNode decoded = networkNodeDecoder.Decode(ref context); + + using (Assert.EnterMultipleScope()) + { + NodeRecord? decodedEnr = decoded.Enr; + Assert.That(decoded.IsEnr, Is.True); + Assert.That(decodedEnr, Is.Not.Null); + Assert.That(decodedEnr!.EnrString, Is.EqualTo(enr.EnrString)); + Assert.That(decoded.NodeId, Is.EqualTo(node.NodeId)); + Assert.That(decoded.Host, Is.EqualTo("8.8.8.8")); + Assert.That(decoded.Port, Is.EqualTo(30304)); + Assert.That(decoded.Reputation, Is.EqualTo(node.Reputation)); + } + } + + private static NodeRecord CreateTestEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new TcpEntry(tcpPort)); + enr.SetEntry(new UdpEntry(udpPort)); + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + + return enr; + } } } diff --git a/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs b/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs index d37f5f474956..ad4c991f1169 100644 --- a/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs +++ b/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; +using System.Text; using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; @@ -16,9 +18,30 @@ public sealed class NetworkNodeDecoder : RlpDecoder protected override NetworkNode DecodeInternal(ref Rlp.ValueDecoderContext decoderContext, RlpBehaviors rlpBehaviors = RlpBehaviors.None) { - decoderContext.ReadSequenceLength(); + int contentEnd = decoderContext.ReadSequenceLength() + decoderContext.Position; + ReadOnlySpan firstItem = decoderContext.DecodeByteArraySpan(RlpLimit); + if (IsEnrString(firstItem)) + { + return DecodeEnrFormat(ref decoderContext, firstItem, contentEnd); + } + + return DecodeLegacyFormat(ref decoderContext, firstItem); + } + + private static NetworkNode DecodeEnrFormat(ref Rlp.ValueDecoderContext decoderContext, ReadOnlySpan firstItem, int contentEnd) + { + string nodeString = Encoding.UTF8.GetString(firstItem); + long reputation = decoderContext.DecodeLong(); + decoderContext.Check(contentEnd); + return new NetworkNode(nodeString) + { + Reputation = reputation + }; + } - PublicKey publicKey = new(decoderContext.DecodeByteArraySpan(RlpLimit.L64)); + private static NetworkNode DecodeLegacyFormat(ref Rlp.ValueDecoderContext decoderContext, ReadOnlySpan publicKeyBytes) + { + PublicKey publicKey = new(publicKeyBytes); string ip = decoderContext.DecodeString(RlpLimit); int port = (int)decoderContext.DecodeByteArraySpan(RlpLimit.L8).ReadEthUInt64(); decoderContext.SkipItem(); @@ -40,6 +63,20 @@ public override void Encode(RlpStream stream, NetworkNode item, RlpBehaviors rlp { int contentLength = GetContentLength(item, rlpBehaviors); stream.StartSequence(contentLength); + if (!item.IsEnr) + { + EncodeLegacyFormat(stream, item); + return; + } + + stream.Encode(item.ToString()); + stream.Encode(item.Reputation); + } + + public override int GetLength(NetworkNode item, RlpBehaviors rlpBehaviors) => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + + private static void EncodeLegacyFormat(RlpStream stream, NetworkNode item) + { stream.Encode(item.NodeId.Bytes); stream.Encode(item.Host); stream.Encode(item.Port); @@ -47,13 +84,18 @@ public override void Encode(RlpStream stream, NetworkNode item, RlpBehaviors rlp stream.Encode(item.Reputation); } - public override int GetLength(NetworkNode item, RlpBehaviors rlpBehaviors) => Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + private static int GetContentLength(NetworkNode item, RlpBehaviors rlpBehaviors) => item.IsEnr + ? Rlp.LengthOf(item.ToString()) + + Rlp.LengthOf(item.Reputation) + : Rlp.LengthOf(item.NodeId.Bytes) + + Rlp.LengthOf(item.Host) + + Rlp.LengthOf(item.Port) + + 1 + + Rlp.LengthOf(item.Reputation); - private static int GetContentLength(NetworkNode item, RlpBehaviors rlpBehaviors) => Rlp.LengthOf(item.NodeId.Bytes) - + Rlp.LengthOf(item.Host) - + Rlp.LengthOf(item.Port) - + 1 - + Rlp.LengthOf(item.Reputation); + private static bool IsEnrString(ReadOnlySpan value) => + value.Length != PublicKey.LengthInBytes && + value is [(byte)'e', (byte)'n', (byte)'r', (byte)':', ..]; public static void Init() { diff --git a/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs b/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs index 00e92fa5c3a2..78b3bc4c9fdd 100644 --- a/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs @@ -34,7 +34,7 @@ public void ForceResync_preserves_peer_and_discovery_directories() Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.PeersDb)), Is.True, "peers should be preserved"); Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryNodes)), Is.True, "discoveryNodes should be preserved"); - Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryV5Nodes)), Is.True, "discoveryV5Nodes should be preserved"); + Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryV5Nodes)), Is.False, "legacy discoveryV5Nodes should be deleted"); } [Test] diff --git a/src/Nethermind/Nethermind.Runner/DatabasePurger.cs b/src/Nethermind/Nethermind.Runner/DatabasePurger.cs index 33c5ecd78fe7..4294a55444d5 100644 --- a/src/Nethermind/Nethermind.Runner/DatabasePurger.cs +++ b/src/Nethermind/Nethermind.Runner/DatabasePurger.cs @@ -14,8 +14,7 @@ internal static class DatabasePurger private static readonly HashSet NetworkDbNames = new(StringComparer.OrdinalIgnoreCase) { DbNames.PeersDb, - DbNames.DiscoveryNodes, - DbNames.DiscoveryV5Nodes + DbNames.DiscoveryNodes }; /// diff --git a/src/Nethermind/Nethermind.Runner/packages.lock.json b/src/Nethermind/Nethermind.Runner/packages.lock.json index c839ff077656..0cebc4ff48df 100644 --- a/src/Nethermind/Nethermind.Runner/packages.lock.json +++ b/src/Nethermind/Nethermind.Runner/packages.lock.json @@ -1014,7 +1014,8 @@ "Nethermind.Config": "[1.39.0-unstable, )", "Nethermind.Core": "[1.39.0-unstable, )", "Nethermind.Logging": "[1.39.0-unstable, )", - "Nethermind.Network.Contract": "[1.39.0-unstable, )" + "Nethermind.Network.Contract": "[1.39.0-unstable, )", + "Nethermind.Network.Enr": "[1.39.0-unstable, )" } }, "nethermind.opcodetracing.plugin": { From 55f9abadd58e03e4ab253503b0b457ba76004f4e Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 4 Jun 2026 11:44:27 +0300 Subject: [PATCH 144/182] Tidy discovery follow-ups --- src/Nethermind/Nethermind.Crypto/PrivateKey.cs | 16 ++++------------ .../Nethermind.Init/Modules/DiscoveryModule.cs | 7 ++++--- .../MicrosoftLoggerExtensions.cs | 13 ++++++------- .../DiscoveryV5AppTests.cs | 1 - .../Nethermind.Network.Test/NodesLoaderTests.cs | 3 ++- .../PeerManagerFilteringIntegrationTests.cs | 4 ++-- .../Nethermind.Network.Test/PeerManagerTests.cs | 2 +- src/Nethermind/Nethermind.Network/NodesLoader.cs | 7 +++++-- 8 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/Nethermind/Nethermind.Crypto/PrivateKey.cs b/src/Nethermind/Nethermind.Crypto/PrivateKey.cs index db0a18dfdaaa..ae7e7a841e3f 100644 --- a/src/Nethermind/Nethermind.Crypto/PrivateKey.cs +++ b/src/Nethermind/Nethermind.Crypto/PrivateKey.cs @@ -57,24 +57,16 @@ public PrivateKey(byte[] keyBytes) /// /// The remote public key. /// The 33-byte compressed ECDH shared EC point. - public byte[] GetCompressedSharedPoint(PublicKey publicKey) - { - ArgumentNullException.ThrowIfNull(publicKey); - - return SecP256k1Ecdh.GetCompressedSharedPoint(publicKey.PrefixedBytes, KeyBytes); - } + public byte[] GetCompressedSharedPoint(PublicKey publicKey) => + SecP256k1Ecdh.GetCompressedSharedPoint(publicKey.PrefixedBytes, KeyBytes); /// /// Computes the compressed secp256k1 ECDH shared EC point for this private key and a remote compressed public key. /// /// The remote compressed public key. /// The 33-byte compressed ECDH shared EC point. - public byte[] GetCompressedSharedPoint(CompressedPublicKey publicKey) - { - ArgumentNullException.ThrowIfNull(publicKey); - - return SecP256k1Ecdh.GetCompressedSharedPoint(publicKey.Bytes, KeyBytes); - } + public byte[] GetCompressedSharedPoint(CompressedPublicKey publicKey) => + SecP256k1Ecdh.GetCompressedSharedPoint(publicKey.Bytes, KeyBytes); private bool Equals(PrivateKey other) => Bytes.AreEqual(KeyBytes, other.KeyBytes); diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 0157537ab986..295d10525e99 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -26,12 +26,13 @@ public class DiscoveryModule(IInitConfig initConfig, INetworkConfig networkConfi { protected override void Load(ContainerBuilder builder) { + builder.Register(static context => new NodesLoaderOptions( + LoadBootnodesAsPeerCandidates: (context.Resolve().DiscoveryVersion & DiscoveryVersion.V4) != 0)) + .SingleInstance(); + builder.RegisterType() .AsSelf() .WithAttributeFiltering() - .WithParameter( - static (parameterInfo, _) => parameterInfo.ParameterType == typeof(bool) && parameterInfo.Name == "loadBootnodesAsPeerCandidates", - static (_, context) => (context.Resolve().DiscoveryVersion & DiscoveryVersion.V4) != 0) .SingleInstance(); builder diff --git a/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs b/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs index 634a3cc57e81..e3c785a9c992 100644 --- a/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs +++ b/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs @@ -1,21 +1,20 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using MsILogger = global::Microsoft.Extensions.Logging.ILogger; -using MsLogLevel = global::Microsoft.Extensions.Logging.LogLevel; +using MsLogging = Microsoft.Extensions.Logging; namespace Nethermind.Logging.Microsoft { public static class MicrosoftLoggerExtensions { - public static bool IsError(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Error); + public static bool IsError(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Error); - public static bool IsWarn(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Warning); + public static bool IsWarn(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Warning); - public static bool IsInfo(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Information); + public static bool IsInfo(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Information); - public static bool IsDebug(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Debug); + public static bool IsDebug(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Debug); - public static bool IsTrace(this MsILogger logger) => logger.IsEnabled(MsLogLevel.Trace); + public static bool IsTrace(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Trace); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index e63b636c6f31..5aaed9e4f392 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -11,7 +11,6 @@ using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Logging; -using Nethermind.Network; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Enr; diff --git a/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs b/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs index 57e259de7e7a..a8c9c02787db 100644 --- a/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NodesLoaderTests.cs @@ -35,7 +35,8 @@ public void SetUp() } private NodesLoader CreateLoader(bool loadBootnodesAsPeerCandidates = true) => - new(_networkConfig, _statsManager, _peerStorage, _rlpxHost, LimboLogs.Instance, loadBootnodesAsPeerCandidates); + new(_networkConfig, _statsManager, _peerStorage, _rlpxHost, LimboLogs.Instance, + new NodesLoaderOptions(LoadBootnodesAsPeerCandidates: loadBootnodesAsPeerCandidates)); [Test] public void When_no_peers_then_no_peers_nada_zero() diff --git a/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs index 9f2599766a5c..32f68a0d0346 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs @@ -44,7 +44,7 @@ public async Task PeerManager_CallsShouldContactBeforeConnectAsync() MaxOutgoingConnectPerSec = 1000000 }; - NodesLoader nodesLoader = new(networkConfig, stats, storage, trackingMock, LimboLogs.Instance); + NodesLoader nodesLoader = new(networkConfig, stats, storage, trackingMock, LimboLogs.Instance, new NodesLoaderOptions()); IStaticNodesManager staticNodesManager = Substitute.For(); staticNodesManager.DiscoverNodes(Arg.Any()).Returns(AsyncEnumerable.Empty()); TestNodeSource testNodeSource = new(); @@ -153,7 +153,7 @@ public Context() MaxOutgoingConnectPerSec = 1000000 }; - NodesLoader nodesLoader = new(networkConfig, stats, storage, RlpxMock, LimboLogs.Instance); + NodesLoader nodesLoader = new(networkConfig, stats, storage, RlpxMock, LimboLogs.Instance, new NodesLoaderOptions()); StaticNodesManager = Substitute.For(); StaticNodesManager.DiscoverNodes(Arg.Any()).Returns(AsyncEnumerable.Empty()); TestNodeSource = new TestNodeSource(); diff --git a/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs index 651a227d5e06..7e89e511ef75 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs @@ -726,7 +726,7 @@ public Context(int parallelism = 0, int maxActivePeers = 25) ITimerFactory timerFactory = Substitute.For(); Stats = new NodeStatsManager(timerFactory, LimboLogs.Instance); Storage = new InMemoryStorage(); - NodesLoader = new NodesLoader(new NetworkConfig(), Stats, Storage, RlpxPeer, LimboLogs.Instance); + NodesLoader = new NodesLoader(new NetworkConfig(), Stats, Storage, RlpxPeer, LimboLogs.Instance, new NodesLoaderOptions()); NetworkConfig = new NetworkConfig(); NetworkConfig.MaxActivePeers = maxActivePeers; NetworkConfig.PeersPersistenceInterval = 50; diff --git a/src/Nethermind/Nethermind.Network/NodesLoader.cs b/src/Nethermind/Nethermind.Network/NodesLoader.cs index 77ed0d5456cf..c9a2b017d817 100644 --- a/src/Nethermind/Nethermind.Network/NodesLoader.cs +++ b/src/Nethermind/Nethermind.Network/NodesLoader.cs @@ -19,26 +19,29 @@ namespace Nethermind.Network /// /// This class should be split into multiple sources /// + public sealed record NodesLoaderOptions(bool LoadBootnodesAsPeerCandidates = true); + public class NodesLoader( INetworkConfig networkConfig, INodeStatsManager stats, [KeyFilter(DbNames.PeersDb)] INetworkStorage peerStorage, IRlpxHost rlpxHost, ILogManager logManager, - bool loadBootnodesAsPeerCandidates = true) : INodeSource + NodesLoaderOptions options) : INodeSource { private readonly INetworkConfig _networkConfig = networkConfig ?? throw new ArgumentNullException(nameof(networkConfig)); private readonly INodeStatsManager _stats = stats ?? throw new ArgumentNullException(nameof(stats)); private readonly INetworkStorage _peerStorage = peerStorage ?? throw new ArgumentNullException(nameof(peerStorage)); private readonly IRlpxHost _rlpxHost = rlpxHost ?? throw new ArgumentNullException(nameof(rlpxHost)); private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); + private readonly NodesLoaderOptions _options = options ?? throw new ArgumentNullException(nameof(options)); public IAsyncEnumerable DiscoverNodes(CancellationToken cancellationToken) { List allPeers = []; LoadPeersFromDb(allPeers); - if (!_networkConfig.OnlyStaticPeers && loadBootnodesAsPeerCandidates) + if (!_networkConfig.OnlyStaticPeers && _options.LoadBootnodesAsPeerCandidates) { LoadConfigPeers(allPeers, _networkConfig.Bootnodes, n => { From 22dc7694b57832496758e310ea133bfcb244f6d0 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 4 Jun 2026 12:08:53 +0300 Subject: [PATCH 145/182] Harden discovery lookup nodes --- .../LookupKNearestNeighbour.cs | 10 +++++ .../Discv5/KademliaAdapterTests.cs | 14 ++++++ .../Kademlia/LookupKNearestNeighbourTests.cs | 44 +++++++++++++++++++ .../Discv5/Kademlia/KademliaAdapter.cs | 28 +++++++++--- 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 35cd78a3f037..90e84c07bc16 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -61,6 +61,11 @@ CancellationToken token foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) { + if (node is null) + { + continue; + } + TKadKey nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); bestSeen.Enqueue((nodeHash, node), nodeHash); @@ -200,6 +205,11 @@ void ProcessResult(TKadKey hash, TNode toQuery, (TNode, TNode[]? neighbours)? va foreach (TNode neighbour in neighbours) { + if (neighbour is null) + { + continue; + } + TKadKey neighbourHash = nodeHashProvider.GetHash(neighbour); // Already queried, we ignore diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 32b74c76799e..cd4c2df64272 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -59,6 +59,20 @@ public void GetNodesAtDistances_ShouldExcludeRequester() Assert.That(result, Is.EqualTo(new[] { returned })); } + [Test] + public void GetNodesAtDistances_ShouldIgnoreRuntimeNullEntries() + { + Node returned = CreateNode(TestItem.PublicKeyB, 2); + + _kademlia.GetAllAtDistance(10).Returns([null!, returned]); + + KademliaAdapter adapter = CreateAdapter(); + + Node[] result = adapter.GetNodesAtDistances([10]); + + Assert.That(result, Is.EqualTo(new[] { returned })); + } + [TestCase(-1)] [TestCase(257)] public void GetNodesAtDistances_ShouldRejectInvalidDistance(int distance) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 52dbf96cd57f..324f2a265a4f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -112,6 +112,44 @@ public async Task Lookup_should_not_mark_node_healthy_when_find_neighbours_retur health.DidNotReceive().OnIncomingMessageFrom(Seed1); } + [Test] + [CancelAfter(10000)] + public async Task Lookup_should_ignore_runtime_null_nodes(CancellationToken token) + { + Hash256 seedHash = ValueHashKeyOperator.ToHash(Seed1); + Hash256 neighbourHash = ValueHashKeyOperator.ToHash(N1); + IRoutingTable routing = Substitute.For>(); + routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(["seed", null!]); + + INodeHealthTracker health = Substitute.For>(); + LookupKNearestNeighbour lookup = new( + routing, + new StringHashProvider(new Dictionary + { + ["seed"] = seedHash, + ["neighbour"] = neighbourHash, + }), + Hash256KademliaDistance.Instance, + health, + new KademliaConfig + { + CurrentNodeId = "self", + Alpha = 1, + KSize = 8, + LookupFindNeighbourHardTimeout = TimeSpan.FromSeconds(10), + }); + + string[] result = await lookup.Lookup( + seedHash, + 8, + (_, _) => Task.FromResult([null!, "neighbour"]), + token); + + Assert.That(result, Does.Contain("seed")); + Assert.That(result, Does.Contain("neighbour")); + health.Received(1).OnIncomingMessageFrom("seed"); + } + [Test] [CancelAfter(10000)] public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(CancellationToken token) @@ -190,4 +228,10 @@ public async Task Lookup_should_drain_cancelled_workers_before_returning(Cancell Assert.That(cancelledWorkerDrained.Task.IsCompleted, Is.True); } + + private sealed class StringHashProvider(Dictionary hashes) : INodeHashProvider + { + public Hash256 GetHash(string node) => + hashes.GetValueOrDefault(node, ValueHashKeyOperator.ToHash(ValueKeccak.Compute(node))); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 27f01a1c72d9..aa1dd4d0d340 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -78,7 +78,12 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = Node[] nodes = kademlia.Value.GetAllAtDistance(distance); for (int i = 0; i < nodes.Length; i++) { - Node node = nodes[i]; + Node? node = nodes[i]; + if (node is null) + { + continue; + } + if (excludedHash is not null && node.IdHash.Equals(excludedHash)) { continue; @@ -130,13 +135,21 @@ public async Task Ping(Node receiver, CancellationToken token) } Node[] nodes = responseHandler.GetNodes(); - if (_logger.IsTrace) _logger.Trace($"Discv5 FINDNODE {findNode.RequestId} to {receiver:s} returned {nodes.Length} nodes."); + int validCount = 0; for (int i = 0; i < nodes.Length; i++) { - kademlia.Value.AddOrRefresh(nodes[i]); + Node? node = nodes[i]; + if (node is null) + { + continue; + } + + kademlia.Value.AddOrRefresh(node); + nodes[validCount++] = node; } - return nodes; + if (_logger.IsTrace) _logger.Trace($"Discv5 FINDNODE {findNode.RequestId} to {receiver:s} returned {validCount} nodes."); + return validCount == nodes.Length ? nodes : nodes[..validCount]; } public async Task RunAsync(CancellationToken token) @@ -579,7 +592,12 @@ private void AddFindNodeRecordsAtDistance( Hash256 requesterHash = requester.IdHash; for (int i = 0; i < nodes.Length && result.Count < MaxFindNodeRecords; i++) { - Node node = nodes[i]; + Node? node = nodes[i]; + if (node is null) + { + continue; + } + if (node.IdHash.Equals(requesterHash) || string.IsNullOrEmpty(node.Enr) || !seen.Add(node.Id.Hash)) { continue; From 34a5c467e42a49b80d6bb26e6d5d437f207339f3 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 4 Jun 2026 13:44:01 +0300 Subject: [PATCH 146/182] Review --- .../Caching/LruCacheTests.cs | 16 ++++ .../Nethermind.Core/Caching/LruCache.cs | 37 ++++++-- .../Nethermind.Kademlia/Kademlia.cs | 5 - .../Kademlia/KademliaTests.cs | 28 ++++++ .../Discv5/Kademlia/KademliaAdapter.cs | 7 +- .../Discv5/Packets/Session.cs | 7 ++ .../DiscoveryKademliaConfigFactory.cs | 1 + .../Kademlia/DiscoveryPersistenceManager.cs | 1 + .../NodeRecordTests.cs | 25 +++++ .../Nethermind.Network.Enr/NodeRecord.cs | 45 ++++----- .../NetworkStorageTests.cs | 19 ++++ .../PeerManagerFilteringIntegrationTests.cs | 1 + .../PeerManagerTests.cs | 4 + .../Nethermind.Network.Test/PeerPoolTests.cs | 1 + .../Nethermind.Network/INetworkStorage.cs | 1 + .../Nethermind.Network/NetworkStorage.cs | 91 +++++++++++++++---- src/Nethermind/Nethermind.Network/PeerPool.cs | 2 + 17 files changed, 228 insertions(+), 63 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index c46bffbb3202..538e3342442e 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -312,6 +312,22 @@ public void Clear_should_free_all_capacity() } } + [Test] + public async Task Clear_invokes_eviction_callback_outside_lock() + { + LruCache cache = null!; + TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); + cache = new LruCache(2, "test", _ => callbackResult.SetResult(cache.Contains(1))); + cache.Set(1, 10); + + Task clearTask = Task.Run(cache.Clear); + Task completedTask = await Task.WhenAny(clearTask, Task.Delay(TimeSpan.FromSeconds(5))); + + Assert.That(completedTask, Is.SameAs(clearTask)); + await clearTask; + Assert.That(await callbackResult.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.False); + } + [Test] public void Delete_keeps_internal_structure() { diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 8bf66c6cf8e5..52479bccc0f8 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -38,11 +38,15 @@ public LruCache(int maxCapacity, string name, Action? onEvict = null) public void Clear() { - using McsLock.Disposable lockRelease = _lock.Acquire(); + TValue[]? evictedValues; + using (McsLock.Disposable lockRelease = _lock.Acquire()) + { + evictedValues = GetEvictedValues(); + _leastRecentlyUsed = null; + _cacheMap.Clear(); + } - NotifyEvictedValues(); - _leastRecentlyUsed = null; - _cacheMap.Clear(); + NotifyEvictedValues(evictedValues); } public TValue Get(TKey key) @@ -250,16 +254,33 @@ private void Replace(TKey key, TValue value) $"{nameof(LruCache)} called {nameof(Replace)} when empty."); } - private void NotifyEvictedValues() + private TValue[]? GetEvictedValues() { - if (_onEvict is null) + if (_onEvict is null || _cacheMap.Count == 0) { - return; + return null; } + int i = 0; + TValue[] evictedValues = new TValue[_cacheMap.Count]; foreach (KeyValuePair> kvp in _cacheMap) { - _onEvict(kvp.Value.Value.Value); + evictedValues[i++] = kvp.Value.Value.Value; + } + + return evictedValues; + } + + private void NotifyEvictedValues(TValue[]? evictedValues) + { + if (evictedValues is null) + { + return; + } + + for (int i = 0; i < evictedValues.Length; i++) + { + NotifyEvicted(evictedValues[i]); } } diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index e68a68c62ce0..ba17d649a7be 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -195,11 +195,6 @@ private void PruneLastBucketRefreshTicks(HashSet activeBucketPrefixes) { lock (_lastBucketRefreshLock) { - if (_lastBucketRefreshTicks.Count <= activeBucketPrefixes.Count) - { - return; - } - List? stalePrefixes = null; foreach (TKadKey prefix in _lastBucketRefreshTicks.Keys) { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 2da078c83ab5..fd95206e6423 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Autofac; @@ -197,6 +198,29 @@ public async Task TestTooManyNodeWithAcceleratedLookup() Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(testHashes[10..].ToHashSet())); } + [Test] + public void PruneLastBucketRefreshTicks_removes_stale_prefixes_even_when_counts_match() + { + Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + { + KSize = 5, + Beta = 0, + }); + + Hash256 activePrefix = new("0x1111111111111111111111111111111111111111111111111111111111111111"); + Hash256 stalePrefix = new("0x2222222222222222222222222222222222222222222222222222222222222222"); + Dictionary lastRefreshTicks = GetLastBucketRefreshTicks(kad); + lastRefreshTicks[activePrefix] = 1; + lastRefreshTicks[stalePrefix] = 2; + + typeof(Nethermind.Kademlia.Kademlia) + .GetMethod("PruneLastBucketRefreshTicks", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(kad, [new HashSet { activePrefix, new("0x3333333333333333333333333333333333333333333333333333333333333333") }]); + + Assert.That(lastRefreshTicks.ContainsKey(activePrefix), Is.True); + Assert.That(lastRefreshTicks.ContainsKey(stalePrefix), Is.False); + } + private static Hash256 ToHash(ValueHash256 hash) => ValueHashKeyOperator.ToHash(hash); private static ValueHash256 ToValueHash(Hash256 hash) => ValueHashKeyOperator.ToValueHash(hash); @@ -204,4 +228,8 @@ public async Task TestTooManyNodeWithAcceleratedLookup() private static ValueHash256 RandomValueHashAtDistance(ValueHash256 currentHash, int distance) => ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(ToHash(currentHash), distance)); + private static Dictionary GetLastBucketRefreshTicks(Nethermind.Kademlia.Kademlia kad) + => (Dictionary)typeof(Nethermind.Kademlia.Kademlia) + .GetField("_lastBucketRefreshTicks", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(kad)!; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index aa1dd4d0d340..de08c460645a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -233,11 +233,7 @@ private async Task SendRequest( if (TryGetSession(sessionKey, out Session? session)) { Span writeKey = stackalloc byte[Session.KeySize]; - if (!session.TryCopyWriteKey(writeKey)) - { - _sessions.TryRemove(sessionKey, out _); - } - else + if (session.TryCopyWriteKey(writeKey)) { Span sessionNonce = stackalloc byte[PacketCodec.NonceSize]; session.WriteNextNonce(cryptoRandom, sessionNonce); @@ -291,7 +287,6 @@ private async Task SendResponse(Node receiver, Discv5Message message, Cancellati Span writeKey = stackalloc byte[Session.KeySize]; if (!session.TryCopyWriteKey(writeKey)) { - _sessions.TryRemove(sessionKey, out _); return; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs index 65f4dd70c3c5..bdcc81242469 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs @@ -16,6 +16,13 @@ internal sealed record Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] private long _nonceCounter; private bool _disposed; + /// + /// Writes the next nonce for an ordinary packet sent on this session. + /// + /// + /// Callers must first copy the write key with ; a false result means the session is + /// disposed and must not be used for another packet. + /// public void WriteNextNonce(ICryptoRandom random, Span nonce) { if (nonce.Length != PacketCodec.NonceSize) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs index a5b444b30ec3..74928046ef03 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs @@ -12,6 +12,7 @@ internal static class DiscoveryKademliaConfigFactory public static KademliaConfig Create(PublicKey masterNode, IReadOnlyList bootNodes, IDiscoveryConfig discoveryConfig) => new() { + // The table only needs the local node identity here; its endpoint is never contacted. CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), KSize = discoveryConfig.BucketSize, Alpha = discoveryConfig.Concurrency, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs index 153f3844c375..1405ad10181c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs @@ -120,6 +120,7 @@ public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationTo } catch (Exception ex) { + _discoveryStorage.DiscardBatch(); _logger.Error($"Error during discovery commit: {ex}"); } } diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs index baf198f88cd0..6dd78e84a9ab 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Net; using Nethermind.Core.Crypto; using NUnit.Framework; @@ -36,6 +37,30 @@ public void Cannot_get_enr_string_when_signature_missing() Assert.Throws(() => _ = nodeRecord.EnrString); } + [Test] + public void Discovery_endpoint_uses_udp6_when_ipv4_udp_is_missing() + { + IPAddress ip = IPAddress.Parse("192.0.2.1"); + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(new IpEntry(ip)); + nodeRecord.SetEntry(new Udp6Entry(30304)); + + Assert.That(nodeRecord.DiscoveryIp, Is.EqualTo(ip)); + Assert.That(nodeRecord.DiscoveryPort, Is.EqualTo(30304)); + } + + [Test] + public void Discovery_endpoint_uses_udp_as_ipv6_fallback() + { + IPAddress ip = IPAddress.Parse("2001:db8::1"); + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(new Ip6Entry(ip)); + nodeRecord.SetEntry(new UdpEntry(30303)); + + Assert.That(nodeRecord.DiscoveryIp, Is.EqualTo(ip)); + Assert.That(nodeRecord.DiscoveryPort, Is.EqualTo(30303)); + } + [Test] public void Enr_content_entry_has_hash_code() { diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 3275834e3f2f..36e8c352a329 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -115,22 +115,10 @@ public Signature? Signature /// Gets the IP address advertised for discovery traffic. /// /// - /// IPv4 is preferred when both ip and udp are present. Otherwise IPv6 is returned when ip6 - /// is present. + /// IPv4 is preferred when both ip and udp are present. Otherwise IPv6 is returned when it has a + /// discovery port, with udp as the EIP-778 fallback. /// - public IPAddress? DiscoveryIp - { - get - { - IPAddress? ip = GetObj(EnrContentKey.Ip); - if (ip is not null && GetValue(EnrContentKey.Udp) is not null) - { - return ip; - } - - return GetObj(EnrContentKey.Ip6); - } - } + public IPAddress? DiscoveryIp => GetDiscoveryEndpoint().Ip; /// /// Gets the UDP port advertised for discovery traffic. @@ -138,21 +126,26 @@ public IPAddress? DiscoveryIp /// /// For IPv6, udp6 is preferred and udp is used as the EIP-778 fallback. /// - public int? DiscoveryPort + public int? DiscoveryPort => GetDiscoveryEndpoint().Port; + + private (IPAddress? Ip, int? Port) GetDiscoveryEndpoint() { - get + IPAddress? ip = GetObj(EnrContentKey.Ip); + int? udp = GetValue(EnrContentKey.Udp); + if (ip is not null && udp is not null) { - IPAddress? ip = GetObj(EnrContentKey.Ip); - int? udp = GetValue(EnrContentKey.Udp); - if (ip is not null && udp is not null) - { - return udp; - } + return (ip, udp); + } - return GetObj(EnrContentKey.Ip6) is not null - ? GetValue(EnrContentKey.Udp6) ?? udp - : null; + IPAddress? ip6 = GetObj(EnrContentKey.Ip6); + int? udp6 = GetValue(EnrContentKey.Udp6); + if (ip6 is not null) + { + int? port = udp6 ?? udp; + return port is null ? (null, null) : (ip6, port); } + + return ip is not null && udp6 is not null ? (ip, udp6) : (null, null); } public static NodeRecord FromEnrString(string enrString) diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs index 36147430c503..a89514ec2017 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs @@ -146,4 +146,23 @@ public void Can_store_peers() Assert.That(persistedNode.Reputation, Is.EqualTo(peer.Reputation)); } } + + [Test] + public void Discard_batch_drops_pending_nodes() + { + NetworkStorage storage = new(new SnapshotableMemDb(), LimboLogs.Instance); + NetworkNode node = new(TestItem.PublicKeyA, "192.1.1.1", 3441, 0L); + + storage.StartBatch(); + storage.UpdateNode(node); + storage.DiscardBatch(); + + Assert.That(storage.GetPersistedNodes(), Is.Empty); + + storage.StartBatch(); + storage.UpdateNode(node); + storage.Commit(); + + Assert.That(storage.GetPersistedNodes(), Has.Length.EqualTo(1)); + } } diff --git a/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs index 32f68a0d0346..3edcd90f3c3a 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs @@ -254,6 +254,7 @@ public void UpdateNodes(IEnumerable nodes) public void RemoveNode(PublicKey nodeId) => _pendingChanges = true; public void StartBatch() { } public void Commit() { } + public void DiscardBatch() { } public bool AnyPendingChange() => _pendingChanges; } } diff --git a/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs index 7e89e511ef75..b9937b0bf28a 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs @@ -1006,6 +1006,10 @@ public void Commit() { } + public void DiscardBatch() + { + } + private bool _pendingChanges; public int PersistedNodesCount => _nodes.Count; diff --git a/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs index d9339a6aefb6..ae0e6f708230 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs @@ -229,6 +229,7 @@ private sealed class TestNetworkStorage : INetworkStorage public void RemoveNode(PublicKey nodeId) => Pending = true; public void StartBatch() { Interlocked.Increment(ref _startBatchCountBacking); StartBatchCount = _startBatchCountBacking; } public void Commit() { Interlocked.Increment(ref _commitCountBacking); CommitCount = _commitCountBacking; } + public void DiscardBatch() { } public bool AnyPendingChange() => Pending; private int _commitCountBacking; diff --git a/src/Nethermind/Nethermind.Network/INetworkStorage.cs b/src/Nethermind/Nethermind.Network/INetworkStorage.cs index 0aa0fbdc6015..67918d1ca608 100644 --- a/src/Nethermind/Nethermind.Network/INetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/INetworkStorage.cs @@ -17,6 +17,7 @@ public interface INetworkStorage void RemoveNode(PublicKey nodeId); void StartBatch(); void Commit(); + void DiscardBatch(); bool AnyPendingChange(); } } diff --git a/src/Nethermind/Nethermind.Network/NetworkStorage.cs b/src/Nethermind/Nethermind.Network/NetworkStorage.cs index cf59e1a913b1..afc25faf0901 100644 --- a/src/Nethermind/Nethermind.Network/NetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/NetworkStorage.cs @@ -85,13 +85,14 @@ public void UpdateNode(NetworkNode node) { lock (_lock) { - UpdateNodeImpl(node); + byte[] rlp = Rlp.Encode(node).Bytes; + UpdateNodeImpl(node, rlp); } } - private void UpdateNodeImpl(NetworkNode node) + private void UpdateNodeImpl(NetworkNode node, byte[] rlp) { - (_currentBatch ?? (IWriteOnlyKeyValueStore)_fullDb)[node.NodeId.Bytes] = Rlp.Encode(node).Bytes; + (_currentBatch ?? (IWriteOnlyKeyValueStore)_fullDb)[node.NodeId.Bytes] = rlp; _updateCounter++; if (!_nodesDict.ContainsKey(node.NodeId)) @@ -108,27 +109,29 @@ private void UpdateNodeImpl(NetworkNode node) public void UpdateNodes(IEnumerable nodes) { + List<(NetworkNode Node, byte[] Rlp)> encodedNodes = []; + foreach (NetworkNode node in nodes) + { + encodedNodes.Add((node, Rlp.Encode(node).Bytes)); + } + lock (_lock) { - foreach (NetworkNode node in nodes) + for (int i = 0; i < encodedNodes.Count; i++) { - UpdateNodeImpl(node); + (NetworkNode node, byte[] rlp) = encodedNodes[i]; + UpdateNodeImpl(node, rlp); } } } public void RemoveNode(PublicKey nodeId) - { - (_currentBatch ?? (IWriteOnlyKeyValueStore)_fullDb)[nodeId.Bytes] = null; - _removeCounter++; - - RemoveLocal(nodeId); - } - - private void RemoveLocal(PublicKey nodeId) { lock (_lock) { + (_currentBatch ?? (IWriteOnlyKeyValueStore)_fullDb)[nodeId.Bytes] = null; + _removeCounter++; + if (_nodesDict.Remove(nodeId)) { // Clear the cache @@ -141,21 +144,73 @@ private void RemoveLocal(PublicKey nodeId) public void StartBatch() { - _currentBatch = _fullDb.StartWriteBatch(); - _updateCounter = 0; - _removeCounter = 0; + lock (_lock) + { + DiscardBatchNoLock(); + _currentBatch = _fullDb.StartWriteBatch(); + } } public void Commit() { - if (_logger.IsTrace) _logger.Trace($"[{_fullDb.Name}] Committing nodes, updates: {_updateCounter}, removes: {_removeCounter}"); - _currentBatch?.Dispose(); + IWriteBatch? currentBatch; + lock (_lock) + { + if (_logger.IsTrace) _logger.Trace($"[{_fullDb.Name}] Committing nodes, updates: {_updateCounter}, removes: {_removeCounter}"); + currentBatch = _currentBatch; + _currentBatch = null; + _updateCounter = 0; + _removeCounter = 0; + } + + try + { + currentBatch?.Dispose(); + } + catch + { + ClearLocalCache(); + throw; + } + if (_logger.IsTrace) { LogDbContent(_fullDb.Values); } } + public void DiscardBatch() + { + lock (_lock) + { + DiscardBatchNoLock(); + } + } + + private void DiscardBatchNoLock() + { + IWriteBatch? currentBatch = _currentBatch; + _currentBatch = null; + _updateCounter = 0; + _removeCounter = 0; + + if (currentBatch is not null) + { + currentBatch.Clear(); + currentBatch.Dispose(); + ClearLocalCache(); + } + } + + private void ClearLocalCache() + { + lock (_lock) + { + _nodesDict.Clear(); + _nodes = null; + } + } + public bool AnyPendingChange() => _updateCounter > 0 || _removeCounter > 0; private static NetworkNode GetNode(byte[] networkNodeRaw) diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index 629be9fa089c..e54ac103b4f6 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -190,6 +190,8 @@ private async Task RunPeerCommit() } catch (Exception ex) { + _peerStorage.DiscardBatch(); + _peerStorage.StartBatch(); if (_logger.IsError) ErrorPeerStorageCommit(ex); } } From fb775f47b004fe12482b30f842008f367ce46497 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 4 Jun 2026 17:26:25 +0300 Subject: [PATCH 147/182] More fixes --- .../DiscoveryV5AppTests.cs | 31 ++++++++- .../Discv5/KademliaAdapterTests.cs | 48 ++++++++++++- .../Discv5/NodeSourceTests.cs | 52 +++++++++++++- .../Discv5/DiscoveryV5App.cs | 12 +++- .../Kademlia/Handlers/NodesResponseHandler.cs | 3 +- .../Discv5/Kademlia/KademliaAdapter.cs | 27 ++++++-- .../Discv5/Kademlia/NodeSource.cs | 38 +++++++++-- .../Nethermind.Network.Enr/EnrContentKey.cs | 6 ++ .../Nethermind.Network.Enr/NodeRecord.cs | 44 ++++++++++++ .../NodeRecordSigner.cs | 7 +- .../Nethermind.Network.Enr/UnknownEntry.cs | 15 +++++ .../Nethermind.Network.Stats/Model/Node.cs | 22 ++++-- .../Stats/NodeTests.cs | 67 +++++++++++++++++++ 13 files changed, 346 insertions(+), 26 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Enr/UnknownEntry.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 5aaed9e4f392..e268f5e30466 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -93,7 +93,7 @@ public async Task Teardown() _discoveryDb.Dispose(); } - private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true, bool includeUdp = true) + private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true, bool includeUdp = true, bool includeEth2 = false) { NodeRecord enr = new(); enr.SetEntry(IdEntry.Instance); @@ -107,6 +107,10 @@ private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, { enr.SetEntry(new UdpEntry(udpPort ?? port)); } + if (includeEth2) + { + enr.SetEntry(new TestEth2Entry()); + } enr.EnrSequence = 1; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); @@ -213,6 +217,22 @@ public void Should_Accept_Public_Ip_Enr() Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); } + [Test] + public void Should_Reject_Consensus_Only_Enr() + { + NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), includeEth2: true); + NodeRecord decoded = NodeRecord.FromEnrString(enr.EnrString); + + bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(decoded, out Node? node); + + using (Assert.EnterMultipleScope()) + { + Assert.That(decoded.HasEntry(EnrContentKey.Eth2), Is.True); + Assert.That(result, Is.False); + Assert.That(node, Is.Null); + } + } + [Test] public async Task AddNodeToDiscovery_ShouldSkipNodeWithoutEnr() { @@ -358,4 +378,13 @@ public void Should_Use_Udp_Port_From_Configured_Enr_Bootnode() Assert.That(bootNodes[0].Enr, Is.EqualTo(enr.EnrString)); } } + + private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) + { + public override string Key => EnrContentKey.Eth2; + + protected override int GetRlpLengthOfValue() => Nethermind.Serialization.Rlp.Rlp.LengthOf(Value); + + protected override void EncodeValue(Nethermind.Serialization.Rlp.RlpStream rlpStream) => rlpStream.Encode(Value); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index cd4c2df64272..2d994b741b2c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -148,16 +148,60 @@ public void IsAcceptableNodeRecord_ShouldAllowNonRoutableWhenRequested() private static Node CreateNode(PublicKey publicKey, int hostSuffix) => new(publicKey, $"192.168.1.{hostSuffix}", 30303); - private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) + [Test] + public void IsAcceptableNodeRecord_ShouldRejectConsensusOnlyRecord() + { + NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.8.8"), includeEth2: true); + + Assert.That( + KademliaAdapter.IsAcceptableNodeRecord( + NodeRecord.FromEnrString(record.EnrString), + TestItem.PrivateKeyB.PublicKey.Hash, + allowNonRoutable: false), + Is.False); + } + + [Test] + public void TrySetKnownRecord_ShouldNotDowngradeSequence() + { + KademliaAdapter adapter = CreateAdapter(); + NodeRecord newer = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.8.8"), enrSequence: 2); + NodeRecord stale = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.4.4"), enrSequence: 1); + + Assert.That(adapter.TrySetKnownRecord(TestItem.PrivateKeyB.PublicKey.Hash, newer, out NodeRecord current), Is.True); + Assert.That(current, Is.SameAs(newer)); + + Assert.That(adapter.TrySetKnownRecord(TestItem.PrivateKeyB.PublicKey.Hash, stale, out current), Is.False); + using (Assert.EnterMultipleScope()) + { + Assert.That(current, Is.SameAs(newer)); + Assert.That(current.EnrSequence, Is.EqualTo(2)); + } + } + + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, ulong enrSequence = 1, bool includeEth2 = false) { NodeRecord enr = new(); enr.SetEntry(IdEntry.Instance); enr.SetEntry(new IpEntry(ipAddress)); enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); enr.SetEntry(new UdpEntry(30303)); - enr.EnrSequence = 1; + if (includeEth2) + { + enr.SetEntry(new TestEth2Entry()); + } + enr.EnrSequence = enrSequence; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); return enr; } + private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) + { + public override string Key => EnrContentKey.Eth2; + + protected override int GetRlpLengthOfValue() => Nethermind.Serialization.Rlp.Rlp.LengthOf(Value); + + protected override void EncodeValue(Nethermind.Serialization.Rlp.RlpStream rlpStream) => rlpStream.Encode(Value); + } + } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index f00281764caa..4a8a2819e390 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -3,13 +3,16 @@ using System; using System.Collections.Generic; +using System.Net; using System.Threading; using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Discv5.Kademlia; +using Nethermind.Network.Enr; using Nethermind.Stats.Model; using NSubstitute; using NUnit.Framework; @@ -59,8 +62,53 @@ public async Task DiscoverNodes_ShouldNotRetainDroppedNodesInRecentDedupe(Cancel Assert.That(enumerator.Current, Is.EqualTo(droppedNode)); } - private static Node CreateNode(int index) => - new(TestItem.PublicKeys[index], $"192.168.1.{index + 1}", 30303); + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_ShouldEmitPeerCandidateWithTcpEndpoint(CancellationToken token) + { + IKademlia kademlia = Substitute.For>(); + kademlia.IterateNodes().Returns(Array.Empty()); + NodeSource source = new( + kademlia, + new KademliaConfig { CurrentNodeId = CreateNode(0) }, + LimboLogs.Instance); + + await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); + ValueTask firstMove = enumerator.MoveNextAsync(); + await Task.Yield(); + RaiseNode(kademlia, CreateNode(1, tcpPort: 30303, udpPort: 30304)); + + Assert.That(await firstMove.AsTask(), Is.True); + using (Assert.EnterMultipleScope()) + { + Assert.That(enumerator.Current.Id, Is.EqualTo(TestItem.PrivateKeys[1].PublicKey)); + Assert.That(enumerator.Current.Port, Is.EqualTo(30303)); + } + } + + private static Node CreateNode(int index, int tcpPort = 30303, int udpPort = 30304) + { + PrivateKey privateKey = TestItem.PrivateKeys[index]; + string host = $"192.168.1.{index + 1}"; + NodeRecord enr = CreateEnr(privateKey, IPAddress.Parse(host), tcpPort, udpPort); + return new Node(privateKey.PublicKey, host, udpPort) + { + Enr = enr.EnrString + }; + } + + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.SetEntry(new TcpEntry(tcpPort)); + enr.SetEntry(new UdpEntry(udpPort)); + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + return enr; + } private static void RaiseNode(IKademlia kademlia, Node node) => kademlia.OnNodeAdded += Raise.Event>(null, node); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 28fe16ae0ab0..21d9b30b3c06 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -186,7 +186,14 @@ private static string[] GetDefaultBootnodes() => internal bool TryGetAcceptableNodeFromEnr(NodeRecord enr, [NotNullWhen(true)] out Node? node) { - if (Node.TryFromEnr(enr, out Node? enrNode) && IsDiscoveryAddressAcceptable(enrNode.Address.Address, _allowNonRoutableEnrs)) + if (IsConsensusOnlyNodeRecord(enr)) + { + node = null; + if (Logger.IsTrace) Logger.Trace("Enr declined, consensus-only ENRs are not execution discovery peers."); + return false; + } + + if (Node.TryFromDiscoveryEnr(enr, out Node? enrNode) && IsDiscoveryAddressAcceptable(enrNode.Address.Address, _allowNonRoutableEnrs)) { node = enrNode; return true; @@ -220,6 +227,9 @@ internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allo internal static bool IsDiscoveryAddressRoutable(IPAddress ipAddress) => IsDiscoveryAddressAcceptable(ipAddress, allowNonRoutable: false); + internal static bool IsConsensusOnlyNodeRecord(NodeRecord enr) + => enr.HasEntry(EnrContentKey.Eth2) && !enr.HasEntry(EnrContentKey.Eth); + private static bool ShouldAcceptNonRoutableEnrs(IPAddress externalIp) => !IPAddress.Any.Equals(externalIp) && !IPAddress.None.Equals(externalIp) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 5ac118ca941d..3161fcf6a949 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -49,7 +49,8 @@ public override bool Handle(NodesMsg nodes) for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; - if (!Node.TryFromEnr(record, out Node? node) || + if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record) || + !Node.TryFromDiscoveryEnr(record, out Node? node) || !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || !_seenNodeIds.Add(node.Id.Hash) || !MatchesRequestedDistance(node, requestedDistances)) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index de08c460645a..3a659024540a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -56,6 +56,7 @@ public class KademliaAdapter( private readonly LruCache _pendingByNonce = new(MaxPendingRequests, "discv5 pending requests"); private readonly LruCache _responseHandlers = new(MaxResponseHandlers, "discv5 response handlers"); private readonly LruCache _knownRecords = new(MaxKnownRecords, "discv5 known records"); + private readonly object _knownRecordsLock = new(); private readonly LruCache _endpointChecks = new(MaxEndpointChecks, "discv5 endpoint checks"); private readonly AddressBurstLimiter _challengeRateLimiter = new(ChallengeRateLimitBurstPerIp, ChallengeRateLimitFilterSize, ChallengeRateLimitWindow); @@ -423,8 +424,8 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) { - SetKnownRecord(nodeId, nodeRecord); - messageRecord = nodeRecord; + TrySetKnownRecord(nodeId, nodeRecord, out NodeRecord currentRecord); + messageRecord = currentRecord; } } @@ -637,7 +638,7 @@ private void RegisterKnownRecord(Node node) NodeRecord record = NodeRecord.FromEnrString(node.Enr); if (IsAcceptableNodeRecord(record, node.Id.Hash, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address))) { - SetKnownRecord(node.Id.Hash, record); + TrySetKnownRecord(node.Id.Hash, record, out _); } } catch (Exception e) @@ -708,11 +709,25 @@ private void SetSession(SessionKey sessionKey, Session session) private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGet(nodeId, out record); - private void SetKnownRecord(Hash256 nodeId, NodeRecord record) - => _knownRecords.Set(nodeId, record); + internal bool TrySetKnownRecord(Hash256 nodeId, NodeRecord record, out NodeRecord currentRecord) + { + lock (_knownRecordsLock) + { + if (_knownRecords.TryGet(nodeId, out NodeRecord? knownRecord) && knownRecord.EnrSequence >= record.EnrSequence) + { + currentRecord = knownRecord; + return false; + } + + _knownRecords.Set(nodeId, record); + currentRecord = record; + return true; + } + } internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable) - => Node.TryFromEnr(record, out Node? node) && + => !DiscoveryV5App.IsConsensusOnlyNodeRecord(record) && + Node.TryFromDiscoveryEnr(record, out Node? node) && node.Id.Hash.Equals(expectedNodeId) && DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, allowNonRoutable); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index 25d86b8e7dcb..1eedd707eb17 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Channels; using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Enr; using Nethermind.Stats.Model; namespace Nethermind.Network.Discovery.Discv5.Kademlia; @@ -33,10 +35,12 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance foreach (Node node in kademlia.IterateNodes()) { - if (!IsExcluded(node) && recentlyWrittenNodes.TryReserve(node.IdHash)) + if (!IsExcluded(node) && + TryCreatePeerCandidate(node, out Node? peerCandidate) && + recentlyWrittenNodes.TryReserve(peerCandidate.IdHash)) { initialNodes++; - yield return node; + yield return peerCandidate; } } @@ -57,18 +61,20 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance void Handler(object? _, Node node) { - if (IsExcluded(node) || !recentlyWrittenNodes.TryReserve(node.IdHash)) + if (IsExcluded(node) || + !TryCreatePeerCandidate(node, out Node? peerCandidate) || + !recentlyWrittenNodes.TryReserve(peerCandidate.IdHash)) { return; } - if (channel.Writer.TryWrite(node)) + if (channel.Writer.TryWrite(peerCandidate)) { - if (_logger.IsDebug) _logger.Debug($"Discv5 node source queued discovered node {node:s}."); + if (_logger.IsDebug) _logger.Debug($"Discv5 node source queued discovered node {peerCandidate:s}."); return; } - recentlyWrittenNodes.Release(node.IdHash); + recentlyWrittenNodes.Release(peerCandidate.IdHash); if (_logger.IsTrace) { _logger.Trace($"Discv5 node source queue is full, dropping discovered node {node:s}."); @@ -77,4 +83,24 @@ void Handler(object? _, Node node) } private bool IsExcluded(Node node) => node.IsBootnode || node.IdHash.Equals(_currentNodeHash); + + private bool TryCreatePeerCandidate(Node discoveryNode, [NotNullWhen(true)] out Node? peerCandidate) + { + peerCandidate = null; + if (string.IsNullOrEmpty(discoveryNode.Enr)) + { + return false; + } + + try + { + NodeRecord record = NodeRecord.FromEnrString(discoveryNode.Enr); + return Node.TryFromEnr(record, out peerCandidate); + } + catch (Exception e) + { + if (_logger.IsTrace) _logger.Trace($"Unable to parse discv5 discovered ENR for {discoveryNode}: {e}"); + return false; + } + } } diff --git a/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs b/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs index 95824f8c6070..c08805cb48fc 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EnrContentKey.cs @@ -14,6 +14,12 @@ public static class EnrContentKey public const string Eth = "eth"; public static ReadOnlySpan EthU8 => "eth"u8; + /// + /// Consensus-layer information. + /// + public const string Eth2 = "eth2"; + public static ReadOnlySpan Eth2U8 => "eth2"u8; + /// /// Name of identity scheme, e.g. "v4" /// diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 36e8c352a329..1b895641e22b 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -128,6 +128,23 @@ public Signature? Signature /// public int? DiscoveryPort => GetDiscoveryEndpoint().Port; + /// + /// Gets the IP address advertised for RLPx TCP traffic. + /// + /// + /// IPv4 is preferred when both ip and tcp are present. Otherwise IPv6 is returned when it has a + /// TCP port, with tcp as the EIP-778 fallback. + /// + public IPAddress? TcpIp => GetTcpEndpoint().Ip; + + /// + /// Gets the TCP port advertised for RLPx traffic. + /// + /// + /// For IPv6, tcp6 is preferred and tcp is used as the EIP-778 fallback. + /// + public int? TcpPort => GetTcpEndpoint().Port; + private (IPAddress? Ip, int? Port) GetDiscoveryEndpoint() { IPAddress? ip = GetObj(EnrContentKey.Ip); @@ -148,6 +165,26 @@ public Signature? Signature return ip is not null && udp6 is not null ? (ip, udp6) : (null, null); } + private (IPAddress? Ip, int? Port) GetTcpEndpoint() + { + IPAddress? ip = GetObj(EnrContentKey.Ip); + int? tcp = GetValue(EnrContentKey.Tcp); + if (ip is not null && tcp is not null) + { + return (ip, tcp); + } + + IPAddress? ip6 = GetObj(EnrContentKey.Ip6); + int? tcp6 = GetValue(EnrContentKey.Tcp6); + if (ip6 is not null) + { + int? port = tcp6 ?? tcp; + return port is null ? (null, null) : (ip6, port); + } + + return (null, null); + } + public static NodeRecord FromEnrString(string enrString) { const string prefix = "enr:"; @@ -216,6 +253,13 @@ public void SetEntry(EnrContentEntry entry) _signature = null; } + /// + /// Checks whether an ENR entry with the specified key is present. + /// + /// Key of the entry to check. + /// when the entry is present; otherwise . + public bool HasEntry(string entryKey) => Entries.ContainsKey(entryKey); + /// /// Gets a record entry value (in case of the value types). Use for reference types./> /// diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index 18cf3ed9bdaf..84aabffc54b5 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Net; +using System.Text; using Nethermind.Core.Crypto; using Nethermind.Crypto; using Nethermind.Serialization.Rlp; @@ -140,9 +141,13 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) nodeRecord.SetEntry(new SecP256k1Entry(reportedKey)); break; default: - // snap canVerify = false; + int valueStart = ctx.Position; ctx.SkipItem(); + int valueLength = ctx.Position - valueStart; + nodeRecord.SetEntry(new UnknownEntry( + Encoding.UTF8.GetString(key), + ctx.Data.Slice(valueStart, valueLength).ToArray())); nodeRecord.Snap = true; break; } diff --git a/src/Nethermind/Nethermind.Network.Enr/UnknownEntry.cs b/src/Nethermind/Nethermind.Network.Enr/UnknownEntry.cs new file mode 100644 index 000000000000..49944afb419b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Enr/UnknownEntry.cs @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Enr; + +internal sealed class UnknownEntry(string key, byte[] rlpValue) : EnrContentEntry(rlpValue) +{ + public override string Key { get; } = key; + + protected override int GetRlpLengthOfValue() => Value.Length; + + protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Write(Value); +} diff --git a/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs b/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs index 4751786aa9ca..89697aa89b0e 100644 --- a/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs +++ b/src/Nethermind/Nethermind.Network.Stats/Model/Node.cs @@ -90,24 +90,34 @@ public Node(NetworkNode networkNode, bool isStatic = false) } /// - /// Tries to create a node from an Ethereum Node Record with a secp256k1 key and discovery endpoint. + /// Tries to create an RLPx peer candidate from an Ethereum Node Record with a secp256k1 key and TCP endpoint. /// /// The Ethereum Node Record to read. - /// The node created from the record when the record contains a usable discovery endpoint. + /// The node created from the record when the record contains a usable TCP endpoint. /// when a node could be created; otherwise . public static bool TryFromEnr(NodeRecord enr, [MaybeNullWhen(false)] out Node node) + => TryFromEnrEndpoint(enr, enr.TcpIp, enr.TcpPort, out node); + + /// + /// Tries to create a discovery-routing node from an Ethereum Node Record with a secp256k1 key and UDP endpoint. + /// + /// The Ethereum Node Record to read. + /// The node created from the record when the record contains a usable UDP discovery endpoint. + /// when a node could be created; otherwise . + public static bool TryFromDiscoveryEnr(NodeRecord enr, [MaybeNullWhen(false)] out Node node) + => TryFromEnrEndpoint(enr, enr.DiscoveryIp, enr.DiscoveryPort, out node); + + private static bool TryFromEnrEndpoint(NodeRecord enr, IPAddress ip, int? port, [MaybeNullWhen(false)] out Node node) { node = null; PublicKey key = enr.GetObj(EnrContentKey.SecP256k1)?.Decompress(); - IPAddress ip = enr.DiscoveryIp; - int? discoveryPort = enr.DiscoveryPort; - if (key is null || ip is null || discoveryPort is null || discoveryPort.Value == 0 || (uint)discoveryPort.Value > ushort.MaxValue) + if (key is null || ip is null || port is null || port.Value == 0 || (uint)port.Value > ushort.MaxValue) { return false; } - node = new Node(key, new IPEndPoint(ip, discoveryPort.Value)) + node = new Node(key, new IPEndPoint(ip, port.Value)) { Enr = enr.EnrString }; diff --git a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs index 9056a391283b..020859e091bd 100644 --- a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs @@ -1,7 +1,11 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Net; +using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Network.Enr; using Nethermind.Stats.Model; using NUnit.Framework; @@ -36,6 +40,51 @@ public void Not_equal_to_another_type() Assert.That(node.Equals(1), Is.False); } + [Test] + public void TryFromEnr_uses_tcp_endpoint_for_peer_candidate() + { + NodeRecord enr = CreateEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), tcpPort: 30303, udpPort: 30304); + + bool result = Node.TryFromEnr(enr, out Node? node); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Is.True); + Assert.That(node, Is.Not.Null); + Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); + Assert.That(node.Port, Is.EqualTo(30303)); + Assert.That(node.Enr, Is.EqualTo(enr.EnrString)); + } + } + + [Test] + public void TryFromEnr_rejects_udp_only_record() + { + NodeRecord enr = CreateEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), tcpPort: null, udpPort: 30304); + + bool result = Node.TryFromEnr(enr, out Node? node); + + Assert.That(result, Is.False); + Assert.That(node, Is.Null); + } + + [Test] + public void TryFromDiscoveryEnr_uses_udp_endpoint_for_discovery() + { + NodeRecord enr = CreateEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), tcpPort: 30303, udpPort: 30304); + + bool result = Node.TryFromDiscoveryEnr(enr, out Node? node); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Is.True); + Assert.That(node, Is.Not.Null); + Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); + Assert.That(node.Port, Is.EqualTo(30304)); + Assert.That(node.Enr, Is.EqualTo(enr.EnrString)); + } + } + [TestCase("s", "127.0.0.1:303")] [TestCase("a", " 127.0.0.1: 303")] [TestCase("c", "[Node|127.0.0.1:303|Details|ClientId]")] @@ -55,6 +104,24 @@ static Node GetNode(string host) => Assert.That(node.ToString(format), Is.EqualTo(expectedFormat)); } + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, int? tcpPort, int? udpPort) + { + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + enr.SetEntry(new IpEntry(ipAddress)); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + if (tcpPort is not null) + { + enr.SetEntry(new TcpEntry(tcpPort.Value)); + } + if (udpPort is not null) + { + enr.SetEntry(new UdpEntry(udpPort.Value)); + } + enr.EnrSequence = 1; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + return enr; + } } } From 15cc266780939e01373cb998be138fdd1b24082a Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 4 Jun 2026 20:49:27 +0300 Subject: [PATCH 148/182] Clean NodeTests imports --- src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs index 020859e091bd..ab68a8f432c4 100644 --- a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Net; -using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; using Nethermind.Network.Enr; From d00b47fd41ab007041b135ea75ecf41458d62187 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 5 Jun 2026 19:02:21 +0300 Subject: [PATCH 149/182] Refactor discovery types --- .../Discv5/CodecTests.cs | 21 +++++++- .../CompositeDiscoveryApp.cs | 2 +- .../DiscoveryConnectionsPool.cs | 2 +- .../Kademlia/Handlers/EnrResponseHandler.cs | 2 +- .../Kademlia/Handlers/NeighbourMsgHandler.cs | 2 +- .../Kademlia/Handlers/PongMsgHandler.cs | 2 +- .../Discv4/Kademlia/KademliaAdapter.cs | 2 +- .../Discv4/Kademlia/KademliaModule.cs | 2 +- .../Discv4/Kademlia/NodeSource.cs | 2 +- .../Discv4/Messages/EnrRequestMsg.cs | 2 +- .../Discv4/Messages/EnrResponseMsg.cs | 2 +- .../Discv4/Messages/FindNodeMsg.cs | 2 +- .../Discv4/Messages/NeighborsMsg.cs | 2 +- .../Discv4/Messages/NodeIdResolver.cs | 2 +- .../Discv4/Messages/PingMsg.cs | 2 +- .../Discv4/Messages/PongMsg.cs | 2 +- .../Discv4/NodeSession.cs | 2 +- .../Discv4/NodeSourceToDiscV4Feeder.cs | 2 +- .../Serializers/EnrRequestMsgSerializer.cs | 2 +- .../Serializers/EnrResponseMsgSerializer.cs | 2 +- .../Serializers/FindNodeMsgSerializer.cs | 2 +- .../Serializers/NeighborsMsgSerializer.cs | 2 +- .../Discv4/Serializers/PongMsgSerializer.cs | 2 +- .../Discv5/Kademlia/KademliaAdapter.cs | 2 +- .../Discv5/Kademlia/KademliaModule.cs | 2 +- .../Discv5/Kademlia/NodeSource.cs | 2 +- .../Discv5/MessageCodec.cs | 30 +++-------- .../Discv5/Messages/RequestId.cs | 8 +++ .../Discv5/NettyDiscoveryV5Handler.cs | 2 +- .../Serializers/FindNodeMsgSerializer.cs | 15 +++--- .../Discv5/Serializers/MsgSerializerBase.cs | 54 ++++++++++++------- .../Discv5/Serializers/NodesMsgSerializer.cs | 11 ++-- .../Discv5/Serializers/PingMsgSerializer.cs | 15 +++--- .../Discv5/Serializers/PongMsgSerializer.cs | 12 ++--- .../Serializers/TalkReqMsgSerializer.cs | 11 ++-- .../Serializers/TalkRespMsgSerializer.cs | 15 +++--- .../Kademlia/DiscoveryPersistenceManager.cs | 2 +- .../Kademlia/IteratorNodeLookup.cs | 2 +- .../Kademlia/PublicKeyKeyOperator.cs | 2 +- .../NodeRecordProvider.cs | 2 +- .../NullDiscoveryApp.cs | 2 +- 41 files changed, 133 insertions(+), 121 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index fa8c89c01106..d11944eddc52 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Core.Test.Modules; @@ -201,6 +202,18 @@ public void MessageCodec_Roundtrips_FindNode() Assert.That(decodedFindNode.Distances, Is.EqualTo(message.Distances)); } + [TestCase(new byte[] { }, 1)] + [TestCase(new byte[] { 0x7f }, 1)] + [TestCase(new byte[] { 0x80 }, 2)] + [TestCase(new byte[] { 0x00, 0x01 }, 3)] + [TestCase(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }, 9)] + public void RequestId_GetRlpLength_ShouldMatchByteStringRules(byte[] requestId, int expectedLength) + { + RequestId value = RequestId.From(requestId); + + Assert.That(value.GetRlpLength(), Is.EqualTo(expectedLength)); + } + [Test] public void MessageCodec_Roundtrips_Pong() { @@ -223,7 +236,9 @@ public void MessageCodec_Roundtrips_TalkReq() using TalkReqMsg message = new([0, 0, 0, 3], "eth"u8.ToArray(), new byte[] { 1, 2, 3, 4 }); using NettyRlpStream encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.DecodeCopied(encoded.AsSpan()); + ArrayPoolSpan owner = new(encoded.AsSpan().Length); + encoded.AsSpan().CopyTo(owner); + using Discv5Message decoded = MessageCodec.DecodeOwned(owner.AsReadOnlyMemory(), owner); Assert.That(decoded, Is.InstanceOf()); TalkReqMsg decodedTalkReq = (TalkReqMsg)decoded; @@ -238,7 +253,9 @@ public void MessageCodec_Roundtrips_TalkResp() using TalkRespMsg message = new([0, 0, 0, 4], new byte[] { 5, 6, 7, 8 }); using NettyRlpStream encoded = MessageCodec.Encode(message); - using Discv5Message decoded = MessageCodec.DecodeCopied(encoded.AsSpan()); + ArrayPoolSpan owner = new(encoded.AsSpan().Length); + encoded.AsSpan().CopyTo(owner); + using Discv5Message decoded = MessageCodec.DecodeOwned(owner.AsReadOnlyMemory(), owner); Assert.That(decoded, Is.InstanceOf()); TalkRespMsg decodedTalkResp = (TalkRespMsg)decoded; diff --git a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs index 5853ff9a2f97..ab643030b362 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/CompositeDiscoveryApp.cs @@ -20,7 +20,7 @@ namespace Nethermind.Network.Discovery; /// /// Combines several protocol versions under a single implementation. /// -public class CompositeDiscoveryApp : IDiscoveryApp +public sealed class CompositeDiscoveryApp : IDiscoveryApp { private readonly INetworkConfig _networkConfig; private readonly IConnectionsPool _connections; diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs index 103a270d7e78..29efb02c0162 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs @@ -13,7 +13,7 @@ namespace Nethermind.Network.Discovery; /// Manages connections (Netty ) allocated for all Discovery protocol versions. /// /// Not thread-safe -public class DiscoveryConnectionsPool(ILogger logger, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig) : IConnectionsPool +public sealed class DiscoveryConnectionsPool(ILogger logger, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig) : IConnectionsPool { private readonly ILogger _logger = logger; private readonly INetworkConfig _networkConfig = networkConfig; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs index d7353c6764dc..1adf86076f9f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs @@ -6,7 +6,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; -public class EnrResponseHandler(EnrRequestMsg request) : ITaskCompleter +public sealed class EnrResponseHandler(EnrRequestMsg request) : ITaskCompleter { public TaskCompletionSource> TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs index 696c97a63417..3d01ff2e831f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs @@ -6,7 +6,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; -public class NeighbourMsgHandler(int k) : ITaskCompleter +public sealed class NeighbourMsgHandler(int k) : ITaskCompleter { private Node[] _current = []; public TaskCompletionSource> TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs index 4a696249c9a3..9c4617752662 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs @@ -6,7 +6,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; -public class PongMsgHandler(PingMsg ping) : ITaskCompleter +public sealed class PongMsgHandler(PingMsg ping) : ITaskCompleter { public TaskCompletionSource> TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index 9499f11546c2..eaaeed4082ec 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -15,7 +15,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; -public class KademliaAdapter( +public sealed class KademliaAdapter( Lazy> kademlia, // Cyclic dependency Lazy> nodeHealthTracker, IDiscoveryConfig discoveryConfig, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index 6e81bb26971f..8c682a6b3dd3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -17,7 +17,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; /// /// /// -public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) +public sealed class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index f5f07f68a497..87ffb314b65c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -12,7 +12,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; -public class NodeSource( +public sealed class NodeSource( IKademlia kademlia, IIteratorNodeLookup lookup, IKademliaAdapter discv4Adapter, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs index a1fbfc111756..3ba850dac39e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; /// /// https://eips.ethereum.org/EIPS/eip-868 /// -public class EnrRequestMsg : DiscoveryMsg +public sealed class EnrRequestMsg : DiscoveryMsg { public override MsgType MsgType => MsgType.EnrRequest; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs index 294618356a22..901eef6ffa74 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrResponseMsg.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; /// /// https://eips.ethereum.org/EIPS/eip-868 /// -public class EnrResponseMsg : DiscoveryMsg +public sealed class EnrResponseMsg : DiscoveryMsg { private const long MaxTime = long.MaxValue; // non-expiring message diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs index fbf17bcee903..e35ce4f8ebb4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/FindNodeMsg.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; -public class FindNodeMsg : DiscoveryMsg +public sealed class FindNodeMsg : DiscoveryMsg { public byte[] SearchedNodeId { get; set; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs index eb080a5cdcce..a1b59cdfcef7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NeighborsMsg.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; -public class NeighborsMsg : DiscoveryMsg +public sealed class NeighborsMsg : DiscoveryMsg { public ArraySegment Nodes { get; init; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs index a0178a2d9335..64e82798f8c0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/NodeIdResolver.cs @@ -6,7 +6,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; -public class NodeIdResolver(IEcdsa ecdsa) : INodeIdResolver +public sealed class NodeIdResolver(IEcdsa ecdsa) : INodeIdResolver { private readonly IEcdsa _ecdsa = ecdsa; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs index da0e37ba6159..5600acf224a4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; -public class PingMsg : DiscoveryMsg +public sealed class PingMsg : DiscoveryMsg { public IPEndPoint SourceAddress { get; } public IPEndPoint DestinationAddress { get; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs index 6de0b77113b7..ec7298634972 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv4.Messages; -public class PongMsg : DiscoveryMsg +public sealed class PongMsg : DiscoveryMsg { public byte[] PingMdc { get; init; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs index 0a583e4c32c8..d56802cc09d0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSession.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Discv4; -public record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) +public sealed record NodeSession(INodeStats NodeStats, ITimestamper Timestamper) { public static readonly TimeSpan BondTimeout = TimeSpan.FromHours(12); public static readonly TimeSpan PingRetryTimeout = TimeSpan.FromMinutes(10); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs index e94298761ec3..c63b5a6810d9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NodeSourceToDiscV4Feeder.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv4; -public class NodeSourceToDiscV4Feeder([KeyFilter(NodeSourceToDiscV4Feeder.SourceKey)] INodeSource nodeSource, IDiscoveryApp discoveryApp, IProcessExitSource exitSource, int maxNodes = 50) +public sealed class NodeSourceToDiscV4Feeder([KeyFilter(NodeSourceToDiscV4Feeder.SourceKey)] INodeSource nodeSource, IDiscoveryApp discoveryApp, IProcessExitSource exitSource, int maxNodes = 50) { public const string SourceKey = "Enr"; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs index 5b0eb032adef..d22586c05e67 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv4.Serializers; -public class EnrRequestMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer +public sealed class EnrRequestMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { public void Serialize(IByteBuffer byteBuffer, EnrRequestMsg msg) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs index 4cafc69140ba..5f2f3bdd397b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrResponseMsgSerializer.cs @@ -12,7 +12,7 @@ namespace Nethermind.Network.Discovery.Discv4.Serializers; -public class EnrResponseMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer +public sealed class EnrResponseMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { private readonly NodeRecordSigner _nodeRecordSigner = new(ecdsa, nodeKey.Generate()); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs index 3bb7867432fc..342e74495219 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/FindNodeMsgSerializer.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv4.Serializers; -public class FindNodeMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer +public sealed class FindNodeMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { public void Serialize(IByteBuffer byteBuffer, FindNodeMsg msg) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs index 558fafc54706..de1eff7f8dd3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs @@ -12,7 +12,7 @@ namespace Nethermind.Network.Discovery.Discv4.Serializers; -public class NeighborsMsgSerializer( +public sealed class NeighborsMsgSerializer( IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs index 51b277a28214..96337201147a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv4.Serializers; -public class PongMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer +public sealed class PongMsgSerializer(IEcdsa ecdsa, [KeyFilter(IProtectedPrivateKey.NodeKey)] IPrivateKeyGenerator nodeKey, INodeIdResolver nodeIdResolver) : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { public void Serialize(IByteBuffer byteBuffer, PongMsg msg) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 3a659024540a..63592ccdd017 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -19,7 +19,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Maps discv5 FINDNODE distance requests onto the protocol-specific Kademlia table. /// -public class KademliaAdapter( +public sealed class KademliaAdapter( Lazy> kademlia, // Cyclic dependency: Kademlia uses this adapter as its message sender. NettyDiscoveryV5Handler discoveryHandler, PacketCodec packetCodec, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs index 843fb207a6a4..d6facf4bf57b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs @@ -14,7 +14,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Specifies the protocol-specific Kademlia services used by discv5. /// -public class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) +public sealed class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder .AddSingleton() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index 1eedd707eb17..5d63dd99a0d5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -13,7 +13,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; -public class NodeSource( +public sealed class NodeSource( IKademlia kademlia, KademliaConfig kademliaConfig, ILogManager logManager) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs index 1269265b7168..766f176cd337 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs @@ -40,7 +40,7 @@ public static Discv5Message Decode(ReadOnlySpan message) { if (NeedsOwnedMessage(message)) { - throw new RlpException("discv5 TALK messages require owned message memory. Use DecodeOwned or DecodeCopied."); + throw new RlpException("discv5 TALK messages require owned message memory. Use DecodeOwned."); } return Decode(message, default, null); @@ -49,21 +49,6 @@ public static Discv5Message Decode(ReadOnlySpan message) public static Discv5Message DecodeOwned(ReadOnlyMemory message, ArrayPoolSpan owner) => Decode(message.Span, message, owner); - public static Discv5Message DecodeCopied(ReadOnlySpan message) - { - ArrayPoolSpan owner = new(message.Length); - try - { - message.CopyTo(owner); - return DecodeOwned(owner.AsReadOnlyMemory(), owner); - } - catch - { - owner.Dispose(); - throw; - } - } - private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { if (message.IsEmpty) @@ -79,15 +64,14 @@ private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory PingSerializer.Deserialize(requestId, ref ctx, owner), - MessageType.Pong => PongSerializer.Deserialize(requestId, ref ctx, owner), - MessageType.FindNode => FindNodeSerializer.Deserialize(requestId, ref ctx, owner), - MessageType.Nodes => NodesSerializer.Deserialize(requestId, ref ctx, owner), - MessageType.TalkReq => TalkReqSerializer.Deserialize(requestId, ref ctx, ownedMessage, owner), - MessageType.TalkResp => TalkRespSerializer.Deserialize(requestId, ref ctx, ownedMessage, owner), + MessageType.Ping => PingSerializer.Deserialize(ref ctx, ownedMessage, owner), + MessageType.Pong => PongSerializer.Deserialize(ref ctx, ownedMessage, owner), + MessageType.FindNode => FindNodeSerializer.Deserialize(ref ctx, ownedMessage, owner), + MessageType.Nodes => NodesSerializer.Deserialize(ref ctx, ownedMessage, owner), + MessageType.TalkReq => TalkReqSerializer.Deserialize(ref ctx, ownedMessage, owner), + MessageType.TalkResp => TalkRespSerializer.Deserialize(ref ctx, ownedMessage, owner), _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") }; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs index 8130544c6e23..75818f33d75b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Serialization.Rlp; + namespace Nethermind.Network.Discovery.Discv5.Messages; internal readonly record struct RequestId(ulong Value, byte Length) @@ -35,6 +37,12 @@ public void CopyTo(Span destination) } } + public int GetRlpLength() + { + byte firstByte = Length == 0 ? (byte)0 : (byte)(Value >> ((Length - 1) * 8)); + return Rlp.LengthOfByteString(Length, firstByte); + } + public byte[] ToArray() { byte[] bytes = new byte[Length]; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index a5de07eadb88..7799361c3e9d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -17,7 +17,7 @@ namespace Nethermind.Network.Discovery.Discv5; /// /// DotNetty UDP bridge used by the native discv5 implementation. /// -public class NettyDiscoveryV5Handler(ILogManager loggerManager, IChannel? channel = null) : NettyDiscoveryBaseHandler(loggerManager, channel) +public sealed class NettyDiscoveryV5Handler(ILogManager loggerManager, IChannel? channel = null) : NettyDiscoveryBaseHandler(loggerManager, channel) { private const int MaxMessagesBuffered = 1024; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs index a2d4a9dcd3ad..27b6e3b80f02 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs @@ -7,18 +7,15 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class FindNodeMsgSerializer : MsgSerializerBase +internal sealed class FindNodeMsgSerializer : MsgSerializerBase { - public int GetContentLength(FindNodeMsg msg) - => GetRequestIdLength(msg.RequestId) + GetDistancesLength(msg.Distances); + protected override int GetContentLengthCore(FindNodeMsg msg) + => GetDistancesLength(msg.Distances); - public void Serialize(NettyRlpStream stream, FindNodeMsg msg) - { - EncodeRequestId(stream, msg.RequestId); - EncodeDistances(stream, msg.Distances); - } + protected override void SerializeCore(NettyRlpStream stream, FindNodeMsg msg) + => EncodeDistances(stream, msg.Distances); - public FindNodeMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + protected override FindNodeMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, DecodeDistances(ref ctx), owner); private static int GetDistancesLength(Distances distances) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs index 6b738f738fa3..9b45a688034b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs @@ -1,38 +1,36 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Core.Collections; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal abstract class MsgSerializerBase +internal abstract class MsgSerializerBase + where TMessage : Discv5Message { - internal static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) - { - ReadOnlySpan requestId = ctx.DecodeByteArraySpan(); - if (requestId.Length > RequestId.MaxLength) - { - throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {RequestId.MaxLength}."); - } - - return RequestId.From(requestId); - } + public int GetContentLength(TMessage msg) + => msg.RequestId.GetRlpLength() + GetContentLengthCore(msg); - protected static int GetRequestIdLength(RequestId requestId) + public void Serialize(NettyRlpStream stream, TMessage msg) { - Span bytes = stackalloc byte[RequestId.MaxLength]; - requestId.CopyTo(bytes); - return Rlp.LengthOf(bytes[..requestId.Length]); + EncodeRequestId(stream, msg.RequestId); + SerializeCore(stream, msg); } - protected static void EncodeRequestId(NettyRlpStream stream, RequestId requestId) + public TMessage Deserialize(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { - Span bytes = stackalloc byte[RequestId.MaxLength]; - requestId.CopyTo(bytes); - stream.Encode(bytes[..requestId.Length]); + RequestId requestId = DecodeRequestId(ref ctx); + return DeserializeCore(requestId, ref ctx, ownedMessage, owner); } + protected abstract int GetContentLengthCore(TMessage msg); + + protected abstract void SerializeCore(NettyRlpStream stream, TMessage msg); + + protected abstract TMessage DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner); + protected static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) { ReadOnlySpan value = ctx.DecodeByteArraySpan(); @@ -47,4 +45,22 @@ protected static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderConte protected static void Encode(NettyRlpStream stream, ulong value) => stream.Encode(value); protected static void Encode(NettyRlpStream stream, int value) => stream.Encode(value); + + private static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) + { + ReadOnlySpan requestId = ctx.DecodeByteArraySpan(); + if (requestId.Length > RequestId.MaxLength) + { + throw new RlpException($"discv5 request-id length {requestId.Length} exceeds {RequestId.MaxLength}."); + } + + return RequestId.From(requestId); + } + + private static void EncodeRequestId(NettyRlpStream stream, RequestId requestId) + { + Span bytes = stackalloc byte[RequestId.MaxLength]; + requestId.CopyTo(bytes); + stream.Encode(bytes[..requestId.Length]); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index e6371c453f5b..20c67fc18cbf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -10,21 +10,20 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class NodesMsgSerializer : MsgSerializerBase +internal sealed class NodesMsgSerializer : MsgSerializerBase { private readonly IEcdsa _ecdsa = new Ecdsa(); - public int GetContentLength(NodesMsg msg) - => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Total) + GetNodeRecordsLength(msg.Records); + protected override int GetContentLengthCore(NodesMsg msg) + => Rlp.LengthOf(msg.Total) + GetNodeRecordsLength(msg.Records); - public void Serialize(NettyRlpStream stream, NodesMsg msg) + protected override void SerializeCore(NettyRlpStream stream, NodesMsg msg) { - EncodeRequestId(stream, msg.RequestId); Encode(stream, msg.Total); EncodeNodeRecords(stream, msg.Records); } - public NodesMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + protected override NodesMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { int total = ctx.DecodePositiveInt(); return new NodesMsg(requestId, total, DecodeNodeRecords(ref ctx), owner); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs index c021b5fb6cf4..4b4c30a05f17 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs @@ -7,17 +7,14 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class PingMsgSerializer : MsgSerializerBase +internal sealed class PingMsgSerializer : MsgSerializerBase { - public int GetContentLength(PingMsg msg) - => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.EnrSequence); + protected override int GetContentLengthCore(PingMsg msg) + => Rlp.LengthOf(msg.EnrSequence); - public void Serialize(NettyRlpStream stream, PingMsg msg) - { - EncodeRequestId(stream, msg.RequestId); - Encode(stream, msg.EnrSequence); - } + protected override void SerializeCore(NettyRlpStream stream, PingMsg msg) + => Encode(stream, msg.EnrSequence); - public PingMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + protected override PingMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, ctx.DecodeULong(), owner); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs index 0472ff8e1ae6..c8e1d26ae1f1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -9,23 +9,21 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class PongMsgSerializer : MsgSerializerBase +internal sealed class PongMsgSerializer : MsgSerializerBase { - public int GetContentLength(PongMsg msg) - => GetRequestIdLength(msg.RequestId) + - Rlp.LengthOf(msg.EnrSequence) + + protected override int GetContentLengthCore(PongMsg msg) + => Rlp.LengthOf(msg.EnrSequence) + IPAddressRlp.GetLength(msg.RecipientIp) + Rlp.LengthOf(msg.RecipientPort); - public void Serialize(NettyRlpStream stream, PongMsg msg) + protected override void SerializeCore(NettyRlpStream stream, PongMsg msg) { - EncodeRequestId(stream, msg.RequestId); Encode(stream, msg.EnrSequence); IPAddressRlp.Encode(stream, msg.RecipientIp); Encode(stream, msg.RecipientPort); } - public PongMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ArrayPoolSpan? owner) + protected override PongMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { ulong enrSequence = ctx.DecodeULong(); IPAddress recipientIp = new(ctx.DecodeByteArraySpan()); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs index a7aa88f36804..abb43f9da685 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs @@ -7,18 +7,17 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class TalkReqMsgSerializer : MsgSerializerBase +internal sealed class TalkReqMsgSerializer : MsgSerializerBase { - public int GetContentLength(TalkReqMsg msg) - => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Protocol) + Rlp.LengthOf(msg.Request); + protected override int GetContentLengthCore(TalkReqMsg msg) + => Rlp.LengthOf(msg.Protocol) + Rlp.LengthOf(msg.Request); - public void Serialize(NettyRlpStream stream, TalkReqMsg msg) + protected override void SerializeCore(NettyRlpStream stream, TalkReqMsg msg) { - EncodeRequestId(stream, msg.RequestId); stream.Encode(msg.Protocol); stream.Encode(msg.Request); } - public TalkReqMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override TalkReqMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, DecodeByteMemory(ref ctx, ownedMessage), DecodeByteMemory(ref ctx, ownedMessage), owner); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs index f50bf06cc110..4519c43d6321 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs @@ -7,17 +7,14 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class TalkRespMsgSerializer : MsgSerializerBase +internal sealed class TalkRespMsgSerializer : MsgSerializerBase { - public int GetContentLength(TalkRespMsg msg) - => GetRequestIdLength(msg.RequestId) + Rlp.LengthOf(msg.Response); + protected override int GetContentLengthCore(TalkRespMsg msg) + => Rlp.LengthOf(msg.Response); - public void Serialize(NettyRlpStream stream, TalkRespMsg msg) - { - EncodeRequestId(stream, msg.RequestId); - stream.Encode(msg.Response); - } + protected override void SerializeCore(NettyRlpStream stream, TalkRespMsg msg) + => stream.Encode(msg.Response); - public TalkRespMsg Deserialize(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override TalkRespMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs index 1405ad10181c..5705ea92afea 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs @@ -26,7 +26,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Configuration for the discovery process. /// Log manager for logging events. /// Thrown if any required parameter is null. -public class DiscoveryPersistenceManager( +public sealed class DiscoveryPersistenceManager( [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage discoveryStorage, INodeStatsManager nodeStatsManager, IKademliaMessageSender messageSender, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs index 2995b8ca2d2c..4ce9afa45813 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs @@ -19,7 +19,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// is to reach all node. The lookup is not parallelized as it is expected to be parallelized at a higher level with /// each worker having different target to look into. /// -public class IteratorNodeLookup( +public sealed class IteratorNodeLookup( IRoutingTable routingTable, KademliaConfig kademliaConfig, IKademliaMessageSender msgSender, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs index 8f398e6efcaa..c8384a8b0d12 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Kademlia; -public class PublicKeyKeyOperator : IKeyOperator +public sealed class PublicKeyKeyOperator : IKeyOperator { public PublicKey GetKey(Node node) => node.Id; diff --git a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs index 2ae6832dbced..1429e5734dfd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NodeRecordProvider.cs @@ -8,7 +8,7 @@ namespace Nethermind.Network.Discovery; -public class NodeRecordProvider( +public sealed class NodeRecordProvider( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, IIPResolver ipResolver, IEthereumEcdsa ethereumEcdsa, diff --git a/src/Nethermind/Nethermind.Network.Discovery/NullDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/NullDiscoveryApp.cs index b488ef6b142b..d0abeb0c4358 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/NullDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/NullDiscoveryApp.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery; -public class NullDiscoveryApp : IDiscoveryApp +public sealed class NullDiscoveryApp : IDiscoveryApp { public void Initialize(PublicKey masterPublicKey) { From 8d91fb8cbc16a5d62241d7ed1234cf2791b40693 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 8 Jun 2026 11:12:17 +0300 Subject: [PATCH 150/182] Better naming for double lru --- .../Nethermind.Kademlia/DoubleEndedLru.cs | 64 +++++++++---------- src/Nethermind/Nethermind.Kademlia/KBucket.cs | 14 ++-- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 51b9fb84c36c..57aa0661fb66 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -5,24 +5,24 @@ namespace Nethermind.Kademlia; -public class DoubleEndedLru(int capacity) - where TNode : notnull - where TKadKey : notnull +public class DoubleEndedLru(int capacity) + where TKey : notnull + where TValue : notnull { private readonly object _lock = new(); - private readonly LinkedList<(TKadKey, TNode)> _queue = new(); - private readonly ConcurrentDictionary> _hashMapping = new(); + private readonly LinkedList<(TKey Key, TValue Value)> _queue = new(); + private readonly ConcurrentDictionary> _index = new(); public int Count => _queue.Count; - public BucketAddResult AddOrRefresh(in TKadKey hash, TNode node) + public BucketAddResult AddOrRefresh(in TKey key, TValue value) { lock (_lock) { - if (_hashMapping.TryGetValue(hash, out LinkedListNode<(TKadKey, TNode)>? listNode)) + if (_index.TryGetValue(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) { _queue.Remove(listNode); - listNode.Value = (hash, node); + listNode.Value = (key, value); _queue.AddFirst(listNode); return BucketAddResult.Refreshed; } @@ -32,54 +32,54 @@ public BucketAddResult AddOrRefresh(in TKadKey hash, TNode node) return BucketAddResult.Full; } - listNode = _queue.AddFirst((hash, node)); - _hashMapping.TryAdd(hash, listNode); + listNode = _queue.AddFirst((key, value)); + _index.TryAdd(key, listNode); return BucketAddResult.Added; } } - public bool TryPopHead(out TKadKey hash, out TNode? node) + public bool TryPopHead(out TKey key, out TValue? value) { lock (_lock) { - LinkedListNode<(TKadKey, TNode)>? front = _queue.First; + LinkedListNode<(TKey Key, TValue Value)>? front = _queue.First; if (front == null) { - hash = default!; - node = default; + key = default!; + value = default; return false; } _queue.Remove(front); - hash = front.Value.Item1; - node = front.Value.Item2; - _hashMapping.TryRemove(front.Value.Item1, out front); + key = front.Value.Key; + value = front.Value.Value; + _index.TryRemove(front.Value.Key, out front); return true; } } - public bool TryGetLast(out TNode? last) + public bool TryGetLast(out TValue? last) { lock (_lock) { - LinkedListNode<(TKadKey, TNode)>? lastNode = _queue.Last; + LinkedListNode<(TKey Key, TValue Value)>? lastNode = _queue.Last; if (lastNode == null) { last = default; return false; } - last = lastNode.Value.Item2; + last = lastNode.Value.Value; return true; } } - public bool Remove(TKadKey hash) + public bool Remove(TKey key) { lock (_lock) { - if (_hashMapping.TryRemove(hash, out LinkedListNode<(TKadKey, TNode)>? listNode)) + if (_index.TryRemove(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) { _queue.Remove(listNode); return true; @@ -89,35 +89,35 @@ public bool Remove(TKadKey hash) } } - public TNode[] GetAll() + public TValue[] GetAll() { lock (_lock) { - TNode[] result = new TNode[_queue.Count]; + TValue[] result = new TValue[_queue.Count]; int i = 0; - foreach ((TKadKey, TNode node) entry in _queue) result[i++] = entry.node; + foreach ((TKey Key, TValue Value) entry in _queue) result[i++] = entry.Value; return result; } } - public (TKadKey, TNode)[] GetAllWithHash() + public (TKey Key, TValue Value)[] GetAllWithKey() { lock (_lock) { - (TKadKey, TNode)[] result = new (TKadKey, TNode)[_queue.Count]; + (TKey Key, TValue Value)[] result = new (TKey Key, TValue Value)[_queue.Count]; int i = 0; - foreach ((TKadKey, TNode) entry in _queue) result[i++] = entry; + foreach ((TKey Key, TValue Value) entry in _queue) result[i++] = entry; return result; } } - public bool Contains(in TKadKey hash) => _hashMapping.ContainsKey(hash); + public bool Contains(in TKey key) => _index.ContainsKey(key); - public TNode? GetByHash(TKadKey hash) + public TValue? GetByKey(TKey key) { - if (_hashMapping.TryGetValue(hash, out LinkedListNode<(TKadKey, TNode)>? listNode)) + if (_index.TryGetValue(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) { - return listNode.Value.Item2; + return listNode.Value.Value; } return default; diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index 9b7f9d17c99e..b19981cae0b7 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -9,8 +9,8 @@ public class KBucket(int k) where TKadKey : notnull { private readonly int _k = k; - private DoubleEndedLru _items = new(k); - private DoubleEndedLru _replacement = new(k); + private DoubleEndedLru _items = new(k); + private DoubleEndedLru _replacement = new(k); public int Count => _items.Count; @@ -25,7 +25,7 @@ public class KBucket(int k) /// public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? toRefresh) { - TNode? previous = _items.GetByHash(hash); + TNode? previous = _items.GetByKey(hash); BucketAddResult addResult = _items.AddOrRefresh(hash, item); if (addResult == BucketAddResult.Added || (addResult == BucketAddResult.Refreshed && ShouldUpdateCachedArray(previous, item))) @@ -47,7 +47,7 @@ public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? t public TNode[] GetAll() => _cachedArray; - public (TKadKey, TNode)[] GetAllWithHash() => _items.GetAllWithHash(); + public (TKadKey, TNode)[] GetAllWithHash() => _items.GetAllWithKey(); public bool RemoveAndReplace(in TKadKey hash) { @@ -64,14 +64,14 @@ public bool RemoveAndReplace(in TKadKey hash) public void Clear() { - _items = new DoubleEndedLru(_k); - _replacement = new DoubleEndedLru(_k); + _items = new DoubleEndedLru(_k); + _replacement = new DoubleEndedLru(_k); _cachedArray = _items.GetAll(); } public bool ContainsNode(in TKadKey hash) => _items.Contains(hash); - public TNode? GetByHash(TKadKey hash) => _items.GetByHash(hash); + public TNode? GetByHash(TKadKey hash) => _items.GetByKey(hash); private static bool ShouldUpdateCachedArray(TNode? previous, TNode item) { From 84a7beed3c9c6df5db2fbfe91db4039e816fa5e3 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 9 Jun 2026 13:40:08 +0300 Subject: [PATCH 151/182] Review --- .../Caching/LruCacheTests.cs | 60 ++++++- .../Nethermind.Core/Caching/LruCache.cs | 157 +++++++++++------- .../Nethermind.Crypto/SecP256k1Ecdh.cs | 2 + .../Nethermind.Kademlia/DoubleEndedLru.cs | 69 ++++++-- src/Nethermind/Nethermind.Kademlia/KBucket.cs | 14 +- .../Nethermind.Kademlia/KBucketTree.cs | 71 ++++++-- .../Nethermind.Kademlia/Kademlia.cs | 43 +++-- .../LookupKNearestNeighbour.cs | 90 +++++----- .../Nethermind.Kademlia.csproj | 4 - .../Nethermind.Kademlia/NodeHealthTracker.cs | 9 +- .../MicrosoftLoggerExtensions.cs | 20 --- .../NethermindLoggerFactory.cs | 20 +-- .../Discv5/NodeSourceTests.cs | 38 ++++- .../Kademlia/KademliaSimulation.cs | 21 +-- .../Discv4/DiscoveryApp.cs | 8 +- .../Kademlia/Handlers/NeighbourMsgHandler.cs | 43 +++-- .../Discv4/Kademlia/KademliaAdapter.cs | 8 +- .../Discv4/Kademlia/NodeSource.cs | 50 +++--- .../Discv4/NettyDiscoveryHandler.cs | 2 +- .../Discv5/Kademlia/AdapterState.cs | 15 +- .../Kademlia/Handlers/NodesResponseHandler.cs | 21 ++- .../Discv5/Kademlia/KademliaAdapter.cs | 134 ++++++++------- .../Discv5/Kademlia/NodeSource.cs | 5 + .../Discv5/Packets/Session.cs | 3 +- .../Kademlia/DiscoveryPersistenceManager.cs | 1 - .../Kademlia/PublicKeyKeyOperator.cs | 2 +- .../Kademlia/RecentNodeFilter.cs | 4 +- .../NodeRecordTests.cs | 6 +- .../Nethermind.Network.Enr/NodeRecord.cs | 2 +- .../NetworkStorageTests.cs | 88 ++++++++++ .../Nethermind.Network/NetworkStorage.cs | 35 +++- src/Nethermind/Nethermind.Network/PeerPool.cs | 1 - 32 files changed, 701 insertions(+), 345 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index 538e3342442e..512b9e300629 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -251,7 +251,8 @@ public void Can_remove_and_return_value() public void Eviction_callback_is_called_when_capacity_replaces_oldest() { int evicted = 0; - LruCache cache = new(2, "test", value => evicted = value); + LruCache cache = new(2, "test"); + cache.OnEvict += value => evicted = value; cache.Set(1, 10); cache.Set(2, 20); @@ -264,7 +265,8 @@ public void Eviction_callback_is_called_when_capacity_replaces_oldest() public void Eviction_callback_is_called_when_existing_value_is_replaced() { int evicted = 0; - LruCache cache = new(2, "test", value => evicted = value); + LruCache cache = new(2, "test"); + cache.OnEvict += value => evicted = value; cache.Set(1, 10); cache.Set(1, 11); @@ -277,7 +279,8 @@ public void Eviction_callback_is_called_when_existing_value_is_replaced() public void TryRemove_returns_value_without_calling_eviction_callback() { int evicted = 0; - LruCache cache = new(2, "test", value => evicted = value); + LruCache cache = new(2, "test"); + cache.OnEvict += value => evicted = value; cache.Set(1, 10); Assert.That(cache.TryRemove(1, out int removed), Is.True); @@ -317,7 +320,8 @@ public async Task Clear_invokes_eviction_callback_outside_lock() { LruCache cache = null!; TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); - cache = new LruCache(2, "test", _ => callbackResult.SetResult(cache.Contains(1))); + cache = new LruCache(2, "test"); + cache.OnEvict += _ => callbackResult.SetResult(cache.Contains(1)); cache.Set(1, 10); Task clearTask = Task.Run(cache.Clear); @@ -328,6 +332,29 @@ public async Task Clear_invokes_eviction_callback_outside_lock() Assert.That(await callbackResult.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.False); } + [TestCase(EvictionOperation.Delete, false)] + [TestCase(EvictionOperation.ReplaceExisting, true)] + [TestCase(EvictionOperation.ReplaceOldest, false)] + public async Task Eviction_callback_is_invoked_outside_lock(EvictionOperation operation, bool expectedContainsResult) + { + LruCache cache = null!; + TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); + cache = new LruCache(2, "test"); + cache.OnEvict += _ => callbackResult.SetResult(cache.Contains(1)); + cache.Set(1, 10); + if (operation == EvictionOperation.ReplaceOldest) + { + cache.Set(2, 20); + } + + Task operationTask = Task.Run(() => RunEvictionOperation(cache, operation)); + Task completedTask = await Task.WhenAny(operationTask, Task.Delay(TimeSpan.FromSeconds(5))); + + Assert.That(completedTask, Is.SameAs(operationTask)); + await operationTask; + Assert.That(await callbackResult.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.EqualTo(expectedContainsResult)); + } + [Test] public void Delete_keeps_internal_structure() { @@ -368,5 +395,30 @@ public void Wrong_capacity_number_at_constructor() }); } + + private static void RunEvictionOperation(LruCache cache, EvictionOperation operation) + { + switch (operation) + { + case EvictionOperation.Delete: + cache.Delete(1); + return; + case EvictionOperation.ReplaceExisting: + cache.Set(1, 11); + return; + case EvictionOperation.ReplaceOldest: + cache.Set(3, 30); + return; + default: + throw new ArgumentOutOfRangeException(nameof(operation), operation, null); + } + } + + public enum EvictionOperation + { + Delete, + ReplaceExisting, + ReplaceOldest + } } } diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 52479bccc0f8..02a5727da5ab 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -16,7 +16,6 @@ public sealed class LruCache : ICache where TKey : n private readonly Dictionary> _cacheMap; private readonly McsLock _lock = new(); private readonly string _name; - private readonly Action? _onEvict; private LinkedListNode? _leastRecentlyUsed; public LruCache(int maxCapacity, int startCapacity, string name, Action? onEvict = null) @@ -25,7 +24,11 @@ public LruCache(int maxCapacity, int startCapacity, string name, Action? _name = name; _maxCapacity = maxCapacity; - _onEvict = onEvict; + if (onEvict is not null) + { + OnEvict += onEvict; + } + _cacheMap = typeof(TKey) == typeof(byte[]) ? new Dictionary>((IEqualityComparer)Bytes.EqualityComparer) : new Dictionary>(startCapacity); // do not initialize it at the full capacity @@ -36,6 +39,8 @@ public LruCache(int maxCapacity, string name, Action? onEvict = null) { } + public event Action? OnEvict; + public void Clear() { TValue[]? evictedValues; @@ -90,71 +95,105 @@ public TValue SetOrGet(TKey key, TState state, Func? node)) - { - TValue value = node.Value.Value; - LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); - return value; - } - - TValue newValue = valueFactory(key, state); - if (newValue is null) + TValue evictedValue = default!; + bool notifyEviction = false; + TValue result; + using (McsLock.Disposable lockRelease = _lock.Acquire()) { - return newValue; + if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) + { + TValue value = node.Value.Value; + LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); + return value; + } + + TValue newValue = valueFactory(key, state); + if (newValue is null) + { + return newValue; + } + + if (_cacheMap.Count >= _maxCapacity) + { + evictedValue = Replace(key, newValue); + notifyEviction = true; + } + else + { + LinkedListNode newNode = new(new(key, newValue)); + LinkedListNode.AddMostRecent(ref _leastRecentlyUsed, newNode); + _cacheMap.Add(key, newNode); + } + + result = newValue; } - if (_cacheMap.Count >= _maxCapacity) - { - Replace(key, newValue); - } - else + if (notifyEviction) { - LinkedListNode newNode = new(new(key, newValue)); - LinkedListNode.AddMostRecent(ref _leastRecentlyUsed, newNode); - _cacheMap.Add(key, newNode); + NotifyEvicted(evictedValue); } - return newValue; + return result; } public bool Set(TKey key, TValue val) { - using McsLock.Disposable lockRelease = _lock.Acquire(); - - if (val is null) - { - return DeleteNoLock(key); - } - - if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) + TValue evictedValue = default!; + bool notifyEviction = false; + bool added; + using (McsLock.Disposable lockRelease = _lock.Acquire()) { - NotifyEvicted(node.Value.Value); - node.Value.Value = val; - LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); - return false; + if (val is null) + { + added = DeleteNoLock(key, out evictedValue); + notifyEviction = added; + } + else if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) + { + evictedValue = node.Value.Value; + notifyEviction = true; + node.Value.Value = val; + LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); + added = false; + } + else if (_cacheMap.Count >= _maxCapacity) + { + evictedValue = Replace(key, val); + notifyEviction = true; + added = true; + } + else + { + LinkedListNode newNode = new(new(key, val)); + LinkedListNode.AddMostRecent(ref _leastRecentlyUsed, newNode); + _cacheMap.Add(key, newNode); + added = true; + } } - if (_cacheMap.Count >= _maxCapacity) - { - Replace(key, val); - } - else + if (notifyEviction) { - LinkedListNode newNode = new(new(key, val)); - LinkedListNode.AddMostRecent(ref _leastRecentlyUsed, newNode); - _cacheMap.Add(key, newNode); + NotifyEvicted(evictedValue); } - return true; + return added; } public bool Delete(TKey key) { - using McsLock.Disposable lockRelease = _lock.Acquire(); + TValue evictedValue = default!; + bool removed; + using (McsLock.Disposable lockRelease = _lock.Acquire()) + { + removed = DeleteNoLock(key, out evictedValue); + } + + if (removed) + { + NotifyEvicted(evictedValue); + } - return DeleteNoLock(key); + return removed; } /// @@ -167,7 +206,7 @@ public bool TryRemove(TKey key, [MaybeNullWhen(false)] out TValue value) if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) { value = node.Value.Value; - RemoveNoLock(key, node, notifyEviction: false); + RemoveNoLock(key, node); return true; } @@ -175,24 +214,21 @@ public bool TryRemove(TKey key, [MaybeNullWhen(false)] out TValue value) return false; } - private bool DeleteNoLock(TKey key) + private bool DeleteNoLock(TKey key, out TValue evictedValue) { if (_cacheMap.TryGetValue(key, out LinkedListNode? node)) { - RemoveNoLock(key, node, notifyEviction: true); + evictedValue = node.Value.Value; + RemoveNoLock(key, node); return true; } + evictedValue = default!; return false; } - private void RemoveNoLock(TKey key, LinkedListNode node, bool notifyEviction) + private void RemoveNoLock(TKey key, LinkedListNode node) { - if (notifyEviction) - { - NotifyEvicted(node.Value.Value); - } - LinkedListNode.Remove(ref _leastRecentlyUsed, node); _cacheMap.Remove(key); } @@ -233,7 +269,7 @@ public TValue[] GetValues() public int Count => _cacheMap.Count; - private void Replace(TKey key, TValue value) + private TValue Replace(TKey key, TValue value) { LinkedListNode? node = _leastRecentlyUsed; if (node is null) @@ -247,7 +283,7 @@ private void Replace(TKey key, TValue value) node.Value = new(key, value); LinkedListNode.MoveToMostRecent(ref _leastRecentlyUsed, node); _cacheMap.Add(key, node); - NotifyEvicted(evictedValue); + return evictedValue; [DoesNotReturn] static void ThrowInvalidOperationException() => throw new InvalidOperationException( @@ -256,7 +292,7 @@ private void Replace(TKey key, TValue value) private TValue[]? GetEvictedValues() { - if (_onEvict is null || _cacheMap.Count == 0) + if (OnEvict is null || _cacheMap.Count == 0) { return null; } @@ -286,9 +322,10 @@ private void NotifyEvictedValues(TValue[]? evictedValues) private void NotifyEvicted(TValue value) { - if (_onEvict is not null && value is not null) + Action? onEvict = OnEvict; + if (onEvict is not null && value is not null) { - _onEvict(value); + onEvict(value); } } diff --git a/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs b/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs index c3b36a412e30..37ca297ce58e 100644 --- a/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs +++ b/src/Nethermind/Nethermind.Crypto/SecP256k1Ecdh.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using Nethermind.Core.Crypto; namespace Nethermind.Crypto; @@ -17,6 +18,7 @@ internal static unsafe class SecP256k1Ecdh private static readonly EcdhHashFunction CompressedPointHashFunction = WriteCompressedPoint; private static readonly SecP256k1ContextHandle Context = new(); + [SkipLocalsInit] internal static byte[] GetCompressedSharedPoint(ReadOnlySpan publicKey, ReadOnlySpan privateKey) { if (Context.IsInvalid) diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 57aa0661fb66..02d3a5c417e2 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using NonBlocking; +using System.Threading; namespace Nethermind.Kademlia; @@ -9,11 +9,20 @@ public class DoubleEndedLru(int capacity) where TKey : notnull where TValue : notnull { - private readonly object _lock = new(); + private readonly Lock _lock = new(); private readonly LinkedList<(TKey Key, TValue Value)> _queue = new(); - private readonly ConcurrentDictionary> _index = new(); - public int Count => _queue.Count; + private readonly Dictionary> _index = new(capacity); + public int Count + { + get + { + lock (_lock) + { + return _queue.Count; + } + } + } public BucketAddResult AddOrRefresh(in TKey key, TValue value) { @@ -33,7 +42,7 @@ public BucketAddResult AddOrRefresh(in TKey key, TValue value) } listNode = _queue.AddFirst((key, value)); - _index.TryAdd(key, listNode); + _index.Add(key, listNode); return BucketAddResult.Added; } } @@ -43,7 +52,7 @@ public bool TryPopHead(out TKey key, out TValue? value) lock (_lock) { LinkedListNode<(TKey Key, TValue Value)>? front = _queue.First; - if (front == null) + if (front is null) { key = default!; value = default; @@ -53,7 +62,7 @@ public bool TryPopHead(out TKey key, out TValue? value) _queue.Remove(front); key = front.Value.Key; value = front.Value.Value; - _index.TryRemove(front.Value.Key, out front); + _index.Remove(front.Value.Key); return true; } @@ -64,7 +73,7 @@ public bool TryGetLast(out TValue? last) lock (_lock) { LinkedListNode<(TKey Key, TValue Value)>? lastNode = _queue.Last; - if (lastNode == null) + if (lastNode is null) { last = default; return false; @@ -79,7 +88,7 @@ public bool Remove(TKey key) { lock (_lock) { - if (_index.TryRemove(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) + if (_index.Remove(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) { _queue.Remove(listNode); return true; @@ -95,7 +104,11 @@ public TValue[] GetAll() { TValue[] result = new TValue[_queue.Count]; int i = 0; - foreach ((TKey Key, TValue Value) entry in _queue) result[i++] = entry.Value; + foreach ((TKey Key, TValue Value) entry in _queue) + { + result[i++] = entry.Value; + } + return result; } } @@ -106,20 +119,44 @@ public TValue[] GetAll() { (TKey Key, TValue Value)[] result = new (TKey Key, TValue Value)[_queue.Count]; int i = 0; - foreach ((TKey Key, TValue Value) entry in _queue) result[i++] = entry; + foreach ((TKey Key, TValue Value) entry in _queue) + { + result[i++] = entry; + } + return result; } } - public bool Contains(in TKey key) => _index.ContainsKey(key); + internal int CopyAllWithKey((TKey Key, TValue Value)[] destination) + { + lock (_lock) + { + int i = 0; + foreach ((TKey Key, TValue Value) entry in _queue) + { + destination[i++] = entry; + } - public TValue? GetByKey(TKey key) + return i; + } + } + + public bool Contains(in TKey key) { - if (_index.TryGetValue(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) + lock (_lock) { - return listNode.Value.Value; + return _index.ContainsKey(key); } + } - return default; + public TValue? GetByKey(TKey key) + { + lock (_lock) + { + return _index.TryGetValue(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode) + ? listNode.Value.Value + : default; + } } } diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index b19981cae0b7..6f7c7af4df28 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -49,6 +49,8 @@ public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? t public (TKadKey, TNode)[] GetAllWithHash() => _items.GetAllWithKey(); + internal int CopyAllWithHash((TKadKey Hash, TNode Node)[] destination) => _items.CopyAllWithKey(destination); + public bool RemoveAndReplace(in TKadKey hash) { if (!_items.Remove(hash)) return false; @@ -74,14 +76,8 @@ public void Clear() public TNode? GetByHash(TKadKey hash) => _items.GetByKey(hash); private static bool ShouldUpdateCachedArray(TNode? previous, TNode item) - { - if (previous is null) - { - return false; - } - - return typeof(TNode).IsValueType + => previous is not null && + (typeof(TNode).IsValueType ? !EqualityComparer.Default.Equals(previous, item) - : !ReferenceEquals(previous, item); - } + : !ReferenceEquals(previous, item)); } diff --git a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index 3c42e35a2cb9..cba1bae3f017 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -1,7 +1,10 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Buffers; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -16,7 +19,7 @@ private class TreeNode(int k, TKadKey prefix) public TreeNode? Left { get; set; } public TreeNode? Right { get; set; } public TKadKey Prefix { get; } = prefix; - public bool IsLeaf => Left == null && Right == null; + public bool IsLeaf => Left is null && Right is null; } private readonly TreeNode _root; @@ -26,7 +29,7 @@ private class TreeNode(int k, TKadKey prefix) private readonly IKademliaDistance _distance; private readonly ILogger _logger; - private readonly object _lock = new(); + private readonly Lock _lock = new(); public KBucketTree( KademliaConfig config, @@ -175,31 +178,50 @@ public TNode[] GetAllAtDistance(int distance) lock (_lock) { if (_logger.IsDebug) _logger.Debug($"Getting all nodes at distance {distance}"); - List result = []; - GetAllAtDistanceRecursive(_root, 0, distance, result); - if (_logger.IsDebug) _logger.Debug($"Found {result.Count} nodes at distance {distance}"); - return [.. result]; + TNode[] result = ArrayPool.Shared.Rent(_k); + (TKadKey Hash, TNode Node)[] bucketEntries = ArrayPool<(TKadKey Hash, TNode Node)>.Shared.Rent(_k); + int count = 0; + try + { + GetAllAtDistanceRecursive(_root, 0, distance, ref result, ref count, bucketEntries); + if (_logger.IsDebug) _logger.Debug($"Found {count} nodes at distance {distance}"); + + TNode[] copy = new TNode[count]; + Array.Copy(result, copy, count); + return copy; + } + finally + { + ArrayPool<(TKadKey Hash, TNode Node)>.Shared.Return(bucketEntries, RuntimeHelpers.IsReferenceOrContainsReferences<(TKadKey Hash, TNode Node)>()); + ArrayPool.Shared.Return(result, RuntimeHelpers.IsReferenceOrContainsReferences()); + } } } - private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, List result) + private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, ref TNode[] result, ref int count, (TKadKey Hash, TNode Node)[] bucketEntries) { int targetDepth = _distance.MaxDistance - distance; if (node.IsLeaf) { if (depth <= targetDepth) { - foreach ((TKadKey hash, TNode item) in node.Bucket.GetAllWithHash()) + int entryCount = node.Bucket.CopyAllWithHash(bucketEntries); + for (int i = 0; i < entryCount; i++) { + (TKadKey hash, TNode item) = bucketEntries[i]; if (_distance.CalculateLogDistance(hash, _currentNodeHash) == distance) { - result.Add(item); + AddResult(item, ref result, ref count); } } } else { - result.AddRange(node.Bucket.GetAll()); + TNode[] items = node.Bucket.GetAll(); + for (int i = 0; i < items.Length; i++) + { + AddResult(items[i], ref result, ref count); + } } } else @@ -209,11 +231,11 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L bool goRight = _distance.GetBit(_currentNodeHash, depth); if (goRight) { - GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, ref result, ref count, bucketEntries); } else { - GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result); + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, ref result, ref count, bucketEntries); } } else if (depth == targetDepth) @@ -222,21 +244,34 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, L // Note: We go the opposite direction here, as the same direction would have a distance + 1 if (goRight) { - GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result); + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, ref result, ref count, bucketEntries); } else { - GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, ref result, ref count, bucketEntries); } } else { - GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result); - GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result); + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, ref result, ref count, bucketEntries); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, ref result, ref count, bucketEntries); } } } + private static void AddResult(TNode item, ref TNode[] result, ref int count) + { + if (count == result.Length) + { + TNode[] expanded = ArrayPool.Shared.Rent(Math.Max(1, result.Length * 2)); + Array.Copy(result, expanded, result.Length); + ArrayPool.Shared.Return(result, RuntimeHelpers.IsReferenceOrContainsReferences()); + result = expanded; + } + + result[count++] = item; + } + public IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> IterateBuckets() { lock (_lock) @@ -366,7 +401,7 @@ private void LogTreeStructureRecursive(TreeNode node, string indent, bool last, indent += "│ "; } - if (node.Left == null && node.Right == null) + if (node.Left is null && node.Right is null) { sb.AppendLine($"Bucket (Depth: {depth}, Count: {node.Bucket.Count})"); return; @@ -391,7 +426,7 @@ void TraverseTree(TreeNode node, int depth) totalNodes++; maxDepth = Math.Max(maxDepth, depth); - if (node.Left == null && node.Right == null) + if (node.Left is null && node.Right is null) { totalBuckets++; totalItems += node.Bucket.Count; diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index ba17d649a7be..6eea6384c87f 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -28,7 +30,7 @@ public class Kademlia : IKademlia private readonly IReadOnlyList _bootNodes; private readonly TimeProvider _timeProvider; private readonly Dictionary _lastBucketRefreshTicks = []; - private readonly object _lastBucketRefreshLock = new(); + private readonly Lock _lastBucketRefreshLock = new(); /// /// Creates a Kademlia table over the supplied routing, lookup, health, and transport abstractions. @@ -154,7 +156,13 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => // Refresh stale non-empty buckets one by one. Protocols whose wire lookup target cannot be synthesized from a // bucket prefix may return a best-effort random lookup key here; discv4 public keys are one example. - HashSet activeBucketPrefixes = []; + int activeBucketPrefixCapacity; + lock (_lastBucketRefreshLock) + { + activeBucketPrefixCapacity = _lastBucketRefreshTicks.Count; + } + + PooledHashSet activeBucketPrefixes = new(activeBucketPrefixCapacity); foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { activeBucketPrefixes.Add(Prefix); @@ -195,24 +203,31 @@ private void PruneLastBucketRefreshTicks(HashSet activeBucketPrefixes) { lock (_lastBucketRefreshLock) { - List? stalePrefixes = null; - foreach (TKadKey prefix in _lastBucketRefreshTicks.Keys) + TKadKey[] stalePrefixes = ArrayPool.Shared.Rent(_lastBucketRefreshTicks.Count); + int stalePrefixCount = 0; + try { - if (!activeBucketPrefixes.Contains(prefix)) + foreach (TKadKey prefix in _lastBucketRefreshTicks.Keys) { - stalePrefixes ??= []; - stalePrefixes.Add(prefix); + if (!activeBucketPrefixes.Contains(prefix)) + { + stalePrefixes[stalePrefixCount++] = prefix; + } } - } - if (stalePrefixes is null) - { - return; - } + if (stalePrefixCount == 0) + { + return; + } - for (int i = 0; i < stalePrefixes.Count; i++) + for (int i = 0; i < stalePrefixCount; i++) + { + _lastBucketRefreshTicks.Remove(stalePrefixes[i]); + } + } + finally { - _lastBucketRefreshTicks.Remove(stalePrefixes[i]); + ArrayPool.Shared.Return(stalePrefixes, RuntimeHelpers.IsReferenceOrContainsReferences()); } } } diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 90e84c07bc16..d692efe20446 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; +using System.Threading; using Nethermind.Logging; -using NonBlocking; namespace Nethermind.Kademlia; @@ -51,7 +52,7 @@ CancellationToken token IComparer comparerReverse = Comparer.Create((h1, h2) => distance.Compare(h2, h1, targetHash)); - object queueLock = new(); + Lock queueLock = new(); // Ordered by lowest distance. Will get popped for next round. PriorityQueue<(TKadKey, TNode), TKadKey> bestSeen = new(comparer); @@ -77,53 +78,57 @@ CancellationToken token int queryingTask = 0; bool finished = false; - Task[] worker = [.. Enumerable.Range(0, config.Alpha).Select((i) => Task.Run(async () => + Task[] worker = new Task[config.Alpha]; + for (int i = 0; i < worker.Length; i++) { - while (!Volatile.Read(ref finished)) + worker[i] = Task.Run(async () => { - token.ThrowIfCancellationRequested(); - if (!TryGetNodeToQuery(out (TKadKey hash, TNode node)? toQuery)) + while (!Volatile.Read(ref finished)) { - if (queryingTask > 0) + token.ThrowIfCancellationRequested(); + if (!TryGetNodeToQuery(out (TKadKey hash, TNode node)? toQuery)) { - // Need to wait for all querying tasks first here. - await Task.WhenAny(Volatile.Read(ref roundComplete).Task, Task.Delay(100, token)); - continue; - } - - // No node to query and running query. - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); - break; - } + if (queryingTask > 0) + { + // Need to wait for all querying tasks first here. + await Task.WhenAny(Volatile.Read(ref roundComplete).Task, Task.Delay(100, token)); + continue; + } - try - { - if (ShouldStopDueToNoBetterResult(out int round)) - { - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); + // No node to query and running query. + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); break; } - queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); - (TNode, TNode[]? neighbours)? result = await WrappedFindNeighbourOp(toQuery.Value.node); - if (result == null) continue; + try + { + if (ShouldStopDueToNoBetterResult(out int round)) + { + if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); + break; + } + + queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); + (TNode, TNode[]? neighbours)? result = await WrappedFindNeighbourOp(toQuery.Value.node); + if (result is null) continue; - ProcessResult(toQuery.Value.hash, toQuery.Value.node, result, round); - } - finally - { - Interlocked.Decrement(ref queryingTask); - TaskCompletionSource current = Volatile.Read(ref roundComplete); - if (current.TrySetResult()) + ProcessResult(toQuery.Value.hash, toQuery.Value.node, result, round); + } + finally { - Interlocked.CompareExchange( - ref roundComplete, - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously), - current); + Interlocked.Decrement(ref queryingTask); + TaskCompletionSource current = Volatile.Read(ref roundComplete); + if (current.TrySetResult()) + { + Interlocked.CompareExchange( + ref roundComplete, + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously), + current); + } } } - } - }, token))]; + }, token); + } // When any of the worker is finished, we consider the whole query as done. // This prevent this operation from hanging on a timed out request @@ -201,7 +206,7 @@ void ProcessResult(TKadKey hash, TNode toQuery, (TNode, TNode[]? neighbours)? va } TNode[]? neighbours = valueTuple?.neighbours; - if (neighbours == null) return; + if (neighbours is null) return; foreach (TNode neighbour in neighbours) { @@ -242,7 +247,14 @@ TNode[] CompileResult() lock (queueLock) { if (finalResult.Count > k) finalResult.Dequeue(); - return [.. finalResult.UnorderedItems.Select((kv) => kv.Element.Item2)]; + TNode[] result = new TNode[finalResult.Count]; + int i = 0; + foreach (((TKadKey, TNode) Element, TKadKey Priority) entry in finalResult.UnorderedItems) + { + result[i++] = entry.Element.Item2; + } + + return result; } } diff --git a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj index e5ce246513b6..96c64153e535 100644 --- a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj +++ b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj @@ -5,10 +5,6 @@ enable - - - - diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index 53a5601fcbc6..56693bf28800 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -1,8 +1,9 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Collections.Concurrent; +using System.Threading; using Nethermind.Logging; -using NonBlocking; namespace Nethermind.Kademlia; @@ -100,7 +101,7 @@ public void OnIncomingMessageFrom(TNode node) _isRefreshing.TryRemove(nodeHashProvider.GetHash(node), out _); BucketAddResult addResult = routingTable.TryAddOrRefresh(nodeHashProvider.GetHash(node), node, out TNode? toRefresh); - if (addResult == BucketAddResult.Full && toRefresh != null) + if (addResult == BucketAddResult.Full && toRefresh is not null) { if (SameAsSelf(toRefresh)) { @@ -242,8 +243,8 @@ private static bool HasOnlyCancellationExceptions(AggregateException e) private sealed class PeerFailureCache(int capacity) { - private readonly object _lock = new(); - private readonly Dictionary OrderNode)> _values = []; + private readonly Lock _lock = new(); + private readonly Dictionary OrderNode)> _values = new(capacity); private readonly LinkedList _order = []; public bool TryGet(TKadKey hash, out int failureCount) diff --git a/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs b/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs deleted file mode 100644 index e3c785a9c992..000000000000 --- a/src/Nethermind/Nethermind.Logging.Microsoft/MicrosoftLoggerExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using MsLogging = Microsoft.Extensions.Logging; - -namespace Nethermind.Logging.Microsoft -{ - public static class MicrosoftLoggerExtensions - { - public static bool IsError(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Error); - - public static bool IsWarn(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Warning); - - public static bool IsInfo(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Information); - - public static bool IsDebug(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Debug); - - public static bool IsTrace(this MsLogging.ILogger logger) => logger.IsEnabled(MsLogging.LogLevel.Trace); - } -} diff --git a/src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs b/src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs index 6cc2ac1f4f4f..f10bb2a00ee8 100644 --- a/src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs +++ b/src/Nethermind/Nethermind.Logging.Microsoft/NethermindLoggerFactory.cs @@ -2,23 +2,21 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using MsEventId = global::Microsoft.Extensions.Logging.EventId; -using MsILogger = global::Microsoft.Extensions.Logging.ILogger; -using MsILoggerFactory = global::Microsoft.Extensions.Logging.ILoggerFactory; -using MsILoggerProvider = global::Microsoft.Extensions.Logging.ILoggerProvider; -using MsLogLevel = global::Microsoft.Extensions.Logging.LogLevel; +using Microsoft.Extensions.Logging; +using MicrosoftLogger = Microsoft.Extensions.Logging.ILogger; +using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Nethermind.Logging.Microsoft; -public sealed class NethermindLoggerFactory(ILogManager logManager, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : MsILoggerFactory +public sealed class NethermindLoggerFactory(ILogManager logManager, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : ILoggerFactory { - public MsILogger CreateLogger(string categoryName) => new NethermindLogger(logManager.GetLogger(categoryName), lowerLogLevel, maxLogLevel); + public MicrosoftLogger CreateLogger(string categoryName) => new NethermindLoggerAdapter(logManager.GetLogger(categoryName), lowerLogLevel, maxLogLevel); public void Dispose() { } - public void AddProvider(MsILoggerProvider provider) { } + public void AddProvider(ILoggerProvider provider) { } - private sealed class NethermindLogger(ILogger logger, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : MsILogger + private sealed class NethermindLoggerAdapter(ILogger logger, bool lowerLogLevel = false, MsLogLevel? maxLogLevel = null) : MicrosoftLogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; @@ -40,7 +38,7 @@ public bool IsEnabled(MsLogLevel logLevel) }; } - public void Log(MsLogLevel logLevel, MsEventId eventId, + public void Log(MsLogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (lowerLogLevel) @@ -97,7 +95,7 @@ private static MsLogLevel LowerLogLevel(MsLogLevel logLevel, MsLogLevel? maxLogL _ => logLevel, }; - return loweredLogLevel > maxLogLevel ? maxLogLevel.Value : loweredLogLevel; + return maxLogLevel is not null && loweredLogLevel > maxLogLevel.Value ? maxLogLevel.Value : loweredLogLevel; } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index 4a8a2819e390..e2b4d03e0d96 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -86,18 +86,37 @@ public async Task DiscoverNodes_ShouldEmitPeerCandidateWithTcpEndpoint(Cancellat } } - private static Node CreateNode(int index, int tcpPort = 30303, int udpPort = 30304) + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_ShouldSkipConsensusOnlyEnrs(CancellationToken token) + { + Node consensusOnlyNode = CreateNode(1, includeEth2: true); + Node executionNode = CreateNode(2); + IKademlia kademlia = Substitute.For>(); + kademlia.IterateNodes().Returns([consensusOnlyNode, executionNode]); + NodeSource source = new( + kademlia, + new KademliaConfig { CurrentNodeId = CreateNode(0) }, + LimboLogs.Instance); + + await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); + + Assert.That(await enumerator.MoveNextAsync(), Is.True); + Assert.That(enumerator.Current.Id, Is.EqualTo(TestItem.PrivateKeys[2].PublicKey)); + } + + private static Node CreateNode(int index, int tcpPort = 30303, int udpPort = 30304, bool includeEth2 = false) { PrivateKey privateKey = TestItem.PrivateKeys[index]; string host = $"192.168.1.{index + 1}"; - NodeRecord enr = CreateEnr(privateKey, IPAddress.Parse(host), tcpPort, udpPort); + NodeRecord enr = CreateEnr(privateKey, IPAddress.Parse(host), tcpPort, udpPort, includeEth2); return new Node(privateKey.PublicKey, host, udpPort) { Enr = enr.EnrString }; } - private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort) + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort, bool includeEth2) { NodeRecord enr = new(); enr.SetEntry(IdEntry.Instance); @@ -105,6 +124,10 @@ private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); enr.SetEntry(new TcpEntry(tcpPort)); enr.SetEntry(new UdpEntry(udpPort)); + if (includeEth2) + { + enr.SetEntry(new TestEth2Entry()); + } enr.EnrSequence = 1; new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); return enr; @@ -112,4 +135,13 @@ private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, private static void RaiseNode(IKademlia kademlia, Node node) => kademlia.OnNodeAdded += Raise.Event>(null, node); + + private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) + { + public override string Key => EnrContentKey.Eth2; + + protected override int GetRlpLengthOfValue() => Nethermind.Serialization.Rlp.Rlp.LengthOf(Value); + + protected override void EncodeValue(Nethermind.Serialization.Rlp.RlpStream rlpStream) => rlpStream.Encode(Value); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs index f6316bc1d4a3..f675f121e9a8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaSimulation.cs @@ -15,6 +15,7 @@ using Nethermind.Network.Discovery.Kademlia; using NonBlocking; using NUnit.Framework; +using TestKademlia = Nethermind.Kademlia.Kademlia; namespace Nethermind.Network.Discovery.Test.Kademlia; @@ -49,9 +50,9 @@ public async Task TestBootstrap() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Nethermind.Kademlia.Kademlia node1 = fabric.CreateNode(node1Hash); - Nethermind.Kademlia.Kademlia node2 = fabric.CreateNode(node2Hash); - Nethermind.Kademlia.Kademlia node3 = fabric.CreateNode(node3Hash); + TestKademlia node1 = fabric.CreateNode(node1Hash); + TestKademlia node2 = fabric.CreateNode(node2Hash); + TestKademlia node3 = fabric.CreateNode(node3Hash); Assert.That(node1.GetKNeighbour(Keccak.Zero, null).Select(n => n.Hash).ToArray(), Is.EquivalentTo(new[] { node1Hash })); @@ -85,7 +86,7 @@ public async Task TestKNearestNeighbour() ValueHash256 node2Hash = RandomKeccak(rand); ValueHash256 node3Hash = RandomKeccak(rand); - Nethermind.Kademlia.Kademlia node1 = fabric.CreateNode(node1Hash); + TestKademlia node1 = fabric.CreateNode(node1Hash); Assert.That( (await node1.LookupNodesClosest(node1Hash, cts.Token)) @@ -93,7 +94,7 @@ public async Task TestKNearestNeighbour() .ToArray(), Is.EquivalentTo(new[] { node1Hash })); - Nethermind.Kademlia.Kademlia node2 = fabric.CreateNode(node2Hash); + TestKademlia node2 = fabric.CreateNode(node2Hash); fabric.CreateNode(node3Hash); node1.AddOrRefresh(new TestNode(node2Hash)); @@ -118,13 +119,13 @@ public async Task SimulateLargeKNearestNeighbour() TestFabric fabric = CreateFabric(); Random rand = new(0); ValueHash256 mainNodeHash = RandomKeccak(rand); - Nethermind.Kademlia.Kademlia mainNode = fabric.CreateNode(mainNodeHash); + TestKademlia mainNode = fabric.CreateNode(mainNodeHash); List nodeIds = []; for (int i = 0; i < nodeCount; i++) { ValueHash256 nodeHash = RandomKeccak(rand); - Nethermind.Kademlia.Kademlia kad = fabric.CreateNode(nodeHash); + TestKademlia kad = fabric.CreateNode(nodeHash); kad.AddOrRefresh(new TestNode(mainNodeHash)); nodeIds.Add(nodeHash); } @@ -212,7 +213,7 @@ private bool TryGetReceiver(TestNode receiverHash, out ReceiverForNode contentKa return false; } - public Nethermind.Kademlia.Kademlia CreateNode(ValueHash256 nodeID) + public TestKademlia CreateNode(ValueHash256 nodeID) { TestNode nodeIDTestNode = new(nodeID); @@ -233,13 +234,13 @@ public Nethermind.Kademlia.Kademlia CreateNode( }) .AddSingleton>(new SenderForNode(nodeIDTestNode, this)) .AddSingleton() - .AddSingleton>(); + .AddSingleton(); IContainer container = builder.Build(); _nodes[nodeID] = container; - return container.Resolve>(); + return container.Resolve(); } private class SenderForNode(TestNode sender, TestFabric fabric) : IKademliaMessageSender diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs index bfa29de8d1a2..d1bc50d55c54 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs @@ -38,15 +38,15 @@ public DiscoveryApp( : base("discv4", networkConfig, processExitSource, logManager.GetClassLogger()) { List bootNodes = []; - NetworkNode[] bootnodes = networkConfig.Bootnodes; - if (bootnodes.Length == 0) + NetworkNode[] configuredBootnodes = networkConfig.Bootnodes; + if (configuredBootnodes.Length == 0) { if (Logger.IsWarn) Logger.Warn("No bootnodes specified in configuration"); } - for (int i = 0; i < bootnodes.Length; i++) + for (int i = 0; i < configuredBootnodes.Length; i++) { - NetworkNode bootnode = bootnodes[i]; + NetworkNode bootnode = configuredBootnodes[i]; if (!bootnode.IsEnode) { if (Logger.IsTrace) Logger.Trace($"Ignoring ENR in discovery V4: {bootnode}"); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs index 3d01ff2e831f..530f585797a4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs @@ -8,31 +8,39 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; public sealed class NeighbourMsgHandler(int k) : ITaskCompleter { - private Node[] _current = []; + private readonly Lock _lock = new(); + private readonly Node[] _nodes = new Node[k]; + private int _count; public TaskCompletionSource> TaskCompletionSource { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); // The peer should send the two packet pretty much immediately. In any case, if the second packet is loss, its not a huge deal. private static readonly TimeSpan _secondRequestTimeout = TimeSpan.FromMilliseconds(100); - private bool _timeoutInitiated = false; + private int _timeoutInitiated; public bool Handle(DiscoveryMsg msg) { if (TaskCompletionSource.Task.IsCompleted) return false; NeighborsMsg neighborsMsg = (NeighborsMsg)msg; + bool isComplete; - while (true) + lock (_lock) { if (TaskCompletionSource.Task.IsCompleted) return false; - Node[] current = _current; - if (current.Length >= k || current.Length + neighborsMsg.Nodes.Count > k) return false; - if (Interlocked.CompareExchange(ref _current, [.. current, .. neighborsMsg.Nodes], current) == current) break; + if (_count >= k || _count + neighborsMsg.Nodes.Count > k) return false; + + for (int i = 0; i < neighborsMsg.Nodes.Count; i++) + { + _nodes[_count++] = neighborsMsg.Nodes[i]; + } + + isComplete = _count == k; } - if (_current.Length == k) + if (isComplete) { - return TaskCompletionSource.TrySetResult(DiscoveryResponse.From(_current)); + return TaskCompletionSource.TrySetResult(DiscoveryResponse.From(GetCurrentNodes())); } else { @@ -41,12 +49,27 @@ public bool Handle(DiscoveryMsg msg) async Task CompleteAfterDelay() { - if (Interlocked.CompareExchange(ref _timeoutInitiated, true, false)) return; + if (Interlocked.Exchange(ref _timeoutInitiated, 1) != 0) return; await Task.Delay(_secondRequestTimeout); - TaskCompletionSource.TrySetResult(DiscoveryResponse.From(_current)); + TaskCompletionSource.TrySetResult(DiscoveryResponse.From(GetCurrentNodes())); } } return !TaskCompletionSource.Task.IsCompleted; } + + private Node[] GetCurrentNodes() + { + lock (_lock) + { + if (_count == 0) + { + return []; + } + + Node[] nodes = new Node[_count]; + Array.Copy(_nodes, nodes, _count); + return nodes; + } + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index eaaeed4082ec..d4a9b102699c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -87,7 +87,13 @@ private void AddMessageHandler( MsgType msgType, ValueHash256 nodeId, IMessageHandler handler) => _incomingMessageHandlers.AddOrUpdate( (nodeId, msgType), (_) => [handler], - (_, currentHandler) => [.. currentHandler, handler] + (_, currentHandler) => + { + IMessageHandler[] newValue = new IMessageHandler[currentHandler.Length + 1]; + Array.Copy(currentHandler, newValue, currentHandler.Length); + newValue[^1] = handler; + return newValue; + } ); private void RemoveMessageHandler( diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index 87ffb314b65c..38ae8b178c73 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -87,35 +87,41 @@ async Task DiscoverAsync(PublicKey target) } } - Task discoverTask = Task.WhenAll(Enumerable.Range(0, discoveryConfig.ConcurrentDiscoveryJob).Select((_) => Task.Run(async () => + Task[] discoverTasks = new Task[discoveryConfig.ConcurrentDiscoveryJob]; + for (int i = 0; i < discoverTasks.Length; i++) { - Random random = new(); - byte[] randomBytes = new byte[64]; - while (!discoveryToken.IsCancellationRequested) + discoverTasks[i] = Task.Run(async () => { - Stopwatch iterationTime = Stopwatch.StartNew(); - - try + Random random = new(); + byte[] randomBytes = new byte[PublicKey.LengthInBytes]; + while (!discoveryToken.IsCancellationRequested) { - random.NextBytes(randomBytes); - await DiscoverAsync(new PublicKey(randomBytes)); + Stopwatch iterationTime = Stopwatch.StartNew(); - // Prevent high CPU when all node is not reachable due to network connectivity issue. - if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) + try { - await Task.Delay(TimeSpan.FromSeconds(1), discoveryToken); + random.NextBytes(randomBytes); + await DiscoverAsync(new PublicKey(randomBytes)); + + // Prevent high CPU when all node is not reachable due to network connectivity issue. + if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) + { + await Task.Delay(TimeSpan.FromSeconds(1), discoveryToken); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); } } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); - } - } - }))); + }); + } + + Task discoverTask = Task.WhenAll(discoverTasks); try { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs index cfe77b2760d2..fe2f7ec0bf14 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs @@ -391,7 +391,7 @@ private void EnsureDispatchWorkersStarted() private sealed class FixedWindowLimiter(int maxCount, TimeSpan window) { - private readonly object _lock = new(); + private readonly Lock _lock = new(); private long _windowStartTicks = Stopwatch.GetTimestamp(); private int _count; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs index adbe9e65d646..90fca4991648 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs @@ -23,16 +23,11 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; internal readonly record struct NonceKey(ulong Prefix, uint Suffix) { public static NonceKey From(ReadOnlySpan nonce) - { - if (nonce.Length != PacketCodec.NonceSize) - { - throw new ArgumentException($"Nonce must be {PacketCodec.NonceSize} bytes.", nameof(nonce)); - } - - return new NonceKey( - BinaryPrimitives.ReadUInt64BigEndian(nonce[..sizeof(ulong)]), - BinaryPrimitives.ReadUInt32BigEndian(nonce.Slice(sizeof(ulong), sizeof(uint)))); - } + => nonce.Length == PacketCodec.NonceSize + ? new( + BinaryPrimitives.ReadUInt64BigEndian(nonce[..sizeof(ulong)]), + BinaryPrimitives.ReadUInt32BigEndian(nonce.Slice(sizeof(ulong), sizeof(uint)))) + : throw new ArgumentException($"Nonce must be {PacketCodec.NonceSize} bytes.", nameof(nonce)); } internal sealed record PendingRequest(Node Receiver, Discv5Message Message); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 3161fcf6a949..2fd014ca9862 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -16,11 +16,12 @@ internal sealed class NodesResponseHandler(Node receiver, Distances requestedDis private const int MaxNodesResponseRecords = 64; private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly List _nodes = []; + private readonly Node[] _nodes = new Node[MaxNodesResponseRecords]; private readonly HashSet _seenNodeIds = []; private readonly bool _allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); private int? _total; private int _received; + private int _nodeCount; public override Task Task => _completion.Task; @@ -46,7 +47,7 @@ public override bool Handle(NodesMsg nodes) _total ??= nodes.Total; _received++; - for (int i = 0; i < nodes.Records.Count && _nodes.Count < MaxNodesResponseRecords; i++) + for (int i = 0; i < nodes.Records.Count && _nodeCount < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record) || @@ -58,10 +59,10 @@ public override bool Handle(NodesMsg nodes) continue; } - _nodes.Add(node); + _nodes[_nodeCount++] = node; } - if (_received >= _total || _nodes.Count >= MaxNodesResponseRecords) + if (_received >= _total || _nodeCount >= MaxNodesResponseRecords) { _completion.TrySetResult(); } @@ -69,7 +70,17 @@ public override bool Handle(NodesMsg nodes) return true; } - public Node[] GetNodes() => [.. _nodes]; + public Node[] GetNodes() + { + if (_nodeCount == 0) + { + return []; + } + + Node[] nodes = new Node[_nodeCount]; + Array.Copy(_nodes, nodes, _nodeCount); + return nodes; + } private bool MatchesRequestedDistance(Node node, Distances requestedDistances) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 63592ccdd017..0df5e2b4d68a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -3,7 +3,9 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Threading; using Nethermind.Core.Caching; +using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Crypto; using Nethermind.Kademlia; @@ -51,12 +53,12 @@ public sealed class KademliaAdapter( private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions", static session => session.Dispose()); private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges", static sentChallenge => sentChallenge.Dispose()); private readonly Queue _sentChallengeExpiries = new(); - private readonly object _sentChallengeExpiriesLock = new(); + private readonly Lock _sentChallengeExpiriesLock = new(); private long _lastSentChallengeTrimMilliseconds; private readonly LruCache _pendingByNonce = new(MaxPendingRequests, "discv5 pending requests"); private readonly LruCache _responseHandlers = new(MaxResponseHandlers, "discv5 response handlers"); private readonly LruCache _knownRecords = new(MaxKnownRecords, "discv5 known records"); - private readonly object _knownRecordsLock = new(); + private readonly Lock _knownRecordsLock = new(); private readonly LruCache _endpointChecks = new(MaxEndpointChecks, "discv5 endpoint checks"); private readonly AddressBurstLimiter _challengeRateLimiter = new(ChallengeRateLimitBurstPerIp, ChallengeRateLimitFilterSize, ChallengeRateLimitWindow); @@ -65,8 +67,8 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = { ArgumentNullException.ThrowIfNull(distances); - HashSet seen = []; - List result = []; + HashSet seen = new(MaxFindNodeRecords); + using ArrayPoolListRef result = new(MaxFindNodeRecords); Hash256? excludedHash = excluding?.IdHash; foreach (int distance in distances) @@ -97,7 +99,7 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = } } - return [.. result]; + return result.ToArray(); } /// @@ -136,21 +138,13 @@ public async Task Ping(Node receiver, CancellationToken token) } Node[] nodes = responseHandler.GetNodes(); - int validCount = 0; for (int i = 0; i < nodes.Length; i++) { - Node? node = nodes[i]; - if (node is null) - { - continue; - } - - kademlia.Value.AddOrRefresh(node); - nodes[validCount++] = node; + kademlia.Value.AddOrRefresh(nodes[i]); } - if (_logger.IsTrace) _logger.Trace($"Discv5 FINDNODE {findNode.RequestId} to {receiver:s} returned {validCount} nodes."); - return validCount == nodes.Length ? nodes : nodes[..validCount]; + if (_logger.IsTrace) _logger.Trace($"Discv5 FINDNODE {findNode.RequestId} to {receiver:s} returned {nodes.Length} nodes."); + return nodes; } public async Task RunAsync(CancellationToken token) @@ -229,6 +223,20 @@ private async Task SendRequest( } private async Task SendMessage(Node receiver, Discv5Message message) + { + if (TryEncodeWithExistingSession(receiver, message, out PendingNonceKey pendingNonceKey, out byte[]? packet)) + { + return await SendPendingPacket(receiver, message, pendingNonceKey, packet, hasSession: true); + } + + return await SendMessageWithoutSession(receiver, message); + } + + private bool TryEncodeWithExistingSession( + Node receiver, + Discv5Message message, + out PendingNonceKey pendingNonceKey, + [NotNullWhen(true)] out byte[]? packet) { SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); if (TryGetSession(sessionKey, out Session? session)) @@ -238,36 +246,40 @@ private async Task SendRequest( { Span sessionNonce = stackalloc byte[PacketCodec.NonceSize]; session.WriteNextNonce(cryptoRandom, sessionNonce); - PendingNonceKey sessionPendingNonceKey = new(receiver.Address, NonceKey.From(sessionNonce)); - _pendingByNonce.Set(sessionPendingNonceKey, new PendingRequest(receiver, message)); - byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, writeKey, message, sessionNonce); - try - { - if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} with existing session, bytes: {packet.Length}."); - await discoveryHandler.SendAsync(packet, receiver.Address); - return sessionPendingNonceKey; - } - catch - { - _pendingByNonce.TryRemove(sessionPendingNonceKey, out _); - throw; - } + pendingNonceKey = new PendingNonceKey(receiver.Address, NonceKey.From(sessionNonce)); + packet = packetCodec.EncodeOrdinary(receiver.Id, writeKey, message, sessionNonce); + return true; } } + pendingNonceKey = default; + packet = null; + return false; + } + + private async Task SendMessageWithoutSession(Node receiver, Discv5Message message) + { Span nonce = stackalloc byte[PacketCodec.NonceSize]; cryptoRandom.GenerateRandomBytes(nonce); Span encryptionKey = stackalloc byte[Session.KeySize]; cryptoRandom.GenerateRandomBytes(encryptionKey); - PendingRequest pendingRequest = new(receiver, message); PendingNonceKey pendingNonceKey = new(receiver.Address, NonceKey.From(nonce)); - _pendingByNonce.Set(pendingNonceKey, pendingRequest); - byte[] initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); + return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false); + } + + private async Task SendPendingPacket( + Node receiver, + Discv5Message message, + PendingNonceKey pendingNonceKey, + byte[] packet, + bool hasSession) + { + _pendingByNonce.Set(pendingNonceKey, new PendingRequest(receiver, message)); try { - if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} without session, bytes: {initialPacket.Length}."); - await discoveryHandler.SendAsync(initialPacket, receiver.Address); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} {(hasSession ? "with existing session" : "without session")}, bytes: {packet.Length}."); + await discoveryHandler.SendAsync(packet, receiver.Address); return pendingNonceKey; } catch @@ -511,14 +523,7 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, } private string? GetKnownEnr(Hash256 nodeId, NodeRecord? nodeRecord) - { - if (nodeRecord is not null) - { - return nodeRecord.EnrString; - } - - return _knownRecords.TryGet(nodeId, out NodeRecord? knownRecord) ? knownRecord.EnrString : null; - } + => nodeRecord?.EnrString ?? (_knownRecords.TryGet(nodeId, out NodeRecord? knownRecord) ? knownRecord.EnrString : null); private bool HandleResponse(Hash256 nodeId, Discv5Message message) { @@ -549,32 +554,39 @@ private async Task HandleFindNode(Node remoteNode, FindNodeMsg findNode, Cancell private NodeRecord[] GetFindNodeRecords(Distances distances, Node requester) { HashSet seen = new(MaxFindNodeRecords); - List result = new(MaxFindNodeRecords); - bool allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); - bool includedSelf = false; - for (int i = 0; i < distances.Count && result.Count < MaxFindNodeRecords; i++) + ArrayPoolListRef result = new(MaxFindNodeRecords); + try { - int distance = distances[i]; - if (distance < 0 || distance > _distance.MaxDistance) + bool allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); + bool includedSelf = false; + for (int i = 0; i < distances.Count && result.Count < MaxFindNodeRecords; i++) { - continue; - } + int distance = distances[i]; + if (distance < 0 || distance > _distance.MaxDistance) + { + continue; + } - if (distance == 0) - { - if (!includedSelf) + if (distance == 0) { - result.Add(nodeRecordProvider.Current); - includedSelf = true; + if (!includedSelf) + { + result.Add(nodeRecordProvider.Current); + includedSelf = true; + } + + continue; } - continue; + AddFindNodeRecordsAtDistance(distance, requester, allowNonRoutableRelays, seen, ref result); } - AddFindNodeRecordsAtDistance(distance, requester, allowNonRoutableRelays, seen, result); + return result.ToArray(); + } + finally + { + result.Dispose(); } - - return [.. result]; } private void AddFindNodeRecordsAtDistance( @@ -582,7 +594,7 @@ private void AddFindNodeRecordsAtDistance( Node requester, bool allowNonRoutableRelays, HashSet seen, - List result) + ref ArrayPoolListRef result) { Node[] nodes = kademlia.Value.GetAllAtDistance(distance); Hash256 requesterHash = requester.IdHash; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index 5d63dd99a0d5..e32be1d0aeb9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -95,6 +95,11 @@ private bool TryCreatePeerCandidate(Node discoveryNode, [NotNullWhen(true)] out try { NodeRecord record = NodeRecord.FromEnrString(discoveryNode.Enr); + if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record)) + { + return false; + } + return Node.TryFromEnr(record, out peerCandidate); } catch (Exception e) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs index bdcc81242469..8a7d1c0e5892 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Security.Cryptography; +using System.Threading; using Nethermind.Core.Crypto; using Nethermind.Crypto; @@ -12,7 +13,7 @@ internal sealed record Session(PublicKey RemotePublicKey, byte[] ReadKey, byte[] { public const int KeySize = 16; - private readonly object _lock = new(); + private readonly Lock _lock = new(); private long _nonceCounter; private bool _disposed; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs index 5705ea92afea..4c45f55cd50d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs @@ -120,7 +120,6 @@ public async Task RunDiscoveryPersistenceCommit(CancellationToken cancellationTo } catch (Exception ex) { - _discoveryStorage.DiscardBatch(); _logger.Error($"Error during discovery commit: {ex}"); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs index c8384a8b0d12..ba5839cb1254 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs @@ -23,7 +23,7 @@ public sealed class PublicKeyKeyOperator : IKeyOperator public PublicKey CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) { - Span randomBytes = new byte[64]; + Span randomBytes = stackalloc byte[PublicKey.LengthInBytes]; Random.Shared.NextBytes(randomBytes); return new PublicKey(randomBytes); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs index a33d3184cecc..898c12bda4f9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs @@ -7,8 +7,8 @@ internal sealed class RecentNodeFilter(int maxCount) where TKey : notnull { private readonly LinkedList _recentNodes = []; - private readonly Dictionary> _nodes = []; - private readonly object _lock = new(); + private readonly Dictionary> _nodes = new(maxCount); + private readonly Lock _lock = new(); public bool TryReserve(TKey nodeId) { diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs index 6dd78e84a9ab..6ff56eaa403a 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs @@ -38,15 +38,15 @@ public void Cannot_get_enr_string_when_signature_missing() } [Test] - public void Discovery_endpoint_uses_udp6_when_ipv4_udp_is_missing() + public void Discovery_endpoint_rejects_ipv4_with_udp6_only() { IPAddress ip = IPAddress.Parse("192.0.2.1"); NodeRecord nodeRecord = new(); nodeRecord.SetEntry(new IpEntry(ip)); nodeRecord.SetEntry(new Udp6Entry(30304)); - Assert.That(nodeRecord.DiscoveryIp, Is.EqualTo(ip)); - Assert.That(nodeRecord.DiscoveryPort, Is.EqualTo(30304)); + Assert.That(nodeRecord.DiscoveryIp, Is.Null); + Assert.That(nodeRecord.DiscoveryPort, Is.Null); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index 1b895641e22b..c8b5f0d013c4 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -162,7 +162,7 @@ public Signature? Signature return port is null ? (null, null) : (ip6, port); } - return ip is not null && udp6 is not null ? (ip, udp6) : (null, null); + return (null, null); } private (IPAddress? Ip, int? Port) GetTcpEndpoint() diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs index a89514ec2017..225eff921f03 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Generic; using System.Linq; using Nethermind.Config; +using Nethermind.Core; using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.IO; using Nethermind.Core.Timers; @@ -165,4 +167,90 @@ public void Discard_batch_drops_pending_nodes() Assert.That(storage.GetPersistedNodes(), Has.Length.EqualTo(1)); } + + [Test] + public void Failed_commit_reloads_persisted_nodes_before_new_updates() + { + FailingBatchDb db = new(); + NetworkStorage storage = new(db, LimboLogs.Instance); + NetworkNode persistedNode = new(TestItem.PublicKeyA, "192.1.1.1", 3441, 1L); + NetworkNode discardedNode = new(TestItem.PublicKeyB, "192.1.1.2", 3442, 2L); + NetworkNode pendingNode = new(TestItem.PublicKeyC, "192.1.1.3", 3443, 3L); + storage.UpdateNode(persistedNode); + + db.ThrowOnNextBatchDispose = true; + storage.StartBatch(); + storage.UpdateNode(discardedNode); + Assert.Throws(storage.Commit); + + storage.StartBatch(); + storage.UpdateNode(pendingNode); + + NetworkNode[] nodes = storage.GetPersistedNodes(); + using (Assert.EnterMultipleScope()) + { + Assert.That(nodes, Has.Some.Matches(node => node.NodeId.Equals(persistedNode.NodeId))); + Assert.That(nodes, Has.Some.Matches(node => node.NodeId.Equals(pendingNode.NodeId))); + Assert.That(nodes, Has.None.Matches(node => node.NodeId.Equals(discardedNode.NodeId))); + } + } + + private sealed class FailingBatchDb : IFullDb + { + private readonly SnapshotableMemDb _inner = new(); + + public bool ThrowOnNextBatchDispose { get; set; } + + public string Name => _inner.Name; + + public byte[]? this[ReadOnlySpan key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + public KeyValuePair[] this[byte[][] keys] => _inner[keys]; + + public ICollection Keys => _inner.Keys; + + public ICollection Values => _inner.Values; + + public int Count => _inner.Count; + + public IEnumerable> GetAll(bool ordered = false) => _inner.GetAll(ordered); + + public IEnumerable GetAllKeys(bool ordered = false) => _inner.GetAllKeys(ordered); + + public IEnumerable GetAllValues(bool ordered = false) => _inner.GetAllValues(ordered); + + public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) => _inner.Get(key, flags); + + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) => _inner.Set(key, value, flags); + + public IWriteBatch StartWriteBatch() + { + if (!ThrowOnNextBatchDispose) + { + return _inner.StartWriteBatch(); + } + + ThrowOnNextBatchDispose = false; + return new FailingWriteBatch(); + } + + public void Flush(bool onlyWal = false) => _inner.Flush(onlyWal); + + public void Dispose() => _inner.Dispose(); + } + + private sealed class FailingWriteBatch : IWriteBatch + { + public void Clear() { } + + public void Dispose() => throw new InvalidOperationException("Failed batch dispose."); + + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) { } + } } diff --git a/src/Nethermind/Nethermind.Network/NetworkStorage.cs b/src/Nethermind/Nethermind.Network/NetworkStorage.cs index afc25faf0901..114c328e34cc 100644 --- a/src/Nethermind/Nethermind.Network/NetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/NetworkStorage.cs @@ -24,6 +24,7 @@ public class NetworkStorage(IFullDb? fullDb, ILogManager? logManager) : INetwork private long _updateCounter; private long _removeCounter; private NetworkNode[]? _nodes; + private bool _loadedFromDb; public int PersistedNodesCount => GetPersistedNodes().Length; @@ -44,10 +45,7 @@ private NetworkNode[] GenerateNodes() return nodes; } - if (_nodesDict.Count == 0) - { - LoadFromDb(); - } + EnsureLoadedFromDbNoLock(); return _nodesDict.Count == 0 ? [] : CopyDictToArray(); } @@ -60,7 +58,16 @@ private NetworkNode[] CopyDictToArray() return (_nodes = nodes); } - private void LoadFromDb() + private void EnsureLoadedFromDbNoLock() + { + if (!_loadedFromDb) + { + LoadFromDbNoLock(); + _loadedFromDb = true; + } + } + + private void LoadFromDbNoLock() { foreach (byte[]? nodeRlp in _fullDb.Values) { @@ -72,7 +79,7 @@ private void LoadFromDb() try { NetworkNode node = GetNode(nodeRlp); - _nodesDict[node.NodeId] = node; + _nodesDict.TryAdd(node.NodeId, node); } catch (Exception e) { @@ -92,6 +99,8 @@ public void UpdateNode(NetworkNode node) private void UpdateNodeImpl(NetworkNode node, byte[] rlp) { + EnsureLoadedFromDbNoLock(); + (_currentBatch ?? (IWriteOnlyKeyValueStore)_fullDb)[node.NodeId.Bytes] = rlp; _updateCounter++; @@ -129,6 +138,8 @@ public void RemoveNode(PublicKey nodeId) { lock (_lock) { + EnsureLoadedFromDbNoLock(); + (_currentBatch ?? (IWriteOnlyKeyValueStore)_fullDb)[nodeId.Bytes] = null; _removeCounter++; @@ -198,7 +209,7 @@ private void DiscardBatchNoLock() { currentBatch.Clear(); currentBatch.Dispose(); - ClearLocalCache(); + ClearLocalCacheNoLock(); } } @@ -206,11 +217,17 @@ private void ClearLocalCache() { lock (_lock) { - _nodesDict.Clear(); - _nodes = null; + ClearLocalCacheNoLock(); } } + private void ClearLocalCacheNoLock() + { + _nodesDict.Clear(); + _nodes = null; + _loadedFromDb = false; + } + public bool AnyPendingChange() => _updateCounter > 0 || _removeCounter > 0; private static NetworkNode GetNode(byte[] networkNodeRaw) diff --git a/src/Nethermind/Nethermind.Network/PeerPool.cs b/src/Nethermind/Nethermind.Network/PeerPool.cs index e54ac103b4f6..cfbe65954f7f 100644 --- a/src/Nethermind/Nethermind.Network/PeerPool.cs +++ b/src/Nethermind/Nethermind.Network/PeerPool.cs @@ -190,7 +190,6 @@ private async Task RunPeerCommit() } catch (Exception ex) { - _peerStorage.DiscardBatch(); _peerStorage.StartBatch(); if (_logger.IsError) ErrorPeerStorageCommit(ex); } From 815862ffca60f3c70d4e9198d381c496b0e8e58a Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 9 Jun 2026 14:08:52 +0300 Subject: [PATCH 152/182] Fix enodes in discv5 and build --- .../Nethermind.Kademlia/Kademlia.cs | 2 +- .../DiscoveryV5AppTests.cs | 23 +++++++++++++++++++ .../Discv5/DiscoveryV5App.cs | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index 6eea6384c87f..a7ab3ccc8fe8 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -162,7 +162,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => activeBucketPrefixCapacity = _lastBucketRefreshTicks.Count; } - PooledHashSet activeBucketPrefixes = new(activeBucketPrefixCapacity); + HashSet activeBucketPrefixes = new(activeBucketPrefixCapacity); foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { activeBucketPrefixes.Add(Prefix); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index e268f5e30466..d6dad65a29a9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -379,6 +379,29 @@ public void Should_Use_Udp_Port_From_Configured_Enr_Bootnode() } } + [Test] + public void Should_Use_Discovery_Port_From_Configured_Enode_Bootnode() + { + Enode enode = new(TestItem.PrivateKeyA.PublicKey, IPAddress.Parse("8.8.8.8"), 30303, discoveryPort: 9001); + NetworkConfig networkConfig = new() + { + Bootnodes = [new NetworkNode(enode)] + }; + DiscoveryConfig discoveryConfig = new() + { + UseDefaultDiscv5Bootnodes = false + }; + + List bootNodes = _discoveryV5App.CreateBootNodes(networkConfig, discoveryConfig); + + using (Assert.EnterMultipleScope()) + { + Assert.That(bootNodes, Has.Count.EqualTo(1)); + Assert.That(bootNodes[0].Port, Is.EqualTo(9001)); + Assert.That(bootNodes[0].Host, Is.EqualTo("8.8.8.8")); + } + } + private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) { public override string Key => EnrContentKey.Eth2; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 21d9b30b3c06..45a296c4ca4c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -153,7 +153,7 @@ private BootNodeAddResult AddBootNode(List bootNodes, HashSet see return AddBootNode(bootNodes, seen, networkNode.Enr); } - Node node = new(networkNode.NodeId, networkNode.Host, networkNode.Port); + Node node = new(networkNode.NodeId, networkNode.Host, networkNode.Enode.DiscoveryPort); return AddBootNode(bootNodes, seen, node); } From caab1ae45aeff62c3459150d3e5057a9f09babea Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 9 Jun 2026 14:34:06 +0300 Subject: [PATCH 153/182] Use pooled set in Kademlia --- src/Nethermind/Nethermind.Kademlia/Kademlia.cs | 5 +++-- .../Nethermind.Kademlia/Nethermind.Kademlia.csproj | 4 ++++ src/Nethermind/Nethermind.Runner/packages.lock.json | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index a7ab3ccc8fe8..2e617b804a50 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; +using Collections.Pooled; using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -162,7 +163,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => activeBucketPrefixCapacity = _lastBucketRefreshTicks.Count; } - HashSet activeBucketPrefixes = new(activeBucketPrefixCapacity); + using PooledSet activeBucketPrefixes = new(activeBucketPrefixCapacity); foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { activeBucketPrefixes.Add(Prefix); @@ -199,7 +200,7 @@ private bool ShouldRefreshBucket(TKadKey prefix, KBucket bucket) } } - private void PruneLastBucketRefreshTicks(HashSet activeBucketPrefixes) + private void PruneLastBucketRefreshTicks(PooledSet activeBucketPrefixes) { lock (_lastBucketRefreshLock) { diff --git a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj index 96c64153e535..6995c2b9edb6 100644 --- a/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj +++ b/src/Nethermind/Nethermind.Kademlia/Nethermind.Kademlia.csproj @@ -5,6 +5,10 @@ enable + + + + diff --git a/src/Nethermind/Nethermind.Runner/packages.lock.json b/src/Nethermind/Nethermind.Runner/packages.lock.json index 0cebc4ff48df..6b6c97fe64c1 100644 --- a/src/Nethermind/Nethermind.Runner/packages.lock.json +++ b/src/Nethermind/Nethermind.Runner/packages.lock.json @@ -902,8 +902,8 @@ "nethermind.kademlia": { "type": "Project", "dependencies": { - "Nethermind.Logging": "[1.39.0-unstable, )", - "NonBlocking": "[2.1.2, )" + "Collections.Pooled": "[1.0.82, )", + "Nethermind.Logging": "[1.39.0-unstable, )" } }, "nethermind.keystore": { From 1a8e52632e475020b926847d2aaca1e48c2f1426 Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Tue, 9 Jun 2026 13:39:45 +0200 Subject: [PATCH 154/182] Consolidate ENR builders in discovery tests Six near-identical NodeRecord builders across DiscoveryV5AppTests, Discv4/Kademlia, and Discv5 test files are replaced by a single shared TestEnrBuilder.BuildSigned helper. Two duplicate TestEth2Entry nested classes are merged into one shared internal type. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DiscoveryV5AppTests.cs | 60 +++-------------- .../Discv4/Kademlia/KademliaAdapterTests.cs | 25 ++----- .../Discv5/CodecTests.cs | 12 +--- .../Handlers/NodesResponseHandlerTests.cs | 13 +--- .../Discv5/KademliaAdapterTests.cs | 32 ++------- .../Discv5/NodeSourceTests.cs | 33 ++-------- .../TestEnrBuilder.cs | 66 +++++++++++++++++++ 7 files changed, 97 insertions(+), 144 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index d6dad65a29a9..c283d6d7a550 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -93,49 +93,16 @@ public async Task Teardown() _discoveryDb.Dispose(); } - private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true, bool includeUdp = true, bool includeEth2 = false) - { - NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); - enr.SetEntry(new IpEntry(ipAddress ?? IPAddress.Loopback)); - enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - if (includeTcp) - { - enr.SetEntry(new TcpEntry(port)); - } - if (includeUdp) - { - enr.SetEntry(new UdpEntry(udpPort ?? port)); - } - if (includeEth2) - { - enr.SetEntry(new TestEth2Entry()); - } - enr.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + private static NodeRecord CreateTestEnr(Nethermind.Crypto.PrivateKey privateKey, IPAddress? ipAddress = null, int port = 30303, int? udpPort = null, bool includeTcp = true, bool includeUdp = true, bool includeEth2 = false) => + TestEnrBuilder.BuildSigned( + privateKey, + ipAddress ?? IPAddress.Loopback, + tcpPort: includeTcp ? port : null, + udpPort: includeUdp ? udpPort ?? port : null, + configureExtras: includeEth2 ? static enr => enr.SetEntry(new TestEth2Entry()) : null); - return enr; - } - - private static NodeRecord CreateTestIpv6Enr(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress, int udpPort, bool useUdp6 = true) - { - NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); - enr.SetEntry(new Ip6Entry(ipAddress)); - enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - if (useUdp6) - { - enr.SetEntry(new Udp6Entry(udpPort)); - } - else - { - enr.SetEntry(new UdpEntry(udpPort)); - } - enr.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); - - return enr; - } + private static NodeRecord CreateTestIpv6Enr(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress, int udpPort, bool useUdp6 = true) => + TestEnrBuilder.BuildSigned(privateKey, ipAddress, tcpPort: null, udpPort: udpPort, useUdp6: useUdp6); private static NodeRecord CreateEnrForAddress(Nethermind.Crypto.PrivateKey privateKey, IPAddress ipAddress) => ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 @@ -401,13 +368,4 @@ public void Should_Use_Discovery_Port_From_Configured_Enode_Bootnode() Assert.That(bootNodes[0].Host, Is.EqualTo("8.8.8.8")); } } - - private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) - { - public override string Key => EnrContentKey.Eth2; - - protected override int GetRlpLengthOfValue() => Nethermind.Serialization.Rlp.Rlp.LengthOf(Value); - - protected override void EncodeValue(Nethermind.Serialization.Rlp.RlpStream rlpStream) => rlpStream.Encode(Value); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs index 491f90d815fa..bfc8d2a3d853 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs @@ -92,7 +92,11 @@ public void Setup() _networkConfig.MaxActivePeers.Returns(25); _kademliaConfig = new KademliaConfig { CurrentNodeId = _testNode }; - _selfNodeRecord = CreateNodeRecord(); + _selfNodeRecord = TestEnrBuilder.BuildSigned( + TestItem.PrivateKeyA, + IPAddress.Parse("192.168.1.1"), + tcpPort: _networkConfig.P2PPort, + udpPort: _networkConfig.DiscoveryPort); _logManager = LimboLogs.Instance; _timestamper = Substitute.For(); @@ -143,25 +147,6 @@ public async Task GetSession_should_return_single_session_for_concurrent_calls() _nodeStatsManager.Received(1).GetOrAdd(Arg.Is(node => node.Id == _receiver.Id)); } - private NodeRecord CreateNodeRecord() - { - NodeRecord selfNodeRecord = new(); - selfNodeRecord.SetEntry(IdEntry.Instance); - selfNodeRecord.SetEntry(new IpEntry(IPAddress.Parse("192.168.1.1"))); - selfNodeRecord.SetEntry(new TcpEntry(_networkConfig.P2PPort)); - selfNodeRecord.SetEntry(new UdpEntry(_networkConfig.DiscoveryPort)); - selfNodeRecord.SetEntry(new SecP256k1Entry(TestItem.PrivateKeyA.CompressedPublicKey)); - selfNodeRecord.EnrSequence = 1; - NodeRecordSigner enrSigner = new(new EthereumEcdsa(BlockchainIds.Mainnet), TestItem.PrivateKeyA); - enrSigner.Sign(selfNodeRecord); - if (!enrSigner.Verify(selfNodeRecord)) - { - throw new NetworkingException("Self ENR initialization failed", NetworkExceptionType.Discovery); - } - - return selfNodeRecord; - } - [TearDown] public async Task TearDown() => await _adapter.DisposeAsync(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index d11944eddc52..d37502ed6513 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -329,16 +329,8 @@ private static byte[] CreateDevp2pPingPacketBytes() "ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3" + "4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc"); - private static NodeRecord CreateNodeRecord(PrivateKey privateKey) - { - NodeRecord nodeRecord = new(); - nodeRecord.SetEntry(IdEntry.Instance); - nodeRecord.SetEntry(new IpEntry(IPAddress.Loopback)); - nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - nodeRecord.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(nodeRecord); - return nodeRecord; - } + private static NodeRecord CreateNodeRecord(PrivateKey privateKey) => + TestEnrBuilder.BuildSigned(privateKey, tcpPort: null, udpPort: null); private sealed class TestNodeRecordProvider(PrivateKey privateKey) : INodeRecordProvider { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index 0957a08ed8a3..72a5a119b83c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -31,17 +31,8 @@ public void ShouldFilterRecordByReceiverAndRecordAddress(string receiverIp, stri Assert.That(handler.GetNodes(), Has.Length.EqualTo(expectedCount)); } - private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) - { - NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); - enr.SetEntry(new IpEntry(ipAddress)); - enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - enr.SetEntry(new UdpEntry(30303)); - enr.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); - return enr; - } + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) => + TestEnrBuilder.BuildSigned(privateKey, ipAddress, tcpPort: null); private static NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 2d994b741b2c..24448e8f9fdb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -179,29 +179,11 @@ public void TrySetKnownRecord_ShouldNotDowngradeSequence() } } - private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, ulong enrSequence = 1, bool includeEth2 = false) - { - NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); - enr.SetEntry(new IpEntry(ipAddress)); - enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - enr.SetEntry(new UdpEntry(30303)); - if (includeEth2) - { - enr.SetEntry(new TestEth2Entry()); - } - enr.EnrSequence = enrSequence; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); - return enr; - } - - private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) - { - public override string Key => EnrContentKey.Eth2; - - protected override int GetRlpLengthOfValue() => Nethermind.Serialization.Rlp.Rlp.LengthOf(Value); - - protected override void EncodeValue(Nethermind.Serialization.Rlp.RlpStream rlpStream) => rlpStream.Encode(Value); - } - + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, ulong enrSequence = 1, bool includeEth2 = false) => + TestEnrBuilder.BuildSigned( + privateKey, + ipAddress, + tcpPort: null, + enrSequence: enrSequence, + configureExtras: includeEth2 ? static enr => enr.SetEntry(new TestEth2Entry()) : null); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index e2b4d03e0d96..9a71cb9f0bd2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -109,39 +109,18 @@ private static Node CreateNode(int index, int tcpPort = 30303, int udpPort = 303 { PrivateKey privateKey = TestItem.PrivateKeys[index]; string host = $"192.168.1.{index + 1}"; - NodeRecord enr = CreateEnr(privateKey, IPAddress.Parse(host), tcpPort, udpPort, includeEth2); + NodeRecord enr = TestEnrBuilder.BuildSigned( + privateKey, + IPAddress.Parse(host), + tcpPort: tcpPort, + udpPort: udpPort, + configureExtras: includeEth2 ? static enr => enr.SetEntry(new TestEth2Entry()) : null); return new Node(privateKey.PublicKey, host, udpPort) { Enr = enr.EnrString }; } - private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort, bool includeEth2) - { - NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); - enr.SetEntry(new IpEntry(ipAddress)); - enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - enr.SetEntry(new TcpEntry(tcpPort)); - enr.SetEntry(new UdpEntry(udpPort)); - if (includeEth2) - { - enr.SetEntry(new TestEth2Entry()); - } - enr.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); - return enr; - } - private static void RaiseNode(IKademlia kademlia, Node node) => kademlia.OnNodeAdded += Raise.Event>(null, node); - - private sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) - { - public override string Key => EnrContentKey.Eth2; - - protected override int GetRlpLengthOfValue() => Nethermind.Serialization.Rlp.Rlp.LengthOf(Value); - - protected override void EncodeValue(Nethermind.Serialization.Rlp.RlpStream rlpStream) => rlpStream.Encode(Value); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs new file mode 100644 index 000000000000..5baa173cbe3b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Net; +using System.Net.Sockets; +using Nethermind.Crypto; +using Nethermind.Network.Enr; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Network.Discovery.Test; + +internal static class TestEnrBuilder +{ + public static NodeRecord BuildSigned( + PrivateKey privateKey, + IPAddress? ipAddress = null, + int? tcpPort = 30303, + int? udpPort = 30303, + bool useUdp6 = true, + ulong enrSequence = 1, + Action? configureExtras = null) + { + IPAddress ip = ipAddress ?? IPAddress.Loopback; + bool isIpv6 = ip.AddressFamily == AddressFamily.InterNetworkV6; + NodeRecord enr = new(); + enr.SetEntry(IdEntry.Instance); + if (isIpv6) + { + enr.SetEntry(new Ip6Entry(ip)); + } + else + { + enr.SetEntry(new IpEntry(ip)); + } + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + if (tcpPort is { } tcp) + { + enr.SetEntry(new TcpEntry(tcp)); + } + if (udpPort is { } udp) + { + if (isIpv6 && useUdp6) + { + enr.SetEntry(new Udp6Entry(udp)); + } + else + { + enr.SetEntry(new UdpEntry(udp)); + } + } + configureExtras?.Invoke(enr); + enr.EnrSequence = enrSequence; + new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + return enr; + } +} + +internal sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) +{ + public override string Key => EnrContentKey.Eth2; + + protected override int GetRlpLengthOfValue() => Rlp.LengthOf(Value); + + protected override void EncodeValue(RlpStream rlpStream) => rlpStream.Encode(Value); +} From 7813457660235181653a40f7dd5d127b03b89c85 Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Tue, 9 Jun 2026 13:39:51 +0200 Subject: [PATCH 155/182] Remove duplicate ToHash test helpers KBucketTests, KademliaTests, and NodeHealthTrackerTests each defined a private static ToHash(ValueHash256) that resolved to the same expression as the existing IdentityNodeHashProvider.ToHash and ValueHashKeyOperator.ToHash. Call the shared helpers directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Kademlia/KBucketTests.cs | 16 +++++++--------- .../Kademlia/KademliaTests.cs | 7 ++----- .../Kademlia/NodeHealthTrackerTests.cs | 14 ++++++-------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index 293165146ee8..357db9bbbecd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -19,12 +19,12 @@ public void TryAddOrRefresh_ShouldLimitToK() AddNodes(bucket, toAdd); Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(toAdd[..5].ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (ToHash(it), it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (IdentityNodeHashProvider.ToHash(it), it)).ToHashSet())); foreach (ValueHash256 valueHash256 in toAdd[..5]) { - Assert.That(bucket.ContainsNode(ToHash(valueHash256)), Is.True); - Assert.That(bucket.GetByHash(ToHash(valueHash256)), Is.EqualTo(valueHash256)); + Assert.That(bucket.ContainsNode(IdentityNodeHashProvider.ToHash(valueHash256)), Is.True); + Assert.That(bucket.GetByHash(IdentityNodeHashProvider.ToHash(valueHash256)), Is.EqualTo(valueHash256)); } } @@ -44,7 +44,7 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNewInstance() { KBucket bucket = new(5); - Hash256 hash = ToHash(ValueKeccak.Compute("node")); + Hash256 hash = IdentityNodeHashProvider.ToHash(ValueKeccak.Compute("node")); bucket.TryAddOrRefresh(hash, "old", out _); bucket.TryAddOrRefresh(hash, "new", out _); @@ -59,15 +59,13 @@ public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() { (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); - bucket.RemoveAndReplace(ToHash(toAdd[0])); + bucket.RemoveAndReplace(IdentityNodeHashProvider.ToHash(toAdd[0])); ValueHash256[] expected = [.. toAdd[1..5], toAdd[9]]; Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(expected.ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (ToHash(it), it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (IdentityNodeHashProvider.ToHash(it), it)).ToHashSet())); } - private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - private static (KBucket Bucket, ValueHash256[] Nodes) BuildFullBucket() { KBucket bucket = new(5); @@ -80,7 +78,7 @@ private static void AddNodes(KBucket bucket, ValueHash256 { foreach (ValueHash256 node in nodes) { - bucket.TryAddOrRefresh(ToHash(node), node, out _); + bucket.TryAddOrRefresh(IdentityNodeHashProvider.ToHash(node), node, out _); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index fd95206e6423..5a3f29277ff5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -221,12 +221,9 @@ public void PruneLastBucketRefreshTicks_removes_stale_prefixes_even_when_counts_ Assert.That(lastRefreshTicks.ContainsKey(stalePrefix), Is.False); } - private static Hash256 ToHash(ValueHash256 hash) => ValueHashKeyOperator.ToHash(hash); - - private static ValueHash256 ToValueHash(Hash256 hash) => ValueHashKeyOperator.ToValueHash(hash); - private static ValueHash256 RandomValueHashAtDistance(ValueHash256 currentHash, int distance) => - ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(ToHash(currentHash), distance)); + ValueHashKeyOperator.ToValueHash( + Hash256KademliaDistance.Instance.GetRandomHashAtDistance(ValueHashKeyOperator.ToHash(currentHash), distance)); private static Dictionary GetLastBucketRefreshTicks(Nethermind.Kademlia.Kademlia kad) => (Dictionary)typeof(Nethermind.Kademlia.Kademlia) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index 836bcb891329..09b5705b9a30 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -49,7 +49,7 @@ public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSe tracker.OnIncomingMessageFrom(Remote); Assert.That(routing.AddCalls, Has.Count.EqualTo(2)); - Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(ToHash(ValueKeccak.Compute(Self)))); + Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Self)))); Assert.That(routing.AddCalls[1].Node, Is.EqualTo(Self)); } @@ -67,7 +67,7 @@ public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(Cancellation tracker.OnIncomingMessageFrom(Remote); - Hash256 staleHash = ToHash(ValueKeccak.Compute(Stale)); + Hash256 staleHash = IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Stale)); await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(staleHash), token); } @@ -84,7 +84,7 @@ public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken t tracker.OnIncomingMessageFrom(Remote); - Hash256 staleHash = ToHash(ValueKeccak.Compute(Stale)); + Hash256 staleHash = IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Stale)); await AssertEventuallyAsync(() => routing.HasAddedNode(staleHash), token); Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); } @@ -131,7 +131,7 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(bool asyn } await pingCancelled.Task.WaitAsync(token); - Assert.That(routing.RemoveCalls, Does.Not.Contain(ToHash(ValueKeccak.Compute(Stale)))); + Assert.That(routing.RemoveCalls, Does.Not.Contain(IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Stale)))); } [Test] @@ -144,7 +144,7 @@ public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() tracker.OnRequestFailed(Remote); Assert.That(routing.RemoveCalls, Has.Count.EqualTo(1)); - Assert.That(routing.RemoveCalls[0], Is.EqualTo(ToHash(ValueKeccak.Compute(Remote)))); + Assert.That(routing.RemoveCalls[0], Is.EqualTo(IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Remote)))); } private static async Task AssertEventuallyAsync(Func condition, CancellationToken token) @@ -157,13 +157,11 @@ private static async Task AssertEventuallyAsync(Func condition, Cancellati Assert.Fail("Condition not met within timeout."); } - private static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - private sealed class StringNodeHashProvider : INodeHashProvider { public static readonly StringNodeHashProvider Instance = new(); - public Hash256 GetHash(string node) => ToHash(ValueKeccak.Compute(node)); + public Hash256 GetHash(string node) => IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(node)); } private sealed class RoutingTableStub : IRoutingTable From 1abe8a970cb17ea0f32db2c042700639533d52a3 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 9 Jun 2026 15:03:14 +0300 Subject: [PATCH 156/182] More pooled --- .../Kademlia/KademliaTests.cs | 7 ++++++- .../Discv5/DiscoveryV5App.cs | 9 +++++---- .../Discv5/Kademlia/Handlers/NodesResponseHandler.cs | 7 +++++-- .../Discv5/Kademlia/KademliaAdapter.cs | 9 +++++---- .../Nethermind.Network.Discovery.csproj | 1 + src/Nethermind/Nethermind.Runner/packages.lock.json | 1 + 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index fd95206e6423..45edfd7aa8cf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Autofac; +using Collections.Pooled; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Logging; @@ -213,9 +214,13 @@ public void PruneLastBucketRefreshTicks_removes_stale_prefixes_even_when_counts_ lastRefreshTicks[activePrefix] = 1; lastRefreshTicks[stalePrefix] = 2; + using PooledSet activePrefixes = new(2); + activePrefixes.Add(activePrefix); + activePrefixes.Add(new("0x3333333333333333333333333333333333333333333333333333333333333333")); + typeof(Nethermind.Kademlia.Kademlia) .GetMethod("PruneLastBucketRefreshTicks", BindingFlags.Instance | BindingFlags.NonPublic)! - .Invoke(kad, [new HashSet { activePrefix, new("0x3333333333333333333333333333333333333333333333333333333333333333") }]); + .Invoke(kad, [activePrefixes]); Assert.That(lastRefreshTicks.ContainsKey(activePrefix), Is.True); Assert.That(lastRefreshTicks.ContainsKey(stalePrefix), Is.False); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 45a296c4ca4c..7978df800869 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -7,6 +7,7 @@ using System.Text.Json; using Autofac; using Autofac.Features.AttributeFilters; +using Collections.Pooled; using DotNetty.Transport.Channels; using Nethermind.Config; using Nethermind.Core; @@ -85,7 +86,7 @@ Func NettyDiscoveryHandlerFactory internal List CreateBootNodes(INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig) { List bootNodes = []; - HashSet seen = []; + using PooledSet seen = new(networkConfig.Bootnodes.Length); BootNodeStats configuredStats = new(); BootNodeStats defaultStats = new(); @@ -146,7 +147,7 @@ public override void AddNodeToDiscovery(Node node) } } - private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, NetworkNode networkNode) + private BootNodeAddResult AddBootNode(List bootNodes, PooledSet seen, NetworkNode networkNode) { if (networkNode.IsEnr) { @@ -157,7 +158,7 @@ private BootNodeAddResult AddBootNode(List bootNodes, HashSet see return AddBootNode(bootNodes, seen, node); } - private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, NodeRecord nodeRecord) + private BootNodeAddResult AddBootNode(List bootNodes, PooledSet seen, NodeRecord nodeRecord) { if (TryGetAcceptableNodeFromEnr(nodeRecord, out Node? node)) { @@ -167,7 +168,7 @@ private BootNodeAddResult AddBootNode(List bootNodes, HashSet see return BootNodeAddResult.Skipped; } - private BootNodeAddResult AddBootNode(List bootNodes, HashSet seen, Node node) + private BootNodeAddResult AddBootNode(List bootNodes, PooledSet seen, Node node) { if (!seen.Add(node.IdHash)) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 2fd014ca9862..35b3da7bc9a3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Collections.Pooled; using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Discv5.Messages; @@ -10,14 +11,14 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; internal sealed class NodesResponseHandler(Node receiver, Distances requestedDistances, IKademliaDistance distanceCalculator) - : ResponseHandler(MessageType.Nodes) + : ResponseHandler(MessageType.Nodes), IDisposable { private const int MaxNodesResponseMessages = 16; private const int MaxNodesResponseRecords = 64; private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly Node[] _nodes = new Node[MaxNodesResponseRecords]; - private readonly HashSet _seenNodeIds = []; + private readonly PooledSet _seenNodeIds = new(MaxNodesResponseRecords); private readonly bool _allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); private int? _total; private int _received; @@ -25,6 +26,8 @@ internal sealed class NodesResponseHandler(Node receiver, Distances requestedDis public override Task Task => _completion.Task; + public void Dispose() => _seenNodeIds.Dispose(); + public override bool Handle(NodesMsg nodes) { if (_completion.Task.IsCompleted) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 0df5e2b4d68a..ec249a003d54 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading; +using Collections.Pooled; using Nethermind.Core.Caching; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; @@ -67,7 +68,7 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = { ArgumentNullException.ThrowIfNull(distances); - HashSet seen = new(MaxFindNodeRecords); + using PooledSet seen = new(MaxFindNodeRecords); using ArrayPoolListRef result = new(MaxFindNodeRecords); Hash256? excludedHash = excluding?.IdHash; @@ -128,7 +129,7 @@ public async Task Ping(Node receiver, CancellationToken token) RegisterKnownRecord(receiver); Distances distances = GetLookupDistances(receiver, target); using FindNodeMsg findNode = new(CreateRequestId(), distances); - NodesResponseHandler responseHandler = new(receiver, distances, _distance); + using NodesResponseHandler responseHandler = new(receiver, distances, _distance); if (_logger.IsTrace) _logger.Trace($"Sending discv5 FINDNODE {findNode.RequestId} to {receiver:s}, distances: {FormatDistances(distances)}."); if (!await SendRequest(receiver, findNode, responseHandler, _findNodeTimeout, token)) @@ -553,7 +554,7 @@ private async Task HandleFindNode(Node remoteNode, FindNodeMsg findNode, Cancell private NodeRecord[] GetFindNodeRecords(Distances distances, Node requester) { - HashSet seen = new(MaxFindNodeRecords); + using PooledSet seen = new(MaxFindNodeRecords); ArrayPoolListRef result = new(MaxFindNodeRecords); try { @@ -593,7 +594,7 @@ private void AddFindNodeRecordsAtDistance( int distance, Node requester, bool allowNonRoutableRelays, - HashSet seen, + PooledSet seen, ref ArrayPoolListRef result) { Node[] nodes = kademlia.Value.GetAllAtDistance(distance); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj b/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj index bf37362bb6ef..619eb40c09da 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj +++ b/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Nethermind/Nethermind.Runner/packages.lock.json b/src/Nethermind/Nethermind.Runner/packages.lock.json index 6b6c97fe64c1..30d03dfd7221 100644 --- a/src/Nethermind/Nethermind.Runner/packages.lock.json +++ b/src/Nethermind/Nethermind.Runner/packages.lock.json @@ -985,6 +985,7 @@ "nethermind.network.discovery": { "type": "Project", "dependencies": { + "Collections.Pooled": "[1.0.82, )", "Nethermind.Api": "[1.39.0-unstable, )", "Nethermind.Crypto": "[1.39.0-unstable, )", "Nethermind.Facade": "[1.39.0-unstable, )", From 7aeafbd03667fe27d0e2263b658c9071dd45f6a7 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 9 Jun 2026 23:18:29 +0300 Subject: [PATCH 157/182] More locals init skipped --- .../Discv5/Kademlia/KademliaAdapter.cs | 60 +++++++++++++++---- .../Discv5/Packets/PacketCodec.cs | 10 ++++ .../Discv5/Serializers/MsgSerializerBase.cs | 2 + .../Kademlia/Hash256KademliaDistance.cs | 5 ++ .../Kademlia/PublicKeyKeyOperator.cs | 2 + .../Nethermind.Network.Discovery.csproj | 1 + .../Serializers/IPAddressRlp.cs | 2 + 7 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index ec249a003d54..6694ada05a51 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Runtime.CompilerServices; using System.Threading; using Collections.Pooled; using Nethermind.Core.Caching; @@ -233,6 +234,7 @@ private async Task SendRequest( return await SendMessageWithoutSession(receiver, message); } + [SkipLocalsInit] private bool TryEncodeWithExistingSession( Node receiver, Discv5Message message, @@ -259,14 +261,21 @@ private bool TryEncodeWithExistingSession( } private async Task SendMessageWithoutSession(Node receiver, Discv5Message message) + { + PendingNonceKey pendingNonceKey = EncodeMessageWithoutSession(receiver, message, out byte[] initialPacket); + return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false); + } + + [SkipLocalsInit] + private PendingNonceKey EncodeMessageWithoutSession(Node receiver, Discv5Message message, out byte[] initialPacket) { Span nonce = stackalloc byte[PacketCodec.NonceSize]; cryptoRandom.GenerateRandomBytes(nonce); Span encryptionKey = stackalloc byte[Session.KeySize]; cryptoRandom.GenerateRandomBytes(encryptionKey); PendingNonceKey pendingNonceKey = new(receiver.Address, NonceKey.From(nonce)); - byte[] initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); - return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false); + initialPacket = packetCodec.EncodeOrdinary(receiver.Id, encryptionKey, message, nonce); + return pendingNonceKey; } private async Task SendPendingPacket( @@ -291,24 +300,37 @@ private async Task SendPendingPacket( } private async Task SendResponse(Node receiver, Discv5Message message, CancellationToken token) + { + if (!TryEncodeResponse(receiver, message, out byte[]? packet)) + { + return; + } + + if (_logger.IsTrace) _logger.Trace($"Sending discv5 response {message.MessageType} {message.RequestId} to {receiver:s}, bytes: {packet.Length}."); + await discoveryHandler.SendAsync(packet, receiver.Address); + } + + [SkipLocalsInit] + private bool TryEncodeResponse(Node receiver, Discv5Message message, [NotNullWhen(true)] out byte[]? packet) { SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); if (!TryGetSession(sessionKey, out Session? session)) { - return; + packet = null; + return false; } Span writeKey = stackalloc byte[Session.KeySize]; if (!session.TryCopyWriteKey(writeKey)) { - return; + packet = null; + return false; } Span nonce = stackalloc byte[PacketCodec.NonceSize]; session.WriteNextNonce(cryptoRandom, nonce); - byte[] packet = packetCodec.EncodeOrdinary(receiver.Id, writeKey, message, nonce); - if (_logger.IsTrace) _logger.Trace($"Sending discv5 response {message.MessageType} {message.RequestId} to {receiver:s}, bytes: {packet.Length}."); - await discoveryHandler.SendAsync(packet, receiver.Address); + packet = packetCodec.EncodeOrdinary(receiver.Id, writeKey, message, nonce); + return true; } private async Task HandlePacket(PooledUdpReceiveResult udpPacket, CancellationToken token) @@ -372,10 +394,7 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati } SessionKey sessionKey = new(nodeId, endpoint); - Span readKey = stackalloc byte[Session.KeySize]; - if (!TryGetSession(sessionKey, out Session? session) || - !session.TryCopyReadKey(readKey) || - !packetCodec.TryDecryptMessage(packet, readKey, out Discv5Message message)) + if (!TryDecryptOrdinaryMessage(packet, sessionKey, out Session? session, out Discv5Message? message)) { if (_logger.IsTrace) _logger.Trace($"Discv5 ordinary packet from {endpoint} could not be decrypted with an existing session; sending WHOAREYOU."); await SendWhoAreYou(endpoint, packet, nodeId); @@ -393,6 +412,22 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati } } + [SkipLocalsInit] + private bool TryDecryptOrdinaryMessage(Packet packet, SessionKey sessionKey, [NotNullWhen(true)] out Session? session, [NotNullWhen(true)] out Discv5Message? message) + { + Span readKey = stackalloc byte[Session.KeySize]; + if (TryGetSession(sessionKey, out session) && + session.TryCopyReadKey(readKey) && + packetCodec.TryDecryptMessage(packet, readKey, out Discv5Message decodedMessage)) + { + message = decodedMessage; + return true; + } + + message = null; + return false; + } + private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, CancellationToken token) { if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) @@ -660,6 +695,7 @@ private void RegisterKnownRecord(Node node) } } + [SkipLocalsInit] internal Distances GetLookupDistances(Node receiver, PublicKey target) { int distance = _distance.CalculateLogDistance(receiver.Id.Hash, target.Hash); @@ -680,6 +716,7 @@ internal Distances GetLookupDistances(Node receiver, PublicKey target) return new Distances(distances[..count]); } + [SkipLocalsInit] private static string FormatDistances(Distances distances) { Span chars = stackalloc char[16]; @@ -702,6 +739,7 @@ private static string FormatDistances(Distances distances) return chars[..position].ToString(); } + [SkipLocalsInit] private RequestId CreateRequestId() { Span requestId = stackalloc byte[sizeof(ulong)]; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 40743f242529..d29185948bad 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using Autofac.Features.AttributeFilters; using Microsoft.Extensions.ObjectPool; @@ -58,6 +59,7 @@ public void Dispose() internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encryptionKey, Discv5Message message, ReadOnlySpan nonce) => EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, nonce, _localNodeId, encryptionKey, message); + [SkipLocalsInit] internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySpan requestNonce, ulong enrSequence, out Challenge challenge) { byte[] idNonce = _cryptoRandom.GenerateRandomBytes(IdNonceSize); @@ -70,6 +72,7 @@ internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySp return packet; } + [SkipLocalsInit] internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Discv5Message message, out Session session) { using PrivateKey ephemeralKey = new PrivateKeyGenerator(_cryptoRandom).Generate(); @@ -141,6 +144,7 @@ internal static bool TryDecode(ReadOnlyMemory packetMemory, ReadOnlySpan packetMemory, Aes localNodeMaskingAes, out Packet decoded) { decoded = default; @@ -368,6 +372,7 @@ private byte[] EncodePacket( return EncodePacketCore(destinationNodeId, flag, nonce, authData, encryptionKey, encodedMessage.AsSpan(), out messageAd); } + [SkipLocalsInit] private byte[] EncodePacketCore( ReadOnlySpan destinationNodeId, PacketFlag flag, @@ -458,6 +463,7 @@ private static void AesCtrTransform(ReadOnlySpan key, ReadOnlySpan i AesCtrTransform(aes, iv, input, output); } + [SkipLocalsInit] private static void AesCtrTransform(Aes aes, ReadOnlySpan iv, ReadOnlySpan input, Span output) { if (output.Length < input.Length) @@ -552,6 +558,7 @@ private void DeriveKeys( DeriveKeys(sharedPoint, initiatorNodeId, recipientNodeId, challengeData, out initiatorKey, out recipientKey); } + [SkipLocalsInit] private static void DeriveKeys( byte[] secret, ReadOnlySpan initiatorNodeId, @@ -589,6 +596,7 @@ internal static (byte[] InitiatorKey, byte[] RecipientKey) DeriveKeysForTest( return (initiatorKey, recipientKey); } + [SkipLocalsInit] private void SignIdNonce( ReadOnlySpan challengeData, ReadOnlySpan ephemeralPublicKey, @@ -601,6 +609,7 @@ private void SignIdNonce( signature.Bytes[..IdSignatureSize].CopyTo(destination); } + [SkipLocalsInit] private bool VerifyIdSignature( CompressedPublicKey signer, ReadOnlySpan signatureBytes, @@ -630,6 +639,7 @@ internal static byte[] CalculateIdSignatureHashForTest(byte[] challengeData, byt return signingHash; } + [SkipLocalsInit] private static void CalculateIdSignatureHash( ReadOnlySpan challengeData, ReadOnlySpan ephemeralPublicKey, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs index 9b45a688034b..8139e2ba8a8c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Runtime.CompilerServices; using Nethermind.Core.Collections; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Serialization.Rlp; @@ -57,6 +58,7 @@ private static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) return RequestId.From(requestId); } + [SkipLocalsInit] private static void EncodeRequestId(NettyRlpStream stream, RequestId requestId) { Span bytes = stackalloc byte[RequestId.MaxLength]; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs index d79fc760f1b9..a501696c7f19 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/Hash256KademliaDistance.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Numerics; +using System.Runtime.CompilerServices; using Nethermind.Core.Crypto; using Nethermind.Kademlia; @@ -24,6 +25,7 @@ public sealed class Hash256KademliaDistance : IKademliaDistance public Hash256 Zero => Hash256.Zero; /// + [SkipLocalsInit] public int CalculateLogDistance(Hash256 left, Hash256 right) { Span xorDistance = stackalloc byte[Hash256.Size]; @@ -53,6 +55,7 @@ public int CalculateLogDistance(Hash256 left, Hash256 right) } /// + [SkipLocalsInit] public int Compare(Hash256 left, Hash256 right, Hash256 target) { Span leftDistance = stackalloc byte[Hash256.Size]; @@ -73,6 +76,7 @@ public bool GetBit(Hash256 key, int index) } /// + [SkipLocalsInit] public Hash256 SetBit(Hash256 key, int index) { Span bytes = stackalloc byte[Hash256.Size]; @@ -90,6 +94,7 @@ public Hash256 GetRandomHashAtDistance(Hash256 currentHash, int distance) => /// /// Creates a random 256-bit key at the requested XOR log distance from . /// + [SkipLocalsInit] public Hash256 GetRandomHashAtDistance(Hash256 currentHash, int distance, Random random) { if ((uint)distance > MaxDistance) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs index ba5839cb1254..8afc478b936d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/PublicKeyKeyOperator.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Runtime.CompilerServices; using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Stats.Model; @@ -21,6 +22,7 @@ public sealed class PublicKeyKeyOperator : IKeyOperator + [SkipLocalsInit] public PublicKey CreateRandomKeyAtDistance(Hash256 nodePrefix, int depth) { Span randomBytes = stackalloc byte[PublicKey.LengthInBytes]; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj b/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj index 619eb40c09da..10167006d534 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj +++ b/src/Nethermind/Nethermind.Network.Discovery/Nethermind.Network.Discovery.csproj @@ -1,6 +1,7 @@ + true enable enable diff --git a/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs b/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs index f9c14a9f9dec..844564be9928 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Serializers/IPAddressRlp.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Sockets; +using System.Runtime.CompilerServices; using Nethermind.Serialization.Rlp; namespace Nethermind.Network.Discovery.Serializers; @@ -17,6 +18,7 @@ public static int GetLength(IPAddress ip) _ => Rlp.LengthOf(ip.GetAddressBytes()) }; + [SkipLocalsInit] public static void Encode(RlpStream stream, IPAddress ip) { Span bytes = stackalloc byte[16]; From 43cc5d257f60bcfdfd7225f7b2983e4dfae2a46b Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 10 Jun 2026 10:01:33 +0300 Subject: [PATCH 158/182] Remove unused discovery usings --- src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs | 2 -- src/Nethermind/Nethermind.Kademlia/KBucketTree.cs | 1 - src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs | 1 - src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs | 1 - .../Discv4/Kademlia/KademliaAdapterTests.cs | 3 +-- .../Discv5/Kademlia/KademliaAdapter.cs | 1 - .../Nethermind.Network.Discovery/Discv5/Packets/Session.cs | 1 - 7 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 02d3a5c417e2..aae1490bb942 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Threading; - namespace Nethermind.Kademlia; public class DoubleEndedLru(int capacity) diff --git a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index cba1bae3f017..2a4f5e91a236 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using Nethermind.Logging; namespace Nethermind.Kademlia; diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index d692efe20446..4642c882182f 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Collections.Concurrent; -using System.Threading; using Nethermind.Logging; namespace Nethermind.Kademlia; diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index 56693bf28800..a9a25dda6c0d 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Collections.Concurrent; -using System.Threading; using Nethermind.Logging; namespace Nethermind.Kademlia; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs index bfc8d2a3d853..8e1352a3e6c5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs @@ -11,9 +11,8 @@ using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; -using Nethermind.Crypto; -using Nethermind.Logging; using Nethermind.Kademlia; +using Nethermind.Logging; using Nethermind.Network.Config; using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Kademlia; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 6694ada05a51..34d7742cff1d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.CompilerServices; -using System.Threading; using Collections.Pooled; using Nethermind.Core.Caching; using Nethermind.Core.Collections; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs index 8a7d1c0e5892..308326569696 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Session.cs @@ -3,7 +3,6 @@ using System.Buffers.Binary; using System.Security.Cryptography; -using System.Threading; using Nethermind.Core.Crypto; using Nethermind.Crypto; From 9af85fc9bd4bc897880a57a654e582daebc06131 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 10 Jun 2026 17:01:01 +0300 Subject: [PATCH 159/182] More fixes --- .../Nethermind.Config/NetworkNode.cs | 1 + .../Caching/LruCacheTests.cs | 15 +- .../Nethermind.Kademlia/DoubleEndedLru.cs | 194 ++++++++++++++---- src/Nethermind/Nethermind.Kademlia/KBucket.cs | 14 +- .../Nethermind.Kademlia/KBucketTree.cs | 44 ++-- .../Nethermind.Kademlia/Kademlia.cs | 38 +--- .../Discv4/DiscoveryAppTests.cs | 31 +++ .../DiscoveryPersistenceManagerTests.cs | 16 +- .../Discv5/WireTests.cs | 22 +- .../Kademlia/DoubleEndedLruTests.cs | 95 +++++++++ .../Kademlia/KademliaTests.cs | 5 +- .../TestEnrBuilder.cs | 14 +- .../Discv4/DiscoveryApp.cs | 46 +++-- .../Kademlia/Handlers/NeighbourMsgHandler.cs | 9 +- .../Discv4/Kademlia/KademliaAdapter.cs | 8 +- .../Discv5/DiscoveryV5App.cs | 8 +- .../Discv5/Kademlia/KademliaAdapter.cs | 9 +- .../NetworkStorageTests.cs | 44 +--- .../PeerManagerFilteringIntegrationTests.cs | 1 - .../PeerManagerTests.cs | 4 - .../Nethermind.Network.Test/PeerPoolTests.cs | 1 - .../Nethermind.Network/INetworkStorage.cs | 1 - .../Nethermind.Network/NetworkStorage.cs | 8 - 23 files changed, 369 insertions(+), 259 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryAppTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DoubleEndedLruTests.cs diff --git a/src/Nethermind/Nethermind.Config/NetworkNode.cs b/src/Nethermind/Nethermind.Config/NetworkNode.cs index 4ac8a6e68f64..20aa7f559a59 100644 --- a/src/Nethermind/Nethermind.Config/NetworkNode.cs +++ b/src/Nethermind/Nethermind.Config/NetworkNode.cs @@ -91,6 +91,7 @@ public NetworkNode(PublicKey publicKey, string ip, int port, long reputation = 0 public string Host => IsEnode ? Enode.HostIp.ToString() : HostIp.ToString(); public IPAddress HostIp => IsEnode ? Enode.HostIp : Enr!.DiscoveryIp ?? IPAddress.None; public int Port => IsEnode ? Enode.Port : Enr!.DiscoveryPort ?? 0; + public int DiscoveryPort => IsEnode ? Enode.DiscoveryPort : Enr!.DiscoveryPort ?? 0; public long Reputation { get; set; } private PublicKey GetEnrPublicKey() diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index 512b9e300629..5d5d88d5a5f2 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -251,8 +251,7 @@ public void Can_remove_and_return_value() public void Eviction_callback_is_called_when_capacity_replaces_oldest() { int evicted = 0; - LruCache cache = new(2, "test"); - cache.OnEvict += value => evicted = value; + LruCache cache = new(2, "test", value => evicted = value); cache.Set(1, 10); cache.Set(2, 20); @@ -265,8 +264,7 @@ public void Eviction_callback_is_called_when_capacity_replaces_oldest() public void Eviction_callback_is_called_when_existing_value_is_replaced() { int evicted = 0; - LruCache cache = new(2, "test"); - cache.OnEvict += value => evicted = value; + LruCache cache = new(2, "test", value => evicted = value); cache.Set(1, 10); cache.Set(1, 11); @@ -279,8 +277,7 @@ public void Eviction_callback_is_called_when_existing_value_is_replaced() public void TryRemove_returns_value_without_calling_eviction_callback() { int evicted = 0; - LruCache cache = new(2, "test"); - cache.OnEvict += value => evicted = value; + LruCache cache = new(2, "test", value => evicted = value); cache.Set(1, 10); Assert.That(cache.TryRemove(1, out int removed), Is.True); @@ -320,8 +317,7 @@ public async Task Clear_invokes_eviction_callback_outside_lock() { LruCache cache = null!; TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); - cache = new LruCache(2, "test"); - cache.OnEvict += _ => callbackResult.SetResult(cache.Contains(1)); + cache = new LruCache(2, "test", _ => callbackResult.SetResult(cache.Contains(1))); cache.Set(1, 10); Task clearTask = Task.Run(cache.Clear); @@ -339,8 +335,7 @@ public async Task Eviction_callback_is_invoked_outside_lock(EvictionOperation op { LruCache cache = null!; TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); - cache = new LruCache(2, "test"); - cache.OnEvict += _ => callbackResult.SetResult(cache.Contains(1)); + cache = new LruCache(2, "test", _ => callbackResult.SetResult(cache.Contains(1))); cache.Set(1, 10); if (operation == EvictionOperation.ReplaceOldest) { diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index aae1490bb942..98cafc032ac8 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -3,44 +3,75 @@ namespace Nethermind.Kademlia; +/// +/// Fixed-capacity LRU map with O(1) access to both the most and the least recently used entry. +/// +/// +/// Entries live in a preallocated array threaded into an intrusive doubly-linked list (head = most recently +/// used, tail = least recently used); removed slots are recycled through a free list, so steady-state +/// operations do not allocate. All members are thread-safe. +/// public class DoubleEndedLru(int capacity) where TKey : notnull where TValue : notnull { + private const int None = -1; + + private struct Entry + { + public TKey Key; + public TValue Value; + public int Prev; + public int Next; + } + private readonly Lock _lock = new(); + private readonly Entry[] _entries = new Entry[capacity]; + private readonly Dictionary _index = new(capacity); + private int _head = None; + private int _tail = None; + private int _freeList = None; + private int _used; - private readonly LinkedList<(TKey Key, TValue Value)> _queue = new(); - private readonly Dictionary> _index = new(capacity); public int Count { get { lock (_lock) { - return _queue.Count; + return _index.Count; } } } - public BucketAddResult AddOrRefresh(in TKey key, TValue value) + public BucketAddResult AddOrRefresh(in TKey key, TValue value) => AddOrRefresh(key, value, out _); + + /// The value previously stored under when the entry is refreshed; default otherwise. + public BucketAddResult AddOrRefresh(in TKey key, TValue value, out TValue? previous) { lock (_lock) { - if (_index.TryGetValue(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) + if (_index.TryGetValue(key, out int i)) { - _queue.Remove(listNode); - listNode.Value = (key, value); - _queue.AddFirst(listNode); + ref Entry entry = ref _entries[i]; + previous = entry.Value; + entry.Value = value; + MoveToHead(i); return BucketAddResult.Refreshed; } - if (_queue.Count >= capacity) + previous = default; + if (_index.Count >= _entries.Length) { return BucketAddResult.Full; } - listNode = _queue.AddFirst((key, value)); - _index.Add(key, listNode); + int slot = TakeSlot(); + ref Entry added = ref _entries[slot]; + added.Key = key; + added.Value = value; + LinkAtHead(slot); + _index.Add(key, slot); return BucketAddResult.Added; } } @@ -49,19 +80,19 @@ public bool TryPopHead(out TKey key, out TValue? value) { lock (_lock) { - LinkedListNode<(TKey Key, TValue Value)>? front = _queue.First; - if (front is null) + if (_head == None) { key = default!; value = default; return false; } - _queue.Remove(front); - key = front.Value.Key; - value = front.Value.Value; - _index.Remove(front.Value.Key); - + int head = _head; + key = _entries[head].Key; + value = _entries[head].Value; + _index.Remove(key); + Unlink(head); + ReleaseSlot(head); return true; } } @@ -70,14 +101,13 @@ public bool TryGetLast(out TValue? last) { lock (_lock) { - LinkedListNode<(TKey Key, TValue Value)>? lastNode = _queue.Last; - if (lastNode is null) + if (_tail == None) { last = default; return false; } - last = lastNode.Value.Value; + last = _entries[_tail].Value; return true; } } @@ -86,13 +116,14 @@ public bool Remove(TKey key) { lock (_lock) { - if (_index.Remove(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode)) + if (!_index.Remove(key, out int i)) { - _queue.Remove(listNode); - return true; + return false; } - return false; + Unlink(i); + ReleaseSlot(i); + return true; } } @@ -100,11 +131,11 @@ public TValue[] GetAll() { lock (_lock) { - TValue[] result = new TValue[_queue.Count]; - int i = 0; - foreach ((TKey Key, TValue Value) entry in _queue) + TValue[] result = new TValue[_index.Count]; + int n = 0; + for (int i = _head; i != None; i = _entries[i].Next) { - result[i++] = entry.Value; + result[n++] = _entries[i].Value; } return result; @@ -115,11 +146,11 @@ public TValue[] GetAll() { lock (_lock) { - (TKey Key, TValue Value)[] result = new (TKey Key, TValue Value)[_queue.Count]; - int i = 0; - foreach ((TKey Key, TValue Value) entry in _queue) + (TKey Key, TValue Value)[] result = new (TKey Key, TValue Value)[_index.Count]; + int n = 0; + for (int i = _head; i != None; i = _entries[i].Next) { - result[i++] = entry; + result[n++] = (_entries[i].Key, _entries[i].Value); } return result; @@ -130,13 +161,13 @@ internal int CopyAllWithKey((TKey Key, TValue Value)[] destination) { lock (_lock) { - int i = 0; - foreach ((TKey Key, TValue Value) entry in _queue) + int n = 0; + for (int i = _head; i != None; i = _entries[i].Next) { - destination[i++] = entry; + destination[n++] = (_entries[i].Key, _entries[i].Value); } - return i; + return n; } } @@ -152,9 +183,92 @@ public bool Contains(in TKey key) { lock (_lock) { - return _index.TryGetValue(key, out LinkedListNode<(TKey Key, TValue Value)>? listNode) - ? listNode.Value.Value - : default; + return _index.TryGetValue(key, out int i) ? _entries[i].Value : default; + } + } + + public void Clear() + { + lock (_lock) + { + _index.Clear(); + Array.Clear(_entries); + _head = None; + _tail = None; + _freeList = None; + _used = 0; + } + } + + private int TakeSlot() + { + if (_freeList == None) + { + return _used++; + } + + int slot = _freeList; + _freeList = _entries[slot].Next; + return slot; + } + + private void ReleaseSlot(int i) + { + ref Entry entry = ref _entries[i]; + // Drop the key/value references so released slots do not keep them alive. + entry.Key = default!; + entry.Value = default!; + entry.Next = _freeList; + _freeList = i; + } + + private void LinkAtHead(int i) + { + ref Entry entry = ref _entries[i]; + entry.Prev = None; + entry.Next = _head; + if (_head != None) + { + _entries[_head].Prev = i; + } + else + { + _tail = i; + } + + _head = i; + } + + private void MoveToHead(int i) + { + if (_head == i) + { + return; + } + + Unlink(i); + LinkAtHead(i); + } + + private void Unlink(int i) + { + ref Entry entry = ref _entries[i]; + if (entry.Prev == None) + { + _head = entry.Next; + } + else + { + _entries[entry.Prev].Next = entry.Next; + } + + if (entry.Next == None) + { + _tail = entry.Prev; + } + else + { + _entries[entry.Next].Prev = entry.Prev; } } } diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index 6f7c7af4df28..fd06026683dd 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -8,9 +8,8 @@ public class KBucket(int k) where TNode : notnull where TKadKey : notnull { - private readonly int _k = k; - private DoubleEndedLru _items = new(k); - private DoubleEndedLru _replacement = new(k); + private readonly DoubleEndedLru _items = new(k); + private readonly DoubleEndedLru _replacement = new(k); public int Count => _items.Count; @@ -25,8 +24,7 @@ public class KBucket(int k) /// public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? toRefresh) { - TNode? previous = _items.GetByKey(hash); - BucketAddResult addResult = _items.AddOrRefresh(hash, item); + BucketAddResult addResult = _items.AddOrRefresh(hash, item, out TNode? previous); if (addResult == BucketAddResult.Added || (addResult == BucketAddResult.Refreshed && ShouldUpdateCachedArray(previous, item))) { @@ -66,9 +64,9 @@ public bool RemoveAndReplace(in TKadKey hash) public void Clear() { - _items = new DoubleEndedLru(_k); - _replacement = new DoubleEndedLru(_k); - _cachedArray = _items.GetAll(); + _items.Clear(); + _replacement.Clear(); + _cachedArray = []; } public bool ContainsNode(in TKadKey hash) => _items.Contains(hash); diff --git a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index 2a4f5e91a236..fb888b6eac6f 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Text; +using Collections.Pooled; using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -177,27 +178,23 @@ public TNode[] GetAllAtDistance(int distance) lock (_lock) { if (_logger.IsDebug) _logger.Debug($"Getting all nodes at distance {distance}"); - TNode[] result = ArrayPool.Shared.Rent(_k); + using PooledList result = new(_k); (TKadKey Hash, TNode Node)[] bucketEntries = ArrayPool<(TKadKey Hash, TNode Node)>.Shared.Rent(_k); - int count = 0; try { - GetAllAtDistanceRecursive(_root, 0, distance, ref result, ref count, bucketEntries); - if (_logger.IsDebug) _logger.Debug($"Found {count} nodes at distance {distance}"); + GetAllAtDistanceRecursive(_root, 0, distance, result, bucketEntries); + if (_logger.IsDebug) _logger.Debug($"Found {result.Count} nodes at distance {distance}"); - TNode[] copy = new TNode[count]; - Array.Copy(result, copy, count); - return copy; + return result.Span.ToArray(); } finally { ArrayPool<(TKadKey Hash, TNode Node)>.Shared.Return(bucketEntries, RuntimeHelpers.IsReferenceOrContainsReferences<(TKadKey Hash, TNode Node)>()); - ArrayPool.Shared.Return(result, RuntimeHelpers.IsReferenceOrContainsReferences()); } } } - private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, ref TNode[] result, ref int count, (TKadKey Hash, TNode Node)[] bucketEntries) + private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, PooledList result, (TKadKey Hash, TNode Node)[] bucketEntries) { int targetDepth = _distance.MaxDistance - distance; if (node.IsLeaf) @@ -210,7 +207,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, r (TKadKey hash, TNode item) = bucketEntries[i]; if (_distance.CalculateLogDistance(hash, _currentNodeHash) == distance) { - AddResult(item, ref result, ref count); + result.Add(item); } } } @@ -219,7 +216,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, r TNode[] items = node.Bucket.GetAll(); for (int i = 0; i < items.Length; i++) { - AddResult(items[i], ref result, ref count); + result.Add(items[i]); } } } @@ -230,11 +227,11 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, r bool goRight = _distance.GetBit(_currentNodeHash, depth); if (goRight) { - GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, ref result, ref count, bucketEntries); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result, bucketEntries); } else { - GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, ref result, ref count, bucketEntries); + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result, bucketEntries); } } else if (depth == targetDepth) @@ -243,34 +240,21 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, r // Note: We go the opposite direction here, as the same direction would have a distance + 1 if (goRight) { - GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, ref result, ref count, bucketEntries); + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result, bucketEntries); } else { - GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, ref result, ref count, bucketEntries); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result, bucketEntries); } } else { - GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, ref result, ref count, bucketEntries); - GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, ref result, ref count, bucketEntries); + GetAllAtDistanceRecursive(node.Left!, depth + 1, distance, result, bucketEntries); + GetAllAtDistanceRecursive(node.Right!, depth + 1, distance, result, bucketEntries); } } } - private static void AddResult(TNode item, ref TNode[] result, ref int count) - { - if (count == result.Length) - { - TNode[] expanded = ArrayPool.Shared.Rent(Math.Max(1, result.Length * 2)); - Array.Copy(result, expanded, result.Length); - ArrayPool.Shared.Return(result, RuntimeHelpers.IsReferenceOrContainsReferences()); - result = expanded; - } - - result[count++] = item; - } - public IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> IterateBuckets() { lock (_lock) diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index 2e617b804a50..ef06d4a2fe24 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -1,9 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Buffers; using System.Diagnostics; -using System.Runtime.CompilerServices; using Collections.Pooled; using Nethermind.Logging; @@ -157,13 +155,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => // Refresh stale non-empty buckets one by one. Protocols whose wire lookup target cannot be synthesized from a // bucket prefix may return a best-effort random lookup key here; discv4 public keys are one example. - int activeBucketPrefixCapacity; - lock (_lastBucketRefreshLock) - { - activeBucketPrefixCapacity = _lastBucketRefreshTicks.Count; - } - - using PooledSet activeBucketPrefixes = new(activeBucketPrefixCapacity); + using PooledSet activeBucketPrefixes = new(); foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) { activeBucketPrefixes.Add(Prefix); @@ -200,35 +192,17 @@ private bool ShouldRefreshBucket(TKadKey prefix, KBucket bucket) } } - private void PruneLastBucketRefreshTicks(PooledSet activeBucketPrefixes) + private void PruneLastBucketRefreshTicks(ISet activeBucketPrefixes) { lock (_lastBucketRefreshLock) { - TKadKey[] stalePrefixes = ArrayPool.Shared.Rent(_lastBucketRefreshTicks.Count); - int stalePrefixCount = 0; - try + // Dictionary.Remove is safe during key enumeration since .NET Core 3.0. + foreach (TKadKey prefix in _lastBucketRefreshTicks.Keys) { - foreach (TKadKey prefix in _lastBucketRefreshTicks.Keys) + if (!activeBucketPrefixes.Contains(prefix)) { - if (!activeBucketPrefixes.Contains(prefix)) - { - stalePrefixes[stalePrefixCount++] = prefix; - } + _lastBucketRefreshTicks.Remove(prefix); } - - if (stalePrefixCount == 0) - { - return; - } - - for (int i = 0; i < stalePrefixCount; i++) - { - _lastBucketRefreshTicks.Remove(stalePrefixes[i]); - } - } - finally - { - ArrayPool.Shared.Return(stalePrefixes, RuntimeHelpers.IsReferenceOrContainsReferences()); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryAppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryAppTests.cs new file mode 100644 index 000000000000..2144931f2a01 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryAppTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Net; +using Nethermind.Config; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv4; +using Nethermind.Stats.Model; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Discv4; + +public class DiscoveryAppTests +{ + [Test] + public void Should_use_discovery_port_from_configured_enode_bootnode() + { + Enode enode = new(TestItem.PrivateKeyA.PublicKey, IPAddress.Parse("8.8.8.8"), 30303, discoveryPort: 9001); + + List bootNodes = DiscoveryApp.CreateBootNodes([new NetworkNode(enode)], LimboLogs.Instance.GetClassLogger()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(bootNodes, Has.Count.EqualTo(1)); + Assert.That(bootNodes[0].Port, Is.EqualTo(9001)); + Assert.That(bootNodes[0].Host, Is.EqualTo("8.8.8.8")); + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs index a24f762f529b..241282601427 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs @@ -9,7 +9,6 @@ using Nethermind.Config; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; -using Nethermind.Crypto; using Nethermind.Db; using Nethermind.Logging; using Nethermind.Kademlia; @@ -184,7 +183,7 @@ public async Task RunDiscoveryPersistenceCommit_Should_Update_Nodes_In_Storage() [Test] public async Task RunDiscoveryPersistenceCommit_Should_Preserve_Enr_In_Common_Storage() { - NodeRecord enr = CreateTestEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), 30303, 30304); + NodeRecord enr = TestEnrBuilder.BuildSigned(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), tcpPort: 30303, udpPort: 30304); Node node = new(TestItem.PrivateKeyA.PublicKey, "8.8.8.8", 30304) { Enr = enr.EnrString @@ -221,18 +220,5 @@ public async Task RunDiscoveryPersistenceCommit_Should_Preserve_Enr_In_Common_St } } - private static NodeRecord CreateTestEnr(PrivateKey privateKey, IPAddress ipAddress, int tcpPort, int udpPort) - { - NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); - enr.SetEntry(new IpEntry(ipAddress)); - enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - enr.SetEntry(new TcpEntry(tcpPort)); - enr.SetEntry(new UdpEntry(udpPort)); - enr.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); - - return enr; - } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index deceb31d9773..210804a88778 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -265,24 +265,10 @@ private sealed record TestPeer( TestNodeRecordProvider NodeRecordProvider, IPEndPoint Endpoint); - private sealed class TestNodeRecordProvider : INodeRecordProvider + private sealed class TestNodeRecordProvider(PrivateKey privateKey, IPEndPoint endpoint, bool includeEndpoint) : INodeRecordProvider { - public TestNodeRecordProvider(PrivateKey privateKey, IPEndPoint endpoint, bool includeEndpoint) - { - NodeRecord nodeRecord = new(); - nodeRecord.SetEntry(IdEntry.Instance); - if (includeEndpoint) - { - nodeRecord.SetEntry(new IpEntry(endpoint.Address)); - nodeRecord.SetEntry(new TcpEntry(endpoint.Port)); - nodeRecord.SetEntry(new UdpEntry(endpoint.Port)); - } - nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); - nodeRecord.EnrSequence = 1; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(nodeRecord); - Current = nodeRecord; - } - - public NodeRecord Current { get; } + public NodeRecord Current { get; } = includeEndpoint + ? TestEnrBuilder.BuildSigned(privateKey, endpoint.Address, tcpPort: endpoint.Port, udpPort: endpoint.Port) + : TestEnrBuilder.BuildSignedWithoutEndpoint(privateKey); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DoubleEndedLruTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DoubleEndedLruTests.cs new file mode 100644 index 000000000000..a73eb997863b --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DoubleEndedLruTests.cs @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Kademlia; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class DoubleEndedLruTests +{ + [Test] + public void AddOrRefresh_ReturnsAddedUntilCapacity_ThenFull() + { + DoubleEndedLru lru = new(2); + + Assert.That(lru.AddOrRefresh("a", "1"), Is.EqualTo(BucketAddResult.Added)); + Assert.That(lru.AddOrRefresh("b", "2"), Is.EqualTo(BucketAddResult.Added)); + Assert.That(lru.AddOrRefresh("c", "3"), Is.EqualTo(BucketAddResult.Full)); + Assert.That(lru.Count, Is.EqualTo(2)); + Assert.That(lru.Contains("c"), Is.False); + } + + [Test] + public void AddOrRefresh_RefreshMovesEntryToHeadAndReturnsPreviousValue() + { + DoubleEndedLru lru = new(3); + lru.AddOrRefresh("a", "1"); + lru.AddOrRefresh("b", "2"); + lru.AddOrRefresh("c", "3"); + + Assert.That(lru.AddOrRefresh("a", "1b", out string? previous), Is.EqualTo(BucketAddResult.Refreshed)); + + Assert.That(previous, Is.EqualTo("1")); + Assert.That(lru.GetByKey("a"), Is.EqualTo("1b")); + Assert.That(lru.GetAll(), Is.EqualTo(new[] { "1b", "3", "2" })); + Assert.That(lru.GetAllWithKey(), Is.EqualTo(new[] { ("a", "1b"), ("c", "3"), ("b", "2") })); + } + + [Test] + public void TryPopHead_RemovesMostRecentEntry() + { + DoubleEndedLru lru = new(3); + lru.AddOrRefresh("a", "1"); + lru.AddOrRefresh("b", "2"); + + Assert.That(lru.TryPopHead(out string key, out string? value), Is.True); + Assert.That((key, value), Is.EqualTo(("b", "2"))); + Assert.That(lru.GetAll(), Is.EqualTo(new[] { "1" })); + + Assert.That(lru.TryPopHead(out _, out _), Is.True); + Assert.That(lru.TryPopHead(out _, out _), Is.False); + } + + [Test] + public void TryGetLast_ReturnsLeastRecentEntry() + { + DoubleEndedLru lru = new(3); + Assert.That(lru.TryGetLast(out _), Is.False); + + lru.AddOrRefresh("a", "1"); + lru.AddOrRefresh("b", "2"); + lru.AddOrRefresh("a", "1"); + + Assert.That(lru.TryGetLast(out string? last), Is.True); + Assert.That(last, Is.EqualTo("2")); + } + + [Test] + public void Remove_FreesCapacityForNewEntries() + { + DoubleEndedLru lru = new(2); + lru.AddOrRefresh("a", "1"); + lru.AddOrRefresh("b", "2"); + + Assert.That(lru.Remove("a"), Is.True); + Assert.That(lru.Remove("a"), Is.False); + Assert.That(lru.AddOrRefresh("c", "3"), Is.EqualTo(BucketAddResult.Added)); + Assert.That(lru.GetAll(), Is.EqualTo(new[] { "3", "2" })); + } + + [Test] + public void Clear_EmptiesAndAllowsReuse() + { + DoubleEndedLru lru = new(2); + lru.AddOrRefresh("a", "1"); + lru.AddOrRefresh("b", "2"); + + lru.Clear(); + + Assert.That(lru.Count, Is.Zero); + Assert.That(lru.GetAll(), Is.Empty); + Assert.That(lru.AddOrRefresh("c", "3"), Is.EqualTo(BucketAddResult.Added)); + Assert.That(lru.GetAll(), Is.EqualTo(new[] { "3" })); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 0a268fea9f2b..629498e9a562 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Autofac; -using Collections.Pooled; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Logging; @@ -214,9 +213,7 @@ public void PruneLastBucketRefreshTicks_removes_stale_prefixes_even_when_counts_ lastRefreshTicks[activePrefix] = 1; lastRefreshTicks[stalePrefix] = 2; - using PooledSet activePrefixes = new(2); - activePrefixes.Add(activePrefix); - activePrefixes.Add(new("0x3333333333333333333333333333333333333333333333333333333333333333")); + HashSet activePrefixes = [activePrefix, new("0x3333333333333333333333333333333333333333333333333333333333333333")]; typeof(Nethermind.Kademlia.Kademlia) .GetMethod("PruneLastBucketRefreshTicks", BindingFlags.Instance | BindingFlags.NonPublic)! diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs index 5baa173cbe3b..7b2863d43bea 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/TestEnrBuilder.cs @@ -24,7 +24,6 @@ public static NodeRecord BuildSigned( IPAddress ip = ipAddress ?? IPAddress.Loopback; bool isIpv6 = ip.AddressFamily == AddressFamily.InterNetworkV6; NodeRecord enr = new(); - enr.SetEntry(IdEntry.Instance); if (isIpv6) { enr.SetEntry(new Ip6Entry(ip)); @@ -51,9 +50,20 @@ public static NodeRecord BuildSigned( } configureExtras?.Invoke(enr); enr.EnrSequence = enrSequence; - new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); + Sign(enr, privateKey); return enr; } + + public static NodeRecord BuildSignedWithoutEndpoint(PrivateKey privateKey, ulong enrSequence = 1) + { + NodeRecord enr = new(); + enr.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + enr.EnrSequence = enrSequence; + Sign(enr, privateKey); + return enr; + } + + private static void Sign(NodeRecord enr, PrivateKey privateKey) => new NodeRecordSigner(new EthereumEcdsa(0), privateKey).Sign(enr); } internal sealed class TestEth2Entry() : EnrContentEntry([1, 2, 3, 4]) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs index d1bc50d55c54..588e8cd1716b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs @@ -36,12 +36,32 @@ public DiscoveryApp( ILogManager logManager, Action? configureDiscv4Services = null) : base("discv4", networkConfig, processExitSource, logManager.GetClassLogger()) + { + List bootNodes = CreateBootNodes(networkConfig.Bootnodes, Logger); + + _discv4Services = rootScope.BeginLifetimeScope( + (builder) => + { + builder + .AddModule(new KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddSingleton(); + + configureDiscv4Services?.Invoke(builder); + }); + + DiscV4Services services = _discv4Services.Resolve(); + _persistenceManager = services.PersistenceManager; + _discv4Adapter = services.Discv4Adapter; + _discoveryHandlerFactory = services.NettyDiscoveryHandlerFactory; + UseKademliaServices(services.NodeSource, services.Kademlia); + } + + internal static List CreateBootNodes(NetworkNode[] configuredBootnodes, ILogger logger) { List bootNodes = []; - NetworkNode[] configuredBootnodes = networkConfig.Bootnodes; if (configuredBootnodes.Length == 0) { - if (Logger.IsWarn) Logger.Warn("No bootnodes specified in configuration"); + if (logger.IsWarn) logger.Warn("No bootnodes specified in configuration"); } for (int i = 0; i < configuredBootnodes.Length; i++) @@ -49,34 +69,20 @@ public DiscoveryApp( NetworkNode bootnode = configuredBootnodes[i]; if (!bootnode.IsEnode) { - if (Logger.IsTrace) Logger.Trace($"Ignoring ENR in discovery V4: {bootnode}"); + if (logger.IsTrace) logger.Trace($"Ignoring ENR in discovery V4: {bootnode}"); continue; } if (bootnode.NodeId is null) { - Logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); + logger.Warn($"Bootnode ignored because of missing node ID: {bootnode}"); continue; } - bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.Port)); + bootNodes.Add(new(bootnode.NodeId, bootnode.Host, bootnode.DiscoveryPort)); } - _discv4Services = rootScope.BeginLifetimeScope( - (builder) => - { - builder - .AddModule(new KademliaModule(nodeKey.PublicKey, bootNodes)) - .AddSingleton(); - - configureDiscv4Services?.Invoke(builder); - }); - - DiscV4Services services = _discv4Services.Resolve(); - _persistenceManager = services.PersistenceManager; - _discv4Adapter = services.Discv4Adapter; - _discoveryHandlerFactory = services.NettyDiscoveryHandlerFactory; - UseKademliaServices(services.NodeSource, services.Kademlia); + return bootNodes; } /// diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs index 530f585797a4..df00efcf17e5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs @@ -62,14 +62,7 @@ private Node[] GetCurrentNodes() { lock (_lock) { - if (_count == 0) - { - return []; - } - - Node[] nodes = new Node[_count]; - Array.Copy(_nodes, nodes, _count); - return nodes; + return _nodes.AsSpan(0, _count).ToArray(); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index d4a9b102699c..eaaeed4082ec 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -87,13 +87,7 @@ private void AddMessageHandler( MsgType msgType, ValueHash256 nodeId, IMessageHandler handler) => _incomingMessageHandlers.AddOrUpdate( (nodeId, msgType), (_) => [handler], - (_, currentHandler) => - { - IMessageHandler[] newValue = new IMessageHandler[currentHandler.Length + 1]; - Array.Copy(currentHandler, newValue, currentHandler.Length); - newValue[^1] = handler; - return newValue; - } + (_, currentHandler) => [.. currentHandler, handler] ); private void RemoveMessageHandler( diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 7978df800869..71d908467bf0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -147,18 +147,18 @@ public override void AddNodeToDiscovery(Node node) } } - private BootNodeAddResult AddBootNode(List bootNodes, PooledSet seen, NetworkNode networkNode) + private BootNodeAddResult AddBootNode(List bootNodes, ISet seen, NetworkNode networkNode) { if (networkNode.IsEnr) { return AddBootNode(bootNodes, seen, networkNode.Enr); } - Node node = new(networkNode.NodeId, networkNode.Host, networkNode.Enode.DiscoveryPort); + Node node = new(networkNode.NodeId, networkNode.Host, networkNode.DiscoveryPort); return AddBootNode(bootNodes, seen, node); } - private BootNodeAddResult AddBootNode(List bootNodes, PooledSet seen, NodeRecord nodeRecord) + private BootNodeAddResult AddBootNode(List bootNodes, ISet seen, NodeRecord nodeRecord) { if (TryGetAcceptableNodeFromEnr(nodeRecord, out Node? node)) { @@ -168,7 +168,7 @@ private BootNodeAddResult AddBootNode(List bootNodes, PooledSet s return BootNodeAddResult.Skipped; } - private BootNodeAddResult AddBootNode(List bootNodes, PooledSet seen, Node node) + private BootNodeAddResult AddBootNode(List bootNodes, ISet seen, Node node) { if (!seen.Add(node.IdHash)) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 34d7742cff1d..895378c2a169 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -230,7 +230,8 @@ private async Task SendRequest( return await SendPendingPacket(receiver, message, pendingNonceKey, packet, hasSession: true); } - return await SendMessageWithoutSession(receiver, message); + pendingNonceKey = EncodeMessageWithoutSession(receiver, message, out byte[] initialPacket); + return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false); } [SkipLocalsInit] @@ -259,12 +260,6 @@ private bool TryEncodeWithExistingSession( return false; } - private async Task SendMessageWithoutSession(Node receiver, Discv5Message message) - { - PendingNonceKey pendingNonceKey = EncodeMessageWithoutSession(receiver, message, out byte[] initialPacket); - return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false); - } - [SkipLocalsInit] private PendingNonceKey EncodeMessageWithoutSession(Node receiver, Discv5Message message, out byte[] initialPacket) { diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs index 225eff921f03..30e686a8e8c5 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Collections.Generic; using System.Linq; using Nethermind.Config; using Nethermind.Core; @@ -150,18 +149,17 @@ public void Can_store_peers() } [Test] - public void Discard_batch_drops_pending_nodes() + public void Start_batch_discards_pending_nodes_from_stale_batch() { NetworkStorage storage = new(new SnapshotableMemDb(), LimboLogs.Instance); NetworkNode node = new(TestItem.PublicKeyA, "192.1.1.1", 3441, 0L); storage.StartBatch(); storage.UpdateNode(node); - storage.DiscardBatch(); + storage.StartBatch(); Assert.That(storage.GetPersistedNodes(), Is.Empty); - storage.StartBatch(); storage.UpdateNode(node); storage.Commit(); @@ -195,52 +193,20 @@ public void Failed_commit_reloads_persisted_nodes_before_new_updates() } } - private sealed class FailingBatchDb : IFullDb + private sealed class FailingBatchDb : MemDb { - private readonly SnapshotableMemDb _inner = new(); - public bool ThrowOnNextBatchDispose { get; set; } - public string Name => _inner.Name; - - public byte[]? this[ReadOnlySpan key] - { - get => _inner[key]; - set => _inner[key] = value; - } - - public KeyValuePair[] this[byte[][] keys] => _inner[keys]; - - public ICollection Keys => _inner.Keys; - - public ICollection Values => _inner.Values; - - public int Count => _inner.Count; - - public IEnumerable> GetAll(bool ordered = false) => _inner.GetAll(ordered); - - public IEnumerable GetAllKeys(bool ordered = false) => _inner.GetAllKeys(ordered); - - public IEnumerable GetAllValues(bool ordered = false) => _inner.GetAllValues(ordered); - - public byte[]? Get(ReadOnlySpan key, ReadFlags flags = ReadFlags.None) => _inner.Get(key, flags); - - public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) => _inner.Set(key, value, flags); - - public IWriteBatch StartWriteBatch() + public override IWriteBatch StartWriteBatch() { if (!ThrowOnNextBatchDispose) { - return _inner.StartWriteBatch(); + return base.StartWriteBatch(); } ThrowOnNextBatchDispose = false; return new FailingWriteBatch(); } - - public void Flush(bool onlyWal = false) => _inner.Flush(onlyWal); - - public void Dispose() => _inner.Dispose(); } private sealed class FailingWriteBatch : IWriteBatch diff --git a/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs index 3edcd90f3c3a..32f68a0d0346 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerManagerFilteringIntegrationTests.cs @@ -254,7 +254,6 @@ public void UpdateNodes(IEnumerable nodes) public void RemoveNode(PublicKey nodeId) => _pendingChanges = true; public void StartBatch() { } public void Commit() { } - public void DiscardBatch() { } public bool AnyPendingChange() => _pendingChanges; } } diff --git a/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs index b9937b0bf28a..7e89e511ef75 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerManagerTests.cs @@ -1006,10 +1006,6 @@ public void Commit() { } - public void DiscardBatch() - { - } - private bool _pendingChanges; public int PersistedNodesCount => _nodes.Count; diff --git a/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs b/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs index ae0e6f708230..d9339a6aefb6 100644 --- a/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/PeerPoolTests.cs @@ -229,7 +229,6 @@ private sealed class TestNetworkStorage : INetworkStorage public void RemoveNode(PublicKey nodeId) => Pending = true; public void StartBatch() { Interlocked.Increment(ref _startBatchCountBacking); StartBatchCount = _startBatchCountBacking; } public void Commit() { Interlocked.Increment(ref _commitCountBacking); CommitCount = _commitCountBacking; } - public void DiscardBatch() { } public bool AnyPendingChange() => Pending; private int _commitCountBacking; diff --git a/src/Nethermind/Nethermind.Network/INetworkStorage.cs b/src/Nethermind/Nethermind.Network/INetworkStorage.cs index 67918d1ca608..0aa0fbdc6015 100644 --- a/src/Nethermind/Nethermind.Network/INetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/INetworkStorage.cs @@ -17,7 +17,6 @@ public interface INetworkStorage void RemoveNode(PublicKey nodeId); void StartBatch(); void Commit(); - void DiscardBatch(); bool AnyPendingChange(); } } diff --git a/src/Nethermind/Nethermind.Network/NetworkStorage.cs b/src/Nethermind/Nethermind.Network/NetworkStorage.cs index 114c328e34cc..80ff4ec8622c 100644 --- a/src/Nethermind/Nethermind.Network/NetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/NetworkStorage.cs @@ -190,14 +190,6 @@ public void Commit() } } - public void DiscardBatch() - { - lock (_lock) - { - DiscardBatchNoLock(); - } - } - private void DiscardBatchNoLock() { IWriteBatch? currentBatch = _currentBatch; From a49775a80475461959544b6651cc9d5d65f83ba7 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 11 Jun 2026 12:21:37 +0300 Subject: [PATCH 160/182] Null not expected --- .../LookupKNearestNeighbour.cs | 10 ---- .../Discv5/KademliaAdapterTests.cs | 54 +++++++++++-------- .../Kademlia/LookupKNearestNeighbourTests.cs | 44 --------------- .../Discv5/Kademlia/KademliaAdapter.cs | 12 +---- 4 files changed, 33 insertions(+), 87 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 4642c882182f..18760520ccc3 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -61,11 +61,6 @@ CancellationToken token foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) { - if (node is null) - { - continue; - } - TKadKey nodeHash = nodeHashProvider.GetHash(node); seen.TryAdd(nodeHash, node); bestSeen.Enqueue((nodeHash, node), nodeHash); @@ -209,11 +204,6 @@ void ProcessResult(TKadKey hash, TNode toQuery, (TNode, TNode[]? neighbours)? va foreach (TNode neighbour in neighbours) { - if (neighbour is null) - { - continue; - } - TKadKey neighbourHash = nodeHashProvider.GetHash(neighbour); // Already queried, we ignore diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 24448e8f9fdb..b430317c72cd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -5,10 +5,13 @@ using System.Net; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; +using Nethermind.Core.Test.Modules; using Nethermind.Crypto; using Nethermind.Kademlia; using Nethermind.Logging; +using Nethermind.Network.Discovery.Discv5; using Nethermind.Network.Discovery.Discv5.Kademlia; +using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; @@ -20,10 +23,18 @@ namespace Nethermind.Network.Discovery.Test.Discv5; public class KademliaAdapterTests { private IKademlia _kademlia = null!; + private PacketCodec? _packetCodec; [SetUp] public void SetUp() => _kademlia = Substitute.For>(); + [TearDown] + public void TearDown() + { + _packetCodec?.Dispose(); + _packetCodec = null; + } + [Test] public void GetNodesAtDistances_ShouldMapEachDistanceToKademliaTable() { @@ -59,20 +70,6 @@ public void GetNodesAtDistances_ShouldExcludeRequester() Assert.That(result, Is.EqualTo(new[] { returned })); } - [Test] - public void GetNodesAtDistances_ShouldIgnoreRuntimeNullEntries() - { - Node returned = CreateNode(TestItem.PublicKeyB, 2); - - _kademlia.GetAllAtDistance(10).Returns([null!, returned]); - - KademliaAdapter adapter = CreateAdapter(); - - Node[] result = adapter.GetNodesAtDistances([10]); - - Assert.That(result, Is.EqualTo(new[] { returned })); - } - [TestCase(-1)] [TestCase(257)] public void GetNodesAtDistances_ShouldRejectInvalidDistance(int distance) @@ -135,15 +132,26 @@ public void IsAcceptableNodeRecord_ShouldAllowNonRoutableWhenRequested() Is.True); } - private KademliaAdapter CreateAdapter() => new( - new Lazy>(_kademlia), - null!, - null!, - null!, - new DiscoveryConfig(), - new CryptoRandom(), - Hash256KademliaDistance.Instance, - LimboLogs.Instance); + private KademliaAdapter CreateAdapter() + { + INodeRecordProvider nodeRecordProvider = Substitute.For(); + nodeRecordProvider.Current.Returns(CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback)); + _packetCodec = new PacketCodec( + new InsecureProtectedPrivateKey(TestItem.PrivateKeyA), + nodeRecordProvider, + new CryptoRandom(), + new EthereumEcdsa(0)); + + return new( + new Lazy>(_kademlia), + new NettyDiscoveryV5Handler(LimboLogs.Instance), + _packetCodec, + nodeRecordProvider, + new DiscoveryConfig(), + new CryptoRandom(), + Hash256KademliaDistance.Instance, + LimboLogs.Instance); + } private static Node CreateNode(PublicKey publicKey, int hostSuffix) => new(publicKey, $"192.168.1.{hostSuffix}", 30303); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 324f2a265a4f..52dbf96cd57f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -112,44 +112,6 @@ public async Task Lookup_should_not_mark_node_healthy_when_find_neighbours_retur health.DidNotReceive().OnIncomingMessageFrom(Seed1); } - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_ignore_runtime_null_nodes(CancellationToken token) - { - Hash256 seedHash = ValueHashKeyOperator.ToHash(Seed1); - Hash256 neighbourHash = ValueHashKeyOperator.ToHash(N1); - IRoutingTable routing = Substitute.For>(); - routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(["seed", null!]); - - INodeHealthTracker health = Substitute.For>(); - LookupKNearestNeighbour lookup = new( - routing, - new StringHashProvider(new Dictionary - { - ["seed"] = seedHash, - ["neighbour"] = neighbourHash, - }), - Hash256KademliaDistance.Instance, - health, - new KademliaConfig - { - CurrentNodeId = "self", - Alpha = 1, - KSize = 8, - LookupFindNeighbourHardTimeout = TimeSpan.FromSeconds(10), - }); - - string[] result = await lookup.Lookup( - seedHash, - 8, - (_, _) => Task.FromResult([null!, "neighbour"]), - token); - - Assert.That(result, Does.Contain("seed")); - Assert.That(result, Does.Contain("neighbour")); - health.Received(1).OnIncomingMessageFrom("seed"); - } - [Test] [CancelAfter(10000)] public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(CancellationToken token) @@ -228,10 +190,4 @@ public async Task Lookup_should_drain_cancelled_workers_before_returning(Cancell Assert.That(cancelledWorkerDrained.Task.IsCompleted, Is.True); } - - private sealed class StringHashProvider(Dictionary hashes) : INodeHashProvider - { - public Hash256 GetHash(string node) => - hashes.GetValueOrDefault(node, ValueHashKeyOperator.ToHash(ValueKeccak.Compute(node))); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 895378c2a169..847d544245c7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -82,11 +82,7 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = Node[] nodes = kademlia.Value.GetAllAtDistance(distance); for (int i = 0; i < nodes.Length; i++) { - Node? node = nodes[i]; - if (node is null) - { - continue; - } + Node node = nodes[i]; if (excludedHash is not null && node.IdHash.Equals(excludedHash)) { @@ -630,11 +626,7 @@ private void AddFindNodeRecordsAtDistance( Hash256 requesterHash = requester.IdHash; for (int i = 0; i < nodes.Length && result.Count < MaxFindNodeRecords; i++) { - Node? node = nodes[i]; - if (node is null) - { - continue; - } + Node node = nodes[i]; if (node.IdHash.Equals(requesterHash) || string.IsNullOrEmpty(node.Enr) || !seen.Add(node.Id.Hash)) { From 34a6cf643f689dbde299eb5ec2c50bbe380d3511 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 11 Jun 2026 14:40:27 +0300 Subject: [PATCH 161/182] Fix tests --- .../Nethermind.Core.Test/Modules/InsecureProtectedPrivateKey.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Modules/InsecureProtectedPrivateKey.cs b/src/Nethermind/Nethermind.Core.Test/Modules/InsecureProtectedPrivateKey.cs index 879fbd4019cb..ab55871b4a0d 100644 --- a/src/Nethermind/Nethermind.Core.Test/Modules/InsecureProtectedPrivateKey.cs +++ b/src/Nethermind/Nethermind.Core.Test/Modules/InsecureProtectedPrivateKey.cs @@ -10,5 +10,5 @@ public class InsecureProtectedPrivateKey(PrivateKey privateKey) : IProtectedPriv { public PublicKey PublicKey => privateKey.PublicKey; public CompressedPublicKey CompressedPublicKey => privateKey.CompressedPublicKey; - public PrivateKey Unprotect() => privateKey; + public PrivateKey Unprotect() => new(privateKey.KeyBytes); } From 5a3c9b4d393e2234777c8ecc3d27029e7c2313fa Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 11 Jun 2026 19:50:31 +0300 Subject: [PATCH 162/182] Fix dos vector --- .../Nethermind.Core/Caching/LruCache.cs | 16 +-- .../Discv4/DiscoveryMessageSerializerTests.cs | 50 +++++++++ .../Discv5/KademliaAdapterTests.cs | 1 + .../Serializers/NeighborsMsgSerializer.cs | 20 +++- .../Kademlia/Handlers/NodesResponseHandler.cs | 100 +++++++++++------- 5 files changed, 136 insertions(+), 51 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 02a5727da5ab..6d6062b28fe3 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -16,6 +16,7 @@ public sealed class LruCache : ICache where TKey : n private readonly Dictionary> _cacheMap; private readonly McsLock _lock = new(); private readonly string _name; + private readonly Action? _onEvict; private LinkedListNode? _leastRecentlyUsed; public LruCache(int maxCapacity, int startCapacity, string name, Action? onEvict = null) @@ -24,11 +25,7 @@ public LruCache(int maxCapacity, int startCapacity, string name, Action? _name = name; _maxCapacity = maxCapacity; - if (onEvict is not null) - { - OnEvict += onEvict; - } - + _onEvict = onEvict; _cacheMap = typeof(TKey) == typeof(byte[]) ? new Dictionary>((IEqualityComparer)Bytes.EqualityComparer) : new Dictionary>(startCapacity); // do not initialize it at the full capacity @@ -39,8 +36,6 @@ public LruCache(int maxCapacity, string name, Action? onEvict = null) { } - public event Action? OnEvict; - public void Clear() { TValue[]? evictedValues; @@ -292,7 +287,7 @@ private TValue Replace(TKey key, TValue value) private TValue[]? GetEvictedValues() { - if (OnEvict is null || _cacheMap.Count == 0) + if (_onEvict is null || _cacheMap.Count == 0) { return null; } @@ -322,10 +317,9 @@ private void NotifyEvictedValues(TValue[]? evictedValues) private void NotifyEvicted(TValue value) { - Action? onEvict = OnEvict; - if (onEvict is not null && value is not null) + if (_onEvict is not null && value is not null) { - onEvict(value); + _onEvict(value); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs index 46a4a4e37d82..6a4d91ddaa0f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Net; using System.Linq; using DotNetty.Buffers; @@ -275,6 +276,55 @@ public void NeighborsMessageTest() } } + [Test] + public void NeighborsMessage_Drops_Empty_List_Node_Entries() + { + // A misbehaving peer can encode a node entry as an RLP empty list (0xc0), which + // DecodeArray materializes as a null node; such entries must never reach consumers. + byte[] ip = [192, 168, 1, 2]; + byte[] id = TestItem.PublicKeyA.Bytes; + const int port = 30303; + long expirationTime = 60 + _timestamper.UnixTime.MillisecondsLong; + + int nodeContentLength = Rlp.LengthOf(ip) + 2 * Rlp.LengthOf(port) + Rlp.LengthOf(id); + int nodesContentLength = Rlp.LengthOfSequence(nodeContentLength) + Rlp.OfEmptyList.Bytes.Length; + int contentLength = Rlp.LengthOfSequence(nodesContentLength) + Rlp.LengthOf(expirationTime); + + RlpStream stream = new(Rlp.LengthOfSequence(contentLength)); + stream.StartSequence(contentLength); + stream.StartSequence(nodesContentLength); + stream.StartSequence(nodeContentLength); + stream.Encode(ip); + stream.Encode(port); + stream.Encode(port); + stream.Encode(id); + stream.Encode(Rlp.OfEmptyList); + stream.Encode(expirationTime); + + NeighborsMsg deserialized = _messageSerializationService.Deserialize( + SignAndWrapDiscoveryPacket((byte)MsgType.Neighbors, stream.Data.ToArray()!)); + + Assert.That(deserialized.Nodes, Has.Count.EqualTo(1)); + Assert.That(deserialized.Nodes[0].Id, Is.EqualTo(TestItem.PublicKeyA)); + } + + private byte[] SignAndWrapDiscoveryPacket(byte msgType, byte[] data) + { + // [] + byte[] packet = new byte[32 + 64 + 1 + 1 + data.Length]; + packet[97] = msgType; + data.CopyTo(packet, 98); + + ValueHash256 toSign = ValueKeccak.Compute(packet.AsSpan(97)); + Signature signature = new Ecdsa().Sign(_privateKey, in toSign); + signature.Bytes.CopyTo(packet.AsSpan(32)); + packet[96] = signature.RecoveryId; + + ValueHash256 mdc = ValueKeccak.Compute(packet.AsSpan(32)); + mdc.BytesAsSpan.CopyTo(packet); + return packet; + } + private EnrResponseMsg BuildEnrResponse(CompressedPublicKey enrPublicKey) { NodeRecord nodeRecord = new(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index b430317c72cd..e9756694bad1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -136,6 +136,7 @@ private KademliaAdapter CreateAdapter() { INodeRecordProvider nodeRecordProvider = Substitute.For(); nodeRecordProvider.Current.Returns(CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback)); + _packetCodec?.Dispose(); _packetCodec = new PacketCodec( new InsecureProtectedPrivateKey(TestItem.PrivateKeyA), nodeRecordProvider, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs index de1eff7f8dd3..b669c1e26ee0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs @@ -69,11 +69,27 @@ public NeighborsMsg Deserialize(IByteBuffer msgBytes) Rlp.ValueDecoderContext ctx = Data.AsRlpContext(); ctx.ReadSequenceLength(); - Node[] nodes = ctx.DecodeArray(_decodeItem, limit: NodesRlpLimit)!; + Node?[] decoded = ctx.DecodeArray(_decodeItem, limit: NodesRlpLimit); + + // DecodeArray substitutes null for empty-list items, so compact them away to uphold + // the invariant that consumers never observe null nodes. + int nodeCount = 0; + for (int i = 0; i < decoded.Length; i++) + { + if (decoded[i] is not null) + { + decoded[nodeCount++] = decoded[i]; + } + } + + if (nodeCount != decoded.Length) + { + Array.Resize(ref decoded, nodeCount); + } long expirationTime = ctx.DecodeLong(); Data.SetReaderIndex(Data.ReaderIndex + ctx.Position); - NeighborsMsg msg = new(FarPublicKey, expirationTime, nodes); + NeighborsMsg msg = new(FarPublicKey, expirationTime, decoded!); return msg; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 35b3da7bc9a3..a2df08be41d8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -20,69 +20,93 @@ internal sealed class NodesResponseHandler(Node receiver, Distances requestedDis private readonly Node[] _nodes = new Node[MaxNodesResponseRecords]; private readonly PooledSet _seenNodeIds = new(MaxNodesResponseRecords); private readonly bool _allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); + + // Packet workers can race each other and can still hold this handler after the request owner removed + // it from the response cache and disposed it, so all state access is serialized and Handle becomes a + // no-op once disposed; otherwise it could write into pooled arrays already returned by Dispose. + private readonly Lock _lock = new(); + private bool _disposed; private int? _total; private int _received; private int _nodeCount; public override Task Task => _completion.Task; - public void Dispose() => _seenNodeIds.Dispose(); - - public override bool Handle(NodesMsg nodes) + public void Dispose() { - if (_completion.Task.IsCompleted) + lock (_lock) { - return true; - } + if (_disposed) + { + return; + } - if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) - { - _completion.TrySetResult(); - return true; + _disposed = true; + _seenNodeIds.Dispose(); } + } - if (_total is not null && _total.Value != nodes.Total) + public override bool Handle(NodesMsg nodes) + { + lock (_lock) { - _completion.TrySetResult(); - return true; - } + if (_disposed || _completion.Task.IsCompleted) + { + return true; + } + + if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) + { + _completion.TrySetResult(); + return true; + } - _total ??= nodes.Total; - _received++; + if (_total is not null && _total.Value != nodes.Total) + { + _completion.TrySetResult(); + return true; + } - for (int i = 0; i < nodes.Records.Count && _nodeCount < MaxNodesResponseRecords; i++) - { - NodeRecord record = nodes.Records[i]; - if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record) || - !Node.TryFromDiscoveryEnr(record, out Node? node) || - !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || - !_seenNodeIds.Add(node.Id.Hash) || - !MatchesRequestedDistance(node, requestedDistances)) + _total ??= nodes.Total; + _received++; + + for (int i = 0; i < nodes.Records.Count && _nodeCount < MaxNodesResponseRecords; i++) { - continue; + NodeRecord record = nodes.Records[i]; + if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record) || + !Node.TryFromDiscoveryEnr(record, out Node? node) || + !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || + !_seenNodeIds.Add(node.Id.Hash) || + !MatchesRequestedDistance(node, requestedDistances)) + { + continue; + } + + _nodes[_nodeCount++] = node; } - _nodes[_nodeCount++] = node; - } + if (_received >= _total || _nodeCount >= MaxNodesResponseRecords) + { + _completion.TrySetResult(); + } - if (_received >= _total || _nodeCount >= MaxNodesResponseRecords) - { - _completion.TrySetResult(); + return true; } - - return true; } public Node[] GetNodes() { - if (_nodeCount == 0) + lock (_lock) { - return []; - } + if (_nodeCount == 0) + { + return []; + } - Node[] nodes = new Node[_nodeCount]; - Array.Copy(_nodes, nodes, _nodeCount); - return nodes; + Node[] nodes = new Node[_nodeCount]; + Array.Copy(_nodes, nodes, _nodeCount); + return nodes; + } } private bool MatchesRequestedDistance(Node node, Distances requestedDistances) From 0ea629ed346060dd94f8bef04a09d2a4ab997641 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 12 Jun 2026 12:42:10 +0300 Subject: [PATCH 163/182] Fix discovery serialization review --- .../Discv4/DiscoveryMessageSerializerTests.cs | 35 +++++++ .../Discv4/NettyDiscoveryHandlerTests.cs | 38 +++++++ .../Discv5/CodecTests.cs | 52 ++++++++++ .../KademliaDiscoveryAppTests.cs | 99 +++++++++++++++++++ .../Discv4/NettyDiscoveryHandler.cs | 26 ++--- .../Serializers/EnrRequestMsgSerializer.cs | 2 +- .../Discv4/Serializers/PongMsgSerializer.cs | 3 +- .../Discv5/Messages/Distances.cs | 6 ++ .../Discv5/NettyDiscoveryV5Handler.cs | 19 +++- .../Discv5/Packets/PacketCodec.cs | 4 +- .../Serializers/FindNodeMsgSerializer.cs | 5 + .../Discv5/Serializers/NodesMsgSerializer.cs | 7 ++ .../Discv5/Serializers/PongMsgSerializer.cs | 5 +- .../KademliaDiscoveryApp.cs | 44 ++++++--- 14 files changed, 312 insertions(+), 33 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/KademliaDiscoveryAppTests.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs index 6a4d91ddaa0f..8e664809b5aa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs @@ -157,6 +157,22 @@ public void Enr_request_contains_hash() Assert.That(hash, Is.EqualTo(new Hash256("0x64c2e38e89cdfca030166b7a271c301dd77cf043172966ab112d97fc3430fa16"))); } + [Test] + public void Enr_request_hash_does_not_alias_input_buffer() + { + EnrRequestMsg msg = new(TestItem.PublicKeyA, long.MaxValue); + using DisposableByteBuffer serialized = _messageSerializationService.ZeroSerialize(msg).AsDisposable(); + byte[] packet = serialized.ReadAllBytesAsArray(); + Hash256 expectedHash = new(packet.AsSpan(0, 32)); + + using DisposableByteBuffer input = Unpooled.WrappedBuffer(packet).AsDisposable(); + EnrRequestMsg deserialized = _messageSerializationService.Deserialize(input); + Array.Clear(packet); + + Assert.That(deserialized.Hash, Is.Not.Null); + Assert.That(new Hash256(deserialized.Hash!.Value.Span), Is.EqualTo(expectedHash)); + } + [Test] public void Enr_response_there_and_back() { @@ -364,4 +380,23 @@ public void NeighborsMessage_Rejects_Too_Many_Nodes() using DisposableByteBuffer data = _messageSerializationService.ZeroSerialize(message).AsDisposable(); Assert.Throws(() => _messageSerializationService.Deserialize(data)); } + + [Test] + public void PongMessage_Rejects_Oversized_Ping_Mdc() + { + RlpStream stream = new(128); + long expirationTime = 60 + _timestamper.UnixTime.MillisecondsLong; + int addressContentLength = Rlp.LengthOf(new byte[] { 127, 0, 0, 1 }) + Rlp.LengthOf(30303) + Rlp.LengthOf(30303); + int contentLength = Rlp.LengthOfSequence(addressContentLength) + Rlp.LengthOf(new byte[33]) + Rlp.LengthOf(expirationTime); + stream.StartSequence(contentLength); + stream.StartSequence(addressContentLength); + stream.Encode(new byte[] { 127, 0, 0, 1 }); + stream.Encode(30303); + stream.Encode(30303); + stream.Encode(new byte[33]); + stream.Encode(expirationTime); + + Assert.Throws(() => _messageSerializationService.Deserialize( + SignAndWrapDiscoveryPacket((byte)MsgType.Pong, stream.Data.ToArray()!))); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs index 46e1ae3efa9a..a8fe4a59de31 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs @@ -22,6 +22,7 @@ using Nethermind.Network.Discovery.Discv4; using Nethermind.Network.Discovery.Discv4.Kademlia; using Nethermind.Network.Discovery.Discv4.Messages; +using Nethermind.Network.Enr; using Nethermind.Network.Test.Builders; using Nethermind.Serialization.Rlp; using Nethermind.Stats.Model; @@ -229,6 +230,31 @@ public async Task FarFutureMessagesAreRejected() _ = _kademliaAdaptersMocks[1].DidNotReceive().OnIncomingMsg(Arg.Any()); } + [Test] + public async Task EnrResponseWithoutExpirationIsAccepted() + { + (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler(); + + EnrResponseMsg msg = BuildEnrResponse(_privateKey2); + IByteBuffer serialized = service.ZeroSerialize(msg); + byte[] data; + try + { + data = serialized.ReadAllBytesAsArray(); + } + finally + { + serialized.SafeRelease(); + } + + handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer(data), _address2, _address)); + + await SleepWhileWaiting(); + + await adapter.Received(1).OnIncomingMsg(Arg.Is(static m => m.MsgType == MsgType.EnrResponse)); + ctx.DidNotReceive().FireChannelRead(Arg.Any()); + } + [Test] public async Task RateLimitedMessagesAreIgnored() { @@ -245,6 +271,7 @@ public async Task RateLimitedMessagesAreIgnored() await Task.Delay(50); await adapter.Received(1).OnIncomingMsg(Arg.Any()); + ctx.DidNotReceive().FireChannelRead(Arg.Any()); } [Test] @@ -260,6 +287,7 @@ public async Task DefaultInboundRateLimiter_Allows_ShortBurstFromSameIp() await SleepWhileWaiting(); await adapter.Received(2).OnIncomingMsg(Arg.Any()); + ctx.DidNotReceive().FireChannelRead(Arg.Any()); } [Test] @@ -345,6 +373,16 @@ private byte[] SerializePing(IMessageSerializationService service) return data; } + private EnrResponseMsg BuildEnrResponse(PrivateKey signingKey) + { + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(new SecP256k1Entry(signingKey.CompressedPublicKey)); + nodeRecord.EnrSequence = 5; + NodeRecordSigner signer = new(new Ecdsa(), signingKey); + signer.Sign(nodeRecord); + return new EnrResponseMsg(_address, nodeRecord, TestItem.KeccakA); + } + private async Task StartUdpChannel(string address, int port, IKademliaAdapter kademliaAdapter, IMessageSerializationService service) { MultithreadEventLoopGroup group = new(1); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index d37502ed6513..2825911b9812 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -85,6 +85,18 @@ public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() } } + [Test] + public void PacketCodec_Rejects_Packets_Larger_Than_Spec() + { + byte[] packetBytes = new byte[PacketCodec.MaxPacketSize + 1]; + + bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); + using (packet) + { + Assert.That(decoded, Is.False); + } + } + [Test] public void PacketCodec_Decodes_Packets_Concurrently() { @@ -230,6 +242,18 @@ public void MessageCodec_Roundtrips_Pong() Assert.That(decodedPong.RecipientPort, Is.EqualTo(message.RecipientPort)); } + [Test] + public void MessageCodec_Rejects_Oversized_Pong_Ip() + { + byte[] message = CreateMessage(MessageType.Pong, Rlp.Encode( + Rlp.Encode(new byte[] { 1 }), + Rlp.Encode(1), + Rlp.Encode(new byte[17]), + Rlp.Encode(30303))); + + Assert.That(() => MessageCodec.Decode(message), Throws.InstanceOf()); + } + [Test] public void MessageCodec_Roundtrips_TalkReq() { @@ -316,6 +340,26 @@ public void MessageCodec_Skips_Invalid_Enrs_In_Nodes() Assert.That(nodes.Records[0].EnrString, Is.EqualTo(expectedRecord.EnrString)); } + [Test] + public void MessageCodec_Rejects_Too_Many_FindNode_Distances() + { + Rlp[] distances = new Rlp[Distances.MaxCount + 1]; + Array.Fill(distances, Rlp.Encode(1)); + byte[] message = CreateMessage(MessageType.FindNode, Rlp.Encode(Rlp.Encode(new byte[] { 1 }), Rlp.Encode(distances))); + + Assert.That(() => MessageCodec.Decode(message), Throws.TypeOf()); + } + + [Test] + public void MessageCodec_Rejects_Too_Many_Nodes_Records() + { + Rlp[] records = new Rlp[17]; + Array.Fill(records, Rlp.Encode(Array.Empty())); + byte[] message = CreateMessage(MessageType.Nodes, Rlp.Encode(Rlp.Encode(new byte[] { 1 }), Rlp.Encode(1), Rlp.Encode(records))); + + Assert.That(() => MessageCodec.Decode(message), Throws.TypeOf()); + } + private static PacketCodec CreateCodec(PrivateKey privateKey) => new( new InsecureProtectedPrivateKey(privateKey), @@ -332,6 +376,14 @@ private static byte[] CreateDevp2pPingPacketBytes() private static NodeRecord CreateNodeRecord(PrivateKey privateKey) => TestEnrBuilder.BuildSigned(privateKey, tcpPort: null, udpPort: null); + private static byte[] CreateMessage(MessageType messageType, Rlp payload) + { + byte[] message = new byte[payload.Length + 1]; + message[0] = (byte)messageType; + payload.Bytes.CopyTo(message.AsSpan(1)); + return message; + } + private sealed class TestNodeRecordProvider(PrivateKey privateKey) : INodeRecordProvider { public NodeRecord Current { get; } = CreateNodeRecord(privateKey); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/KademliaDiscoveryAppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/KademliaDiscoveryAppTests.cs new file mode 100644 index 000000000000..1d6e4c11b7bf --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/KademliaDiscoveryAppTests.cs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using DotNetty.Transport.Channels; +using Nethermind.Config; +using Nethermind.Logging; +using Nethermind.Network.Config; +using NUnit.Framework; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Nethermind.Network.Discovery.Test; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class KademliaDiscoveryAppTests +{ + [Test] + public async Task DisposeAsync_StopsRunningDiscovery() + { + TestKademliaDiscoveryApp app = new(); + + await app.StartAsync(); + await app.Started.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + await app.DisposeAsync(); + await app.DisposeAsync(); + + Assert.That(app.Stopped.Task.IsCompletedSuccessfully, Is.True); + Assert.That(app.StopAsyncCoreCalls, Is.EqualTo(1)); + Assert.That(app.DisposeAsyncCoreCalls, Is.EqualTo(1)); + } + + [Test] + public async Task DisposeAsync_DisposesCore_WhenStopFails() + { + TestKademliaDiscoveryApp app = new(throwOnStop: true); + + await app.StartAsync(); + await app.Started.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + InvalidOperationException? exception = Assert.ThrowsAsync(async () => await app.DisposeAsync()); + + Assert.That(exception?.Message, Is.EqualTo("Stop failed")); + Assert.That(app.Stopped.Task.IsCompletedSuccessfully, Is.True); + Assert.That(app.StopAsyncCoreCalls, Is.EqualTo(1)); + Assert.That(app.DisposeAsyncCoreCalls, Is.EqualTo(1)); + } + + private sealed class TestKademliaDiscoveryApp(bool throwOnStop = false) : KademliaDiscoveryApp( + "test discovery", + new NetworkConfig { ExternalIp = "127.0.0.1" }, + new ProcessExitSource(CancellationToken.None), + LimboLogs.Instance.GetClassLogger()) + { + public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public TaskCompletionSource Stopped { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public int StopAsyncCoreCalls { get; private set; } + + public int DisposeAsyncCoreCalls { get; private set; } + + public override void InitializeChannel(IChannel channel) + { + } + + protected override async Task RunDiscoveryAsync(CancellationToken cancellationToken) + { + Started.SetResult(); + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + finally + { + Stopped.SetResult(); + } + } + + protected override Task StopAsyncCore() + { + StopAsyncCoreCalls++; + if (throwOnStop) + { + throw new InvalidOperationException("Stop failed"); + } + + return Task.CompletedTask; + } + + protected override ValueTask DisposeAsyncCore() + { + DisposeAsyncCoreCalls++; + return ValueTask.CompletedTask; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs index fe2f7ec0bf14..63bb58e5cceb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/NettyDiscoveryHandler.cs @@ -145,6 +145,7 @@ private bool TryAcceptPacket(DatagramPacket packet, out MsgType type, out bool s } type = resolvedType; + shouldForward = false; if (_logger.IsTrace) _logger.Trace($"Received message: {type}"); if (!_globalInboundMessageLimiter.TryAcquire()) @@ -213,19 +214,22 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket private bool ValidateMsg(DiscoveryMsg msg, MsgType type, EndPoint address, DatagramPacket packet, int size) { - long timeToExpire = msg.ExpirationTime - _timestamper.UnixTime.SecondsLong; - if (timeToExpire < 0) + if (msg is not EnrResponseMsg) { - if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", $"{msg.MsgType} expired", size); - if (_logger.IsDebug) _logger.Debug($"Received a discovery message that has expired {-timeToExpire} seconds ago, type: {type}, sender: {address}, message: {msg}"); - return false; - } + long timeToExpire = msg.ExpirationTime - _timestamper.UnixTime.SecondsLong; + if (timeToExpire < 0) + { + if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", $"{msg.MsgType} expired", size); + if (_logger.IsDebug) _logger.Debug($"Received a discovery message that has expired {-timeToExpire} seconds ago, type: {type}, sender: {address}, message: {msg}"); + return false; + } - if (timeToExpire > MaxFutureExpirationOffset.TotalSeconds) - { - if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", $"{msg.MsgType} far future", size); - if (_logger.IsDebug) _logger.Debug($"Received a discovery message that expires too far in the future ({timeToExpire} seconds), type: {type}, sender: {address}, message: {msg}"); - return false; + if (timeToExpire > MaxFutureExpirationOffset.TotalSeconds) + { + if (NetworkDiagTracer.IsEnabled) NetworkDiagTracer.ReportIncomingMessage(msg.FarAddress, "disc v4", $"{msg.MsgType} far future", size); + if (_logger.IsDebug) _logger.Debug($"Received a discovery message that expires too far in the future ({timeToExpire} seconds), type: {type}, sender: {address}, message: {msg}"); + return false; + } } if (msg.FarAddress is null) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs index d22586c05e67..f2745d889b50 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs @@ -40,7 +40,7 @@ public EnrRequestMsg Deserialize(IByteBuffer msgBytes) long expirationTime = ctx.DecodeLong(); data.SetReaderIndex(data.ReaderIndex + ctx.Position); - EnrRequestMsg msg = new(farPublicKey, mdc, expirationTime); + EnrRequestMsg msg = new(farPublicKey, mdc.ToArray(), expirationTime); return msg; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs index 96337201147a..5dbe334b0102 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs @@ -43,11 +43,10 @@ public PongMsg Deserialize(IByteBuffer msgBytes) ctx.ReadSequenceLength(); ctx.ReadSequenceLength(); - // GetAddress(ctx.DecodeByteArray(), ctx.DecodeInt()); ctx.DecodeByteArraySpan(IpAddressRlpLimit); ctx.DecodeInt(); // UDP port (we ignore and take it from Netty) ctx.DecodeInt(); // TCP port - byte[] token = ctx.DecodeByteArray(); + byte[] token = ctx.DecodeByteArraySpan(RlpLimit.L32).ToArray(); long expirationTime = ctx.DecodeLong(); data.SetReaderIndex(data.ReaderIndex + ctx.Position); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs index 800d6c9215a1..6028896f6aed 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs @@ -8,6 +8,7 @@ namespace Nethermind.Network.Discovery.Discv5.Messages; internal sealed class Distances : IReadOnlyList, IDisposable { + internal const int MaxCount = 257; private const int InlineCapacity = 3; private int[]? _rented; @@ -26,6 +27,11 @@ public Distances(ReadOnlySpan distances) internal Distances(int count) { + if ((uint)count > MaxCount) + { + throw new ArgumentOutOfRangeException(nameof(count), count, $"Distance count must be between 0 and {MaxCount}."); + } + Count = count; if (count > InlineCapacity) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 7799361c3e9d..117c5bb65907 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Threading.Channels; using DotNetty.Buffers; using DotNetty.Common.Utilities; @@ -90,15 +92,20 @@ internal async IAsyncEnumerable ReadMessagesAsync([Syste private static PooledUdpReceiveResult CreateReceiveResult(DatagramPacket packet) { - ArrayPoolSpan buffer = new(packet.Content.ReadableBytes); + IByteBuffer content = packet.Content; + int readerIndex = content.ReaderIndex; + int readableBytes = content.ReadableBytes; + ArrayPoolSpan buffer = new(readableBytes); try { - Span bytes = buffer; - for (int i = 0; i < bytes.Length; i++) + if (!MemoryMarshal.TryGetArray(buffer.AsMemory(), out ArraySegment segment)) { - bytes[i] = packet.Content.ReadByte(); + ThrowMissingArraySegment(); } + content.GetBytes(readerIndex, segment.Array!, segment.Offset, readableBytes); + content.SetReaderIndex(readerIndex + readableBytes); + return new PooledUdpReceiveResult(NormalizeEndpoint((IPEndPoint)packet.Sender), buffer); } catch @@ -106,6 +113,10 @@ private static PooledUdpReceiveResult CreateReceiveResult(DatagramPacket packet) buffer.Dispose(); throw; } + + [DoesNotReturn] + static void ThrowMissingArraySegment() + => throw new InvalidOperationException("Pooled UDP receive buffer must be array-backed."); } private static IPEndPoint NormalizeEndpoint(IPEndPoint endpoint) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index d29185948bad..8fa134ccd9ea 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -24,6 +24,8 @@ public sealed class PacketCodec( { public const int NonceSize = 12; + internal const int MaxPacketSize = 1280; + private const int MaskingIvSize = 16; private const int StaticHeaderSize = 23; private const int NodeIdSize = 32; @@ -149,7 +151,7 @@ private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMa { decoded = default; ReadOnlySpan packet = packetMemory.Span; - if (packet.Length < MaskingIvSize + StaticHeaderSize) + if (packet.Length is < MaskingIvSize + StaticHeaderSize or > MaxPacketSize) { return false; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs index 27b6e3b80f02..6e7ea14caa30 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs @@ -48,6 +48,11 @@ private static Distances DecodeDistances(ref Rlp.ValueDecoderContext ctx) { int checkPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(checkPosition); + if (count > Distances.MaxCount) + { + throw new RlpException($"discv5 FINDNODE distance count {count} exceeds {Distances.MaxCount}."); + } + Distances distances = new(count); try { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index 20c67fc18cbf..8bc8ba87362a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -12,6 +12,8 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; internal sealed class NodesMsgSerializer : MsgSerializerBase { + private const int MaxNodeRecordsPerMessage = 16; + private readonly IEcdsa _ecdsa = new Ecdsa(); protected override int GetContentLengthCore(NodesMsg msg) @@ -59,6 +61,11 @@ private NodeRecord[] DecodeNodeRecords(ref Rlp.ValueDecoderContext ctx) { int checkPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(checkPosition); + if (count > MaxNodeRecordsPerMessage) + { + throw new RlpException($"discv5 NODES record count {count} exceeds {MaxNodeRecordsPerMessage}."); + } + NodeRecord[] records = new NodeRecord[count]; int recordCount = 0; for (int i = 0; i < count; i++) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs index c8e1d26ae1f1..e9be8241b580 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -11,6 +11,8 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; internal sealed class PongMsgSerializer : MsgSerializerBase { + private static readonly RlpLimit IpAddressRlpLimit = RlpLimit.For(16, nameof(PongMsg.RecipientIp)); + protected override int GetContentLengthCore(PongMsg msg) => Rlp.LengthOf(msg.EnrSequence) + IPAddressRlp.GetLength(msg.RecipientIp) + @@ -26,9 +28,8 @@ protected override void SerializeCore(NettyRlpStream stream, PongMsg msg) protected override PongMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { ulong enrSequence = ctx.DecodeULong(); - IPAddress recipientIp = new(ctx.DecodeByteArraySpan()); + IPAddress recipientIp = new(ctx.DecodeByteArraySpan(IpAddressRlpLimit)); int recipientPort = ctx.DecodePositiveInt(); return new PongMsg(requestId, enrSequence, recipientIp, recipientPort, owner); } - } diff --git a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs index d5c5b4c2d1f6..57c6ebd14425 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/KademliaDiscoveryApp.cs @@ -26,6 +26,9 @@ public abstract class KademliaDiscoveryApp( private IKademliaNodeSource? _kademliaNodeSource; private IKademlia? _kademlia; private Task? _runningTask; + private Task? _stopTask; + private Task? _disposeTask; + private readonly object _lifetimeLock = new(); private int _activationStarted; protected ILogger Logger { get; } = logger; @@ -47,17 +50,19 @@ public Task StartAsync() } } - public async Task StopAsync() + public Task StopAsync() { - DetachEventHandlers(); - - try - { - await _stopCts.CancelAsync(); - } - catch (ObjectDisposedException) + lock (_lifetimeLock) { + return _stopTask ??= StopAsyncInternal(); } + } + + private async Task StopAsyncInternal() + { + DetachEventHandlers(); + + await _stopCts.CancelAsync(); try { @@ -97,14 +102,29 @@ public IAsyncEnumerable DiscoverNodes(CancellationToken token) public event EventHandler? NodeRemoved; - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { - if (_kademlia is not null) + lock (_lifetimeLock) { - _kademlia.OnNodeRemoved -= OnKademliaNodeRemoved; + return new ValueTask(_disposeTask ??= DisposeAsyncInternal()); } + } - await DisposeAsyncCore(); + private async Task DisposeAsyncInternal() + { + try + { + await StopAsync(); + } + finally + { + if (_kademlia is not null) + { + _kademlia.OnNodeRemoved -= OnKademliaNodeRemoved; + } + + await DisposeAsyncCore(); + } } protected void UseKademliaServices(IKademliaNodeSource kademliaNodeSource, IKademlia kademlia) From 7a93ef67ed523a883bc809b85c700a5995a5707a Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 12 Jun 2026 15:00:56 +0300 Subject: [PATCH 164/182] Reduce discovery token allocations --- .../Discv4/DiscoveryMessageSerializerTests.cs | 6 ++--- .../Discv4/Kademlia/KademliaAdapterTests.cs | 13 ++++++----- .../Discv4/NettyDiscoveryHandlerTests.cs | 4 ++-- .../Discv5/CodecTests.cs | 14 +++++++++-- .../Kademlia/Handlers/EnrResponseHandler.cs | 3 +-- .../Kademlia/Handlers/NeighbourMsgHandler.cs | 5 ++++ .../Kademlia/Handlers/PongMsgHandler.cs | 4 ++-- .../Discv4/Kademlia/KademliaAdapter.cs | 10 ++++++-- .../Discv4/Messages/EnrRequestMsg.cs | 4 ++-- .../Discv4/Messages/PingMsg.cs | 23 +++++++++++++++---- .../Discv4/Messages/PongMsg.cs | 11 +++++---- .../Serializers/DiscoveryMsgSerializerBase.cs | 17 +++++++++++--- .../Serializers/EnrRequestMsgSerializer.cs | 6 ++--- .../Discv4/Serializers/PingMsgSerializer.cs | 6 ++--- .../Discv4/Serializers/PongMsgSerializer.cs | 15 ++++++++---- .../Discv5/Kademlia/KademliaAdapter.cs | 4 ++-- .../Discv5/Messages/RequestId.cs | 6 ----- .../Discv5/Packets/PacketCodec.cs | 12 +++++----- 18 files changed, 106 insertions(+), 57 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs index 8e664809b5aa..b3e4fd4eb5c5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs @@ -106,7 +106,7 @@ public void PongMessageTest() { using PooledBufferLeakDetector detector = new(_leakDetectionAllocator); PongMsg message = - new(_privateKey.PublicKey, 60 + _timestamper.UnixTime.MillisecondsLong, new byte[] { 1, 2, 3 }) + new(_privateKey.PublicKey, 60 + _timestamper.UnixTime.MillisecondsLong, TestItem.KeccakA.ValueHash256) { FarAddress = _farAddress }; @@ -152,7 +152,7 @@ public void Enr_request_contains_hash() EnrRequestMsg deserialized = _messageSerializationService.Deserialize(serialized); Assert.That(deserialized.Hash, Is.Not.Null); - Hash256 hash = new(deserialized.Hash!.Value.Span); + Hash256 hash = new(deserialized.Hash!.Value); Assert.That(hash, Is.EqualTo(new Hash256("0x64c2e38e89cdfca030166b7a271c301dd77cf043172966ab112d97fc3430fa16"))); } @@ -170,7 +170,7 @@ public void Enr_request_hash_does_not_alias_input_buffer() Array.Clear(packet); Assert.That(deserialized.Hash, Is.Not.Null); - Assert.That(new Hash256(deserialized.Hash!.Value.Span), Is.EqualTo(expectedHash)); + Assert.That(new Hash256(deserialized.Hash!.Value), Is.EqualTo(expectedHash)); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs index 8e1352a3e6c5..b908a0b8a109 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs @@ -65,7 +65,7 @@ private void ConfigureBondCallback() => PongMsg pong = new( msg.FarPublicKey!, _timestamper.UnixTime.SecondsLong + 1, - sent.Mdc!); + sent.Mdc!.Value); pong.FarAddress = _receiver.Address; Task.Run(() => _adapter.OnIncomingMsg(pong)); }); @@ -214,7 +214,7 @@ public async Task SendEnrRequest_should_ping_then_enr_request_and_return_respons { ConfigureBondCallback(); - byte[] requestHash = TestItem.KeccakA.BytesToArray(); + ValueHash256 requestHash = TestItem.KeccakA.ValueHash256; _msgSender .When(x => x.SendMsg(Arg.Any())) .Do(ci => @@ -242,7 +242,7 @@ public async Task SendEnrRequest_should_reject_unsolicited_response_with_wrong_k .Do(ci => { EnrRequestMsg sent = (EnrRequestMsg)ci[0]!; - sent.Hash = TestItem.KeccakA.BytesToArray(); + sent.Hash = TestItem.KeccakA.ValueHash256; EnrResponseMsg response = AddReceiverFarAddress(new EnrResponseMsg(_receiver.Address, _selfNodeRecord, TestItem.KeccakB)); Task.Run(() => _adapter.OnIncomingMsg(response)); }); @@ -335,7 +335,7 @@ public async Task Failed_send_should_remove_response_handler(CancellationToken t _nodeHealthTracker.ClearReceivedCalls(); - PongMsg response = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 1, sent!.Mdc!); + PongMsg response = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 1, sent!.Mdc!.Value); response = AddReceiverFarAddress(response); await _adapter.OnIncomingMsg(response); @@ -356,9 +356,10 @@ public async Task OnIncomingMsg_ping_should_respond_with_pong(CancellationToken await Task.Delay(100); + ValueHash256 expectedPingMdc = pingMsg.Mdc!.Value; await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && - m.PingMdc!.SequenceEqual(pingMsg.Mdc!))); + m.PingMdc == expectedPingMdc)); } [Test] @@ -432,7 +433,7 @@ public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(Can EnrRequestMsg enrRequestMsg = new(_receiver.Address, _timestamper.UnixTime.SecondsLong + 20); enrRequestMsg = AddReceiverFarAddress(enrRequestMsg); - Hash256 expectedRequestHash = new(enrRequestMsg.Hash!.Value.Span); + Hash256 expectedRequestHash = new(enrRequestMsg.Hash!.Value); await _adapter.OnIncomingMsg(enrRequestMsg); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs index a8fe4a59de31..58143c28c3a8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs @@ -101,7 +101,7 @@ public async Task PingSentReceivedTest() [Test] public async Task PongSentReceivedTest() { - PongMsg msg = new(_privateKey2.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new byte[] { 1, 2, 3 }) + PongMsg msg = new(_privateKey2.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, TestItem.KeccakA.ValueHash256) { FarAddress = _address2 }; @@ -110,7 +110,7 @@ public async Task PongSentReceivedTest() await SleepWhileWaiting(); await _kademliaAdaptersMocks[1].Received(1).OnIncomingMsg(Arg.Is(static x => x.MsgType == MsgType.Pong)); - PongMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, new byte[] { 1, 2, 3 }) + PongMsg msg2 = new(_privateKey.PublicKey, Timestamper.Default.UnixTime.SecondsLong + 1200, TestItem.KeccakA.ValueHash256) { FarAddress = _address }; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 2825911b9812..4da36282e548 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -22,6 +22,7 @@ public class CodecTests { private static readonly byte[] NodeAId = Bytes.FromHexString("0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb"); private static readonly byte[] NodeBId = Bytes.FromHexString("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9"); + private static readonly byte[] Devp2pPingRequestId = [0, 0, 0, 1]; private const string GethNodeAPrivateKey = "0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f"; private const string GethNodeBPrivateKey = "0x66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628"; @@ -79,7 +80,7 @@ public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() Assert.That(decrypted, Is.True); Assert.That(message, Is.InstanceOf()); PingMsg ping = (PingMsg)message; - Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + AssertRequestId(ping.RequestId, Devp2pPingRequestId); Assert.That(ping.EnrSequence, Is.EqualTo(2)); message.Dispose(); } @@ -193,7 +194,7 @@ public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( Assert.That(session.ReadKey.ToHexString(true), Is.EqualTo(expectedReadKeyHex)); Assert.That(message, Is.InstanceOf()); PingMsg ping = (PingMsg)message; - Assert.That(ping.RequestId.ToArray(), Is.EqualTo(new byte[] { 0, 0, 0, 1 })); + AssertRequestId(ping.RequestId, Devp2pPingRequestId); Assert.That(ping.EnrSequence, Is.EqualTo(1)); Assert.That(nodeRecord is not null, Is.EqualTo(includesRecord)); message.Dispose(); @@ -384,6 +385,15 @@ private static byte[] CreateMessage(MessageType messageType, Rlp payload) return message; } + private static void AssertRequestId(RequestId requestId, ReadOnlySpan expected) + { + Assert.That(requestId.Length, Is.EqualTo(expected.Length)); + + Span actual = stackalloc byte[RequestId.MaxLength]; + requestId.CopyTo(actual); + Assert.That(actual[..requestId.Length].SequenceEqual(expected), Is.True); + } + private sealed class TestNodeRecordProvider(PrivateKey privateKey) : INodeRecordProvider { public NodeRecord Current { get; } = CreateNodeRecord(privateKey); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs index 1adf86076f9f..7c7d7c2ef8a8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/EnrResponseHandler.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Extensions; using Nethermind.Network.Discovery.Discv4.Messages; namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; @@ -14,6 +13,6 @@ public bool Handle(DiscoveryMsg msg) => !TaskCompletionSource.Task.IsCompleted && msg is EnrResponseMsg resp && request.Hash is { } expected - && Bytes.AreEqual(resp.RequestKeccak.Bytes, expected.Span) + && resp.RequestKeccak == expected && TaskCompletionSource.TrySetResult(DiscoveryResponse.From(resp)); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs index df00efcf17e5..bf888030602a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/NeighbourMsgHandler.cs @@ -62,6 +62,11 @@ private Node[] GetCurrentNodes() { lock (_lock) { + if (_count == _nodes.Length) + { + return _nodes; + } + return _nodes.AsSpan(0, _count).ToArray(); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs index 9c4617752662..f071ce01f1f5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/Handlers/PongMsgHandler.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Extensions; using Nethermind.Network.Discovery.Discv4.Messages; namespace Nethermind.Network.Discovery.Discv4.Kademlia.Handlers; @@ -13,6 +12,7 @@ public sealed class PongMsgHandler(PingMsg ping) : ITaskCompleter public bool Handle(DiscoveryMsg msg) => !TaskCompletionSource.Task.IsCompleted && msg is PongMsg pong - && Bytes.AreEqual(pong.PingMdc, ping.Mdc) + && ping.Mdc is { } expected + && pong.PingMdc == expected && TaskCompletionSource.TrySetResult(DiscoveryResponse.From(pong)); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index eaaeed4082ec..bd2c19c67d9f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -243,7 +243,7 @@ private async Task HandleEnrRequest(Node node, NodeSession session, EnrReq return false; } - await SendMessage(session, new EnrResponseMsg(node.Address, nodeRecordProvider.Current, new Hash256(requestHash.Span)), token); + await SendMessage(session, new EnrResponseMsg(node.Address, nodeRecordProvider.Current, new Hash256(requestHash)), token); return true; } @@ -275,7 +275,13 @@ private async Task HandleFindNode(Node node, NodeSession session, FindNode private async Task HandlePing(Node node, NodeSession session, PingMsg ping, CancellationToken token) { if (_logger.IsTrace) _logger.Trace($"Receive ping from {node}"); - PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), ping.Mdc!); + if (ping.Mdc is not { } pingMdc) + { + if (_logger.IsDebug) _logger.Debug($"Rejecting ping without packet hash from {node}"); + return; + } + + PongMsg msg = new(ping.FarAddress!, CalculateExpirationTime(), pingMdc); session.OnPingReceived(); await SendMessage(session, msg, token); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs index 3ba850dac39e..30b0a5841fc2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/EnrRequestMsg.cs @@ -13,7 +13,7 @@ public sealed class EnrRequestMsg : DiscoveryMsg { public override MsgType MsgType => MsgType.EnrRequest; - public Memory? Hash { get; set; } + public ValueHash256? Hash { get; set; } public EnrRequestMsg(IPEndPoint farAddress, long expirationDate) : base(farAddress, expirationDate) @@ -25,6 +25,6 @@ public EnrRequestMsg(PublicKey farPublicKey, long expirationDate) { } - internal EnrRequestMsg(PublicKey farPublicKey, Memory hash, long expirationDate) + internal EnrRequestMsg(PublicKey farPublicKey, ValueHash256 hash, long expirationDate) : base(farPublicKey, expirationDate) => Hash = hash; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs index 5600acf224a4..6e4a4ca087a5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PingMsg.cs @@ -3,7 +3,6 @@ using System.Net; using Nethermind.Core.Crypto; -using Nethermind.Core.Extensions; namespace Nethermind.Network.Discovery.Discv4.Messages; @@ -15,7 +14,7 @@ public sealed class PingMsg : DiscoveryMsg /// /// Modification detection code /// - public byte[]? Mdc { get; set; } + public ValueHash256? Mdc { get; set; } /// /// https://eips.ethereum.org/EIPS/eip-868 @@ -23,11 +22,16 @@ public sealed class PingMsg : DiscoveryMsg public ulong? EnrSequence { get; set; } public PingMsg(PublicKey farPublicKey, long expirationTime, IPEndPoint source, IPEndPoint destination, byte[] mdc) + : this(farPublicKey, expirationTime, source, destination, CreateHash(mdc)) + { + } + + public PingMsg(PublicKey farPublicKey, long expirationTime, IPEndPoint source, IPEndPoint destination, ValueHash256 mdc) : base(farPublicKey, expirationTime) { SourceAddress = source ?? throw new ArgumentNullException(nameof(source)); DestinationAddress = destination ?? throw new ArgumentNullException(nameof(destination)); - Mdc = mdc ?? throw new ArgumentNullException(nameof(mdc)); + Mdc = mdc; } public PingMsg(IPEndPoint farAddress, long expirationTime, IPEndPoint sourceAddress) @@ -37,7 +41,18 @@ public PingMsg(IPEndPoint farAddress, long expirationTime, IPEndPoint sourceAddr DestinationAddress = farAddress; } - public override string ToString() => base.ToString() + $", SourceAddress: {SourceAddress}, DestinationAddress: {DestinationAddress}, Version: {Version}, Mdc: {Mdc?.ToHexString()}"; + public override string ToString() => base.ToString() + $", SourceAddress: {SourceAddress}, DestinationAddress: {DestinationAddress}, Version: {Version}, Mdc: {(Mdc is { } mdc ? mdc.ToString() : null)}"; public override MsgType MsgType => MsgType.Ping; + + private static ValueHash256 CreateHash(byte[] mdc) + { + ArgumentNullException.ThrowIfNull(mdc); + if (mdc.Length != Hash256.Size) + { + throw new ArgumentException($"Discovery MDC must be {Hash256.Size} bytes.", nameof(mdc)); + } + + return new ValueHash256(mdc); + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs index ec7298634972..eac445fa0d21 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Messages/PongMsg.cs @@ -3,19 +3,20 @@ using System.Net; using Nethermind.Core.Crypto; -using Nethermind.Core.Extensions; namespace Nethermind.Network.Discovery.Discv4.Messages; public sealed class PongMsg : DiscoveryMsg { - public byte[] PingMdc { get; init; } + public ValueHash256 PingMdc { get; init; } - public PongMsg(IPEndPoint farAddress, long expirationTime, byte[] pingMdc) : base(farAddress, expirationTime) => PingMdc = pingMdc ?? throw new ArgumentNullException(nameof(pingMdc)); + public PongMsg(IPEndPoint farAddress, long expirationTime, ValueHash256 pingMdc) + : base(farAddress, expirationTime) => PingMdc = pingMdc; - public PongMsg(PublicKey farPublicKey, long expirationTime, byte[] pingMdc) : base(farPublicKey, expirationTime) => PingMdc = pingMdc ?? throw new ArgumentNullException(nameof(pingMdc)); + public PongMsg(PublicKey farPublicKey, long expirationTime, ValueHash256 pingMdc) + : base(farPublicKey, expirationTime) => PingMdc = pingMdc; - public override string ToString() => base.ToString() + $", PingMdc: {PingMdc?.ToHexString() ?? "empty"}"; + public override string ToString() => base.ToString() + $", PingMdc: {PingMdc}"; public override MsgType MsgType => MsgType.Pong; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs index 29009e4cd5c8..0eef47311e27 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/DiscoveryMsgSerializerBase.cs @@ -89,7 +89,7 @@ protected void AddSignatureAndMdc(IByteBuffer byteBuffer, int dataLength) byteBuffer.SetWriterIndex(startWriteIndex + length); } - protected (PublicKey FarPublicKey, Memory Mdc, IByteBuffer Data) PrepareForDeserialization(IByteBuffer msg) + protected (PublicKey FarPublicKey, ValueHash256 Mdc, IByteBuffer Data) PrepareForDeserialization(IByteBuffer msg) { if (msg.ReadableBytes < 98) { @@ -97,11 +97,11 @@ protected void AddSignatureAndMdc(IByteBuffer byteBuffer, int dataLength) } IByteBuffer data = msg.Slice(98, msg.ReadableBytes - 98); Memory msgBytes = msg.ReadAllBytesAsMemory(); - Memory mdc = msgBytes[..32]; + ValueHash256 mdc = new(msgBytes.Span[..Hash256.Size]); Span sigAndData = msgBytes.Span[32..]; Span computedMdc = ValueKeccak.Compute(sigAndData).BytesAsSpan; - if (!Bytes.AreEqual(mdc.Span, computedMdc)) + if (!Bytes.AreEqual(mdc.Bytes, computedMdc)) { throw new NetworkingException("Invalid MDC", NetworkExceptionType.Validation); } @@ -110,6 +110,17 @@ protected void AddSignatureAndMdc(IByteBuffer byteBuffer, int dataLength) return (nodeId, mdc, data); } + protected static ValueHash256 ReadHash(IByteBuffer byteBuffer, int index) + { + Span hash = stackalloc byte[Hash256.Size]; + for (int i = 0; i < Hash256.Size; i++) + { + hash[i] = byteBuffer.GetByte(index + i); + } + + return new ValueHash256(hash); + } + protected static void Encode(RlpStream stream, IPEndPoint address, int length) { stream.StartSequence(length); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs index f2745d889b50..24db5d37b4f4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/EnrRequestMsgSerializer.cs @@ -27,20 +27,20 @@ public void Serialize(IByteBuffer byteBuffer, EnrRequestMsg msg) AddSignatureAndMdc(byteBuffer, length + 1); byteBuffer.MarkReaderIndex(); - msg.Hash = byteBuffer.Slice(0, 32).ReadAllBytesAsArray(); + msg.Hash = ReadHash(byteBuffer, byteBuffer.ReaderIndex); byteBuffer.ResetReaderIndex(); } public EnrRequestMsg Deserialize(IByteBuffer msgBytes) { - (PublicKey farPublicKey, Memory mdc, IByteBuffer data) = PrepareForDeserialization(msgBytes); + (PublicKey farPublicKey, ValueHash256 mdc, IByteBuffer data) = PrepareForDeserialization(msgBytes); Rlp.ValueDecoderContext ctx = data.AsRlpContext(); ctx.ReadSequenceLength(); long expirationTime = ctx.DecodeLong(); data.SetReaderIndex(data.ReaderIndex + ctx.Position); - EnrRequestMsg msg = new(farPublicKey, mdc.ToArray(), expirationTime); + EnrRequestMsg msg = new(farPublicKey, mdc, expirationTime); return msg; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs index 7390ae65cbb6..64461fac5188 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PingMsgSerializer.cs @@ -37,13 +37,13 @@ public void Serialize(IByteBuffer byteBuffer, PingMsg msg) AddSignatureAndMdc(byteBuffer, totalLength + 1); byteBuffer.MarkReaderIndex(); - msg.Mdc = byteBuffer.Slice(0, 32).ReadAllBytesAsArray(); + msg.Mdc = ReadHash(byteBuffer, byteBuffer.ReaderIndex); byteBuffer.ResetReaderIndex(); } public PingMsg Deserialize(IByteBuffer msgBytes) { - (PublicKey FarPublicKey, Memory Mdc, IByteBuffer Data) = PrepareForDeserialization(msgBytes); + (PublicKey FarPublicKey, ValueHash256 Mdc, IByteBuffer Data) = PrepareForDeserialization(msgBytes); Rlp.ValueDecoderContext ctx = Data.AsRlpContext(); ctx.ReadSequenceLength(); int version = ctx.DecodeInt(); @@ -62,7 +62,7 @@ public PingMsg Deserialize(IByteBuffer msgBytes) IPEndPoint destination = GetAddress(destinationAddress, destinationUdpPort, allowZeroPort: true); long expireTime = ctx.DecodeLong(); - PingMsg msg = new(FarPublicKey, expireTime, source, destination, Mdc.ToArray()) { Version = version }; + PingMsg msg = new(FarPublicKey, expireTime, source, destination, Mdc) { Version = version }; if (version == 4) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs index 5dbe334b0102..b9d0dbabcc8d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/PongMsgSerializer.cs @@ -26,7 +26,8 @@ public void Serialize(IByteBuffer byteBuffer, PongMsg msg) NettyRlpStream stream = new(byteBuffer); stream.StartSequence(contentLength); Encode(stream, msg.FarAddress, farAddressLength); - stream.Encode(msg.PingMdc); + ValueHash256? pingMdc = msg.PingMdc; + stream.Encode(in pingMdc); stream.Encode(msg.ExpirationTime); byteBuffer.ResetIndex(); @@ -46,11 +47,16 @@ public PongMsg Deserialize(IByteBuffer msgBytes) ctx.DecodeByteArraySpan(IpAddressRlpLimit); ctx.DecodeInt(); // UDP port (we ignore and take it from Netty) ctx.DecodeInt(); // TCP port - byte[] token = ctx.DecodeByteArraySpan(RlpLimit.L32).ToArray(); + ReadOnlySpan token = ctx.DecodeByteArraySpan(RlpLimit.L32); + if (token.Length != Hash256.Size) + { + throw new NetworkingException($"PONG ping MDC must be {Hash256.Size} bytes.", NetworkExceptionType.Validation); + } + long expirationTime = ctx.DecodeLong(); data.SetReaderIndex(data.ReaderIndex + ctx.Position); - PongMsg msg = new(farPublicKey, expirationTime, token); + PongMsg msg = new(farPublicKey, expirationTime, new ValueHash256(token)); return msg; } @@ -70,7 +76,8 @@ private static (int totalLength, int contentLength, int farAddressLength) GetLen int farAddressLength = GetIPEndPointLength(message.FarAddress); int contentLength = Rlp.LengthOfSequence(farAddressLength); - contentLength += Rlp.LengthOf(message.PingMdc); + ValueHash256? pingMdc = message.PingMdc; + contentLength += Rlp.LengthOf(in pingMdc); contentLength += Rlp.LengthOf(message.ExpirationTime); return (Rlp.LengthOfSequence(contentLength), contentLength, farAddressLength); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 847d544245c7..33f9723bdfc5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -96,7 +96,7 @@ public Node[] GetNodesAtDistances(IEnumerable distances, Node? excluding = } } - return result.ToArray(); + return result.Count == 0 ? [] : result.ToArray(); } /// @@ -607,7 +607,7 @@ private NodeRecord[] GetFindNodeRecords(Distances distances, Node requester) AddFindNodeRecordsAtDistance(distance, requester, allowNonRoutableRelays, seen, ref result); } - return result.ToArray(); + return result.Count == 0 ? [] : result.ToArray(); } finally { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs index 75818f33d75b..71ca168abfde 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/RequestId.cs @@ -43,10 +43,4 @@ public int GetRlpLength() return Rlp.LengthOfByteString(Length, firstByte); } - public byte[] ToArray() - { - byte[] bytes = new byte[Length]; - CopyTo(bytes); - return bytes; - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 8fa134ccd9ea..416bd24c86ed 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -46,7 +46,7 @@ public sealed class PacketCodec( private readonly PrivateKey _privateKey = nodeKey.Unprotect(); private readonly PublicKey _publicKey = nodeKey.PublicKey; - private readonly byte[] _localNodeId = nodeKey.PublicKey.Hash.BytesToArray(); + private readonly Hash256 _localNodeId = nodeKey.PublicKey.Hash; private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; private readonly ICryptoRandom _cryptoRandom = cryptoRandom; private readonly IEcdsa _ecdsa = ecdsa; @@ -59,7 +59,7 @@ public void Dispose() } internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encryptionKey, Discv5Message message, ReadOnlySpan nonce) - => EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, nonce, _localNodeId, encryptionKey, message); + => EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, nonce, _localNodeId.Bytes, encryptionKey, message); [SkipLocalsInit] internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySpan requestNonce, ulong enrSequence, out Challenge challenge) @@ -81,7 +81,7 @@ internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Disc DeriveKeys( destination, ephemeralKey, - _localNodeId, + _localNodeId.Bytes, destination.Hash.Bytes, challenge.ChallengeData, out byte[] initiatorKey, @@ -100,7 +100,7 @@ internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Disc try { - _localNodeId.CopyTo(authData); + _localNodeId.Bytes.CopyTo(authData); authData[NodeIdSize] = IdSignatureSize; authData[NodeIdSize + 1] = EphemeralPublicKeySize; SignIdNonce(challenge.ChallengeData, ephemeralPublicKey, destination.Hash.Bytes, authData.Slice(HandshakeAuthDataHeadSize, IdSignatureSize)); @@ -315,12 +315,12 @@ internal bool TryDecryptHandshake( return false; } - if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature.Span, challenge.ChallengeData, ephemeralPublicKey.Bytes, _localNodeId)) + if (!VerifyIdSignature(remoteCompressedPublicKey, idSignature.Span, challenge.ChallengeData, ephemeralPublicKey.Bytes, _localNodeId.Bytes)) { return false; } - DeriveKeys(ephemeralPublicKey, sourceNodeId.Bytes, _localNodeId, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); + DeriveKeys(ephemeralPublicKey, sourceNodeId.Bytes, _localNodeId.Bytes, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); if (!TryDecryptMessage(packet, initiatorKey, out message)) { From cfdf4f2fa0b44a8799a01bc7995ba26aca217901 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Sat, 13 Jun 2026 14:43:12 +0300 Subject: [PATCH 165/182] Prevent a malformed discv5 packet from killing a worker loop A throw from packetCodec.TryDecode ran outside the per-packet handler's catch, so a single crafted packet could terminate one of the packet workers. Catch per iteration and rethrow only on shutdown cancellation. Co-Authored-By: Claude Opus 4.8 --- .../Discv5/Kademlia/KademliaAdapter.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 33f9723bdfc5..44bf87acbf95 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -165,6 +165,14 @@ private async Task RunPacketWorkerAsync(CancellationToken token) { await HandlePacket(result, token); } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + throw; + } + catch (Exception e) + { + if (_logger.IsTrace) _logger.Trace($"Error handling discv5 packet from {result.RemoteEndPoint}: {e}"); + } finally { result.Dispose(); From b3f5e091a9421af57f804598aced15f8fa3c8122 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 15 Jun 2026 09:53:35 +0300 Subject: [PATCH 166/182] Test cases --- .../Caching/LruCacheTests.cs | 23 ++--- .../DiscoveryV5AppTests.cs | 20 +--- .../Discv4/DiscoveryMessageSerializerTests.cs | 14 +-- .../Discv5/KademliaAdapterTests.cs | 94 ++++++++++--------- .../NetworkNodeDecoderTests.cs | 22 +---- .../Stats/NodeTests.cs | 40 ++++---- 6 files changed, 83 insertions(+), 130 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index 5d5d88d5a5f2..737a79f7d0c9 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -312,25 +312,10 @@ public void Clear_should_free_all_capacity() } } - [Test] - public async Task Clear_invokes_eviction_callback_outside_lock() - { - LruCache cache = null!; - TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); - cache = new LruCache(2, "test", _ => callbackResult.SetResult(cache.Contains(1))); - cache.Set(1, 10); - - Task clearTask = Task.Run(cache.Clear); - Task completedTask = await Task.WhenAny(clearTask, Task.Delay(TimeSpan.FromSeconds(5))); - - Assert.That(completedTask, Is.SameAs(clearTask)); - await clearTask; - Assert.That(await callbackResult.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.False); - } - [TestCase(EvictionOperation.Delete, false)] [TestCase(EvictionOperation.ReplaceExisting, true)] [TestCase(EvictionOperation.ReplaceOldest, false)] + [TestCase(EvictionOperation.Clear, false)] public async Task Eviction_callback_is_invoked_outside_lock(EvictionOperation operation, bool expectedContainsResult) { LruCache cache = null!; @@ -404,6 +389,9 @@ private static void RunEvictionOperation(LruCache cache, EvictionOpera case EvictionOperation.ReplaceOldest: cache.Set(3, 30); return; + case EvictionOperation.Clear: + cache.Clear(); + return; default: throw new ArgumentOutOfRangeException(nameof(operation), operation, null); } @@ -413,7 +401,8 @@ public enum EvictionOperation { Delete, ReplaceExisting, - ReplaceOldest + ReplaceOldest, + Clear } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index c283d6d7a550..4716e05781e2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -297,23 +297,11 @@ public void Should_Reject_Tcp_Only_Enr() Assert.That(node, Is.Null); } - [Test] - public void Should_Accept_Ipv6_Enr() - { - NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001); - - bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); - - Assert.That(result, Is.True); - Assert.That(node, Is.Not.Null); - Assert.That(node!.Host, Is.EqualTo("2001:4860:4860::8888")); - Assert.That(node.Port, Is.EqualTo(9001)); - } - - [Test] - public void Should_Accept_Ipv6_Enr_With_Default_Udp_Port() + [TestCase(true)] + [TestCase(false)] + public void Should_Accept_Ipv6_Enr(bool useUdp6) { - NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001, useUdp6: false); + NodeRecord enr = CreateTestIpv6Enr(TestItem.PrivateKeyA, IPAddress.Parse("2001:4860:4860::8888"), 9001, useUdp6); bool result = _discoveryV5App.TryGetAcceptableNodeFromEnr(enr, out Node? node); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs index b3e4fd4eb5c5..18b09e33c59f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs @@ -144,19 +144,6 @@ public void Enr_request_there_and_back() Assert.That(_privateKey.PublicKey, Is.EqualTo(deserialized.FarPublicKey)); } - [Test] - public void Enr_request_contains_hash() - { - EnrRequestMsg msg = new(TestItem.PublicKeyA, long.MaxValue); - using DisposableByteBuffer serialized = _messageSerializationService.ZeroSerialize(msg).AsDisposable(); - EnrRequestMsg deserialized = _messageSerializationService.Deserialize(serialized); - - Assert.That(deserialized.Hash, Is.Not.Null); - Hash256 hash = new(deserialized.Hash!.Value); - - Assert.That(hash, Is.EqualTo(new Hash256("0x64c2e38e89cdfca030166b7a271c301dd77cf043172966ab112d97fc3430fa16"))); - } - [Test] public void Enr_request_hash_does_not_alias_input_buffer() { @@ -164,6 +151,7 @@ public void Enr_request_hash_does_not_alias_input_buffer() using DisposableByteBuffer serialized = _messageSerializationService.ZeroSerialize(msg).AsDisposable(); byte[] packet = serialized.ReadAllBytesAsArray(); Hash256 expectedHash = new(packet.AsSpan(0, 32)); + Assert.That(expectedHash, Is.EqualTo(new Hash256("0x64c2e38e89cdfca030166b7a271c301dd77cf043172966ab112d97fc3430fa16"))); using DisposableByteBuffer input = Unpooled.WrappedBuffer(packet).AsDisposable(); EnrRequestMsg deserialized = _messageSerializationService.Deserialize(input); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index e9756694bad1..78d81b8309f5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Generic; using System.Net; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; @@ -93,43 +94,17 @@ public void TryAcceptChallenge_ShouldLimitBurstPerIp() Assert.That(adapter.TryAcceptChallenge(endpoint), Is.False); } - [Test] - public void IsAcceptableNodeRecord_ShouldRejectSpecialUseRecord() - { - NodeRecord documentationRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("192.0.2.1")); - - Assert.That( - KademliaAdapter.IsAcceptableNodeRecord( - documentationRecord, - TestItem.PrivateKeyB.PublicKey.Hash, - allowNonRoutable: true), - Is.False); - } - - [Test] - public void IsAcceptableNodeRecord_ShouldRejectNodeIdMismatch() - { - NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.8.8")); - - Assert.That( - KademliaAdapter.IsAcceptableNodeRecord( - record, - TestItem.PrivateKeyA.PublicKey.Hash, - allowNonRoutable: false), - Is.False); - } - - [Test] - public void IsAcceptableNodeRecord_ShouldAllowNonRoutableWhenRequested() + [TestCaseSource(nameof(AcceptableNodeRecordCases))] + public void IsAcceptableNodeRecord_ShouldValidateRecord(AcceptableNodeRecordCase testCase) { - NodeRecord loopbackRecord = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + NodeRecord record = CreateEnr(testCase.PrivateKey, testCase.IpAddress, includeEth2: testCase.IncludeEth2); Assert.That( KademliaAdapter.IsAcceptableNodeRecord( - loopbackRecord, - TestItem.PrivateKeyB.PublicKey.Hash, - allowNonRoutable: true), - Is.True); + NodeRecord.FromEnrString(record.EnrString), + testCase.ExpectedNodeId, + testCase.AllowNonRoutable), + Is.EqualTo(testCase.ExpectedResult)); } private KademliaAdapter CreateAdapter() @@ -157,19 +132,6 @@ private KademliaAdapter CreateAdapter() private static Node CreateNode(PublicKey publicKey, int hostSuffix) => new(publicKey, $"192.168.1.{hostSuffix}", 30303); - [Test] - public void IsAcceptableNodeRecord_ShouldRejectConsensusOnlyRecord() - { - NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Parse("8.8.8.8"), includeEth2: true); - - Assert.That( - KademliaAdapter.IsAcceptableNodeRecord( - NodeRecord.FromEnrString(record.EnrString), - TestItem.PrivateKeyB.PublicKey.Hash, - allowNonRoutable: false), - Is.False); - } - [Test] public void TrySetKnownRecord_ShouldNotDowngradeSequence() { @@ -195,4 +157,44 @@ private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, tcpPort: null, enrSequence: enrSequence, configureExtras: includeEth2 ? static enr => enr.SetEntry(new TestEth2Entry()) : null); + + private static IEnumerable AcceptableNodeRecordCases() + { + yield return new TestCaseData(new AcceptableNodeRecordCase( + TestItem.PrivateKeyB, + IPAddress.Parse("192.0.2.1"), + TestItem.PrivateKeyB.PublicKey.Hash, + AllowNonRoutable: true, + IncludeEth2: false, + ExpectedResult: false)).SetName("Rejects special-use record"); + yield return new TestCaseData(new AcceptableNodeRecordCase( + TestItem.PrivateKeyB, + IPAddress.Parse("8.8.8.8"), + TestItem.PrivateKeyA.PublicKey.Hash, + AllowNonRoutable: false, + IncludeEth2: false, + ExpectedResult: false)).SetName("Rejects node-id mismatch"); + yield return new TestCaseData(new AcceptableNodeRecordCase( + TestItem.PrivateKeyB, + IPAddress.Loopback, + TestItem.PrivateKeyB.PublicKey.Hash, + AllowNonRoutable: true, + IncludeEth2: false, + ExpectedResult: true)).SetName("Allows non-routable when requested"); + yield return new TestCaseData(new AcceptableNodeRecordCase( + TestItem.PrivateKeyB, + IPAddress.Parse("8.8.8.8"), + TestItem.PrivateKeyB.PublicKey.Hash, + AllowNonRoutable: false, + IncludeEth2: true, + ExpectedResult: false)).SetName("Rejects consensus-only record"); + } + + public readonly record struct AcceptableNodeRecordCase( + PrivateKey PrivateKey, + IPAddress IpAddress, + Hash256 ExpectedNodeId, + bool AllowNonRoutable, + bool IncludeEth2, + bool ExpectedResult); } diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs index d6062b2a39d5..bee16d01ea00 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs @@ -17,17 +17,12 @@ namespace Nethermind.Network.Test [TestFixture] public class NetworkNodeDecoderTests { - [Test] - public void Can_do_roundtrip() - { - NetworkNode node = new(TestItem.PublicKeyA, "127.0.0.1", 30303, 100L); - AssertRoundtripPreservesFields(node); - } - - [Test] - public void Can_do_roundtrip_negative_reputation() + [TestCase("127.0.0.1", 30303, 100L)] + [TestCase("127.0.0.1", 30303, -100L)] + [TestCase("127.0.0.1", -1, -100L)] + public void Can_do_roundtrip(string host, int port, long reputation) { - NetworkNode node = new(TestItem.PublicKeyA, "127.0.0.1", 30303, -100L); + NetworkNode node = new(TestItem.PublicKeyA, host, port, reputation); AssertRoundtripPreservesFields(node); } @@ -47,13 +42,6 @@ public void Can_read_regression() } } - [Test] - public void Negative_port_just_in_case_for_resilience() - { - NetworkNode node = new(TestItem.PublicKeyA, "127.0.0.1", -1, -100L); - AssertRoundtripPreservesFields(node); - } - private static void AssertRoundtripPreservesFields(NetworkNode node) { NetworkNodeDecoder networkNodeDecoder = new(); diff --git a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs index ab68a8f432c4..fd2f5b79f9f4 100644 --- a/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/Stats/NodeTests.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Net; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; @@ -39,19 +40,20 @@ public void Not_equal_to_another_type() Assert.That(node.Equals(1), Is.False); } - [Test] - public void TryFromEnr_uses_tcp_endpoint_for_peer_candidate() + [TestCase(NodeFromEnrMode.PeerCandidate, 30303)] + [TestCase(NodeFromEnrMode.Discovery, 30304)] + public void TryFromEnr_uses_expected_endpoint(NodeFromEnrMode mode, int expectedPort) { NodeRecord enr = CreateEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), tcpPort: 30303, udpPort: 30304); - bool result = Node.TryFromEnr(enr, out Node? node); + bool result = TryCreateNodeFromEnr(mode, enr, out Node? node); using (Assert.EnterMultipleScope()) { Assert.That(result, Is.True); Assert.That(node, Is.Not.Null); Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); - Assert.That(node.Port, Is.EqualTo(30303)); + Assert.That(node.Port, Is.EqualTo(expectedPort)); Assert.That(node.Enr, Is.EqualTo(enr.EnrString)); } } @@ -67,23 +69,6 @@ public void TryFromEnr_rejects_udp_only_record() Assert.That(node, Is.Null); } - [Test] - public void TryFromDiscoveryEnr_uses_udp_endpoint_for_discovery() - { - NodeRecord enr = CreateEnr(TestItem.PrivateKeyA, IPAddress.Parse("8.8.8.8"), tcpPort: 30303, udpPort: 30304); - - bool result = Node.TryFromDiscoveryEnr(enr, out Node? node); - - using (Assert.EnterMultipleScope()) - { - Assert.That(result, Is.True); - Assert.That(node, Is.Not.Null); - Assert.That(node!.Host, Is.EqualTo("8.8.8.8")); - Assert.That(node.Port, Is.EqualTo(30304)); - Assert.That(node.Enr, Is.EqualTo(enr.EnrString)); - } - } - [TestCase("s", "127.0.0.1:303")] [TestCase("a", " 127.0.0.1: 303")] [TestCase("c", "[Node|127.0.0.1:303|Details|ClientId]")] @@ -122,5 +107,18 @@ private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress, return enr; } + private static bool TryCreateNodeFromEnr(NodeFromEnrMode mode, NodeRecord enr, out Node? node) => + mode switch + { + NodeFromEnrMode.PeerCandidate => Node.TryFromEnr(enr, out node), + NodeFromEnrMode.Discovery => Node.TryFromDiscoveryEnr(enr, out node), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) + }; + + public enum NodeFromEnrMode + { + PeerCandidate, + Discovery + } } } From dc9dedb8bd324194a7cc81abb55f7cf08aaafcd9 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 15 Jun 2026 12:08:15 +0300 Subject: [PATCH 167/182] Better rlp encoder init --- .../DiscoveryV5AppTests.cs | 2 -- .../Discv4/DiscoveryPersistenceManagerTests.cs | 2 -- .../Nethermind.Network.Test/NetworkNodeDecoderTests.cs | 4 ++-- .../Nethermind.Network.Test/NetworkStorageTests.cs | 1 - src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs | 8 ++------ src/Nethermind/Nethermind.Network/NetworkStorage.cs | 9 +++++---- 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 4716e05781e2..3c9236e46a10 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -37,8 +37,6 @@ public class DiscoveryV5AppTests [SetUp] public void Setup() { - NetworkNodeDecoder.Init(); - _discoveryDb = new MemDb(); _discoveryV5App = CreateDiscoveryV5App(IPAddress.Parse("8.8.8.8")); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs index 241282601427..c0df4d8a6104 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryPersistenceManagerTests.cs @@ -40,8 +40,6 @@ public class DiscoveryPersistenceManagerTests [SetUp] public void Setup() { - NetworkNodeDecoder.Init(); - _discoveryDb = new MemDb(); _networkStorage = new NetworkStorage(_discoveryDb, LimboLogs.Instance); _nodeStatsManager = Substitute.For(); diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs index bee16d01ea00..fb2d612791c7 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkNodeDecoderTests.cs @@ -45,7 +45,7 @@ public void Can_read_regression() private static void AssertRoundtripPreservesFields(NetworkNode node) { NetworkNodeDecoder networkNodeDecoder = new(); - Rlp encoded = Rlp.Encode(node); + Rlp encoded = networkNodeDecoder.Encode(node); Rlp.ValueDecoderContext context = encoded.Bytes.AsRlpValueContext(); NetworkNode decoded = networkNodeDecoder.Decode(ref context); using (Assert.EnterMultipleScope()) @@ -67,7 +67,7 @@ public void Can_do_enr_roundtrip() Reputation = 100L }; - Rlp encoded = Rlp.Encode(node); + Rlp encoded = networkNodeDecoder.Encode(node); Rlp.ValueDecoderContext context = encoded.Bytes.AsRlpValueContext(); NetworkNode decoded = networkNodeDecoder.Decode(ref context); diff --git a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs index 30e686a8e8c5..283ed34712d4 100644 --- a/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NetworkStorageTests.cs @@ -22,7 +22,6 @@ public class NetworkStorageTests [SetUp] public void SetUp() { - NetworkNodeDecoder.Init(); ILogManager logManager = LimboLogs.Instance; _ = new ConfigProvider(); _tempDir = TempPath.GetTempDirectory(); diff --git a/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs b/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs index ad4c991f1169..a9a5bd48c8f2 100644 --- a/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs +++ b/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs @@ -12,9 +12,9 @@ namespace Nethermind.Network { public sealed class NetworkNodeDecoder : RlpDecoder { - private static readonly RlpLimit RlpLimit = RlpLimit.For((int)1.KiB, nameof(NetworkNode.HostIp)); + public static NetworkNodeDecoder Instance { get; } = new(); - static NetworkNodeDecoder() => Rlp.RegisterDecoder(typeof(NetworkNode), new NetworkNodeDecoder()); + private static readonly RlpLimit RlpLimit = RlpLimit.For((int)1.KiB, nameof(NetworkNode.HostIp)); protected override NetworkNode DecodeInternal(ref Rlp.ValueDecoderContext decoderContext, RlpBehaviors rlpBehaviors = RlpBehaviors.None) { @@ -97,9 +97,5 @@ private static bool IsEnrString(ReadOnlySpan value) => value.Length != PublicKey.LengthInBytes && value is [(byte)'e', (byte)'n', (byte)'r', (byte)':', ..]; - public static void Init() - { - // here to register with RLP in static constructor - } } } diff --git a/src/Nethermind/Nethermind.Network/NetworkStorage.cs b/src/Nethermind/Nethermind.Network/NetworkStorage.cs index 80ff4ec8622c..54791b616252 100644 --- a/src/Nethermind/Nethermind.Network/NetworkStorage.cs +++ b/src/Nethermind/Nethermind.Network/NetworkStorage.cs @@ -11,12 +11,13 @@ using Nethermind.Core.Extensions; using Nethermind.Db; using Nethermind.Logging; -using Nethermind.Serialization.Rlp; namespace Nethermind.Network { public class NetworkStorage(IFullDb? fullDb, ILogManager? logManager) : INetworkStorage { + private static readonly NetworkNodeDecoder NodeDecoder = NetworkNodeDecoder.Instance; + private readonly Lock _lock = new(); private readonly IFullDb _fullDb = fullDb ?? throw new ArgumentNullException(nameof(fullDb)); private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); @@ -92,7 +93,7 @@ public void UpdateNode(NetworkNode node) { lock (_lock) { - byte[] rlp = Rlp.Encode(node).Bytes; + byte[] rlp = NodeDecoder.Encode(node).Bytes; UpdateNodeImpl(node, rlp); } } @@ -121,7 +122,7 @@ public void UpdateNodes(IEnumerable nodes) List<(NetworkNode Node, byte[] Rlp)> encodedNodes = []; foreach (NetworkNode node in nodes) { - encodedNodes.Add((node, Rlp.Encode(node).Bytes)); + encodedNodes.Add((node, NodeDecoder.Encode(node).Bytes)); } lock (_lock) @@ -224,7 +225,7 @@ private void ClearLocalCacheNoLock() private static NetworkNode GetNode(byte[] networkNodeRaw) { - NetworkNode persistedNode = Rlp.Decode(networkNodeRaw); + NetworkNode persistedNode = NodeDecoder.DecodeComplete(networkNodeRaw); return persistedNode; } From f1aa511696815e59b8672f7582903bbea4f78a86 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 16 Jun 2026 12:14:15 +0300 Subject: [PATCH 168/182] Reduce discovery retention --- src/Nethermind/Nethermind.Kademlia/KBucket.cs | 21 ++++-- .../Kademlia/KBucketTests.cs | 32 +++++++++ .../Kademlia/RecentNodeFilterTests.cs | 42 ++++++++++++ .../Discv4/Kademlia/NodeSource.cs | 2 +- .../Discv5/Kademlia/NodeSource.cs | 2 +- .../Kademlia/RecentNodeFilter.cs | 66 +++++++++++++++---- 6 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RecentNodeFilterTests.cs diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index fd06026683dd..5eb239bb25d3 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -8,8 +8,11 @@ public class KBucket(int k) where TNode : notnull where TKadKey : notnull { + public const int DefaultReplacementCacheSize = 16; + private readonly DoubleEndedLru _items = new(k); - private readonly DoubleEndedLru _replacement = new(k); + private readonly DoubleEndedLru _replacement = new(GetReplacementCacheSize(k)); + private readonly bool _cacheItems = k <= DefaultReplacementCacheSize; public int Count => _items.Count; @@ -25,8 +28,9 @@ public class KBucket(int k) public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? toRefresh) { BucketAddResult addResult = _items.AddOrRefresh(hash, item, out TNode? previous); - if (addResult == BucketAddResult.Added || - (addResult == BucketAddResult.Refreshed && ShouldUpdateCachedArray(previous, item))) + if (_cacheItems + && (addResult == BucketAddResult.Added + || (addResult == BucketAddResult.Refreshed && ShouldUpdateCachedArray(previous, item)))) { _cachedArray = _items.GetAll(); } @@ -43,7 +47,7 @@ public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? t return BucketAddResult.Full; } - public TNode[] GetAll() => _cachedArray; + public TNode[] GetAll() => _cacheItems ? _cachedArray : _items.GetAll(); public (TKadKey, TNode)[] GetAllWithHash() => _items.GetAllWithKey(); @@ -57,7 +61,11 @@ public bool RemoveAndReplace(in TKadKey hash) { _items.AddOrRefresh(replacementHash, replacement!); } - _cachedArray = _items.GetAll(); + + if (_cacheItems) + { + _cachedArray = _items.GetAll(); + } return true; } @@ -78,4 +86,7 @@ private static bool ShouldUpdateCachedArray(TNode? previous, TNode item) (typeof(TNode).IsValueType ? !EqualityComparer.Default.Equals(previous, item) : !ReferenceEquals(previous, item)); + + private static int GetReplacementCacheSize(int bucketSize) + => bucketSize < DefaultReplacementCacheSize ? bucketSize : DefaultReplacementCacheSize; } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index 357db9bbbecd..6c3b96b135d5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -40,6 +40,19 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() Assert.That(bucket.GetAll(), Is.SameAs(nodes)); } + [Test] + public void GetAll_should_not_keep_cached_array_for_large_bucket() + { + KBucket bucket = new(KBucket.DefaultReplacementCacheSize + 1); + AddNodes(bucket, Enumerable.Range(0, KBucket.DefaultReplacementCacheSize + 1) + .Select(static k => ValueKeccak.Compute(k.ToString())) + .ToArray()); + + ValueHash256[] nodes = bucket.GetAll(); + + Assert.That(bucket.GetAll(), Is.Not.SameAs(nodes)); + } + [Test] public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNewInstance() { @@ -66,6 +79,25 @@ public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (IdentityNodeHashProvider.ToHash(it), it)).ToHashSet())); } + [Test] + public void Replacement_cache_should_not_scale_with_large_bucket_size() + { + const int bucketSize = KBucket.DefaultReplacementCacheSize * 2; + + KBucket bucket = new(bucketSize); + ValueHash256[] nodes = Enumerable.Range(0, bucketSize + KBucket.DefaultReplacementCacheSize + 1) + .Select(static k => ValueKeccak.Compute(k.ToString())) + .ToArray(); + + AddNodes(bucket, nodes); + foreach (ValueHash256 node in nodes[..bucketSize]) + { + bucket.RemoveAndReplace(IdentityNodeHashProvider.ToHash(node)); + } + + Assert.That(bucket.Count, Is.EqualTo(KBucket.DefaultReplacementCacheSize)); + } + private static (KBucket Bucket, ValueHash256[] Nodes) BuildFullBucket() { KBucket bucket = new(5); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RecentNodeFilterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RecentNodeFilterTests.cs new file mode 100644 index 000000000000..a9d40a31c331 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RecentNodeFilterTests.cs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Discovery.Kademlia; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class RecentNodeFilterTests +{ + [TestCase(0, 64)] + [TestCase(4, 1024)] + [TestCase(16, 4096)] + [TestCase(160, 4096)] + public void GetLimit_should_cap_large_bucket_multiplier(int bucketSize, int expected) + => Assert.That(RecentNodeFilter.GetLimit(bucketSize, maxDistance: 256, minimumCount: 64), Is.EqualTo(expected)); + + [Test] + public void TryReserve_should_reject_recent_node_until_released() + { + RecentNodeFilter filter = new(2); + + Assert.That(filter.TryReserve("a"), Is.True); + Assert.That(filter.TryReserve("a"), Is.False); + + filter.Release("a"); + + Assert.That(filter.TryReserve("a"), Is.True); + } + + [Test] + public void TryReserve_should_evict_oldest_active_node_when_limit_is_exceeded() + { + RecentNodeFilter filter = new(2); + + Assert.That(filter.TryReserve("a"), Is.True); + Assert.That(filter.TryReserve("b"), Is.True); + Assert.That(filter.TryReserve("c"), Is.True); + + Assert.That(filter.TryReserve("a"), Is.True); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index 38ae8b178c73..313d91ea815a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -24,7 +24,7 @@ public sealed class NodeSource( private const int ChannelCapacity = 64; private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256KademliaDistance.Instance.MaxDistance); + private readonly int _recentNodeLimit = RecentNodeFilter.GetLimit(kademliaConfig.KSize, Hash256KademliaDistance.Instance.MaxDistance, ChannelCapacity); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index e32be1d0aeb9..5f87991b08d2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -23,7 +23,7 @@ public sealed class NodeSource( private readonly ILogger _logger = logManager.GetClassLogger(); private readonly Hash256 _currentNodeHash = kademliaConfig.CurrentNodeId.IdHash; - private readonly int _recentNodeLimit = Math.Max(ChannelCapacity, kademliaConfig.KSize * Hash256KademliaDistance.Instance.MaxDistance); + private readonly int _recentNodeLimit = RecentNodeFilter.GetLimit(kademliaConfig.KSize, Hash256KademliaDistance.Instance.MaxDistance, ChannelCapacity); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs index 898c12bda4f9..71883ea01bf3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/RecentNodeFilter.cs @@ -3,12 +3,21 @@ namespace Nethermind.Network.Discovery.Kademlia; +internal static class RecentNodeFilter +{ + private const int MaxBucketSizeForLimit = 16; + + public static int GetLimit(int bucketSize, int maxDistance, int minimumCount) + => Math.Max(minimumCount, Math.Min(bucketSize, MaxBucketSizeForLimit) * maxDistance); +} + internal sealed class RecentNodeFilter(int maxCount) where TKey : notnull { - private readonly LinkedList _recentNodes = []; - private readonly Dictionary> _nodes = new(maxCount); + private readonly Dictionary _nodes = new(maxCount); private readonly Lock _lock = new(); + private Queue<(TKey NodeId, long Generation)> _recentNodes = new(maxCount); + private long _generation; public bool TryReserve(TKey nodeId) { @@ -19,14 +28,10 @@ public bool TryReserve(TKey nodeId) return false; } - LinkedListNode listNode = _recentNodes.AddLast(nodeId); - _nodes.Add(nodeId, listNode); - while (_nodes.Count > maxCount) - { - LinkedListNode oldestNode = _recentNodes.First!; - _recentNodes.RemoveFirst(); - _nodes.Remove(oldestNode.Value); - } + long generation = unchecked(++_generation); + _nodes.Add(nodeId, generation); + _recentNodes.Enqueue((nodeId, generation)); + Trim(); return true; } @@ -36,10 +41,47 @@ public void Release(TKey nodeId) { lock (_lock) { - if (_nodes.Remove(nodeId, out LinkedListNode? listNode)) + _nodes.Remove(nodeId); + DropReleasedHeadEntries(); + if (_recentNodes.Count > Math.Max(maxCount * 2, 256)) + { + CompactQueue(); + } + } + } + + private void Trim() + { + DropReleasedHeadEntries(); + while (_nodes.Count > maxCount && _recentNodes.TryDequeue(out (TKey NodeId, long Generation) oldestNode)) + { + if (_nodes.TryGetValue(oldestNode.NodeId, out long generation) && generation == oldestNode.Generation) { - _recentNodes.Remove(listNode); + _nodes.Remove(oldestNode.NodeId); } } } + + private void DropReleasedHeadEntries() + { + while (_recentNodes.TryPeek(out (TKey NodeId, long Generation) oldestNode) && + (!_nodes.TryGetValue(oldestNode.NodeId, out long generation) || generation != oldestNode.Generation)) + { + _recentNodes.Dequeue(); + } + } + + private void CompactQueue() + { + Queue<(TKey NodeId, long Generation)> compacted = new(_nodes.Count); + foreach ((TKey NodeId, long Generation) node in _recentNodes) + { + if (_nodes.TryGetValue(node.NodeId, out long generation) && generation == node.Generation) + { + compacted.Enqueue(node); + } + } + + _recentNodes = compacted; + } } From da8eb5fd08878596fd5713d9e062b58a7c8bd7ff Mon Sep 17 00:00:00 2001 From: Amirul Ashraf Date: Thu, 18 Jun 2026 15:51:46 +0800 Subject: [PATCH 169/182] feat: host-side prerequisites for the embedded beacon chain plugin (SSZ ulong limits, injectable discv5 ENR filter) (#11985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ssz): support list limits beyond int range in the SSZ generator Beacon-chain lists use limits up to 2^40 (VALIDATOR_REGISTRY_LIMIT), which overflowed the int-typed SszListAttribute. The limit is now ulong end to end; decode-side count guards are omitted when the limit exceeds int.MaxValue since an int-typed count can never violate them. Co-Authored-By: Claude Fable 5 * refactor(discv5): make the consensus-only ENR drop policy injectable The shared discv5 stack hard-dropped consensus-only node records in the response handler, adapter acceptance check, and node source — fatal for a consumer that wants CL peers. Discv5RecordFilter makes the policy injectable with the execution-layer behavior as the default (unchanged); a beacon-chain scope can register AcceptAll. Needed by the embedded beacon chain driver plugin (#11976). Co-Authored-By: Claude Fable 5 * refactor(discv5): dedicated IDiscv5RecordFilter, required everywhere Replace the bool-parameterized filter class with an interface and two implementations, and make the dependency non-optional at every record-handling component (no null-coalesced defaults) so each discv5 instance states its policy explicitly. The interface documents why the policy exists: discv5 is one DHT shared by EL and CL nodes, and which records are useful is per instance — the EL must drop consensus-only ENRs it can never dial over RLPx, while a CL instance exists precisely to find them. Co-Authored-By: Claude Fable 5 * fix(ssz): lift the bitlist limit guard to ulong; drop a dead generator arm Review follow-up: ValidateSszBitlistLimit now takes ulong like the list variant, the BitList validation emission carries the UL suffix, and the bitlist Encode emission clamps to int.MaxValue (the runtime parameter is int-typed and unused). The Kind.List-with-BitArray arm could never fire — such properties resolve to Kind.BitList — and is removed. Regression: the huge-limit test now also covers a 2^40-limit bitlist. Co-Authored-By: Claude Fable 5 * refactor(ssz): reject huge bitlist limits; address review feedback - Generator's SszListAttribute mirror now takes `ulong limit`, matching the runtime attribute (flcl42). - SszProperty parses the limit as `ulong` directly (the attribute is `ulong`, so the value is never `int`) and now rejects a `BitArray` list whose limit exceeds `int.MaxValue` with an SSZ003 generator error instead of silently clamping the int-typed Encode parameter — a BitArray cannot hold more than int.MaxValue bits (flcl42). - Drop the now-unsupported huge-limit bitlist test type; add a generator diagnostic regression test for the rejection. - Trim the IDiscv5RecordFilter block (flcl42). Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Fable 5 --- .../Handlers/NodesResponseHandlerTests.cs | 3 +- .../Discv5/KademliaAdapterTests.cs | 4 +- .../Discv5/NodeSourceTests.cs | 3 ++ .../Discv5/WireTests.cs | 1 + .../Kademlia/Handlers/NodesResponseHandler.cs | 4 +- .../Discv5/Kademlia/IDiscv5RecordFilter.cs | 29 +++++++++++ .../Discv5/Kademlia/KademliaAdapter.cs | 15 +++--- .../Discv5/Kademlia/KademliaModule.cs | 1 + .../Discv5/Kademlia/NodeSource.cs | 3 +- .../SszSerializableAttribute.cs | 4 +- .../EncodingTest.cs | 35 ++++++++++++++ .../SszGeneratorDiagnosticTest.cs | 20 ++++++++ .../SszTypes.cs | 15 ++++++ .../Attributes.cs | 4 +- .../SszGenerator.cs | 48 ++++++++++--------- .../SszProperty.cs | 11 ++++- 16 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IDiscv5RecordFilter.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index 72a5a119b83c..3f0cbce16537 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -8,6 +8,7 @@ using Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; using Nethermind.Network.Discovery.Discv5.Messages; using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Network.Discovery.Discv5.Kademlia; using Nethermind.Network.Enr; using Nethermind.Stats.Model; using NUnit.Framework; @@ -38,6 +39,6 @@ private static NodesResponseHandler CreateNodesResponseHandler(Node receiver, No { PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); - return new NodesResponseHandler(receiver, new Distances([distance]), Hash256KademliaDistance.Instance); + return new NodesResponseHandler(receiver, new Distances([distance]), Hash256KademliaDistance.Instance, ExecutionLayerDiscv5RecordFilter.Instance); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 78d81b8309f5..f7dcac72e1e7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -103,7 +103,8 @@ public void IsAcceptableNodeRecord_ShouldValidateRecord(AcceptableNodeRecordCase KademliaAdapter.IsAcceptableNodeRecord( NodeRecord.FromEnrString(record.EnrString), testCase.ExpectedNodeId, - testCase.AllowNonRoutable), + testCase.AllowNonRoutable, + ExecutionLayerDiscv5RecordFilter.Instance), Is.EqualTo(testCase.ExpectedResult)); } @@ -126,6 +127,7 @@ private KademliaAdapter CreateAdapter() new DiscoveryConfig(), new CryptoRandom(), Hash256KademliaDistance.Instance, + ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index 9a71cb9f0bd2..2591b1b1b841 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -30,6 +30,7 @@ public async Task DiscoverNodes_ShouldNotRetainDroppedNodesInRecentDedupe(Cancel NodeSource source = new( kademlia, new KademliaConfig { CurrentNodeId = CreateNode(0) }, + ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); @@ -71,6 +72,7 @@ public async Task DiscoverNodes_ShouldEmitPeerCandidateWithTcpEndpoint(Cancellat NodeSource source = new( kademlia, new KademliaConfig { CurrentNodeId = CreateNode(0) }, + ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); @@ -97,6 +99,7 @@ public async Task DiscoverNodes_ShouldSkipConsensusOnlyEnrs(CancellationToken to NodeSource source = new( kademlia, new KademliaConfig { CurrentNodeId = CreateNode(0) }, + ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index 210804a88778..5e89b22f2b66 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -216,6 +216,7 @@ private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, b new DiscoveryConfig(), new CryptoRandom(), Hash256KademliaDistance.Instance, + ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); return new TestPeer(adapter, handler, channel, kademlia, nodeRecordProvider, endpoint); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index a2df08be41d8..5fd6428906f0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia.Handlers; -internal sealed class NodesResponseHandler(Node receiver, Distances requestedDistances, IKademliaDistance distanceCalculator) +internal sealed class NodesResponseHandler(Node receiver, Distances requestedDistances, IKademliaDistance distanceCalculator, IDiscv5RecordFilter recordFilter) : ResponseHandler(MessageType.Nodes), IDisposable { private const int MaxNodesResponseMessages = 16; @@ -73,7 +73,7 @@ public override bool Handle(NodesMsg nodes) for (int i = 0; i < nodes.Records.Count && _nodeCount < MaxNodesResponseRecords; i++) { NodeRecord record = nodes.Records[i]; - if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record) || + if (recordFilter.Excludes(record) || !Node.TryFromDiscoveryEnr(record, out Node? node) || !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || !_seenNodeIds.Add(node.Id.Hash) || diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IDiscv5RecordFilter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IDiscv5RecordFilter.cs new file mode 100644 index 000000000000..5dd1cc8c70b5 --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/IDiscv5RecordFilter.cs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Network.Enr; + +namespace Nethermind.Network.Discovery.Discv5.Kademlia; + +/// Protocol-level ENR acceptance policy of a discv5 instance. +public interface IDiscv5RecordFilter +{ + /// Returns whether this discv5 instance must drop the record. + bool Excludes(NodeRecord record); +} + +/// Drops consensus-only ENRs: the execution layer cannot dial beacon nodes over RLPx. +public sealed class ExecutionLayerDiscv5RecordFilter : IDiscv5RecordFilter +{ + public static ExecutionLayerDiscv5RecordFilter Instance { get; } = new(); + + public bool Excludes(NodeRecord record) => DiscoveryV5App.IsConsensusOnlyNodeRecord(record); +} + +/// Accepts every record; used by consensus-layer discovery, which filters by fork digest downstream. +public sealed class AcceptAllDiscv5RecordFilter : IDiscv5RecordFilter +{ + public static AcceptAllDiscv5RecordFilter Instance { get; } = new(); + + public bool Excludes(NodeRecord record) => false; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 44bf87acbf95..32b476ac1726 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -30,6 +30,7 @@ public sealed class KademliaAdapter( IDiscoveryConfig discoveryConfig, ICryptoRandom cryptoRandom, IKademliaDistance distance, + IDiscv5RecordFilter recordFilter, ILogManager logManager) : IKademliaAdapter { private const int MaxFindNodeRecords = 16; @@ -125,7 +126,7 @@ public async Task Ping(Node receiver, CancellationToken token) RegisterKnownRecord(receiver); Distances distances = GetLookupDistances(receiver, target); using FindNodeMsg findNode = new(CreateRequestId(), distances); - using NodesResponseHandler responseHandler = new(receiver, distances, _distance); + using NodesResponseHandler responseHandler = new(receiver, distances, _distance, recordFilter); if (_logger.IsTrace) _logger.Trace($"Sending discv5 FINDNODE {findNode.RequestId} to {receiver:s}, distances: {FormatDistances(distances)}."); if (!await SendRequest(receiver, findNode, responseHandler, _findNodeTimeout, token)) @@ -468,7 +469,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat return; } - if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address))) + if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address), recordFilter)) { TrySetKnownRecord(nodeId, nodeRecord, out NodeRecord currentRecord); messageRecord = currentRecord; @@ -653,13 +654,13 @@ private void AddFindNodeRecordsAtDistance( { if (TryGetKnownRecord(node.Id.Hash, out NodeRecord? knownRecord)) { - return IsAcceptableNodeRecord(knownRecord, node.Id.Hash, allowNonRoutableRelays) ? knownRecord : null; + return IsAcceptableNodeRecord(knownRecord, node.Id.Hash, allowNonRoutableRelays, recordFilter) ? knownRecord : null; } try { NodeRecord record = NodeRecord.FromEnrString(node.Enr); - return IsAcceptableNodeRecord(record, node.Id.Hash, allowNonRoutableRelays) ? record : null; + return IsAcceptableNodeRecord(record, node.Id.Hash, allowNonRoutableRelays, recordFilter) ? record : null; } catch (Exception e) { @@ -678,7 +679,7 @@ private void RegisterKnownRecord(Node node) try { NodeRecord record = NodeRecord.FromEnrString(node.Enr); - if (IsAcceptableNodeRecord(record, node.Id.Hash, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address))) + if (IsAcceptableNodeRecord(record, node.Id.Hash, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address), recordFilter)) { TrySetKnownRecord(node.Id.Hash, record, out _); } @@ -770,8 +771,8 @@ internal bool TrySetKnownRecord(Hash256 nodeId, NodeRecord record, out NodeRecor } } - internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable) - => !DiscoveryV5App.IsConsensusOnlyNodeRecord(record) && + internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable, IDiscv5RecordFilter recordFilter) + => !recordFilter.Excludes(record) && Node.TryFromDiscoveryEnr(record, out Node? node) && node.Id.Hash.Equals(expectedNodeId) && DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, allowNonRoutable); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs index d6facf4bf57b..0e0f1c86bcbf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs @@ -17,6 +17,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; public sealed class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder + .AddSingleton(ExecutionLayerDiscv5RecordFilter.Instance) .AddSingleton() .AddSingleton() .Bind, IKademliaAdapter>() diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index 5f87991b08d2..9fb81dc0c663 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -16,6 +16,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; public sealed class NodeSource( IKademlia kademlia, KademliaConfig kademliaConfig, + IDiscv5RecordFilter recordFilter, ILogManager logManager) : IKademliaNodeSource { @@ -95,7 +96,7 @@ private bool TryCreatePeerCandidate(Node discoveryNode, [NotNullWhen(true)] out try { NodeRecord record = NodeRecord.FromEnrString(discoveryNode.Enr); - if (DiscoveryV5App.IsConsensusOnlyNodeRecord(record)) + if (recordFilter.Excludes(record)) { return false; } diff --git a/src/Nethermind/Nethermind.Serialization.Ssz/SszSerializableAttribute.cs b/src/Nethermind/Nethermind.Serialization.Ssz/SszSerializableAttribute.cs index f5c493fec8d1..341d443e85ae 100644 --- a/src/Nethermind/Nethermind.Serialization.Ssz/SszSerializableAttribute.cs +++ b/src/Nethermind/Nethermind.Serialization.Ssz/SszSerializableAttribute.cs @@ -33,9 +33,9 @@ public class SszFieldAttribute(int index) : Attribute } [AttributeUsage(AttributeTargets.Property)] -public class SszListAttribute(int limit) : Attribute +public class SszListAttribute(ulong limit) : Attribute { - public int Limit { get; } = limit; + public ulong Limit { get; } = limit; } [AttributeUsage(AttributeTargets.Property)] diff --git a/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/EncodingTest.cs b/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/EncodingTest.cs index 2f9246217d9f..66a994edaa21 100644 --- a/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/EncodingTest.cs +++ b/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/EncodingTest.cs @@ -99,6 +99,41 @@ public void Decode_bitvector_preserves_declared_length() } } + [Test] + public void Supports_list_limits_beyond_int_range() + { + const ulong Limit = 1_099_511_627_776; // 2^40, VALIDATOR_REGISTRY_LIMIT + ulong[] basicItems = [1, 2, 3]; + FixedC[] compositeItems = [new() { Fixed1 = 1, Fixed2 = 2 }, new() { Fixed1 = 3, Fixed2 = 4 }]; + + HugeLimitBasicList basicList = new() { Items = basicItems }; + byte[] encoded = HugeLimitBasicList.Encode(basicList); + HugeLimitBasicList.Decode(encoded, out HugeLimitBasicList decodedBasic); + HugeLimitBasicList.Merkleize(basicList, out UInt256 basicRoot); + + // Reference roots computed via the runtime ulong-limit merkleization primitives + Merkle.Merkleize(out UInt256 expectedBasicRoot, MemoryMarshal.AsBytes(basicItems), Limit / 4); + Merkle.MixIn(ref expectedBasicRoot, basicItems.Length); + + HugeLimitCompositeList compositeList = new() { Items = compositeItems }; + HugeLimitCompositeList.Merkleize(compositeList, out UInt256 compositeRoot); + + Span itemRoots = stackalloc UInt256[compositeItems.Length]; + for (int i = 0; i < compositeItems.Length; i++) + { + FixedC.Merkleize(compositeItems[i], out itemRoots[i]); + } + Merkle.Merkleize(out UInt256 expectedCompositeRoot, itemRoots, Limit); + Merkle.MixIn(ref expectedCompositeRoot, compositeItems.Length); + + using (Assert.EnterMultipleScope()) + { + Assert.That(decodedBasic.Items, Is.EqualTo(basicItems)); + Assert.That(basicRoot, Is.EqualTo(expectedBasicRoot)); + Assert.That(compositeRoot, Is.EqualTo(expectedCompositeRoot)); + } + } + [Test] public void Encode_and_decode_signed_primitive_collections_round_trip() { diff --git a/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszGeneratorDiagnosticTest.cs b/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszGeneratorDiagnosticTest.cs index 23f1cab476cc..ba45ec5c6a7c 100644 --- a/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszGeneratorDiagnosticTest.cs +++ b/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszGeneratorDiagnosticTest.cs @@ -219,6 +219,26 @@ public static void Feed(ref Merkleizer merkleizer, DuplicateFixedBytes value) Assert.That(diagnostic.GetMessage(), Does.Contain("Multiple SSZ converters")); } + [Test] + public void Bitlist_with_limit_beyond_int_range_reports_diagnostic() + { + const string source = """ + using System.Collections; + using Nethermind.Serialization.Ssz; + + [SszContainer] + public partial struct HugeBitlistContainer + { + [SszList(1_099_511_627_776)] + public BitArray? Bits { get; set; } + } + """; + + CSharpParseOptions parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + Diagnostic diagnostic = GetSsz003Diagnostic(source, parseOptions, nameof(Bitlist_with_limit_beyond_int_range_reports_diagnostic)); + Assert.That(diagnostic.GetMessage(), Does.Contain("BitArray cannot exceed int.MaxValue bits")); + } + [Test] public void Converter_backed_primitive_collections_emit_converter_calls() { diff --git a/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszTypes.cs b/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszTypes.cs index 78ca4ceb3f5c..e9657bd25915 100644 --- a/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszTypes.cs +++ b/src/Nethermind/Nethermind.Serialization.SszGenerator.Test/SszTypes.cs @@ -463,4 +463,19 @@ public partial class ShadowDerived : ShadowBase public new uint X { get; set; } } + [SszContainer(isCollectionItself: true)] + public partial struct HugeLimitBasicList + { + // VALIDATOR_REGISTRY_LIMIT-sized list (2^40), exceeds int.MaxValue + [SszList(1_099_511_627_776)] + public ulong[] Items { get; set; } + } + + [SszContainer(isCollectionItself: true)] + public partial struct HugeLimitCompositeList + { + [SszList(1_099_511_627_776)] + public FixedC[] Items { get; set; } + } + } diff --git a/src/Nethermind/Nethermind.Serialization.SszGenerator/Attributes.cs b/src/Nethermind/Nethermind.Serialization.SszGenerator/Attributes.cs index be420f0c2336..7548e95634f2 100644 --- a/src/Nethermind/Nethermind.Serialization.SszGenerator/Attributes.cs +++ b/src/Nethermind/Nethermind.Serialization.SszGenerator/Attributes.cs @@ -21,9 +21,9 @@ public class SszFieldAttribute(int index) : Attribute } [AttributeUsage(AttributeTargets.Property)] -public class SszListAttribute(int limit) : Attribute +public class SszListAttribute(ulong limit) : Attribute { - public int Limit { get; } = limit; + public ulong Limit { get; } = limit; } [AttributeUsage(AttributeTargets.Property)] diff --git a/src/Nethermind/Nethermind.Serialization.SszGenerator/SszGenerator.cs b/src/Nethermind/Nethermind.Serialization.SszGenerator/SszGenerator.cs index 207587069f58..3c5e967ee1e2 100644 --- a/src/Nethermind/Nethermind.Serialization.SszGenerator/SszGenerator.cs +++ b/src/Nethermind/Nethermind.Serialization.SszGenerator/SszGenerator.cs @@ -301,9 +301,9 @@ internal static void ValidateSszVectorLength(ReadOnlySpan items, int expec } } - internal static void ValidateSszListLimit(ReadOnlySpan items, int limit, string typeName, string fieldName) + internal static void ValidateSszListLimit(ReadOnlySpan items, ulong limit, string typeName, string fieldName) { - if (items.Length > limit) + if ((ulong)items.Length > limit) { ThrowInvalidSszValue(typeName, fieldName, $"expected at most {limit} elements but found {items.Length}."); } @@ -318,9 +318,9 @@ internal static void ValidateSszBitvectorLength(BitArray? bits, int expectedLeng } } - internal static void ValidateSszBitlistLimit(BitArray? bits, int limit, string typeName, string fieldName) + internal static void ValidateSszBitlistLimit(BitArray? bits, ulong limit, string typeName, string fieldName) { - if (bits is not null && bits.Length > limit) + if (bits is not null && (ulong)bits.Length > limit) { ThrowInvalidSszValue(typeName, fieldName, $"expected at most {limit} bits but found {bits.Length}."); } @@ -645,10 +645,9 @@ private static string ValidationStatement(SszType decl, SszProperty property, st { Kind.Vector when property.Type.Name == "BitArray" => $"ValidateSszBitvectorLength({expression}, {property.Length}, nameof({decl.TypeReferenceName}), nameof({property.Name}));", Kind.Vector => $"ValidateSszVectorLength({SpanExpression(property, expression)}, {property.Length}, nameof({decl.TypeReferenceName}), nameof({property.Name}));", - Kind.List when property.Type.Name == "BitArray" => $"ValidateSszBitlistLimit({expression}, {property.Limit}, nameof({decl.TypeReferenceName}), nameof({property.Name}));", - Kind.List => $"ValidateSszListLimit({SpanExpression(property, expression)}, {property.Limit}, nameof({decl.TypeReferenceName}), nameof({property.Name}));", + Kind.List => $"ValidateSszListLimit({SpanExpression(property, expression)}, {property.Limit}UL, nameof({decl.TypeReferenceName}), nameof({property.Name}));", Kind.BitVector => $"ValidateSszBitvectorLength({expression}, {property.Length}, nameof({decl.TypeReferenceName}), nameof({property.Name}));", - Kind.BitList => $"ValidateSszBitlistLimit({expression}, {property.Limit}, nameof({decl.TypeReferenceName}), nameof({property.Name}));", + Kind.BitList => $"ValidateSszBitlistLimit({expression}, {property.Limit}UL, nameof({decl.TypeReferenceName}), nameof({property.Name}));", _ => string.Empty, }; @@ -783,7 +782,8 @@ private static string DecodeAndAssign(SszType decl, SszProperty property, string ? string.Empty : $"int __count = {sliceExpression}.Length / {itemSize};"; string countExpression = property.Kind == Kind.Vector ? property.Length!.Value.ToString() : "__count"; - string limitGuard = (property.Kind == Kind.List && property.Limit.HasValue) + // No guard for limits beyond int.MaxValue: an int-typed count can never exceed them + string limitGuard = (property.Kind == Kind.List && property.Limit is <= int.MaxValue) ? $"if (__count > {property.Limit.Value}) throw new System.IO.InvalidDataException($\"{decl.TypeReferenceName}.{property.Name}: list count {{__count}} exceeds SSZ limit {property.Limit.Value}\");" : string.Empty; string assignment = DecodeAssignmentExpression(property, variableName, sourceIsArray: true); @@ -803,7 +803,8 @@ private static string DecodeAndAssign(SszType decl, SszProperty property, string ? string.Empty : $"int __count = {sliceExpression}.Length / {itemSize};"; string countExpression = property.Kind == Kind.Vector ? property.Length!.Value.ToString() : "__count"; - string limitGuard = (property.Kind == Kind.List && property.Limit.HasValue) + // No guard for limits beyond int.MaxValue: an int-typed count can never exceed them + string limitGuard = (property.Kind == Kind.List && property.Limit is <= int.MaxValue) ? $"if (__count > {property.Limit.Value}) throw new System.IO.InvalidDataException($\"{decl.TypeReferenceName}.{property.Name}: list count {{__count}} exceeds SSZ limit {property.Limit.Value}\");" : string.Empty; string assignment = DecodeAssignmentExpression(property, variableName, sourceIsArray: true); @@ -832,7 +833,8 @@ private static string DecodeAndAssign(SszType decl, SszProperty property, string string validation2 = ValidationStatement(decl, property, $"container.{property.Name}"); string preAllocationListGuard = string.Empty; - if (property.Kind == Kind.List && property.Limit.HasValue && !property.HandledByStd) + // No guard for limits beyond int.MaxValue: an int-typed count can never exceed them + if (property.Kind == Kind.List && property.Limit is <= int.MaxValue && !property.HandledByStd) { preAllocationListGuard = property.Type.IsVariable ? $"if ({sliceExpression}.Length >= {SszType.PointerLength}) {{ int __firstOffset = DecodeSszOffset({sliceExpression}.Slice(0, {SszType.PointerLength})); int __preCount = __firstOffset / {SszType.PointerLength}; if (__preCount > {property.Limit.Value}) throw new System.IO.InvalidDataException($\"{decl.TypeReferenceName}.{property.Name}: list count {{__preCount}} exceeds SSZ limit {property.Limit.Value}\"); }}" @@ -882,22 +884,22 @@ private static string MerkleizeRootStatement(SszProperty property, string expres Kind.Basic when property.Type.CustomFeedMethod is not null => ConverterMerkleizeStatement(property, expression, rootName), Kind.Basic => $"Merkle.Merkleize(out {rootName}, {expression});", Kind.BitVector => $"Merkle.Merkleize(out {rootName}, {expression}!);", - Kind.BitList => $"Merkle.Merkleize(out {rootName}, {expression} ?? new BitArray(0), {property.Limit});", + Kind.BitList => $"Merkle.Merkleize(out {rootName}, {expression} ?? new BitArray(0), {property.Limit}UL);", Kind.ProgressiveBitList => $"MerkleizeProgressiveBitList({expression}, out {rootName});", Kind.Vector when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeBasicVectorWithConverter<{enumType.TypeReferenceName}>({EnumSpanExpression(property, expression)}, {enumType.StaticLength}, {property.Length}, {enumType.CustomEncodeMethod}, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeBasicListWithConverter<{enumType.TypeReferenceName}>({EnumSpanExpression(property, expression)}, {enumType.StaticLength}, {property.Limit}, {enumType.CustomEncodeMethod}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeBasicListWithConverter<{enumType.TypeReferenceName}>({EnumSpanExpression(property, expression)}, {enumType.StaticLength}, {property.Limit}UL, {enumType.CustomEncodeMethod}, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeProgressiveBasicListWithConverter<{enumType.TypeReferenceName}>({EnumSpanExpression(property, expression)}, {enumType.StaticLength}, {enumType.CustomEncodeMethod}, out {rootName});", Kind.Vector when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeBasicVectorWithConverter<{property.Type.TypeReferenceName}>({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Length}, {property.Type.CustomEncodeMethod}, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeBasicListWithConverter<{property.Type.TypeReferenceName}>({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Limit}, {property.Type.CustomEncodeMethod}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeBasicListWithConverter<{property.Type.TypeReferenceName}>({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Limit}UL, {property.Type.CustomEncodeMethod}, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeProgressiveBasicListWithConverter<{property.Type.TypeReferenceName}>({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Type.CustomEncodeMethod}, out {rootName});", Kind.Vector when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeBasicVector({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Length}, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeBasicList({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Limit}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeBasicList({SpanExpression(property, expression)}, {property.Type.StaticLength}, {property.Limit}UL, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeProgressiveBasicList({SpanExpression(property, expression)}, out {rootName});", Kind.Vector when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeVectorWithConverter({SpanExpression(property, expression)}, {property.Length}, {property.Type.CustomFeedMethod}, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeListWithConverter({SpanExpression(property, expression)}, {property.Limit}, {property.Type.CustomFeedMethod}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeListWithConverter({SpanExpression(property, expression)}, {property.Limit}UL, {property.Type.CustomFeedMethod}, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeProgressiveListWithConverter({SpanExpression(property, expression)}, {property.Type.CustomFeedMethod}, out {rootName});", Kind.Vector => $"{property.Type.StaticMemberAccess}.MerkleizeVector({SpanExpression(property, expression)}, out {rootName});", - Kind.List => $"{property.Type.StaticMemberAccess}.MerkleizeList({SpanExpression(property, expression)}, {property.Limit}, out {rootName});", + Kind.List => $"{property.Type.StaticMemberAccess}.MerkleizeList({SpanExpression(property, expression)}, {property.Limit}UL, out {rootName});", Kind.ProgressiveList => $"{property.Type.StaticMemberAccess}.MerkleizeProgressiveList({SpanExpression(property, expression)}, out {rootName});", _ => $"{property.Type.StaticMemberAccess}.Merkleize({expression}, out {rootName});", }; @@ -912,15 +914,15 @@ private static string MerkleizeEmptyCollectionRootStatement(SszProperty property Kind.Vector when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeDefaultCompositeVectorWithConverter<{property.Type.TypeReferenceName}>({property.Type.StaticLength}, {property.Length}, {property.Type.CustomDecodeMethod}, {property.Type.CustomFeedMethod}, out {rootName});", Kind.Vector when property.Type.Kind == Kind.Basic => $"Merkle.Merkleize(out {rootName}, ReadOnlySpan.Empty, {property.Length});", Kind.Vector => $"{{ {property.Type.TypeReferenceName}[] __empty{property.Name} = new {property.Type.TypeReferenceName}[{property.Length}]; {property.Type.StaticMemberAccess}.MerkleizeVector(__empty{property.Name}, out {rootName}); }}", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeBasicListWithConverter<{enumType.TypeReferenceName}>(System.Runtime.InteropServices.MemoryMarshal.Cast<{property.Type.TypeReferenceName}, {enumType.TypeReferenceName}>(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty), {enumType.StaticLength}, {property.Limit}, {enumType.CustomEncodeMethod}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeBasicListWithConverter<{enumType.TypeReferenceName}>(System.Runtime.InteropServices.MemoryMarshal.Cast<{property.Type.TypeReferenceName}, {enumType.TypeReferenceName}>(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty), {enumType.StaticLength}, {property.Limit}UL, {enumType.CustomEncodeMethod}, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.EnumType is { HasCustomInlineCodec: true, IsSszBasicType: true } enumType => $"MerkleizeProgressiveBasicListWithConverter<{enumType.TypeReferenceName}>(System.Runtime.InteropServices.MemoryMarshal.Cast<{property.Type.TypeReferenceName}, {enumType.TypeReferenceName}>(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty), {enumType.StaticLength}, {enumType.CustomEncodeMethod}, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeBasicListWithConverter<{property.Type.TypeReferenceName}>(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Type.StaticLength}, {property.Limit}, {property.Type.CustomEncodeMethod}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeBasicListWithConverter<{property.Type.TypeReferenceName}>(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Type.StaticLength}, {property.Limit}UL, {property.Type.CustomEncodeMethod}, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec && property.Type.IsSszBasicType => $"MerkleizeProgressiveBasicListWithConverter<{property.Type.TypeReferenceName}>(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Type.StaticLength}, {property.Type.CustomEncodeMethod}, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeBasicList(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Type.StaticLength}, {property.Limit}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeBasicList(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Type.StaticLength}, {property.Limit}UL, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.IsSszBasicType => $"MerkleizeProgressiveBasicList(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, out {rootName});", - Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeListWithConverter(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Limit}, {property.Type.CustomFeedMethod}, out {rootName});", + Kind.List when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeListWithConverter(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Limit}UL, {property.Type.CustomFeedMethod}, out {rootName});", Kind.ProgressiveList when property.Type.Kind == Kind.Basic && property.Type.HasCustomInlineCodec => $"MerkleizeCompositeProgressiveListWithConverter(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Type.CustomFeedMethod}, out {rootName});", - Kind.List => $"{property.Type.StaticMemberAccess}.MerkleizeList(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Limit}, out {rootName});", + Kind.List => $"{property.Type.StaticMemberAccess}.MerkleizeList(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, {property.Limit}UL, out {rootName});", Kind.ProgressiveList => $"{property.Type.StaticMemberAccess}.MerkleizeProgressiveList(ReadOnlySpan<{property.Type.TypeReferenceName}>.Empty, out {rootName});", _ => throw new InvalidOperationException($"Cannot merkleize an empty {property.Kind} collection."), }; @@ -1032,7 +1034,9 @@ private static string EncodeStatement(string target, SszProperty property, strin string arguments = $"{target}, {EncodeValueExpression(property, expression)}"; if (property.Kind == Kind.BitList) { - arguments += $", {property.Limit}"; + // The Encode limit parameter is int-typed; bitlist limits beyond int.MaxValue are + // rejected at parse time (a BitArray cannot exceed int.MaxValue bits), so this fits. + arguments += $", {property.Limit!.Value}"; } else if (property.Kind == Kind.ProgressiveBitList) { diff --git a/src/Nethermind/Nethermind.Serialization.SszGenerator/SszProperty.cs b/src/Nethermind/Nethermind.Serialization.SszGenerator/SszProperty.cs index 4a2aad5a16c0..f314740252f1 100644 --- a/src/Nethermind/Nethermind.Serialization.SszGenerator/SszProperty.cs +++ b/src/Nethermind/Nethermind.Serialization.SszGenerator/SszProperty.cs @@ -60,7 +60,14 @@ public static SszProperty From(SemanticModel semanticModel, List types, AttributeData? listAttr = GetAttribute(attributes, nameof(SszListAttribute)); if (listAttr is not null) { - result.Limit = listAttr.ConstructorArguments.FirstOrDefault().Value as int? ?? 0; + ulong limit = listAttr.ConstructorArguments.FirstOrDefault().Value as ulong? ?? 0UL; + if (prop.Type.Name == nameof(BitArray) && limit > int.MaxValue) + { + throw new InvalidOperationException( + $"Bitlist property {prop.ContainingType.Name}.{prop.Name} declares limit {limit}, but a BitArray cannot exceed int.MaxValue bits."); + } + + result.Limit = limit; } result.IsProgressiveList = HasAttribute(attributes, nameof(SszProgressiveListAttribute)); @@ -247,7 +254,7 @@ public int StaticLength } public int? Length { get; set; } - public int? Limit { get; set; } + public ulong? Limit { get; set; } public bool IsCompatibleWith(SszProperty other, HashSet<(SszType, SszType)> visited) { From 0640843d48eea6d2fc1183ce8521f66477d88afe Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 18 Jun 2026 13:16:24 +0300 Subject: [PATCH 170/182] Harden discv4 unsolicited responses --- .../Discv4/Kademlia/KademliaAdapterTests.cs | 23 +++++++++++++++++++ .../Discv4/Kademlia/KademliaAdapter.cs | 19 +++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs index b908a0b8a109..8dccd7e432cf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/KademliaAdapterTests.cs @@ -167,6 +167,15 @@ private async Task HasResponse(NoResponseRequest request, CancellationToke _ => throw new ArgumentOutOfRangeException(nameof(request), request, null) }; + private DiscoveryMsg CreateUnsolicitedResponse(MsgType msgType) => + msgType switch + { + MsgType.Pong => AddReceiverFarAddress(new PongMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 1, TestItem.KeccakA.ValueHash256)), + MsgType.Neighbors => AddReceiverFarAddress(new NeighborsMsg(_receiver.Address, _timestamper.UnixTime.SecondsLong + 1, Array.Empty())), + MsgType.EnrResponse => AddReceiverFarAddress(new EnrResponseMsg(_receiver.Address, _selfNodeRecord, TestItem.KeccakA)), + _ => throw new ArgumentOutOfRangeException(nameof(msgType), msgType, null) + }; + [Test] [CancelAfter(10000)] public async Task Ping_should_send_ping_and_receive_pong(CancellationToken token) @@ -280,6 +289,20 @@ public async Task Timed_out_response_handler_should_not_consume_later_unsolicite _nodeHealthTracker.DidNotReceive().OnIncomingMessageFrom(Arg.Is(n => n.Id.Equals(_receiver.Id))); } + [TestCase(MsgType.Pong)] + [TestCase(MsgType.Neighbors)] + [TestCase(MsgType.EnrResponse)] + [CancelAfter(10000)] + public async Task OnIncomingMsg_unsolicited_response_should_not_create_session_stats(MsgType msgType) + { + DiscoveryMsg response = CreateUnsolicitedResponse(msgType); + + await _adapter.OnIncomingMsg(response); + + _nodeStatsManager.DidNotReceive().GetOrAdd(Arg.Any()); + _nodeHealthTracker.DidNotReceive().OnIncomingMessageFrom(Arg.Any()); + } + [TestCase(NoResponseRequest.Ping)] [TestCase(NoResponseRequest.FindNeighbours)] [TestCase(NoResponseRequest.SendEnrRequest)] diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs index bd2c19c67d9f..b8b54f397ea2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs @@ -300,15 +300,20 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) if (_logger.IsTrace) _logger.Trace($"Received msg: {msg}"); MsgType msgType = msg.MsgType; Node node = new(msg.FarPublicKey, msg.FarAddress); - NodeSession session = GetSession(node); - session.RecordStatsForIncomingMsg(msg); - if (HandleViaMessageHandlers(node, msg)) + if (IsResponse(msgType)) { + if (!HandleViaMessageHandlers(node, msg)) return; + + NodeSession responseSession = GetSession(node); + responseSession.RecordStatsForIncomingMsg(msg); nodeHealthTracker.Value.OnIncomingMessageFrom(node); return; } + NodeSession session = GetSession(node); + session.RecordStatsForIncomingMsg(msg); + CancellationToken token = processExitSource.Token; switch (msgType) { @@ -330,12 +335,6 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) nodeHealthTracker.Value.OnIncomingMessageFrom(node); } break; - - // Unsolicited response. - case MsgType.Neighbors: - case MsgType.Pong: - case MsgType.EnrResponse: - break; default: if (_logger.IsError) _logger.Error($"Unsupported msgType: {msgType}"); return; @@ -351,6 +350,8 @@ public async Task OnIncomingMsg(DiscoveryMsg msg) } } + private static bool IsResponse(MsgType msgType) => msgType is MsgType.Neighbors or MsgType.Pong or MsgType.EnrResponse; + private bool ValidatePingAddress(PingMsg msg) { if (msg.DestinationAddress is null || msg.FarAddress is null) From c7cbf75dc02ffe39da881e75afbbc28f4ec3884c Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 18 Jun 2026 14:53:01 +0300 Subject: [PATCH 171/182] More magic words --- .../Nethermind.Kademlia/DoubleEndedLru.cs | 2 +- .../LookupKNearestNeighbour.cs | 33 +++++++++---------- .../Discv5/CodecTests.cs | 6 ++-- .../Discv5/Kademlia/KademliaAdapter.cs | 14 ++++---- .../Discv5/Messages/Discv5Message.cs | 2 +- .../Discv5/Messages/FindNodeMsg.cs | 4 +-- .../Discv5/Messages/NodesMsg.cs | 4 +-- .../Discv5/Messages/PingMsg.cs | 4 +-- .../Discv5/Messages/PongMsg.cs | 4 +-- .../Discv5/Messages/TalkReqMsg.cs | 4 +-- .../Discv5/Messages/TalkRespMsg.cs | 4 +-- .../Discv5/Packets/PacketCodec.cs | 14 ++++---- .../Serializers/FindNodeMsgSerializer.cs | 2 +- .../Discv5/Serializers/MsgSerializerBase.cs | 9 ++--- .../Discv5/Serializers/NodesMsgSerializer.cs | 2 +- .../Discv5/Serializers/PingMsgSerializer.cs | 2 +- .../Discv5/Serializers/PongMsgSerializer.cs | 2 +- .../Serializers/TalkReqMsgSerializer.cs | 2 +- .../Serializers/TalkRespMsgSerializer.cs | 2 +- .../Nethermind.Network/IPAddressClassifier.cs | 7 ++-- .../Nethermind.Network/NodeFilter.cs | 6 ++-- 21 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs index 98cafc032ac8..58618ce0a847 100644 --- a/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs +++ b/src/Nethermind/Nethermind.Kademlia/DoubleEndedLru.cs @@ -44,7 +44,7 @@ public int Count } } - public BucketAddResult AddOrRefresh(in TKey key, TValue value) => AddOrRefresh(key, value, out _); + public BucketAddResult AddOrRefresh(in TKey key, TValue value) => AddOrRefresh(in key, value, out _); /// The value previously stored under when the entry is refreshed; default otherwise. public BucketAddResult AddOrRefresh(in TKey key, TValue value, out TValue? previous) diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 18760520ccc3..723fc1f071ff 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Diagnostics.CodeAnalysis; using System.Collections.Concurrent; using Nethermind.Logging; @@ -80,7 +79,7 @@ CancellationToken token while (!Volatile.Read(ref finished)) { token.ThrowIfCancellationRequested(); - if (!TryGetNodeToQuery(out (TKadKey hash, TNode node)? toQuery)) + if (!TryGetNodeToQuery(out TKadKey toQueryHash, out TNode toQueryNode)) { if (queryingTask > 0) { @@ -102,11 +101,11 @@ CancellationToken token break; } - queried.TryAdd(toQuery.Value.hash, toQuery.Value.node); - (TNode, TNode[]? neighbours)? result = await WrappedFindNeighbourOp(toQuery.Value.node); - if (result is null) continue; + queried.TryAdd(toQueryHash, toQueryNode); + TNode[]? neighbours = await WrappedFindNeighbourOp(toQueryNode); + if (neighbours is null) continue; - ProcessResult(toQuery.Value.hash, toQuery.Value.node, result, round); + ProcessResult(toQueryHash, toQueryNode, neighbours, round); } finally { @@ -139,7 +138,7 @@ CancellationToken token return CompileResult(); - async Task<(TNode target, TNode[]? retVal)> WrappedFindNeighbourOp(TNode node) + async Task WrappedFindNeighbourOp(TNode node) { using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); cts.CancelAfter(_findNeighbourHardTimeout); @@ -148,16 +147,16 @@ CancellationToken token { // targetHash is implied in findNeighbourOp TNode[]? ret = await findNeighbourOp(node, cts.Token); - if (ret is null) return (node, null); + if (ret is null) return null; nodeHealthTracker.OnIncomingMessageFrom(node); - return (node, ret); + return ret; } catch (OperationCanceledException) when (!token.IsCancellationRequested) { nodeHealthTracker.OnRequestFailed(node); - return (node, null); + return null; } catch (OperationCanceledException) { @@ -167,29 +166,30 @@ CancellationToken token { nodeHealthTracker.OnRequestFailed(node); if (_logger.IsWarn) _logger.Warn($"Find neighbour op failed: {e}"); - return (node, null); + return null; } } - bool TryGetNodeToQuery([NotNullWhen(true)] out (TKadKey, TNode)? toQuery) + bool TryGetNodeToQuery(out TKadKey hash, out TNode node) { lock (queueLock) { if (bestSeen.Count == 0) { - toQuery = default; + hash = default!; + node = default!; // No more node to query. // Note: its possible that there are other worker currently which may add to bestSeen. return false; } Interlocked.Increment(ref queryingTask); - toQuery = bestSeen.Dequeue(); + (hash, node) = bestSeen.Dequeue(); return true; } } - void ProcessResult(TKadKey hash, TNode toQuery, (TNode, TNode[]? neighbours)? valueTuple, int round) + void ProcessResult(TKadKey hash, TNode toQuery, TNode[] neighbours, int round) { lock (queueLock) { @@ -199,9 +199,6 @@ void ProcessResult(TKadKey hash, TNode toQuery, (TNode, TNode[]? neighbours)? va finalResult.Dequeue(); } - TNode[]? neighbours = valueTuple?.neighbours; - if (neighbours is null) return; - foreach (TNode neighbour in neighbours) { TKadKey neighbourHash = nodeHashProvider.GetHash(neighbour); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 4da36282e548..8e6200e1868a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -72,7 +72,7 @@ public void PacketCodec_Decodes_PingPacket_Devp2p_Vector() bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); using (packet) { - bool decrypted = PacketCodec.TryDecryptMessageForTest(packet, new byte[16], out Discv5Message message); + bool decrypted = PacketCodec.TryDecryptMessageForTest(in packet, new byte[16], out Discv5Message message); Assert.That(decoded, Is.True); Assert.That(packet.Flag, Is.EqualTo(PacketFlag.Ordinary)); @@ -128,7 +128,7 @@ public void PacketCodec_Decodes_WhoAreYou_GoEthereum_Vector() using (packet) { using PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); - Challenge challenge = codec.DecodeWhoAreYou(packet); + Challenge challenge = codec.DecodeWhoAreYou(in packet); Assert.That(decoded, Is.True); Assert.That(packet.Flag, Is.EqualTo(PacketFlag.WhoAreYou)); @@ -186,7 +186,7 @@ public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( bool decoded = PacketCodec.TryDecode(packetBytes, NodeBId, out Packet packet); using (packet) { - bool decrypted = codec.TryDecryptHandshake(packet, challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord); + bool decrypted = codec.TryDecryptHandshake(in packet, challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord); Assert.That(decoded, Is.True); Assert.That(packet.Flag, Is.EqualTo(PacketFlag.Handshake)); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 32b476ac1726..69de85030373 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -377,7 +377,7 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat return; } - Challenge challenge = packetCodec.DecodeWhoAreYou(packet); + Challenge challenge = packetCodec.DecodeWhoAreYou(in packet); byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Session session); SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash, endpoint), session); if (_logger.IsTrace) _logger.Trace($"Sending discv5 HANDSHAKE for {pendingRequest.Message.MessageType} {pendingRequest.Message.RequestId} to {endpoint}, bytes: {handshakePacket.Length}, requested ENR seq: {challenge.EnrSequence}."); @@ -386,14 +386,14 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) + if (!PacketCodec.TryGetSourceNodeId(in packet, out Hash256? nodeId)) { if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 ordinary packet from {endpoint}; source node id missing."); return; } SessionKey sessionKey = new(nodeId, endpoint); - if (!TryDecryptOrdinaryMessage(packet, sessionKey, out Session? session, out Discv5Message? message)) + if (!TryDecryptOrdinaryMessage(in packet, sessionKey, out Session? session, out Discv5Message? message)) { if (_logger.IsTrace) _logger.Trace($"Discv5 ordinary packet from {endpoint} could not be decrypted with an existing session; sending WHOAREYOU."); await SendWhoAreYou(endpoint, packet, nodeId); @@ -412,12 +412,12 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati } [SkipLocalsInit] - private bool TryDecryptOrdinaryMessage(Packet packet, SessionKey sessionKey, [NotNullWhen(true)] out Session? session, [NotNullWhen(true)] out Discv5Message? message) + private bool TryDecryptOrdinaryMessage(scoped in Packet packet, SessionKey sessionKey, [NotNullWhen(true)] out Session? session, [NotNullWhen(true)] out Discv5Message? message) { Span readKey = stackalloc byte[Session.KeySize]; if (TryGetSession(sessionKey, out session) && session.TryCopyReadKey(readKey) && - packetCodec.TryDecryptMessage(packet, readKey, out Discv5Message decodedMessage)) + packetCodec.TryDecryptMessage(in packet, readKey, out Discv5Message decodedMessage)) { message = decodedMessage; return true; @@ -429,7 +429,7 @@ private bool TryDecryptOrdinaryMessage(Packet packet, SessionKey sessionKey, [No private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!PacketCodec.TryGetSourceNodeId(packet, out Hash256? nodeId)) + if (!PacketCodec.TryGetSourceNodeId(in packet, out Hash256? nodeId)) { if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; source node id missing."); return; @@ -452,7 +452,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat try { TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); - if (!packetCodec.TryDecryptHandshake(packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) + if (!packetCodec.TryDecryptHandshake(in packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) { if (_logger.IsTrace) _logger.Trace($"Unable to decrypt discv5 handshake packet from {endpoint}."); return; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs index 43a16477477e..0a33754b5230 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Discv5Message.cs @@ -14,7 +14,7 @@ internal abstract record Discv5Message : IDisposable private ArrayPoolSpan _owner; private bool _hasOwner; - protected Discv5Message(RequestId requestId, ArrayPoolSpan? owner = null) + protected Discv5Message(in RequestId requestId, ArrayPoolSpan? owner = null) { RequestId = requestId; if (owner is { } ownerValue) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs index 41c86db54ca6..602ba6644568 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/FindNodeMsg.cs @@ -12,8 +12,8 @@ public FindNodeMsg(ReadOnlySpan requestId, ReadOnlySpan distances) { } - public FindNodeMsg(RequestId requestId, Distances distances, ArrayPoolSpan? owner = null) - : base(requestId, owner) + public FindNodeMsg(in RequestId requestId, Distances distances, ArrayPoolSpan? owner = null) + : base(in requestId, owner) => Distances = distances; public override MessageType MessageType => MessageType.FindNode; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs index 85f42a634654..197691adfdb5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/NodesMsg.cs @@ -13,8 +13,8 @@ public NodesMsg(ReadOnlySpan requestId, int total, IReadOnlyList records, ArrayPoolSpan? owner = null) - : base(requestId, owner) + public NodesMsg(in RequestId requestId, int total, IReadOnlyList records, ArrayPoolSpan? owner = null) + : base(in requestId, owner) { Total = total; Records = records; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs index fc2fbc4a0c56..fb2bdb794158 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PingMsg.cs @@ -12,8 +12,8 @@ public PingMsg(ReadOnlySpan requestId, ulong enrSequence) { } - public PingMsg(RequestId requestId, ulong enrSequence, ArrayPoolSpan? owner = null) - : base(requestId, owner) + public PingMsg(in RequestId requestId, ulong enrSequence, ArrayPoolSpan? owner = null) + : base(in requestId, owner) => EnrSequence = enrSequence; public override MessageType MessageType => MessageType.Ping; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs index 967c27ce5e79..9d1a57da02e1 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/PongMsg.cs @@ -13,8 +13,8 @@ public PongMsg(ReadOnlySpan requestId, ulong enrSequence, IPAddress recipi { } - public PongMsg(RequestId requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort, ArrayPoolSpan? owner = null) - : base(requestId, owner) + public PongMsg(in RequestId requestId, ulong enrSequence, IPAddress recipientIp, int recipientPort, ArrayPoolSpan? owner = null) + : base(in requestId, owner) { EnrSequence = enrSequence; RecipientIp = recipientIp; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs index 7755225ebf0a..97ff45528430 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkReqMsg.cs @@ -12,8 +12,8 @@ public TalkReqMsg(ReadOnlySpan requestId, ReadOnlyMemory protocol, R { } - public TalkReqMsg(RequestId requestId, ReadOnlyMemory protocol, ReadOnlyMemory request, ArrayPoolSpan? owner = null) - : base(requestId, owner) + public TalkReqMsg(in RequestId requestId, ReadOnlyMemory protocol, ReadOnlyMemory request, ArrayPoolSpan? owner = null) + : base(in requestId, owner) { _protocol = protocol; _request = request; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs index 0e47e591e64c..ca594da9b87e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/TalkRespMsg.cs @@ -12,8 +12,8 @@ public TalkRespMsg(ReadOnlySpan requestId, ReadOnlyMemory response) { } - public TalkRespMsg(RequestId requestId, ReadOnlyMemory response, ArrayPoolSpan? owner = null) - : base(requestId, owner) + public TalkRespMsg(in RequestId requestId, ReadOnlyMemory response, ArrayPoolSpan? owner = null) + : base(in requestId, owner) => _response = response; public override MessageType MessageType => MessageType.TalkResp; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 416bd24c86ed..4e724530cb00 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -213,10 +213,10 @@ private static bool TryDecode(ReadOnlyMemory packetMemory, Aes localNodeMa } } - internal bool TryDecryptMessage(Packet packet, ReadOnlySpan encryptionKey, out Discv5Message message) - => TryDecryptMessageForTest(packet, encryptionKey, out message); + internal bool TryDecryptMessage(scoped in Packet packet, ReadOnlySpan encryptionKey, out Discv5Message message) + => TryDecryptMessageForTest(in packet, encryptionKey, out message); - internal static bool TryDecryptMessageForTest(Packet packet, ReadOnlySpan encryptionKey, out Discv5Message message) + internal static bool TryDecryptMessageForTest(scoped in Packet packet, ReadOnlySpan encryptionKey, out Discv5Message message) { message = null!; ReadOnlySpan encryptedMessage = packet.Message.Span; @@ -261,7 +261,7 @@ internal static bool TryDecryptMessageForTest(Packet packet, ReadOnlySpan } } - internal Challenge DecodeWhoAreYou(Packet packet) + internal Challenge DecodeWhoAreYou(scoped in Packet packet) { if (packet.AuthData.Length != WhoAreYouAuthDataSize) { @@ -274,7 +274,7 @@ internal Challenge DecodeWhoAreYou(Packet packet) } internal bool TryDecryptHandshake( - Packet packet, + scoped in Packet packet, Challenge challenge, NodeRecord? knownRecord, out Session session, @@ -322,7 +322,7 @@ internal bool TryDecryptHandshake( DeriveKeys(ephemeralPublicKey, sourceNodeId.Bytes, _localNodeId.Bytes, challenge.ChallengeData, out byte[] initiatorKey, out byte[] recipientKey); - if (!TryDecryptMessage(packet, initiatorKey, out message)) + if (!TryDecryptMessage(in packet, initiatorKey, out message)) { return false; } @@ -331,7 +331,7 @@ internal bool TryDecryptHandshake( return true; } - internal static bool TryGetSourceNodeId(Packet packet, [NotNullWhen(true)] out Hash256? sourceNodeId) + internal static bool TryGetSourceNodeId(scoped in Packet packet, [NotNullWhen(true)] out Hash256? sourceNodeId) { sourceNodeId = null; switch (packet.Flag) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs index 6e7ea14caa30..a37421042888 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs @@ -15,7 +15,7 @@ protected override int GetContentLengthCore(FindNodeMsg msg) protected override void SerializeCore(NettyRlpStream stream, FindNodeMsg msg) => EncodeDistances(stream, msg.Distances); - protected override FindNodeMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override FindNodeMsg DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, DecodeDistances(ref ctx), owner); private static int GetDistancesLength(Distances distances) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs index 8139e2ba8a8c..97970282512a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs @@ -16,21 +16,22 @@ public int GetContentLength(TMessage msg) public void Serialize(NettyRlpStream stream, TMessage msg) { - EncodeRequestId(stream, msg.RequestId); + RequestId requestId = msg.RequestId; + EncodeRequestId(stream, in requestId); SerializeCore(stream, msg); } public TMessage Deserialize(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { RequestId requestId = DecodeRequestId(ref ctx); - return DeserializeCore(requestId, ref ctx, ownedMessage, owner); + return DeserializeCore(in requestId, ref ctx, ownedMessage, owner); } protected abstract int GetContentLengthCore(TMessage msg); protected abstract void SerializeCore(NettyRlpStream stream, TMessage msg); - protected abstract TMessage DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner); + protected abstract TMessage DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner); protected static ReadOnlyMemory DecodeByteMemory(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage) { @@ -59,7 +60,7 @@ private static RequestId DecodeRequestId(ref Rlp.ValueDecoderContext ctx) } [SkipLocalsInit] - private static void EncodeRequestId(NettyRlpStream stream, RequestId requestId) + private static void EncodeRequestId(NettyRlpStream stream, in RequestId requestId) { Span bytes = stackalloc byte[RequestId.MaxLength]; requestId.CopyTo(bytes); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index 8bc8ba87362a..87debcc2d9a7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -25,7 +25,7 @@ protected override void SerializeCore(NettyRlpStream stream, NodesMsg msg) EncodeNodeRecords(stream, msg.Records); } - protected override NodesMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override NodesMsg DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { int total = ctx.DecodePositiveInt(); return new NodesMsg(requestId, total, DecodeNodeRecords(ref ctx), owner); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs index 4b4c30a05f17..4c0b475f7e67 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs @@ -15,6 +15,6 @@ protected override int GetContentLengthCore(PingMsg msg) protected override void SerializeCore(NettyRlpStream stream, PingMsg msg) => Encode(stream, msg.EnrSequence); - protected override PingMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override PingMsg DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, ctx.DecodeULong(), owner); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs index e9be8241b580..4f3f3cf2bd35 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -25,7 +25,7 @@ protected override void SerializeCore(NettyRlpStream stream, PongMsg msg) Encode(stream, msg.RecipientPort); } - protected override PongMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override PongMsg DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) { ulong enrSequence = ctx.DecodeULong(); IPAddress recipientIp = new(ctx.DecodeByteArraySpan(IpAddressRlpLimit)); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs index abb43f9da685..c230d9adbda6 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs @@ -18,6 +18,6 @@ protected override void SerializeCore(NettyRlpStream stream, TalkReqMsg msg) stream.Encode(msg.Request); } - protected override TalkReqMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override TalkReqMsg DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, DecodeByteMemory(ref ctx, ownedMessage), DecodeByteMemory(ref ctx, ownedMessage), owner); } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs index 4519c43d6321..7e6b433551d7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs @@ -15,6 +15,6 @@ protected override int GetContentLengthCore(TalkRespMsg msg) protected override void SerializeCore(NettyRlpStream stream, TalkRespMsg msg) => stream.Encode(msg.Response); - protected override TalkRespMsg DeserializeCore(RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + protected override TalkRespMsg DeserializeCore(in RequestId requestId, ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) => new(requestId, DecodeByteMemory(ref ctx, ownedMessage), owner); } diff --git a/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs b/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs index 58149ac1e177..4a710a5e7df7 100644 --- a/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs +++ b/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs @@ -27,7 +27,10 @@ internal readonly struct ParsedIPAddress(ParsedIPAddressFamily family, uint v4, /// Returns true for loopback, private, link-local, CGNAT, and IPv6 ULA addresses. /// public static bool IsLoopbackOrPrivateOrLinkLocal(IPAddress ipAddress) - => IsLoopbackOrPrivateOrLinkLocal(Parse(ipAddress)); + { + ParsedIPAddress parsed = Parse(ipAddress); + return IsLoopbackOrPrivateOrLinkLocal(in parsed); + } /// /// Returns true for IPv4 or IPv6 multicast addresses. @@ -110,7 +113,7 @@ internal static ParsedIPAddress Parse(IPAddress ipAddress) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsLoopbackOrPrivateOrLinkLocal(ParsedIPAddress parsed) + internal static bool IsLoopbackOrPrivateOrLinkLocal(in ParsedIPAddress parsed) => parsed.Family == ParsedIPAddressFamily.IPv4 ? IsIPv4LoopbackOrPrivateOrLinkLocal(parsed.V4) : IsIPv6LoopbackOrPrivateOrLinkLocal(parsed.Hi, parsed.Lo); diff --git a/src/Nethermind/Nethermind.Network/NodeFilter.cs b/src/Nethermind/Nethermind.Network/NodeFilter.cs index f5067b1d55b3..c9c1da7a9e61 100644 --- a/src/Nethermind/Nethermind.Network/NodeFilter.cs +++ b/src/Nethermind/Nethermind.Network/NodeFilter.cs @@ -106,7 +106,7 @@ private IpSubnetKey GetKey(IPAddress ipAddress, bool exactOnly) => _exactMatchOnly || exactOnly ? IpSubnetKey.Exact(ipAddress) : (_parsedCurrentIp is { } current - ? IpSubnetKey.CreateNodeFilterKey(ipAddress, current) + ? IpSubnetKey.CreateNodeFilterKey(ipAddress, in current) : IpSubnetKey.DefaultKey(ipAddress)); /// @@ -245,7 +245,7 @@ public static IpSubnetKey CreateNodeFilterKey( bool requireCurrentIpIsLocalForExact = true) { ParsedIp current = new(currentIp); - return CreateNodeFilterKey(remoteIp, current, + return CreateNodeFilterKey(remoteIp, in current, v4BucketPrefixBits, v6BucketPrefixBits, v4LocalPrefixBits, v6LocalPrefixBits, exactIfSameSubnetAsCurrentIp, requireCurrentIpIsLocalForExact); @@ -253,7 +253,7 @@ public static IpSubnetKey CreateNodeFilterKey( public static IpSubnetKey CreateNodeFilterKey( IPAddress remoteIp, - ParsedIp currentIp, + in ParsedIp currentIp, byte v4BucketPrefixBits = 24, byte v6BucketPrefixBits = 64, byte v4LocalPrefixBits = 24, From f4b4eba2681e2f18c5678cb59818a232d02fe685 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Thu, 18 Jun 2026 18:19:13 +0300 Subject: [PATCH 172/182] Some fixes --- .../Serializers/NeighborsMsgSerializer.cs | 25 +++++--- .../Discv5/DiscoveryV5App.cs | 11 +--- .../Discv5/MessageCodec.cs | 11 +--- .../NodeRecordSignerTests.cs | 63 ++++++++----------- .../NodeRecordTests.cs | 48 +++++++++----- .../IP/IPAddressExtensions.cs | 18 ------ .../Nethermind.Network/IP/WebIPSource.cs | 3 +- .../Nethermind.Network/NetworkNodeDecoder.cs | 9 +-- .../Module/MainProcessingContextTests.cs | 12 ++-- 9 files changed, 86 insertions(+), 114 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs index b669c1e26ee0..0067a452ef37 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Serializers/NeighborsMsgSerializer.cs @@ -20,7 +20,8 @@ public sealed class NeighborsMsgSerializer( : DiscoveryMsgSerializerBase(ecdsa, nodeKey, nodeIdResolver), IZeroInnerMessageSerializer { private static readonly RlpLimit NodesRlpLimit = RlpLimit.For(16, nameof(NeighborsMsg.Nodes)); - private static readonly DecodeRlpValue _decodeItem = static (ref ctx) => + + private static Node DecodeNode(ref Rlp.ValueDecoderContext ctx) { int lastPosition = ctx.ReadSequenceLength() + ctx.Position; int count = ctx.PeekNumberOfItemsRemaining(lastPosition); @@ -33,7 +34,7 @@ public sealed class NeighborsMsgSerializer( ReadOnlySpan id = ctx.DecodeByteArraySpan(NodeIdRlpLimit); return new Node(new PublicKey(id), address); - }; + } public void Serialize(IByteBuffer byteBuffer, NeighborsMsg msg) { @@ -69,19 +70,23 @@ public NeighborsMsg Deserialize(IByteBuffer msgBytes) Rlp.ValueDecoderContext ctx = Data.AsRlpContext(); ctx.ReadSequenceLength(); - Node?[] decoded = ctx.DecodeArray(_decodeItem, limit: NodesRlpLimit); - - // DecodeArray substitutes null for empty-list items, so compact them away to uphold - // the invariant that consumers never observe null nodes. + int nodesEnd = ctx.ReadSequenceLength() + ctx.Position; + int count = ctx.PeekNumberOfItemsRemaining(nodesEnd); + ctx.GuardLimit(count, NodesRlpLimit); + Node[] decoded = new Node[count]; int nodeCount = 0; - for (int i = 0; i < decoded.Length; i++) + for (int i = 0; i < count; i++) { - if (decoded[i] is not null) + if (ctx.IsNextItemEmptyList()) { - decoded[nodeCount++] = decoded[i]; + ctx.SkipItem(); + continue; } + + decoded[nodeCount++] = DecodeNode(ref ctx); } + ctx.Check(nodesEnd); if (nodeCount != decoded.Length) { Array.Resize(ref decoded, nodeCount); @@ -89,7 +94,7 @@ public NeighborsMsg Deserialize(IByteBuffer msgBytes) long expirationTime = ctx.DecodeLong(); Data.SetReaderIndex(Data.ReaderIndex + ctx.Position); - NeighborsMsg msg = new(FarPublicKey, expirationTime, decoded!); + NeighborsMsg msg = new(FarPublicKey, expirationTime, decoded); return msg; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 71d908467bf0..5aae6073d7a4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -159,14 +159,9 @@ private BootNodeAddResult AddBootNode(List bootNodes, ISet seen, } private BootNodeAddResult AddBootNode(List bootNodes, ISet seen, NodeRecord nodeRecord) - { - if (TryGetAcceptableNodeFromEnr(nodeRecord, out Node? node)) - { - return AddBootNode(bootNodes, seen, node); - } - - return BootNodeAddResult.Skipped; - } + => TryGetAcceptableNodeFromEnr(nodeRecord, out Node? node) + ? AddBootNode(bootNodes, seen, node) + : BootNodeAddResult.Skipped; private BootNodeAddResult AddBootNode(List bootNodes, ISet seen, Node node) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs index 766f176cd337..316a167c37dd 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs @@ -37,14 +37,9 @@ public static NettyRlpStream Encode(Discv5Message message) } public static Discv5Message Decode(ReadOnlySpan message) - { - if (NeedsOwnedMessage(message)) - { - throw new RlpException("discv5 TALK messages require owned message memory. Use DecodeOwned."); - } - - return Decode(message, default, null); - } + => NeedsOwnedMessage(message) + ? throw new RlpException("discv5 TALK messages require owned message memory. Use DecodeOwned.") + : Decode(message, default, null); public static Discv5Message DecodeOwned(ReadOnlyMemory message, ArrayPoolSpan owner) => Decode(message.Span, message, owner); diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index 1d623469c637..5f2db9d6817d 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.Net; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; @@ -123,46 +124,11 @@ public void Throws_when_encoded_record_is_bigger_than_300_bytes() Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); } - [Test] - public void Throws_when_keys_are_not_sorted() - { - NodeRecordSigner signer = new(new Ecdsa()); - RlpStream rlpStream = CreateRecord( - (EnrContentKey.Udp, static stream => stream.Encode(30303), Rlp.LengthOf(30303)), - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))); - - Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); - } - - [Test] - public void Throws_when_keys_are_duplicated() - { - NodeRecordSigner signer = new(new Ecdsa()); - RlpStream rlpStream = CreateRecord( - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))); - - Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); - } - - [Test] - public void Throws_when_id_is_missing() + [TestCaseSource(nameof(InvalidRecordCases))] + public void Throws_when_record_is_invalid(Func createRecord) { NodeRecordSigner signer = new(new Ecdsa()); - RlpStream rlpStream = CreateRecord( - ("z", static stream => stream.Encode(Array.Empty()), Rlp.LengthOf(Array.Empty()))); - - Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); - } - - [Test] - public void Throws_when_id_is_not_v4() - { - NodeRecordSigner signer = new(new Ecdsa()); - RlpStream rlpStream = CreateRecord( - (EnrContentKey.Id, static stream => stream.Encode("v5"), Rlp.LengthOf("v5"))); - - Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); + Assert.That(() => signer.Deserialize(createRecord()), Throws.TypeOf()); } [TestCase("f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f")] @@ -235,6 +201,27 @@ private static RlpStream CreateRecord(params (string Key, Action Enco return rlpStream; } + private static IEnumerable InvalidRecordCases() + { + yield return new TestCaseData((Func)(static () => CreateRecord( + (EnrContentKey.Udp, static stream => stream.Encode(30303), Rlp.LengthOf(30303)), + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))))) + .SetName("Throws_when_keys_are_not_sorted"); + + yield return new TestCaseData((Func)(static () => CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))))) + .SetName("Throws_when_keys_are_duplicated"); + + yield return new TestCaseData((Func)(static () => CreateRecord( + ("z", static stream => stream.Encode(Array.Empty()), Rlp.LengthOf(Array.Empty()))))) + .SetName("Throws_when_id_is_missing"); + + yield return new TestCaseData((Func)(static () => CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v5"), Rlp.LengthOf("v5"))))) + .SetName("Throws_when_id_is_not_v4"); + } + private static byte[] FindFillerForOversizedEncodedRecord() { for (int i = 0; i <= 300; i++) diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs index b570d2756383..0d2b179d22bf 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordTests.cs @@ -43,28 +43,42 @@ public void Cannot_get_enr_string_when_signature_missing() Assert.Throws(() => _ = nodeRecord.EnrString); } - [Test] - public void Discovery_endpoint_rejects_ipv4_with_udp6_only() + [TestCase("192.0.2.1", "", -1, 30304, "", -1)] + [TestCase("", "2001:db8::1", 30303, -1, "2001:db8::1", 30303)] + public void Discovery_endpoint_uses_expected_ip_udp_fallback(string ip, string ip6, int udp, int udp6, string expectedIp, int expectedPort) { - IPAddress ip = IPAddress.Parse("192.0.2.1"); NodeRecord nodeRecord = new(); - nodeRecord.SetEntry(new IpEntry(ip)); - nodeRecord.SetEntry(new Udp6Entry(30304)); - Assert.That(nodeRecord.DiscoveryIp, Is.Null); - Assert.That(nodeRecord.DiscoveryPort, Is.Null); - } + if (!string.IsNullOrEmpty(ip)) + { + nodeRecord.SetEntry(new IpEntry(IPAddress.Parse(ip))); + } - [Test] - public void Discovery_endpoint_uses_udp_as_ipv6_fallback() - { - IPAddress ip = IPAddress.Parse("2001:db8::1"); - NodeRecord nodeRecord = new(); - nodeRecord.SetEntry(new Ip6Entry(ip)); - nodeRecord.SetEntry(new UdpEntry(30303)); + if (!string.IsNullOrEmpty(ip6)) + { + nodeRecord.SetEntry(new Ip6Entry(IPAddress.Parse(ip6))); + } - Assert.That(nodeRecord.DiscoveryIp, Is.EqualTo(ip)); - Assert.That(nodeRecord.DiscoveryPort, Is.EqualTo(30303)); + if (udp >= 0) + { + nodeRecord.SetEntry(new UdpEntry(udp)); + } + + if (udp6 >= 0) + { + nodeRecord.SetEntry(new Udp6Entry(udp6)); + } + + if (expectedPort < 0) + { + Assert.That(nodeRecord.DiscoveryIp, Is.Null); + Assert.That(nodeRecord.DiscoveryPort, Is.Null); + } + else + { + Assert.That(nodeRecord.DiscoveryIp, Is.EqualTo(IPAddress.Parse(expectedIp))); + Assert.That(nodeRecord.DiscoveryPort, Is.EqualTo(expectedPort)); + } } [Test] diff --git a/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs b/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs deleted file mode 100644 index 0705c45a9036..000000000000 --- a/src/Nethermind/Nethermind.Network/IP/IPAddressExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Net; - -namespace Nethermind.Network.IP -{ - public static class IPAddressExtensions - { - /// - /// An extension method to determine if an IP address is internal or otherwise local to the node. - /// - /// The IP address that will be tested - /// Returns true if the IP is internal, false if it is external - public static bool IsInternal(this IPAddress toTest) - => IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(toTest); - } -} diff --git a/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs b/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs index 0ac1a0d4205a..7ba5fdc44d29 100644 --- a/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs +++ b/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs @@ -23,7 +23,8 @@ class WebIPSource(string url, ILogManager logManager) : IIPSource string ip = httpClient.GetStringAsync(_url).Result.Trim(); if (_logger.IsDebug) _logger.Debug($"External ip: {ip}"); bool result = IPAddress.TryParse(ip, out IPAddress ipAddress); - return Task.FromResult(result && !ipAddress.IsInternal() ? (true, ipAddress) : (false, (IPAddress)null)); + bool isExternal = result && !Nethermind.Network.IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(ipAddress); + return Task.FromResult(isExternal ? (true, ipAddress) : (false, (IPAddress)null)); } catch (Exception e) { diff --git a/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs b/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs index a9a5bd48c8f2..ad9bb2fd7db8 100644 --- a/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs +++ b/src/Nethermind/Nethermind.Network/NetworkNodeDecoder.cs @@ -20,12 +20,9 @@ protected override NetworkNode DecodeInternal(ref Rlp.ValueDecoderContext decode { int contentEnd = decoderContext.ReadSequenceLength() + decoderContext.Position; ReadOnlySpan firstItem = decoderContext.DecodeByteArraySpan(RlpLimit); - if (IsEnrString(firstItem)) - { - return DecodeEnrFormat(ref decoderContext, firstItem, contentEnd); - } - - return DecodeLegacyFormat(ref decoderContext, firstItem); + return IsEnrString(firstItem) + ? DecodeEnrFormat(ref decoderContext, firstItem, contentEnd) + : DecodeLegacyFormat(ref decoderContext, firstItem); } private static NetworkNode DecodeEnrFormat(ref Rlp.ValueDecoderContext decoderContext, ReadOnlySpan firstItem, int contentEnd) diff --git a/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs b/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs index 13e3b0400eac..57da898a2bb6 100644 --- a/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/Module/MainProcessingContextTests.cs @@ -11,7 +11,6 @@ using Nethermind.Core.Test.Builders; using Nethermind.Core.Test.Container; using Nethermind.Core.Test.Modules; -using Nethermind.Crypto; using Nethermind.Evm; using Nethermind.Evm.State; using Nethermind.Specs.Forks; @@ -25,14 +24,11 @@ public class MainProcessingContextTests [CancelAfter(10000)] public async Task Test_TransactionProcessed_EventIsFired(CancellationToken cancellationToken) { - using PrivateKey privateKeyA = new("010102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"); - using PrivateKey privateKeyB = new("020102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"); - await using IContainer ctx = new ContainerBuilder() .AddModule(new TestNethermindModule(Cancun.Instance)) .WithGenesisPostProcessor((_, state) => { - state.AddToBalanceAndCreateIfNotExists(privateKeyA.Address, 10.Ether, Osaka.Instance); + state.AddToBalanceAndCreateIfNotExists(TestItem.PrivateKeyA.Address, 10.Ether, Osaka.Instance); }) .Build(); @@ -44,13 +40,13 @@ public async Task Test_TransactionProcessed_EventIsFired(CancellationToken cance await ctx.Resolve().AddBlockAndWaitForHead(false, cancellationToken, Build.A.Transaction .WithGasLimit(100_000) - .WithSenderAddress(privateKeyA.Address) + .WithSenderAddress(TestItem.PrivateKeyA.Address) .WithCode(Prepare.EvmCode .ForInitOf(Prepare.EvmCode - .PushData(privateKeyB.Address) + .PushData(TestItem.PrivateKeyB.Address) .Done) .Done) - .Signed(privateKeyA) + .Signed(TestItem.PrivateKeyA) .TestObject); Assert.That(totalTransactionProcessed, Is.EqualTo(1)); From b6a1c6ce9ba5c96cb29d3ddaadb2060e554a14b4 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 19 Jun 2026 11:25:34 +0300 Subject: [PATCH 173/182] Simplify Kademlia test keys --- .../Discv4/DiscoveryMessageSerializerTests.cs | 4 +- .../Kademlia/IdentityNodeHashProvider.cs | 16 ---- .../Kademlia/Int32KademliaDistance.cs | 31 ++++++ .../Kademlia/IntNodeHashProvider.cs | 13 +++ .../Kademlia/KBucketTests.cs | 71 +++++++------- .../Kademlia/KBucketTreeTests.cs | 50 +++++----- .../Kademlia/LookupKNearestNeighbourTests.cs | 66 +++++++------ .../Kademlia/NodeHealthTrackerTests.cs | 96 +++++++++---------- 8 files changed, 177 insertions(+), 170 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Int32KademliaDistance.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IntNodeHashProvider.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs index 18b09e33c59f..6a1687c86021 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/DiscoveryMessageSerializerTests.cs @@ -283,8 +283,8 @@ public void NeighborsMessageTest() [Test] public void NeighborsMessage_Drops_Empty_List_Node_Entries() { - // A misbehaving peer can encode a node entry as an RLP empty list (0xc0), which - // DecodeArray materializes as a null node; such entries must never reach consumers. + // A misbehaving peer can encode a node entry as an RLP empty list (0xc0); + // such entries must be skipped instead of reaching consumers. byte[] ip = [192, 168, 1, 2]; byte[] id = TestItem.PublicKeyA.Bytes; const int port = 30303; diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs deleted file mode 100644 index d2a14bbf0267..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IdentityNodeHashProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core.Crypto; -using Nethermind.Kademlia; - -namespace Nethermind.Network.Discovery.Test.Kademlia; - -internal sealed class IdentityNodeHashProvider : INodeHashProvider -{ - public static readonly IdentityNodeHashProvider Instance = new(); - - public static Hash256 ToHash(ValueHash256 hash) => hash.ToHash256(); - - public Hash256 GetHash(ValueHash256 node) => ToHash(node); -} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Int32KademliaDistance.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Int32KademliaDistance.cs new file mode 100644 index 000000000000..e6918b4926fc --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/Int32KademliaDistance.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Numerics; +using Nethermind.Kademlia; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +internal sealed class Int32KademliaDistance : IKademliaDistance +{ + public static Int32KademliaDistance Instance { get; } = new(); + + public int MaxDistance => 32; + + public int Zero => 0; + + public int CalculateLogDistance(int left, int right) + { + uint distance = (uint)(left ^ right); + return distance == 0 ? 0 : MaxDistance - BitOperations.LeadingZeroCount(distance); + } + + public int Compare(int left, int right, int target) + => ((uint)(left ^ target)).CompareTo((uint)(right ^ target)); + + public bool GetBit(int key, int index) + => ((uint)key & (1u << (31 - index))) != 0; + + public int SetBit(int key, int index) + => key | (int)(1u << (31 - index)); +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IntNodeHashProvider.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IntNodeHashProvider.cs new file mode 100644 index 000000000000..6158c2a68c2f --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IntNodeHashProvider.cs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Kademlia; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +internal sealed class IntNodeHashProvider : INodeHashProvider +{ + public static readonly IntNodeHashProvider Instance = new(); + + public int GetHash(int node) => node; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index 6c3b96b135d5..ab7ab1d28bd7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Linq; -using Nethermind.Core.Crypto; using Nethermind.Kademlia; using NUnit.Framework; @@ -13,27 +12,27 @@ public class KBucketTests [Test] public void TryAddOrRefresh_ShouldLimitToK() { - (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); + (KBucket bucket, int[] toAdd) = BuildFullBucket(); // Again AddNodes(bucket, toAdd); Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(toAdd[..5].ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (IdentityNodeHashProvider.ToHash(it), it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(toAdd[..5].Select(static it => (it, it)).ToHashSet())); - foreach (ValueHash256 valueHash256 in toAdd[..5]) + foreach (int node in toAdd[..5]) { - Assert.That(bucket.ContainsNode(IdentityNodeHashProvider.ToHash(valueHash256)), Is.True); - Assert.That(bucket.GetByHash(IdentityNodeHashProvider.ToHash(valueHash256)), Is.EqualTo(valueHash256)); + Assert.That(bucket.ContainsNode(node), Is.True); + Assert.That(bucket.GetByHash(node), Is.EqualTo(node)); } } [Test] public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() { - (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); + (KBucket bucket, int[] toAdd) = BuildFullBucket(); - ValueHash256[] nodes = bucket.GetAll(); + int[] nodes = bucket.GetAll(); AddNodes(bucket, toAdd); @@ -43,12 +42,10 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() [Test] public void GetAll_should_not_keep_cached_array_for_large_bucket() { - KBucket bucket = new(KBucket.DefaultReplacementCacheSize + 1); - AddNodes(bucket, Enumerable.Range(0, KBucket.DefaultReplacementCacheSize + 1) - .Select(static k => ValueKeccak.Compute(k.ToString())) - .ToArray()); + KBucket bucket = new(KBucket.DefaultReplacementCacheSize + 1); + AddNodes(bucket, Enumerable.Range(0, KBucket.DefaultReplacementCacheSize + 1).ToArray()); - ValueHash256[] nodes = bucket.GetAll(); + int[] nodes = bucket.GetAll(); Assert.That(bucket.GetAll(), Is.Not.SameAs(nodes)); } @@ -56,61 +53,59 @@ public void GetAll_should_not_keep_cached_array_for_large_bucket() [Test] public void TryAddOrRefresh_ShouldReplaceCachedNode_WhenRefreshingSameHashWithNewInstance() { - KBucket bucket = new(5); - Hash256 hash = IdentityNodeHashProvider.ToHash(ValueKeccak.Compute("node")); + KBucket bucket = new(5); + const int hash = 1; - bucket.TryAddOrRefresh(hash, "old", out _); - bucket.TryAddOrRefresh(hash, "new", out _); + bucket.TryAddOrRefresh(hash, 10, out _); + bucket.TryAddOrRefresh(hash, 11, out _); - Assert.That(bucket.GetByHash(hash), Is.EqualTo("new")); - Assert.That(bucket.GetAll(), Is.EqualTo(new[] { "new" })); - Assert.That(bucket.GetAllWithHash(), Is.EqualTo(new[] { (hash, "new") })); + Assert.That(bucket.GetByHash(hash), Is.EqualTo(11)); + Assert.That(bucket.GetAll(), Is.EqualTo(new[] { 11 })); + Assert.That(bucket.GetAllWithHash(), Is.EqualTo(new[] { (hash, 11) })); } [Test] public void RemoveAndReplace_ShouldReplaceNodeWithLatestInReplacementCache() { - (KBucket bucket, ValueHash256[] toAdd) = BuildFullBucket(); + (KBucket bucket, int[] toAdd) = BuildFullBucket(); - bucket.RemoveAndReplace(IdentityNodeHashProvider.ToHash(toAdd[0])); + bucket.RemoveAndReplace(toAdd[0]); - ValueHash256[] expected = [.. toAdd[1..5], toAdd[9]]; + int[] expected = [.. toAdd[1..5], toAdd[9]]; Assert.That(bucket.GetAll().ToHashSet(), Is.EquivalentTo(expected.ToHashSet())); - Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (IdentityNodeHashProvider.ToHash(it), it)).ToHashSet())); + Assert.That(bucket.GetAllWithHash().ToHashSet(), Is.EquivalentTo(expected.Select(static it => (it, it)).ToHashSet())); } [Test] public void Replacement_cache_should_not_scale_with_large_bucket_size() { - const int bucketSize = KBucket.DefaultReplacementCacheSize * 2; + const int bucketSize = KBucket.DefaultReplacementCacheSize * 2; - KBucket bucket = new(bucketSize); - ValueHash256[] nodes = Enumerable.Range(0, bucketSize + KBucket.DefaultReplacementCacheSize + 1) - .Select(static k => ValueKeccak.Compute(k.ToString())) - .ToArray(); + KBucket bucket = new(bucketSize); + int[] nodes = Enumerable.Range(0, bucketSize + KBucket.DefaultReplacementCacheSize + 1).ToArray(); AddNodes(bucket, nodes); - foreach (ValueHash256 node in nodes[..bucketSize]) + foreach (int node in nodes[..bucketSize]) { - bucket.RemoveAndReplace(IdentityNodeHashProvider.ToHash(node)); + bucket.RemoveAndReplace(node); } - Assert.That(bucket.Count, Is.EqualTo(KBucket.DefaultReplacementCacheSize)); + Assert.That(bucket.Count, Is.EqualTo(KBucket.DefaultReplacementCacheSize)); } - private static (KBucket Bucket, ValueHash256[] Nodes) BuildFullBucket() + private static (KBucket Bucket, int[] Nodes) BuildFullBucket() { - KBucket bucket = new(5); - ValueHash256[] nodes = Enumerable.Range(0, 10).Select(static k => ValueKeccak.Compute(k.ToString())).ToArray(); + KBucket bucket = new(5); + int[] nodes = Enumerable.Range(0, 10).ToArray(); AddNodes(bucket, nodes); return (bucket, nodes); } - private static void AddNodes(KBucket bucket, ValueHash256[] nodes) + private static void AddNodes(KBucket bucket, int[] nodes) { - foreach (ValueHash256 node in nodes) + foreach (int node in nodes) { - bucket.TryAddOrRefresh(IdentityNodeHashProvider.ToHash(node), node, out _); + bucket.TryAddOrRefresh(node, node, out _); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs index 1d90d1d1805d..7f63e6738779 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTreeTests.cs @@ -3,65 +3,61 @@ using System; using System.Linq; -using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Kademlia; using NUnit.Framework; namespace Nethermind.Network.Discovery.Test.Kademlia; public class KBucketTreeTests { - private static readonly ValueHash256 SelfHash = new("0x0000000000000000000000000000000000000000000000000000000000000000"); + private const int SelfHash = 0; - private static KBucketTree CreateTree(int k = 4, int beta = 0) => new( - new KademliaConfig { CurrentNodeId = SelfHash, KSize = k, Beta = beta }, - IdentityNodeHashProvider.Instance, - Hash256KademliaDistance.Instance); + private static KBucketTree CreateTree(int k = 4, int beta = 0) => new( + new KademliaConfig { CurrentNodeId = SelfHash, KSize = k, Beta = beta }, + IntNodeHashProvider.Instance, + Int32KademliaDistance.Instance); - private static void Add(KBucketTree tree, ValueHash256 hash) => - tree.TryAddOrRefresh(IdentityNodeHashProvider.ToHash(hash), hash, out _); - - private static ValueHash256 HashAtDistance(int distance, byte tag) => - ToValueHash(Hash256KademliaDistance.Instance.GetRandomHashAtDistance(IdentityNodeHashProvider.ToHash(SelfHash), distance, new Random(tag))); - - private static ValueHash256 ToValueHash(Hash256 hash) => hash.ValueHash256; + private static void Add(KBucketTree tree, int hash) => + tree.TryAddOrRefresh(hash, hash, out _); [Test] public void Split_should_preserve_lru_order_in_child_buckets() { - KBucketTree tree = CreateTree(k: 2, beta: 0); + KBucketTree tree = CreateTree(k: 2, beta: 0); - ValueHash256 left0 = HashAtDistance(255, 0x10); - ValueHash256 left1 = HashAtDistance(255, 0x11); - ValueHash256 right0 = HashAtDistance(254, 0x20); - ValueHash256 right1 = HashAtDistance(254, 0x21); + int left0 = KeyAtDistance(31, 0x10); + int left1 = KeyAtDistance(31, 0x11); + int right0 = KeyAtDistance(30, 0x20); + int right1 = KeyAtDistance(30, 0x21); Add(tree, left0); Add(tree, right0); Add(tree, left1); Add(tree, right1); - Assert.That(tree.GetAllAtDistance(255), Is.EqualTo(new[] { left1, left0 })); - Assert.That(tree.GetAllAtDistance(254), Is.EqualTo(new[] { right1, right0 })); + Assert.That(tree.GetAllAtDistance(31), Is.EqualTo(new[] { left1, left0 })); + Assert.That(tree.GetAllAtDistance(30), Is.EqualTo(new[] { right1, right0 })); } [Test] public void GetAllAtDistance_should_include_nodes_in_deeper_split_buckets() { - KBucketTree tree = CreateTree(k: 2, beta: 4); + KBucketTree tree = CreateTree(k: 2, beta: 4); - ValueHash256 deep1 = HashAtDistance(252, 0x40); - ValueHash256 deep2 = HashAtDistance(252, 0x41); - ValueHash256 deep3 = HashAtDistance(252, 0x42); + int deep1 = KeyAtDistance(28, 0x40); + int deep2 = KeyAtDistance(28, 0x41); + int deep3 = KeyAtDistance(28, 0x42); Add(tree, deep1); Add(tree, deep2); Add(tree, deep3); - ValueHash256[] expectedCandidates = [deep1, deep2, deep3]; - ValueHash256[] result = tree.GetAllAtDistance(252); + int[] expectedCandidates = [deep1, deep2, deep3]; + int[] result = tree.GetAllAtDistance(28); Assert.That(result, Is.SupersetOf(new[] { deep1, deep2 })); Assert.That(result.All(expectedCandidates.Contains), Is.True); } + + private static int KeyAtDistance(int distance, int suffix) + => Int32KademliaDistance.Instance.SetBit(suffix, Int32KademliaDistance.Instance.MaxDistance - distance); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 52dbf96cd57f..55645afa6b6d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Nethermind.Core.Crypto; using Nethermind.Kademlia; -using Nethermind.Network.Discovery.Kademlia; using NSubstitute; using NUnit.Framework; @@ -15,26 +13,26 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; public class LookupKNearestNeighbourTests { - private static readonly ValueHash256 Self = new("0x0000000000000000000000000000000000000000000000000000000000000000"); - private static readonly ValueHash256 Seed1 = new("0x1100000000000000000000000000000000000000000000000000000000000000"); - private static readonly ValueHash256 Seed2 = new("0x2200000000000000000000000000000000000000000000000000000000000000"); - private static readonly ValueHash256 Seed3 = new("0x3300000000000000000000000000000000000000000000000000000000000000"); - private static readonly ValueHash256 N1 = new("0x4400000000000000000000000000000000000000000000000000000000000000"); - private static readonly ValueHash256 N2 = new("0x5500000000000000000000000000000000000000000000000000000000000000"); - - private static (LookupKNearestNeighbour Lookup, IRoutingTable Routing, INodeHealthTracker Health) CreateLookup(int alpha, TimeSpan hardTimeout, ValueHash256[] seeds) + private const int Self = 0; + private const int Seed1 = 1; + private const int Seed2 = 2; + private const int Seed3 = 3; + private const int N1 = 4; + private const int N2 = 5; + + private static (LookupKNearestNeighbour Lookup, IRoutingTable Routing, INodeHealthTracker Health) CreateLookup(int alpha, TimeSpan hardTimeout, int[] seeds) { - IRoutingTable routing = Substitute.For>(); - routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); + IRoutingTable routing = Substitute.For>(); + routing.GetKNearestNeighbour(Arg.Any(), Arg.Any()).Returns(seeds); - INodeHealthTracker health = Substitute.For>(); + INodeHealthTracker health = Substitute.For>(); - LookupKNearestNeighbour lookup = new( + LookupKNearestNeighbour lookup = new( routing, - IdentityNodeHashProvider.Instance, - Hash256KademliaDistance.Instance, + IntNodeHashProvider.Instance, + Int32KademliaDistance.Instance, health, - new KademliaConfig + new KademliaConfig { CurrentNodeId = Self, Alpha = alpha, @@ -50,15 +48,15 @@ private static (LookupKNearestNeighbour Loo [CancelAfter(10000)] public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, CancellationToken token) { - (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = CreateLookup(alpha, TimeSpan.FromSeconds(30), [Seed1]); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); TaskCompletionSource requestInFlight = new(TaskCreationOptions.RunContinuationsAsynchronously); - Task task = lookup.Lookup( - IdentityNodeHashProvider.ToHash(Seed1), + Task task = lookup.Lookup( + Seed1, 8, async (_, t) => { @@ -80,11 +78,11 @@ public async Task Lookup_should_unblock_on_mid_flight_cancellation(int alpha, Ca [CancelAfter(10000)] public async Task Lookup_should_record_request_failure_on_hard_timeout(int alpha, CancellationToken token) { - (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = CreateLookup(alpha, TimeSpan.FromMilliseconds(100), [Seed1]); _ = await lookup.Lookup( - IdentityNodeHashProvider.ToHash(Seed1), + Seed1, 8, async (_, t) => { @@ -100,13 +98,13 @@ public async Task Lookup_should_record_request_failure_on_hard_timeout(int alpha [CancelAfter(10000)] public async Task Lookup_should_not_mark_node_healthy_when_find_neighbours_returns_null(CancellationToken token) { - (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = CreateLookup(1, TimeSpan.FromSeconds(10), [Seed1]); _ = await lookup.Lookup( - IdentityNodeHashProvider.ToHash(Seed1), + Seed1, 8, - (_, _) => Task.FromResult(null), + (_, _) => Task.FromResult(null), token); health.DidNotReceive().OnIncomingMessageFrom(Seed1); @@ -116,11 +114,11 @@ public async Task Lookup_should_not_mark_node_healthy_when_find_neighbours_retur [CancelAfter(10000)] public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(CancellationToken token) { - (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = + (LookupKNearestNeighbour lookup, _, INodeHealthTracker health) = CreateLookup(1, TimeSpan.FromMilliseconds(50), [Seed1]); _ = await lookup.Lookup( - IdentityNodeHashProvider.ToHash(Seed1), + Seed1, 8, async (_, t) => { @@ -137,20 +135,20 @@ public async Task Lookup_should_record_peer_failure_on_find_neighbour_timeout(Ca [CancelAfter(10000)] public async Task Lookup_should_return_results_with_different_alpha(int alpha, CancellationToken token) { - (LookupKNearestNeighbour lookup, _, _) = + (LookupKNearestNeighbour lookup, _, _) = CreateLookup(alpha, TimeSpan.FromSeconds(10), [Seed1, Seed2, Seed3]); - Dictionary neighbours = new() + Dictionary neighbours = new() { [Seed1] = [N1], [Seed2] = [N2], [Seed3] = [], }; - ValueHash256[] result = await lookup.Lookup( - IdentityNodeHashProvider.ToHash(Self), + int[] result = await lookup.Lookup( + Self, 8, - (node, _) => Task.FromResult(neighbours.GetValueOrDefault(node, [])), + (node, _) => Task.FromResult(neighbours.GetValueOrDefault(node, [])), token); Assert.That(result, Is.Not.Empty); @@ -160,12 +158,12 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C [CancelAfter(10000)] public async Task Lookup_should_drain_cancelled_workers_before_returning(CancellationToken token) { - (LookupKNearestNeighbour lookup, _, _) = + (LookupKNearestNeighbour lookup, _, _) = CreateLookup(2, TimeSpan.FromSeconds(10), [Seed1, Seed2, Seed3, N1]); TaskCompletionSource cancelledWorkerDrained = new(TaskCreationOptions.RunContinuationsAsynchronously); _ = await lookup.Lookup( - IdentityNodeHashProvider.ToHash(Self), + Self, 1, async (node, findToken) => { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index 09b5705b9a30..a88846649a7a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Nethermind.Core.Crypto; using Nethermind.Kademlia; using NSubstitute; using NUnit.Framework; @@ -14,29 +13,29 @@ namespace Nethermind.Network.Discovery.Test.Kademlia; public class NodeHealthTrackerTests { - private const string Self = "self"; - private const string Remote = "remote"; - private const string Stale = "stale"; + private const int Self = 0; + private const int Remote = 1; + private const int Stale = 2; - private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( - string? toRefresh = null, + private static (NodeHealthTracker Tracker, RoutingTableStub Routing, IKademliaMessageSender Sender) CreateTracker( + int? toRefresh = null, int failureThreshold = 5, TimeSpan? refreshPingTimeout = null, - IKademliaMessageSender? sender = null) + IKademliaMessageSender? sender = null) { - RoutingTableStub routing = new() { ToRefresh = toRefresh ?? string.Empty }; - sender ??= Substitute.For>(); - KademliaConfig config = new() + RoutingTableStub routing = new() { ToRefresh = toRefresh }; + sender ??= Substitute.For>(); + KademliaConfig config = new() { CurrentNodeId = Self, NodeRequestFailureThreshold = failureThreshold, }; if (refreshPingTimeout is { } timeout) config.RefreshPingTimeout = timeout; - NodeHealthTracker tracker = new( + NodeHealthTracker tracker = new( config, routing, - StringNodeHashProvider.Instance, + IntNodeHashProvider.Instance, sender); return (tracker, routing, sender); } @@ -44,12 +43,12 @@ private static (NodeHealthTracker Tracker, RoutingTabl [Test] public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSelectsSelf() { - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(toRefresh: Self); tracker.OnIncomingMessageFrom(Remote); Assert.That(routing.AddCalls, Has.Count.EqualTo(2)); - Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Self)))); + Assert.That(routing.AddCalls[1].Hash, Is.EqualTo(Self)); Assert.That(routing.AddCalls[1].Node, Is.EqualTo(Self)); } @@ -57,36 +56,34 @@ public void OnIncomingMessageFrom_ShouldRefreshSelfWithSelfNode_WhenFullBucketSe [CancelAfter(10000)] public async Task TryRefresh_ShouldRemoveStaleNode_WhenPingTimesOut(CancellationToken token) { - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()) .Returns(false); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, sender: sender); tracker.OnIncomingMessageFrom(Remote); - Hash256 staleHash = IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Stale)); - await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(staleHash), token); + await AssertEventuallyAsync(() => routing.RemoveCalls.Contains(Stale), token); } [Test] [CancelAfter(10000)] public async Task TryRefresh_ShouldKeepNode_WhenPingSucceeds(CancellationToken token) { - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()).Returns(true); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, sender: sender); tracker.OnIncomingMessageFrom(Remote); - Hash256 staleHash = IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Stale)); - await AssertEventuallyAsync(() => routing.HasAddedNode(staleHash), token); - Assert.That(routing.RemoveCalls, Does.Not.Contain(staleHash)); + await AssertEventuallyAsync(() => routing.HasAddedNode(Stale), token); + Assert.That(routing.RemoveCalls, Does.Not.Contain(Stale)); } [TestCase(false)] @@ -96,7 +93,7 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(bool asyn { TaskCompletionSource pingStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); TaskCompletionSource pingCancelled = new(TaskCreationOptions.RunContinuationsAsynchronously); - IKademliaMessageSender sender = Substitute.For>(); + IKademliaMessageSender sender = Substitute.For>(); sender.Ping(Stale, Arg.Any()).Returns(async call => { CancellationToken pingToken = call.Arg(); @@ -113,7 +110,7 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(bool asyn } }); - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker( toRefresh: Stale, refreshPingTimeout: TimeSpan.FromSeconds(10), sender: sender); @@ -131,20 +128,20 @@ public async Task Dispose_ShouldCancelActiveRefreshWithoutRemovingNode(bool asyn } await pingCancelled.Task.WaitAsync(token); - Assert.That(routing.RemoveCalls, Does.Not.Contain(IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Stale)))); + Assert.That(routing.RemoveCalls, Does.Not.Contain(Stale)); } [Test] public void OnRequestFailed_ShouldClearFailureCount_WhenNodeIsRemoved() { - (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); + (NodeHealthTracker tracker, RoutingTableStub routing, _) = CreateTracker(failureThreshold: 1); tracker.OnRequestFailed(Remote); tracker.OnRequestFailed(Remote); tracker.OnRequestFailed(Remote); Assert.That(routing.RemoveCalls, Has.Count.EqualTo(1)); - Assert.That(routing.RemoveCalls[0], Is.EqualTo(IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(Remote)))); + Assert.That(routing.RemoveCalls[0], Is.EqualTo(Remote)); } private static async Task AssertEventuallyAsync(Func condition, CancellationToken token) @@ -157,22 +154,15 @@ private static async Task AssertEventuallyAsync(Func condition, Cancellati Assert.Fail("Condition not met within timeout."); } - private sealed class StringNodeHashProvider : INodeHashProvider + private sealed class RoutingTableStub : IRoutingTable { - public static readonly StringNodeHashProvider Instance = new(); + public int? ToRefresh { get; init; } - public Hash256 GetHash(string node) => IdentityNodeHashProvider.ToHash(ValueKeccak.Compute(node)); - } - - private sealed class RoutingTableStub : IRoutingTable - { - public string ToRefresh { get; init; } = string.Empty; - - public List<(Hash256 Hash, string Node)> AddCalls { get; } = []; + public List<(int Hash, int Node)> AddCalls { get; } = []; - public List RemoveCalls { get; } = []; + public List RemoveCalls { get; } = []; - public BucketAddResult TryAddOrRefresh(in Hash256 hash, string item, out string? toRefresh) + public BucketAddResult TryAddOrRefresh(in int hash, int item, out int toRefresh) { bool isFirstAdd; lock (AddCalls) @@ -181,21 +171,21 @@ public BucketAddResult TryAddOrRefresh(in Hash256 hash, string item, out string? isFirstAdd = AddCalls.Count == 1; } - if (isFirstAdd) + if (isFirstAdd && ToRefresh is not null) { - toRefresh = ToRefresh; + toRefresh = ToRefresh.Value; return BucketAddResult.Full; } - toRefresh = null; + toRefresh = default; return BucketAddResult.Refreshed; } - public bool HasAddedNode(Hash256 hash) + public bool HasAddedNode(int hash) { lock (AddCalls) { - foreach ((Hash256 h, string _) in AddCalls) + foreach ((int h, int _) in AddCalls) { if (h == hash) return true; } @@ -203,34 +193,34 @@ public bool HasAddedNode(Hash256 hash) return false; } - public bool Remove(in Hash256 hash) + public bool Remove(in int hash) { lock (RemoveCalls) RemoveCalls.Add(hash); return true; } - public string[] GetKNearestNeighbour(Hash256 hash, bool excludeSelf = false) => + public int[] GetKNearestNeighbour(int hash, bool excludeSelf = false) => throw new NotSupportedException(); - public string[] GetKNearestNeighbourExcluding(Hash256 hash, Hash256 exclude, bool excludeSelf = false) => + public int[] GetKNearestNeighbourExcluding(int hash, int exclude, bool excludeSelf = false) => throw new NotSupportedException(); - public string[] GetAllAtDistance(int i) => throw new NotSupportedException(); + public int[] GetAllAtDistance(int i) => throw new NotSupportedException(); - public IEnumerable<(Hash256 Prefix, int Distance, KBucket Bucket)> IterateBuckets() => + public IEnumerable<(int Prefix, int Distance, KBucket Bucket)> IterateBuckets() => throw new NotSupportedException(); - public string? GetByHash(Hash256 nodeId) => throw new NotSupportedException(); + public int GetByHash(int nodeId) => throw new NotSupportedException(); public void LogDebugInfo() => throw new NotSupportedException(); - public event EventHandler? OnNodeAdded + public event EventHandler? OnNodeAdded { add { } remove { } } - public event EventHandler? OnNodeRemoved + public event EventHandler? OnNodeRemoved { add { } remove { } From ca09665e97acc6a30f67353af906b239d16a72f7 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 19 Jun 2026 19:32:45 +0300 Subject: [PATCH 174/182] Refine discovery review fixes --- .../Handlers/NodesResponseHandlerTests.cs | 66 ++++++- .../Discv5/DiscoveryV5App.cs | 10 +- .../Discv5/Kademlia/AdapterState.cs | 6 +- .../Kademlia/Handlers/NodesResponseHandler.cs | 157 ++++++++++----- .../Discv5/Kademlia/KademliaAdapter.cs | 47 ++--- .../Discv5/MessageCodec.cs | 88 +++------ .../Discv5/Messages/Distances.cs | 34 +--- .../Discv5/Packets/PacketCodec.cs | 20 +- .../Serializers/FindNodeMsgSerializer.cs | 2 +- .../Discv5/Serializers/MsgSerializerBase.cs | 26 ++- .../Discv5/Serializers/NodesMsgSerializer.cs | 2 +- .../Discv5/Serializers/PingMsgSerializer.cs | 2 +- .../Discv5/Serializers/PongMsgSerializer.cs | 2 +- .../Serializers/TalkReqMsgSerializer.cs | 2 +- .../Serializers/TalkRespMsgSerializer.cs | 2 +- .../Nethermind.Network.Enr/NodeRecord.cs | 17 +- .../NodeRecordSigner.cs | 34 +--- .../NodeFilterTests.cs | 67 ++----- .../Nethermind.Network/IP/WebIPSource.cs | 2 +- .../Nethermind.Network/IPAddressExtensions.cs | 43 ++++ .../Nethermind.Network/NodeFilter.cs | 184 +++--------------- ...ddressClassifier.cs => ParsedIPAddress.cs} | 109 ++++------- 22 files changed, 416 insertions(+), 506 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network/IPAddressExtensions.cs rename src/Nethermind/Nethermind.Network/{IPAddressClassifier.cs => ParsedIPAddress.cs} (65%) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs index 3f0cbce16537..18ce998f5dcf 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/Handlers/NodesResponseHandlerTests.cs @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Net; +using System.Threading.Tasks; using Nethermind.Core.Crypto; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; @@ -32,13 +34,71 @@ public void ShouldFilterRecordByReceiverAndRecordAddress(string receiverIp, stri Assert.That(handler.GetNodes(), Has.Length.EqualTo(expectedCount)); } + [Test] + public void ShouldRejectNodesReadBeforeCompletion() + { + Node receiver = new(TestItem.PublicKeyA, "127.0.0.1", 30303); + NodeRecord record = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + NodesResponseHandler handler = CreateNodesResponseHandler(receiver, record); + + Assert.That(handler.GetNodes, Throws.TypeOf()); + } + + [Test] + public async Task ShouldCollectConcurrentBatchesOnce() + { + Node receiver = new(TestItem.PublicKeyA, "127.0.0.1", 30303); + NodeRecord first = CreateEnr(TestItem.PrivateKeyB, IPAddress.Loopback); + NodeRecord second = CreateEnr(TestItem.PrivateKeyC, IPAddress.Loopback); + NodeRecord third = CreateEnr(TestItem.PrivateKeyD, IPAddress.Loopback); + NodeRecord fourth = CreateEnr(TestItem.PrivateKeyE, IPAddress.Loopback); + using Distances distances = CreateDistances(receiver, first, second, third, fourth); + NodesResponseHandler handler = new(receiver, distances, Hash256KademliaDistance.Instance, ExecutionLayerDiscv5RecordFilter.Instance); + + using NodesMsg firstBatch = new([1], 2, [first, second, first]); + using NodesMsg secondBatch = new([2], 2, [third, fourth, second]); + + await Task.WhenAll( + Task.Run(() => handler.Handle(firstBatch)), + Task.Run(() => handler.Handle(secondBatch))); + await handler.Task.WaitAsync(TimeSpan.FromSeconds(1)); + + Node[] nodes = handler.GetNodes(); + Assert.That(nodes, Has.Length.EqualTo(4)); + AssertUniqueNodeIds(nodes); + } + private static NodeRecord CreateEnr(PrivateKey privateKey, IPAddress ipAddress) => TestEnrBuilder.BuildSigned(privateKey, ipAddress, tcpPort: null); - private static NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) + private static NodesResponseHandler CreateNodesResponseHandler(Node receiver, NodeRecord record) => + new(receiver, CreateDistances(receiver, record), Hash256KademliaDistance.Instance, ExecutionLayerDiscv5RecordFilter.Instance); + + private static Distances CreateDistances(Node receiver, params NodeRecord[] records) + { + int[] distances = new int[records.Length]; + for (int i = 0; i < records.Length; i++) + { + distances[i] = GetDistance(receiver, records[i]); + } + + return new Distances(distances); + } + + private static int GetDistance(Node receiver, NodeRecord record) { PublicKey nodeId = record.GetObj(EnrContentKey.SecP256k1)!.Decompress(); - int distance = Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); - return new NodesResponseHandler(receiver, new Distances([distance]), Hash256KademliaDistance.Instance, ExecutionLayerDiscv5RecordFilter.Instance); + return Hash256KademliaDistance.Instance.CalculateLogDistance(receiver.Id.Hash, nodeId.Hash); + } + + private static void AssertUniqueNodeIds(Node[] nodes) + { + for (int i = 0; i < nodes.Length; i++) + { + for (int j = i + 1; j < nodes.Length; j++) + { + Assert.That(nodes[i].Id.Hash, Is.Not.EqualTo(nodes[j].Id.Hash)); + } + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 5aae6073d7a4..b4b78facc116 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -207,17 +207,17 @@ internal static bool IsDiscoveryAddressAcceptable(IPAddress ipAddress, bool allo return false; } - if (IPAddressClassifier.IsMulticast(ipAddress)) + if (ipAddress.IsMulticast) { return false; } - if (IPAddressClassifier.IsSpecialUseAddress(ipAddress)) + if (ipAddress.IsSpecialUseAddress) { return false; } - return allowNonRoutable || !IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(ipAddress); + return allowNonRoutable || !ipAddress.IsLoopbackOrPrivateOrLinkLocal; } internal static bool IsDiscoveryAddressRoutable(IPAddress ipAddress) @@ -229,7 +229,7 @@ internal static bool IsConsensusOnlyNodeRecord(NodeRecord enr) private static bool ShouldAcceptNonRoutableEnrs(IPAddress externalIp) => !IPAddress.Any.Equals(externalIp) && !IPAddress.None.Equals(externalIp) - && IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(externalIp); + && externalIp.IsLoopbackOrPrivateOrLinkLocal; public override void InitializeChannel(IChannel channel) { @@ -297,7 +297,7 @@ private enum BootNodeAddResult Skipped } - private sealed class BootNodeStats + private struct BootNodeStats { public int Total { get; private set; } public int Added { get; private set; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs index 90fca4991648..3b8322c70953 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs @@ -10,13 +10,13 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; -internal readonly record struct SessionKey(Hash256 NodeId, IPEndPoint Endpoint); +internal readonly record struct SessionKey(ValueHash256 NodeId, IPEndPoint Endpoint); -internal readonly record struct ChallengeKey(Hash256 NodeId, IPEndPoint Endpoint); +internal readonly record struct ChallengeKey(ValueHash256 NodeId, IPEndPoint Endpoint); internal readonly record struct PendingNonceKey(IPEndPoint Endpoint, NonceKey Nonce); -internal readonly record struct ResponseKey(Hash256 NodeId, RequestId RequestId, MessageType MessageType); +internal readonly record struct ResponseKey(ValueHash256 NodeId, RequestId RequestId, MessageType MessageType); internal readonly record struct SentChallengeExpiry(ChallengeKey Key, long CreatedAtMilliseconds); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs index 5fd6428906f0..8d24847fcc31 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/Handlers/NodesResponseHandler.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Collections.Pooled; using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Discv5.Messages; @@ -15,19 +14,18 @@ internal sealed class NodesResponseHandler(Node receiver, Distances requestedDis { private const int MaxNodesResponseMessages = 16; private const int MaxNodesResponseRecords = 64; + private const int SeenNodeIdsCapacity = 128; + private const int SeenNodeIdsMask = SeenNodeIdsCapacity - 1; private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly Node[] _nodes = new Node[MaxNodesResponseRecords]; - private readonly PooledSet _seenNodeIds = new(MaxNodesResponseRecords); - private readonly bool _allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(receiver.Address.Address); + private readonly Hash256?[] _seenNodeIds = new Hash256?[SeenNodeIdsCapacity]; + private readonly bool _allowNonRoutableRelays = receiver.Address.Address.IsLoopbackOrPrivateOrLinkLocal; - // Packet workers can race each other and can still hold this handler after the request owner removed - // it from the response cache and disposed it, so all state access is serialized and Handle becomes a - // no-op once disposed; otherwise it could write into pooled arrays already returned by Dispose. private readonly Lock _lock = new(); - private bool _disposed; - private int? _total; - private int _received; + private bool _done; + private int _totalMessages; + private int _receivedMessages; private int _nodeCount; public override Task Task => _completion.Task; @@ -36,77 +34,140 @@ public void Dispose() { lock (_lock) { - if (_disposed) - { - return; - } - - _disposed = true; - _seenNodeIds.Dispose(); + _done = true; } } public override bool Handle(NodesMsg nodes) { + if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) + { + Complete(); + return true; + } + + bool complete = false; + lock (_lock) { - if (_disposed || _completion.Task.IsCompleted) + if (_done) { return true; } - if (nodes.Total <= 0 || nodes.Total > MaxNodesResponseMessages) + if (_totalMessages != 0 && _totalMessages != nodes.Total) { - _completion.TrySetResult(); - return true; + complete = CompleteLocked(); } - - if (_total is not null && _total.Value != nodes.Total) + else { - _completion.TrySetResult(); - return true; - } + _totalMessages = nodes.Total; + _receivedMessages++; - _total ??= nodes.Total; - _received++; - - for (int i = 0; i < nodes.Records.Count && _nodeCount < MaxNodesResponseRecords; i++) - { - NodeRecord record = nodes.Records[i]; - if (recordFilter.Excludes(record) || - !Node.TryFromDiscoveryEnr(record, out Node? node) || - !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || - !_seenNodeIds.Add(node.Id.Hash) || - !MatchesRequestedDistance(node, requestedDistances)) + if (_receivedMessages <= nodes.Total) { - continue; + AddRecords(nodes); } - _nodes[_nodeCount++] = node; + if (_receivedMessages >= nodes.Total || _nodeCount >= MaxNodesResponseRecords) + { + complete = CompleteLocked(); + } } + } + + if (complete) + { + _completion.TrySetResult(); + } + + return true; + } + + public Node[] GetNodes() + { + if (!Task.IsCompleted) + { + throw new InvalidOperationException($"{nameof(GetNodes)} must be called after the response handler completes."); + } + + int nodeCount = _nodeCount; + if (nodeCount == 0) + { + return []; + } - if (_received >= _total || _nodeCount >= MaxNodesResponseRecords) + Node[] nodes = _nodes; + if (nodeCount != nodes.Length) + { + Array.Resize(ref nodes, nodeCount); + } + + return nodes; + } + + private void AddRecords(NodesMsg nodes) + { + for (int i = 0; i < nodes.Records.Count && _nodeCount < MaxNodesResponseRecords; i++) + { + NodeRecord record = nodes.Records[i]; + if (recordFilter.Excludes(record) || + !Node.TryFromDiscoveryEnr(record, out Node? node) || + !DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, _allowNonRoutableRelays) || + !TryMarkSeen(node.Id.Hash) || + !MatchesRequestedDistance(node, requestedDistances)) { - _completion.TrySetResult(); + continue; } - return true; + _nodes[_nodeCount++] = node; } } - public Node[] GetNodes() + private bool TryMarkSeen(Hash256 nodeId) { - lock (_lock) + for (int i = 0; i < SeenNodeIdsCapacity; i++) { - if (_nodeCount == 0) + int index = (nodeId.GetHashCode() + i) & SeenNodeIdsMask; + Hash256? current = _seenNodeIds[index]; + if (current is null) { - return []; + _seenNodeIds[index] = nodeId; + return true; } - Node[] nodes = new Node[_nodeCount]; - Array.Copy(_nodes, nodes, _nodeCount); - return nodes; + if (current.Equals(nodeId)) + { + return false; + } + } + + return false; + } + + private void Complete() + { + bool complete; + lock (_lock) + { + complete = CompleteLocked(); } + + if (complete) + { + _completion.TrySetResult(); + } + } + + private bool CompleteLocked() + { + if (_done) + { + return false; + } + + _done = true; + return true; } private bool MatchesRequestedDistance(Node node, Distances requestedDistances) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 69de85030373..32ee6da31a75 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -59,7 +59,7 @@ public sealed class KademliaAdapter( private long _lastSentChallengeTrimMilliseconds; private readonly LruCache _pendingByNonce = new(MaxPendingRequests, "discv5 pending requests"); private readonly LruCache _responseHandlers = new(MaxResponseHandlers, "discv5 response handlers"); - private readonly LruCache _knownRecords = new(MaxKnownRecords, "discv5 known records"); + private readonly LruCache _knownRecords = new(MaxKnownRecords, "discv5 known records"); private readonly Lock _knownRecordsLock = new(); private readonly LruCache _endpointChecks = new(MaxEndpointChecks, "discv5 endpoint checks"); private readonly AddressBurstLimiter _challengeRateLimiter = new(ChallengeRateLimitBurstPerIp, ChallengeRateLimitFilterSize, ChallengeRateLimitWindow); @@ -200,7 +200,7 @@ private async Task SendRequest( CancellationToken token) where TResponse : Discv5Message { - ResponseKey responseKey = new(receiver.Id.Hash, request.RequestId, responseHandler.MessageType); + ResponseKey responseKey = new(receiver.Id.Hash.ValueHash256, request.RequestId, responseHandler.MessageType); _responseHandlers.Set(responseKey, responseHandler); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); @@ -246,7 +246,7 @@ private bool TryEncodeWithExistingSession( out PendingNonceKey pendingNonceKey, [NotNullWhen(true)] out byte[]? packet) { - SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); + SessionKey sessionKey = new(receiver.Id.Hash.ValueHash256, receiver.Address); if (TryGetSession(sessionKey, out Session? session)) { Span writeKey = stackalloc byte[Session.KeySize]; @@ -312,7 +312,7 @@ private async Task SendResponse(Node receiver, Discv5Message message, Cancellati [SkipLocalsInit] private bool TryEncodeResponse(Node receiver, Discv5Message message, [NotNullWhen(true)] out byte[]? packet) { - SessionKey sessionKey = new(receiver.Id.Hash, receiver.Address); + SessionKey sessionKey = new(receiver.Id.Hash.ValueHash256, receiver.Address); if (!TryGetSession(sessionKey, out Session? session)) { packet = null; @@ -379,14 +379,14 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat Challenge challenge = packetCodec.DecodeWhoAreYou(in packet); byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Session session); - SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash, endpoint), session); + SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash.ValueHash256, endpoint), session); if (_logger.IsTrace) _logger.Trace($"Sending discv5 HANDSHAKE for {pendingRequest.Message.MessageType} {pendingRequest.Message.RequestId} to {endpoint}, bytes: {handshakePacket.Length}, requested ENR seq: {challenge.EnrSequence}."); await discoveryHandler.SendAsync(handshakePacket, endpoint); } private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!PacketCodec.TryGetSourceNodeId(in packet, out Hash256? nodeId)) + if (!PacketCodec.TryGetSourceNodeId(in packet, out ValueHash256 nodeId)) { if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 ordinary packet from {endpoint}; source node id missing."); return; @@ -429,7 +429,7 @@ private bool TryDecryptOrdinaryMessage(scoped in Packet packet, SessionKey sessi private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, CancellationToken token) { - if (!PacketCodec.TryGetSourceNodeId(in packet, out Hash256? nodeId)) + if (!PacketCodec.TryGetSourceNodeId(in packet, out ValueHash256 nodeId)) { if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; source node id missing."); return; @@ -469,7 +469,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat return; } - if (IsAcceptableNodeRecord(nodeRecord, nodeId, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(endpoint.Address), recordFilter)) + if (IsAcceptableNodeRecord(nodeRecord, nodeId, endpoint.Address.IsLoopbackOrPrivateOrLinkLocal, recordFilter)) { TrySetKnownRecord(nodeId, nodeRecord, out NodeRecord currentRecord); messageRecord = currentRecord; @@ -491,7 +491,7 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat } } - private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Hash256 nodeId) + private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, ValueHash256 nodeId) { ChallengeKey challengeKey = new(nodeId, endpoint); long now = Environment.TickCount64; @@ -517,11 +517,12 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Hash private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, Discv5Message message, CancellationToken token, NodeRecord? nodeRecord = null) { + ValueHash256 remoteNodeId = remotePublicKey.Hash.ValueHash256; Node remoteNode = new(remotePublicKey, endpoint) { - Enr = GetKnownEnr(remotePublicKey.Hash, nodeRecord) + Enr = GetKnownEnr(remoteNodeId, nodeRecord) }; - if (HandleResponse(remotePublicKey.Hash, message)) + if (HandleResponse(remoteNodeId, message)) { if (_logger.IsTrace) _logger.Trace($"Handled discv5 response {message.MessageType} {message.RequestId} from {endpoint}."); kademlia.Value.AddOrRefresh(remoteNode); @@ -557,10 +558,10 @@ private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, } } - private string? GetKnownEnr(Hash256 nodeId, NodeRecord? nodeRecord) + private string? GetKnownEnr(ValueHash256 nodeId, NodeRecord? nodeRecord) => nodeRecord?.EnrString ?? (_knownRecords.TryGet(nodeId, out NodeRecord? knownRecord) ? knownRecord.EnrString : null); - private bool HandleResponse(Hash256 nodeId, Discv5Message message) + private bool HandleResponse(ValueHash256 nodeId, Discv5Message message) { ResponseKey responseKey = new(nodeId, message.RequestId, message.MessageType); return _responseHandlers.TryGet(responseKey, out IResponseHandler? handler) && handler.Handle(message); @@ -592,7 +593,7 @@ private NodeRecord[] GetFindNodeRecords(Distances distances, Node requester) ArrayPoolListRef result = new(MaxFindNodeRecords); try { - bool allowNonRoutableRelays = IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(requester.Address.Address); + bool allowNonRoutableRelays = requester.Address.Address.IsLoopbackOrPrivateOrLinkLocal; bool includedSelf = false; for (int i = 0; i < distances.Count && result.Count < MaxFindNodeRecords; i++) { @@ -679,7 +680,7 @@ private void RegisterKnownRecord(Node node) try { NodeRecord record = NodeRecord.FromEnrString(node.Enr); - if (IsAcceptableNodeRecord(record, node.Id.Hash, IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(node.Address.Address), recordFilter)) + if (IsAcceptableNodeRecord(record, node.Id.Hash, node.Address.Address.IsLoopbackOrPrivateOrLinkLocal, recordFilter)) { TrySetKnownRecord(node.Id.Hash, record, out _); } @@ -753,9 +754,9 @@ private RequestId CreateRequestId() private void SetSession(SessionKey sessionKey, Session session) => _sessions.Set(sessionKey, session); - private bool TryGetKnownRecord(Hash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGet(nodeId, out record); + private bool TryGetKnownRecord(ValueHash256 nodeId, [NotNullWhen(true)] out NodeRecord? record) => _knownRecords.TryGet(nodeId, out record); - internal bool TrySetKnownRecord(Hash256 nodeId, NodeRecord record, out NodeRecord currentRecord) + internal bool TrySetKnownRecord(ValueHash256 nodeId, NodeRecord record, out NodeRecord currentRecord) { lock (_knownRecordsLock) { @@ -771,14 +772,14 @@ internal bool TrySetKnownRecord(Hash256 nodeId, NodeRecord record, out NodeRecor } } - internal static bool IsAcceptableNodeRecord(NodeRecord record, Hash256 expectedNodeId, bool allowNonRoutable, IDiscv5RecordFilter recordFilter) + internal static bool IsAcceptableNodeRecord(NodeRecord record, ValueHash256 expectedNodeId, bool allowNonRoutable, IDiscv5RecordFilter recordFilter) => !recordFilter.Excludes(record) && Node.TryFromDiscoveryEnr(record, out Node? node) && - node.Id.Hash.Equals(expectedNodeId) && + node.Id.Hash == expectedNodeId && DiscoveryV5App.IsDiscoveryAddressAcceptable(node.Address.Address, allowNonRoutable); - internal static bool HasExpectedNodeId(NodeRecord record, Hash256 expectedNodeId) - => record.GetObj(EnrContentKey.SecP256k1)?.Decompress().Hash.Equals(expectedNodeId) == true; + internal static bool HasExpectedNodeId(NodeRecord record, ValueHash256 expectedNodeId) + => record.GetObj(EnrContentKey.SecP256k1)?.Decompress().Hash == expectedNodeId; private void SetSentChallenge(ChallengeKey challengeKey, Challenge challenge, byte[] packet) { @@ -855,11 +856,11 @@ private async Task RunEndpointCheck(Node remoteNode, CancellationToken token) } private void ReserveEndpointCheck(Node remoteNode) - => _endpointChecks.Set(new SessionKey(remoteNode.Id.Hash, remoteNode.Address), Environment.TickCount64); + => _endpointChecks.Set(new SessionKey(remoteNode.Id.Hash.ValueHash256, remoteNode.Address), Environment.TickCount64); private bool TryReserveEndpointCheck(Node remoteNode) { - SessionKey sessionKey = new(remoteNode.Id.Hash, remoteNode.Address); + SessionKey sessionKey = new(remoteNode.Id.Hash.ValueHash256, remoteNode.Address); long now = Environment.TickCount64; if (_endpointChecks.TryGet(sessionKey, out long startedAt) && now - startedAt <= EndpointCheckTtlMilliseconds) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs index 316a167c37dd..2b71685fd6df 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/MessageCodec.cs @@ -10,22 +10,26 @@ namespace Nethermind.Network.Discovery.Discv5; internal static class MessageCodec { - private static readonly PingMsgSerializer PingSerializer = new(); - private static readonly PongMsgSerializer PongSerializer = new(); - private static readonly FindNodeMsgSerializer FindNodeSerializer = new(); - private static readonly NodesMsgSerializer NodesSerializer = new(); - private static readonly TalkReqMsgSerializer TalkReqSerializer = new(); - private static readonly TalkRespMsgSerializer TalkRespSerializer = new(); + private static readonly IMsgSerializer?[] Serializers = [null, + new PingMsgSerializer(), + new PongMsgSerializer(), + new FindNodeMsgSerializer(), + new NodesMsgSerializer(), + new TalkReqMsgSerializer(), + new TalkRespMsgSerializer(), + ]; public static NettyRlpStream Encode(Discv5Message message) { - int contentLength = GetContentLength(message); + IMsgSerializer serializer = GetSerializer(message.MessageType); + int contentLength = serializer.GetContentLength(message); NettyRlpStream stream = new(NethermindBuffers.Default.Buffer(Rlp.LengthOfSequence(contentLength) + 1)); + try { stream.WriteByte((byte)message.MessageType); stream.StartSequence(contentLength); - EncodeContent(stream, message); + serializer.Serialize(stream, message); } catch { @@ -37,7 +41,7 @@ public static NettyRlpStream Encode(Discv5Message message) } public static Discv5Message Decode(ReadOnlySpan message) - => NeedsOwnedMessage(message) + => RequiresOwnedMessage(message) ? throw new RlpException("discv5 TALK messages require owned message memory. Use DecodeOwned.") : Decode(message, default, null); @@ -56,20 +60,11 @@ private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory PingSerializer.Deserialize(ref ctx, ownedMessage, owner), - MessageType.Pong => PongSerializer.Deserialize(ref ctx, ownedMessage, owner), - MessageType.FindNode => FindNodeSerializer.Deserialize(ref ctx, ownedMessage, owner), - MessageType.Nodes => NodesSerializer.Deserialize(ref ctx, ownedMessage, owner), - MessageType.TalkReq => TalkReqSerializer.Deserialize(ref ctx, ownedMessage, owner), - MessageType.TalkResp => TalkRespSerializer.Deserialize(ref ctx, ownedMessage, owner), - _ => throw new RlpException($"Unsupported discv5 message type {(byte)messageType}.") - }; - + decoded = serializer.Deserialize(ref ctx, ownedMessage, owner); ctx.Check(checkPosition); ctx.CheckEnd(); return decoded; @@ -89,52 +84,27 @@ private static Discv5Message Decode(ReadOnlySpan message, ReadOnlyMemory? owner) + private static IMsgSerializer GetSerializer(MessageType messageType) { - if (owner is { } ownerValue) + int type = (byte)messageType; + if ((uint)type < (uint)Serializers.Length && Serializers[type] is { } serializer) { - ownerValue.Dispose(); + return serializer; } - } - private static bool NeedsOwnedMessage(ReadOnlySpan message) - => !message.IsEmpty && (MessageType)message[0] is MessageType.TalkReq or MessageType.TalkResp; + throw new RlpException($"Unsupported discv5 message type {(byte)messageType}."); + } - private static int GetContentLength(Discv5Message message) => message switch - { - PingMsg ping => PingSerializer.GetContentLength(ping), - PongMsg pong => PongSerializer.GetContentLength(pong), - FindNodeMsg findNode => FindNodeSerializer.GetContentLength(findNode), - NodesMsg nodes => NodesSerializer.GetContentLength(nodes), - TalkReqMsg talkReq => TalkReqSerializer.GetContentLength(talkReq), - TalkRespMsg talkResp => TalkRespSerializer.GetContentLength(talkResp), - _ => throw new RlpException($"Unsupported discv5 message {message.GetType().Name}.") - }; - - private static void EncodeContent(NettyRlpStream stream, Discv5Message message) + private static void DisposeOwner(ArrayPoolSpan? owner) { - switch (message) + if (owner is { } ownerValue) { - case PingMsg ping: - PingSerializer.Serialize(stream, ping); - break; - case PongMsg pong: - PongSerializer.Serialize(stream, pong); - break; - case FindNodeMsg findNode: - FindNodeSerializer.Serialize(stream, findNode); - break; - case NodesMsg nodes: - NodesSerializer.Serialize(stream, nodes); - break; - case TalkReqMsg talkReq: - TalkReqSerializer.Serialize(stream, talkReq); - break; - case TalkRespMsg talkResp: - TalkRespSerializer.Serialize(stream, talkResp); - break; - default: - throw new RlpException($"Unsupported discv5 message {message.GetType().Name}."); + ownerValue.Dispose(); } } + + private static bool RequiresOwnedMessage(ReadOnlySpan message) + => !message.IsEmpty + && message[0] < Serializers.Length + && Serializers[message[0]] is { RequiresOwnedMemory: true }; } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs index 6028896f6aed..52f61d4d3421 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Messages/Distances.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Collections; +using System.Runtime.CompilerServices; using Nethermind.Core.Collections; namespace Nethermind.Network.Discovery.Discv5.Messages; @@ -12,9 +13,7 @@ internal sealed class Distances : IReadOnlyList, IDisposable private const int InlineCapacity = 3; private int[]? _rented; - private int _first; - private int _second; - private int _third; + private InlineDistances _inline; public Distances(ReadOnlySpan distances) : this(distances.Length) @@ -52,13 +51,7 @@ public int this[int index] return _rented[index]; } - return index switch - { - 0 => _first, - 1 => _second, - 2 => _third, - _ => throw new ArgumentOutOfRangeException(nameof(index)) - }; + return _inline[index]; } } @@ -91,19 +84,12 @@ internal void Set(int index, int value) return; } - switch (index) - { - case 0: - _first = value; - return; - case 1: - _second = value; - return; - case 2: - _third = value; - return; - default: - throw new ArgumentOutOfRangeException(nameof(index)); - } + _inline[index] = value; + } + + [InlineArray(InlineCapacity)] + private struct InlineDistances + { + private int _element0; } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 4e724530cb00..4f64538cc1c0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -46,7 +46,7 @@ public sealed class PacketCodec( private readonly PrivateKey _privateKey = nodeKey.Unprotect(); private readonly PublicKey _publicKey = nodeKey.PublicKey; - private readonly Hash256 _localNodeId = nodeKey.PublicKey.Hash; + private readonly ValueHash256 _localNodeId = nodeKey.PublicKey.Hash.ValueHash256; private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; private readonly ICryptoRandom _cryptoRandom = cryptoRandom; private readonly IEcdsa _ecdsa = ecdsa; @@ -285,7 +285,7 @@ internal bool TryDecryptHandshake( message = null!; nodeRecord = null; - if (!TryReadHandshakeAuthData(packet.AuthData, out Hash256? sourceNodeId, out ReadOnlyMemory idSignature, out CompressedPublicKey? ephemeralPublicKey, out ReadOnlyMemory recordBytes)) + if (!TryReadHandshakeAuthData(packet.AuthData, out ValueHash256 sourceNodeId, out ReadOnlyMemory idSignature, out CompressedPublicKey? ephemeralPublicKey, out ReadOnlyMemory recordBytes)) { return false; } @@ -310,7 +310,7 @@ internal bool TryDecryptHandshake( } PublicKey remotePublicKey = remoteCompressedPublicKey.Decompress(); - if (!remotePublicKey.Hash.Equals(sourceNodeId)) + if (remotePublicKey.Hash != sourceNodeId) { return false; } @@ -331,16 +331,16 @@ internal bool TryDecryptHandshake( return true; } - internal static bool TryGetSourceNodeId(scoped in Packet packet, [NotNullWhen(true)] out Hash256? sourceNodeId) + internal static bool TryGetSourceNodeId(scoped in Packet packet, out ValueHash256 sourceNodeId) { - sourceNodeId = null; + sourceNodeId = default; switch (packet.Flag) { case PacketFlag.Ordinary when packet.AuthData.Length == NodeIdSize: - sourceNodeId = new Hash256(packet.AuthData.Span); + sourceNodeId = new ValueHash256(packet.AuthData.Span); return true; case PacketFlag.Handshake when packet.AuthData.Length >= HandshakeAuthDataHeadSize: - sourceNodeId = new Hash256(packet.AuthData.Span[..NodeIdSize]); + sourceNodeId = new ValueHash256(packet.AuthData.Span[..NodeIdSize]); return true; default: return false; @@ -674,12 +674,12 @@ private static void CalculateIdSignatureHash( private static bool TryReadHandshakeAuthData( ReadOnlyMemory authDataMemory, - [NotNullWhen(true)] out Hash256? sourceNodeId, + out ValueHash256 sourceNodeId, out ReadOnlyMemory idSignature, [NotNullWhen(true)] out CompressedPublicKey? ephemeralPublicKey, out ReadOnlyMemory record) { - sourceNodeId = null; + sourceNodeId = default; idSignature = ReadOnlyMemory.Empty; ephemeralPublicKey = null; record = ReadOnlyMemory.Empty; @@ -690,7 +690,7 @@ record = ReadOnlyMemory.Empty; return false; } - sourceNodeId = new Hash256(authData[..NodeIdSize]); + sourceNodeId = new ValueHash256(authData[..NodeIdSize]); int signatureSize = authData[NodeIdSize]; int ephemeralKeySize = authData[NodeIdSize + 1]; if (signatureSize != IdSignatureSize || ephemeralKeySize != EphemeralPublicKeySize) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs index a37421042888..93aa9032ae28 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/FindNodeMsgSerializer.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class FindNodeMsgSerializer : MsgSerializerBase +internal sealed class FindNodeMsgSerializer() : MsgSerializerBase(MessageType.FindNode) { protected override int GetContentLengthCore(FindNodeMsg msg) => GetDistancesLength(msg.Distances); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs index 97970282512a..f3f83238ffea 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/MsgSerializerBase.cs @@ -8,9 +8,26 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal abstract class MsgSerializerBase +internal interface IMsgSerializer +{ + MessageType MessageType { get; } + + bool RequiresOwnedMemory { get; } + + int GetContentLength(Discv5Message msg); + + void Serialize(NettyRlpStream stream, Discv5Message msg); + + Discv5Message Deserialize(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner); +} + +internal abstract class MsgSerializerBase(MessageType messageType, bool requiresOwnedMemory = false) : IMsgSerializer where TMessage : Discv5Message { + public MessageType MessageType { get; } = messageType; + + public bool RequiresOwnedMemory { get; } = requiresOwnedMemory; + public int GetContentLength(TMessage msg) => msg.RequestId.GetRlpLength() + GetContentLengthCore(msg); @@ -27,6 +44,13 @@ public TMessage Deserialize(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory GetContentLength((TMessage)msg); + + void IMsgSerializer.Serialize(NettyRlpStream stream, Discv5Message msg) => Serialize(stream, (TMessage)msg); + + Discv5Message IMsgSerializer.Deserialize(ref Rlp.ValueDecoderContext ctx, ReadOnlyMemory ownedMessage, ArrayPoolSpan? owner) + => Deserialize(ref ctx, ownedMessage, owner); + protected abstract int GetContentLengthCore(TMessage msg); protected abstract void SerializeCore(NettyRlpStream stream, TMessage msg); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs index 87debcc2d9a7..ad1b9ff6cca5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/NodesMsgSerializer.cs @@ -10,7 +10,7 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class NodesMsgSerializer : MsgSerializerBase +internal sealed class NodesMsgSerializer() : MsgSerializerBase(MessageType.Nodes) { private const int MaxNodeRecordsPerMessage = 16; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs index 4c0b475f7e67..1559cb78d3ae 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PingMsgSerializer.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class PingMsgSerializer : MsgSerializerBase +internal sealed class PingMsgSerializer() : MsgSerializerBase(MessageType.Ping) { protected override int GetContentLengthCore(PingMsg msg) => Rlp.LengthOf(msg.EnrSequence); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs index 4f3f3cf2bd35..403eef5375b9 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/PongMsgSerializer.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class PongMsgSerializer : MsgSerializerBase +internal sealed class PongMsgSerializer() : MsgSerializerBase(MessageType.Pong) { private static readonly RlpLimit IpAddressRlpLimit = RlpLimit.For(16, nameof(PongMsg.RecipientIp)); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs index c230d9adbda6..90b7485efb0a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkReqMsgSerializer.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class TalkReqMsgSerializer : MsgSerializerBase +internal sealed class TalkReqMsgSerializer() : MsgSerializerBase(MessageType.TalkReq, requiresOwnedMemory: true) { protected override int GetContentLengthCore(TalkReqMsg msg) => Rlp.LengthOf(msg.Protocol) + Rlp.LengthOf(msg.Request); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs index 7e6b433551d7..a33f8a9b292e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Serializers/TalkRespMsgSerializer.cs @@ -7,7 +7,7 @@ namespace Nethermind.Network.Discovery.Discv5.Serializers; -internal sealed class TalkRespMsgSerializer : MsgSerializerBase +internal sealed class TalkRespMsgSerializer() : MsgSerializerBase(MessageType.TalkResp, requiresOwnedMemory: true) { protected override int GetContentLengthCore(TalkRespMsg msg) => Rlp.LengthOf(msg.Response); diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs index c8b5f0d013c4..bf0882c2c40e 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecord.cs @@ -6,7 +6,6 @@ using Nethermind.Crypto; using Nethermind.Serialization.Rlp; using System.Net; -using Convert = System.Convert; namespace Nethermind.Network.Enr; @@ -25,17 +24,10 @@ public class NodeRecord private Signature? _signature; - private SortedDictionary Entries { get; } = new(System.StringComparer.Ordinal); + private SortedDictionary Entries { get; } = new(StringComparer.Ordinal); internal byte[]? OriginalRlp { get; set; } - /// - /// This field is used when this is deserialized and an unknown entry is encountered. - /// In such cases we do not know the RLP serialization format of such an entry and we store the original RLP - /// in order to be able to verify the signature. I think that we may replace it by Keccak(OriginalContentRlp). - /// - public byte[]? OriginalContentRlp { get; set; } - /// /// Represents the version / id / sequence of the node record data. It should be increased by one with each /// update to the node data. Setting sequence on this class wipes out and @@ -81,11 +73,6 @@ public Hash256 ContentHash private Hash256 CalculateContentHash() { - if (OriginalContentRlp is not null) - { - return ValueKeccak.Compute(OriginalContentRlp).ToCommitment(); - } - KeccakRlpStream rlpStream = new(); EncodeContent(rlpStream); return rlpStream.GetHash(); @@ -101,7 +88,6 @@ public Signature? Signature { _signature = value; OriginalRlp = null; - OriginalContentRlp = null; _enrString = null; _contentHash = null; } @@ -247,7 +233,6 @@ public void SetEntry(EnrContentEntry entry) Entries[entry.Key] = entry; OriginalRlp = null; - OriginalContentRlp = null; _enrString = null; _contentHash = null; _signature = null; diff --git a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs index 84aabffc54b5..51db861cba7a 100644 --- a/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs +++ b/src/Nethermind/Nethermind.Network.Enr/NodeRecordSigner.cs @@ -30,7 +30,6 @@ public void Sign(NodeRecord nodeRecord) } nodeRecord.OriginalRlp = null; - nodeRecord.OriginalContentRlp = null; nodeRecord.Signature = _ecdsa.Sign(_privateKey, in nodeRecord.ContentHash.ValueHash256); } @@ -60,13 +59,11 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) if (checkPosition - startPosition > 300) throw new RlpException("RLP received for ENR is bigger than 300 bytes"); NodeRecord nodeRecord = new(); - byte[]? originalContent = null; - byte[] previousKey = []; + ReadOnlySpan previousKey = default; ReadOnlySpan sigBytes = ctx.DecodeByteArraySpan(RlpLimit.L65); Signature signature = new(sigBytes, 0); - bool canVerify = true; bool hasV4Id = false; ulong enrSequence = ctx.DecodeULong(); while (ctx.Position < checkPosition) @@ -76,7 +73,7 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) { throw new RlpException("ENR keys must be sorted and unique."); } - previousKey = key.ToArray(); + previousKey = key; switch (key.Length) { @@ -141,7 +138,6 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) nodeRecord.SetEntry(new SecP256k1Entry(reportedKey)); break; default: - canVerify = false; int valueStart = ctx.Position; ctx.SkipItem(); int valueLength = ctx.Position - valueStart; @@ -160,24 +156,8 @@ public NodeRecord Deserialize(ref Rlp.ValueDecoderContext ctx) } int endPosition = ctx.Position; - if (!canVerify) - { - ctx.Position = startPosition; - ctx.ReadSequenceLength(); - ctx.SkipItem(); // signature - int noSigContentLength = endPosition - ctx.Position; - int noSigSequenceLength = Rlp.LengthOfSequence(noSigContentLength); - originalContent = new byte[noSigSequenceLength]; - RlpStream originalContentStream = new(originalContent); - originalContentStream.StartSequence(noSigContentLength); - originalContentStream.Write(ctx.Read(noSigContentLength)); - ctx.Position = endPosition; - originalContent = originalContentStream.Data.ToArray()!; - } - nodeRecord.EnrSequence = enrSequence; nodeRecord.Signature = signature; - nodeRecord.OriginalContentRlp = originalContent; nodeRecord.OriginalRlp = ctx.Data.Slice(startPosition, endPosition - startPosition).ToArray(); return nodeRecord; @@ -198,15 +178,7 @@ public bool Verify(NodeRecord nodeRecord) throw new Exception("Cannot verify an ENR with an empty signature."); } - ValueHash256 contentHash; - if (nodeRecord.OriginalContentRlp is not null) - { - contentHash = ValueKeccak.Compute(nodeRecord.OriginalContentRlp); - } - else - { - contentHash = nodeRecord.ContentHash; - } + ValueHash256 contentHash = nodeRecord.ContentHash; CompressedPublicKey? publicKeyA = _ecdsa.RecoverCompressedPublicKey(nodeRecord.Signature!, in contentHash); diff --git a/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs b/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs index b03276c646b6..a7320da252ad 100644 --- a/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/NodeFilterTests.cs @@ -134,18 +134,6 @@ public void ThreadSafety_ConcurrentSetCallsSameAddress() Assert.That(acceptedCount, Is.LessThan(threadCount * attemptsPerThread), "not all concurrent attempts should be accepted for the same address"); } - [TestCase("192.0.2.1", "10.0.0.1", 0, 0, Description = "IPv4 /0 prefix - all match")] - [TestCase("192.0.2.1", "192.0.2.1", 32, 128, Description = "IPv4 /32 prefix - exact match")] - [TestCase("2001:db8::1", "fe80::1", 0, 0, Description = "IPv6 /0 prefix - all match")] - [TestCase("2001:db8::1", "2001:db8::1", 32, 128, Description = "IPv6 /128 prefix - exact match")] - public void SubnetMasking_EdgeCase_PrefixBoundary(string addr1, string addr2, byte v4Prefix, byte v6Prefix) - { - NodeFilter.IpSubnetKey key1 = new(IPAddress.Parse(addr1), v4PrefixBits: v4Prefix, v6PrefixBits: v6Prefix); - NodeFilter.IpSubnetKey key2 = new(IPAddress.Parse(addr2), v4PrefixBits: v4Prefix, v6PrefixBits: v6Prefix); - - Assert.That(key1, Is.EqualTo(key2)); - } - [Test] public void CapacityBounded_EvictsOldEntries() { @@ -181,40 +169,6 @@ public void Create_WhenDisabled_ReturnsAcceptAll() Assert.That(filter, Is.SameAs(NodeFilter.AcceptAll)); } - [TestCase("192.0.2.1", "192.0.2.1", true, Description = "Same address - equal")] - [TestCase("192.0.2.1", "192.0.2.2", false, Description = "Different address - not equal")] - public void IpSubnetKey_Equality_Exact(string addr1, string addr2, bool expectEqual) - { - NodeFilter.IpSubnetKey key1 = NodeFilter.IpSubnetKey.Exact(IPAddress.Parse(addr1)); - NodeFilter.IpSubnetKey key2 = NodeFilter.IpSubnetKey.Exact(IPAddress.Parse(addr2)); - - if (expectEqual) - { - Assert.That(key1, Is.EqualTo(key2)); - Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); - } - else - { - Assert.That(key1, Is.Not.EqualTo(key2)); - } - } - - [TestCase("192.0.2.1", "192.0.2.50", true, Description = "Same /24 subnet matches")] - [TestCase("192.0.2.1", "192.0.3.1", false, Description = "Different /24 subnet does not match")] - public void IpSubnetKey_Matches(string bucketAddr, string testAddr, bool expected) - { - NodeFilter.IpSubnetKey key = NodeFilter.IpSubnetKey.Bucket(IPAddress.Parse(bucketAddr), v4PrefixBits: 24, v6PrefixBits: 64); - Assert.That(key.Matches(IPAddress.Parse(testAddr)), Is.EqualTo(expected)); - } - - [TestCase("192.0.2.1", "192.0.2.50", true, Description = "Same /24 IPv4")] - [TestCase("192.0.2.1", "192.0.3.1", false, Description = "Different /24 IPv4")] - [TestCase("2001:db8::1", "2001:db8::ffff", true, Description = "Same /64 IPv6")] - [TestCase("2001:db8::1", "2001:db8:0:1::1", false, Description = "Different /64 IPv6")] - public void IpSubnetKey_AreInSameSubnet(string a, string b, bool expected) => Assert.That(NodeFilter.IpSubnetKey.AreInSameSubnet( - IPAddress.Parse(a), IPAddress.Parse(b), - v4PrefixBits: 24, v6PrefixBits: 64), Is.EqualTo(expected)); - [TestCase("127.0.0.1", true, Description = "IPv4 loopback")] [TestCase("::1", true, Description = "IPv6 loopback")] [TestCase("10.0.0.1", true, Description = "RFC1918 10.x")] @@ -227,7 +181,7 @@ public void IpSubnetKey_AreInSameSubnet(string a, string b, bool expected) => As [TestCase("fe80::1", true, Description = "IPv6 link-local")] [TestCase("8.8.8.8", false, Description = "Public IPv4")] [TestCase("2001:4860:4860::8888", false, Description = "Public IPv6")] - public void IPAddressClassifier_IsLoopbackOrPrivateOrLinkLocal(string address, bool expected) => Assert.That(IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(IPAddress.Parse(address)), Is.EqualTo(expected)); + public void IPAddressExtensions_IsLoopbackOrPrivateOrLinkLocal(string address, bool expected) => Assert.That(IPAddress.Parse(address).IsLoopbackOrPrivateOrLinkLocal, Is.EqualTo(expected)); [TestCase("0.1.2.3", true, Description = "IPv4 this-network")] [TestCase("192.0.0.1", true, Description = "IPv4 IETF protocol assignments")] @@ -250,26 +204,31 @@ public void IpSubnetKey_AreInSameSubnet(string a, string b, bool expected) => As [TestCase("3fff::1", true, Description = "IPv6 documentation")] [TestCase("8.8.8.8", false, Description = "Public IPv4")] [TestCase("2001:4860:4860::8888", false, Description = "Public IPv6")] - public void IPAddressClassifier_IsSpecialUseAddress(string address, bool expected) => Assert.That(IPAddressClassifier.IsSpecialUseAddress(IPAddress.Parse(address)), Is.EqualTo(expected)); + public void IPAddressExtensions_IsSpecialUseAddress(string address, bool expected) => Assert.That(IPAddress.Parse(address).IsSpecialUseAddress, Is.EqualTo(expected)); [TestCase("224.0.0.1", true, Description = "IPv4 multicast")] [TestCase("ff02::1", true, Description = "IPv6 multicast")] [TestCase("8.8.8.8", false, Description = "Public IPv4")] [TestCase("2001:4860:4860::8888", false, Description = "Public IPv6")] - public void IPAddressClassifier_IsMulticast(string address, bool expected) => Assert.That(IPAddressClassifier.IsMulticast(IPAddress.Parse(address)), Is.EqualTo(expected)); + public void IPAddressExtensions_IsMulticast(string address, bool expected) => Assert.That(IPAddress.Parse(address).IsMulticast, Is.EqualTo(expected)); [TestCase("192.168.1.10", "192.168.1.20", "203.0.113.1", false, Description = "Private addresses use exact keying")] [TestCase("203.0.113.1", "203.0.113.50", "198.51.100.1", true, Description = "Public addresses in same /24 use subnet bucketing")] [TestCase("192.168.1.10", "192.168.1.20", "192.168.1.1", false, Description = "Same local subnet uses exact keying")] - public void CreateNodeFilterKey_KeyingBehavior(string remote1, string remote2, string current, bool expectEqual) + public void TryAccept_UsesExpectedKeyingBehavior(string remote1, string remote2, string current, bool expectEqual) { - NodeFilter.IpSubnetKey key1 = NodeFilter.IpSubnetKey.CreateNodeFilterKey(IPAddress.Parse(remote1), IPAddress.Parse(current)); - NodeFilter.IpSubnetKey key2 = NodeFilter.IpSubnetKey.CreateNodeFilterKey(IPAddress.Parse(remote2), IPAddress.Parse(current)); + NodeFilter filter = CreateFilter(currentIp: IPAddress.Parse(current)); if (expectEqual) - Assert.That(key1, Is.EqualTo(key2)); + { + Assert.That(filter.TryAccept(IPAddress.Parse(remote1)), Is.True); + Assert.That(filter.TryAccept(IPAddress.Parse(remote2)), Is.False); + } else - Assert.That(key1, Is.Not.EqualTo(key2)); + { + Assert.That(filter.TryAccept(IPAddress.Parse(remote1)), Is.True); + Assert.That(filter.TryAccept(IPAddress.Parse(remote2)), Is.True); + } } [TestCase(true, "192.0.2.1", "192.0.2.1", Description = "Exact match reaccepts after timeout")] diff --git a/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs b/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs index 7ba5fdc44d29..a3fc5e791478 100644 --- a/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs +++ b/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs @@ -23,7 +23,7 @@ class WebIPSource(string url, ILogManager logManager) : IIPSource string ip = httpClient.GetStringAsync(_url).Result.Trim(); if (_logger.IsDebug) _logger.Debug($"External ip: {ip}"); bool result = IPAddress.TryParse(ip, out IPAddress ipAddress); - bool isExternal = result && !Nethermind.Network.IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(ipAddress); + bool isExternal = result && !ipAddress.IsLoopbackOrPrivateOrLinkLocal; return Task.FromResult(isExternal ? (true, ipAddress) : (false, (IPAddress)null)); } catch (Exception e) diff --git a/src/Nethermind/Nethermind.Network/IPAddressExtensions.cs b/src/Nethermind/Nethermind.Network/IPAddressExtensions.cs new file mode 100644 index 000000000000..a9b8e09f3016 --- /dev/null +++ b/src/Nethermind/Nethermind.Network/IPAddressExtensions.cs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Net; + +namespace Nethermind.Network; + +/// +/// IP address classification helpers used by peer and discovery filtering. +/// +public static class IPAddressExtensions +{ + extension(IPAddress ipAddress) + { + /// + /// Returns true for loopback, private, link-local, CGNAT, and IPv6 ULA addresses. + /// + public bool IsLoopbackOrPrivateOrLinkLocal + => ParsedIPAddress.Parse(ipAddress).IsLoopbackOrPrivateOrLinkLocal; + + /// + /// Returns true for IPv4 or IPv6 multicast addresses. + /// + public bool IsMulticast + => ParsedIPAddress.Parse(ipAddress).IsMulticast; + + /// + /// Returns true for IPv4 multicast addresses. + /// + public bool IsIPv4Multicast + => ParsedIPAddress.Parse(ipAddress).IsIPv4Multicast; + + /// + /// Returns true for special-use addresses that should not be accepted as routable peers. + /// + /// + /// This intentionally does not include loopback, private, link-local, CGNAT, or IPv6 ULA ranges; + /// callers that support private deployments can decide whether to accept those separately. + /// + public bool IsSpecialUseAddress + => ParsedIPAddress.Parse(ipAddress).IsSpecialUseAddress; + } +} diff --git a/src/Nethermind/Nethermind.Network/NodeFilter.cs b/src/Nethermind/Nethermind.Network/NodeFilter.cs index c9c1da7a9e61..1dfd6cce8bdd 100644 --- a/src/Nethermind/Nethermind.Network/NodeFilter.cs +++ b/src/Nethermind/Nethermind.Network/NodeFilter.cs @@ -25,7 +25,7 @@ public sealed class NodeFilter private readonly ClockCache? _cache; private readonly bool _exactMatchOnly; - private readonly IpSubnetKey.ParsedIp? _parsedCurrentIp; + private readonly ParsedIPAddress? _parsedCurrentIp; private readonly long _timeoutMs; private NodeFilter() { } @@ -37,7 +37,7 @@ internal NodeFilter(int size, bool exactMatchOnly, IPAddress? currentIp, long ti { _cache = new(size); _exactMatchOnly = exactMatchOnly; - _parsedCurrentIp = currentIp is not null ? new IpSubnetKey.ParsedIp(currentIp) : null; + _parsedCurrentIp = currentIp is not null ? ParsedIPAddress.Parse(currentIp) : null; _timeoutMs = timeoutMs; } @@ -53,12 +53,6 @@ public static NodeFilter Create(int maxActivePeers, bool filterEnabled, bool sub public static NodeFilter CreateExact(int size, TimeSpan timeout) => new(size, exactMatchOnly: true, currentIp: null, (long)timeout.TotalMilliseconds); - public static bool IsLoopbackOrPrivateOrLinkLocal(IPAddress ipAddress) - => IPAddressClassifier.IsLoopbackOrPrivateOrLinkLocal(ipAddress); - - public static bool IsIPv4Multicast(IPAddress ipAddress) - => IPAddressClassifier.IsIPv4Multicast(ipAddress); - /// /// Checks whether should be accepted. /// Returns true if the address was not seen recently, false if it was. @@ -79,18 +73,6 @@ public bool TryAccept(IPAddress ipAddress, bool exactOnly = false) return true; } - /// - /// Read-only check: returns true if the address would be accepted (not seen recently), - /// without inserting it into the cache. - /// - public bool CanAccept(IPAddress ipAddress, bool exactOnly = false) - { - if (_cache is null) return true; - - IpSubnetKey key = GetKey(ipAddress, exactOnly); - return !_cache.TryGet(key, out long lastSeen) || Environment.TickCount64 - lastSeen >= _timeoutMs; - } - public void Touch(IPAddress ipAddress, bool exactOnly = false) { if (_cache is null) @@ -113,28 +95,8 @@ private IpSubnetKey GetKey(IPAddress ipAddress, bool exactOnly) /// Allocation-free key for an IP address or a masked subnet prefix, suitable for hash lookups and prefix checks. /// [StructLayout(LayoutKind.Explicit)] - internal readonly struct IpSubnetKey : IEquatable + private readonly struct IpSubnetKey : IEquatable { - internal enum IpFamily : byte { IPv4 = 4, IPv6 = 6 } - - internal readonly struct ParsedIp - { - public readonly IpFamily Family; - public readonly uint V4; - public readonly ulong Hi; - public readonly ulong Lo; - public readonly bool IsLocal; - - public ParsedIp(IPAddress ip) - { - Family = ReadAddress(ip, out uint v4, out ulong hi, out ulong lo); - V4 = v4; - Hi = hi; - Lo = lo; - IsLocal = IsLoopbackOrPrivateOrLinkLocal(Family, v4, hi, lo); - } - } - // For IPv6: _hi/_lo are the masked 128-bit network prefix (big-endian). // For IPv4: _hi holds the masked v4 in the low 32 bits (big-endian), _lo is 0. [FieldOffset(0)] @@ -147,44 +109,23 @@ public ParsedIp(IPAddress ip) public static IpSubnetKey DefaultKey(IPAddress ipAddress, byte v4BucketPrefixBits = 24, byte v6BucketPrefixBits = 64) { - IpFamily family = ReadAddress(ipAddress, out uint v4, out ulong hi, out ulong lo); + ParsedIPAddress parsed = ParsedIPAddress.Parse(ipAddress); - if (IsLoopbackOrPrivateOrLinkLocal(family, v4, hi, lo)) + if (parsed.IsLoopbackOrPrivateOrLinkLocal) { v4BucketPrefixBits = 32; v6BucketPrefixBits = 128; } - return family == IpFamily.IPv4 - ? CreateFromV4(v4, v4BucketPrefixBits) - : CreateFromV6(hi, lo, v6BucketPrefixBits); - } - - public IpSubnetKey(IPAddress ipAddress, byte v4PrefixBits = 24, byte v6PrefixBits = 64) - { - IpFamily family = ReadAddress(ipAddress, out uint v4, out ulong hi, out ulong lo); - - if (family == IpFamily.IPv4) - { - _meta = MakeMeta(IpFamily.IPv4, v4PrefixBits); - _hi = MaskV4(v4, v4PrefixBits); - _lo = 0; - return; - } - - _meta = MakeMeta(IpFamily.IPv6, v6PrefixBits); - MaskV6(ref hi, ref lo, v6PrefixBits); - _hi = hi; - _lo = lo; + return CreateFromParsed(in parsed, v4BucketPrefixBits, v6BucketPrefixBits); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IpSubnetKey Exact(IPAddress ipAddress) - => new(ipAddress, v4PrefixBits: 32, v6PrefixBits: 128); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IpSubnetKey Bucket(IPAddress ipAddress, byte v4PrefixBits = 24, byte v6PrefixBits = 64) - => new(ipAddress, v4PrefixBits, v6PrefixBits); + { + ParsedIPAddress parsed = ParsedIPAddress.Parse(ipAddress); + return CreateExactFromParsed(in parsed); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(IpSubnetKey other) @@ -202,41 +143,9 @@ public override bool Equals(object? obj) public override int GetHashCode() => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in _hi)), 2 * sizeof(ulong) + sizeof(ushort)).FastHash(); - public bool Matches(IPAddress other) - { - IpFamily family = (IpFamily)(_meta >> 8); - byte prefix = (byte)_meta; - - IpFamily otherFamily = ReadAddress(other, out uint v4, out ulong hi, out ulong lo); - if (otherFamily != family) - return false; - - if (family == IpFamily.IPv4) - return _hi == MaskV4Trusted(v4, prefix); - - MaskV6Trusted(ref hi, ref lo, prefix); - return _hi == hi && _lo == lo; - } - - public static bool AreInSameSubnet(IPAddress a, IPAddress b, byte v4PrefixBits = 24, byte v6PrefixBits = 64) - { - IpFamily fa = ReadAddress(a, out uint a4, out ulong aHi, out ulong aLo); - IpFamily fb = ReadAddress(b, out uint b4, out ulong bHi, out ulong bLo); - - if (fa != fb) - return false; - - if (fa == IpFamily.IPv4) - return MaskV4(a4, v4PrefixBits) == MaskV4(b4, v4PrefixBits); - - MaskV6(ref aHi, ref aLo, v6PrefixBits); - MaskV6(ref bHi, ref bLo, v6PrefixBits); - return aHi == bHi && aLo == bLo; - } - public static IpSubnetKey CreateNodeFilterKey( IPAddress remoteIp, - IPAddress currentIp, + in ParsedIPAddress currentIp, byte v4BucketPrefixBits = 24, byte v6BucketPrefixBits = 64, byte v4LocalPrefixBits = 24, @@ -244,67 +153,48 @@ public static IpSubnetKey CreateNodeFilterKey( bool exactIfSameSubnetAsCurrentIp = true, bool requireCurrentIpIsLocalForExact = true) { - ParsedIp current = new(currentIp); - return CreateNodeFilterKey(remoteIp, in current, - v4BucketPrefixBits, v6BucketPrefixBits, - v4LocalPrefixBits, v6LocalPrefixBits, - exactIfSameSubnetAsCurrentIp, requireCurrentIpIsLocalForExact); - } + ParsedIPAddress remote = ParsedIPAddress.Parse(remoteIp); - public static IpSubnetKey CreateNodeFilterKey( - IPAddress remoteIp, - in ParsedIp currentIp, - byte v4BucketPrefixBits = 24, - byte v6BucketPrefixBits = 64, - byte v4LocalPrefixBits = 24, - byte v6LocalPrefixBits = 64, - bool exactIfSameSubnetAsCurrentIp = true, - bool requireCurrentIpIsLocalForExact = true) - { - IpFamily rFamily = ReadAddress(remoteIp, out uint rV4, out ulong rHi, out ulong rLo); - - if (IsLoopbackOrPrivateOrLinkLocal(rFamily, rV4, rHi, rLo)) - return CreateExactFromParsed(rFamily, rV4, rHi, rLo); + if (remote.IsLoopbackOrPrivateOrLinkLocal) + return CreateExactFromParsed(in remote); if (exactIfSameSubnetAsCurrentIp) { - if (!requireCurrentIpIsLocalForExact || currentIp.IsLocal) + if (!requireCurrentIpIsLocalForExact || currentIp.IsLoopbackOrPrivateOrLinkLocal) { - if (rFamily == currentIp.Family) + if (remote.Family == currentIp.Family) { - if (rFamily == IpFamily.IPv4) + if (remote.Family == IpFamily.IPv4) { - if (MaskV4(rV4, v4LocalPrefixBits) == MaskV4(currentIp.V4, v4LocalPrefixBits)) - return CreateExactFromParsed(rFamily, rV4, rHi, rLo); + if (MaskV4(remote.V4, v4LocalPrefixBits) == MaskV4(currentIp.V4, v4LocalPrefixBits)) + return CreateExactFromParsed(in remote); } else { - ulong rNetHi = rHi, rNetLo = rLo; + ulong rNetHi = remote.Hi, rNetLo = remote.Lo; ulong cNetHi = currentIp.Hi, cNetLo = currentIp.Lo; MaskV6(ref rNetHi, ref rNetLo, v6LocalPrefixBits); MaskV6(ref cNetHi, ref cNetLo, v6LocalPrefixBits); if (rNetHi == cNetHi && rNetLo == cNetLo) - return CreateExactFromParsed(rFamily, rV4, rHi, rLo); + return CreateExactFromParsed(in remote); } } } } - return rFamily == IpFamily.IPv4 - ? CreateFromV4(rV4, v4BucketPrefixBits) - : CreateFromV6(rHi, rLo, v6BucketPrefixBits); + return CreateFromParsed(in remote, v4BucketPrefixBits, v6BucketPrefixBits); } - public static bool IsLoopbackOrPrivateOrLinkLocal(IPAddress ip) - { - IpFamily family = ReadAddress(ip, out uint v4, out ulong hi, out ulong lo); - return IsLoopbackOrPrivateOrLinkLocal(family, v4, hi, lo); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IpSubnetKey CreateExactFromParsed(in ParsedIPAddress parsed) + => CreateFromParsed(in parsed, 32, 128); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static IpSubnetKey CreateExactFromParsed(IpFamily family, uint v4, ulong hi, ulong lo) - => family == IpFamily.IPv4 ? CreateFromV4(v4, 32) : CreateFromV6(hi, lo, 128); + private static IpSubnetKey CreateFromParsed(in ParsedIPAddress parsed, byte v4PrefixBits, byte v6PrefixBits) + => parsed.Family == IpFamily.IPv4 + ? CreateFromV4(parsed.V4, v4PrefixBits) + : CreateFromV6(parsed.Hi, parsed.Lo, v6PrefixBits); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IpSubnetKey CreateFromV4(uint v4, byte prefixBits) @@ -329,16 +219,6 @@ private IpSubnetKey(ulong hi, ulong lo, ushort meta) private static ushort MakeMeta(IpFamily family, byte prefixBits) => (ushort)(((byte)family << 8) | prefixBits); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static IpFamily ReadAddress(IPAddress ip, out uint v4, out ulong hi, out ulong lo) - { - IPAddressClassifier.ParsedIPAddress parsed = IPAddressClassifier.Parse(ip); - v4 = parsed.V4; - hi = parsed.Hi; - lo = parsed.Lo; - return parsed.Family == IPAddressClassifier.ParsedIPAddressFamily.IPv4 ? IpFamily.IPv4 : IpFamily.IPv6; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong MaskV4(uint v4, byte prefixBits) { @@ -379,11 +259,5 @@ private static void MaskV6Trusted(ref ulong hi, ref ulong lo, byte prefixBits) return; } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsLoopbackOrPrivateOrLinkLocal(IpFamily family, uint v4, ulong hi, ulong lo) - => family == IpFamily.IPv4 - ? IPAddressClassifier.IsIPv4LoopbackOrPrivateOrLinkLocal(v4) - : IPAddressClassifier.IsIPv6LoopbackOrPrivateOrLinkLocal(hi, lo); } } diff --git a/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs b/src/Nethermind/Nethermind.Network/ParsedIPAddress.cs similarity index 65% rename from src/Nethermind/Nethermind.Network/IPAddressClassifier.cs rename to src/Nethermind/Nethermind.Network/ParsedIPAddress.cs index 4a710a5e7df7..d55176a78d77 100644 --- a/src/Nethermind/Nethermind.Network/IPAddressClassifier.cs +++ b/src/Nethermind/Nethermind.Network/ParsedIPAddress.cs @@ -8,67 +8,19 @@ namespace Nethermind.Network; -/// -/// Classifies IP addresses into reusable networking categories used by peer and discovery filtering. -/// -public static class IPAddressClassifier -{ - internal enum ParsedIPAddressFamily : byte { IPv4 = 4, IPv6 = 6 } - - internal readonly struct ParsedIPAddress(ParsedIPAddressFamily family, uint v4, ulong hi, ulong lo) - { - public readonly ParsedIPAddressFamily Family = family; - public readonly uint V4 = v4; - public readonly ulong Hi = hi; - public readonly ulong Lo = lo; - } +internal enum IpFamily : byte { IPv4 = 4, IPv6 = 6 } - /// - /// Returns true for loopback, private, link-local, CGNAT, and IPv6 ULA addresses. - /// - public static bool IsLoopbackOrPrivateOrLinkLocal(IPAddress ipAddress) - { - ParsedIPAddress parsed = Parse(ipAddress); - return IsLoopbackOrPrivateOrLinkLocal(in parsed); - } - - /// - /// Returns true for IPv4 or IPv6 multicast addresses. - /// - public static bool IsMulticast(IPAddress ipAddress) - { - ParsedIPAddress parsed = Parse(ipAddress); - return parsed.Family == ParsedIPAddressFamily.IPv4 - ? IsIPv4Multicast(parsed.V4) - : IsIPv6Multicast(parsed.Hi); - } - - /// - /// Returns true for IPv4 multicast addresses. - /// - public static bool IsIPv4Multicast(IPAddress ipAddress) - { - ParsedIPAddress parsed = Parse(ipAddress); - return parsed.Family == ParsedIPAddressFamily.IPv4 && IsIPv4Multicast(parsed.V4); - } - - /// - /// Returns true for special-use addresses that should not be accepted as routable peers. - /// - /// - /// This intentionally does not include loopback, private, link-local, CGNAT, or IPv6 ULA ranges; - /// callers that support private deployments can decide whether to accept those separately. - /// - public static bool IsSpecialUseAddress(IPAddress ipAddress) - { - ParsedIPAddress parsed = Parse(ipAddress); - return parsed.Family == ParsedIPAddressFamily.IPv4 - ? IsIPv4SpecialUseAddress(parsed.V4) - : IsIPv6SpecialUseAddress(parsed.Hi, parsed.Lo); - } +internal readonly struct ParsedIPAddress(IpFamily family, uint v4, ulong hi, ulong lo) +{ + public readonly IpFamily Family = family; + public readonly uint V4 = v4; + public readonly ulong Hi = hi; + public readonly ulong Lo = lo; internal static ParsedIPAddress Parse(IPAddress ipAddress) { + ArgumentNullException.ThrowIfNull(ipAddress); + Span bytes = stackalloc byte[16]; if (!ipAddress.TryWriteBytes(bytes, out int written)) { @@ -79,7 +31,7 @@ internal static ParsedIPAddress Parse(IPAddress ipAddress) { case 4: return new ParsedIPAddress( - ParsedIPAddressFamily.IPv4, + IpFamily.IPv4, BinaryPrimitives.ReadUInt32BigEndian(bytes), hi: 0, lo: 0); @@ -87,14 +39,13 @@ internal static ParsedIPAddress Parse(IPAddress ipAddress) { ulong hi = BinaryPrimitives.ReadUInt64BigEndian(bytes); - // Fast-path IPv4-mapped IPv6 (::ffff:a.b.c.d) - treat as IPv4. if (hi == 0) { uint mid = BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(8, 4)); if (mid == 0x0000_FFFFu) { return new ParsedIPAddress( - ParsedIPAddressFamily.IPv4, + IpFamily.IPv4, BinaryPrimitives.ReadUInt32BigEndian(bytes.Slice(12, 4)), hi: 0, lo: 0); @@ -102,7 +53,7 @@ internal static ParsedIPAddress Parse(IPAddress ipAddress) } return new ParsedIPAddress( - ParsedIPAddressFamily.IPv6, + IpFamily.IPv6, v4: 0, hi, BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8))); @@ -112,14 +63,38 @@ internal static ParsedIPAddress Parse(IPAddress ipAddress) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsLoopbackOrPrivateOrLinkLocal(in ParsedIPAddress parsed) - => parsed.Family == ParsedIPAddressFamily.IPv4 - ? IsIPv4LoopbackOrPrivateOrLinkLocal(parsed.V4) - : IsIPv6LoopbackOrPrivateOrLinkLocal(parsed.Hi, parsed.Lo); + public bool IsLoopbackOrPrivateOrLinkLocal + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Family == IpFamily.IPv4 + ? IsIPv4LoopbackOrPrivateOrLinkLocal(V4) + : IsIPv6LoopbackOrPrivateOrLinkLocal(Hi, Lo); + } + + public bool IsMulticast + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Family == IpFamily.IPv4 + ? IsIPv4MulticastAddress(V4) + : IsIPv6Multicast(Hi); + } + + public bool IsIPv4Multicast + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Family == IpFamily.IPv4 && IsIPv4MulticastAddress(V4); + } + + public bool IsSpecialUseAddress + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Family == IpFamily.IPv4 + ? IsIPv4SpecialUseAddress(V4) + : IsIPv6SpecialUseAddress(Hi, Lo); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsIPv4Multicast(uint v4) + internal static bool IsIPv4MulticastAddress(uint v4) => (byte)(v4 >> 24) is >= 224 and <= 239; [MethodImpl(MethodImplOptions.AggressiveInlining)] From dc729689887bfcf61cbfb7c31c79b3ba96027499 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 19 Jun 2026 20:25:06 +0300 Subject: [PATCH 175/182] Refine LRU eviction --- .../Caching/LruCacheTests.cs | 61 ++++++++++++++++--- .../Caching/DisposingLruCache.cs | 23 +++++++ .../Nethermind.Core/Caching/LruCache.cs | 48 +++++++-------- .../Discv5/Kademlia/KademliaAdapter.cs | 4 +- 4 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 src/Nethermind/Nethermind.Core/Caching/DisposingLruCache.cs diff --git a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs index 737a79f7d0c9..3bb04e45acaa 100755 --- a/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Caching/LruCacheTests.cs @@ -248,10 +248,10 @@ public void Can_remove_and_return_value() } [Test] - public void Eviction_callback_is_called_when_capacity_replaces_oldest() + public void Evict_is_called_when_capacity_replaces_oldest() { int evicted = 0; - LruCache cache = new(2, "test", value => evicted = value); + LruCache cache = new TestEvictingLruCache(2, "test", value => evicted = value); cache.Set(1, 10); cache.Set(2, 20); @@ -261,10 +261,10 @@ public void Eviction_callback_is_called_when_capacity_replaces_oldest() } [Test] - public void Eviction_callback_is_called_when_existing_value_is_replaced() + public void Evict_is_called_when_existing_value_is_replaced() { int evicted = 0; - LruCache cache = new(2, "test", value => evicted = value); + LruCache cache = new TestEvictingLruCache(2, "test", value => evicted = value); cache.Set(1, 10); cache.Set(1, 11); @@ -274,10 +274,10 @@ public void Eviction_callback_is_called_when_existing_value_is_replaced() } [Test] - public void TryRemove_returns_value_without_calling_eviction_callback() + public void TryRemove_returns_value_without_calling_evict() { int evicted = 0; - LruCache cache = new(2, "test", value => evicted = value); + LruCache cache = new TestEvictingLruCache(2, "test", value => evicted = value); cache.Set(1, 10); Assert.That(cache.TryRemove(1, out int removed), Is.True); @@ -286,6 +286,31 @@ public void TryRemove_returns_value_without_calling_eviction_callback() Assert.That(evicted, Is.Zero); } + [Test] + public void Disposing_cache_disposes_evicted_values() + { + DisposingLruCache cache = new(1, "test"); + DisposableValue evicted = new(); + + cache.Set(1, evicted); + cache.Set(2, new DisposableValue()); + + Assert.That(evicted.IsDisposed, Is.True); + } + + [Test] + public void Disposing_cache_try_remove_transfers_ownership() + { + DisposingLruCache cache = new(1, "test"); + DisposableValue removed = new(); + cache.Set(1, removed); + + Assert.That(cache.TryRemove(1, out DisposableValue? actual), Is.True); + + Assert.That(actual, Is.SameAs(removed)); + Assert.That(removed.IsDisposed, Is.False); + } + [Test] public void Clear_should_free_all_capacity() { @@ -316,11 +341,11 @@ public void Clear_should_free_all_capacity() [TestCase(EvictionOperation.ReplaceExisting, true)] [TestCase(EvictionOperation.ReplaceOldest, false)] [TestCase(EvictionOperation.Clear, false)] - public async Task Eviction_callback_is_invoked_outside_lock(EvictionOperation operation, bool expectedContainsResult) + public async Task Evict_is_invoked_outside_lock(EvictionOperation operation, bool expectedContainsResult) { LruCache cache = null!; - TaskCompletionSource callbackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); - cache = new LruCache(2, "test", _ => callbackResult.SetResult(cache.Contains(1))); + TaskCompletionSource evictResult = new(TaskCreationOptions.RunContinuationsAsynchronously); + cache = new TestEvictingLruCache(2, "test", _ => evictResult.SetResult(cache.Contains(1))); cache.Set(1, 10); if (operation == EvictionOperation.ReplaceOldest) { @@ -332,7 +357,7 @@ public async Task Eviction_callback_is_invoked_outside_lock(EvictionOperation op Assert.That(completedTask, Is.SameAs(operationTask)); await operationTask; - Assert.That(await callbackResult.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.EqualTo(expectedContainsResult)); + Assert.That(await evictResult.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.EqualTo(expectedContainsResult)); } [Test] @@ -404,5 +429,21 @@ public enum EvictionOperation ReplaceOldest, Clear } + + private sealed class TestEvictingLruCache( + int maxCapacity, + string name, + Action evict) : LruCache(maxCapacity, name) + where TKey : notnull + { + protected override void Evict(TValue value) => evict(value); + } + + private sealed class DisposableValue : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() => IsDisposed = true; + } } } diff --git a/src/Nethermind/Nethermind.Core/Caching/DisposingLruCache.cs b/src/Nethermind/Nethermind.Core/Caching/DisposingLruCache.cs new file mode 100644 index 000000000000..9cc55123f2bb --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Caching/DisposingLruCache.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; + +namespace Nethermind.Core.Caching; + +public sealed class DisposingLruCache : LruCache + where TKey : notnull + where TValue : IDisposable +{ + public DisposingLruCache(int maxCapacity, int startCapacity, string name) + : base(maxCapacity, startCapacity, name) + { + } + + public DisposingLruCache(int maxCapacity, string name) + : base(maxCapacity, name) + { + } + + protected override void Evict(TValue value) => value.Dispose(); +} diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 6d6062b28fe3..1b9b16d1ad0a 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -10,38 +10,45 @@ namespace Nethermind.Core.Caching { - public sealed class LruCache : ICache where TKey : notnull + public class LruCache : ICache where TKey : notnull { private readonly int _maxCapacity; private readonly Dictionary> _cacheMap; private readonly McsLock _lock = new(); private readonly string _name; - private readonly Action? _onEvict; private LinkedListNode? _leastRecentlyUsed; - public LruCache(int maxCapacity, int startCapacity, string name, Action? onEvict = null) + public LruCache(int maxCapacity, int startCapacity, string name) { ArgumentOutOfRangeException.ThrowIfLessThan(maxCapacity, 1); _name = name; _maxCapacity = maxCapacity; - _onEvict = onEvict; _cacheMap = typeof(TKey) == typeof(byte[]) ? new Dictionary>((IEqualityComparer)Bytes.EqualityComparer) : new Dictionary>(startCapacity); // do not initialize it at the full capacity } - public LruCache(int maxCapacity, string name, Action? onEvict = null) - : this(maxCapacity, 0, name, onEvict) + public LruCache(int maxCapacity, string name) + : this(maxCapacity, 0, name) { } public void Clear() { - TValue[]? evictedValues; + TValue[]? evictedValues = null; using (McsLock.Disposable lockRelease = _lock.Acquire()) { - evictedValues = GetEvictedValues(); + if (_cacheMap.Count != 0) + { + int i = 0; + evictedValues = new TValue[_cacheMap.Count]; + foreach (KeyValuePair> kvp in _cacheMap) + { + evictedValues[i++] = kvp.Value.Value.Value; + } + } + _leastRecentlyUsed = null; _cacheMap.Clear(); } @@ -264,6 +271,10 @@ public TValue[] GetValues() public int Count => _cacheMap.Count; + protected virtual void Evict(TValue value) + { + } + private TValue Replace(TKey key, TValue value) { LinkedListNode? node = _leastRecentlyUsed; @@ -285,23 +296,6 @@ private TValue Replace(TKey key, TValue value) $"{nameof(LruCache)} called {nameof(Replace)} when empty."); } - private TValue[]? GetEvictedValues() - { - if (_onEvict is null || _cacheMap.Count == 0) - { - return null; - } - - int i = 0; - TValue[] evictedValues = new TValue[_cacheMap.Count]; - foreach (KeyValuePair> kvp in _cacheMap) - { - evictedValues[i++] = kvp.Value.Value.Value; - } - - return evictedValues; - } - private void NotifyEvictedValues(TValue[]? evictedValues) { if (evictedValues is null) @@ -317,9 +311,9 @@ private void NotifyEvictedValues(TValue[]? evictedValues) private void NotifyEvicted(TValue value) { - if (_onEvict is not null && value is not null) + if (value is not null) { - _onEvict(value); + Evict(value); } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 32ee6da31a75..16b0a6928b46 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -52,8 +52,8 @@ public sealed class KademliaAdapter( private readonly TimeSpan _findNodeTimeout = TimeSpan.FromMilliseconds(discoveryConfig.SendNodeTimeout); private readonly IKademliaDistance _distance = distance; private readonly ILogger _logger = logManager.GetClassLogger(); - private readonly LruCache _sessions = new(MaxSessions, "discv5 sessions", static session => session.Dispose()); - private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges", static sentChallenge => sentChallenge.Dispose()); + private readonly DisposingLruCache _sessions = new(MaxSessions, "discv5 sessions"); + private readonly DisposingLruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); private readonly Queue _sentChallengeExpiries = new(); private readonly Lock _sentChallengeExpiriesLock = new(); private long _lastSentChallengeTrimMilliseconds; From 44d7ff64fde45aa8a29628eb84767b5af4d35203 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Fri, 19 Jun 2026 20:49:40 +0300 Subject: [PATCH 176/182] Deduplicate ENR signer tests --- .../NodeRecordSignerTests.cs | 236 ++++++++++++------ 1 file changed, 165 insertions(+), 71 deletions(-) diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index 5f2db9d6817d..65dd6580ba5f 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -15,10 +15,7 @@ namespace Nethermind.Network.Enr.Test; public class NodeRecordSignerTests { - [SetUp] - public void Setup() - { - } + private const string TestPrivateKey = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"; [Test(Description = "https://eips.ethereum.org/EIPS/eip-778")] public void Is_correct_on_eip_test_vector() @@ -40,7 +37,7 @@ public void Is_correct_on_eip_test_vector() Console.WriteLine("expected: " + expectedHexString); Ecdsa ecdsa = new(); - PrivateKey privateKey = new("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + PrivateKey privateKey = new(TestPrivateKey); NodeRecordSigner signer = new(ecdsa, privateKey); NodeRecord nodeRecord = new(); nodeRecord.SetEntry(new IpEntry( @@ -76,7 +73,7 @@ public void Can_verify_signature() Console.WriteLine("expected: " + expectedHexString); Ecdsa ecdsa = new(); - PrivateKey privateKey = new("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + PrivateKey privateKey = new(TestPrivateKey); NodeRecordSigner signer = new(ecdsa, privateKey); NodeRecord nodeRecord = new(); @@ -97,38 +94,11 @@ public void Can_verify_signature() Assert.That(signer.Verify(nodeRecord), Is.True); } - [TestCase] - public void Throws_when_record_is_t() - { - Ecdsa ecdsa = new(); - PrivateKey privateKey = new("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); - NodeRecordSigner signer = new(ecdsa, privateKey); - Span bytes = Bytes.FromHexString("540b38f8b160f23b1cd30972338a09ba4a296e2f0cb63f76ce0b38201a8dd9aa2a9c306370904877ddab397f7845ff67ea0a1dbf094b86794bb5d739e6bda891a486098717e2fb744e04c4665d307a590c6e4141a3805de15eb1eb62b0c6ff0aa75db9559545e294e158b7dc9e4a118cf0c2c6259af2df7c1742731064df376182b2df2e714df9e87ec6492effb4de8e2a92bdb405bbe3d8ddf96622bbcb11592fdb2600356cb39fd2c36cac66e19cd1b136ac3be993ef0ed07905d95f16cc67cfbe9bc7c180b90023d55d9218bef9e052c9f655a5c2464abe24271cc1dc2f3df7d3abd926f4657b724b0435868a09f7136ec115cbc3ec1c675972315e4cc140907e4772c118d51917b16a00a7809cfa767ea3ae5557c0b972c37f77d85062910e3e15ae4613cac178220deadc6d729da20c85166e8532d8f88cd246e6102f5268cd5e29796d06713d0f684e096e5edfca6b6c7adf9e51e10f5140d92216123eb31984a61d5a9caf904a2e12f3f479b27d75aeafe0d35b8995468aa12ba7d8f17fbb0aeea63b4d2c74e43b60e06a62bed5ee3ae34f5d74465087b5932865a2cb41f1fdaa9b2b9143fe1923d7f0e4b18a3139ee469df8e6cfea46101674e5fde4c84f9f9d77dee3d0545897a69d9eb42ccc48b699baa9d932dc36783da3580a78abc68b20a1f8bda90afb5ed78a9ac46e63792182b7669e4daaf3ca7e9b5690a3bbf0a184b14470f899582d4a0423897a295441b4bf27db3d2e8adf41824538942198a064bc489fd0936e11f5266146432a8efc992e1d304a4ab6bf661fa1ab3b59d1f14155c5e6a8d1e9eed717bee86a9b6bdabde638c0d1"); - RlpStream rlpStream = new(600); - rlpStream.StartSequence(500); - rlpStream.Encode(bytes[..500]); - rlpStream.Position = 0; - Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); - } - - [Test] - public void Throws_when_encoded_record_is_bigger_than_300_bytes() + [TestCaseSource(nameof(InvalidRecordRlpCases))] + public void Throws_when_record_is_invalid(Func createRecord, Type exceptionType) { NodeRecordSigner signer = new(new Ecdsa()); - byte[] filler = FindFillerForOversizedEncodedRecord(); - RlpStream rlpStream = CreateRecord( - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), - ("z", stream => stream.Encode(filler), Rlp.LengthOf(filler))); - - Assert.That(rlpStream.Data.Length, Is.GreaterThan(300)); - Assert.That(() => signer.Deserialize(rlpStream), Throws.TypeOf()); - } - - [TestCaseSource(nameof(InvalidRecordCases))] - public void Throws_when_record_is_invalid(Func createRecord) - { - NodeRecordSigner signer = new(new Ecdsa()); - Assert.That(() => signer.Deserialize(createRecord()), Throws.TypeOf()); + Assert.That(() => signer.Deserialize(createRecord()), Throws.TypeOf(exceptionType)); } [TestCase("f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f")] @@ -137,7 +107,7 @@ public void Throws_when_record_is_invalid(Func createRecord) public void Can_deserialize_and_verify_real_world_cases(string testCase) { Ecdsa ecdsa = new(); - PrivateKey privateKey = new("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); + PrivateKey privateKey = new(TestPrivateKey); NodeRecordSigner signer = new(ecdsa, privateKey); RlpStream rlpStream = Bytes.FromHexString(testCase).AsRlpStream(); NodeRecord nodeRecord = signer.Deserialize(rlpStream); @@ -148,39 +118,150 @@ public void Can_deserialize_and_verify_real_world_cases(string testCase) Assert.That(nodeRecord.ToRlpBytes(), Is.EqualTo(Bytes.FromHexString(testCase))); } + [TestCaseSource(nameof(InvalidRecordByteCases))] + public void FromBytes_throws_when_record_bytes_are_invalid(Func createRecordBytes) + => Assert.That(() => NodeRecord.FromBytes(createRecordBytes()), Throws.TypeOf()); + [Test] - public void FromBytes_throws_when_record_has_trailing_bytes() + public void Cannot_verify_when_signature_missing() { - byte[] recordBytes = Bytes.FromHexString( - "f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f"); - byte[] recordWithTrailingBytes = [.. recordBytes, 0x80]; + PrivateKey privateKey = new(TestPrivateKey); + NodeRecordSigner signer = new(new Ecdsa(), privateKey); + NodeRecord nodeRecord = new(); + Assert.Throws(() => _ = signer.Verify(nodeRecord)); + } + + private static RlpStream CreateRecord(params (string Key, Action EncodeValue, int ValueLength)[] entries) + { + byte[] signature = new byte[64]; + int contentLength = Rlp.LengthOf(signature) + Rlp.LengthOf(1UL); + foreach ((string key, _, int valueLength) in entries) + { + contentLength += Rlp.LengthOf(key) + valueLength; + } + + RlpStream rlpStream = new(Rlp.LengthOfSequence(contentLength)); + rlpStream.StartSequence(contentLength); + rlpStream.Encode(signature); + rlpStream.Encode(1UL); + foreach ((string key, Action encodeValue, _) in entries) + { + rlpStream.Encode(key); + encodeValue(rlpStream); + } + + rlpStream.Position = 0; + return rlpStream; + } - Assert.That(() => NodeRecord.FromBytes(recordWithTrailingBytes), Throws.TypeOf()); + private static IEnumerable InvalidRecordRlpCases() + { + yield return InvalidRecordCase( + CreateNonSequenceRecord, + typeof(RlpException), + "Throws_when_record_is_not_a_sequence"); + + yield return InvalidRecordCase( + CreateRecordWithDeclaredLengthOverLimit, + typeof(RlpException), + "Throws_when_declared_record_payload_is_bigger_than_300_bytes"); + + yield return InvalidRecordCase( + CreateEncodedRecordOverLimit, + typeof(RlpException), + "Throws_when_encoded_record_is_bigger_than_300_bytes"); + + yield return InvalidRecordCase( + CreateRecordWithOversizedSignature, + typeof(RlpLimitException), + "Throws_when_signature_is_too_long"); + + yield return InvalidRecordCase( + static () => CreateRecord( + (EnrContentKey.Udp, static stream => stream.Encode(30303), Rlp.LengthOf(30303)), + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))), + typeof(RlpException), + "Throws_when_keys_are_not_sorted"); + + yield return InvalidRecordCase( + static () => CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))), + typeof(RlpException), + "Throws_when_keys_are_duplicated"); + + yield return InvalidRecordCase( + static () => CreateRecord( + ("z", static stream => stream.Encode(Array.Empty()), Rlp.LengthOf(Array.Empty()))), + typeof(RlpException), + "Throws_when_id_is_missing"); + + yield return InvalidRecordCase( + static () => CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode(string.Empty), Rlp.LengthOf(string.Empty))), + typeof(RlpException), + "Throws_when_id_is_empty"); + + yield return InvalidRecordCase( + static () => CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("V4"), Rlp.LengthOf("V4"))), + typeof(RlpException), + "Throws_when_id_has_wrong_case"); + + yield return InvalidRecordCase( + static () => CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v5"), Rlp.LengthOf("v5"))), + typeof(RlpException), + "Throws_when_id_is_not_v4"); } - [Test] - public void FromBytes_throws_rlp_exception_when_signature_cannot_recover() + private static IEnumerable InvalidRecordByteCases() { - byte[] publicKey = new byte[CompressedPublicKey.LengthInBytes]; - RlpStream rlpStream = CreateRecord( - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), - (EnrContentKey.SecP256k1, stream => stream.Encode(publicKey), Rlp.LengthOf(publicKey))); + yield return new TestCaseData((Func)CreateRecordWithTrailingBytes) + .SetName("FromBytes_throws_when_record_has_trailing_bytes"); + + yield return new TestCaseData((Func)CreateRecordWithUnrecoverableSignature) + .SetName("FromBytes_throws_when_signature_cannot_recover"); - Assert.That(() => NodeRecord.FromBytes(rlpStream.Data), Throws.TypeOf()); + yield return new TestCaseData((Func)CreateRecordWithInvalidSignature) + .SetName("FromBytes_throws_when_signature_does_not_match_public_key"); } - [Test] - public void Cannot_verify_when_signature_missing() + private static RlpStream CreateNonSequenceRecord() { - PrivateKey privateKey = new("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); - NodeRecordSigner signer = new(new Ecdsa(), privateKey); - NodeRecord nodeRecord = new(); - Assert.Throws(() => _ = signer.Verify(nodeRecord)); + RlpStream rlpStream = new(Rlp.LengthOf(EnrContentKey.Id)); + rlpStream.Encode(EnrContentKey.Id); + rlpStream.Position = 0; + return rlpStream; } - private static RlpStream CreateRecord(params (string Key, Action EncodeValue, int ValueLength)[] entries) + private static RlpStream CreateRecordWithDeclaredLengthOverLimit() { - byte[] signature = new byte[64]; + Span bytes = Bytes.FromHexString("540b38f8b160f23b1cd30972338a09ba4a296e2f0cb63f76ce0b38201a8dd9aa2a9c306370904877ddab397f7845ff67ea0a1dbf094b86794bb5d739e6bda891a486098717e2fb744e04c4665d307a590c6e4141a3805de15eb1eb62b0c6ff0aa75db9559545e294e158b7dc9e4a118cf0c2c6259af2df7c1742731064df376182b2df2e714df9e87ec6492effb4de8e2a92bdb405bbe3d8ddf96622bbcb11592fdb2600356cb39fd2c36cac66e19cd1b136ac3be993ef0ed07905d95f16cc67cfbe9bc7c180b90023d55d9218bef9e052c9f655a5c2464abe24271cc1dc2f3df7d3abd926f4657b724b0435868a09f7136ec115cbc3ec1c675972315e4cc140907e4772c118d51917b16a00a7809cfa767ea3ae5557c0b972c37f77d85062910e3e15ae4613cac178220deadc6d729da20c85166e8532d8f88cd246e6102f5268cd5e29796d06713d0f684e096e5edfca6b6c7adf9e51e10f5140d92216123eb31984a61d5a9caf904a2e12f3f479b27d75aeafe0d35b8995468aa12ba7d8f17fbb0aeea63b4d2c74e43b60e06a62bed5ee3ae34f5d74465087b5932865a2cb41f1fdaa9b2b9143fe1923d7f0e4b18a3139ee469df8e6cfea46101674e5fde4c84f9f9d77dee3d0545897a69d9eb42ccc48b699baa9d932dc36783da3580a78abc68b20a1f8bda90afb5ed78a9ac46e63792182b7669e4daaf3ca7e9b5690a3bbf0a184b14470f899582d4a0423897a295441b4bf27db3d2e8adf41824538942198a064bc489fd0936e11f5266146432a8efc992e1d304a4ab6bf661fa1ab3b59d1f14155c5e6a8d1e9eed717bee86a9b6bdabde638c0d1"); + RlpStream rlpStream = new(600); + rlpStream.StartSequence(500); + rlpStream.Encode(bytes[..500]); + rlpStream.Position = 0; + return rlpStream; + } + + private static RlpStream CreateEncodedRecordOverLimit() + { + byte[] filler = FindFillerForOversizedEncodedRecord(); + return CreateRecord( + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), + ("z", stream => stream.Encode(filler), Rlp.LengthOf(filler))); + } + + private static RlpStream CreateRecordWithOversizedSignature() + => CreateRecordWithSignatureLength(66, + (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))); + + private static RlpStream CreateRecordWithSignatureLength( + int signatureLength, + params (string Key, Action EncodeValue, int ValueLength)[] entries) + { + byte[] signature = new byte[signatureLength]; int contentLength = Rlp.LengthOf(signature) + Rlp.LengthOf(1UL); foreach ((string key, _, int valueLength) in entries) { @@ -201,27 +282,40 @@ private static RlpStream CreateRecord(params (string Key, Action Enco return rlpStream; } - private static IEnumerable InvalidRecordCases() + private static byte[] CreateRecordWithTrailingBytes() { - yield return new TestCaseData((Func)(static () => CreateRecord( - (EnrContentKey.Udp, static stream => stream.Encode(30303), Rlp.LengthOf(30303)), - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))))) - .SetName("Throws_when_keys_are_not_sorted"); + byte[] recordBytes = Bytes.FromHexString( + "f897b840421561b4ed5de28a7100e0a5005ecc0ba6ba6cc18528061e811704c8794fec965cba63831051d134bdc801c0c90d31a30d241074095311ffe6628d5545478b770a83657468c7c68496516d06808269648276348269708436ed0a0a89736563703235366b31a103f5c110132b0374805d4453f55577cc9c58bb1a08f822b9b3722132e3095f69728374637082765f8375647082765f"); + return [.. recordBytes, 0x80]; + } - yield return new TestCaseData((Func)(static () => CreateRecord( + private static byte[] CreateRecordWithUnrecoverableSignature() + { + byte[] publicKey = new byte[CompressedPublicKey.LengthInBytes]; + RlpStream rlpStream = CreateRecord( (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4")), - (EnrContentKey.Id, static stream => stream.Encode("v4"), Rlp.LengthOf("v4"))))) - .SetName("Throws_when_keys_are_duplicated"); + (EnrContentKey.SecP256k1, stream => stream.Encode(publicKey), Rlp.LengthOf(publicKey))); + + return rlpStream.Data.AsSpan().ToArray(); + } - yield return new TestCaseData((Func)(static () => CreateRecord( - ("z", static stream => stream.Encode(Array.Empty()), Rlp.LengthOf(Array.Empty()))))) - .SetName("Throws_when_id_is_missing"); + private static byte[] CreateRecordWithInvalidSignature() + { + Ecdsa ecdsa = new(); + PrivateKey privateKey = new(TestPrivateKey); + NodeRecordSigner signer = new(ecdsa, privateKey); + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + signer.Sign(nodeRecord); - yield return new TestCaseData((Func)(static () => CreateRecord( - (EnrContentKey.Id, static stream => stream.Encode("v5"), Rlp.LengthOf("v5"))))) - .SetName("Throws_when_id_is_not_v4"); + byte[] recordBytes = nodeRecord.ToRlpBytes().AsSpan().ToArray(); + recordBytes[4] ^= 0x01; + return recordBytes; } + private static TestCaseData InvalidRecordCase(Func createRecord, Type exceptionType, string name) + => new TestCaseData(createRecord, exceptionType).SetName(name); + private static byte[] FindFillerForOversizedEncodedRecord() { for (int i = 0; i <= 300; i++) From cd50eaf5971f77e122ea8c67639b19f7ef47fa04 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 22 Jun 2026 17:26:10 +0300 Subject: [PATCH 177/182] Move more discovery into kad --- .../IIteratorNodeLookup.cs | 9 - .../Nethermind.Kademlia/IKademlia.cs | 8 + .../Nethermind.Kademlia/IKademliaDiscovery.cs | 20 ++ .../Nethermind.Kademlia/ILookupAlgo.cs | 14 ++ .../Nethermind.Kademlia/Kademlia.cs | 31 ++- .../LookupKNearestNeighbour.cs | 134 +++++++++-- .../RandomWalkKademliaDiscovery.cs | 116 ++++++++++ .../Discv4/Kademlia/NodeSourceTests.cs | 69 +++--- .../Discv5/CodecTests.cs | 14 +- .../Discv5/NodeSourceTests.cs | 62 ++++- .../Kademlia/IteratorNodeLookupTests.cs | 212 ----------------- .../Kademlia/LookupKNearestNeighbourTests.cs | 47 ++++ .../RandomWalkKademliaDiscoveryTests.cs | 90 ++++++++ .../Discv4/Kademlia/NodeSource.cs | 119 ++++------ .../Discv5/Kademlia/AdapterState.cs | 5 +- .../Discv5/Kademlia/KademliaAdapter.cs | 122 ++++++---- .../Discv5/Kademlia/NodeSource.cs | 63 ++++- .../Discv5/Packets/Challenge.cs | 29 ++- .../Discv5/Packets/PacketCodec.cs | 33 +-- .../Kademlia/IteratorNodeLookup.cs | 215 ------------------ .../Kademlia/KademliaModule.cs | 2 +- 21 files changed, 739 insertions(+), 675 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs create mode 100644 src/Nethermind/Nethermind.Kademlia/IKademliaDiscovery.cs create mode 100644 src/Nethermind/Nethermind.Kademlia/RandomWalkKademliaDiscovery.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RandomWalkKademliaDiscoveryTests.cs delete mode 100644 src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs diff --git a/src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs b/src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs deleted file mode 100644 index 18828c4a3b61..000000000000 --- a/src/Nethermind/Nethermind.Kademlia/IIteratorNodeLookup.cs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Kademlia; - -public interface IIteratorNodeLookup -{ - IAsyncEnumerable Lookup(TKey target, CancellationToken token); -} diff --git a/src/Nethermind/Nethermind.Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs index c7eeb022b775..58fa58a785d9 100644 --- a/src/Nethermind/Nethermind.Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs @@ -42,6 +42,14 @@ public interface IKademlia /// Optional result size. Defaults to . Task LookupNodesClosest(TKey key, CancellationToken token, int? k = null); + /// + /// Looks up nodes near and streams newly discovered candidates as soon as they are seen. + /// + /// Protocol-specific lookup key. + /// Cancellation token for the lookup. + /// Optional maximum number of candidates to emit. Defaults to . + IAsyncEnumerable LookupNodes(TKey key, CancellationToken token, int? maxResults = null); + /// /// Returns the closest routing-table entries to without traversing the network. /// diff --git a/src/Nethermind/Nethermind.Kademlia/IKademliaDiscovery.cs b/src/Nethermind/Nethermind.Kademlia/IKademliaDiscovery.cs new file mode 100644 index 000000000000..d15bdede0d09 --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/IKademliaDiscovery.cs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Kademlia; + +/// +/// Produces active Kademlia lookup candidates without owning protocol-specific peer admission. +/// +/// The protocol-specific lookup key type. +/// The protocol-specific node/contact type. +public interface IKademliaDiscovery +{ + /// + /// Runs active random lookup jobs and streams nodes discovered by those lookups. + /// + /// Number of active lookup jobs to run. + /// Maximum number of candidates requested from each lookup. + /// Cancellation token that stops active discovery. + IAsyncEnumerable DiscoverNodes(int concurrentDiscoveryJobs, int lookupResultLimit, CancellationToken token); +} diff --git a/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs b/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs index 293179724db2..b48927ec6f61 100644 --- a/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs +++ b/src/Nethermind/Nethermind.Kademlia/ILookupAlgo.cs @@ -29,4 +29,18 @@ Task Lookup( Func> findNeighbourOp, CancellationToken token ); + + /// + /// Streams lookup candidates as soon as the lookup sees them. + /// + /// The hash to search near. + /// Maximum number of candidates to emit before stopping the lookup. + /// Operation that fetches neighbours from a candidate node. + /// Cancellation token. + IAsyncEnumerable LookupNodes( + TKadKey targetHash, + int maxResults, + Func> findNeighbourOp, + CancellationToken token + ); } diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index ef06d4a2fe24..6ccb893c6886 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -82,19 +82,32 @@ public Task LookupNodesClosest(TKey key, CancellationToken token, int? return _lookupAlgo.Lookup( keyHash, k ?? _kSize, - async (nextNode, token) => - { - if (SameAsSelf(nextNode)) - { - return _routingTable.GetKNearestNeighbour(keyHash); - } + (nextNode, token) => FindNeighbours(key, keyHash, nextNode, token), + token + ); + } - return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); - }, + public IAsyncEnumerable LookupNodes(TKey key, CancellationToken token, int? maxResults = null) + { + TKadKey keyHash = _keyOperator.GetKeyHash(key); + return _lookupAlgo.LookupNodes( + keyHash, + maxResults ?? _kSize, + (nextNode, token) => FindNeighbours(key, keyHash, nextNode, token), token ); } + private async Task FindNeighbours(TKey key, TKadKey keyHash, TNode nextNode, CancellationToken token) + { + if (SameAsSelf(nextNode)) + { + return _routingTable.GetKNearestNeighbour(keyHash); + } + + return await _kademliaMessageSender.FindNeighbours(nextNode, key, token); + } + public async Task Run(CancellationToken token) { while (true) @@ -130,7 +143,7 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => // Should be added on Pong. if (await _kademliaMessageSender.Ping(node, token)) { - System.Threading.Interlocked.Increment(ref onlineBootNodes); + Interlocked.Increment(ref onlineBootNodes); } } catch (OperationCanceledException) diff --git a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs index 723fc1f071ff..8e65bfdcd54d 100644 --- a/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs +++ b/src/Nethermind/Nethermind.Kademlia/LookupKNearestNeighbour.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading.Channels; using Nethermind.Logging; namespace Nethermind.Kademlia; @@ -32,6 +34,96 @@ public async Task Lookup( int k, Func> findNeighbourOp, CancellationToken token + ) + => await LookupCore(targetHash, k, findNeighbourOp, null, token); + + public async IAsyncEnumerable LookupNodes( + TKadKey targetHash, + int maxResults, + Func> findNeighbourOp, + [EnumeratorCancellation] CancellationToken token + ) + { + if (maxResults <= 0) + { + yield break; + } + + Channel results = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false + }); + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + int emitted = 0; + Task producer = ProduceResults(); + + try + { + await foreach (TNode node in results.Reader.ReadAllAsync(token)) + { + yield return node; + } + + await producer; + } + finally + { + await cts.CancelAsync(); + try + { + await producer; + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + } + } + + async Task ProduceResults() + { + Exception? error = null; + try + { + _ = await LookupCore(targetHash, maxResults, findNeighbourOp, Publish, cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + } + catch (Exception e) + { + error = e; + } + finally + { + results.Writer.TryComplete(error); + } + } + + bool Publish(TNode node) + { + int count = Interlocked.Increment(ref emitted); + if (count <= maxResults) + { + results.Writer.TryWrite(node); + } + + if (count >= maxResults) + { + cts.Cancel(); + return false; + } + + return true; + } + } + + private async Task LookupCore( + TKadKey targetHash, + int k, + Func> findNeighbourOp, + Func? publishNode, + CancellationToken token ) { if (_logger.IsDebug) @@ -58,23 +150,32 @@ CancellationToken token // Ordered by highest distance. Added on result. Get popped as result. PriorityQueue<(TKadKey, TNode), TKadKey> finalResult = new(comparerReverse); - foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) - { - TKadKey nodeHash = nodeHashProvider.GetHash(node); - seen.TryAdd(nodeHash, node); - bestSeen.Enqueue((nodeHash, node), nodeHash); - } - TaskCompletionSource roundComplete = new(TaskCreationOptions.RunContinuationsAsynchronously); int closestNodeRound = 0; int currentRound = 0; int queryingTask = 0; bool finished = false; - Task[] worker = new Task[config.Alpha]; - for (int i = 0; i < worker.Length; i++) + foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) { - worker[i] = Task.Run(async () => + TKadKey nodeHash = nodeHashProvider.GetHash(node); + if (!seen.TryAdd(nodeHash, node)) + { + continue; + } + + bestSeen.Enqueue((nodeHash, node), nodeHash); + if (!TryPublish(node)) + { + Volatile.Write(ref finished, true); + break; + } + } + + Task[] workers = new Task[config.Alpha]; + for (int i = 0; i < workers.Length; i++) + { + workers[i] = Task.Run(async () => { while (!Volatile.Read(ref finished)) { @@ -125,18 +226,18 @@ CancellationToken token // When any of the worker is finished, we consider the whole query as done. // This prevent this operation from hanging on a timed out request - await Task.WhenAny(worker); + await Task.WhenAny(workers); Volatile.Write(ref finished, true); await cts.CancelAsync(); try { - await Task.WhenAll(worker); + await Task.WhenAll(workers); } catch (OperationCanceledException) when (token.IsCancellationRequested) { } - return CompileResult(); + return publishNode is null ? CompileResult() : []; async Task WrappedFindNeighbourOp(TNode node) { @@ -210,6 +311,11 @@ void ProcessResult(TKadKey hash, TNode toQuery, TNode[] neighbours, int round) if (!seen.TryAdd(neighbourHash, neighbour)) continue; bestSeen.Enqueue((neighbourHash, neighbour), neighbourHash); + if (!TryPublish(neighbour)) + { + Volatile.Write(ref finished, true); + break; + } if (closestNodeRound < round) { @@ -263,5 +369,7 @@ bool ShouldStopDueToNoBetterResult(out int round) return false; } } + + bool TryPublish(TNode node) => publishNode?.Invoke(node) ?? true; } } diff --git a/src/Nethermind/Nethermind.Kademlia/RandomWalkKademliaDiscovery.cs b/src/Nethermind/Nethermind.Kademlia/RandomWalkKademliaDiscovery.cs new file mode 100644 index 000000000000..d03314a6b81d --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/RandomWalkKademliaDiscovery.cs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Nethermind.Logging; + +namespace Nethermind.Kademlia; + +/// +/// Runs active random Kademlia lookups and streams discovered nodes. +/// +public sealed class RandomWalkKademliaDiscovery( + IKademlia kademlia, + IKeyOperator keyOperator, + IKademliaDistance distance, + KademliaConfig kademliaConfig, + ILogManager? logManager = null) + : IKademliaDiscovery + where TNode : notnull + where TKadKey : notnull +{ + private static readonly TimeSpan MinimumIterationDuration = TimeSpan.FromSeconds(1); + + private readonly ILogger _logger = (logManager ?? NullLogManager.Instance).GetClassLogger>(); + private readonly TKadKey _currentNodeHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); + private readonly int _maxDistance = distance.MaxDistance; + + /// + public IAsyncEnumerable DiscoverNodes(int concurrentDiscoveryJobs, int lookupResultLimit, CancellationToken token) + { + ArgumentOutOfRangeException.ThrowIfNegative(concurrentDiscoveryJobs); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(lookupResultLimit); + + return DiscoverNodesCore(concurrentDiscoveryJobs, lookupResultLimit, token); + } + + private async IAsyncEnumerable DiscoverNodesCore( + int concurrentDiscoveryJobs, + int lookupResultLimit, + [EnumeratorCancellation] CancellationToken token) + { + if (concurrentDiscoveryJobs == 0) + { + yield break; + } + + using CancellationTokenSource disposeCts = CancellationTokenSource.CreateLinkedTokenSource(token); + CancellationToken discoveryToken = disposeCts.Token; + Channel channel = Channel.CreateBounded(lookupResultLimit); + + Task[] discoverTasks = new Task[concurrentDiscoveryJobs]; + for (int i = 0; i < discoverTasks.Length; i++) + { + discoverTasks[i] = Task.Run(() => RunDiscoveryJob(channel.Writer, lookupResultLimit, discoveryToken)); + } + + Task discoverTask = Task.WhenAll(discoverTasks); + try + { + await foreach (TNode node in channel.Reader.ReadAllAsync(token)) + { + yield return node; + } + } + finally + { + await disposeCts.CancelAsync(); + channel.Writer.TryComplete(); + try + { + await discoverTask; + } + catch (OperationCanceledException) when (discoveryToken.IsCancellationRequested) + { + } + } + } + + private async Task RunDiscoveryJob(ChannelWriter writer, int lookupResultLimit, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + Stopwatch iterationTime = Stopwatch.StartNew(); + try + { + int targetDistance = Random.Shared.Next(_maxDistance) + 1; + TKey target = keyOperator.CreateRandomKeyAtDistance(_currentNodeHash, targetDistance); + if (_logger.IsDebug) _logger.Debug($"Looking up random Kademlia target at distance {targetDistance}."); + + int count = 0; + await foreach (TNode node in kademlia.LookupNodes(target, token, lookupResultLimit).WithCancellation(token)) + { + count++; + await writer.WriteAsync(node, token); + } + + if (_logger.IsDebug) _logger.Debug($"Found {count} nodes from random Kademlia lookup."); + + if (iterationTime.Elapsed < MinimumIterationDuration) + { + await Task.Delay(MinimumIterationDuration - iterationTime.Elapsed, token); + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + if (_logger.IsError) _logger.Error("Random Kademlia discovery lookup failed.", ex); + } + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs index 3f8a31c4d7e4..3d0543f03835 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs @@ -27,7 +27,7 @@ namespace Nethermind.Network.Discovery.Test.Discv4.Kademlia public class NodeSourceTests { private TestKademlia _kademlia = null!; - private IIteratorNodeLookup _lookup = null!; + private TestKademliaDiscovery _kademliaDiscovery = null!; private IKademliaAdapter _discv4Adapter = null!; private NodeSource _nodeSource = null!; private NodeSession _nodeSession = null!; @@ -40,7 +40,7 @@ public class NodeSourceTests public void Setup() { _kademlia = new(); - _lookup = Substitute.For>(); + _kademliaDiscovery = new(); _discv4Adapter = Substitute.For(); _discoveryConfig = new DiscoveryConfig @@ -62,7 +62,7 @@ public void Setup() _nodeSource = new NodeSource( _kademlia, - _lookup, + _kademliaDiscovery, _discv4Adapter, _discoveryConfig, _kademliaConfig, @@ -74,14 +74,13 @@ public void Setup() [Test] [CancelAfter(10000)] - public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToken token) + public async Task DiscoverNodes_should_use_kademlia_discovery_to_find_nodes(CancellationToken token) { Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); _nodeSession.OnPongReceived(node1.Address); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node1, node2)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node1, node2); _discv4Adapter.Ping(node1, Arg.Any()) .Returns(true); _discv4Adapter.Ping(node2, Arg.Any()) @@ -93,7 +92,7 @@ public async Task DiscoverNodes_should_use_lookup_to_find_nodes(CancellationToke await enumerator.MoveNextAsync(); Assert.That(enumerator.Current, Is.EqualTo(node2)); - _lookup.Received().Lookup(Arg.Any(), Arg.Any()); + Assert.That(_kademliaDiscovery.DiscoverNodesCalls, Is.GreaterThanOrEqualTo(1)); } [Test] @@ -104,8 +103,7 @@ public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(Ca Node node = new(TestItem.PublicKeyA, "192.168.1.1", 30303); _discv4Adapter.Ping(node, Arg.Any()) .Returns(true); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); @@ -135,8 +133,7 @@ public async Task DiscoverNodes_should_skip_nodes_that_have_tried_ping_recently_ // Set up session2 to have received a pong session2.OnPongReceived(node2.Address); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node1, node2)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node1, node2); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -162,8 +159,7 @@ public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken tok _discv4Adapter.Ping(node2, Arg.Any()) .Returns(true); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node1, node2)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node1, node2); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -188,8 +184,7 @@ public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(Cancellat _nodeSession.OnPongReceived(node1.Address); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node1)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node1); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -212,8 +207,7 @@ public async Task DiscoverNodes_should_not_emit_duplicate_nodes(CancellationToke _nodeSession.OnPongReceived(node.Address); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node, node)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node, node); using AutoCancelTokenSource shortTimeout = token.CreateChildTokenSource(TimeSpan.FromMilliseconds(100)); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(shortTimeout.Token); @@ -226,23 +220,14 @@ public async Task DiscoverNodes_should_not_emit_duplicate_nodes(CancellationToke [Test] [CancelAfter(10000)] - public async Task DiscoverNodes_should_use_multiple_concurrent_discovery_jobs(CancellationToken token) + public async Task DiscoverNodes_should_pass_concurrent_discovery_jobs_to_kademlia_discovery(CancellationToken token) { Node node1 = new(TestItem.PublicKeyA, "192.168.1.1", 30303); Node node2 = new(TestItem.PublicKeyB, "192.168.1.2", 30303); _nodeSession.OnPongReceived(node1.Address); - // Set up the lookup to return different nodes for different calls - int callCount = 0; - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(_ => - { - callCount++; - return callCount == 1 - ? CreateAsyncEnumerable(node1) - : CreateAsyncEnumerable(node2); - }); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node1, node2); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); @@ -250,10 +235,7 @@ public async Task DiscoverNodes_should_use_multiple_concurrent_discovery_jobs(Ca await enumerator.MoveNextAsync(); await enumerator.MoveNextAsync(); - // Assert - Verify that lookup was called at least twice - _lookup.Received(2).Lookup( - Arg.Any(), - Arg.Any()); + Assert.That(_kademliaDiscovery.ConcurrentDiscoveryJobs, Is.EqualTo(_discoveryConfig.ConcurrentDiscoveryJob)); } [Test] @@ -263,8 +245,7 @@ public async Task DiscoverNodes_should_stop_background_jobs_when_enumeration_is_ _discoveryConfig.ConcurrentDiscoveryJob = 1; Node node = new(TestItem.PublicKeyA, "192.168.1.1", 30303); _nodeSession.OnPongReceived(node.Address); - _lookup.Lookup(Arg.Any(), Arg.Any()) - .Returns(CreateAsyncEnumerable(node)); + _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node); List nodes = await _nodeSource.DiscoverNodes(CancellationToken.None).Take(1).ToListAsync(token); @@ -343,11 +324,31 @@ public event EventHandler? OnNodeRemoved { add { } remove { } } public Task LookupNodesClosest(PublicKey key, CancellationToken token, int? k = null) => Task.FromResult(Array.Empty()); + public IAsyncEnumerable LookupNodes(PublicKey key, CancellationToken token, int? maxResults = null) => + throw new NotSupportedException(); + public Node[] GetKNeighbour(PublicKey target, Node? excluding = null, bool excludeSelf = false) => []; public Node[] GetAllAtDistance(int distance) => []; public IEnumerable IterateNodes() => []; } + + private sealed class TestKademliaDiscovery : IKademliaDiscovery + { + public int DiscoverNodesCalls { get; private set; } + + public int ConcurrentDiscoveryJobs { get; private set; } + + public Func> DiscoverNodesHandler { private get; set; } = + (_, _, _) => CreateAsyncEnumerable(); + + public IAsyncEnumerable DiscoverNodes(int concurrentDiscoveryJobs, int lookupResultLimit, CancellationToken token) + { + DiscoverNodesCalls++; + ConcurrentDiscoveryJobs = concurrentDiscoveryJobs; + return DiscoverNodesHandler(concurrentDiscoveryJobs, lookupResultLimit, token); + } + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index 8e6200e1868a..93b1be86735d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -128,14 +128,14 @@ public void PacketCodec_Decodes_WhoAreYou_GoEthereum_Vector() using (packet) { using PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); - Challenge challenge = codec.DecodeWhoAreYou(in packet); + using Challenge challenge = codec.DecodeWhoAreYou(in packet); Assert.That(decoded, Is.True); Assert.That(packet.Flag, Is.EqualTo(PacketFlag.WhoAreYou)); - Assert.That(challenge.RequestNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c")); - Assert.That(challenge.IdNonce.ToHexString(true), Is.EqualTo("0x0102030405060708090a0b0c0d0e0f10")); + Assert.That(packet.Nonce.Span.SequenceEqual(Bytes.FromHexString("0x0102030405060708090a0b0c")), Is.True); + Assert.That(packet.AuthData.Span[..16].SequenceEqual(Bytes.FromHexString("0x0102030405060708090a0b0c0d0e0f10")), Is.True); Assert.That(challenge.EnrSequence, Is.Zero); - Assert.That(challenge.ChallengeData, Is.EqualTo(challengeData)); + Assert.That(challenge.ChallengeData.SequenceEqual(challengeData), Is.True); } } @@ -175,11 +175,7 @@ public void PacketCodec_Decodes_PingHandshake_GoEthereum_Vectors( bool includesRecord) { byte[] packetBytes = Bytes.FromHexString(packetHex); - Challenge challenge = new( - Bytes.FromHexString("0x0102030405060708090a0b0c"), - Bytes.FromHexString("0x0102030405060708090a0b0c0d0e0f10"), - challengeEnrSequence, - Bytes.FromHexString(challengeDataHex)); + using Challenge challenge = new(challengeEnrSequence, Bytes.FromHexString(challengeDataHex)); using PacketCodec codec = CreateCodec(new PrivateKey(GethNodeBPrivateKey)); NodeRecord? knownRecord = includesRecord ? null : CreateNodeRecord(new PrivateKey(GethNodeAPrivateKey)); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs index 2591b1b1b841..159ada58b0b3 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/NodeSourceTests.cs @@ -27,11 +27,7 @@ public async Task DiscoverNodes_ShouldNotRetainDroppedNodesInRecentDedupe(Cancel { IKademlia kademlia = Substitute.For>(); kademlia.IterateNodes().Returns(Array.Empty()); - NodeSource source = new( - kademlia, - new KademliaConfig { CurrentNodeId = CreateNode(0) }, - ExecutionLayerDiscv5RecordFilter.Instance, - LimboLogs.Instance); + NodeSource source = CreateSource(kademlia); await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); ValueTask firstMove = enumerator.MoveNextAsync(); @@ -69,11 +65,7 @@ public async Task DiscoverNodes_ShouldEmitPeerCandidateWithTcpEndpoint(Cancellat { IKademlia kademlia = Substitute.For>(); kademlia.IterateNodes().Returns(Array.Empty()); - NodeSource source = new( - kademlia, - new KademliaConfig { CurrentNodeId = CreateNode(0) }, - ExecutionLayerDiscv5RecordFilter.Instance, - LimboLogs.Instance); + NodeSource source = CreateSource(kademlia); await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); ValueTask firstMove = enumerator.MoveNextAsync(); @@ -96,8 +88,28 @@ public async Task DiscoverNodes_ShouldSkipConsensusOnlyEnrs(CancellationToken to Node executionNode = CreateNode(2); IKademlia kademlia = Substitute.For>(); kademlia.IterateNodes().Returns([consensusOnlyNode, executionNode]); + NodeSource source = CreateSource(kademlia); + + await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); + + Assert.That(await enumerator.MoveNextAsync(), Is.True); + Assert.That(enumerator.Current.Id, Is.EqualTo(TestItem.PrivateKeys[2].PublicKey)); + } + + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_ShouldEmitPeerCandidateFromActiveKademliaDiscovery(CancellationToken token) + { + IKademlia kademlia = Substitute.For>(); + kademlia.IterateNodes().Returns(Array.Empty()); + IKademliaDiscovery discovery = Substitute.For>(); + discovery.DiscoverNodes(1, Arg.Any(), Arg.Any()) + .Returns(CreateAsyncEnumerable(CreateNode(1, tcpPort: 30303, udpPort: 30304))); + NodeSource source = new( kademlia, + discovery, + new DiscoveryConfig { ConcurrentDiscoveryJob = 1 }, new KademliaConfig { CurrentNodeId = CreateNode(0) }, ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); @@ -105,7 +117,26 @@ public async Task DiscoverNodes_ShouldSkipConsensusOnlyEnrs(CancellationToken to await using IAsyncEnumerator enumerator = source.DiscoverNodes(token).GetAsyncEnumerator(token); Assert.That(await enumerator.MoveNextAsync(), Is.True); - Assert.That(enumerator.Current.Id, Is.EqualTo(TestItem.PrivateKeys[2].PublicKey)); + using (Assert.EnterMultipleScope()) + { + Assert.That(enumerator.Current.Id, Is.EqualTo(TestItem.PrivateKeys[1].PublicKey)); + Assert.That(enumerator.Current.Port, Is.EqualTo(30303)); + } + } + + private static NodeSource CreateSource(IKademlia kademlia) + { + IKademliaDiscovery discovery = Substitute.For>(); + discovery.DiscoverNodes(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(CreateAsyncEnumerable()); + + return new NodeSource( + kademlia, + discovery, + new DiscoveryConfig { ConcurrentDiscoveryJob = 0 }, + new KademliaConfig { CurrentNodeId = CreateNode(0) }, + ExecutionLayerDiscv5RecordFilter.Instance, + LimboLogs.Instance); } private static Node CreateNode(int index, int tcpPort = 30303, int udpPort = 30304, bool includeEth2 = false) @@ -126,4 +157,13 @@ private static Node CreateNode(int index, int tcpPort = 30303, int udpPort = 303 private static void RaiseNode(IKademlia kademlia, Node node) => kademlia.OnNodeAdded += Raise.Event>(null, node); + + private static async IAsyncEnumerable CreateAsyncEnumerable(params IEnumerable items) + { + foreach (T item in items) + { + await Task.Yield(); + yield return item; + } + } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs deleted file mode 100644 index 36d185cfbda6..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/IteratorNodeLookupTests.cs +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Nethermind.Core; -using Nethermind.Core.Crypto; -using Nethermind.Core.Test.Builders; -using Nethermind.Logging; -using Nethermind.Network.Discovery.Kademlia; -using Nethermind.Kademlia; -using Nethermind.Stats.Model; -using NSubstitute; -using NUnit.Framework; - -namespace Nethermind.Network.Discovery.Test.Kademlia -{ - [Parallelizable(ParallelScope.Self)] - [TestFixture] - public class IteratorNodeLookupTests - { - private static readonly Node InitialNode = new(TestItem.PublicKeyC, "192.168.1.3", 30303); - private static readonly Node NeighbourNode = new(TestItem.PublicKeyD, "192.168.1.4", 30303); - - private IRoutingTable _routingTable = null!; - private IteratorNodeLookup _lookup = null!; - private IKademliaMessageSender _msgSender = null!; - private Node _currentNode = null!; - private PublicKey _targetKey = null!; - - [SetUp] - public void Setup() - { - _currentNode = new(TestItem.PublicKeyA, "192.168.1.1", 30303); - _targetKey = TestItem.PublicKeyB; - - _routingTable = Substitute.For>(); - KademliaConfig kademliaConfig = new() { CurrentNodeId = _currentNode }; - _msgSender = Substitute.For>(); - ILogManager logManager = Substitute.For(); - - _lookup = new IteratorNodeLookup( - _routingTable, - kademliaConfig, - _msgSender, - new PublicKeyKeyOperator(), - Hash256KademliaDistance.Instance, - new ManualTimestamper(new DateTime(2025, 5, 13, 21, 0, 0, DateTimeKind.Utc)), - logManager); - } - - private void RoutingTableReturns(params Node[] nodes) => - _routingTable.GetKNearestNeighbour(Arg.Any(), Arg.Any()) - .Returns(nodes); - - private void FindNeighboursReturns(Node from, params Node[] result) => - _msgSender.FindNeighbours(from, _targetKey, Arg.Any()) - .Returns(result); - - private void FindNeighboursThrows(Node from, Exception exception) => - _msgSender.FindNeighbours(from, _targetKey, Arg.Any()) - .Returns(Task.FromException(exception)); - - private Task AssertFindNeighboursCalledOnce(Node node) => - _msgSender.Received(1).FindNeighbours( - Arg.Is(n => n == node), - Arg.Is(k => k == _targetKey), - Arg.Any()); - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_return_nodes_from_routing_table(CancellationToken token) - { - Node[] expectedNodes = [InitialNode, NeighbourNode]; - RoutingTableReturns(expectedNodes); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - - Assert.That(result, Is.EquivalentTo(expectedNodes)); - _routingTable.Received(1).GetKNearestNeighbour( - Arg.Is(h => h == TargetHash), - Arg.Any()); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_query_nodes_and_return_neighbours(CancellationToken token) - { - RoutingTableReturns(InitialNode); - FindNeighboursReturns(InitialNode, NeighbourNode); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - - Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); - await AssertFindNeighboursCalledOnce(InitialNode); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_not_query_self_node(CancellationToken token) - { - RoutingTableReturns(_currentNode); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - - Assert.That(result, Is.EquivalentTo(new[] { _currentNode })); - - await _msgSender.DidNotReceive().FindNeighbours( - Arg.Any(), - Arg.Any(), - Arg.Any()); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_handle_empty_neighbour_response(CancellationToken token) - { - RoutingTableReturns(InitialNode); - FindNeighboursReturns(InitialNode); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - - Assert.That(result, Is.EquivalentTo(new[] { InitialNode })); - await AssertFindNeighboursCalledOnce(InitialNode); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_handle_exception_in_find_neighbours(CancellationToken token) - { - RoutingTableReturns(InitialNode); - FindNeighboursThrows(InitialNode, new Exception("Test exception")); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - - Assert.That(result, Is.EquivalentTo(new[] { InitialNode })); - await AssertFindNeighboursCalledOnce(InitialNode); - } - - [Test] - [CancelAfter(10000)] - public void Lookup_should_respect_cancellation_token(CancellationToken token) - { - RoutingTableReturns(InitialNode); - - using CancellationTokenSource cts = new(); - cts.Cancel(); - - Assert.ThrowsAsync(async () => await _lookup.Lookup(_targetKey, cts.Token).ToListAsync()); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_not_cache_node_as_unreachable_when_lookup_is_cancelled(CancellationToken token) - { - RoutingTableReturns(InitialNode); - using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); - - _msgSender.FindNeighbours(InitialNode, _targetKey, Arg.Any()) - .Returns(call => - { - cts.Cancel(); - return Task.FromException(new OperationCanceledException((CancellationToken)call[2])); - }); - - Assert.ThrowsAsync(async () => await _lookup.Lookup(_targetKey, cts.Token).ToListAsync()); - - FindNeighboursReturns(InitialNode, NeighbourNode); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(token); - - Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); - await _msgSender.Received(2).FindNeighbours( - Arg.Is(n => n == InitialNode), - Arg.Is(k => k == _targetKey), - Arg.Any()); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_not_query_same_node_twice(CancellationToken token) - { - RoutingTableReturns(InitialNode); - FindNeighboursReturns(InitialNode, NeighbourNode); - FindNeighboursReturns(NeighbourNode, InitialNode); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); - - Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); - await AssertFindNeighboursCalledOnce(InitialNode); - await AssertFindNeighboursCalledOnce(NeighbourNode); - } - - [Test] - [CancelAfter(10000)] - public async Task Lookup_should_not_return_duplicate_nodes(CancellationToken token) - { - RoutingTableReturns(InitialNode); - FindNeighboursReturns(InitialNode, NeighbourNode); - FindNeighboursReturns(NeighbourNode, InitialNode, NeighbourNode); - - List result = await _lookup.Lookup(_targetKey, token).ToListAsync(); - - Assert.That(result, Is.EquivalentTo(new[] { InitialNode, NeighbourNode })); - } - - private Hash256 TargetHash => _targetKey.Hash; - } -} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs index 55645afa6b6d..62cd25a0b70f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/LookupKNearestNeighbourTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Nethermind.Kademlia; @@ -154,6 +155,52 @@ public async Task Lookup_should_return_results_with_different_alpha(int alpha, C Assert.That(result, Is.Not.Empty); } + [Test] + [CancelAfter(10000)] + public async Task Lookup_nodes_should_stream_routing_table_nodes_before_network_lookup_finishes(CancellationToken token) + { + (LookupKNearestNeighbour lookup, _, _) = + CreateLookup(1, TimeSpan.FromSeconds(10), [Seed1]); + TaskCompletionSource requestStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + + await using IAsyncEnumerator enumerator = lookup.LookupNodes( + Self, + 8, + async (_, findToken) => + { + requestStarted.SetResult(); + await Task.Delay(Timeout.Infinite, findToken); + return []; + }, + token).GetAsyncEnumerator(token); + + Assert.That(await enumerator.MoveNextAsync(), Is.True); + Assert.That(enumerator.Current, Is.EqualTo(Seed1)); + await requestStarted.Task.WaitAsync(token); + } + + [Test] + [CancelAfter(10000)] + public async Task Lookup_nodes_should_stop_when_enough_candidates_are_streamed(CancellationToken token) + { + (LookupKNearestNeighbour lookup, _, _) = + CreateLookup(1, TimeSpan.FromSeconds(10), [Seed1, Seed2]); + int requests = 0; + + List result = await lookup.LookupNodes( + Self, + 1, + (_, _) => + { + requests++; + return Task.FromResult([]); + }, + token).ToListAsync(token); + + Assert.That(result, Is.EqualTo(new[] { Seed1 })); + Assert.That(requests, Is.Zero); + } + [Test] [CancelAfter(10000)] public async Task Lookup_should_drain_cancelled_workers_before_returning(CancellationToken token) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RandomWalkKademliaDiscoveryTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RandomWalkKademliaDiscoveryTests.cs new file mode 100644 index 000000000000..9bd0b311824e --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/RandomWalkKademliaDiscoveryTests.cs @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Nethermind.Kademlia; +using Nethermind.Logging; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class RandomWalkKademliaDiscoveryTests +{ + [Test] + [CancelAfter(10000)] + public async Task DiscoverNodes_should_stream_nodes_from_random_lookup(CancellationToken token) + { + TestKademlia kademlia = new(); + RandomWalkKademliaDiscovery discovery = new( + kademlia, + IntKeyOperator.Instance, + Int32KademliaDistance.Instance, + new KademliaConfig { CurrentNodeId = 0 }, + LimboLogs.Instance); + + List nodes = await discovery.DiscoverNodes(1, 2, token).Take(2).ToListAsync(token); + + using (Assert.EnterMultipleScope()) + { + Assert.That(nodes, Is.EqualTo(new[] { 1, 2 })); + Assert.That(kademlia.LookupNodesCalls, Is.EqualTo(1)); + Assert.That(kademlia.LastMaxResults, Is.EqualTo(2)); + } + } + + private sealed class TestKademlia : IKademlia + { + public event EventHandler? OnNodeAdded { add { } remove { } } + public event EventHandler? OnNodeRemoved { add { } remove { } } + + public int LookupNodesCalls { get; private set; } + public int? LastMaxResults { get; private set; } + + public void AddOrRefresh(int node) => throw new NotSupportedException(); + + public void Remove(int node) => throw new NotSupportedException(); + + public Task Run(CancellationToken token) => throw new NotSupportedException(); + + public Task Bootstrap(CancellationToken token) => throw new NotSupportedException(); + + public Task LookupNodesClosest(int key, CancellationToken token, int? k = null) => throw new NotSupportedException(); + + public IAsyncEnumerable LookupNodes(int key, CancellationToken token, int? maxResults = null) + { + LookupNodesCalls++; + LastMaxResults = maxResults; + return CreateAsyncEnumerable(1, 2); + } + + public int[] GetKNeighbour(int target, int excluding = 0, bool excludeSelf = false) => throw new NotSupportedException(); + + public int[] GetAllAtDistance(int distance) => throw new NotSupportedException(); + + public IEnumerable IterateNodes() => throw new NotSupportedException(); + } + + private sealed class IntKeyOperator : IKeyOperator + { + public static IntKeyOperator Instance { get; } = new(); + + public int GetKey(int node) => node; + + public int GetKeyHash(int key) => key; + + public int CreateRandomKeyAtDistance(int nodePrefix, int depth) => depth; + } + + private static async IAsyncEnumerable CreateAsyncEnumerable(params IEnumerable items) + { + foreach (T item in items) + { + await Task.Yield(); + yield return item; + } + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index 313d91ea815a..314638c16a5d 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Channels; using Nethermind.Core.Crypto; @@ -14,7 +13,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; public sealed class NodeSource( IKademlia kademlia, - IIteratorNodeLookup lookup, + IKademliaDiscovery kademliaDiscovery, IKademliaAdapter discv4Adapter, IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, @@ -33,95 +32,26 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance CancellationToken discoveryToken = disposeCts.Token; Channel ch = Channel.CreateBounded(ChannelCapacity); RecentNodeFilter recentlyWrittenNodes = new(_recentNodeLimit); - int duplicated = 0; - int total = 0; - async Task DiscoverAsync(PublicKey target) + async Task DiscoverAsync() { - if (_logger.IsDebug) _logger.Debug($"Looking up {target}"); - bool anyFound = false; - int count = 0; - - await foreach (Node node in lookup.Lookup(target, discoveryToken)) + try { - if (!discv4Adapter.GetSession(node).HasReceivedPong) - { - if (discv4Adapter.GetSession(node).HasTriedPingRecently) - { - // Tried ping before and did not receive a response - continue; - } - if (!await discv4Adapter.Ping(node, discoveryToken)) - { - continue; - } - } - - anyFound = true; - count++; - total++; - if (!recentlyWrittenNodes.TryReserve(node.IdHash)) - { - duplicated++; - continue; - } - - try - { - await ch.Writer.WriteAsync(node, discoveryToken); - } - catch + await foreach (Node node in kademliaDiscovery.DiscoverNodes(discoveryConfig.ConcurrentDiscoveryJob, ChannelCapacity, discoveryToken)) { - recentlyWrittenNodes.Release(node.IdHash); - throw; + await WriteDiscoveredNode(node); } } - - if (!anyFound) + catch (OperationCanceledException) when (discoveryToken.IsCancellationRequested) { - if (_logger.IsDebug) _logger.Debug($"No node found for {target}"); } - else + catch (Exception ex) { - if (_logger.IsDebug) _logger.Debug($"Found {count} nodes"); + if (_logger.IsError) _logger.Error("Kademlia discovery node stream failed.", ex); } } - Task[] discoverTasks = new Task[discoveryConfig.ConcurrentDiscoveryJob]; - for (int i = 0; i < discoverTasks.Length; i++) - { - discoverTasks[i] = Task.Run(async () => - { - Random random = new(); - byte[] randomBytes = new byte[PublicKey.LengthInBytes]; - while (!discoveryToken.IsCancellationRequested) - { - Stopwatch iterationTime = Stopwatch.StartNew(); - - try - { - random.NextBytes(randomBytes); - await DiscoverAsync(new PublicKey(randomBytes)); - - // Prevent high CPU when all node is not reachable due to network connectivity issue. - if (iterationTime.Elapsed < TimeSpan.FromSeconds(1)) - { - await Task.Delay(TimeSpan.FromSeconds(1), discoveryToken); - } - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - if (_logger.IsError) _logger.Error($"Discovery via custom random walk failed.", ex); - } - } - }); - } - - Task discoverTask = Task.WhenAll(discoverTasks); + Task discoverTask = DiscoverAsync(); try { @@ -148,6 +78,37 @@ async Task DiscoverAsync(PublicKey target) yield break; + async Task WriteDiscoveredNode(Node node) + { + if (!discv4Adapter.GetSession(node).HasReceivedPong) + { + if (discv4Adapter.GetSession(node).HasTriedPingRecently) + { + return; + } + + if (!await discv4Adapter.Ping(node, discoveryToken)) + { + return; + } + } + + if (!recentlyWrittenNodes.TryReserve(node.IdHash)) + { + return; + } + + try + { + await ch.Writer.WriteAsync(node, discoveryToken); + } + catch + { + recentlyWrittenNodes.Release(node.IdHash); + throw; + } + } + void Handler(object? _, Node addedNode) { if (!recentlyWrittenNodes.TryReserve(addedNode.IdHash)) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs index 3b8322c70953..324651eb9701 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/AdapterState.cs @@ -32,7 +32,4 @@ public static NonceKey From(ReadOnlySpan nonce) internal sealed record PendingRequest(Node Receiver, Discv5Message Message); -internal readonly record struct SentChallenge(Challenge Challenge, byte[] Packet, long CreatedAtMilliseconds) : IDisposable -{ - public void Dispose() => Challenge.Dispose(); -} +internal readonly record struct SentChallenge(byte[] Packet, long CreatedAtMilliseconds); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 16b0a6928b46..a11fefd77741 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -53,7 +53,7 @@ public sealed class KademliaAdapter( private readonly IKademliaDistance _distance = distance; private readonly ILogger _logger = logManager.GetClassLogger(); private readonly DisposingLruCache _sessions = new(MaxSessions, "discv5 sessions"); - private readonly DisposingLruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); + private readonly LruCache _sentChallenges = new(MaxSentChallenges, "discv5 sent challenges"); private readonly Queue _sentChallengeExpiries = new(); private readonly Lock _sentChallengeExpiriesLock = new(); private long _lastSentChallengeTrimMilliseconds; @@ -377,10 +377,17 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat return; } - Challenge challenge = packetCodec.DecodeWhoAreYou(in packet); - byte[] handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out Session session); + byte[] handshakePacket; + Session session; + ulong requestedEnrSequence; + using (Challenge challenge = packetCodec.DecodeWhoAreYou(in packet)) + { + handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out session); + requestedEnrSequence = challenge.EnrSequence; + } + SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash.ValueHash256, endpoint), session); - if (_logger.IsTrace) _logger.Trace($"Sending discv5 HANDSHAKE for {pendingRequest.Message.MessageType} {pendingRequest.Message.RequestId} to {endpoint}, bytes: {handshakePacket.Length}, requested ENR seq: {challenge.EnrSequence}."); + if (_logger.IsTrace) _logger.Trace($"Sending discv5 HANDSHAKE for {pendingRequest.Message.MessageType} {pendingRequest.Message.RequestId} to {endpoint}, bytes: {handshakePacket.Length}, requested ENR seq: {requestedEnrSequence}."); await discoveryHandler.SendAsync(handshakePacket, endpoint); } @@ -444,51 +451,31 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat if (IsExpired(sentChallenge, Environment.TickCount64)) { - sentChallenge.Dispose(); if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake packet from {endpoint}; matching challenge expired."); return; } - try + TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); + if (!PacketCodec.TryDecode(sentChallenge.Packet, nodeId.Bytes, out Packet challengePacket)) { - TryGetKnownRecord(nodeId, out NodeRecord? knownRecord); - if (!packetCodec.TryDecryptHandshake(in packet, sentChallenge.Challenge, knownRecord, out Session session, out Discv5Message message, out NodeRecord? nodeRecord)) + if (_logger.IsTrace) _logger.Trace($"Unable to decode matching discv5 WHOAREYOU challenge for {endpoint}."); + return; + } + + Session session; + Discv5Message message; + NodeRecord? nodeRecord; + using (challengePacket) + using (Challenge challenge = packetCodec.DecodeWhoAreYou(in challengePacket)) + { + if (!packetCodec.TryDecryptHandshake(in packet, challenge, knownRecord, out session, out message, out nodeRecord)) { if (_logger.IsTrace) _logger.Trace($"Unable to decrypt discv5 handshake packet from {endpoint}."); return; } - - try - { - NodeRecord? messageRecord = knownRecord; - if (nodeRecord is not null) - { - if (!HasExpectedNodeId(nodeRecord, nodeId)) - { - if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake ENR from {endpoint}; ENR node id does not match packet source."); - return; - } - - if (IsAcceptableNodeRecord(nodeRecord, nodeId, endpoint.Address.IsLoopbackOrPrivateOrLinkLocal, recordFilter)) - { - TrySetKnownRecord(nodeId, nodeRecord, out NodeRecord currentRecord); - messageRecord = currentRecord; - } - } - - SetSession(new SessionKey(nodeId, endpoint), session); - if (_logger.IsTrace) _logger.Trace($"Received discv5 handshake message {message.MessageType} {message.RequestId} from {endpoint}, ENR included: {nodeRecord is not null}."); - await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); - } - finally - { - message.Dispose(); - } - } - finally - { - sentChallenge.Dispose(); } + + await HandleHandshakeMessage(endpoint, nodeId, session, message, nodeRecord, knownRecord, token); } private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, ValueHash256 nodeId) @@ -509,12 +496,56 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Valu } ulong enrSequence = TryGetKnownRecord(nodeId, out NodeRecord? record) ? record.EnrSequence : 0UL; - byte[] packet = packetCodec.EncodeWhoAreYou(nodeId.Bytes, requestPacket.Nonce.Span, enrSequence, out Challenge challenge); - SetSentChallenge(challengeKey, challenge, packet); + byte[] packet = packetCodec.EncodeWhoAreYou(nodeId.Bytes, requestPacket.Nonce.Span, enrSequence); + SetSentChallenge(challengeKey, packet); if (_logger.IsTrace) _logger.Trace($"Sending discv5 WHOAREYOU challenge to {endpoint}, known ENR seq: {enrSequence}, bytes: {packet.Length}."); await discoveryHandler.SendAsync(packet, endpoint); } + private async Task HandleHandshakeMessage( + IPEndPoint endpoint, + ValueHash256 nodeId, + Session session, + Discv5Message message, + NodeRecord? nodeRecord, + NodeRecord? knownRecord, + CancellationToken token) + { + bool sessionStored = false; + try + { + NodeRecord? messageRecord = knownRecord; + if (nodeRecord is not null) + { + if (!HasExpectedNodeId(nodeRecord, nodeId)) + { + if (_logger.IsTrace) _logger.Trace($"Ignoring discv5 handshake ENR from {endpoint}; ENR node id does not match packet source."); + return; + } + + if (IsAcceptableNodeRecord(nodeRecord, nodeId, endpoint.Address.IsLoopbackOrPrivateOrLinkLocal, recordFilter)) + { + TrySetKnownRecord(nodeId, nodeRecord, out NodeRecord currentRecord); + messageRecord = currentRecord; + } + } + + SetSession(new SessionKey(nodeId, endpoint), session); + sessionStored = true; + if (_logger.IsTrace) _logger.Trace($"Received discv5 handshake message {message.MessageType} {message.RequestId} from {endpoint}, ENR included: {nodeRecord is not null}."); + await HandleMessage(session.RemotePublicKey, endpoint, message, token, messageRecord); + } + finally + { + if (!sessionStored) + { + session.Dispose(); + } + + message.Dispose(); + } + } + private async Task HandleMessage(PublicKey remotePublicKey, IPEndPoint endpoint, Discv5Message message, CancellationToken token, NodeRecord? nodeRecord = null) { ValueHash256 remoteNodeId = remotePublicKey.Hash.ValueHash256; @@ -781,11 +812,11 @@ internal static bool IsAcceptableNodeRecord(NodeRecord record, ValueHash256 expe internal static bool HasExpectedNodeId(NodeRecord record, ValueHash256 expectedNodeId) => record.GetObj(EnrContentKey.SecP256k1)?.Decompress().Hash == expectedNodeId; - private void SetSentChallenge(ChallengeKey challengeKey, Challenge challenge, byte[] packet) + private void SetSentChallenge(ChallengeKey challengeKey, byte[] packet) { long now = Environment.TickCount64; TryTrimExpiredChallenges(now); - _sentChallenges.Set(challengeKey, new SentChallenge(challenge, packet, now)); + _sentChallenges.Set(challengeKey, new SentChallenge(packet, now)); lock (_sentChallengeExpiriesLock) { _sentChallengeExpiries.Enqueue(new SentChallengeExpiry(challengeKey, now)); @@ -815,10 +846,7 @@ private void TrimExpiredChallenges(long now) if (_sentChallenges.TryGet(expiry.Key, out SentChallenge challenge) && challenge.CreatedAtMilliseconds == expiry.CreatedAtMilliseconds) { - if (_sentChallenges.TryRemove(expiry.Key, out SentChallenge removed)) - { - removed.Dispose(); - } + _sentChallenges.TryRemove(expiry.Key, out _); } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs index 9fb81dc0c663..99561db80e59 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/NodeSource.cs @@ -15,6 +15,8 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; public sealed class NodeSource( IKademlia kademlia, + IKademliaDiscovery kademliaDiscovery, + IDiscoveryConfig discoveryConfig, KademliaConfig kademliaConfig, IDiscv5RecordFilter recordFilter, ILogManager logManager) @@ -33,6 +35,8 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance Channel channel = Channel.CreateBounded(ChannelCapacity); RecentNodeFilter recentlyWrittenNodes = new(_recentNodeLimit); int initialNodes = 0; + using CancellationTokenSource disposeCts = CancellationTokenSource.CreateLinkedTokenSource(token); + CancellationToken discoveryToken = disposeCts.Token; foreach (Node node in kademlia.IterateNodes()) { @@ -47,6 +51,7 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance if (_logger.IsDebug) _logger.Debug($"Discv5 node source emitted {initialNodes} initial nodes from the routing table."); + Task discoverTask = DiscoverAsync(); kademlia.OnNodeAdded += Handler; try { @@ -58,13 +63,51 @@ public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] Cance finally { kademlia.OnNodeAdded -= Handler; + await disposeCts.CancelAsync(); + channel.Writer.TryComplete(); + try + { + await discoverTask; + } + catch (OperationCanceledException) when (discoveryToken.IsCancellationRequested) + { + } + } + + async Task DiscoverAsync() + { + try + { + await foreach (Node node in kademliaDiscovery.DiscoverNodes(discoveryConfig.ConcurrentDiscoveryJob, ChannelCapacity, discoveryToken)) + { + if (!TryReservePeerCandidate(node, out Node? peerCandidate)) + { + continue; + } + + try + { + await channel.Writer.WriteAsync(peerCandidate, discoveryToken); + } + catch + { + recentlyWrittenNodes.Release(peerCandidate.IdHash); + throw; + } + } + } + catch (OperationCanceledException) when (discoveryToken.IsCancellationRequested) + { + } + catch (Exception ex) + { + if (_logger.IsError) _logger.Error("Discv5 Kademlia discovery node stream failed.", ex); + } } void Handler(object? _, Node node) { - if (IsExcluded(node) || - !TryCreatePeerCandidate(node, out Node? peerCandidate) || - !recentlyWrittenNodes.TryReserve(peerCandidate.IdHash)) + if (!TryReservePeerCandidate(node, out Node? peerCandidate)) { return; } @@ -81,6 +124,20 @@ void Handler(object? _, Node node) _logger.Trace($"Discv5 node source queue is full, dropping discovered node {node:s}."); } } + + bool TryReservePeerCandidate(Node node, [NotNullWhen(true)] out Node? peerCandidate) + { + peerCandidate = null; + if (IsExcluded(node) || + !TryCreatePeerCandidate(node, out Node? candidate) || + !recentlyWrittenNodes.TryReserve(candidate.IdHash)) + { + return false; + } + + peerCandidate = candidate; + return true; + } } private bool IsExcluded(Node node) => node.IsBootnode || node.IdHash.Equals(_currentNodeHash); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs index 0cf8d9a34ea7..cc25044f1602 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/Challenge.cs @@ -5,12 +5,33 @@ namespace Nethermind.Network.Discovery.Discv5.Packets; -internal sealed record Challenge(byte[] RequestNonce, byte[] IdNonce, ulong EnrSequence, byte[] ChallengeData) : IDisposable +internal sealed class Challenge : IDisposable { + private readonly ReadOnlyMemory _challengeData; + private readonly byte[]? _ownedChallengeData; + + public Challenge(ulong enrSequence, ReadOnlyMemory challengeData) + { + EnrSequence = enrSequence; + _challengeData = challengeData; + } + + public Challenge(ulong enrSequence, byte[] ownedChallengeData) + { + EnrSequence = enrSequence; + _ownedChallengeData = ownedChallengeData; + _challengeData = ownedChallengeData; + } + + public ulong EnrSequence { get; } + + public ReadOnlySpan ChallengeData => _challengeData.Span; + public void Dispose() { - CryptographicOperations.ZeroMemory(RequestNonce); - CryptographicOperations.ZeroMemory(IdNonce); - CryptographicOperations.ZeroMemory(ChallengeData); + if (_ownedChallengeData is not null) + { + CryptographicOperations.ZeroMemory(_ownedChallengeData); + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index 4f64538cc1c0..eacbc75bd3a4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -62,16 +62,14 @@ internal byte[] EncodeOrdinary(PublicKey destination, ReadOnlySpan encrypt => EncodePacket(destination.Hash.Bytes, PacketFlag.Ordinary, nonce, _localNodeId.Bytes, encryptionKey, message); [SkipLocalsInit] - internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySpan requestNonce, ulong enrSequence, out Challenge challenge) + internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySpan requestNonce, ulong enrSequence) { - byte[] idNonce = _cryptoRandom.GenerateRandomBytes(IdNonceSize); Span authData = stackalloc byte[WhoAreYouAuthDataSize]; - idNonce.CopyTo(authData); + Span idNonce = authData[..IdNonceSize]; + _cryptoRandom.GenerateRandomBytes(idNonce); BinaryPrimitives.WriteUInt64BigEndian(authData[IdNonceSize..], enrSequence); - byte[] packet = EncodePacket(destinationNodeId, PacketFlag.WhoAreYou, requestNonce, authData, default, null, out byte[] challengeData); - challenge = new Challenge(requestNonce.ToArray(), idNonce, enrSequence, challengeData); - return packet; + return EncodePacket(destinationNodeId, PacketFlag.WhoAreYou, requestNonce, authData, default, null); } [SkipLocalsInit] @@ -268,9 +266,8 @@ internal Challenge DecodeWhoAreYou(scoped in Packet packet) throw new RlpException("Invalid WHOAREYOU authdata length."); } - byte[] idNonce = packet.AuthData.Span[..IdNonceSize].ToArray(); ulong enrSequence = BinaryPrimitives.ReadUInt64BigEndian(packet.AuthData.Span[IdNonceSize..]); - return new Challenge(packet.Nonce.ToArray(), idNonce, enrSequence, packet.ChallengeData.ToArray()); + return new Challenge(enrSequence, packet.ChallengeData); } internal bool TryDecryptHandshake( @@ -354,24 +351,14 @@ private byte[] EncodePacket( ReadOnlySpan authData, ReadOnlySpan encryptionKey, Discv5Message? message) - => EncodePacket(destinationNodeId, flag, nonce, authData, encryptionKey, message, out _); - - private byte[] EncodePacket( - ReadOnlySpan destinationNodeId, - PacketFlag flag, - ReadOnlySpan nonce, - ReadOnlySpan authData, - ReadOnlySpan encryptionKey, - Discv5Message? message, - out byte[] messageAd) { if (message is null) { - return EncodePacketCore(destinationNodeId, flag, nonce, authData, default, default, out messageAd); + return EncodePacketCore(destinationNodeId, flag, nonce, authData, default, default); } using NettyRlpStream encodedMessage = MessageCodec.Encode(message); - return EncodePacketCore(destinationNodeId, flag, nonce, authData, encryptionKey, encodedMessage.AsSpan(), out messageAd); + return EncodePacketCore(destinationNodeId, flag, nonce, authData, encryptionKey, encodedMessage.AsSpan()); } [SkipLocalsInit] @@ -381,8 +368,7 @@ private byte[] EncodePacketCore( ReadOnlySpan nonce, ReadOnlySpan authData, ReadOnlySpan encryptionKey, - ReadOnlySpan plaintext, - out byte[] messageAd) + ReadOnlySpan plaintext) { int headerLength = StaticHeaderSize + authData.Length; int messageAdLength = MaskingIvSize + headerLength; @@ -413,9 +399,6 @@ private byte[] EncodePacketCore( } AesCtrTransform(destinationNodeId[..AesKeySize], maskingIv, messageAdBuffer.Slice(MaskingIvSize, headerLength), packet.AsSpan(MaskingIvSize, headerLength)); - messageAd = plaintext.IsEmpty - ? messageAdBuffer.ToArray() - : []; return packet; } finally diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs deleted file mode 100644 index 4ce9afa45813..000000000000 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/IteratorNodeLookup.cs +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Runtime.CompilerServices; -using Nethermind.Core; -using Nethermind.Core.Caching; -using Nethermind.Core.Extensions; -using Nethermind.Core.Utils; -using Nethermind.Kademlia; -using Nethermind.Logging; -using NonBlocking; - -namespace Nethermind.Network.Discovery.Kademlia; - -/// -/// Special lookup made specially for node discovery as the standard lookup is too slow or unnecessarily parallelized. -/// Instead of returning k closest node, it just returns the nodes that it found along the way and stopped early. -/// This is useful for node discovery as trying to get the k closest node is not completely necessary, as the main goal -/// is to reach all node. The lookup is not parallelized as it is expected to be parallelized at a higher level with -/// each worker having different target to look into. -/// -public sealed class IteratorNodeLookup( - IRoutingTable routingTable, - KademliaConfig kademliaConfig, - IKademliaMessageSender msgSender, - IKeyOperator keyOperator, - IKademliaDistance distance, - ITimestamper timestamper, - ILogManager logManager) : IIteratorNodeLookup - where TNode : notnull - where TKadKey : notnull -{ - private readonly ILogger _logger = logManager.GetClassLogger>(); - private readonly TKadKey _currentNodeIdAsHash = keyOperator.GetNodeHash(kademliaConfig.CurrentNodeId); - - // Small lru of unreachable nodes, prevent retrying. Pretty effective, although does not improve discovery overall. - private readonly LruCache _unreachableNodes = new(256, ""); - - // The maximum round per lookup. Higher means that it will 'see' deeper into the network, but come at a latency - // cost of trying many node for increasingly lower new node. - private const int MaxRounds = 3; - - // These two dont come into effect as MaxRounds is low. - private const int MaxNonProgressingRound = 3; - private const int MinResult = 128; - - private bool SameAsSelf(TNode node) => EqualityComparer.Default.Equals(keyOperator.GetNodeHash(node), _currentNodeIdAsHash); - - public async IAsyncEnumerable Lookup(TKey target, [EnumeratorCancellation] CancellationToken token) - { - TKadKey targetHash = keyOperator.GetKeyHash(target); - if (_logger.IsDebug) _logger.Debug($"Initiate lookup for hash {targetHash}"); - - using AutoCancelTokenSource cts = token.CreateChildTokenSource(); - token = cts.Token; - - ConcurrentDictionary queried = new(); - ConcurrentDictionary seen = new(); - - IComparer comparer = Comparer.Create((h1, h2) => - distance.Compare(h1, h2, targetHash)); - - // Ordered by lowest distance. Will get popped for next round. - PriorityQueue<(TKadKey, TNode), TKadKey> queryQueue = new(comparer); - - // Used to determine if the worker should stop - TKadKey bestNodeId = distance.Zero; - bool hasBestNodeId = false; - int closestNodeRound = 0; - int currentRound = 0; - int totalResult = 0; - - // Check internal table first - foreach (TNode node in routingTable.GetKNearestNeighbour(targetHash)) - { - TKadKey nodeHash = keyOperator.GetNodeHash(node); - seen.TryAdd(nodeHash, node); - - queryQueue.Enqueue((nodeHash, node), nodeHash); - - yield return node; - - if (!hasBestNodeId || comparer.Compare(nodeHash, bestNodeId) < 0) - { - bestNodeId = nodeHash; - hasBestNodeId = true; - } - } - - while (true) - { - token.ThrowIfCancellationRequested(); - if (!queryQueue.TryDequeue(out (TKadKey hash, TNode node) toQuery, out _)) - { - // No node to query and running query. - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No node to query."); - yield break; - } - - if (SameAsSelf(toQuery.node)) continue; - - queried.TryAdd(toQuery.hash, toQuery.node); - if (_logger.IsTrace) _logger.Trace($"Query {toQuery.node} at round {currentRound}"); - - TNode[]? neighbours = await FindNeighbour(toQuery.node, target, token); - if (neighbours == null || neighbours?.Length == 0) - { - if (_logger.IsTrace) _logger.Trace("Empty result"); - continue; - } - - int queryIgnored = 0; - int seenIgnored = 0; - foreach (TNode neighbour in neighbours!) - { - TKadKey neighbourHash = keyOperator.GetNodeHash(neighbour); - - // Already queried, we ignore - if (queried.ContainsKey(neighbourHash)) - { - queryIgnored++; - continue; - } - - // When seen already dont record - if (!seen.TryAdd(neighbourHash, neighbour)) - { - seenIgnored++; - continue; - } - - totalResult++; - yield return neighbour; - - bool foundBetter = comparer.Compare(neighbourHash, bestNodeId) < 0; - queryQueue.Enqueue((neighbourHash, neighbour), neighbourHash); - - // If found a better node, reset closes node round. - // This causes `ShouldStopDueToNoBetterResult` to return false. - if (closestNodeRound < currentRound && foundBetter) - { - if (_logger.IsTrace) - _logger.Trace($"Found better neighbour {neighbour} at round {currentRound}."); - bestNodeId = neighbourHash; - closestNodeRound = currentRound; - } - } - - if (_logger.IsTrace) - _logger.Trace($"Count {neighbours.Length}, queried {queryIgnored}, seen {seenIgnored}"); - - if (ShouldStop()) - { - if (_logger.IsTrace) _logger.Trace("Stopping lookup. No better result."); - break; - } - } - - if (_logger.IsTrace) _logger.Trace("Lookup operation finished."); - yield break; - - bool ShouldStop() - { - int round = ++currentRound; - if (totalResult >= MinResult && round - closestNodeRound >= MaxNonProgressingRound) - { - // No closer node for more than or equal to _alpha*2 round. - // Assume exit condition - // Why not just _alpha? - // Because there could be currently running work that may increase closestNodeRound. - // So including this worker, assume no more - if (_logger.IsTrace) _logger.Trace($"No more closer node. Round: {round}, closestNodeRound {closestNodeRound}"); - return true; - } - - return round >= MaxRounds; - } - } - - async Task FindNeighbour(TNode node, TKey target, CancellationToken token) - { - try - { - TKadKey nodeHash = keyOperator.GetNodeHash(node); - if (_unreachableNodes.TryGet(nodeHash, out DateTimeOffset lastAttempt) && - lastAttempt + TimeSpan.FromMinutes(5) > timestamper.UtcNowOffset) - { - return []; - } - - TNode[]? result = await msgSender.FindNeighbours(node, target, token); - if (result is null) - { - _unreachableNodes.Set(nodeHash, timestamper.UtcNowOffset); - } - - return result; - } - catch (OperationCanceledException) when (!token.IsCancellationRequested) - { - _unreachableNodes.Set(keyOperator.GetNodeHash(node), timestamper.UtcNowOffset); - return null; - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception e) - { - if (_logger.IsDebug) _logger.Debug($"Find neighbour op failed. {e}"); - return null; - } - } - -} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs index 267d6d69c599..4a389b7459cb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/KademliaModule.cs @@ -34,10 +34,10 @@ protected override void Load(ContainerBuilder builder) builder .AddSingleton, Kademlia>() + .AddSingleton, RandomWalkKademliaDiscovery>() .AddSingleton, LookupKNearestNeighbour>() .AddSingleton, FromKeyNodeHashProvider>() .AddSingleton, KBucketTree>() - .AddSingleton, IteratorNodeLookup>() .AddSingleton, NodeHealthTracker>(); } } From dddbe6b232f5197c6da019bc5c14c2dedcbb529e Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 22 Jun 2026 19:46:17 +0300 Subject: [PATCH 178/182] Fix stab config --- .../DiscoveryKademliaConfigFactoryTests.cs | 27 +++++++++++++++++++ .../Discv4/DiscoveryApp.cs | 6 +++-- .../Discv4/Kademlia/KademliaModule.cs | 4 +-- .../Discv5/DiscoveryV5App.cs | 3 ++- .../Discv5/Kademlia/KademliaModule.cs | 2 +- .../DiscoveryKademliaConfigFactory.cs | 6 ++--- .../Kademlia/DiscoveryKademliaModuleBase.cs | 4 +-- 7 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DiscoveryKademliaConfigFactoryTests.cs diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DiscoveryKademliaConfigFactoryTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DiscoveryKademliaConfigFactoryTests.cs new file mode 100644 index 000000000000..be75fe51e37d --- /dev/null +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/DiscoveryKademliaConfigFactoryTests.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Test.Builders; +using Nethermind.Kademlia; +using Nethermind.Network.Discovery.Kademlia; +using Nethermind.Stats.Model; +using NUnit.Framework; + +namespace Nethermind.Network.Discovery.Test.Kademlia; + +public class DiscoveryKademliaConfigFactoryTests +{ + [Test] + public void Create_ShouldUseProvidedCurrentNode() + { + Node currentNode = new(TestItem.PublicKeyA, "192.0.2.10", 30304, true); + + KademliaConfig config = DiscoveryKademliaConfigFactory.Create( + currentNode, + [], + new DiscoveryConfig()); + + Assert.That(config.CurrentNodeId, Is.SameAs(currentNode)); + Assert.That(config.CurrentNodeId.Address, Is.EqualTo(currentNode.Address)); + } +} diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs index 56b90a6a90f9..6226b63f642f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/DiscoveryApp.cs @@ -40,9 +40,11 @@ public DiscoveryApp( _discv4Services = rootScope.BeginLifetimeScope( (builder) => { + Node currentNode = new(enode.PublicKey, enode.HostIp.ToString(), networkConfig.DiscoveryPort, true); + builder - .AddModule(new KademliaModule(enode.PublicKey, bootNodes)) - .AddSingleton(); + .AddModule(new KademliaModule(currentNode, bootNodes)) + .AddSingleton(); configureDiscv4Services?.Invoke(builder); }); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index 8c682a6b3dd3..6c28a1dc6a37 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -15,9 +15,9 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; /// Because kademlia can and probably will be reused outside of discv4, this module is meant to be added within a child /// lifecycle in to prevent unexpected conflict. /// -/// +/// /// -public sealed class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) +public sealed class KademliaModule(Node currentNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(currentNode, bootNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index b4b78facc116..11ef37d3f158 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -56,8 +56,9 @@ public DiscoveryV5App( { builder.RegisterInstance(discoveryConfig).As(); builder.RegisterInstance(timestamper).As(); + Node currentNode = new(nodeKey.PublicKey, ipResolver.ExternalIp.ToString(), networkConfig.DiscoveryPort, true); builder - .AddModule(new Discv5KademliaModule(nodeKey.PublicKey, bootNodes)) + .AddModule(new Discv5KademliaModule(currentNode, bootNodes)) .AddSingleton(); configureDiscv5Services?.Invoke(builder); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs index 0e0f1c86bcbf..e89fd16de896 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs @@ -14,7 +14,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Specifies the protocol-specific Kademlia services used by discv5. /// -public sealed class KademliaModule(PublicKey masterNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(masterNode, bootNodes) +public sealed class KademliaModule(Node currentNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(currentNode, bootNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder .AddSingleton(ExecutionLayerDiscv5RecordFilter.Instance) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs index 74928046ef03..28ebdf9990cb 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaConfigFactory.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core.Crypto; using Nethermind.Kademlia; using Nethermind.Stats.Model; @@ -9,11 +8,10 @@ namespace Nethermind.Network.Discovery.Kademlia; internal static class DiscoveryKademliaConfigFactory { - public static KademliaConfig Create(PublicKey masterNode, IReadOnlyList bootNodes, IDiscoveryConfig discoveryConfig) + public static KademliaConfig Create(Node currentNode, IReadOnlyList bootNodes, IDiscoveryConfig discoveryConfig) => new() { - // The table only needs the local node identity here; its endpoint is never contacted. - CurrentNodeId = new Node(masterNode, "127.0.0.1", 9999, true), + CurrentNodeId = currentNode, KSize = discoveryConfig.BucketSize, Alpha = discoveryConfig.Concurrency, Beta = discoveryConfig.BitsPerHop, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs index 4b65d0f203df..5137b5ead19e 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs @@ -9,7 +9,7 @@ namespace Nethermind.Network.Discovery.Kademlia; -public abstract class DiscoveryKademliaModuleBase(PublicKey masterNode, IReadOnlyList bootNodes) : Module +public abstract class DiscoveryKademliaModuleBase(Node currentNode, IReadOnlyList bootNodes) : Module { protected override void Load(ContainerBuilder builder) { @@ -20,7 +20,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() .AddSingleton() - .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(masterNode, bootNodes, discoveryConfig)); + .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(currentNode, bootNodes, discoveryConfig)); } protected abstract void RegisterProtocolServices(ContainerBuilder builder); From eddc286cb00f9f899b98485522167bf789266342 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 22 Jun 2026 20:06:31 +0300 Subject: [PATCH 179/182] Fix discv4 node source self emission --- .../Discv4/Kademlia/NodeSourceTests.cs | 2 +- .../Discv4/Kademlia/NodeSource.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs index 3d0543f03835..27271dec09fa 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs @@ -49,7 +49,7 @@ public void Setup() }; _kademliaConfig = new() { - CurrentNodeId = new Node(TestItem.PublicKeyA, "127.0.0.1", 30303), + CurrentNodeId = new Node(TestItem.PublicKeyD, "127.0.0.1", 30303), KSize = 1 }; diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs index 314638c16a5d..27f8a385f2f5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/NodeSource.cs @@ -23,6 +23,7 @@ public sealed class NodeSource( private const int ChannelCapacity = 64; private readonly ILogger _logger = logManager.GetClassLogger(); + private readonly Hash256 _currentNodeHash = kademliaConfig.CurrentNodeId.IdHash; private readonly int _recentNodeLimit = RecentNodeFilter.GetLimit(kademliaConfig.KSize, Hash256KademliaDistance.Instance.MaxDistance, ChannelCapacity); public async IAsyncEnumerable DiscoverNodes([EnumeratorCancellation] CancellationToken token) @@ -80,6 +81,11 @@ async Task DiscoverAsync() async Task WriteDiscoveredNode(Node node) { + if (IsExcluded(node)) + { + return; + } + if (!discv4Adapter.GetSession(node).HasReceivedPong) { if (discv4Adapter.GetSession(node).HasTriedPingRecently) @@ -111,6 +117,11 @@ async Task WriteDiscoveredNode(Node node) void Handler(object? _, Node addedNode) { + if (IsExcluded(addedNode)) + { + return; + } + if (!recentlyWrittenNodes.TryReserve(addedNode.IdHash)) { return; @@ -124,4 +135,6 @@ void Handler(object? _, Node addedNode) recentlyWrittenNodes.Release(addedNode.IdHash); } } + + private bool IsExcluded(Node node) => node.IdHash.Equals(_currentNodeHash); } From 6eb3c623a2390bed78e27616254604c9fed22224 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 22 Jun 2026 20:14:09 +0300 Subject: [PATCH 180/182] Remove unused merge usings --- src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index 321677fbc4cc..dc15b6575a40 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -17,9 +17,7 @@ using Nethermind.Network.Config; using Nethermind.Network.Contract.P2P; using Nethermind.Network.Discovery.Discv4; -using Nethermind.Network.P2P.ProtocolHandlers; using Nethermind.Network.Rlpx; -using Nethermind.Stats; using Nethermind.Stats.Model; using Nethermind.Synchronization; using Nethermind.Synchronization.Peers; From 2db358ba22a3fa60a4cc5eb759d7d5f244950158 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 22 Jun 2026 20:37:18 +0300 Subject: [PATCH 181/182] Address discv5 review feedback --- src/Nethermind/Nethermind.Core/Caching/LruCache.cs | 4 ++-- .../Discv5/CodecTests.cs | 9 --------- .../Discv5/KademliaAdapterTests.cs | 1 - .../Discv5/WireTests.cs | 1 - .../Discv5/Kademlia/KademliaAdapter.cs | 10 +++++++++- .../Discv5/Packets/PacketCodec.cs | 5 +---- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs index 1b9b16d1ad0a..e205d56848a3 100644 --- a/src/Nethermind/Nethermind.Core/Caching/LruCache.cs +++ b/src/Nethermind/Nethermind.Core/Caching/LruCache.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Nethermind.Core.Extensions; using Nethermind.Core.Threading; @@ -256,9 +255,10 @@ public KeyValuePair[] ToArray() return array; } - [MethodImpl(MethodImplOptions.Synchronized)] public TValue[] GetValues() { + using McsLock.Disposable lockRelease = _lock.Acquire(); + int i = 0; TValue[] array = new TValue[_cacheMap.Count]; foreach (KeyValuePair> kvp in _cacheMap) diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs index c48c1bf4bb25..6e8481d7d6f2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/CodecTests.cs @@ -14,7 +14,6 @@ using NUnit.Framework; using System; using System.Net; -using System.Threading; using System.Threading.Tasks; namespace Nethermind.Network.Discovery.Test.Discv5; @@ -361,7 +360,6 @@ public void MessageCodec_Rejects_Too_Many_Nodes_Records() private static PacketCodec CreateCodec(PrivateKey privateKey) => new( new InsecureProtectedPrivateKey(privateKey), - new TestNodeRecordProvider(privateKey), new CryptoRandom(), new EthereumEcdsa(0)); @@ -390,11 +388,4 @@ private static void AssertRequestId(RequestId requestId, ReadOnlySpan expe requestId.CopyTo(actual); Assert.That(actual[..requestId.Length].SequenceEqual(expected), Is.True); } - - private sealed class TestNodeRecordProvider(PrivateKey privateKey) : INodeRecordProvider - { - private readonly NodeRecord _current = CreateNodeRecord(privateKey); - - public ValueTask GetCurrentAsync(CancellationToken cancellationToken = default) => new(_current); - } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs index 85694c3f4a13..5ee5306d48a2 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/KademliaAdapterTests.cs @@ -117,7 +117,6 @@ private KademliaAdapter CreateAdapter() _packetCodec?.Dispose(); _packetCodec = new PacketCodec( new InsecureProtectedPrivateKey(TestItem.PrivateKeyA), - nodeRecordProvider, new CryptoRandom(), new EthereumEcdsa(0)); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index d53671417624..e95c4dc0163f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -209,7 +209,6 @@ private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, b handler, new PacketCodec( new InsecureProtectedPrivateKey(privateKey), - nodeRecordProvider, new CryptoRandom(), new EthereumEcdsa(0)), nodeRecordProvider, diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 65e11de64732..4bf4870f50b5 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -382,7 +382,8 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat ulong requestedEnrSequence; using (Challenge challenge = packetCodec.DecodeWhoAreYou(in packet)) { - handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, out session); + NodeRecord currentNodeRecord = await nodeRecordProvider.GetCurrentAsync(token); + handshakePacket = packetCodec.EncodeHandshake(pendingRequest.Receiver.Id, challenge, pendingRequest.Message, currentNodeRecord, out session); requestedEnrSequence = challenge.EnrSequence; } @@ -709,6 +710,13 @@ private void RegisterKnownRecord(Node node) return; } + ValueHash256 nodeId = node.Id.Hash.ValueHash256; + if (TryGetKnownRecord(nodeId, out NodeRecord? knownRecord) && + knownRecord.EnrString == node.Enr) + { + return; + } + try { NodeRecord record = NodeRecord.FromEnrString(node.Enr); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs index f72f0a930ebd..7ac197a21506 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Packets/PacketCodec.cs @@ -18,7 +18,6 @@ namespace Nethermind.Network.Discovery.Discv5.Packets; public sealed class PacketCodec( [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, - INodeRecordProvider nodeRecordProvider, ICryptoRandom cryptoRandom, IEcdsa ecdsa) : IDisposable { @@ -47,7 +46,6 @@ public sealed class PacketCodec( private readonly PrivateKey _privateKey = nodeKey.Unprotect(); private readonly PublicKey _publicKey = nodeKey.PublicKey; private readonly ValueHash256 _localNodeId = nodeKey.PublicKey.Hash.ValueHash256; - private readonly INodeRecordProvider _nodeRecordProvider = nodeRecordProvider; private readonly ICryptoRandom _cryptoRandom = cryptoRandom; private readonly IEcdsa _ecdsa = ecdsa; private readonly ObjectPool _decodeMaskingAesPool = CreateDecodeMaskingAesPool(nodeKey.PublicKey.Hash.Bytes[..AesKeySize]); @@ -73,7 +71,7 @@ internal byte[] EncodeWhoAreYou(ReadOnlySpan destinationNodeId, ReadOnlySp } [SkipLocalsInit] - internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Discv5Message message, out Session session) + internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Discv5Message message, NodeRecord currentNodeRecord, out Session session) { using PrivateKey ephemeralKey = new PrivateKeyGenerator(_cryptoRandom).Generate(); DeriveKeys( @@ -86,7 +84,6 @@ internal byte[] EncodeHandshake(PublicKey destination, Challenge challenge, Disc out byte[] recipientKey); byte[] ephemeralPublicKey = ephemeralKey.CompressedPublicKey.Bytes; - NodeRecord currentNodeRecord = _nodeRecordProvider.GetCurrentAsync().GetAwaiter().GetResult(); byte[] record = challenge.EnrSequence < currentNodeRecord.EnrSequence ? currentNodeRecord.ToRlpBytes() : []; From ae1fb22e6e9b9a98b10302ba4e4584c0d26b037d Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 23 Jun 2026 12:10:49 +0300 Subject: [PATCH 182/182] Refine discovery review fixes --- .../Modules/DiscoveryModule.cs | 1 + .../Nethermind.Kademlia/IKademlia.cs | 2 +- .../Nethermind.Kademlia/IRoutingTable.cs | 2 +- src/Nethermind/Nethermind.Kademlia/KBucket.cs | 4 +- .../Nethermind.Kademlia/KBucketTree.cs | 38 +++--- .../Nethermind.Kademlia/Kademlia.cs | 16 +-- .../Nethermind.Kademlia/KademliaConfig.cs | 5 + .../Nethermind.Kademlia/NodeHealthTracker.cs | 3 +- .../Nethermind.Kademlia/RoutingTableBucket.cs | 20 ++++ .../DiscoveryV5AppTests.cs | 13 ++- .../Discv4/Kademlia/KademliaAdapterTests.cs | 12 +- .../Discv4/Kademlia/NodeSourceTests.cs | 14 +-- .../Discv4/NettyDiscoveryHandlerTests.cs | 52 +++++++-- .../Discv4/NodeSourceToDiscV4FeederTests.cs | 15 ++- .../Discv5/WireTests.cs | 61 ++++++---- .../Kademlia/KBucketTests.cs | 6 +- .../Kademlia/KademliaTests.cs | 108 ++++++++++++------ .../Kademlia/NodeHealthTrackerTests.cs | 2 +- .../NettyDiscoveryV5HandlerTests.cs | 39 ++++++- .../Discv4/Kademlia/KademliaModule.cs | 3 +- .../Discv5/DiscoveryV5App.cs | 5 +- .../Discv5/Kademlia/KademliaAdapter.cs | 31 ++--- .../Discv5/Kademlia/KademliaModule.cs | 3 +- .../Discv5/NettyDiscoveryV5Handler.cs | 6 +- .../Kademlia/DiscoveryKademliaModuleBase.cs | 11 +- .../Kademlia/DiscoveryPersistenceManager.cs | 4 +- .../NodeRecordSignerTests.cs | 26 +++++ .../Nethermind.Network.Enr/EthEntry.cs | 15 ++- .../Nethermind.Network/IP/WebIPSource.cs | 8 +- .../DatabasePurgerTests.cs | 3 +- .../Nethermind.Runner/DatabasePurger.cs | 3 +- 31 files changed, 370 insertions(+), 161 deletions(-) create mode 100644 src/Nethermind/Nethermind.Kademlia/RoutingTableBucket.cs diff --git a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs index 295d10525e99..4b198c09152b 100644 --- a/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/DiscoveryModule.cs @@ -111,6 +111,7 @@ protected override void Load(ContainerBuilder builder) .AddSingleton() .AddNetworkStorage(DbNames.DiscoveryNodes, DbNames.DiscoveryNodes) + .AddNetworkStorage(DbNames.DiscoveryV5Nodes, DbNames.DiscoveryV5Nodes) ; diff --git a/src/Nethermind/Nethermind.Kademlia/IKademlia.cs b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs index 58fa58a785d9..5e58fc424609 100644 --- a/src/Nethermind/Nethermind.Kademlia/IKademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/IKademlia.cs @@ -56,7 +56,7 @@ public interface IKademlia /// Protocol-specific lookup key. /// Optional node to exclude from the result. /// Whether to exclude the local node from the result. - /// The returned array is not sorted and may be an internal routing-table array. + /// The returned array is not sorted. TNode[] GetKNeighbour(TKey target, TNode? excluding = default, bool excludeSelf = false); /// diff --git a/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs b/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs index a087f88ef204..049a4f2ef6f6 100644 --- a/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs +++ b/src/Nethermind/Nethermind.Kademlia/IRoutingTable.cs @@ -12,7 +12,7 @@ public interface IRoutingTable TNode[] GetKNearestNeighbour(TKadKey hash, bool excludeSelf = false); TNode[] GetKNearestNeighbourExcluding(TKadKey hash, TKadKey exclude, bool excludeSelf = false); TNode[] GetAllAtDistance(int i); - IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> IterateBuckets(); + IEnumerable> IterateBuckets(); TNode? GetByHash(TKadKey nodeId); void LogDebugInfo(); event EventHandler? OnNodeAdded; diff --git a/src/Nethermind/Nethermind.Kademlia/KBucket.cs b/src/Nethermind/Nethermind.Kademlia/KBucket.cs index 5eb239bb25d3..87bf6912df81 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucket.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucket.cs @@ -47,7 +47,9 @@ public BucketAddResult TryAddOrRefresh(in TKadKey hash, TNode item, out TNode? t return BucketAddResult.Full; } - public TNode[] GetAll() => _cacheItems ? _cachedArray : _items.GetAll(); + public TNode[] GetAll() => _items.GetAll(); + + internal TNode[] GetAllCached() => _cacheItems ? _cachedArray : _items.GetAll(); public (TKadKey, TNode)[] GetAllWithHash() => _items.GetAllWithKey(); diff --git a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs index fb888b6eac6f..5e83c69178eb 100644 --- a/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs +++ b/src/Nethermind/Nethermind.Kademlia/KBucketTree.cs @@ -213,7 +213,7 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, P } else { - TNode[] items = node.Bucket.GetAll(); + TNode[] items = node.Bucket.GetAllCached(); for (int i = 0; i < items.Length; i++) { result.Add(items[i]); @@ -255,29 +255,29 @@ private void GetAllAtDistanceRecursive(TreeNode node, int depth, int distance, P } } - public IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> IterateBuckets() + public IEnumerable> IterateBuckets() { lock (_lock) { - // Well, it need to ToArray, otherwise the lock does not really do anything. + // Materialize snapshots while holding the tree lock so callers cannot observe live bucket state. return DoIterateBucketRandomHashes(_root, 0).ToArray(); } } - private IEnumerable<(TKadKey Prefix, int Distance, KBucket Bucket)> DoIterateBucketRandomHashes(TreeNode node, int depth) + private IEnumerable> DoIterateBucketRandomHashes(TreeNode node, int depth) { if (node.IsLeaf) { - yield return (node.Prefix, depth, node.Bucket); + yield return new RoutingTableBucket(node.Prefix, depth, node.Bucket.GetAll()); } else { - foreach ((TKadKey Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) + foreach (RoutingTableBucket bucketInfo in DoIterateBucketRandomHashes(node.Left!, depth + 1)) { yield return bucketInfo; } - foreach ((TKadKey Prefix, int Distance, KBucket Bucket) bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) + foreach (RoutingTableBucket bucketInfo in DoIterateBucketRandomHashes(node.Right!, depth + 1)) { yield return bucketInfo; } @@ -344,12 +344,11 @@ private TNode[] GetKNearestNeighbour(TKadKey hash, TKadKey exclude, bool hasExcl if (shouldNotContainExcludedNode && shouldNotContainSelf) { - TNode[] nodes = firstBucket.GetAll(); + TNode[] nodes = firstBucket.GetAllCached(); if (nodes.Length == _k) { - // Fast path. In theory, most of the time, this would be the taken path, where no array - // concatenation or creation is needed. - return nodes; + // Fast path. In theory, most of the time, this avoids neighbour traversal and concatenation. + return (TNode[])nodes.Clone(); } } @@ -451,11 +450,24 @@ public int Size get { int total = 0; - foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in IterateBuckets()) + lock (_lock) { - total += Bucket.Count; + CountNodes(_root); } + return total; + + void CountNodes(TreeNode node) + { + if (node.IsLeaf) + { + total += node.Bucket.Count; + return; + } + + CountNodes(node.Left!); + CountNodes(node.Right!); + } } } } diff --git a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs index 6ccb893c6886..14186c4d513f 100644 --- a/src/Nethermind/Nethermind.Kademlia/Kademlia.cs +++ b/src/Nethermind/Nethermind.Kademlia/Kademlia.cs @@ -169,12 +169,12 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => // Refresh stale non-empty buckets one by one. Protocols whose wire lookup target cannot be synthesized from a // bucket prefix may return a best-effort random lookup key here; discv4 public keys are one example. using PooledSet activeBucketPrefixes = new(); - foreach ((TKadKey Prefix, int Distance, KBucket Bucket) in _routingTable.IterateBuckets()) + foreach (RoutingTableBucket bucket in _routingTable.IterateBuckets()) { - activeBucketPrefixes.Add(Prefix); - if (!ShouldRefreshBucket(Prefix, Bucket)) continue; + activeBucketPrefixes.Add(bucket.Prefix); + if (!ShouldRefreshBucket(bucket.Prefix, bucket.Count)) continue; - TKey? keyToLookup = _keyOperator.CreateRandomKeyAtDistance(Prefix, Distance); + TKey? keyToLookup = _keyOperator.CreateRandomKeyAtDistance(bucket.Prefix, bucket.Distance); await LookupNodesClosest(keyToLookup, token); } @@ -187,9 +187,9 @@ await Parallel.ForEachAsync(_bootNodes, token, async (node, token) => } } - private bool ShouldRefreshBucket(TKadKey prefix, KBucket bucket) + private bool ShouldRefreshBucket(TKadKey prefix, int bucketCount) { - if (bucket.Count == 0) return false; + if (bucketCount == 0) return false; long nowTicks = _timeProvider.GetUtcNow().Ticks; lock (_lastBucketRefreshLock) @@ -246,9 +246,9 @@ public event EventHandler OnNodeRemoved public IEnumerable IterateNodes() { - foreach ((TKadKey _, int _, KBucket Bucket) in _routingTable.IterateBuckets()) + foreach (RoutingTableBucket bucket in _routingTable.IterateBuckets()) { - foreach (TNode node in Bucket.GetAll()) + foreach (TNode node in bucket.Nodes) { yield return node; } diff --git a/src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs b/src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs index a6706f73b3c4..9a2a7a529223 100644 --- a/src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs +++ b/src/Nethermind/Nethermind.Kademlia/KademliaConfig.cs @@ -50,6 +50,11 @@ public class KademliaConfig /// public TimeSpan RefreshPingTimeout { get; set; } = TimeSpan.FromSeconds(10); + /// + /// Delay before pinging the oldest node in a full bucket to give incoming traffic a chance to refresh it first. + /// + public TimeSpan RefreshPingDelay { get; set; } = TimeSpan.FromMilliseconds(100); + /// /// Starting boot nodes. /// diff --git a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs index a9a25dda6c0d..d79e34aa038e 100644 --- a/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs +++ b/src/Nethermind/Nethermind.Kademlia/NodeHealthTracker.cs @@ -26,6 +26,7 @@ public class NodeHealthTracker( private readonly PeerFailureCache _peerFailures = new(1024); private readonly TKadKey _currentNodeIdAsHash = nodeHashProvider.GetHash(config.CurrentNodeId); private readonly TimeSpan _refreshPingTimeout = config.RefreshPingTimeout; + private readonly TimeSpan _refreshPingDelay = config.RefreshPingDelay; private readonly CancellationTokenSource _refreshCancellation = new(); private int _disposed; @@ -52,7 +53,7 @@ private async Task RefreshAsync(TNode toRefresh, TKadKey nodeHash, CancellationT try { // First, we delay in case any new message come and clear the refresh task, so we don't need to send any ping. - await Task.Delay(100, token); + await Task.Delay(_refreshPingDelay, token); if (!_isRefreshing.ContainsKey(nodeHash)) { return; diff --git a/src/Nethermind/Nethermind.Kademlia/RoutingTableBucket.cs b/src/Nethermind/Nethermind.Kademlia/RoutingTableBucket.cs new file mode 100644 index 000000000000..7bda89453d20 --- /dev/null +++ b/src/Nethermind/Nethermind.Kademlia/RoutingTableBucket.cs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Kademlia; + +/// +/// Snapshot of one routing-table bucket. +/// +/// Bucket prefix used by the routing table. +/// Bucket depth, expressed as the distance index used by the routing table traversal. +/// Snapshot of nodes in this bucket. Mutating the collection does not mutate the routing table. +public readonly record struct RoutingTableBucket(TKadKey Prefix, int Distance, IReadOnlyList Nodes) + where TNode : notnull + where TKadKey : notnull +{ + /// + /// Number of nodes captured in the bucket snapshot. + /// + public int Count => Nodes.Count; +} diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs index 3c9236e46a10..1fd8a046b0db 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/DiscoveryV5AppTests.cs @@ -49,16 +49,18 @@ private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp, Action(); builder.RegisterInstance(networkConfig).As(); + builder.RegisterInstance(enode).As(); builder.RegisterInstance(ipResolver).As(); builder.RegisterInstance(nodeKey).Keyed(IProtectedPrivateKey.NodeKey); builder.RegisterInstance(ecdsa).As().As(); builder.RegisterInstance(new CryptoRandom()).As(); - builder.RegisterInstance(new NetworkStorage(_discoveryDb, LimboLogs.Instance)).Keyed(DbNames.DiscoveryNodes); + builder.RegisterInstance(new NetworkStorage(_discoveryDb, LimboLogs.Instance)).Keyed(DbNames.DiscoveryV5Nodes); builder.RegisterInstance(Substitute.For()).As(); builder.RegisterType().As().WithAttributeFiltering().SingleInstance(); IContainer container = builder.Build(); @@ -67,6 +69,7 @@ private DiscoveryV5App CreateDiscoveryV5App(IPAddress externalIp, Action .Do(ci => { PingMsg sent = (PingMsg)ci[0]!; - IByteBuffer buffer = _receiverSerializationManager.ZeroSerialize(sent); + using DisposableByteBuffer buffer = _receiverSerializationManager.ZeroSerialize(sent).AsDisposable(); PingMsg msg = _receiverSerializationManager.Deserialize(buffer); PongMsg pong = new( msg.FarPublicKey!, @@ -151,7 +151,7 @@ public async Task GetSession_should_return_single_session_for_concurrent_calls() private T AddReceiverFarAddress(T msg) where T : DiscoveryMsg { - IByteBuffer buffer = _receiverSerializationManager.ZeroSerialize(msg); + using DisposableByteBuffer buffer = _receiverSerializationManager.ZeroSerialize(msg).AsDisposable(); IPEndPoint? farAddress = msg.FarAddress; msg = _receiverSerializationManager.Deserialize(buffer); msg.FarAddress = farAddress; @@ -377,8 +377,6 @@ public async Task OnIncomingMsg_ping_should_respond_with_pong(CancellationToken await _adapter.OnIncomingMsg(pingMsg); - await Task.Delay(100); - ValueHash256 expectedPingMdc = pingMsg.Mdc!.Value; await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && @@ -402,8 +400,6 @@ public async Task OnIncomingMsg_find_node_should_respond_with_neighbors(Cancella await _adapter.OnIncomingMsg(findNodeMsg); - await Task.Delay(100); - _kademliaMessageReceiver.GetKNeighbour( Arg.Is(pk => pk.Bytes!.SequenceEqual(_testPublicKey.Bytes!)), Arg.Is(n => n.Id == _receiver.Id)); @@ -460,8 +456,6 @@ public async Task OnIncomingMsg_enr_request_should_respond_with_enr_response(Can await _adapter.OnIncomingMsg(enrRequestMsg); - Task.Delay(100).Wait(); - await _msgSender.Received(1).SendMsg(Arg.Is(m => m.FarAddress!.Equals(_receiver.Address) && m.RequestKeccak.Equals(expectedRequestHash) && diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs index 27271dec09fa..703420c2df4c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/Kademlia/NodeSourceTests.cs @@ -86,7 +86,7 @@ public async Task DiscoverNodes_should_use_kademlia_discovery_to_find_nodes(Canc _discv4Adapter.Ping(node2, Arg.Any()) .Returns(true); - IAsyncEnumerator enumerator = _nodeSource.DiscoverNodes(token).GetAsyncEnumerator(token); + await using IAsyncEnumerator enumerator = _nodeSource.DiscoverNodes(token).GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); Assert.That(enumerator.Current, Is.EqualTo(node1)); await enumerator.MoveNextAsync(); @@ -106,7 +106,7 @@ public async Task DiscoverNodes_should_ping_nodes_that_have_not_received_pong(Ca _kademliaDiscovery.DiscoverNodesHandler = (_, _, _) => CreateAsyncEnumerable(node); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); - IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await using IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); // Assert - Verify that ping was called @@ -137,7 +137,7 @@ public async Task DiscoverNodes_should_skip_nodes_that_have_tried_ping_recently_ IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); - IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await using IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); Assert.That(enumerator.Current, Is.EqualTo(node2)); @@ -163,7 +163,7 @@ public async Task DiscoverNodes_should_handle_ping_timeout(CancellationToken tok IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); - IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await using IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); Assert.That(enumerator.Current, Is.EqualTo(node2)); @@ -188,7 +188,7 @@ public async Task DiscoverNodes_should_emit_nodes_from_kademlia_events(Cancellat IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); - IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await using IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); _kademlia.RaiseNodeAdded(node2); @@ -212,7 +212,7 @@ public async Task DiscoverNodes_should_not_emit_duplicate_nodes(CancellationToke using AutoCancelTokenSource shortTimeout = token.CreateChildTokenSource(TimeSpan.FromMilliseconds(100)); IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(shortTimeout.Token); - IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); + await using IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync().AsTask()); @@ -231,7 +231,7 @@ public async Task DiscoverNodes_should_pass_concurrent_discovery_jobs_to_kademli IAsyncEnumerable discoveryEnumerable = _nodeSource.DiscoverNodes(token); - IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(); + await using IAsyncEnumerator enumerator = discoveryEnumerable.GetAsyncEnumerator(token); await enumerator.MoveNextAsync(); await enumerator.MoveNextAsync(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs index 58143c28c3a8..113ab4e52817 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NettyDiscoveryHandlerTests.cs @@ -167,11 +167,12 @@ public async Task NeighborsSentReceivedTest() NodeFilter? nodeFilter = null, int? globalInboundMessageBurst = null, int? inboundMessageQueueCapacity = null, - int? inboundMessageWorkerCount = null) + int? inboundMessageWorkerCount = null, + IMessageSerializationService? messageSerializationService = null) { IKademliaAdapter adapter = Substitute.For(); adapter.OnIncomingMsg(Arg.Any()).Returns(Task.CompletedTask); - IMessageSerializationService service = Build.A.SerializationService().WithDiscovery(_privateKey2).TestObject; + IMessageSerializationService service = messageSerializationService ?? Build.A.SerializationService().WithDiscovery(_privateKey2).TestObject; IChannel channel = Substitute.For(); NettyDiscoveryHandler handler = new( adapter, @@ -332,32 +333,36 @@ public async Task GlobalInboundRateLimiter_Drops_Messages_AboveBurstLimit() [Test] public async Task InboundDispatchQueue_Drops_Messages_WhenFull() { - TaskCompletionSource unblockHandler = new(TaskCreationOptions.RunContinuationsAsynchronously); + IMessageSerializationService innerService = Build.A.SerializationService().WithDiscovery(_privateKey2).TestObject; + using ManualResetEventSlim deserializeEntered = new(); + using ManualResetEventSlim unblockDeserialize = new(); + BlockingSerializationService blockingService = new(innerService, deserializeEntered, unblockDeserialize); (IKademliaAdapter adapter, NettyDiscoveryHandler handler, IChannelHandlerContext ctx, IMessageSerializationService service) = CreateHandler( globalInboundMessageBurst: 64, inboundMessageQueueCapacity: 1, - inboundMessageWorkerCount: 1); + inboundMessageWorkerCount: 1, + messageSerializationService: blockingService); int received = 0; adapter.OnIncomingMsg(Arg.Any()).Returns(_ => { Interlocked.Increment(ref received); - return unblockHandler.Task; + return Task.CompletedTask; }); byte[] data = SerializePing(service); - for (int i = 0; i < 16; i++) + handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer((byte[])data.Clone()), new IPEndPoint(IPAddress.Parse("127.0.2.1"), _address2.Port), _address)); + Assert.That(deserializeEntered.Wait(TimeSpan.FromSeconds(5)), Is.True); + + for (int i = 1; i < 16; i++) { IPEndPoint sender = new(IPAddress.Parse($"127.0.2.{i + 1}"), _address2.Port); handler.ChannelRead(ctx, new DatagramPacket(Unpooled.WrappedBuffer((byte[])data.Clone()), sender, _address)); } - Assert.That(() => Interlocked.CompareExchange(ref received, 0, 0), Is.GreaterThanOrEqualTo(1).After(5000, 10)); - await Task.Delay(100); - unblockHandler.SetResult(); - await Task.Delay(100); + unblockDeserialize.Set(); - Assert.That(Interlocked.CompareExchange(ref received, 0, 0), Is.LessThan(16)); + Assert.That(() => Interlocked.CompareExchange(ref received, 0, 0), Is.EqualTo(2).After(5000, 10)); } private byte[] SerializePing(IMessageSerializationService service) @@ -412,5 +417,30 @@ private void InitializeChannel(IDatagramChannel channel, IKademliaAdapter kademl private static async Task SleepWhileWaiting() => await Task.Delay((TestContext.CurrentContext.CurrentRepeatCount + 1) * 300); + + private sealed class BlockingSerializationService( + IMessageSerializationService innerService, + ManualResetEventSlim deserializeEntered, + ManualResetEventSlim unblockDeserialize) : IMessageSerializationService + { + private int _deserializeCalls; + + public IByteBuffer ZeroSerialize(T message, IByteBufferAllocator? allocator = null) where T : MessageBase + => innerService.ZeroSerialize(message, allocator); + + public T Deserialize(ArraySegment bytes) where T : MessageBase + { + if (typeof(T) == typeof(PingMsg) && Interlocked.Increment(ref _deserializeCalls) == 1) + { + deserializeEntered.Set(); + unblockDeserialize.Wait(TimeSpan.FromSeconds(10)); + } + + return innerService.Deserialize(bytes); + } + + public T Deserialize(IByteBuffer buffer) where T : MessageBase + => innerService.Deserialize(buffer); + } } } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs index 74caf75896c5..6d6db6b3ab70 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv4/NodeSourceToDiscV4FeederTests.cs @@ -24,10 +24,12 @@ public async Task Test_ShouldAddNodeToDiscover(CancellationToken token) IProcessExitSource processExitSource = Substitute.For(); processExitSource.Token.Returns(token); NodeSourceToDiscV4Feeder feeder = new(source, discoveryApp, processExitSource, 10); + TaskCompletionSource nodeAdded = new(TaskCreationOptions.RunContinuationsAsynchronously); + discoveryApp.When(x => x.AddNodeToDiscovery(Arg.Any())).Do(_ => nodeAdded.TrySetResult()); _ = feeder.Run(); source.AddNode(new Node(TestItem.PublicKeyA, TestItem.IPEndPointA)); - await Task.Delay(100); + await nodeAdded.Task.WaitAsync(token); discoveryApp.Received().AddNodeToDiscovery(Arg.Any()); } @@ -41,13 +43,22 @@ public async Task Test_ShouldLimitAddedNode(CancellationToken token) IProcessExitSource processExitSource = Substitute.For(); processExitSource.Token.Returns(token); NodeSourceToDiscV4Feeder feeder = new(source, discoveryApp, processExitSource, 10); + TaskCompletionSource expectedNodesAdded = new(TaskCreationOptions.RunContinuationsAsynchronously); + int addedNodes = 0; + discoveryApp.When(x => x.AddNodeToDiscovery(Arg.Any())).Do(_ => + { + if (Interlocked.Increment(ref addedNodes) == 10) + { + expectedNodesAdded.TrySetResult(); + } + }); _ = feeder.Run(); for (int i = 0; i < 20; i++) { source.AddNode(new Node(TestItem.PublicKeyA, TestItem.IPEndPointA)); } - await Task.Delay(100); + await expectedNodesAdded.Task.WaitAsync(token); discoveryApp.Received(10).AddNodeToDiscovery(Arg.Any()); } diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs index e95c4dc0163f..bd6f680a51ce 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Discv5/WireTests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using DotNetty.Buffers; +using DotNetty.Common.Utilities; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Embedded; using DotNetty.Transport.Channels.Sockets; @@ -36,8 +37,8 @@ public async Task Ping_Completes_After_WhoAreYou_Handshake() { IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); - TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); - TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + await using TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + await using TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) { Enr = peerB.NodeRecordProvider.Current.EnrString @@ -63,8 +64,8 @@ public async Task Ping_Rehandshakes_After_RemoteSessionLost() { IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); - TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); - TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + await using TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + await using TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) { Enr = peerB.NodeRecordProvider.Current.EnrString @@ -82,7 +83,7 @@ public async Task Ping_Rehandshakes_After_RemoteSessionLost() await cancellationSourceB.CancelAsync(); await runB; - TestPeer restartedPeerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + await using TestPeer restartedPeerB = CreatePeer(TestItem.PrivateKeyB, endpointB); using CancellationTokenSource cancellationSourceRestartedB = new(10_000); Task runRestartedB = restartedPeerB.Adapter.RunAsync(cancellationSourceRestartedB.Token); @@ -100,8 +101,8 @@ public async Task Ping_Completes_With_HandshakeRecord_WithoutEndpoint() { IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); - TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA, includeEndpointInRecord: false); - TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + await using TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA, includeEndpointInRecord: false); + await using TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) { Enr = peerB.NodeRecordProvider.Current.EnrString @@ -126,8 +127,8 @@ public async Task InboundPing_Starts_EndpointCheck_PingBack() { IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); - TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); - TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + await using TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + await using TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) { Enr = peerB.NodeRecordProvider.Current.EnrString @@ -160,9 +161,9 @@ public async Task FindNeighbours_Returns_Records_At_Requested_Distance() IPEndPoint endpointA = IPEndPoint.Parse("127.0.0.1:10000"); IPEndPoint endpointB = IPEndPoint.Parse("127.0.0.1:10001"); IPEndPoint endpointC = IPEndPoint.Parse("127.0.0.1:10002"); - TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); - TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); - TestPeer peerC = CreatePeer(TestItem.PrivateKeyC, endpointC); + await using TestPeer peerA = CreatePeer(TestItem.PrivateKeyA, endpointA); + await using TestPeer peerB = CreatePeer(TestItem.PrivateKeyB, endpointB); + await using TestPeer peerC = CreatePeer(TestItem.PrivateKeyC, endpointC); Node nodeB = new(TestItem.PrivateKeyB.PublicKey, endpointB) { Enr = peerB.NodeRecordProvider.Current.EnrString @@ -204,13 +205,14 @@ private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, b handler.InitializeChannel(channel); TestNodeRecordProvider nodeRecordProvider = new(privateKey, endpoint, includeEndpointInRecord); + PacketCodec packetCodec = new( + new InsecureProtectedPrivateKey(privateKey), + new CryptoRandom(), + new EthereumEcdsa(0)); KademliaAdapter adapter = new( new Lazy>(kademlia), handler, - new PacketCodec( - new InsecureProtectedPrivateKey(privateKey), - new CryptoRandom(), - new EthereumEcdsa(0)), + packetCodec, nodeRecordProvider, new DiscoveryConfig(), new CryptoRandom(), @@ -218,7 +220,7 @@ private static TestPeer CreatePeer(PrivateKey privateKey, IPEndPoint endpoint, b ExecutionLayerDiscv5RecordFilter.Instance, LimboLogs.Instance); - return new TestPeer(adapter, handler, channel, kademlia, nodeRecordProvider, endpoint); + return new TestPeer(adapter, handler, channel, packetCodec, kademlia, nodeRecordProvider, endpoint); } private static async Task PumpUntilComplete(Task task, TestPeer peerA, TestPeer peerB, CancellationToken token) @@ -251,9 +253,16 @@ private static void Pump(TestPeer from, TestPeer to) { while (from.Channel.ReadOutbound() is { } packet) { - byte[] data = packet.Content.ReadAllBytesAsArray(); - IChannelHandlerContext context = Substitute.For(); - to.Handler.ChannelRead(context, new DatagramPacket(Unpooled.WrappedBuffer(data), from.Endpoint, to.Endpoint)); + try + { + byte[] data = packet.Content.ReadAllBytesAsArray(); + IChannelHandlerContext context = Substitute.For(); + to.Handler.ChannelRead(context, new DatagramPacket(Unpooled.WrappedBuffer(data), from.Endpoint, to.Endpoint)); + } + finally + { + ReferenceCountUtil.Release(packet); + } } } @@ -261,9 +270,19 @@ private sealed record TestPeer( KademliaAdapter Adapter, NettyDiscoveryV5Handler Handler, EmbeddedChannel Channel, + PacketCodec PacketCodec, IKademlia Kademlia, TestNodeRecordProvider NodeRecordProvider, - IPEndPoint Endpoint); + IPEndPoint Endpoint) : IAsyncDisposable + { + public async ValueTask DisposeAsync() + { + await Adapter.DisposeAsync(); + await Channel.CloseAsync(); + Channel.FinishAndReleaseAll(); + PacketCodec.Dispose(); + } + } private sealed class TestNodeRecordProvider(PrivateKey privateKey, IPEndPoint endpoint, bool includeEndpoint) : INodeRecordProvider { diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs index ab7ab1d28bd7..382717ecd044 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KBucketTests.cs @@ -28,7 +28,7 @@ public void TryAddOrRefresh_ShouldLimitToK() } [Test] - public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() + public void GetAll_should_return_snapshot_when_adding_same_node() { (KBucket bucket, int[] toAdd) = BuildFullBucket(); @@ -36,7 +36,9 @@ public void TryAddOrRefresh_ShouldKeepSameCachedArray_WhenAddingSameNode() AddNodes(bucket, toAdd); - Assert.That(bucket.GetAll(), Is.SameAs(nodes)); + int[] refreshedNodes = bucket.GetAll(); + Assert.That(refreshedNodes, Is.Not.SameAs(nodes)); + Assert.That(refreshedNodes.ToHashSet(), Is.EquivalentTo(nodes.ToHashSet())); } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs index 629498e9a562..f0f108b43540 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/KademliaTests.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -21,7 +22,7 @@ public class KademliaTests { private readonly IKademliaMessageSender _kademliaMessageSender = Substitute.For>(); - private Nethermind.Kademlia.Kademlia CreateKad(KademliaConfig config) => + private IContainer CreateKadContainer(KademliaConfig config) => new ContainerBuilder() .AddModule(new KademliaModule()) .AddSingleton(new TestLogManager(LogLevel.Trace)) @@ -31,17 +32,17 @@ private Nethermind.Kademlia.Kademlia Create .AddSingleton(config) .AddSingleton(_kademliaMessageSender) .AddSingleton>() - .Build() - .Resolve>(); + .Build(); [Test] public void TestNewNodeAdded() { - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { KSize = 5, Beta = 0, }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); int nodeAddedTriggered = 0; kad.OnNodeAdded += (sender, hash256) => nodeAddedTriggered++; @@ -57,11 +58,12 @@ public void TestNewNodeAdded() [Test] public void TestNodeRemoved() { - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { KSize = 5, Beta = 0, }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); int nodeRemovedTriggered = 0; ValueHash256 testHash = new("0x1111111111111111111111111111111111111111111111111111111111111111"); @@ -81,31 +83,49 @@ public void TestNodeRemoved() public void ShouldSeedBootnodes() { ValueHash256 bootNode = ValueKeccak.Compute("bootnode"); - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { KSize = 5, Beta = 0, BootNodes = [bootNode], }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); Assert.That(kad.IterateNodes(), Does.Contain(bootNode)); } [Test] - public async Task TestTooManyNode() + [CancelAfter(10000)] + public async Task TestTooManyNode(CancellationToken token) { TaskCompletionSource pingSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource pingStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource nodeRemoved = new(TaskCreationOptions.RunContinuationsAsynchronously); _kademliaMessageSender .Ping(Arg.Any(), Arg.Any()) - .Returns(pingSource.Task); + .Returns(async call => + { + pingStarted.SetResult(); + return await pingSource.Task.WaitAsync(call.Arg()); + }); - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { KSize = 5, Beta = 0, + RefreshPingDelay = TimeSpan.Zero, }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); ValueHash256[] testHashes = Enumerable.Range(0, 10).Select((k) => RandomValueHashAtDistance(ValueKeccak.Zero, 250)).ToArray(); + kad.OnNodeRemoved += (_, node) => + { + if (node.Equals(testHashes[0])) + { + nodeRemoved.TrySetResult(); + } + }; + foreach (ValueHash256 valueHash256 in testHashes[..10]) { kad.AddOrRefresh(valueHash256); @@ -113,12 +133,12 @@ public async Task TestTooManyNode() Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(testHashes[..5].ToHashSet())); - pingSource.SetCanceled(); - - await Task.Delay(100); + await pingStarted.Task.WaitAsync(token); + pingSource.SetResult(false); + await nodeRemoved.Task.WaitAsync(token); HashSet afterCancelled = (testHashes[1..5].Concat([testHashes[9]])).ToHashSet(); - Assert.That(() => kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(afterCancelled).After(100)); + Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(afterCancelled)); } [Test] @@ -129,47 +149,58 @@ public void TestGetKNeighbours() .Ping(Arg.Any(), Arg.Any()) .Returns(pingSource.Task); - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { CurrentNodeId = ValueKeccak.Compute("something"), KSize = 5, Beta = 0, }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); - ValueHash256[] testHashes = Enumerable.Range(0, 7).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); - foreach (ValueHash256 valueHash256 in testHashes) + try { - kad.AddOrRefresh(valueHash256); + ValueHash256[] testHashes = Enumerable.Range(0, 7).Select((k) => ValueKeccak.Compute(k.ToString())).ToArray(); + foreach (ValueHash256 valueHash256 in testHashes) + { + kad.AddOrRefresh(valueHash256); + } + + Assert.That(kad.GetKNeighbour(ValueKeccak.Zero), Has.Length.EqualTo(5)); + Assert.That(kad.GetKNeighbour(kad.CurrentNode), Does.Contain(kad.CurrentNode)); + foreach (ValueHash256 testHash in testHashes) + { + // It must return K items exactly, taking from other bucket if necessary. + Assert.That(kad.GetKNeighbour(testHash), Has.Length.EqualTo(5)); + + // It must find the closest one at least. + Assert.That(kad.GetKNeighbour(testHash), Does.Contain(testHash)); + + // It must exclude a node when hash is specified + Assert.That(kad.GetKNeighbour(testHash, testHash), Has.Length.EqualTo(5)); + Assert.That(kad.GetKNeighbour(testHash, excludeSelf: true), Does.Not.Contain(kad.CurrentNode)); + } } - - Assert.That(kad.GetKNeighbour(ValueKeccak.Zero), Has.Length.EqualTo(5)); - Assert.That(kad.GetKNeighbour(kad.CurrentNode), Does.Contain(kad.CurrentNode)); - foreach (ValueHash256 testHash in testHashes) + finally { - // It must return K items exactly, taking from other bucket if necessary. - Assert.That(kad.GetKNeighbour(testHash), Has.Length.EqualTo(5)); - - // It must find the closest one at least. - Assert.That(kad.GetKNeighbour(testHash), Does.Contain(testHash)); - - // It must exclude a node when hash is specified - Assert.That(kad.GetKNeighbour(testHash, testHash), Has.Length.EqualTo(5)); - Assert.That(kad.GetKNeighbour(testHash, excludeSelf: true), Does.Not.Contain(kad.CurrentNode)); + pingSource.TrySetCanceled(); } } [Test] - public async Task TestTooManyNodeWithAcceleratedLookup() + [CancelAfter(10000)] + public void TestTooManyNodeWithAcceleratedLookup() { _kademliaMessageSender .Ping(Arg.Any(), Arg.Any()) .Returns(true); - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { KSize = 5, Beta = 1, + RefreshPingDelay = TimeSpan.Zero, }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); ValueHash256[] testHashes = new IEnumerable[] { @@ -192,20 +223,23 @@ public async Task TestTooManyNodeWithAcceleratedLookup() kad.AddOrRefresh(valueHash256); } - await Task.Delay(100); - Assert.That(kad.GetAllAtDistance(248).ToHashSet(), Is.EquivalentTo(testHashes[..5].ToHashSet())); - Assert.That(kad.GetAllAtDistance(249).ToHashSet(), Is.EquivalentTo(testHashes[5..10].ToHashSet())); - Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(testHashes[10..].ToHashSet())); + HashSet expected248 = testHashes[..5].ToHashSet(); + HashSet expected249 = testHashes[5..10].ToHashSet(); + HashSet expected250 = testHashes[10..].ToHashSet(); + Assert.That(kad.GetAllAtDistance(248).ToHashSet(), Is.EquivalentTo(expected248)); + Assert.That(kad.GetAllAtDistance(249).ToHashSet(), Is.EquivalentTo(expected249)); + Assert.That(kad.GetAllAtDistance(250).ToHashSet(), Is.EquivalentTo(expected250)); } [Test] public void PruneLastBucketRefreshTicks_removes_stale_prefixes_even_when_counts_match() { - Nethermind.Kademlia.Kademlia kad = CreateKad(new KademliaConfig + using IContainer container = CreateKadContainer(new KademliaConfig { KSize = 5, Beta = 0, }); + Nethermind.Kademlia.Kademlia kad = container.Resolve>(); Hash256 activePrefix = new("0x1111111111111111111111111111111111111111111111111111111111111111"); Hash256 stalePrefix = new("0x2222222222222222222222222222222222222222222222222222222222222222"); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs index a88846649a7a..1474706a38b4 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/Kademlia/NodeHealthTrackerTests.cs @@ -207,7 +207,7 @@ public int[] GetKNearestNeighbourExcluding(int hash, int exclude, bool excludeSe public int[] GetAllAtDistance(int i) => throw new NotSupportedException(); - public IEnumerable<(int Prefix, int Distance, KBucket Bucket)> IterateBuckets() => + public IEnumerable> IterateBuckets() => throw new NotSupportedException(); public int GetByHash(int nodeId) => throw new NotSupportedException(); diff --git a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs index c4ad99b21bbd..020eef9b710b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Discovery.Test/NettyDiscoveryV5HandlerTests.cs @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using DotNetty.Buffers; +using DotNetty.Common.Utilities; using DotNetty.Transport.Channels; using DotNetty.Transport.Channels.Embedded; using DotNetty.Transport.Channels.Sockets; @@ -42,12 +44,41 @@ public async Task ForwardsSentMessageToChannel() byte[] data = [1, 2, 3]; IPEndPoint to = IPEndPoint.Parse("127.0.0.1:10001"); - await _handler.SendAsync(data, to); + await _handler.SendAsync(data, to, CancellationToken.None); DatagramPacket packet = _channel.ReadOutbound(); - Assert.That(packet, Is.Not.Null); - Assert.That(packet.Content.ReadAllBytesAsArray(), Is.EqualTo(data)); - Assert.That(packet.Recipient, Is.EqualTo(to)); + try + { + Assert.That(packet, Is.Not.Null); + Assert.That(packet.Content.ReadAllBytesAsArray(), Is.EqualTo(data)); + Assert.That(packet.Recipient, Is.EqualTo(to)); + } + finally + { + ReferenceCountUtil.Release(packet); + } + } + + [Test] + public void DoesNotSendWhenTokenIsAlreadyCanceled() + { + byte[] data = [1, 2, 3]; + IPEndPoint to = IPEndPoint.Parse("127.0.0.1:10001"); + using CancellationTokenSource cancellationSource = new(); + cancellationSource.Cancel(); + + Assert.ThrowsAsync( + async () => await _handler.SendAsync(data, to, cancellationSource.Token)); + + DatagramPacket? packet = _channel.ReadOutbound(); + try + { + Assert.That(packet, Is.Null); + } + finally + { + ReferenceCountUtil.Release(packet); + } } [Test] diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs index 6c28a1dc6a37..2d25f6c231d0 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaModule.cs @@ -4,6 +4,7 @@ using Autofac; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Kademlia; using Nethermind.Stats.Model; @@ -17,7 +18,7 @@ namespace Nethermind.Network.Discovery.Discv4.Kademlia; /// /// /// -public sealed class KademliaModule(Node currentNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(currentNode, bootNodes) +public sealed class KademliaModule(Node currentNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(currentNode, bootNodes, DbNames.DiscoveryNodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder // This two class contains the actual `INodeSource` logic. As in finding nodes within the network. diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index 3e314bddc1dc..43c4591a0fb7 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -39,6 +39,7 @@ public sealed class DiscoveryV5App : KademliaDiscoveryApp public DiscoveryV5App( ILifetimeScope rootScope, [KeyFilter(IProtectedPrivateKey.NodeKey)] IProtectedPrivateKey nodeKey, + IEnode enode, IIPResolver ipResolver, INetworkConfig networkConfig, IDiscoveryConfig discoveryConfig, @@ -47,9 +48,7 @@ public DiscoveryV5App( Action? configureDiscv5Services = null) : base("discv5", networkConfig, ipResolver, processExitSource, logManager.GetClassLogger()) { - // DiscoveryV5App is resolved during network startup, after SetupKeyStore (a declared dependency of - // InitializeNetwork) has already awaited Resolve() and warmed the cache, so this does not block. - IPAddress externalIp = ipResolver.Resolve().GetAwaiter().GetResult().ExternalIp; + IPAddress externalIp = enode.HostIp; _allowNonRoutableEnrs = ShouldAcceptNonRoutableEnrs(externalIp); List bootNodes = CreateBootNodes(networkConfig, discoveryConfig); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs index 4bf4870f50b5..fd966efdd21f 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaAdapter.cs @@ -190,7 +190,11 @@ private async Task RunPacketWorkerAsync(CancellationToken token) } /// - public ValueTask DisposeAsync() => ValueTask.CompletedTask; + public ValueTask DisposeAsync() + { + _sessions.Clear(); + return ValueTask.CompletedTask; + } private async Task SendRequest( Node receiver, @@ -209,7 +213,7 @@ private async Task SendRequest( PendingNonceKey? pendingNonceKey = null; try { - pendingNonceKey = await SendMessage(receiver, request); + pendingNonceKey = await SendMessage(receiver, request, timeoutCts.Token); await responseHandler.Task.WaitAsync(timeoutCts.Token); return true; } @@ -228,15 +232,15 @@ private async Task SendRequest( } } - private async Task SendMessage(Node receiver, Discv5Message message) + private async Task SendMessage(Node receiver, Discv5Message message, CancellationToken token) { if (TryEncodeWithExistingSession(receiver, message, out PendingNonceKey pendingNonceKey, out byte[]? packet)) { - return await SendPendingPacket(receiver, message, pendingNonceKey, packet, hasSession: true); + return await SendPendingPacket(receiver, message, pendingNonceKey, packet, hasSession: true, token); } pendingNonceKey = EncodeMessageWithoutSession(receiver, message, out byte[] initialPacket); - return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false); + return await SendPendingPacket(receiver, message, pendingNonceKey, initialPacket, hasSession: false, token); } [SkipLocalsInit] @@ -282,13 +286,14 @@ private async Task SendPendingPacket( Discv5Message message, PendingNonceKey pendingNonceKey, byte[] packet, - bool hasSession) + bool hasSession, + CancellationToken token) { _pendingByNonce.Set(pendingNonceKey, new PendingRequest(receiver, message)); try { if (_logger.IsTrace) _logger.Trace($"Sending discv5 ordinary {message.MessageType} {message.RequestId} to {receiver:s} {(hasSession ? "with existing session" : "without session")}, bytes: {packet.Length}."); - await discoveryHandler.SendAsync(packet, receiver.Address); + await discoveryHandler.SendAsync(packet, receiver.Address, token); return pendingNonceKey; } catch @@ -306,7 +311,7 @@ private async Task SendResponse(Node receiver, Discv5Message message, Cancellati } if (_logger.IsTrace) _logger.Trace($"Sending discv5 response {message.MessageType} {message.RequestId} to {receiver:s}, bytes: {packet.Length}."); - await discoveryHandler.SendAsync(packet, receiver.Address); + await discoveryHandler.SendAsync(packet, receiver.Address, token); } [SkipLocalsInit] @@ -389,7 +394,7 @@ private async Task HandleWhoAreYou(IPEndPoint endpoint, Packet packet, Cancellat SetSession(new SessionKey(pendingRequest.Receiver.Id.Hash.ValueHash256, endpoint), session); if (_logger.IsTrace) _logger.Trace($"Sending discv5 HANDSHAKE for {pendingRequest.Message.MessageType} {pendingRequest.Message.RequestId} to {endpoint}, bytes: {handshakePacket.Length}, requested ENR seq: {requestedEnrSequence}."); - await discoveryHandler.SendAsync(handshakePacket, endpoint); + await discoveryHandler.SendAsync(handshakePacket, endpoint, token); } private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, CancellationToken token) @@ -404,7 +409,7 @@ private async Task HandleOrdinary(IPEndPoint endpoint, Packet packet, Cancellati if (!TryDecryptOrdinaryMessage(in packet, sessionKey, out Session? session, out Discv5Message? message)) { if (_logger.IsTrace) _logger.Trace($"Discv5 ordinary packet from {endpoint} could not be decrypted with an existing session; sending WHOAREYOU."); - await SendWhoAreYou(endpoint, packet, nodeId); + await SendWhoAreYou(endpoint, packet, nodeId, token); return; } @@ -479,14 +484,14 @@ private async Task HandleHandshake(IPEndPoint endpoint, Packet packet, Cancellat await HandleHandshakeMessage(endpoint, nodeId, session, message, nodeRecord, knownRecord, token); } - private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, ValueHash256 nodeId) + private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, ValueHash256 nodeId, CancellationToken token) { ChallengeKey challengeKey = new(nodeId, endpoint); long now = Environment.TickCount64; if (_sentChallenges.TryGet(challengeKey, out SentChallenge existingChallenge) && !IsExpired(existingChallenge, now)) { if (_logger.IsTrace) _logger.Trace($"Resending discv5 WHOAREYOU challenge to {endpoint}."); - await discoveryHandler.SendAsync(existingChallenge.Packet, endpoint); + await discoveryHandler.SendAsync(existingChallenge.Packet, endpoint, token); return; } @@ -500,7 +505,7 @@ private async Task SendWhoAreYou(IPEndPoint endpoint, Packet requestPacket, Valu byte[] packet = packetCodec.EncodeWhoAreYou(nodeId.Bytes, requestPacket.Nonce.Span, enrSequence); SetSentChallenge(challengeKey, packet); if (_logger.IsTrace) _logger.Trace($"Sending discv5 WHOAREYOU challenge to {endpoint}, known ENR seq: {enrSequence}, bytes: {packet.Length}."); - await discoveryHandler.SendAsync(packet, endpoint); + await discoveryHandler.SendAsync(packet, endpoint, token); } private async Task HandleHandshakeMessage( diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs index e89fd16de896..1ae3bbd83f7a 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/Kademlia/KademliaModule.cs @@ -4,6 +4,7 @@ using Autofac; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Network.Discovery.Discv5.Packets; using Nethermind.Network.Discovery.Kademlia; @@ -14,7 +15,7 @@ namespace Nethermind.Network.Discovery.Discv5.Kademlia; /// /// Specifies the protocol-specific Kademlia services used by discv5. /// -public sealed class KademliaModule(Node currentNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(currentNode, bootNodes) +public sealed class KademliaModule(Node currentNode, IReadOnlyList bootNodes) : DiscoveryKademliaModuleBase(currentNode, bootNodes, DbNames.DiscoveryV5Nodes) { protected override void RegisterProtocolServices(ContainerBuilder builder) => builder .AddSingleton(ExecutionLayerDiscv5RecordFilter.Instance) diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs index 117c5bb65907..c4658829761b 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/NettyDiscoveryV5Handler.cs @@ -48,14 +48,16 @@ protected override void ChannelRead0(IChannelHandlerContext ctx, DatagramPacket } } - public async Task SendAsync(byte[] data, IPEndPoint destination) + public async Task SendAsync(byte[] data, IPEndPoint destination, CancellationToken token) { + token.ThrowIfCancellationRequested(); + DatagramPacket packet = new(Unpooled.WrappedBuffer(data), destination); try { if (_logger.IsTrace) _logger.Trace($"Sending discv5 UDP packet to {destination}, bytes: {data.Length}."); - await Channel.WriteAndFlushAsync(packet); + await Channel.WriteAndFlushAsync(packet).WaitAsync(token); } catch (SocketException exception) { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs index 5137b5ead19e..a76e69d06205 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryKademliaModuleBase.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LGPL-3.0-only using Autofac; +using Autofac.Core; +using Autofac.Features.AttributeFilters; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Kademlia; @@ -9,7 +11,7 @@ namespace Nethermind.Network.Discovery.Kademlia; -public abstract class DiscoveryKademliaModuleBase(Node currentNode, IReadOnlyList bootNodes) : Module +public abstract class DiscoveryKademliaModuleBase(Node currentNode, IReadOnlyList bootNodes, string discoveryStorageKey) : Module { protected override void Load(ContainerBuilder builder) { @@ -19,8 +21,13 @@ protected override void Load(ContainerBuilder builder) .AddModule(new KademliaModule()) .AddSingleton>(Hash256KademliaDistance.Instance) .AddSingleton, PublicKeyKeyOperator>() - .AddSingleton() .AddSingleton, IDiscoveryConfig>((discoveryConfig) => DiscoveryKademliaConfigFactory.Create(currentNode, bootNodes, discoveryConfig)); + + builder.RegisterType() + .AsSelf() + .WithAttributeFiltering() + .WithParameter(ResolvedParameter.ForKeyed(discoveryStorageKey)) + .SingleInstance(); } protected abstract void RegisterProtocolServices(ContainerBuilder builder); diff --git a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs index 4c45f55cd50d..e51aa4d2ab72 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Kademlia/DiscoveryPersistenceManager.cs @@ -1,10 +1,8 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Autofac.Features.AttributeFilters; using Nethermind.Config; using Nethermind.Core.Crypto; -using Nethermind.Db; using Nethermind.Kademlia; using Nethermind.Logging; using Nethermind.Stats; @@ -27,7 +25,7 @@ namespace Nethermind.Network.Discovery.Kademlia; /// Log manager for logging events. /// Thrown if any required parameter is null. public sealed class DiscoveryPersistenceManager( - [KeyFilter(DbNames.DiscoveryNodes)] INetworkStorage discoveryStorage, + INetworkStorage discoveryStorage, INodeStatsManager nodeStatsManager, IKademliaMessageSender messageSender, IKademlia kademlia, diff --git a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs index 65dd6580ba5f..22776a5cc79d 100644 --- a/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs +++ b/src/Nethermind/Nethermind.Network.Enr.Test/NodeRecordSignerTests.cs @@ -118,6 +118,32 @@ public void Can_deserialize_and_verify_real_world_cases(string testCase) Assert.That(nodeRecord.ToRlpBytes(), Is.EqualTo(Bytes.FromHexString(testCase))); } + [Test] + public void Can_serialize_eth_entry_as_nested_fork_id_list() + { + byte[] forkHash = [1, 2, 3, 4]; + const long nextBlock = 0x0506; + byte[] expectedEntryBytes = Bytes.FromHexString("83657468c9c88401020304820506"); + + Ecdsa ecdsa = new(); + PrivateKey privateKey = new(TestPrivateKey); + NodeRecordSigner signer = new(ecdsa, privateKey); + NodeRecord nodeRecord = new(); + nodeRecord.SetEntry(new EthEntry(forkHash, nextBlock)); + nodeRecord.SetEntry(new SecP256k1Entry(privateKey.CompressedPublicKey)); + signer.Sign(nodeRecord); + + byte[] recordBytes = nodeRecord.ToRlpBytes(); + Assert.That(recordBytes.AsSpan().IndexOf(expectedEntryBytes), Is.GreaterThanOrEqualTo(0)); + + NodeRecord decoded = NodeRecord.FromBytes(recordBytes, ecdsa); + ForkId? forkId = decoded.GetValue(EnrContentKey.Eth); + + Assert.That(forkId, Is.Not.Null); + Assert.That(forkId.Value.ForkHash, Is.EqualTo(forkHash)); + Assert.That(forkId.Value.NextBlock, Is.EqualTo(nextBlock)); + } + [TestCaseSource(nameof(InvalidRecordByteCases))] public void FromBytes_throws_when_record_bytes_are_invalid(Func createRecordBytes) => Assert.That(() => NodeRecord.FromBytes(createRecordBytes()), Throws.TypeOf()); diff --git a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs index 058a328df499..0101f3694074 100644 --- a/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs +++ b/src/Nethermind/Nethermind.Network.Enr/EthEntry.cs @@ -12,17 +12,20 @@ public class EthEntry(byte[] forkHash, long nextBlock) : EnrContentEntry { public override string Key => EnrContentKey.Eth; - protected override int GetRlpLengthOfValue() => Rlp.LengthOfSequence( - Rlp.LengthOfSequence( - 5 + Rlp.LengthOf(Value.NextBlock))); + protected override int GetRlpLengthOfValue() + { + int forkIdContentLength = GetForkIdContentLength(); + return Rlp.LengthOfSequence(Rlp.LengthOfSequence(forkIdContentLength)); + } protected override void EncodeValue(RlpStream rlpStream) { - // I am just guessing this one - int contentLength = 5 + Rlp.LengthOf(Value.NextBlock); - rlpStream.StartSequence(contentLength + 1); + int contentLength = GetForkIdContentLength(); + rlpStream.StartSequence(Rlp.LengthOfSequence(contentLength)); rlpStream.StartSequence(contentLength); rlpStream.Encode(Value.ForkHash); rlpStream.Encode(Value.NextBlock); } + + private int GetForkIdContentLength() => Rlp.LengthOf(Value.ForkHash) + Rlp.LengthOf(Value.NextBlock); } diff --git a/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs b/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs index a3fc5e791478..ad7bb2cc6a54 100644 --- a/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs +++ b/src/Nethermind/Nethermind.Network/IP/WebIPSource.cs @@ -14,22 +14,22 @@ class WebIPSource(string url, ILogManager logManager) : IIPSource private readonly string _url = url; private readonly ILogger _logger = logManager.GetClassLogger(); - public Task<(bool, IPAddress)> TryGetIP() + public async Task<(bool, IPAddress)> TryGetIP() { try { using HttpClient httpClient = new() { Timeout = TimeSpan.FromSeconds(3) }; if (_logger.IsInfo) _logger.Info($"Using {_url} to get external ip"); - string ip = httpClient.GetStringAsync(_url).Result.Trim(); + string ip = (await httpClient.GetStringAsync(_url)).Trim(); if (_logger.IsDebug) _logger.Debug($"External ip: {ip}"); bool result = IPAddress.TryParse(ip, out IPAddress ipAddress); bool isExternal = result && !ipAddress.IsLoopbackOrPrivateOrLinkLocal; - return Task.FromResult(isExternal ? (true, ipAddress) : (false, (IPAddress)null)); + return isExternal ? (true, ipAddress) : (false, (IPAddress)null); } catch (Exception e) { _logger.DebugError($"Error while getting external ip from {_url}", e); - return Task.FromResult((false, (IPAddress)null)); + return (false, (IPAddress)null); } } } diff --git a/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs b/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs index 6bd6f598d06f..20667b65ceff 100644 --- a/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/DatabasePurgerTests.cs @@ -36,7 +36,7 @@ public void ForceResync_preserves_peer_and_discovery_directories() { Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.PeersDb)), Is.True, "peers should be preserved"); Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryNodes)), Is.True, "discoveryNodes should be preserved"); - Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryV5Nodes)), Is.False, "legacy discoveryV5Nodes should be deleted"); + Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryV5Nodes)), Is.True, "discoveryV5Nodes should be preserved"); } } @@ -90,6 +90,7 @@ public void PurgeDb_deletes_network_directories() { Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.PeersDb)), Is.False); Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryNodes)), Is.False); + Assert.That(Directory.Exists(Path.Combine(_tempDir, DbNames.DiscoveryV5Nodes)), Is.False); } } diff --git a/src/Nethermind/Nethermind.Runner/DatabasePurger.cs b/src/Nethermind/Nethermind.Runner/DatabasePurger.cs index bcfc2edef6d0..e00979e5c27f 100644 --- a/src/Nethermind/Nethermind.Runner/DatabasePurger.cs +++ b/src/Nethermind/Nethermind.Runner/DatabasePurger.cs @@ -14,7 +14,8 @@ internal static class DatabasePurger private static readonly HashSet NetworkDbNames = new(StringComparer.OrdinalIgnoreCase) { DbNames.PeersDb, - DbNames.DiscoveryNodes + DbNames.DiscoveryNodes, + DbNames.DiscoveryV5Nodes }; ///