Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Neo/Network/P2P/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public void Disconnect(bool abort = false)
{
disconnected = true;
tcp?.Tell(abort ? Tcp.Abort.Instance : Tcp.Close.Instance);
OnDisconnect(abort);
Context.Stop(Self);
}

Expand All @@ -85,6 +86,16 @@ protected virtual void OnAck()
{
}

/// <summary>
/// Invoked when a disconnect operation occurs, allowing derived classes to handle cleanup or custom logic.
/// </summary>
/// <remarks>Override this method in a derived class to implement custom behavior when a disconnect
/// occurs. This method is called regardless of whether the disconnect is graceful or due to an abort.</remarks>
/// <param name="abort">true to indicate the disconnect is due to an abort operation; otherwise, false.</param>
protected virtual void OnDisconnect(bool abort)
{
}

/// <summary>
/// Called when data is received.
/// </summary>
Expand Down
195 changes: 195 additions & 0 deletions src/Neo/Network/P2P/KBucket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright (C) 2015-2026 The Neo Project.
//
// KBucket.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using System.Diagnostics.CodeAnalysis;

namespace Neo.Network.P2P;

/// <summary>
/// A Kademlia-style k-bucket: stores up to <see cref="Capacity"/> contacts in LRU order.
/// </summary>
sealed class KBucket
{
private readonly LinkedList<NodeContact> _lru = new();
private readonly Dictionary<UInt256, LinkedListNode<NodeContact>> _index = new();

// Replacement cache: best-effort candidates when the bucket is full.
private readonly LinkedList<NodeContact> _replacements = new();
private readonly Dictionary<UInt256, LinkedListNode<NodeContact>> _repIndex = new();

public int Capacity { get; }
public int ReplacementCapacity { get; }
public int BadThreshold { get; }
public int Count => _lru.Count;
public IReadOnlyCollection<NodeContact> Contacts => _lru;

public KBucket(int capacity, int replacementCapacity, int badThreshold)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
ArgumentOutOfRangeException.ThrowIfNegative(replacementCapacity);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(badThreshold);
Capacity = capacity;
ReplacementCapacity = replacementCapacity;
BadThreshold = badThreshold;
}

public bool TryGet(UInt256 nodeId, [NotNullWhen(true)] out NodeContact? contact)
{
if (_index.TryGetValue(nodeId, out var node))
{
contact = node.Value;
return true;
}
contact = null;
return false;
}

/// <summary>
/// Updates LRU position and contact metadata. If bucket is full and the node is new,
/// the node is placed into replacement cache.
/// </summary>
/// <returns>
/// True if the contact ended up in the main bucket; false if it was cached as a replacement.
/// </returns>
public bool Update(NodeContact incoming)
{
if (_index.TryGetValue(incoming.NodeId, out var existingNode))
{
Merge(existingNode.Value, incoming);
Touch(existingNode);
return true;
}

if (_lru.Count < Capacity)
{
var node = _lru.AddLast(incoming);
_index[incoming.NodeId] = node;
return true;
}

// Bucket full: keep as replacement candidate.
AddOrUpdateReplacement(incoming);
return false;
}

public void MarkSuccess(UInt256 nodeId)
{
if (_index.TryGetValue(nodeId, out var node))
{
node.Value.FailCount = 0;
node.Value.LastSeen = TimeProvider.Current.UtcNow;
Touch(node);
return;
}

// If it was only a replacement, promote its freshness.
if (_repIndex.TryGetValue(nodeId, out var repNode))
{
repNode.Value.FailCount = 0;
repNode.Value.LastSeen = TimeProvider.Current.UtcNow;
Touch(repNode);
}
}

public void MarkFailure(UInt256 nodeId)
{
if (_index.TryGetValue(nodeId, out var node))
{
node.Value.FailCount++;
if (node.Value.FailCount < BadThreshold) return;

// Evict bad node and promote best replacement (if any).
RemoveFrom(node, _index);
PromoteReplacementIfAny();
}
else if (_repIndex.TryGetValue(nodeId, out var repNode))
{
// If it is a replacement, decay it and possibly drop.
repNode.Value.FailCount++;
if (repNode.Value.FailCount >= BadThreshold)
RemoveFrom(repNode, _repIndex);
}
}

public void Remove(UInt256 nodeId)
{
if (_index.TryGetValue(nodeId, out var node))
{
RemoveFrom(node, _index);
PromoteReplacementIfAny();
}
else if (_repIndex.TryGetValue(nodeId, out var repNode))
{
RemoveFrom(repNode, _repIndex);
}
}

void AddOrUpdateReplacement(NodeContact incoming)
{
if (_repIndex.TryGetValue(incoming.NodeId, out var existing))
{
Merge(existing.Value, incoming);
Touch(existing);
return;
}

if (ReplacementCapacity == 0) return;

var node = _replacements.AddLast(incoming);
_repIndex[incoming.NodeId] = node;

if (_replacements.Count > ReplacementCapacity)
{
// Drop oldest replacement.
var first = _replacements.First;
if (first is not null)
RemoveFrom(first, _repIndex);
}
}

void PromoteReplacementIfAny()
{
if (_lru.Count >= Capacity) return;
if (_replacements.Last is null) return;

// Promote the most recently seen replacement.
var rep = _replacements.Last;
RemoveFrom(rep, _repIndex);
var main = _lru.AddLast(rep.Value);
_index[main.Value.NodeId] = main;
}

