Skip to content

Commit 309b74f

Browse files
ak88claude
andauthored
fix(xdc): disable automatic ENR refresh over discv4 (#12556)
* fix(xdc): stop sending ENR requests on the XDC network XDC's discv4 fork repurposed the standard ENR request/response type bytes (5/6) for its own ping, so a real EnrRequest sent to an XDC peer gets misread as a ping and never gets an EnrResponse back. This left Nethermind repeatedly requesting and timing out ENR refreshes whenever a peer advertised a newer ENR sequence. Disable the automatic ENR-refresh flow entirely for XDC via a small XdcKademliaAdapter override, instead of racing peers with requests they can never answer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * remove using --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent aac9c9c commit 309b74f

4 files changed

Lines changed: 174 additions & 6 deletions

File tree

src/Nethermind/Nethermind.Network.Discovery/Discv4/Kademlia/KademliaAdapter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
namespace Nethermind.Network.Discovery.Discv4.Kademlia;
2222

23-
public sealed class KademliaAdapter(
23+
public class KademliaAdapter(
2424
Lazy<IKademlia<PublicKey, Node>> kademlia, // Cyclic dependency
2525
Lazy<INodeHealthTracker<Node>> nodeHealthTracker,
2626
IDiscoveryConfig discoveryConfig,
@@ -261,7 +261,7 @@ public async Task<bool> Ping(Node receiver, CancellationToken token)
261261
return response.HasResponse ? response.Value : null;
262262
}
263263

264-
private Task RefreshRemoteRecordIfNewer(Node node, ulong? advertisedSequence, CancellationToken token)
264+
protected virtual Task RefreshRemoteRecordIfNewer(Node node, ulong? advertisedSequence, CancellationToken token)
265265
=> advertisedSequence is { } sequence
266266
? base.RefreshRemoteRecordIfNewer(node, sequence, token)
267267
: Task.CompletedTask;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Nethermind.Config;
8+
using Nethermind.Core;
9+
using Nethermind.Core.Crypto;
10+
using Nethermind.Core.Test.Builders;
11+
using Nethermind.Crypto;
12+
using Nethermind.Kademlia;
13+
using Nethermind.Logging;
14+
using Nethermind.Network;
15+
using Nethermind.Network.Discovery;
16+
using Nethermind.Network.Discovery.Discv4;
17+
using Nethermind.Network.Discovery.Discv4.Messages;
18+
using Nethermind.Network.Enr;
19+
using Nethermind.Network.Test;
20+
using Nethermind.Network.Test.Builders;
21+
using Nethermind.Stats;
22+
using Nethermind.Stats.Model;
23+
using Nethermind.Xdc.Discovery;
24+
using NSubstitute;
25+
using NUnit.Framework;
26+
27+
namespace Nethermind.Xdc.Test.Discovery;
28+
29+
[TestFixture, Parallelizable(ParallelScope.All)]
30+
public class XdcKademliaAdapterTests
31+
{
32+
private const int RequestTimeoutMs = 10_000;
33+
34+
private IKademlia<PublicKey, Node> _kademliaMessageReceiver = null!;
35+
private INodeHealthTracker<Node> _nodeHealthTracker = null!;
36+
private KademliaConfig<Node> _kademliaConfig = null!;
37+
private ITimestamper _timestamper = null!;
38+
private IMsgSender _msgSender = null!;
39+
private INodeStatsManager _nodeStatsManager = null!;
40+
private INodeRecordProvider _nodeRecordProvider = null!;
41+
private IMessageSerializationService _receiverSerializationManager = null!;
42+
private Node _receiver = null!;
43+
private XdcKademliaAdapter _adapter = null!;
44+
45+
[SetUp]
46+
public void Setup()
47+
{
48+
Node currentNode = new(TestItem.PublicKeyA, "192.168.1.1", 30303);
49+
_kademliaMessageReceiver = Substitute.For<IKademlia<PublicKey, Node>>();
50+
_nodeHealthTracker = Substitute.For<INodeHealthTracker<Node>>();
51+
_kademliaConfig = new KademliaConfig<Node> { CurrentNodeId = currentNode };
52+
53+
_timestamper = Substitute.For<ITimestamper>();
54+
DateTime now = new(2021, 5, 3, 0, 0, 0, DateTimeKind.Utc);
55+
_timestamper.UtcNow.Returns(now);
56+
_timestamper.UnixTime.Returns(new UnixTime(now));
57+
58+
_msgSender = Substitute.For<IMsgSender>();
59+
_msgSender.SendMsg(Arg.Any<DiscoveryMsg>()).Returns(Task.CompletedTask);
60+
61+
_receiver = new(TestItem.PublicKeyB, "192.168.1.2", 30303);
62+
SerializationBuilder builder = new();
63+
builder.WithDiscovery(TestItem.PrivateKeyB);
64+
_receiverSerializationManager = builder.TestObject;
65+
66+
_nodeRecordProvider = Substitute.For<INodeRecordProvider>();
67+
_nodeRecordProvider.GetCurrentAsync(Arg.Any<CancellationToken>()).Returns(new ValueTask<NodeRecord>(new NodeRecord()));
68+
_nodeStatsManager = Substitute.For<INodeStatsManager>();
69+
_nodeStatsManager.GetOrAdd(Arg.Any<Node>()).Returns(Substitute.For<INodeStats>());
70+
71+
_adapter = new XdcKademliaAdapter(
72+
new Lazy<IKademlia<PublicKey, Node>>(() => _kademliaMessageReceiver),
73+
new Lazy<INodeHealthTracker<Node>>(() => _nodeHealthTracker),
74+
new DiscoveryConfig
75+
{
76+
EnrTimeout = RequestTimeoutMs,
77+
PingTimeout = RequestTimeoutMs,
78+
SendNodeTimeout = RequestTimeoutMs,
79+
BondWaitTime = 1,
80+
},
81+
_kademliaConfig,
82+
_nodeRecordProvider,
83+
_nodeStatsManager,
84+
_timestamper,
85+
Substitute.For<IProcessExitSource>(),
86+
new Ecdsa(),
87+
LimboLogs.Instance)
88+
{
89+
MsgSender = _msgSender,
90+
};
91+
}
92+
93+
[TearDown]
94+
public async Task TearDown() => await _adapter.DisposeAsync();
95+
96+
private void ConfigureBondCallback(ulong? pongEnrSequence) =>
97+
_msgSender
98+
.SendMsg(Arg.Any<PingMsg>())
99+
.Returns(ci =>
100+
{
101+
PingMsg sent = (PingMsg)ci[0]!;
102+
using DisposableByteBuffer buffer = _receiverSerializationManager.ZeroSerialize(sent).AsDisposable();
103+
PingMsg msg = _receiverSerializationManager.Deserialize<PingMsg>(buffer);
104+
PongMsg pong = new(msg.FarPublicKey!, _timestamper.UnixTime.SecondsLong + 1, sent.Mdc!.Value, pongEnrSequence);
105+
pong.FarAddress = sent.FarAddress;
106+
return _adapter.OnIncomingMsg(pong);
107+
});
108+
109+
[Test]
110+
[CancelAfter(10000)]
111+
public async Task Ping_should_bond_but_never_send_enr_request_even_when_pong_advertises_newer_sequence(CancellationToken token)
112+
{
113+
ConfigureBondCallback(pongEnrSequence: 42);
114+
115+
bool result = await _adapter.Ping(_receiver, token);
116+
117+
Assert.That(result, Is.True);
118+
await _msgSender.DidNotReceive().SendMsg(Arg.Any<EnrRequestMsg>());
119+
_kademliaMessageReceiver.DidNotReceive().AddOrRefresh(Arg.Is<Node>(n => n.Enr != null));
120+
}
121+
}

src/Nethermind/Nethermind.Xdc/Discovery/XdcDiscoveryApp.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
using Autofac;
55
using Autofac.Features.AttributeFilters;
66
using Nethermind.Config;
7+
using Nethermind.Core;
78
using Nethermind.Logging;
89
using Nethermind.Network;
910
using Nethermind.Network.Config;
1011
using Nethermind.Network.Discovery;
1112
using Nethermind.Network.Discovery.Discv4;
13+
using Nethermind.Network.Discovery.Discv4.Kademlia;
1214

1315
namespace Nethermind.Xdc.Discovery;
1416

@@ -28,9 +30,14 @@ public class XdcDiscoveryApp(
2830
ipResolver,
2931
processExitSource,
3032
logManager,
31-
static builder => builder
32-
.RegisterType<XdcNettyDiscoveryHandler>()
33-
.As<NettyDiscoveryHandler>()
34-
.WithAttributeFiltering())
33+
static builder =>
34+
{
35+
builder.RegisterType<XdcNettyDiscoveryHandler>()
36+
.As<NettyDiscoveryHandler>()
37+
.WithAttributeFiltering();
38+
39+
// XDC does not implement the ENR request/response messages, so remote ENR refresh is disabled.
40+
builder.AddSingleton<IKademliaAdapter, XdcKademliaAdapter>();
41+
})
3542
{
3643
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Nethermind.Config;
8+
using Nethermind.Core;
9+
using Nethermind.Core.Crypto;
10+
using Nethermind.Crypto;
11+
using Nethermind.Kademlia;
12+
using Nethermind.Logging;
13+
using Nethermind.Network.Discovery;
14+
using Nethermind.Network.Discovery.Discv4.Kademlia;
15+
using Nethermind.Stats;
16+
using Nethermind.Stats.Model;
17+
18+
namespace Nethermind.Xdc.Discovery;
19+
20+
/// <summary>
21+
/// XDC's discv4 fork repurposed the standard ENR request/response type bytes for its own ping,
22+
/// so there is no wire-compatible way to fetch a remote node's ENR. Remote ENR refresh is
23+
/// disabled entirely instead of racing XDC nodes with requests they will never answer.
24+
/// </summary>
25+
public sealed class XdcKademliaAdapter(
26+
Lazy<IKademlia<PublicKey, Node>> kademlia,
27+
Lazy<INodeHealthTracker<Node>> nodeHealthTracker,
28+
IDiscoveryConfig discoveryConfig,
29+
KademliaConfig<Node> kademliaConfig,
30+
INodeRecordProvider nodeRecordProvider,
31+
INodeStatsManager nodeStatsManager,
32+
ITimestamper timestamper,
33+
IProcessExitSource processExitSource,
34+
IEcdsa ecdsa,
35+
ILogManager logManager)
36+
: KademliaAdapter(kademlia, nodeHealthTracker, discoveryConfig, kademliaConfig, nodeRecordProvider, nodeStatsManager, timestamper, processExitSource, ecdsa, logManager)
37+
{
38+
protected override Task RefreshRemoteRecordIfNewer(Node node, ulong? advertisedSequence, CancellationToken token)
39+
=> Task.CompletedTask;
40+
}

0 commit comments

Comments
 (0)