/// <summary> /// When a header is checkpointed and has a correct hash, chain that ends with such a header /// will be marked as <see cref="ValidationState.AssumedValid" /> and requested for download. /// </summary> /// <param name="chainedHeader">Checkpointed header.</param> /// <param name="latestNewHeader">The latest new header that was presented by the peer.</param> /// <param name="checkpoint">Information about the checkpoint at the height of the <paramref name="chainedHeader"/>.</param> private ConnectNewHeadersResult HandleCheckpointsHeader(ChainedHeader chainedHeader, ChainedHeader latestNewHeader, CheckpointInfo checkpoint) { this.logger.LogTrace("({0}:'{1}',{2}:'{3}',{4}.{5}:'{6}')", nameof(chainedHeader), chainedHeader, nameof(latestNewHeader), latestNewHeader, nameof(checkpoint), nameof(checkpoint.Hash), checkpoint.Hash); if (checkpoint.Hash != chainedHeader.HashBlock) { this.logger.LogDebug("Chained header '{0}' does not match checkpoint '{1}'.", chainedHeader, checkpoint.Hash); this.logger.LogTrace("(-)[INVALID_HEADER_NOT_MATCHING_CHECKPOINT]"); throw new InvalidHeaderException(); } ChainedHeader subchainTip = chainedHeader; if (chainedHeader.Height == this.checkpoints.GetLastCheckpointHeight()) { subchainTip = latestNewHeader; } ConnectNewHeadersResult connectNewHeadersResult = this.MarkBetterChainAsRequired(subchainTip); this.MarkTrustedChainAsAssumedValid(chainedHeader); this.logger.LogTrace("(-):{0}", connectNewHeadersResult); return(connectNewHeadersResult); }
/// <summary>Presents the headers to <see cref="ConsensusManager"/> and handles exceptions if any.</summary> /// <remarks>Have to be locked by <see cref="asyncLock"/>.</remarks> /// <param name="headers">List of headers that the peer presented.</param> /// <param name="triggerDownload">Specifies if the download should be scheduled for interesting blocks.</param> private async Task <ConnectNewHeadersResult> PresentHeadersLockedAsync(List <BlockHeader> headers, bool triggerDownload = true) { this.logger.LogTrace("({0}.{1}:{2},{3}:{4})", nameof(headers), nameof(headers.Count), headers.Count, nameof(triggerDownload), triggerDownload); ConnectNewHeadersResult result = null; INetworkPeer peer = this.AttachedPeer; if (peer == null) { this.logger.LogTrace("(-)[PEER_DETACHED]:null"); return(null); } try { result = this.consensusManager.HeadersPresented(peer, headers, triggerDownload); } catch (ConnectHeaderException) { this.logger.LogDebug("Unable to connect headers."); this.cachedHeaders.Clear(); // Resync in case can't connect. await this.ResyncAsync().ConfigureAwait(false); } catch (ConsensusRuleException exception) { this.logger.LogDebug("Peer's header is invalid. Peer will be banned and disconnected. Error: {0}.", exception.ConsensusError); this.peerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, $"Peer presented invalid header, error: {exception.ConsensusError}."); } catch (HeaderInvalidException) { this.logger.LogDebug("Peer's header is invalid. Peer will be banned and disconnected."); this.peerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, $"Peer presented invalid header."); } catch (CheckpointMismatchException) { this.logger.LogDebug("Peer's headers violated a checkpoint. Peer will be banned and disconnected."); this.peerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, "Peer presented header that violates a checkpoint."); } catch (MaxReorgViolationException) { this.logger.LogDebug("Peer violates max reorg. Peer will be banned and disconnected."); this.peerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, "Peer violates max reorg rule."); } this.logger.LogTrace("(-):'{0}'", result); return(result); }
/// <summary> /// Notifies the chained header behaviors of all connected peers when a consensus tip is changed. /// Consumes headers from their caches if there are any. /// </summary> private async Task NotifyBehaviorsOnConsensusTipChangedAsync() { this.logger.LogTrace("()"); var behaviors = new List <ConsensusManagerBehavior>(); lock (this.peerLock) { foreach (INetworkPeer peer in this.peersByPeerId.Values) { behaviors.Add(peer.Behavior <ConsensusManagerBehavior>()); } } var blocksToDownload = new List <ConnectNewHeadersResult>(); foreach (ConsensusManagerBehavior consensusManagerBehavior in behaviors) { ConnectNewHeadersResult connectNewHeadersResult = await consensusManagerBehavior.ConsensusTipChangedAsync(this.Tip).ConfigureAwait(false); int?peerId = consensusManagerBehavior.AttachedPeer?.Connection?.Id; if (peerId == null) { continue; } if (connectNewHeadersResult == null) { this.logger.LogTrace("No new blocks to download were presented by peer ID {0}.", peerId); continue; } blocksToDownload.Add(connectNewHeadersResult); this.logger.LogTrace("{0} headers were added to download by peer ID {1}.", connectNewHeadersResult.DownloadTo.Height - connectNewHeadersResult.DownloadFrom.Height + 1, peerId); } foreach (ConnectNewHeadersResult newHeaders in blocksToDownload) { this.DownloadBlocks(newHeaders.ToArray(), this.ProcessDownloadedBlock); } this.logger.LogTrace("(-)"); }
/// <summary>Presents cached headers to <see cref="Consensus.ConsensusManager"/> from the cache if any and removes consumed from the cache.</summary> public async Task <ConnectNewHeadersResult> ConsensusTipChangedAsync() { ConnectNewHeadersResult result = null; bool syncRequired = false; using (await this.asyncLock.LockAsync().ConfigureAwait(false)) { if (this.cachedHeaders.Count != 0 && this.CanConsumeCache()) { result = await this.PresentHeadersLockedAsync(this.cachedHeaders, false).ConfigureAwait(false); if ((result == null) || (result.Consumed == null)) { if (result == null) { this.cachedHeaders.Clear(); } this.logger.LogTrace("(-)[NO_HEADERS_CONNECTED]:null"); return(null); } this.BestReceivedTip = result.Consumed; this.UpdateBestSentHeader(this.BestReceivedTip); int consumedCount = this.GetConsumedHeadersCount(this.cachedHeaders, result.Consumed.Header); this.cachedHeaders.RemoveRange(0, consumedCount); int cacheSize = this.cachedHeaders.Count; this.logger.LogDebug("{0} entries were consumed from the cache, {1} items were left.", consumedCount, cacheSize); syncRequired = cacheSize == 0; } } if (syncRequired) { await this.ResyncAsync().ConfigureAwait(false); } return(result); }
/// <summary> /// A list of headers are presented from a given peer, /// we'll attempt to connect the headers to the tree and if new headers are found they will be queued for download. /// </summary> /// <param name="peerId">The peer that is providing the headers.</param> /// <param name="headers">The list of new headers.</param> /// <returns>The last chained header that is connected to the tree.</returns> /// <exception cref="ConnectHeaderException">Thrown when first presented header can't be connected to any known chain in the tree.</exception> /// <exception cref="CheckpointMismatchException">Thrown if checkpointed header doesn't match the checkpoint hash.</exception> public ChainedHeader HeadersPresented(int peerId, List <BlockHeader> headers) { this.logger.LogTrace("({0}:{1},{2}.{3}:{4})", nameof(peerId), peerId, nameof(headers), nameof(headers.Count), headers.Count); ConnectNewHeadersResult newHeaders = null; lock (this.peerLock) { newHeaders = this.chainedHeaderTree.ConnectNewHeaders(peerId, headers); this.blockPuller.NewTipClaimed(peerId, newHeaders.Consumed); } if (newHeaders.DownloadTo != null) { this.DownloadBlocks(newHeaders.ToHashArray(), this.ProcessDownloadedBlock); } this.logger.LogTrace("(-):'{0}'", newHeaders.Consumed); return(newHeaders.Consumed); }
/// <summary>Presents cached headers to <see cref="ConsensusManager"/> from the cache if any and removes consumed from the cache.</summary> public async Task <ConnectNewHeadersResult> ConsensusTipChangedAsync() { this.logger.LogTrace("()"); ConnectNewHeadersResult result = null; bool syncRequired = false; using (await this.asyncLock.LockAsync().ConfigureAwait(false)) { if (this.cachedHeaders.Count != 0) { result = await this.PresentHeadersLockedAsync(this.cachedHeaders, false).ConfigureAwait(false); if (result == null) { this.logger.LogTrace("(-)[NO_HEADERS_CONNECTED]:null"); return(null); } this.ExpectedPeerTip = result.Consumed; this.UpdateBestSentHeader(this.ExpectedPeerTip); int consumedCount = this.cachedHeaders.IndexOf(result.Consumed.Header) + 1; this.cachedHeaders.RemoveRange(0, consumedCount); int cacheSize = this.cachedHeaders.Count; this.logger.LogTrace("{0} entries were consumed from the cache, {1} items were left.", consumedCount, cacheSize); syncRequired = cacheSize == 0; } } if (syncRequired) { await this.ResyncAsync().ConfigureAwait(false); } this.logger.LogTrace("(-):'{0}'", result); return(result); }
/// <summary> /// The header is assumed to be valid, the header and all of its previous headers will be marked as <see cref="ValidationState.AssumedValid" />. /// If the header's cumulative work is better then <see cref="IChainState.ConsensusTip" /> the header and all its predecessors will be marked with <see cref="BlockDataAvailabilityState.BlockRequired" />. /// </summary> /// <param name="assumedValidHeader">The header that is assumed to be valid.</param> /// <param name="latestNewHeader">The last header in the list of presented new headers.</param> /// <param name="isBelowLastCheckpoint">Set to <c>true</c> if <paramref name="assumedValidHeader"/> is below the last checkpoint, /// <c>false</c> otherwise or if checkpoints are disabled.</param> private ConnectNewHeadersResult HandleAssumedValidHeader(ChainedHeader assumedValidHeader, ChainedHeader latestNewHeader, bool isBelowLastCheckpoint) { this.logger.LogTrace("({0}:'{1}',{2}:'{3}',{4}:{5})", nameof(assumedValidHeader), assumedValidHeader, nameof(latestNewHeader), latestNewHeader, nameof(isBelowLastCheckpoint), isBelowLastCheckpoint); ChainedHeader bestTip = this.GetConsensusTip(); var connectNewHeadersResult = new ConnectNewHeadersResult() { Consumed = latestNewHeader }; if (latestNewHeader.ChainWork > bestTip.ChainWork) { this.logger.LogDebug("Chained header '{0}' is the tip of a chain with more work than our current consensus tip.", latestNewHeader); ChainedHeader latestHeaderToMark = isBelowLastCheckpoint ? assumedValidHeader : latestNewHeader; connectNewHeadersResult = this.MarkBetterChainAsRequired(latestHeaderToMark); } this.MarkTrustedChainAsAssumedValid(assumedValidHeader); this.logger.LogTrace("(-):{0}", connectNewHeadersResult); return(connectNewHeadersResult); }
/// <summary> /// Notifies the chained header behaviors of all connected peers when a reorg happens. /// Consumes headers from their caches if there are any. /// </summary> private void NotifyChainedHeaderBehaviorsOnReorg() { this.logger.LogTrace("()"); var blocksToDownload = new List <ConnectNewHeadersResult>(); foreach (INetworkPeer peer in this.connectionManager.ConnectedPeers) { List <ChainedHeader> headersToConnect = peer.Behavior <ChainHeadersBehavior>().ConsensusAdvanced(this.Tip); if (headersToConnect == null) { this.logger.LogTrace("No cached headers were presented by peer ID {0}.", peer.Connection.Id); continue; } List <BlockHeader> headers = headersToConnect.Select(ch => ch.Header).ToList(); this.logger.LogTrace("{0} cached headers were presented by peer ID {1}.", headers.Count, peer.Connection.Id); lock (this.peerLock) { ConnectNewHeadersResult connectNewHeaders = this.chainedHeaderTree.ConnectNewHeaders(peer.Connection.Id, headers); if (connectNewHeaders.DownloadTo != null) { blocksToDownload.Add(connectNewHeaders); } } } foreach (ConnectNewHeadersResult newHeaders in blocksToDownload) { this.DownloadBlocks(newHeaders.ToHashArray(), this.ProcessDownloadedBlock); } this.logger.LogTrace("(-)"); }
/// <summary> /// A chain with more work than our current consensus tip was found so mark all it's descendants as required. /// </summary> /// <param name="latestNewHeader">The new header that represents a longer chain.</param> /// <returns>The new headers that need to be downloaded.</returns> private ConnectNewHeadersResult MarkBetterChainAsRequired(ChainedHeader latestNewHeader) { this.logger.LogTrace("({0}:'{1}')", nameof(latestNewHeader), latestNewHeader); var connectNewHeadersResult = new ConnectNewHeadersResult(); connectNewHeadersResult.DownloadTo = connectNewHeadersResult.Consumed = latestNewHeader; ChainedHeader current = latestNewHeader; ChainedHeader next = current; while (!this.HeaderWasRequested(current)) { current.BlockDataAvailability = BlockDataAvailabilityState.BlockRequired; next = current; current = current.Previous; } connectNewHeadersResult.DownloadFrom = next; this.logger.LogTrace("(-):{0}", connectNewHeadersResult); return(connectNewHeadersResult); }
/// <summary> /// Processes "headers" message received from the peer. /// </summary> /// <param name="peer">Peer from which the message was received.</param> /// <param name="headersPayload">Payload of "headers" message to process.</param> /// <remarks> /// "headers" message is sent in response to "getheaders" message or it is solicited /// by the peer when a new block is validated (unless in IBD). /// <para> /// When we receive "headers" message from the peer, we can adjust our knowledge /// of the peer's view of the chain. We update its pending tip, which represents /// the tip of the best chain we think the peer has. /// </para> /// </remarks> private async Task ProcessHeadersAsync(INetworkPeer peer, HeadersPayload headersPayload) { List <BlockHeader> headers = headersPayload.Headers; if (headers.Count == 0) { this.logger.LogTrace("Headers payload with no headers was received. Assuming we're synced with the peer."); this.logger.LogTrace("(-)[NO_HEADERS]"); return; } if (!this.ValidateHeadersPayload(peer, headersPayload, out string validationError)) { this.peerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, validationError); this.logger.LogTrace("(-)[VALIDATION_FAILED]"); return; } using (await this.asyncLock.LockAsync().ConfigureAwait(false)) { if (this.cachedHeaders.Count > CacheSyncHeadersThreshold) // TODO when proven headers are implemented combine this with size threshold of N mb. { // Ignore this message because cache is full. this.logger.LogTrace("(-)[CACHE_IS_FULL]"); return; } // If queue is not empty, add to queue instead of calling CM. if (this.cachedHeaders.Count != 0) { this.cachedHeaders.AddRange(headers); this.logger.LogTrace("{0} headers were added to cache, new cache size is {1}.", headers.Count, this.cachedHeaders.Count); this.logger.LogTrace("(-)[CACHED]"); return; } ConnectNewHeadersResult result = await this.PresentHeadersLockedAsync(headers).ConfigureAwait(false); if (result == null) { this.logger.LogTrace("Processing of {0} headers failed.", headers.Count); this.logger.LogTrace("(-)[PROCESSING_FAILED]"); return; } this.ExpectedPeerTip = result.Consumed; this.UpdateBestSentHeader(this.ExpectedPeerTip); if (result.Consumed.HashBlock != headers.Last().GetHash()) { // Some headers were not consumed, add to cache. int consumedCount = headers.IndexOf(result.Consumed.Header) + 1; this.cachedHeaders.AddRange(headers.Skip(consumedCount)); this.logger.LogTrace("{0} out of {1} items were not consumed and added to cache.", headers.Count - consumedCount, headers.Count); } if (this.cachedHeaders.Count == 0) { await this.ResyncAsync().ConfigureAwait(false); } } }
/// <summary>Presents the headers to <see cref="Consensus.ConsensusManager"/> and handles exceptions if any.</summary> /// <remarks>Have to be locked by <see cref="asyncLock"/>.</remarks> /// <param name="headers">List of headers that the peer presented.</param> /// <param name="triggerDownload">Specifies if the download should be scheduled for interesting blocks.</param> protected async Task <ConnectNewHeadersResult> PresentHeadersLockedAsync(List <BlockHeader> headers, bool triggerDownload = true) { ConnectNewHeadersResult result = null; INetworkPeer peer = this.AttachedPeer; if (peer == null) { this.logger.LogDebug("Peer detached!"); this.logger.LogTrace("(-)[PEER_DETACHED]:null"); return(null); } try { result = this.ConsensusManager.HeadersPresented(peer, headers, triggerDownload); } catch (ConnectHeaderException) { // This is typically thrown when the first header refers to a previous block hash that is not in // the header tree currently. This is not regarded as bannable, as it could be a legitimate reorg. this.logger.LogDebug("Unable to connect headers."); if (this.CheckIfUnsolicitedFutureHeader(headers)) { // However, during IBD it is much more likely that the unconnectable header is an unsolicited // block advertisement. In that case we just ignore the failed header and don't modify the cache. // TODO: Review more closely what Bitcoin Core does here - they seem to allow 8 unconnectable headers before // applying partial DoS points to a peer. Incorporate that into rate limiting code? this.logger.LogTrace("(-)[HEADER_FUTURE_CANT_CONNECT_2]"); } else { // Resync in case we can't connect the header. this.cachedHeaders.Clear(); await this.ResyncAsync().ConfigureAwait(false); } } catch (ConsensusRuleException exception) { this.logger.LogDebug("Peer's header is invalid. Peer will be banned and disconnected. Error: {0}.", exception.ConsensusError); this.PeerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, $"Peer presented invalid header, error: {exception.ConsensusError}."); } catch (HeaderInvalidException) { this.logger.LogDebug("Peer's header is invalid. Peer will be banned and disconnected."); this.PeerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, $"Peer presented invalid header."); } catch (CheckpointMismatchException) { this.logger.LogDebug("Peer's headers violated a checkpoint. Peer will be banned and disconnected."); this.PeerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, "Peer presented header that violates a checkpoint."); } catch (MaxReorgViolationException) { this.logger.LogDebug("Peer violates max reorg. Peer will be banned and disconnected."); this.PeerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, "Peer violates max reorg rule."); } return(result); }
/// <summary> /// Processes "headers" message received from the peer. /// </summary> /// <param name="peer">Peer from which the message was received.</param> /// <param name="headers">List of headers to process.</param> /// <remarks> /// "headers" message is sent in response to "getheaders" message or it is solicited /// by the peer when a new block is validated (unless in IBD). /// <para> /// When we receive "headers" message from the peer, we can adjust our knowledge /// of the peer's view of the chain. We update its pending tip, which represents /// the tip of the best chain we think the peer has. /// </para> /// </remarks> protected virtual async Task ProcessHeadersAsync(INetworkPeer peer, List <BlockHeader> headers) { this.logger.LogDebug("Received {0} headers. First: '{1}' Last: '{2}'.", headers.Count, headers.FirstOrDefault()?.ToString(), headers.LastOrDefault()?.ToString()); if (headers.Count == 0) { this.logger.LogDebug("Headers payload with no headers was received. Assuming we're synced with the peer."); this.logger.LogTrace("(-)[NO_HEADERS]"); return; } if (!this.ValidateHeadersFromPayload(peer, headers, out string validationError)) { this.PeerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, validationError); this.logger.LogDebug("Headers are invalid. Peer was banned."); this.logger.LogTrace("(-)[VALIDATION_FAILED]"); return; } using (await this.asyncLock.LockAsync().ConfigureAwait(false)) { if (this.cachedHeaders.Count > CacheSyncHeadersThreshold) // TODO when proven headers are implemented combine this with size threshold of N mb. { // Ignore this message because cache is full. this.logger.LogDebug("Cache is full. Headers ignored."); this.logger.LogTrace("(-)[CACHE_IS_FULL]"); return; } // If queue is not empty, add to queue instead of calling CM. if (this.cachedHeaders.Count != 0) { uint256 cachedHeader = this.cachedHeaders.Last().GetHash(); uint256 prevNewHeader = headers.First().HashPrevBlock; // Ensure headers can connect to cached headers. if (cachedHeader == prevNewHeader) { this.cachedHeaders.AddRange(headers); this.logger.LogDebug("{0} headers were added to cache, new cache size is {1}.", headers.Count, this.cachedHeaders.Count); this.logger.LogTrace("(-)[HEADERS_ADDED_TO_CACHE]"); return; } // Workaround for special case when peer announces new block but we don't want to clean the cache. // For example, we are busy syncing and have D E F in the cache (assume A B C are already in the // CHT). The peer now advertises new block M because we haven't completed IBD yet. // We do not want to clear the currently useful cache unnecessarily. if (this.CheckIfUnsolicitedFutureHeader(headers)) { this.logger.LogTrace("(-)[HEADER_FUTURE_CANT_CONNECT]"); return; } // The header(s) could not be connected and were not an unsolicited block advertisement. this.cachedHeaders.Clear(); await this.ResyncAsync().ConfigureAwait(false); this.logger.LogDebug("Header {0} could not be connected to last cached header {1}, clear cache and resync.", headers[0].GetHash(), cachedHeader); this.logger.LogTrace("(-)[FAILED_TO_ATTACH_TO_CACHE]"); return; } ConnectNewHeadersResult result = await this.PresentHeadersLockedAsync(headers).ConfigureAwait(false); if (result == null) { this.logger.LogDebug("Processing of {0} headers failed.", headers.Count); this.logger.LogTrace("(-)[PROCESSING_FAILED]"); return; } if (result.Consumed == null) { this.cachedHeaders.AddRange(headers); this.logger.LogDebug("All {0} items were not consumed and added to cache.", headers.Count); return; } if (result.Consumed == this.BestReceivedTip) { this.logger.LogDebug("No new headers were presented"); this.logger.LogTrace("(-)[NO_NEW_HEADERS_PRESENTED]"); return; } this.logger.LogTrace("{0} is {1} and {2} is {3}", nameof(this.BestReceivedTip), this.BestReceivedTip, nameof(result.Consumed), result.Consumed); this.BestReceivedTip = result.Consumed; this.UpdateBestSentHeader(this.BestReceivedTip); if (result.Consumed.HashBlock != headers.Last().GetHash()) { // Some headers were not consumed, add to cache. int consumedCount = this.GetConsumedHeadersCount(headers, result.Consumed.Header); this.cachedHeaders.AddRange(headers.Skip(consumedCount)); this.logger.LogDebug("{0} out of {1} items were not consumed and added to cache.", headers.Count - consumedCount, headers.Count); } if (this.cachedHeaders.Count == 0) { await this.ResyncAsync().ConfigureAwait(false); } } }
/// <summary> /// Processes "headers" message received from the peer. /// </summary> /// <param name="peer">Peer from which the message was received.</param> /// <param name="headers">List of headers to process.</param> /// <remarks> /// "headers" message is sent in response to "getheaders" message or it is solicited /// by the peer when a new block is validated (unless in IBD). /// <para> /// When we receive "headers" message from the peer, we can adjust our knowledge /// of the peer's view of the chain. We update its pending tip, which represents /// the tip of the best chain we think the peer has. /// </para> /// </remarks> protected virtual async Task ProcessHeadersAsync(INetworkPeer peer, List <BlockHeader> headers) { this.logger.LogDebug("Received {0} headers. First: '{1}' Last: '{2}'.", headers.Count, headers.FirstOrDefault()?.ToString(), headers.LastOrDefault()?.ToString()); if (headers.Count == 0) { this.logger.LogDebug("Headers payload with no headers was received. Assuming we're synced with the peer."); this.logger.LogTrace("(-)[NO_HEADERS]"); return; } if (!this.ValidateHeadersFromPayload(peer, headers, out string validationError)) { this.peerBanning.BanAndDisconnectPeer(peer.PeerEndPoint, validationError); this.logger.LogDebug("Headers are invalid. Peer was banned."); this.logger.LogTrace("(-)[VALIDATION_FAILED]"); return; } using (await this.asyncLock.LockAsync().ConfigureAwait(false)) { if (this.cachedHeaders.Count > CacheSyncHeadersThreshold) // TODO when proven headers are implemented combine this with size threshold of N mb. { // Ignore this message because cache is full. this.logger.LogDebug("Cache is full. Headers ignored."); this.logger.LogTrace("(-)[CACHE_IS_FULL]"); return; } // If queue is not empty, add to queue instead of calling CM. if (this.cachedHeaders.Count != 0) { uint256 cachedHeader = this.cachedHeaders.Last().GetHash(); uint256 prevNewHeader = headers.First().HashPrevBlock; // ensure headers can connect to cached headers. if (cachedHeader == prevNewHeader) { this.cachedHeaders.AddRange(headers); this.logger.LogDebug("{0} headers were added to cache, new cache size is {1}.", headers.Count, this.cachedHeaders.Count); this.logger.LogTrace("(-)[HEADERS_ADDED_TO_CACHE]"); return; } // Workaround for special case when peer announces new block but we don't want to clean the cache. if (headers.Count == 1 && this.BestReceivedTip != null) { // Distance of header from the peer expected tip. double distanceSeconds = (headers[0].BlockTime - this.BestReceivedTip.Header.BlockTime).TotalSeconds; if (this.chain.Network.MaxTipAge < distanceSeconds) { // A single header that is not connected to last header is likely an // unsolicited header that is a result of the peer tip being extended. // If the header time is far in the future we ignore it. this.logger.LogTrace("(-)[HEADER_FUTURE_CANT_CONNECT]"); return; } } this.cachedHeaders.Clear(); await this.ResyncAsync().ConfigureAwait(false); this.logger.LogDebug("Header {0} could not be connected to last cached header {1}, clear cache and resync.", headers[0].GetHash(), cachedHeader); this.logger.LogTrace("(-)[FAILED_TO_ATTACH_TO_CACHE]"); return; } ConnectNewHeadersResult result = await this.PresentHeadersLockedAsync(headers).ConfigureAwait(false); if (result == null) { this.logger.LogDebug("Processing of {0} headers failed.", headers.Count); this.logger.LogTrace("(-)[PROCESSING_FAILED]"); return; } if (result.Consumed == null) { this.cachedHeaders.AddRange(headers); this.logger.LogDebug("All {0} items were not consumed and added to cache.", headers.Count); return; } this.BestReceivedTip = result.Consumed; this.UpdateBestSentHeader(this.BestReceivedTip); if (result.Consumed.HashBlock != headers.Last().GetHash()) { // Some headers were not consumed, add to cache. int consumedCount = headers.IndexOf(result.Consumed.Header) + 1; this.cachedHeaders.AddRange(headers.Skip(consumedCount)); this.logger.LogDebug("{0} out of {1} items were not consumed and added to cache.", headers.Count - consumedCount, headers.Count); } if (this.cachedHeaders.Count == 0) { await this.ResyncAsync().ConfigureAwait(false); } } }
/// <summary> /// A new list of headers are presented by a peer, the headers will try to be connected to the tree. /// Blocks associated with headers that are interesting (i.e. represent a chain with greater chainwork than our consensus tip) /// will be requested for download. /// </summary> /// <remarks> /// The headers are assumed to be in consecutive order. /// </remarks> /// <param name="networkPeerId">Id of a peer that presented the headers.</param> /// <param name="headers">The list of headers to connect to the tree.</param> /// <returns> /// Information about which blocks need to be downloaded together with information about which input headers were processed. /// Only headers that we can validate will be processed. The rest of the headers will be submitted later again for processing. /// </returns> public ConnectNewHeadersResult ConnectNewHeaders(int networkPeerId, List <BlockHeader> headers) { Guard.NotNull(headers, nameof(headers)); this.logger.LogTrace("({0}:{1},{2}.{3}:{4})", nameof(networkPeerId), networkPeerId, nameof(headers), nameof(headers.Count), headers.Count); if (!this.chainedHeadersByHash.ContainsKey(headers[0].HashPrevBlock)) { this.logger.LogTrace("(-)[HEADER_COULD_NOT_CONNECT]"); throw new ConnectHeaderException(); } List <ChainedHeader> newChainedHeaders = this.CreateNewHeaders(headers); uint256 lastHash = headers.Last().GetHash(); this.AddOrReplacePeerTip(networkPeerId, lastHash); if (newChainedHeaders == null) { this.logger.LogTrace("(-)[NO_NEW_HEADERS]"); return(new ConnectNewHeadersResult() { Consumed = this.chainedHeadersByHash[lastHash] }); } ChainedHeader earliestNewHeader = newChainedHeaders.First(); ChainedHeader latestNewHeader = newChainedHeaders.Last(); ConnectNewHeadersResult connectNewHeadersResult = null; bool isAssumedValidEnabled = this.consensusSettings.BlockAssumedValid != null; bool isBelowLastCheckpoint = this.consensusSettings.UseCheckpoints && (earliestNewHeader.Height <= this.checkpoints.GetLastCheckpointHeight()); if (isBelowLastCheckpoint || isAssumedValidEnabled) { ChainedHeader currentChainedHeader = latestNewHeader; // When we look for a checkpoint header or an assume valid header, we go from the last presented // header to the first one in the reverse order because if there were multiple checkpoints or a checkpoint // and an assume valid header inside of the presented list of headers, we would only be interested in the last // one as it would cover all previous headers. Reversing the order of processing guarantees that we only need // to deal with one special header, which simplifies the implementation. while (currentChainedHeader != earliestNewHeader) { if (currentChainedHeader.HashBlock == this.consensusSettings.BlockAssumedValid) { this.logger.LogDebug("Chained header '{0}' represents an assumed valid block.", currentChainedHeader); connectNewHeadersResult = this.HandleAssumedValidHeader(currentChainedHeader, latestNewHeader, isBelowLastCheckpoint); break; } CheckpointInfo checkpoint = this.checkpoints.GetCheckpoint(currentChainedHeader.Height); if (checkpoint != null) { this.logger.LogDebug("Chained header '{0}' is a checkpoint.", currentChainedHeader); connectNewHeadersResult = this.HandleCheckpointsHeader(currentChainedHeader, latestNewHeader, checkpoint); break; } currentChainedHeader = currentChainedHeader.Previous; } if ((connectNewHeadersResult == null) && isBelowLastCheckpoint) { connectNewHeadersResult = new ConnectNewHeadersResult() { Consumed = latestNewHeader }; this.logger.LogTrace("Chained header '{0}' below last checkpoint.", currentChainedHeader); } if (connectNewHeadersResult != null) { this.logger.LogTrace("(-)[CHECKPOINT_OR_ASSUMED_VALID]:{0}", connectNewHeadersResult); return(connectNewHeadersResult); } } if (latestNewHeader.ChainWork > this.GetConsensusTip().ChainWork) { this.logger.LogDebug("Chained header '{0}' is the tip of a chain with more work than our current consensus tip.", latestNewHeader); connectNewHeadersResult = this.MarkBetterChainAsRequired(latestNewHeader); } this.logger.LogTrace("(-):{0}", connectNewHeadersResult); return(connectNewHeadersResult); }