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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/Neo/Network/P2P/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Neo.Network.P2P;
/// </summary>
public abstract class Connection : UntypedActor
{
internal class Close { public bool Abort; }
internal class Close { public DisconnectReason Reason; }
internal class Ack : Tcp.Event { public static Ack Instance = new(); }

/// <summary>
Expand Down Expand Up @@ -58,7 +58,7 @@ protected Connection(object connection, IPEndPoint remote, IPEndPoint local)
{
Remote = remote;
Local = local;
timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromSeconds(connectionTimeoutLimitStart), Self, new Close { Abort = true }, ActorRefs.NoSender);
timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromSeconds(connectionTimeoutLimitStart), Self, new Close { Reason = DisconnectReason.Timeout }, ActorRefs.NoSender);
switch (connection)
{
case IActorRef tcp:
Expand All @@ -70,11 +70,12 @@ protected Connection(object connection, IPEndPoint remote, IPEndPoint local)
/// <summary>
/// Disconnect from the remote node.
/// </summary>
/// <param name="abort">Indicates whether the TCP ABORT command should be sent.</param>
public void Disconnect(bool abort = false)
/// <param name="reason">The reason for the disconnection.</param>
public void Disconnect(DisconnectReason reason = DisconnectReason.Close)
{
disconnected = true;
tcp?.Tell(abort ? Tcp.Abort.Instance : Tcp.Close.Instance);
tcp?.Tell(reason == DisconnectReason.Close ? Tcp.Close.Instance : Tcp.Abort.Instance);
OnDisconnect(reason);
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="reason">The reason for the disconnection.</param>
protected virtual void OnDisconnect(DisconnectReason reason)
{
}

/// <summary>
/// Called when data is received.
/// </summary>
Expand All @@ -96,7 +107,7 @@ protected override void OnReceive(object message)
switch (message)
{
case Close close:
Disconnect(close.Abort);
Disconnect(close.Reason);
break;
case Ack _:
OnAck();
Expand All @@ -113,8 +124,8 @@ protected override void OnReceive(object message)
private void OnReceived(ByteString data)
{
timer.CancelIfNotNull();
timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromSeconds(connectionTimeoutLimit), Self, new Close { Abort = true }, ActorRefs.NoSender);
data.TryCatch<ByteString, Exception>(OnData, (_, _) => Disconnect(true));
timer = Context.System.Scheduler.ScheduleTellOnceCancelable(TimeSpan.FromSeconds(connectionTimeoutLimit), Self, new Close { Reason = DisconnectReason.Timeout }, ActorRefs.NoSender);
data.TryCatch<ByteString, Exception>(OnData, (_, _) => Disconnect(DisconnectReason.ProtocolViolation));
}

protected override void PostStop()
Expand Down
39 changes: 39 additions & 0 deletions src/Neo/Network/P2P/DisconnectReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (C) 2015-2026 The Neo Project.
//
// DisconnectReason.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.

namespace Neo.Network.P2P;

/// <summary>
/// Specifies the reason for a disconnection event in a network or communication context.
/// </summary>
/// <remarks>Use this enumeration to determine why a connection was terminated.</remarks>
public enum DisconnectReason
{
/// <summary>
/// No specific reason for disconnection.
/// </summary>
None,

/// <summary>
/// The connection was closed normally.
/// </summary>
Close,

/// <summary>
/// The connection was closed due to a timeout.
/// </summary>
Timeout,

/// <summary>
/// The connection was closed due to a protocol violation.
/// </summary>
ProtocolViolation
}
56 changes: 56 additions & 0 deletions src/Neo/Network/P2P/EndpointKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (C) 2015-2026 The Neo Project.
//
// EndpointKind.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.

namespace Neo.Network.P2P;

/// <summary>
/// Describes how an overlay endpoint was learned and how it should be used.
/// </summary>
[Flags]
public enum EndpointKind : byte
{
/// <summary>
/// The endpoint was observed as the remote endpoint of an incoming or
/// outgoing connection.
/// This usually represents a NAT-mapped or ephemeral public endpoint and
/// should NOT be treated as a reliable dial target.
/// </summary>
Observed = 1,

/// <summary>
/// The endpoint was explicitly advertised by the peer itself, typically
/// via protocol handshake metadata (e.g., Version/Listener port).
///
/// Advertised endpoints indicate that the peer claims to be listening
/// for incoming connections on this address and port.
///
/// Actual reachability is still validated through success/failure tracking.
/// </summary>
Advertised = 2,

/// <summary>
/// The endpoint was derived indirectly rather than directly observed or
/// self-advertised.
///
/// Derived endpoints usually require validation before being trusted for
/// active communication.
/// </summary>
Derived = 4,

/// <summary>
/// The endpoint represents a relay or intermediary rather than a direct
/// network address of the target node.
///
/// Relay endpoints are never used for direct dialing and require
/// protocol-specific relay support.
/// </summary>
Relay = 8
}
193 changes: 193 additions & 0 deletions src/Neo/Network/P2P/KBucket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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 overlay endpoints (preserve transport; merge endpoint kinds).
for (int i = 0; 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);
}
}
Loading