/// <inheritdoc /> public override void Initialize(ChainedHeader chainTip) { base.Initialize(chainTip); var breezeCoinView = (DBreezeCoinView)((CachedCoinView)this.UtxoSet).Inner; breezeCoinView.Initialize(); uint256 consensusTipHash = breezeCoinView.GetTipHash(); while (true) { ChainedHeader pendingTip = chainTip.FindAncestorOrSelf(consensusTipHash); if (pendingTip != null) { break; } this.logger.LogInformation("Rewinding coin db from {0}", consensusTipHash); // In case block store initialized behind, rewind until or before the block store tip. // The node will complete loading before connecting to peers so the chain will never know if a reorg happened. consensusTipHash = breezeCoinView.Rewind(); } }
/// <summary> /// Set the tip of <see cref="ConsensusManager"/>, if the given <paramref name="chainTip"/> is not equal to <see cref="Tip"/> /// then rewind consensus until a common header is found. /// </summary> /// <remarks> /// If <see cref="blockStore"/> is not <c>null</c> (block store is available) then all block headers in /// <see cref="chainedHeaderTree"/> will be marked as their block data is available. /// If store is not available the <see cref="ConsensusManager"/> won't be able to serve blocks from disk, /// instead all block requests that are not in memory will be sent to the <see cref="blockPuller"/>. /// </remarks> /// <param name="chainTip">Last common header between chain repository and block store if it's available, /// if the store is not available it is the chain repository tip.</param> public async Task InitializeAsync(ChainedHeader chainTip) { this.logger.LogTrace("({0}:'{1}')", nameof(chainTip), chainTip); // TODO: consensus store // We should consider creating a consensus store class that will internally contain // coinview and it will abstract the methods `RewindAsync()` `GetBlockHashAsync()` uint256 consensusTipHash = await this.consensusRules.GetBlockHashAsync().ConfigureAwait(false); while (true) { this.Tip = chainTip.FindAncestorOrSelf(consensusTipHash); if (this.Tip?.HashBlock == consensusTipHash) { break; } // In case block store initialized behind, rewind until or before the block store tip. // The node will complete loading before connecting to peers so the chain will never know if a reorg happened. consensusTipHash = await this.consensusRules.RewindAsync().ConfigureAwait(false); } this.chainedHeaderTree.Initialize(this.Tip, this.blockStore != null); this.logger.LogTrace("(-)"); }
/// <summary>Find last header that should be included in headers payload.</summary> protected ChainedHeader GetLastHeaderToSend(ChainedHeader fork, uint256 hashStop) { ChainedHeader lastHeader = this.ConsensusManager.Tip; // If the hash stop has been given, calculate the last chained header from it. if (hashStop != null && hashStop != uint256.Zero) { ChainedHeader hashStopHeader = lastHeader.FindAncestorOrSelf(hashStop); if ((hashStopHeader != null) && (lastHeader.Height > fork.Height)) { lastHeader = hashStopHeader; } } // Do not return more than 2000 headers from the fork point. if ((lastHeader.Height - fork.Height) > MaxItemsPerHeadersMessage) { // e.g. If fork = 3000 and tip is 6000 we need to start from block 5000. int startFromHeight = fork.Height + MaxItemsPerHeadersMessage; lastHeader = lastHeader.GetAncestor(startFromHeight); } return(lastHeader); }
/// <summary>Updates the best sent header but only if the new value is better or is on a different chain.</summary> /// <param name="header">The new value to set if it is better or on a different chain.</param> public void UpdateBestSentHeader(ChainedHeader header) { this.logger.LogTrace("({0}:'{1}')", nameof(header), header); if (header == null) { this.logger.LogTrace("(-)[HEADER_NULL]"); return; } ChainedHeader bestSentHeader = null; lock (this.bestSentHeaderLock) { if (this.BestSentHeader != null) { ChainedHeader ancestorOrSelf = header.FindAncestorOrSelf(this.BestSentHeader); if (ancestorOrSelf != header) { this.BestSentHeader = header; } } else { this.BestSentHeader = header; } bestSentHeader = this.BestSentHeader; } this.logger.LogTrace("(-):{0}='{1}'", nameof(this.BestSentHeader), bestSentHeader); }
/// <inheritdoc /> public override void Initialize(ChainedHeader chainTip) { base.Initialize(chainTip); var coinDatabase = ((CachedCoinView)this.UtxoSet).ICoindb; coinDatabase.Initialize(); HashHeightPair coinViewTip = coinDatabase.GetTipHash(); while (true) { ChainedHeader pendingTip = chainTip.FindAncestorOrSelf(coinViewTip.Hash); if (pendingTip != null) { break; } this.logger.LogInformation("Rewinding coin view from '{0}'.", coinViewTip); // If the block store was initialized behind the coin view's tip, rewind it to on or before it's tip. // The node will complete loading before connecting to peers so the chain will never know that a reorg happened. coinViewTip = coinDatabase.Rewind(); } this.logger.LogInformation("Coin view rewound to '{0}'.", coinDatabase.GetTipHash()); }
/// <inheritdoc /> public async Task <ChainedHeader> InitializeAsync(ChainedHeader highestHeader) { Guard.NotNull(highestHeader, nameof(highestHeader)); await this.provenBlockHeaderRepository.InitializeAsync().ConfigureAwait(false); var tip = highestHeader; var repoTip = this.provenBlockHeaderRepository.TipHashHeight; if (repoTip.Hash != tip.HashBlock) { // Repository is behind chain of headers. tip = tip.FindAncestorOrSelf(repoTip.Hash, repoTip.Height); if (tip == null) { // Start at one less of the current repo height as we have already checked // the repo tip. for (var height = repoTip.Height - 1; height > 0; height--) { var provenBlockHeader = await this.provenBlockHeaderRepository.GetAsync(height).ConfigureAwait(false); // Block header at current height not found, go to previous height. if (provenBlockHeader == null) { continue; } tip = highestHeader.FindAncestorOrSelf(provenBlockHeader.GetHash()); if (tip != null) { this.TipHashHeight = new HashHeightPair(provenBlockHeader.GetHash(), height); break; } } if (tip == null) { this.logger.LogDebug("[TIP_NOT_FOUND]:{0}", highestHeader); throw new ProvenBlockHeaderException($"{highestHeader} was not found in the store."); } } else { this.TipHashHeight = new HashHeightPair(tip.HashBlock, tip.Height); } } else { this.TipHashHeight = new HashHeightPair(tip.HashBlock, tip.Height); } this.logger.LogDebug("Proven block header store initialized at '{0}'.", this.TipHashHeight); return(tip); }
/// <inheritdoc /> public override void Initialize(ChainedHeader chainTip) { base.Initialize(chainTip); this.StakeChain.Load(); // A temporary hack until tip manage will be introduced. var coindb = ((CachedCoinView)this.UtxoSet).ICoindb; ChainedHeader coinDbTip = chainTip.FindAncestorOrSelf(coindb.GetTipHash().Hash); this.RewindDataIndexCache.Initialize(coinDbTip.Height, this.UtxoSet); }
/// <inheritdoc /> public override void Initialize(ChainedHeader chainTip) { base.Initialize(chainTip); this.StakeChain.Load(); // A temporary hack until tip manage will be introduced. var breezeCoinView = (DBreezeCoinView)((CachedCoinView)this.UtxoSet).Inner; uint256 hash = breezeCoinView.GetTipHash(); ChainedHeader tip = chainTip.FindAncestorOrSelf(hash); this.RewindDataIndexCache.Initialize(tip.Height, this.UtxoSet); }
/// <inheritdoc /> public override async Task InitializeAsync(ChainedHeader chainTip) { await base.InitializeAsync(chainTip).ConfigureAwait(false); await this.StakeChain.LoadAsync().ConfigureAwait(false); // A temporary hack until tip manage will be introduced. var breezeCoinView = (DBreezeCoinView)((CachedCoinView)this.UtxoSet).Inner; uint256 hash = await breezeCoinView.GetTipHashAsync().ConfigureAwait(false); ChainedHeader tip = chainTip.FindAncestorOrSelf(hash); await this.RewindDataIndexCache.InitializeAsync(tip.Height, this.UtxoSet).ConfigureAwait(false); }
/// <summary> /// Compacts the block and transaction database by recreating the tables without the deleted references. /// </summary> /// <param name="blockRepositoryTip">The last fully validated block of the node.</param> private void PrepareDatabaseForCompacting(ChainedHeader blockRepositoryTip) { int upperHeight = this.blockRepository.TipHashAndHeight.Height - this.storeSettings.AmountOfBlocksToKeep; var toDelete = new List <ChainedHeader>(); ChainedHeader startFromHeader = blockRepositoryTip.GetAncestor(upperHeight); ChainedHeader endAtHeader = blockRepositoryTip.FindAncestorOrSelf(this.PrunedTip.Hash); this.logger.LogInformation($"Pruning blocks from height {upperHeight} to {endAtHeader.Height}."); while (startFromHeader.Previous != null && startFromHeader != endAtHeader) { toDelete.Add(startFromHeader); startFromHeader = startFromHeader.Previous; } this.blockRepository.DeleteBlocks(toDelete.Select(cb => cb.HashBlock).ToList()); this.UpdatePrunedTip(blockRepositoryTip.GetAncestor(upperHeight)); }
/// <inheritdoc /> public override async Task InitializeAsync(ChainedHeader chainTip) { var breezeCoinView = (DBreezeCoinView)((CachedCoinView)this.UtxoSet).Inner; await breezeCoinView.InitializeAsync().ConfigureAwait(false); uint256 consensusTipHash = await breezeCoinView.GetTipHashAsync().ConfigureAwait(false); while (true) { ChainedHeader pendingTip = chainTip.FindAncestorOrSelf(consensusTipHash); if (pendingTip != null) { break; } // In case block store initialized behind, rewind until or before the block store tip. // The node will complete loading before connecting to peers so the chain will never know if a reorg happened. consensusTipHash = await breezeCoinView.RewindAsync().ConfigureAwait(false); } }
/// <inheritdoc /> public void Initialize(ChainedHeader highestHeader) { if (this.commonTipPersistingTask != null) { throw new Exception("Already initialized."); } var commonTipHashHeight = this.keyValueRepo.LoadValue <HashHeightPair>(CommonTipKey); if (commonTipHashHeight != null) { this.lastCommonTip = highestHeader.FindAncestorOrSelf(commonTipHashHeight.Hash, commonTipHashHeight.Height); } else { // Genesis. this.lastCommonTip = highestHeader.GetAncestor(0); } this.logger.LogDebug("Tips manager initialized at '{0}'.", this.lastCommonTip); this.commonTipPersistingTask = this.PersistCommonTipContinuouslyAsync(); }
/// <inheritdoc /> public async Task InitializeAsync(IConsensus consensusParameters, ChainedHeader tip, ICoinView coinView) { // A temporary hack until tip manage will be introduced. var breezeCoinView = (DBreezeCoinView)((CachedCoinView)coinView).Inner; uint256 hash = await breezeCoinView.GetTipHashAsync().ConfigureAwait(false); tip = tip.FindAncestorOrSelf(hash); this.numberOfBlocksToKeep = (int)consensusParameters.MaxReorgLength; int heightToSyncTo = tip.Height > this.numberOfBlocksToKeep ? tip.Height - this.numberOfBlocksToKeep : 0; for (int rewindHeight = tip.Height - 1; rewindHeight >= heightToSyncTo; rewindHeight--) { RewindData rewindData = await coinView.GetRewindData(rewindHeight).ConfigureAwait(false); if (rewindData == null) { throw new ConsensusException($"Rewind data of height '{rewindHeight}' was not found!"); } if (rewindData.OutputsToRestore == null || rewindData.OutputsToRestore.Count == 0) { continue; } foreach (UnspentOutputs unspent in rewindData.OutputsToRestore) { for (int outputIndex = 0; outputIndex < unspent.Outputs.Length; outputIndex++) { string key = $"{unspent.TransactionId}-{outputIndex}"; this.items[key] = rewindHeight; } } } }
/// <summary> /// Processes "getblocks" message received from the peer. /// </summary> /// <param name="peer">Peer that sent the message.</param> /// <param name="getBlocksPayload">Payload of "getblocks" message to process.</param> private async Task ProcessGetBlocksAsync(INetworkPeer peer, GetBlocksPayload getBlocksPayload) { this.logger.LogTrace("({0}:'{1}',{2}:'{3}')", nameof(peer), peer.RemoteSocketEndpoint, nameof(getBlocksPayload), getBlocksPayload); // We only want to work with blocks that are in the store, // so we first get information about the store's tip. ChainedHeader blockStoreTip = this.chain.GetBlock(this.blockRepository.BlockHash); if (blockStoreTip == null) { this.logger.LogTrace("(-)[REORG]"); return; } this.logger.LogTrace("Block store tip is '{0}'.", blockStoreTip); // Now we want to find the last common block between our chain and the block locator the peer sent us. ChainedHeader chainTip = this.chain.Tip; ChainedHeader forkPoint = null; // Find last common block between our chain and the block locator the peer sent us. while (forkPoint == null) { forkPoint = this.chain.FindFork(getBlocksPayload.BlockLocators.Blocks); if (forkPoint == null) { this.logger.LogTrace("(-)[NO_FORK_POINT]"); return; } // In case of reorg, we just try again, eventually we succeed. if (chainTip.FindAncestorOrSelf(forkPoint) == null) { chainTip = this.chain.Tip; forkPoint = null; } } this.logger.LogTrace("Fork point is '{0}'.", forkPoint); // If block store is lower than the fork point, or it is on different chain, we don't have anything to contribute to this peer at this point. if (blockStoreTip.FindAncestorOrSelf(forkPoint) == null) { this.logger.LogTrace("(-)[FORK_OUTSIDE_STORE]"); return; } // Now we compile a list of blocks we want to send to the peer as inventory vectors. // This is needed as we want to traverse the chain in forward direction. int maxHeight = Math.Min(blockStoreTip.Height, forkPoint.Height + InvPayload.MaxGetBlocksInventorySize); ChainedHeader lastBlock = blockStoreTip.GetAncestor(maxHeight); int headersCount = maxHeight - forkPoint.Height; this.logger.LogTrace("Last block to announce is '{0}', number of blocks to announce is {1}.", lastBlock, headersCount); var headersToAnnounce = new ChainedHeader[headersCount]; for (int i = headersCount - 1; i >= 0; i--) { headersToAnnounce[i] = lastBlock; lastBlock = lastBlock.Previous; } // Now we compile inventory payload and we also consider hash stop given by the peer. bool sendContinuation = true; ChainedHeader lastAddedChainedHeader = null; var inv = new InvPayload(); for (int i = 0; i < headersToAnnounce.Length; i++) { ChainedHeader chainedHeader = headersToAnnounce[i]; if (chainedHeader.HashBlock == getBlocksPayload.HashStop) { this.logger.LogTrace("Hash stop has been reached."); break; } this.logger.LogTrace("Adding block '{0}' to the inventory.", chainedHeader); lastAddedChainedHeader = chainedHeader; inv.Inventory.Add(new InventoryVector(InventoryType.MSG_BLOCK, chainedHeader.HashBlock)); if (chainedHeader.HashBlock == chainTip.HashBlock) { this.logger.LogTrace("Tip of the chain has been reached."); sendContinuation = false; } } int count = inv.Inventory.Count; if (count > 0) { var chainBehavior = peer.Behavior <ChainHeadersBehavior>(); ChainedHeader peerTip = chainBehavior.PendingTip; int peersHeight = peerTip != null ? peerTip.Height : 0; if (peersHeight < lastAddedChainedHeader.Height) { this.logger.LogTrace("Setting peer's pending tip to '{0}'.", lastAddedChainedHeader); chainBehavior.SetPendingTip(lastAddedChainedHeader); // Set last item of the batch (unless we are announcing the tip), which is then used // when the peer sends us "getdata" message. When we detect "getdata" message for this block, // we will send continuation inventory message. This will cause the peer to ask for another batch of blocks. // See ProcessGetDataAsync method. if (sendContinuation) { this.getBlocksBatchLastItemHash = lastAddedChainedHeader.HashBlock; } } this.logger.LogTrace("Sending inventory with {0} block hashes.", count); await peer.SendMessageAsync(inv).ConfigureAwait(false); } else { this.logger.LogTrace("Nothing to send."); } this.logger.LogTrace("(-)"); }
/// <inheritdoc /> public async Task AnnounceBlocksAsync(List <ChainedHeader> blocksToAnnounce) { Guard.NotNull(blocksToAnnounce, nameof(blocksToAnnounce)); if (!blocksToAnnounce.Any()) { this.logger.LogTrace("(-)[NO_BLOCKS]"); return; } INetworkPeer peer = this.AttachedPeer; if (peer == null) { this.logger.LogTrace("(-)[NO_PEER]"); return; } bool revertToInv = ((!this.PreferHeaders && (!this.preferHeaderAndIDs || blocksToAnnounce.Count > 1)) || blocksToAnnounce.Count > MaxBlocksToAnnounce); this.logger.LogTrace("Block propagation preferences of the peer '{0}': prefer headers - {1}, prefer headers and IDs - {2}, will{3} revert to 'inv' now.", peer.RemoteSocketEndpoint, this.PreferHeaders, this.preferHeaderAndIDs, revertToInv ? "" : " NOT"); var headers = new List <BlockHeader>(); var inventoryBlockToSend = new List <ChainedHeader>(); try { ChainedHeader bestSentHeader = this.consensusManagerBehavior.BestSentHeader; ChainedHeader bestIndex = null; if (!revertToInv) { bool foundStartingHeader = false; // In case we don't have any information about peer's tip send him only last header and don't update best sent header. // We expect peer to answer with getheaders message. if (bestSentHeader == null) { await peer.SendMessageAsync(this.BuildHeadersAnnouncePayload(new[] { blocksToAnnounce.Last().Header })).ConfigureAwait(false); this.logger.LogTrace("(-)[SENT_SINGLE_HEADER]"); return; } // Try to find first chained block that the peer doesn't have, and then add all chained blocks past that one. foreach (ChainedHeader chainedHeader in blocksToAnnounce) { bestIndex = chainedHeader; if (!foundStartingHeader) { this.logger.LogTrace("Checking is the peer '{0}' can connect header '{1}'.", peer.RemoteSocketEndpoint, chainedHeader); // Peer doesn't have a block at the height of our block and with the same hash? if (bestSentHeader?.FindAncestorOrSelf(chainedHeader) != null) { this.logger.LogTrace("Peer '{0}' already has header '{1}'.", peer.RemoteSocketEndpoint, chainedHeader.Previous); continue; } // Peer doesn't have a block at the height of our block.Previous and with the same hash? if (bestSentHeader?.FindAncestorOrSelf(chainedHeader.Previous) == null) { // Peer doesn't have this header or the prior one - nothing will connect, so bail out. this.logger.LogTrace("Neither the header nor its previous header found for peer '{0}', reverting to 'inv'.", peer.RemoteSocketEndpoint); revertToInv = true; break; } this.logger.LogTrace("Peer '{0}' can connect header '{1}'.", peer.RemoteSocketEndpoint, chainedHeader.Previous); foundStartingHeader = true; } // If we reached here then it means that we've found starting header. headers.Add(chainedHeader.Header); } } if (!revertToInv && headers.Any()) { if ((headers.Count == 1) && this.preferHeaderAndIDs) { // TODO: } else if (this.PreferHeaders) { if (headers.Count > 1) { this.logger.LogDebug("Sending {0} headers, range {1} - {2}, to peer '{3}'.", headers.Count, headers.First(), headers.Last(), peer.RemoteSocketEndpoint); } else { this.logger.LogDebug("Sending header '{0}' to peer '{1}'.", headers.First(), peer.RemoteSocketEndpoint); } this.lastSentHeader = bestIndex; this.consensusManagerBehavior.UpdateBestSentHeader(this.lastSentHeader); await peer.SendMessageAsync(this.BuildHeadersAnnouncePayload(headers)).ConfigureAwait(false); this.logger.LogTrace("(-)[SEND_HEADERS_PAYLOAD]"); return; } else { revertToInv = true; } } if (revertToInv) { // If falling back to using an inv, just try to inv the tip. // The last entry in 'blocksToAnnounce' was our tip at some point in the past. if (blocksToAnnounce.Any()) { ChainedHeader chainedHeader = blocksToAnnounce.Last(); if (chainedHeader != null) { if ((bestSentHeader == null) || (bestSentHeader.GetAncestor(chainedHeader.Height) == null)) { inventoryBlockToSend.Add(chainedHeader); this.logger.LogDebug("Sending inventory hash '{0}' to peer '{1}'.", chainedHeader.HashBlock, peer.RemoteSocketEndpoint); } } } } if (inventoryBlockToSend.Any()) { this.lastSentHeader = inventoryBlockToSend.Last(); this.consensusManagerBehavior.UpdateBestSentHeader(this.lastSentHeader); await this.SendAsBlockInventoryAsync(peer, inventoryBlockToSend).ConfigureAwait(false); this.logger.LogTrace("(-)[SEND_INVENTORY]"); return; } } catch (OperationCanceledException) { this.logger.LogTrace("(-)[CANCELED_EXCEPTION]"); return; } }
/// <inheritdoc /> public void ProcessBlock(Block block) { Guard.NotNull(block, nameof(block)); this.logger.LogTrace("({0}:'{1}')", nameof(block), block.GetHash()); ChainedHeader newTip = this.chain.GetBlock(block.GetHash()); if (newTip == null) { this.logger.LogTrace("(-)[NEW_TIP_REORG]"); return; } // If the new block's previous hash is the same as the // wallet hash then just pass the block to the manager. if (block.Header.HashPrevBlock != this.walletTip.HashBlock) { // If previous block does not match there might have // been a reorg, check if the wallet is still on the main chain. ChainedHeader inBestChain = this.chain.GetBlock(this.walletTip.HashBlock); if (inBestChain == null) { // The current wallet hash was not found on the main chain. // A reorg happened so bring the wallet back top the last known fork. ChainedHeader fork = this.walletTip; // We walk back the chained block object to find the fork. while (this.chain.GetBlock(fork.HashBlock) == null) { fork = fork.Previous; } this.logger.LogInformation("Reorg detected, going back from '{0}' to '{1}'.", this.walletTip, fork); this.walletManager.RemoveBlocks(fork); this.walletTip = fork; this.logger.LogTrace("Wallet tip set to '{0}'.", this.walletTip); } // The new tip can be ahead or behind the wallet. // If the new tip is ahead we try to bring the wallet up to the new tip. // If the new tip is behind we just check the wallet and the tip are in the same chain. if (newTip.Height > this.walletTip.Height) { ChainedHeader findTip = newTip.FindAncestorOrSelf(this.walletTip); if (findTip == null) { this.logger.LogTrace("(-)[NEW_TIP_AHEAD_NOT_IN_WALLET]"); return; } this.logger.LogTrace("Wallet tip '{0}' is behind the new tip '{1}'.", this.walletTip, newTip); // The wallet is falling behind we need to catch up. this.logger.LogWarning("New tip '{0}' is too far in advance, put the puller back.", newTip); this.blockNotification.SyncFrom(this.walletTip.HashBlock); return; } else { ChainedHeader findTip = this.walletTip.FindAncestorOrSelf(newTip); if (findTip == null) { this.logger.LogTrace("(-)[NEW_TIP_BEHIND_NOT_IN_WALLET]"); return; } this.logger.LogTrace("Wallet tip '{0}' is ahead or equal to the new tip '{1}'.", this.walletTip, newTip.HashBlock); } } else { this.logger.LogTrace("New block follows the previously known block '{0}'.", this.walletTip); } this.walletTip = newTip; this.walletManager.ProcessBlock(block, newTip); this.logger.LogTrace("(-)"); }
/// <inheritdoc /> public void ProcessBlock(Block block) { Guard.NotNull(block, nameof(block)); ChainedHeader newTip = this.chainIndexer.GetHeader(block.GetHash()); if (newTip == null) { this.logger.LogTrace("(-)[NEW_TIP_REORG]"); return; } // If the new block's previous hash is the same as the // wallet hash then just pass the block to the manager. if (block.Header.HashPrevBlock != this.walletTip.HashBlock) { // If previous block does not match there might have // been a reorg, check if the wallet is still on the main chain. ChainedHeader inBestChain = this.chainIndexer.GetHeader(this.walletTip.HashBlock); if (inBestChain == null) { // The current wallet hash was not found on the main chain. // A reorg happened so bring the wallet back top the last known fork. ChainedHeader fork = this.walletTip; // We walk back the chained block object to find the fork. while (this.chainIndexer.GetHeader(fork.HashBlock) == null) { fork = fork.Previous; } this.logger.LogInformation("Reorg detected, going back from '{0}' to '{1}'.", this.walletTip, fork); this.walletManager.RemoveBlocks(fork); this.walletTip = fork; this.logger.LogDebug("Wallet tip set to '{0}'.", this.walletTip); } // The new tip can be ahead or behind the wallet. // If the new tip is ahead we try to bring the wallet up to the new tip. // If the new tip is behind we just check the wallet and the tip are in the same chain. if (newTip.Height > this.walletTip.Height) { ChainedHeader findTip = newTip.FindAncestorOrSelf(this.walletTip); if (findTip == null) { this.logger.LogTrace("(-)[NEW_TIP_AHEAD_NOT_IN_WALLET]"); return; } this.logger.LogDebug("Wallet tip '{0}' is behind the new tip '{1}'.", this.walletTip, newTip); // The wallet is falling behind we need to catch up. this.logger.LogWarning("New tip '{0}' is too far in advance.", newTip); CancellationToken token = this.nodeLifetime.ApplicationStopping; ChainedHeader next = this.walletTip; while (next != newTip) { // While the wallet is catching up the entire node will wait. // If a wallet is recovered to a date in the past. Consensus will stop until the wallet is up to date. // TODO: This code should be replaced with a different approach // Similar to BlockStore the wallet should be standalone and not depend on consensus. // The block should be put in a queue and pushed to the wallet in an async way. // If the wallet is behind it will just read blocks from store (or download in case of a pruned node). token.ThrowIfCancellationRequested(); next = newTip.GetAncestor(next.Height + 1); ChainedHeaderBlock nextblock = null; int index = 0; while (true) { token.ThrowIfCancellationRequested(); nextblock = this.consensusManager.GetBlockData(next.HashBlock); if (nextblock != null && nextblock.Block != null) { break; } // The idea in this abandoning of the loop is to release consensus to push the block. // That will make the block available in the next push from consensus. index++; if (index > 10) { this.logger.LogTrace("(-)[WALLET_CATCHUP_INDEX_MAX]"); return; } // Really ugly hack to let store catch up. // This will block the entire consensus pulling. this.logger.LogWarning("Wallet is behind the best chain and the next block is not found in store."); Thread.Sleep(100); continue; } this.walletTip = next; this.walletManager.ProcessBlock(nextblock.Block, next); } return; } else { ChainedHeader findTip = this.walletTip.FindAncestorOrSelf(newTip); if (findTip == null) { this.logger.LogTrace("(-)[NEW_TIP_BEHIND_NOT_IN_WALLET]"); return; } this.logger.LogDebug("Wallet tip '{0}' is ahead or equal to the new tip '{1}'.", this.walletTip, newTip.HashBlock); } } else { this.logger.LogDebug("New block follows the previously known block '{0}'.", this.walletTip); } this.walletTip = newTip; this.walletManager.ProcessBlock(block, newTip); }
/// <summary> /// Computes the metrics of all BIP9 deployments for a given block. /// </summary> /// <param name="indexPrev">The block at which to compute the metrics.</param> /// <param name="thresholdStates">The current state of each BIP9 deployment.</param> /// <returns>A <see cref="ThresholdStateModel" /> object containg the metrics.</returns> public List <ThresholdStateModel> GetThresholdStateMetrics(ChainedHeader indexPrev, ThresholdState[] thresholdStates) { var thresholdStateModels = new List <ThresholdStateModel>(); var array = new ThresholdState[this.consensus.BIP9Deployments.Length]; for (var deploymentIndex = 0; deploymentIndex < array.Length; deploymentIndex++) { if (this.consensus.BIP9Deployments[deploymentIndex] == null) { continue; } var deploymentName = this.consensus.BIP9Deployments[deploymentIndex]?.Name; var timeStart = this.consensus.BIP9Deployments[deploymentIndex]?.StartTime.Date; var timeTimeout = this.consensus.BIP9Deployments[deploymentIndex]?.Timeout.Date; var threshold = this.consensus.BIP9Deployments[deploymentIndex].Threshold; var votes = 0; var currentHeight = indexPrev.Height + 1; var period = this.consensus.MinerConfirmationWindow; // First ancestor outside last confirmation window. If we haven't reached block height 2016 yet this will be the genesis block. var periodStart = indexPrev.Height - currentHeight % period > 0 ? indexPrev.Height - currentHeight % period : 0; var periodStartsHeader = indexPrev.GetAncestor(periodStart); var periodEndsHeight = periodStartsHeader.Height + period; var hexVersions = new Dictionary <string, int>(); var totalBlocks = 0; var headerTemp = indexPrev; while (headerTemp != periodStartsHeader) { if (Condition(headerTemp, deploymentIndex)) { votes++; } totalBlocks++; var hexVersion = headerTemp.Header.Version.ToString("X8"); if (!hexVersions.TryGetValue(hexVersion, out var count)) { count = 0; } hexVersions[hexVersion] = count + 1; headerTemp = headerTemp.Previous; } // look in the cache for the hash of the first block an item was deployed var firstSeenHash = this.cache.FirstOrDefault(c => c.Value[deploymentIndex] == ThresholdState.Started); var sinceHeight = 0; if (firstSeenHash.Key != null) { sinceHeight = indexPrev.FindAncestorOrSelf(firstSeenHash.Key).Height; } thresholdStateModels.Add(new ThresholdStateModel { DeploymentName = deploymentName, DeploymentIndex = deploymentIndex, ConfirmationPeriod = period, Blocks = totalBlocks, Votes = votes, HexVersions = hexVersions, TimeStart = timeStart, TimeTimeOut = timeTimeout, Threshold = threshold, Height = currentHeight, SinceHeight = sinceHeight, PeriodStartHeight = periodStartsHeader.Height, PeriodEndHeight = periodEndsHeight, StateValue = thresholdStates[deploymentIndex], ThresholdState = thresholdStates[deploymentIndex].ToString() }); } return(thresholdStateModels); }
/// <summary>Called when a <see cref="Block"/> is added to the <see cref="blocksQueue"/>. /// Depending on the <see cref="WalletTip"/> and incoming block height, this method will decide whether the <see cref="Block"/> will be processed by the <see cref="WalletManager"/>. /// </summary> /// <param name="block">Block to be processed.</param> /// <param name="cancellationToken">The cancellation token.</param> private async Task OnProcessBlockAsync(Block block, CancellationToken cancellationToken) { Guard.NotNull(block, nameof(block)); long currentBlockQueueSize = Interlocked.Add(ref this.blocksQueueSize, -block.BlockSize.Value); this.logger.LogTrace("Queue sized changed to {0} bytes.", currentBlockQueueSize); ChainedHeader newTip = this.chain.GetBlock(block.GetHash()); if (newTip == null) { this.logger.LogTrace("(-)[NEW_TIP_REORG]"); return; } // If the new block's previous hash is not the same as the one we have, there might have been a reorg. // If the new block follows the previous one, just pass the block to the manager. if (block.Header.HashPrevBlock != this.walletTip.HashBlock) { // If previous block does not match there might have // been a reorg, check if the wallet is still on the main chain. ChainedHeader inBestChain = this.chain.GetBlock(this.walletTip.HashBlock); if (inBestChain == null) { // The current wallet hash was not found on the main chain. // A reorg happened so bring the wallet back top the last known fork. ChainedHeader fork = this.walletTip; // We walk back the chained block object to find the fork. while (this.chain.GetBlock(fork.HashBlock) == null) { fork = fork.Previous; } this.logger.LogInformation("Reorg detected, going back from '{0}' to '{1}'.", this.walletTip, fork); this.walletManager.RemoveBlocks(fork); this.walletTip = fork; this.logger.LogTrace("Wallet tip set to '{0}'.", this.walletTip); } // The new tip can be ahead or behind the wallet. // If the new tip is ahead we try to bring the wallet up to the new tip. // If the new tip is behind we just check the wallet and the tip are in the same chain. if (newTip.Height > this.walletTip.Height) { ChainedHeader findTip = newTip.FindAncestorOrSelf(this.walletTip); if (findTip == null) { this.logger.LogTrace("(-)[NEW_TIP_AHEAD_NOT_IN_WALLET]"); return; } this.logger.LogTrace("Wallet tip '{0}' is behind the new tip '{1}'.", this.walletTip, newTip); ChainedHeader next = this.walletTip; while (next != newTip) { // While the wallet is catching up the entire node will wait. // If a wallet is recovered to a date in the past. Consensus will stop until the wallet is up to date. // TODO: This code should be replaced with a different approach // Similar to BlockStore the wallet should be standalone and not depend on consensus. // The block should be put in a queue and pushed to the wallet in an async way. // If the wallet is behind it will just read blocks from store (or download in case of a pruned node). next = newTip.GetAncestor(next.Height + 1); Block nextblock = null; int index = 0; while (true) { if (cancellationToken.IsCancellationRequested) { this.logger.LogTrace("(-)[CANCELLATION_REQUESTED]"); return; } nextblock = await this.blockStore.GetBlockAsync(next.HashBlock).ConfigureAwait(false); if (nextblock == null) { // The idea in this abandoning of the loop is to release consensus to push the block. // That will make the block available in the next push from consensus. index++; if (index > 10) { this.logger.LogTrace("(-)[WALLET_CATCHUP_INDEX_MAX]"); return; } // Really ugly hack to let store catch up. // This will block the entire consensus pulling. this.logger.LogWarning("Wallet is behind the best chain and the next block is not found in store."); Thread.Sleep(100); continue; } break; } this.walletTip = next; this.walletManager.ProcessBlock(nextblock, next); } } else { ChainedHeader findTip = this.walletTip.FindAncestorOrSelf(newTip); if (findTip == null) { this.logger.LogTrace("(-)[NEW_TIP_BEHIND_NOT_IN_WALLET]"); return; } this.logger.LogTrace("Wallet tip '{0}' is ahead or equal to the new tip '{1}'.", this.walletTip, newTip); } } else { this.logger.LogTrace("New block follows the previously known block '{0}'.", this.walletTip); } this.walletTip = newTip; this.walletManager.ProcessBlock(block, newTip); }
/// <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> /// <para> /// If we receive a valid header from peer which work is higher than the work /// of our best chain's tip, we update our view of the best chain to that tip. /// </para> /// </remarks> private async Task ProcessHeadersAsync(INetworkPeer peer, HeadersPayload headersPayload) { this.logger.LogTrace("({0}:'{1}',{2}:'{3}')", nameof(peer), peer.RemoteSocketEndpoint, nameof(headersPayload), headersPayload); if (!this.CanSync) { this.logger.LogTrace("(-)[CANT_SYNC]"); return; } if (headersPayload.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; } ChainedHeader pendingTipBefore = this.pendingTip; this.logger.LogTrace("Pending tip is '{0}', received {1} new headers.", pendingTipBefore, headersPayload.Headers.Count); bool doTrySync = false; // TODO: implement MAX_HEADERS_RESULTS in NBitcoin.HeadersPayload ChainedHeader tip = pendingTipBefore; foreach (BlockHeader header in headersPayload.Headers) { ChainedHeader prev = tip?.FindAncestorOrSelf(header.HashPrevBlock); if (prev == null) { this.logger.LogTrace("Previous header of the new header '{0}' was not found on the peer's chain, the view of the peer's chain is probably outdated.", header); // We have received a header from the peer for which we don't register a previous header. // This can happen if our information about where the peer is is invalid. // However, if the previous header is on the chain that we recognize, // we can fix it. // Try to find the header's previous hash on our best chain. prev = this.Chain.GetBlock(header.HashPrevBlock); if (prev == null) { this.logger.LogTrace("Previous header of the new header '{0}' was not found on our chain either.", header); // If we can't connect the header we received from the peer, we might be on completely different chain or // a reorg happened recently. If we ignored it, we would have invalid view of the peer and the propagation // of blocks would not work well. So we ask the peer for headers using "getheaders" message. // Enforce a sync. doTrySync = true; break; } // Now we know the previous block header and thus we can connect the new header. } tip = new ChainedHeader(header, header.GetHash(), prev); bool validated = this.Chain.GetBlock(tip.HashBlock) != null || tip.Validate(peer.Network); validated &= !this.chainState.IsMarkedInvalid(tip.HashBlock); if (!validated) { this.logger.LogTrace("Validation of new header '{0}' failed.", tip); this.InvalidHeaderReceived = true; break; } this.pendingTip = tip; } if (pendingTipBefore != this.pendingTip) { this.logger.LogTrace("Pending tip changed to '{0}'.", this.pendingTip); } if ((this.pendingTip != null) && !this.bestChainSelector.TrySetAvailableTip(this.AttachedPeer.Connection.Id, this.pendingTip)) { this.InvalidHeaderReceived = true; } ChainedHeader chainedPendingTip = this.pendingTip == null ? null : this.Chain.GetBlock(this.pendingTip.HashBlock); if (chainedPendingTip != null) { // This allows garbage collection to collect the duplicated pendingTip and ancestors. this.pendingTip = chainedPendingTip; } // If we made any advancement or the sync is enforced by 'doTrySync'- continue syncing. if (doTrySync || (this.pendingTip == null) || (pendingTipBefore == null) || (pendingTipBefore.HashBlock != this.pendingTip.HashBlock)) { await this.TrySyncAsync().ConfigureAwait(false); } this.logger.LogTrace("(-)"); }
/// <summary> /// Computes the metrics of all BIP9 deployments for a given block. /// </summary> /// <param name="indexPrev">The block at which to compute the metrics.</param> /// <param name="thresholdStates">The current state of each BIP9 deployment.</param> /// <returns>A <see cref="ThresholdStateModel" /> object containg the metrics.</returns> public List <ThresholdStateModel> GetThresholdStateMetrics(ChainedHeader indexPrev, ThresholdState[] thresholdStates) { var thresholdStateModels = new List <ThresholdStateModel>(); ThresholdState[] array = new ThresholdState[this.consensus.BIP9Deployments.Length]; for (int deploymentIndex = 0; deploymentIndex < array.Length; deploymentIndex++) { if (this.consensus.BIP9Deployments[deploymentIndex] == null) { continue; } string deploymentName = this.consensus.BIP9Deployments[deploymentIndex]?.Name; DateTime?timeStart = this.consensus.BIP9Deployments[deploymentIndex]?.StartTime.Date; DateTime?timeTimeout = this.consensus.BIP9Deployments[deploymentIndex]?.Timeout.Date; int threshold = this.consensus.RuleChangeActivationThreshold; int votes = 0; int currentHeight = indexPrev.Height + 1; int period = this.consensus.MinerConfirmationWindow; // First ancestor outside last confirmation window. ChainedHeader periodStartsHeader = indexPrev.GetAncestor(indexPrev.Height - (currentHeight % period)); int periodEndsHeight = periodStartsHeader.Height + period; var hexVersions = new Dictionary <string, int>(); int totalBlocks = 0; ChainedHeader headerTemp = indexPrev; while (headerTemp != periodStartsHeader) { if (this.Condition(headerTemp, deploymentIndex)) { votes++; } totalBlocks++; string hexVersion = headerTemp.Header.Version.ToString("X8"); if (!hexVersions.TryGetValue(hexVersion, out int count)) { count = 0; } hexVersions[hexVersion] = count + 1; headerTemp = headerTemp.Previous; } // look in the cache for the hash of the first block an item was deployed var firstSeenHash = this.cache.FirstOrDefault(c => c.Value[deploymentIndex] == ThresholdState.Started); int sinceHeight = 0; if (firstSeenHash.Key != null) { sinceHeight = indexPrev.FindAncestorOrSelf(firstSeenHash.Key).Height; } thresholdStateModels.Add(new ThresholdStateModel() { DeploymentName = deploymentName, DeploymentIndex = deploymentIndex, ConfirmationPeriod = period, Blocks = totalBlocks, Votes = votes, HexVersions = hexVersions, TimeStart = timeStart, TimeTimeOut = timeTimeout, Threshold = threshold, Height = currentHeight, SinceHeight = sinceHeight, PeriodStartHeight = periodStartsHeader.Height, PeriodEndHeight = periodEndsHeight, StateValue = thresholdStates[deploymentIndex], ThresholdState = ((ThresholdState)thresholdStates[deploymentIndex]).ToString() }); } return(thresholdStateModels); }