/// <summary> /// Checks whether the bucket contains given <paramref name="peer"/>. /// </summary> /// <param name="peer">The <see cref="BoundPeer"/> to check.</param> /// <returns><c>true</c> if the bucket contains <paramref name="peer"/>, /// <c>false</c> otherwise.</returns> public bool Contains(BoundPeer peer) { return(_peerStates.Contains(peer.Address)); }
public PingTimeoutException(BoundPeer target, string message) : base(message) { Target = target; }
internal KBucket BucketOf(BoundPeer peer) { int index = GetBucketIndexOf(peer.Address); return(_buckets[index]); }
public KBucket BucketOf(BoundPeer peer) { int index = GetBucketIndexOf(peer); return(_buckets[index]); }
/// <summary> /// Determines whether the <see cref="RoutingTable"/> contains the specified key. /// </summary> /// <param name="peer">Key to locate in the <see cref="RoutingTable"/>.</param> /// <returns><see langword="true"/> if the <see cref="RoutingTable" /> contains /// an element with the specified key; otherwise, <see langword="false"/>.</returns> public bool Contains(BoundPeer peer) { return(BucketOf(peer).Contains(peer)); }
private void RemovePeer(BoundPeer peer) { _logger.Debug("Removing peer {Peer} from table.", peer); _routing.RemovePeer(peer); }
private async Task RemovePeerAsync(BoundPeer peer, CancellationToken cancellationToken) { _logger.Debug("Removing peer [{peer}] from table.", peer); await _routing.RemovePeerAsync(peer); }
public bool Contains(BoundPeer peer) { int index = GetBucketIndexOf(peer); return(_buckets[index].Contains(peer)); }
private void RemovePeer(BoundPeer peer) { _table.RemovePeer(peer); }
/// <inheritdoc/> public Message Decode( NetMQMessage encoded, bool reply) { if (encoded.FrameCount == 0) { throw new ArgumentException("Can't parse empty NetMQMessage."); } // (reply == true) [version, type, peer, timestamp, sign, frames...] // (reply == false) [identity, version, type, peer, timestamp, sign, frames...] NetMQFrame[] remains = reply ? encoded.ToArray() : encoded.Skip(1).ToArray(); var versionToken = remains[(int)Message.MessageFrame.Version].ConvertToString(); AppProtocolVersion remoteVersion = AppProtocolVersion.FromToken(versionToken); Peer remotePeer; var dictionary = (Bencodex.Types.Dictionary)_codec.Decode( remains[(int)Message.MessageFrame.Peer].ToByteArray()); try { remotePeer = new BoundPeer(dictionary); } catch (KeyNotFoundException) { remotePeer = new Peer(dictionary); } _messageValidator.ValidateAppProtocolVersion( remotePeer, reply ? new byte[] { } : encoded[0].ToByteArray(), remoteVersion); var type = (Message.MessageType)remains[(int)Message.MessageFrame.Type].ConvertToInt32(); var ticks = remains[(int)Message.MessageFrame.Timestamp].ConvertToInt64(); var timestamp = new DateTimeOffset(ticks, TimeSpan.Zero); var currentTime = DateTimeOffset.UtcNow; _messageValidator.ValidateTimestamp(remotePeer, currentTime, timestamp); byte[] signature = remains[(int)Message.MessageFrame.Sign].ToByteArray(); NetMQFrame[] body = remains.Skip(Message.CommonFrames) .ToArray(); Message message = CreateMessage( type, body.Select(frame => frame.ToByteArray()).ToArray()); message.Version = remoteVersion; message.Remote = remotePeer; message.Timestamp = timestamp; var headerWithoutSign = new[] { remains[(int)Message.MessageFrame.Version], remains[(int)Message.MessageFrame.Type], remains[(int)Message.MessageFrame.Peer], remains[(int)Message.MessageFrame.Timestamp], }; var messageToVerify = headerWithoutSign.Concat(body).ToByteArray(); if (!remotePeer.PublicKey.Verify(messageToVerify, signature)) { throw new InvalidMessageSignatureException( "The signature of an encoded message is invalid.", remotePeer, remotePeer.PublicKey, messageToVerify, signature); } if (!reply) { message.Identity = encoded[0].Buffer.ToArray(); } return(message); }
public KBucket BucketOf(BoundPeer peer) { int index = GetBucketIndexOf(peer); return(BucketOf(index)); }
internal KBucket BucketOf(BoundPeer peer) { int index = GetBucketIndexOf(peer.Address); return(BucketOf(index)); }
public PingTimeoutException(BoundPeer target) : base() { Target = target; }
public PingTimeoutException(BoundPeer target, string message, Exception innerException) : base(message, innerException) { Target = target; }
private Message CreateMessage(Message.MessageType type) { var privateKey = new PrivateKey(); var boundPeer = new BoundPeer(privateKey.PublicKey, new DnsEndPoint("localhost", 1000)); IBlockPolicy <DumbAction> policy = new BlockPolicy <DumbAction>(); BlockChain <DumbAction> chain = MakeBlockChain( policy, new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore()) ); var codec = new Codec(); Block <DumbAction> genesis = chain.Genesis; var transaction = chain.MakeTransaction(privateKey, new DumbAction[] { }); switch (type) { case Message.MessageType.Ping: return(new Ping()); case Message.MessageType.Pong: return(new Pong()); case Message.MessageType.GetBlockHashes: return(new GetBlockHashes(chain.GetBlockLocator(), genesis.Hash)); case Message.MessageType.TxIds: return(new TxIds(new[] { transaction.Id })); case Message.MessageType.GetBlocks: return(new GetBlocks(new[] { genesis.Hash }, 10)); case Message.MessageType.GetTxs: return(new GetTxs(new[] { transaction.Id })); case Message.MessageType.Blocks: return(new Libplanet.Net.Messages.Blocks( new[] { codec.Encode(genesis.MarshalBlock()) })); case Message.MessageType.Tx: return(new Libplanet.Net.Messages.Tx(transaction.Serialize(true))); case Message.MessageType.FindNeighbors: return(new FindNeighbors(privateKey.ToAddress())); case Message.MessageType.Neighbors: return(new Neighbors(new[] { boundPeer })); case Message.MessageType.BlockHeaderMessage: return(new BlockHeaderMessage(genesis.Hash, genesis.Header)); case Message.MessageType.BlockHashes: return(new BlockHashes(0, new[] { genesis.Hash })); case Message.MessageType.GetChainStatus: return(new GetChainStatus()); case Message.MessageType.ChainStatus: return(new ChainStatus( 0, genesis.Hash, chain.Tip.Index, chain.Tip.Hash, chain.Tip.TotalDifficulty)); case Message.MessageType.DifferentVersion: return(new DifferentVersion()); default: throw new Exception($"Cannot create a message of invalid type {type}"); } }
internal static string ToNetMQAddress(this BoundPeer peer) { return($"tcp://{peer.EndPoint.Host}:{peer.EndPoint.Port}"); }
/// <inheritdoc/> public Message Decode( NetMQMessage encoded, bool reply, Action <byte[], Peer, AppProtocolVersion> appProtocolVersionValidator) { if (encoded.FrameCount == 0) { throw new ArgumentException("Can't parse empty NetMQMessage."); } // (reply == true) [version, type, peer, timestamp, sign, frames...] // (reply == false) [identity, version, type, peer, timestamp, sign, frames...] NetMQFrame[] remains = reply ? encoded.ToArray() : encoded.Skip(1).ToArray(); var versionToken = remains[(int)Message.MessageFrame.Version].ConvertToString(); AppProtocolVersion remoteVersion = AppProtocolVersion.FromToken(versionToken); Peer remotePeer; var dictionary = (Bencodex.Types.Dictionary)_codec.Decode( remains[(int)Message.MessageFrame.Peer].ToByteArray()); try { remotePeer = new BoundPeer(dictionary); } catch (KeyNotFoundException) { remotePeer = new Peer(dictionary); } appProtocolVersionValidator( reply ? new byte[] { } : encoded[0].ToByteArray(), remotePeer, remoteVersion); var type = (Message.MessageType)remains[(int)Message.MessageFrame.Type].ConvertToInt32(); var ticks = remains[(int)Message.MessageFrame.Timestamp].ConvertToInt64(); var timestamp = new DateTimeOffset(ticks, TimeSpan.Zero); var currentTime = DateTimeOffset.UtcNow; if (_messageTimestampBuffer is TimeSpan timestampBuffer && (currentTime - timestamp).Duration() > timestampBuffer) { var msg = $"Received message is invalid, created at " + $"{timestamp.ToString(TimestampFormat, CultureInfo.InvariantCulture)} " + $"but designated lifetime is {timestampBuffer} and " + $"the current datetime offset is " + $"{currentTime.ToString(TimestampFormat, CultureInfo.InvariantCulture)}."; throw new InvalidMessageTimestampException( msg, timestamp, _messageTimestampBuffer, currentTime); } byte[] signature = remains[(int)Message.MessageFrame.Sign].ToByteArray(); NetMQFrame[] body = remains.Skip(Message.CommonFrames) .ToArray(); Message message = CreateMessage( type, body.Select(frame => frame.ToByteArray()).ToArray()); message.Version = remoteVersion; message.Remote = remotePeer; message.Timestamp = timestamp; var headerWithoutSign = new[] { remains[(int)Message.MessageFrame.Version], remains[(int)Message.MessageFrame.Type], remains[(int)Message.MessageFrame.Peer], remains[(int)Message.MessageFrame.Timestamp], }; var messageForVerify = headerWithoutSign.Concat(body).ToArray(); if (!remotePeer.PublicKey.Verify(messageForVerify.ToByteArray(), signature)) { throw new InvalidMessageSignatureException( "The message signature is invalid", message); } if (!reply) { message.Identity = encoded[0].Buffer.ToArray(); } return(message); }
public static async Task <AppProtocolVersion> QueryAppProtocolVersionTcp( this BoundPeer peer, TimeSpan?timeout = null, CancellationToken cancellationToken = default ) { using var client = new TcpClient(); try { await client.ConnectAsync(peer.EndPoint.Host, peer.EndPoint.Port); } catch (SocketException) { throw new TimeoutException("Cannot find peer."); } client.ReceiveTimeout = timeout?.Milliseconds ?? 0; using var stream = client.GetStream(); var key = new PrivateKey(); var ping = new Ping { Identity = Guid.NewGuid().ToByteArray(), }; var messageCodec = new TcpMessageCodec(); byte[] serialized = messageCodec.Encode( ping, key, peer, DateTimeOffset.UtcNow, default); int length = serialized.Length; var buffer = new byte[TcpTransport.MagicCookie.Length + sizeof(int) + length]; TcpTransport.MagicCookie.CopyTo(buffer, 0); BitConverter.GetBytes(length).CopyTo(buffer, TcpTransport.MagicCookie.Length); serialized.CopyTo(buffer, TcpTransport.MagicCookie.Length + sizeof(int)); await stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); buffer = new byte[1000000]; int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); var magicCookieBuffer = new byte[TcpTransport.MagicCookie.Length]; Array.Copy(buffer, 0, magicCookieBuffer, 0, TcpTransport.MagicCookie.Length); var sizeBuffer = new byte[sizeof(int)]; Array.Copy(buffer, TcpTransport.MagicCookie.Length, sizeBuffer, 0, sizeof(int)); length = BitConverter.ToInt32(sizeBuffer, 0); var contentBuffer = new byte[length]; Array.Copy( buffer, TcpTransport.MagicCookie.Length + sizeof(int), contentBuffer, 0, length); // length of identity Array.Copy(contentBuffer, 4, sizeBuffer, 0, 4); length = BitConverter.ToInt32(sizeBuffer, 0); // length of apv token Array.Copy(contentBuffer, 4 + 4 + length, sizeBuffer, 0, 4); int apvLength = BitConverter.ToInt32(sizeBuffer, 0); var apvBytes = new byte[apvLength]; Array.Copy(contentBuffer, 4 + 4 + length + 4, apvBytes, 0, apvLength); var frame = new NetMQFrame(apvBytes); string token = frame.ConvertToString(); AppProtocolVersion version = AppProtocolVersion.FromToken(token); return(version); }
private async Task <BoundPeer> ProcessFoundForSpecificAsync( ConcurrentBag <BoundPeer> history, IEnumerable <BoundPeer> found, Address target, int depth, TimeSpan?timeout, CancellationToken cancellationToken, Address searchAddress) { BoundPeer peerFound = null; List <BoundPeer> peers = found.Where( peer => !peer.Address.Equals(_address)).ToList(); if (peers.Count == 0) { _logger.Debug("No any neighbor received."); return(peerFound); } peers = Kademlia.SortByDistance(peers, target); List <BoundPeer> closestNeighbors = _routing.Neighbors(target, _bucketSize).ToList(); Task[] awaitables = peers.Select(peer => PingAsync(peer, _requestTimeout, cancellationToken) ).ToArray(); try { await Task.WhenAll(awaitables); } catch (AggregateException e) { if (e.InnerExceptions.All(ie => ie is TimeoutException) && e.InnerExceptions.Count == awaitables.Length) { throw new TimeoutException( $"All neighbors found do not respond in {_requestTimeout}." ); } _logger.Error( e, "Some responses from neighbors found unexpectedly terminated: {Exception}", e ); } for (int i = 0; i < closestNeighbors.Count; i++) { if (string.CompareOrdinal( closestNeighbors[i].Address.ToHex(), searchAddress.ToHex() ) == 0 && _routing.Contains(closestNeighbors[i])) { peerFound = closestNeighbors[i]; return(peerFound); } } var findNeighboursTasks = new List <Task <BoundPeer> >(); Peer closestKnown = closestNeighbors.Count == 0 ? null : closestNeighbors[0]; for (int i = 0; i < Kademlia.FindConcurrency && i < peers.Count; i++) { if (peerFound is null || string.CompareOrdinal( Kademlia.CalculateDistance(peers[i].Address, target).ToHex(), Kademlia.CalculateDistance(closestKnown.Address, target).ToHex() ) < 1) { findNeighboursTasks.Add(FindSpecificPeerAsync( history, target, peers[i], (depth == -1) ? depth : depth - 1, searchAddress, timeout, cancellationToken)); } } var foundSpecificPeer = new List <BoundPeer>(); try { foundSpecificPeer.AddRange(await Task.WhenAll(findNeighboursTasks)); } catch (TimeoutException) { if (findNeighboursTasks.All(findPeerTask => findPeerTask.IsFaulted)) { throw new TimeoutException( "Timeout exception occurred during " + $"{nameof(ProcessFoundForSpecificAsync)}."); } } for (int i = 0; i < foundSpecificPeer.Count; i++) { if (string.CompareOrdinal( foundSpecificPeer[i].Address.ToHex(), searchAddress.ToHex() ) == 0 && _routing.Contains(foundSpecificPeer[i])) { peerFound = foundSpecificPeer[i]; return(peerFound); } } return(peerFound); }
public void BucketTest() { var bucket = new KBucket(4, new System.Random(), Logger.None); var peer1 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234)); var peer2 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234)); var peer3 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234)); var peer4 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234)); var peer5 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234)); // Checks for an empty bucket. Assert.True(bucket.IsEmpty); Assert.False(bucket.IsFull); Assert.Empty(bucket.Peers); Assert.Empty(bucket.PeerStates); Assert.Null(bucket.GetRandomPeer()); Assert.Null(bucket.Head); Assert.Null(bucket.Tail); // Checks for a partially filled bucket. bucket.AddPeer(peer1, DateTimeOffset.UtcNow); Assert.False(bucket.IsEmpty); Assert.False(bucket.IsFull); Assert.True(bucket.Contains(peer1)); Assert.False(bucket.Contains(peer2)); Assert.NotNull(bucket.GetRandomPeer()); Assert.Null(bucket.GetRandomPeer(peer1.Address)); Assert.NotNull(bucket.GetRandomPeer(peer2.Address)); Assert.Equal(peer1, bucket.Head?.Peer); Assert.Equal(peer1, bucket.Tail?.Peer); // Sleep statement is used to distinguish updated times. Thread.Sleep(100); bucket.AddPeer(peer2, DateTimeOffset.UtcNow); Assert.Contains( bucket.GetRandomPeer(), new[] { peer1, peer2 } ); Assert.Contains( bucket.GetRandomPeer(peer1.Address), new[] { peer2 } ); // Checks for a full bucket. Thread.Sleep(100); bucket.AddPeer(peer3, DateTimeOffset.UtcNow); Thread.Sleep(100); bucket.AddPeer(peer4, DateTimeOffset.UtcNow); Assert.True(bucket.IsFull); Assert.Equal( bucket.Peers.ToHashSet(), new HashSet <BoundPeer> { peer1, peer2, peer3, peer4 } ); Assert.Contains( bucket.GetRandomPeer(), new[] { peer1, peer2, peer3, peer4 } ); Thread.Sleep(100); bucket.AddPeer(peer5, DateTimeOffset.UtcNow); Assert.Equal( bucket.Peers.ToHashSet(), new HashSet <BoundPeer> { peer1, peer2, peer3, peer4 } ); Assert.False(bucket.Contains(peer5)); Assert.Equal(peer4, bucket.Head?.Peer); Assert.Equal(peer1, bucket.Tail?.Peer); // Check order has changed. Thread.Sleep(100); bucket.AddPeer(peer1, DateTimeOffset.UtcNow); Assert.Equal(peer1, bucket.Head?.Peer); Assert.Equal(peer2, bucket.Tail?.Peer); Assert.False(bucket.RemovePeer(peer5)); Assert.True(bucket.RemovePeer(peer1)); Assert.DoesNotContain(peer1, bucket.Peers); Assert.Equal(3, bucket.Peers.Count()); // Clear the bucket. bucket.Clear(); Assert.True(bucket.IsEmpty); Assert.Empty(bucket.Peers); Assert.Null(bucket.Head); Assert.Null(bucket.Tail); Assert.Null(bucket.GetRandomPeer()); }
public bool RemoveCache(BoundPeer peer) { KBucket bucket = BucketOf(peer); return(bucket.ReplacementCache.TryRemove(peer, out var dateTimeOffset)); }
public void BucketTest() { var bucket = new KBucket(4, new System.Random(), Logger.None); var peer1 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234), 0); var peer2 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234), 0); var peer3 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234), 0); var peer4 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234), 0); var peer5 = new BoundPeer( new PrivateKey().PublicKey, new DnsEndPoint("0.0.0.0", 1234), 0); Assert.Empty(bucket.Peers); Assert.True(bucket.IsEmpty()); bucket.AddPeer(peer1); Assert.True(bucket.Contains(peer1)); Assert.False(bucket.Contains(peer2)); Assert.False(bucket.IsEmpty()); Assert.False(bucket.IsFull()); // This sleep statement is used to distinguish updated time of followings. Thread.Sleep(100); bucket.AddPeer(peer2); Thread.Sleep(100); Assert.Contains( bucket.GetRandomPeer(null), new[] { peer1, peer2 } ); Assert.Contains( bucket.GetRandomPeer(peer1.Address), new[] { peer2 } ); bucket.AddPeer(peer3); Thread.Sleep(100); bucket.AddPeer(peer4); Assert.True(bucket.IsFull()); Assert.Equal( bucket.Peers.ToHashSet(), new HashSet <BoundPeer> { peer1, peer2, peer3, peer4 } ); Assert.Contains( bucket.GetRandomPeer(null), new[] { peer1, peer2, peer3, peer4 } ); Thread.Sleep(100); bucket.AddPeer(peer5); Assert.Equal( bucket.Peers.ToHashSet(), new HashSet <BoundPeer> { peer1, peer2, peer3, peer4 } ); Assert.False(bucket.Contains(peer5)); Assert.Equal(peer4, bucket.Head.Key); Assert.Equal(peer1, bucket.Tail.Key); Thread.Sleep(100); bucket.AddPeer(peer1); Assert.Equal(peer1, bucket.Head.Key); Assert.Equal(peer2, bucket.Tail.Key); Assert.False(bucket.RemovePeer(peer5)); Assert.True(bucket.RemovePeer(peer1)); Assert.DoesNotContain(peer1, bucket.Peers); Assert.Equal(3, bucket.Peers.Count()); bucket.Clear(); Assert.True(bucket.IsEmpty()); }
/// <summary> /// Adds the <paramref name="peer"/> to the table. /// </summary> /// <param name="peer">The <see cref="BoundPeer"/> to add.</param> /// <exception cref="ArgumentException">Thrown when <paramref name="peer"/>'s /// <see cref="Address"/> is equal to the <see cref="Address"/> of self.</exception> public void AddPeer(BoundPeer peer) => AddPeer(peer, DateTimeOffset.UtcNow);
/// <inheritdoc/> public Message Decode( byte[] encoded, bool reply) { if (encoded.Length == 0) { throw new ArgumentException("Can't parse empty byte array."); } using var stream = new MemoryStream(encoded); var buffer = new byte[sizeof(int)]; stream.Read(buffer, 0, sizeof(int)); int frameCount = BitConverter.ToInt32(buffer, 0); var frames = new List <byte[]>(); for (var i = 0; i < frameCount; i++) { buffer = new byte[sizeof(int)]; stream.Read(buffer, 0, sizeof(int)); int frameSize = BitConverter.ToInt32(buffer, 0); buffer = new byte[frameSize]; stream.Read(buffer, 0, frameSize); frames.Add(buffer); } // (reply == true) [version, type, peer, timestamp, sign, frames...] // (reply == false) [identity, version, type, peer, timestamp, sign, frames...] List <byte[]> remains = reply ? frames : frames.Skip(1).ToList(); var versionToken = Encoding.ASCII.GetString(remains[(int)Message.MessageFrame.Version]); AppProtocolVersion remoteVersion = AppProtocolVersion.FromToken(versionToken); Peer remotePeer; var dictionary = (Bencodex.Types.Dictionary)_codec.Decode(remains[(int)Message.MessageFrame.Peer]); try { remotePeer = new BoundPeer(dictionary); } catch (KeyNotFoundException) { remotePeer = new Peer(dictionary); } _messageValidator.ValidateAppProtocolVersion( remotePeer, reply ? new byte[] { } : frames[0], remoteVersion); var type = (Message.MessageType)BitConverter.ToInt32( remains[(int)Message.MessageFrame.Type], 0); long ticks = BitConverter.ToInt64(remains[(int)Message.MessageFrame.Timestamp], 0); var timestamp = new DateTimeOffset(ticks, TimeSpan.Zero); var currentTime = DateTimeOffset.UtcNow; _messageValidator.ValidateTimestamp(remotePeer, currentTime, timestamp); byte[] signature = remains[(int)Message.MessageFrame.Sign]; byte[][] body = remains.Skip(Message.CommonFrames).ToArray(); Message message = CreateMessage(type, body); message.Version = remoteVersion; message.Remote = remotePeer; message.Timestamp = timestamp; var headerWithoutSign = new[] { remains[(int)Message.MessageFrame.Version], remains[(int)Message.MessageFrame.Type], remains[(int)Message.MessageFrame.Peer], remains[(int)Message.MessageFrame.Timestamp], }; var messageToVerify = headerWithoutSign.Concat(body).Aggregate( new byte[] { }, (arr, bytes) => arr.Concat(bytes).ToArray()); if (!remotePeer.PublicKey.Verify(messageToVerify, signature)) { throw new InvalidMessageSignatureException( "The signature of an encoded message is invalid.", remotePeer, remotePeer.PublicKey, messageToVerify, signature); } if (!reply) { message.Identity = frames[0]; } return(message); }
/// <summary> /// Returns <paramref name="k"/> nearest peers to given parameter peer from routing table. /// Return value is already sorted with respect to target. /// </summary> /// <param name="target"><see cref="BoundPeer"/> to look up.</param> /// <param name="k">Number of peers to return.</param> /// <param name="includeTarget">A boolean value indicates to include a peer with /// <see cref="Address"/> of <paramref name="target"/> in return value or not.</param> /// <returns>An enumerable of <see cref="BoundPeer"/>.</returns> public IReadOnlyList <BoundPeer> Neighbors(BoundPeer target, int k, bool includeTarget) => Neighbors(target.Address, k, includeTarget);
public bool Contains(BoundPeer peer) { return(_peers.Any(kv => kv.Key.PublicKey.Equals(peer.PublicKey))); }