/// <summary> /// Announces blocks on all connected nodes memory pool behaviors every five seconds. /// </summary> public void Start() { this.blockConnectedSubscription = this.signals.Subscribe <BlockConnected>(this.OnBlockConnected); this.asyncLoop = this.asyncProvider.CreateAndRunAsyncLoop("MemoryPool.RelayWorker", async token => { IReadOnlyNetworkPeerCollection peers = this.connection.ConnectedPeers; if (!peers.Any()) { return; } // Announce the blocks on each nodes behavior which supports relaying. IEnumerable <MempoolBehavior> behaviors = peers.Where(x => x.PeerVersion?.Relay ?? false) .Select(x => x.Behavior <MempoolBehavior>()) .Where(x => x != null) .ToList(); foreach (MempoolBehavior behavior in behaviors) { await behavior.SendTrickleAsync().ConfigureAwait(false); } }, this.nodeLifetime.ApplicationStopping, repeatEvery: TimeSpans.FiveSeconds, startAfter: TimeSpans.TenSeconds); }
/// <summary> /// Relays a transaction to the connected peers. /// </summary> /// <param name="hash">Hash of the transaction.</param> public void RelayTransaction(uint256 hash) { IReadOnlyNetworkPeerCollection peers = this.connectionManager.ConnectedPeers; if (!peers.Any()) { this.logger.LogTrace("(-)[NO_PEERS]"); return; } // to add the hash to each local collection IEnumerable <MempoolBehavior> behaviours = peers.Select(s => s.Behavior <MempoolBehavior>()); foreach (MempoolBehavior mempoolBehavior in behaviours) { this.logger.LogTrace("Attempting to relaying transaction ID '{0}' to peer '{1}'.", hash, mempoolBehavior?.AttachedPeer.RemoteSocketEndpoint); if (mempoolBehavior?.AttachedPeer.PeerVersion.Relay ?? false) { mempoolBehavior.AddTransactionToSend(hash); this.logger.LogTrace("Added transaction ID '{0}' to send inventory of peer '{1}'.", hash, mempoolBehavior?.AttachedPeer.RemoteSocketEndpoint); } else { this.logger.LogTrace("Peer '{0}' does not support 'Relay', skipped.", mempoolBehavior?.AttachedPeer.RemoteSocketEndpoint); } } }
/// <summary> /// Relays a transaction to the connected peers. /// </summary> /// <param name="hash">Hash of the transaction.</param> public Task RelayTransaction(uint256 hash) { this.logger.LogTrace("({0}:'{1}')", nameof(hash), hash); IReadOnlyNetworkPeerCollection peers = this.connectionManager.ConnectedPeers; if (!peers.Any()) { this.logger.LogTrace("(-)[NO_PEERS]"); return(Task.CompletedTask); } // find all behaviours then start an exclusive task // to add the hash to each local collection IEnumerable <MempoolBehavior> behaviours = peers.Select(s => s.Behavior <MempoolBehavior>()); this.logger.LogTrace("(-)"); return(this.manager.MempoolLock.WriteAsync(() => { foreach (MempoolBehavior mempoolBehavior in behaviours) { if (mempoolBehavior?.AttachedPeer.PeerVersion.Relay ?? false) { if (!mempoolBehavior.filterInventoryKnown.ContainsKey(hash)) { mempoolBehavior.inventoryTxToSend.TryAdd(hash, hash); } } } })); }
/// <summary> /// Implements loop accepting connections from newly connected clients. /// </summary> private async Task AcceptClientsAsync(IReadOnlyNetworkPeerCollection networkPeers, List <IPEndPoint> iprangeFilteringExclusions) { this.logger.LogDebug("Accepting incoming connections."); try { while (!this.serverCancel.IsCancellationRequested) { TcpClient tcpClient = await this.tcpListener.AcceptTcpClientAsync().WithCancellationAsync(this.serverCancel.Token).ConfigureAwait(false); (bool successful, string reason) = this.AllowClientConnection(tcpClient, networkPeers, iprangeFilteringExclusions); if (!successful) { this.signals.Publish(new PeerConnectionAttemptFailed(true, (IPEndPoint)tcpClient.Client.RemoteEndPoint, reason)); this.logger.LogDebug("Connection from client '{0}' was rejected and will be closed, reason: {1}", tcpClient.Client.RemoteEndPoint, reason); tcpClient.Close(); continue; } this.logger.LogDebug("Connection accepted from client '{0}'.", tcpClient.Client.RemoteEndPoint); INetworkPeer connectedPeer = this.networkPeerFactory.CreateNetworkPeer(tcpClient, this.CreateNetworkPeerConnectionParameters(), this.networkPeerDisposer); this.signals.Publish(new PeerConnected(connectedPeer.Inbound, connectedPeer.PeerEndPoint)); } } catch (OperationCanceledException) { this.logger.LogDebug("Shutdown detected, stop accepting connections."); } catch (Exception e) { this.logger.LogDebug("Exception occurred: {0}", e.ToString()); } }
/// <inheritdoc/> public void Initialize(IConnectionManager connectionManager) { this.connectedPeers = connectionManager.ConnectedPeers; this.CurrentParameters = connectionManager.Parameters.Clone(); this.OnInitialize(); }
/// <inheritdoc/> public void Initialize(IConnectionManager connectionManager) { this.connectedPeers = connectionManager.ConnectedPeers; this.CurrentParameters = connectionManager.Parameters.Clone(); this.CurrentParameters.TemplateBehaviors.Add(new ConnectionManagerBehavior(false, connectionManager, this.loggerFactory)); this.OnInitialize(); }
/// <summary> /// A method that relays blocks found in <see cref="batch"/> to connected peers on the network. /// </summary> /// <remarks> /// <para> /// The list <see cref="batch"/> contains hashes of blocks that were validated by the consensus rules. /// </para> /// <para> /// These block hashes need to be relayed to connected peers. A peer that does not have a block /// will then ask for the entire block, that means only blocks that have been stored/cached should be relayed. /// </para> /// <para> /// During IBD blocks are not relayed to peers. /// </para> /// <para> /// If no nodes are connected the blocks are just discarded, however this is very unlikely to happen. /// </para> /// <para> /// Before relaying, verify the block is still in the best chain else discard it. /// </para> /// <para> /// TODO: consider moving the relay logic to the <see cref="LoopSteps.ProcessPendingStorageStep"/>. /// </para> /// </remarks> private async Task SendBatchAsync(List <ChainedHeader> batch) { this.logger.LogTrace("()"); int announceBlockCount = batch.Count; if (announceBlockCount == 0) { this.logger.LogTrace("(-)[NO_BLOCKS]"); return; } this.logger.LogTrace("There are {0} blocks in the announce queue.", announceBlockCount); // Remove blocks that we've reorged away from. foreach (ChainedHeader reorgedBlock in batch.Where(x => this.chainState.ConsensusTip.FindAncestorOrSelf(x) == null).ToList()) { this.logger.LogTrace("Block header '{0}' not found in the consensus chain and will be skipped.", reorgedBlock); // List removal is of O(N) complexity but in this case removals will happen just a few times a day (on orphaned blocks) // and always only the latest items in this list will be subjected to removal so in this case it's better than creating // a new list of blocks on every batch send that were not reorged. batch.Remove(reorgedBlock); } if (!batch.Any()) { this.logger.LogTrace("(-)[NO_BROADCAST_ITEMS]"); return; } IReadOnlyNetworkPeerCollection peers = this.connection.ConnectedPeers; if (!peers.Any()) { this.logger.LogTrace("(-)[NO_PEERS]"); return; } // Announce the blocks to each of the peers. List <BlockStoreBehavior> behaviours = peers.Select(s => s.Behavior <BlockStoreBehavior>()) .Where(b => b != null).ToList(); this.logger.LogTrace("{0} blocks will be sent to {1} peers.", batch.Count, behaviours.Count); foreach (BlockStoreBehavior behaviour in behaviours) { await behaviour.AnnounceBlocksAsync(batch).ConfigureAwait(false); } this.logger.LogTrace("(-)"); }
/// <summary> /// Determines if the peer should be disconnected. /// Peer should be disconnected in case it's IP is from the same group in which any other peer /// is and the peer wasn't added using -connect or -addNode command line arguments. /// </summary> private bool CheckIfPeerFromSameNetworkGroup(IReadOnlyNetworkPeerCollection networkPeers, IPEndPoint ipEndpoint, List <IPEndPoint> iprangeFilteringExclusions) { // Don't disconnect if range filtering is not turned on. if (!this.connectionManagerSettings.IpRangeFiltering) { this.logger.LogTrace("(-)[IP_RANGE_FILTERING_OFF]:false"); return(false); } // Don't disconnect if this peer has a local host address. if (ipEndpoint.Address.IsLocal()) { this.logger.LogTrace("(-)[IP_IS_LOCAL]:false"); return(false); } // Don't disconnect if this peer is in -addnode or -connect. if (this.connectionManagerSettings.RetrieveAddNodes().Union(this.connectionManagerSettings.Connect).Any(ep => ipEndpoint.MatchIpOnly(ep))) { this.logger.LogTrace("(-)[ADD_NODE_OR_CONNECT]:false"); return(false); } // Don't disconnect if this peer is in the exclude from IP range filtering group. if (iprangeFilteringExclusions.Any(ip => ip.MatchIpOnly(ipEndpoint))) { this.logger.LogTrace("(-)[PEER_IN_IPRANGEFILTER_EXCLUSIONS]:false"); return(false); } byte[] peerGroup = ipEndpoint.MapToIpv6().Address.GetGroup(); foreach (INetworkPeer connectedPeer in networkPeers) { if (ipEndpoint == connectedPeer.PeerEndPoint) { continue; } byte[] group = connectedPeer.PeerEndPoint.MapToIpv6().Address.GetGroup(); if (peerGroup.SequenceEqual(group)) { this.logger.LogTrace("(-)[SAME_GROUP]:true"); return(true); } } return(false); }
/// <summary> /// Starts listening on the server's initialized endpoint. /// </summary> public void Listen(IReadOnlyNetworkPeerCollection networkPeers, List <IPEndPoint> iprangeFilteringExclusions) { try { this.tcpListener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); this.tcpListener.Start(); this.acceptTask = this.AcceptClientsAsync(networkPeers, iprangeFilteringExclusions); } catch (Exception e) { this.logger.LogDebug("Exception occurred: {0}", e.ToString()); throw; } }
/// <summary> /// Initializes a new instance of the object having a chain of block headers and a list of available nodes. /// </summary> /// <param name="chain">Chain of block headers.</param> /// <param name="nodes">Network peers of the node.</param> /// <param name="protocolVersion">Version of the protocol that the node supports.</param> /// <param name="loggerFactory">Factory to be used to create logger for the puller.</param> protected BlockPuller(ConcurrentChain chain, IReadOnlyNetworkPeerCollection nodes, ProtocolVersion protocolVersion, ILoggerFactory loggerFactory) { this.Chain = chain; this.Nodes = nodes; this.logger = loggerFactory.CreateLogger(this.GetType().FullName); this.downloadedBlocks = new Dictionary <uint256, DownloadedBlock>(); this.pendingInventoryVectors = new Queue <uint256>(); this.assignedBlockTasks = new Dictionary <uint256, BlockPullerBehavior>(); this.peerQuality = new QualityScore(QualityScoreHistoryLength, loggerFactory); // Set the default requirements. this.requirements = new NetworkPeerRequirement { MinVersion = protocolVersion, RequiredServices = NetworkPeerServices.Network }; }
/// <summary> /// Announces blocks on all connected nodes memory pool behaviours every ten seconds. /// </summary> public void Start() { this.asyncLoop = this.asyncLoopFactory.Run("MemoryPool.RelayWorker", async token => { IReadOnlyNetworkPeerCollection peers = this.connection.ConnectedPeers; if (!peers.Any()) { return; } // announce the blocks on each nodes behaviour IEnumerable <MempoolBehavior> behaviours = peers.Select(s => s.Behavior <MempoolBehavior>()); foreach (MempoolBehavior behaviour in behaviours) { await behaviour.SendTrickleAsync().ConfigureAwait(false); } }, this.nodeLifetime.ApplicationStopping, repeatEvery: TimeSpans.TenSeconds, startAfter: TimeSpans.TenSeconds); }
/// <summary> /// A loop method that continuously relays blocks found in <see cref="blocksToAnnounce"/> to connected peers on the network. /// </summary> /// <remarks> /// <para> /// The queue <see cref="blocksToAnnounce"/> contains /// hashes of blocks that were validated by the consensus rules. /// </para> /// <para> /// This block hashes need to be relayed to connected peers. A peer that does not have a block /// will then ask for the entire block, that means only blocks that have been stored should be relayed. /// </para> /// <para> /// During IBD blocks are not relayed to peers. /// </para> /// <para> /// If no nodes are connected the blocks are just discarded, however this is very unlikely to happen. /// </para> /// <para> /// Before relaying, verify the block is still in the best chain else discard it. /// </para> /// TODO: consider moving the relay logic to the <see cref="LoopSteps.ProcessPendingStorageStep"/>. /// </remarks> public void RelayWorker() { this.logger.LogTrace("()"); this.asyncLoop = this.asyncLoopFactory.Run($"{this.name}.RelayWorker", async token => { this.logger.LogTrace("()"); int announceBlockCount = this.blocksToAnnounce.Count; if (announceBlockCount == 0) { this.logger.LogTrace("(-)[NO_BLOCKS]"); return; } this.logger.LogTrace("There are {0} blocks in the announce queue.", announceBlockCount); // Initialize this list with default size of 'announceBlockCount + 4' to prevent it from autoresizing during adding new items. // This +4 extra size is in case new items will be added to the queue during the loop. var broadcastItems = new List <ChainedBlock>(announceBlockCount + 4); while (this.blocksToAnnounce.TryPeek(out ChainedBlock block)) { this.logger.LogTrace("Checking if block '{0}' is on disk.", block); // The first block that is not on disk will abort the loop. if (!await this.blockRepository.ExistAsync(block.HashBlock).ConfigureAwait(false)) { this.logger.LogTrace("Block '{0}' not found in the store.", block); // In cases when the node had a reorg the 'blocksToAnnounce' contain blocks // that are not anymore on the main chain, those blocks are removed from 'blocksToAnnounce'. // Check if the reason why we don't have a block is a reorg or it hasn't been downloaded yet. if (this.chainState.ConsensusTip.FindAncestorOrSelf(block) == null) { this.logger.LogTrace("Block header '{0}' not found in the consensus chain.", block); // Remove hash that we've reorged away from. this.blocksToAnnounce.TryDequeue(out ChainedBlock unused); continue; } else { this.logger.LogTrace("Block header '{0}' found in the consensus chain, will wait until it is stored on disk.", block); } break; } if (this.blocksToAnnounce.TryDequeue(out ChainedBlock blockToBroadcast)) { this.logger.LogTrace("Block '{0}' moved from the announce queue to broadcast list.", block); broadcastItems.Add(blockToBroadcast); } else { this.logger.LogTrace("Unable to removing block '{0}' from the announce queue.", block); } } if (!broadcastItems.Any()) { this.logger.LogTrace("(-)[NO_BROADCAST_ITEMS]"); return; } IReadOnlyNetworkPeerCollection peers = this.connection.ConnectedPeers; if (!peers.Any()) { this.logger.LogTrace("(-)[NO_PEERS]"); return; } // Announce the blocks to each of the peers. IEnumerable <BlockStoreBehavior> behaviours = peers.Select(s => s.Behavior <BlockStoreBehavior>()); this.logger.LogTrace("{0} blocks will be sent to {1} peers.", broadcastItems.Count, behaviours.Count()); foreach (BlockStoreBehavior behaviour in behaviours) { await behaviour.AnnounceBlocksAsync(broadcastItems).ConfigureAwait(false); } }, this.nodeLifetime.ApplicationStopping, repeatEvery: TimeSpans.Second, startAfter: TimeSpans.FiveSeconds); this.logger.LogTrace("(-)"); }
/// <summary> /// Check if the client is allowed to connect based on certain criteria. /// </summary> /// <returns>When criteria is met returns <c>true</c>, to allow connection.</returns> private (bool successful, string reason) AllowClientConnection(TcpClient tcpClient, IReadOnlyNetworkPeerCollection networkPeers, List <IPEndPoint> iprangeFilteringExclusions) { // This is the IP address of the client connection. var clientRemoteEndPoint = tcpClient.Client.RemoteEndPoint as IPEndPoint; var isFromSameNetworkGroup = this.CheckIfPeerFromSameNetworkGroup(networkPeers, clientRemoteEndPoint, iprangeFilteringExclusions); if (isFromSameNetworkGroup) { this.logger.LogTrace("(-)[PEER_SAME_NETWORK_GROUP]:false"); return(false, $"Inbound Refused: Peer {clientRemoteEndPoint} is from the same network group."); } var peers = this.peerAddressManager.FindPeersByIp(clientRemoteEndPoint); var bannedPeer = peers.FirstOrDefault(p => p.IsBanned(this.dateTimeProvider.GetUtcNow())); if (bannedPeer != null) { this.logger.LogTrace("(-)[PEER_BANNED]:false"); return(false, $"Inbound Refused: Peer {clientRemoteEndPoint} is banned until {bannedPeer.BanUntil}."); } if (this.networkPeerDisposer.ConnectedInboundPeersCount >= this.connectionManagerSettings.MaxInboundConnections) { this.logger.LogTrace("(-)[MAX_CONNECTION_THRESHOLD_REACHED]:false"); return(false, $"Inbound Refused: Max Inbound Connection Threshold Reached, inbounds: {this.networkPeerDisposer.ConnectedInboundPeersCount}"); } if (!this.initialBlockDownloadState.IsInitialBlockDownload()) { this.logger.LogTrace("(-)[IBD_COMPLETE_ALLOW_CONNECTION]:true"); return(true, "Inbound Accepted: IBD Complete."); } // This is the network interface the client connection is being made against, not the IP of the client itself. var clientLocalEndPoint = tcpClient.Client.LocalEndPoint as IPEndPoint; // This checks whether the network interface being connected to by the client is configured to whitelist all inbound connections (i.e. -whitebind). bool endpointCanBeWhiteListed = this.connectionManagerSettings.Bind.Where(x => x.Whitelisted).Any(x => x.Endpoint.MapToIpv6().Contains(clientLocalEndPoint)); if (endpointCanBeWhiteListed) { this.logger.LogTrace("(-)[ENDPOINT_WHITELISTED_ALLOW_CONNECTION]:true"); return(true, "Inbound Accepted: Whitelisted endpoint connected during IBD."); } // This checks whether the client IP itself has been whitelisted (i.e. -whitelist). bool clientEndpointCanBeWhiteListed = this.connectionManagerSettings.Whitelist.Any(x => x.MatchIpOnly(clientRemoteEndPoint)); if (clientEndpointCanBeWhiteListed) { this.logger.LogTrace("(-)[CLIENT_WHITELISTED_ALLOW_CONNECTION]:true"); return(true, "Inbound Accepted: Whitelisted client connected during IBD."); } this.logger.LogInformation("Node '{0}' is not whitelisted via endpoint '{1}' during initial block download.", clientRemoteEndPoint, clientLocalEndPoint); return(false, "Inbound Refused: Non Whitelisted endpoint connected during IBD."); }