-
Notifications
You must be signed in to change notification settings - Fork 1k
Add RoutingTable for DHT #4422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add RoutingTable for DHT #4422
Changes from 2 commits
309a679
6ee3c92
f143c30
3ef1b40
b1d175c
1c960fb
4f2f9f2
aad52c7
f68d5fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
| } |
| 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")})"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -137,6 +137,16 @@ protected override void OnAck() | |||
| CheckMessageQueue(); | ||||
| } | ||||
|
|
||||
| protected override void OnDisconnect(bool abort) | ||||
| { | ||||
| if (abort) | ||||
|
||||
| Disconnect(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AllowNewConnection() disconnects when there is a network mismatch or duplicate node, which can basically be considered an "error".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But error because local unknown limits, and this will clean th dht, that's good for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
Uh oh!
There was an error while loading. Please reload this page.