static void Merge(NodeContact dst, NodeContact src)
{
// Merge endpoints (promote the first src endpoint if present).
if (src.Endpoints.Count > 0)
dst.AddOrPromoteEndpoint(src.Endpoints[0]);
for (int i = 1; i < src.Endpoints.Count; i++)
dst.AddOrPromoteEndpoint(src.Endpoints[i]);

// Prefer latest seen & features.
if (src.LastSeen > dst.LastSeen) dst.LastSeen = src.LastSeen;
dst.Features |= src.Features;
}

static void Touch(LinkedListNode<NodeContact> node)
{
var list = node.List!;
list.Remove(node);
list.AddLast(node);
}

static void RemoveFrom(LinkedListNode<NodeContact> node, Dictionary<UInt256, LinkedListNode<NodeContact>> index)
{
index.Remove(node.Value.NodeId);
node.List!.Remove(node);
}
}
6 changes: 6 additions & 0 deletions src/Neo/Network/P2P/LocalNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public record GetInstance;
/// </summary>
public UInt256 NodeId { get; }

/// <summary>
/// Routing table used by the DHT overlay network.
/// </summary>
public RoutingTable RoutingTable { get; }

/// <summary>
/// The identifier of the client software of the local node.
/// </summary>
Expand All @@ -95,6 +100,7 @@ public LocalNode(NeoSystem system, KeyPair nodeKey)
this.system = system;
NodeKey = nodeKey;
NodeId = nodeKey.PublicKey.GetNodeId(system.Settings);
RoutingTable = new RoutingTable(NodeId);
SeedList = new IPEndPoint[system.Settings.SeedList.Length];

// Start dns resolution in parallel
Expand Down
77 changes: 77 additions & 0 deletions src/Neo/Network/P2P/NodeContact.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (C) 2015-2026 The Neo Project.
//
// NodeContact.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using System.Net;

namespace Neo.Network.P2P;

/// <summary>
/// Represents a reachability hint for a DHT node (NOT a live connection).
/// </summary>
public sealed class NodeContact
{
/// <summary>
/// The verified DHT node identifier.
/// </summary>
public UInt256 NodeId { get; }

/// <summary>
/// Known endpoints for contacting the node. The first item is the preferred endpoint.
/// </summary>
public List<IPEndPoint> Endpoints { get; } = new();

/// <summary>
/// Last time we successfully communicated with this node (handshake or DHT message).
/// </summary>
public DateTime LastSeen { get; internal set; }

/// <summary>
/// Consecutive failures when trying to contact this node.
/// </summary>
public int FailCount { get; internal set; }

/// <summary>
/// Optional capability flags (reserved).
/// </summary>
public ulong Features { get; internal set; }

/// <summary>
/// Initializes a new instance of the NodeContact class with the specified node identifier, optional endpoints, and
/// feature flags.
/// </summary>
/// <param name="nodeId">The unique identifier for the node. This value is used to distinguish the node within the network.</param>
/// <param name="endpoints">A collection of network endpoints associated with the node. If not specified, the contact will have no initial
/// endpoints.</param>
/// <param name="features">A bit field representing the features supported by the node. The default is 0, indicating no features.</param>
public NodeContact(UInt256 nodeId, IEnumerable<IPEndPoint>? endpoints = null, ulong features = 0)
{
NodeId = nodeId;
if (endpoints is not null)
foreach (var ep in endpoints)
AddOrPromoteEndpoint(ep);
LastSeen = TimeProvider.Current.UtcNow;
Features = features;
}

internal void AddOrPromoteEndpoint(IPEndPoint endpoint)
{
// Keep unique endpoints; promote to the front when we learn it's good.
int index = Endpoints.IndexOf(endpoint);
if (index == 0) return;
if (index > 0) Endpoints.RemoveAt(index);
Endpoints.Insert(0, endpoint);
}

public override string ToString()
{
return $"{NodeId} ({(Endpoints.Count > 0 ? Endpoints[0].ToString() : "no-endpoint")})";
}
}
9 changes: 9 additions & 0 deletions src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ private void OnVerackMessageReceived()
{
_verack = true;
_system.TaskManager.Tell(new TaskManager.Register(Version!));

// DHT: a verack means the handshake is complete and the remote identity (NodeId) has been verified.
// Feed the remote contact into the local RoutingTable.
var nodeId = Version!.NodeId;
// Prefer the advertised TCP server endpoint (Listener) when available, otherwise fall back to the connected Remote endpoint.
var ep = ListenerTcpPort > 0 ? Listener : Remote;
_localNode.RoutingTable.Update(nodeId, ep);
_localNode.RoutingTable.MarkSuccess(nodeId);

CheckMessageQueue();
}

Expand Down
10 changes: 10 additions & 0 deletions src/Neo/Network/P2P/RemoteNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ protected override void OnAck()
CheckMessageQueue();
}

protected override void OnDisconnect(bool abort)
{
if (abort)
{
// DHT: connection dropped. Penalize the contact (do not immediately delete; allow churn).
if (Version != null)
_localNode.RoutingTable.MarkFailure(Version.NodeId);
}
}

protected override void OnData(ByteString data)
{
_messageBuffer = _messageBuffer.Concat(data);
Expand Down
Loading