internal static object NBitcoinDeserialize(byte[] bytes, Type type) { if (type == typeof(Coins)) { Coins coin = new Coins(); coin.ReadWrite(bytes); return(coin); } if (type == typeof(BlockHeader)) { BlockHeader header = new BlockHeader(); header.ReadWrite(bytes); return(header); } if (type == typeof(RewindData)) { RewindData rewind = new RewindData(); rewind.ReadWrite(bytes); return(rewind); } if (type == typeof(uint256)) { return(new uint256(bytes)); } if (type == typeof(Block)) { return(new Block(bytes)); } if (type == typeof(BlockStake)) { return(new BlockStake(bytes)); } throw new NotSupportedException(); }
/// <inheritdoc /> public void Initialize(int tipHeight, ICoinView coinView) { this.items.Clear(); if (this.lastCheckpoint > tipHeight) { return; } HashHeightPair finalBlock = this.finalizedBlockInfoRepository.GetFinalizedBlockInfo(); this.numberOfBlocksToKeep = (int)this.network.Consensus.MaxReorgLength; int heightToSyncTo = tipHeight > this.numberOfBlocksToKeep ? tipHeight - this.numberOfBlocksToKeep : 1; if (tipHeight > finalBlock.Height) { if (heightToSyncTo < finalBlock.Height) { heightToSyncTo = finalBlock.Height; } if (heightToSyncTo < this.lastCheckpoint) { heightToSyncTo = this.lastCheckpoint; } } for (int rewindHeight = tipHeight; rewindHeight >= heightToSyncTo; rewindHeight--) { RewindData rewindData = coinView.GetRewindData(rewindHeight); this.AddRewindData(rewindHeight, rewindData); } }
public void NBitcoinDeserializeWithRewindDataDeserializesObject() { var network = Network.RegTest; var genesis = network.GetGenesis(); var rewindData = new RewindData(genesis.GetHash()); var result = (RewindData)DBreezeSingleThreadSession.NBitcoinDeserialize(rewindData.ToBytes(), typeof(RewindData)); Assert.Equal(genesis.GetHash(), result.PreviousBlockHash); }
/// <inheritdoc /> public void Remove(int tipHeight, ICoinView coinView) { this.Flush(tipHeight); int bottomHeight = tipHeight > this.numberOfBlocksToKeep ? tipHeight - this.numberOfBlocksToKeep : 1; RewindData rewindData = coinView.GetRewindData(bottomHeight); this.AddRewindData(bottomHeight, rewindData); }
public void DeserializerWithRewindDataDeserializesObject() { Network network = KnownNetworks.StraxRegTest; Block genesis = network.GetGenesis(); var rewindData = new RewindData(new HashHeightPair(genesis.GetHash(), 0)); var result = (RewindData)this.dbreezeSerializer.Deserialize(rewindData.ToBytes(), typeof(RewindData)); Assert.Equal(genesis.GetHash(), result.PreviousBlockHash.Hash); }
/// <inheritdoc /> public async Task Remove(int tipHeight, ICoinView coinView) { this.Flush(tipHeight); int bottomHeight = tipHeight > this.numberOfBlocksToKeep ? tipHeight - this.numberOfBlocksToKeep : 1; RewindData rewindData = await coinView.GetRewindData(bottomHeight).ConfigureAwait(false); this.AddRewindData(bottomHeight, rewindData); }
public void DeserializerWithRewindDataDeserializesObject() { Network network = Network.StratisRegTest; Block genesis = network.GetGenesis(); var rewindData = new RewindData(genesis.GetHash()); var result = (RewindData)this.dbreezeSerializer.Deserializer(rewindData.ToBytes(), typeof(RewindData)); Assert.Equal(genesis.GetHash(), result.PreviousBlockHash); }
/// <inheritdoc /> public void Initialize(int tipHeight, ICoinView coinView) { this.items.Clear(); this.numberOfBlocksToKeep = (int)this.network.Consensus.MaxReorgLength; int heightToSyncTo = tipHeight > this.numberOfBlocksToKeep ? tipHeight - this.numberOfBlocksToKeep : 1; for (int rewindHeight = tipHeight; rewindHeight >= heightToSyncTo; rewindHeight--) { RewindData rewindData = coinView.GetRewindData(rewindHeight); this.AddRewindData(rewindHeight, rewindData); } }
/// <inheritdoc /> public async Task InitializeAsync(int tipHeight, ICoinView coinView) { this.items.Clear(); this.numberOfBlocksToKeep = (int)this.network.Consensus.MaxReorgLength; int heightToSyncTo = tipHeight > this.numberOfBlocksToKeep ? tipHeight - this.numberOfBlocksToKeep : 1; for (int rewindHeight = tipHeight; rewindHeight >= heightToSyncTo; rewindHeight--) { RewindData rewindData = await coinView.GetRewindData(rewindHeight).ConfigureAwait(false); this.AddRewindData(rewindHeight, rewindData); } }
/// <inheritdoc /> public void Remove(int tipHeight, ICoinView coinView) { if (this.lastCheckpoint > tipHeight) { return; } this.SaveAndEvict(tipHeight, null); int bottomHeight = tipHeight > this.numberOfBlocksToKeep ? tipHeight - this.numberOfBlocksToKeep : 1; RewindData rewindData = coinView.GetRewindData(bottomHeight); this.AddRewindData(bottomHeight, rewindData); }
/// <summary> /// Adding rewind information for a block in to the cache, we only add the unspent outputs. /// The cache key is [trxid-outputIndex] and the value is the height of the block on with the rewind data information is kept. /// </summary> /// <param name="rewindHeight">Height of the rewind data.</param> /// <param name="rewindData">The data itself</param> private void AddRewindData(int rewindHeight, RewindData rewindData) { if (rewindData == null) { throw new ConsensusException($"Rewind data of height '{rewindHeight}' was not found!"); } if (rewindData.OutputsToRestore == null || rewindData.OutputsToRestore.Count == 0) { return; } foreach (RewindDataOutput unspent in rewindData.OutputsToRestore) { this.items[unspent.OutPoint] = rewindHeight; } }
/// <summary> /// Checks if coinstake is spent on another chain. /// </summary> /// <param name="header">The proven block header.</param> /// <param name="context">The POS rule context.</param> /// <returns>The <see cref="UnspentOutputs"> found in a RewindData</returns> private UnspentOutputs CheckIfCoinstakeIsSpentOnAnotherChain(ProvenBlockHeader header, PosRuleContext context) { Transaction coinstake = header.Coinstake; TxIn input = coinstake.Inputs[0]; int?rewindDataIndex = this.PosParent.RewindDataIndexCache.Get(input.PrevOut.Hash, (int)input.PrevOut.N); if (!rewindDataIndex.HasValue) { this.Logger.LogTrace("(-)[NO_REWIND_DATA_INDEX_FOR_INPUT_PREVOUT]"); context.ValidationContext.InsufficientHeaderInformation = true; ConsensusErrors.ReadTxPrevFailedInsufficient.Throw(); } RewindData rewindData = this.PosParent.UtxoSet.GetRewindData(rewindDataIndex.Value); if (rewindData == null) { this.Logger.LogTrace("(-)[NO_REWIND_DATA_FOR_INDEX]"); this.Logger.LogError("Error - Rewind data should always be present"); throw new ConsensusException("Rewind data should always be present"); } UnspentOutputs matchingUnspentUtxo = null; foreach (UnspentOutputs unspent in rewindData.OutputsToRestore) { if (unspent.TransactionId == input.PrevOut.Hash) { if (input.PrevOut.N < unspent.Outputs.Length) { matchingUnspentUtxo = unspent; break; } } } if (matchingUnspentUtxo == null) { this.Logger.LogTrace("(-)[UTXO_NOT_FOUND_IN_REWIND_DATA]"); context.ValidationContext.InsufficientHeaderInformation = true; ConsensusErrors.UtxoNotFoundInRewindData.Throw(); } return(matchingUnspentUtxo); }
/// <summary> /// Adding rewind information for a block in to the cache, we only add the unspent outputs. /// The cache key is [trxid-outputIndex] and the value is the height of the block on with the rewind data information /// is kept. /// </summary> /// <param name="rewindHeight">Height of the rewind data.</param> /// <param name="rewindData">The data itself</param> void AddRewindData(int rewindHeight, RewindData rewindData) { if (rewindData == null) { throw new ConsensusException($"Rewind data of height '{rewindHeight}' was not found!"); } if (rewindData.OutputsToRestore == null || rewindData.OutputsToRestore.Count == 0) { return; } foreach (var unspent in rewindData.OutputsToRestore) { for (var outputIndex = 0; outputIndex < unspent.Outputs.Length; outputIndex++) { var key = new OutPoint(unspent.TransactionId, outputIndex); this.items[key] = rewindHeight; } } }
/// <summary> /// Adding rewind information for a block in to the cache, we only add the unspent outputs. /// The cache key is [trxid-outputIndex] and the value is the height of the block on with the rewind data information is kept. /// </summary> /// <param name="rewindHeight">Height of the rewind data.</param> /// <param name="rewindData">The data itself</param> private void AddRewindData(int rewindHeight, RewindData rewindData) { if (rewindData == null) { throw new ConsensusException($"Rewind data of height '{rewindHeight}' was not found!"); } if (rewindData.OutputsToRestore == null || rewindData.OutputsToRestore.Count == 0) { return; } foreach (UnspentOutputs unspent in rewindData.OutputsToRestore) { for (int outputIndex = 0; outputIndex < unspent.Outputs.Length; outputIndex++) { string key = $"{unspent.TransactionId}-{outputIndex}"; this.items[key] = rewindHeight; } } }
/// <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; } } } }
/// <inheritdoc /> public override Task SaveChangesAsync(IEnumerable <UnspentOutputs> unspentOutputs, IEnumerable <TxOut[]> originalOutputs, uint256 oldBlockHash, uint256 nextBlockHash) { this.logger.LogTrace("({0}.Count():{1},{2}.Count():{3},{4}:'{5}',{6}:'{7}')", nameof(unspentOutputs), unspentOutputs?.Count(), nameof(originalOutputs), originalOutputs?.Count(), nameof(oldBlockHash), oldBlockHash, nameof(nextBlockHash), nextBlockHash); RewindData rewindData = originalOutputs != null ? new RewindData(oldBlockHash) : null; int insertedEntities = 0; List <UnspentOutputs> all = unspentOutputs.ToList(); Dictionary <uint256, TxOut[]> unspentToOriginal = new Dictionary <uint256, TxOut[]>(all.Count); using (new StopwatchDisposable(o => this.PerformanceCounter.AddInsertTime(o))) { if (originalOutputs != null) { IEnumerator <TxOut[]> originalEnumerator = originalOutputs.GetEnumerator(); foreach (UnspentOutputs output in all) { originalEnumerator.MoveNext(); unspentToOriginal.Add(output.TransactionId, originalEnumerator.Current); } } } Task task = Task.Run(() => { this.logger.LogTrace("()"); using (DBreeze.Transactions.Transaction transaction = this.dbreeze.GetTransaction()) { transaction.ValuesLazyLoadingIsOn = false; transaction.SynchronizeTables("BlockHash", "Coins", "Rewind"); transaction.Technical_SetTable_OverwriteIsNotAllowed("Coins"); using (new StopwatchDisposable(o => this.PerformanceCounter.AddInsertTime(o))) { uint256 current = this.GetCurrentHash(transaction); if (current != oldBlockHash) { this.logger.LogTrace("(-)[BLOCKHASH_MISMATCH]"); throw new InvalidOperationException("Invalid oldBlockHash"); } this.SetBlockHash(transaction, nextBlockHash); all.Sort(UnspentOutputsComparer.Instance); foreach (UnspentOutputs coin in all) { this.logger.LogTrace("Outputs of transaction ID '{0}' are {1} and will be {2} to the database.", coin.TransactionId, coin.IsPrunable ? "PRUNABLE" : "NOT PRUNABLE", coin.IsPrunable ? "removed" : "inserted"); if (coin.IsPrunable) { transaction.RemoveKey("Coins", coin.TransactionId.ToBytes(false)); } else { transaction.Insert("Coins", coin.TransactionId.ToBytes(false), coin.ToCoins()); } if (originalOutputs != null) { TxOut[] original = null; unspentToOriginal.TryGetValue(coin.TransactionId, out original); if (original == null) { // This one haven't existed before, if we rewind, delete it. rewindData.TransactionsToRemove.Add(coin.TransactionId); } else { // We'll need to restore the original outputs. UnspentOutputs clone = coin.Clone(); clone.Outputs = original.ToArray(); rewindData.OutputsToRestore.Add(clone); } } } if (rewindData != null) { int nextRewindIndex = this.GetRewindIndex(transaction) + 1; this.logger.LogTrace("Rewind state #{0} created.", nextRewindIndex); transaction.Insert("Rewind", nextRewindIndex, rewindData); } insertedEntities += all.Count; transaction.Commit(); } } this.PerformanceCounter.AddInsertedEntities(insertedEntities); this.logger.LogTrace("(-)"); }); this.logger.LogTrace("(-)"); return(task); }
/// <inheritdoc /> public void SaveChanges(IList <UnspentOutput> outputs, HashHeightPair oldBlockHash, HashHeightPair nextBlockHash, List <RewindData> rewindDataList = null) { Guard.NotNull(oldBlockHash, nameof(oldBlockHash)); Guard.NotNull(nextBlockHash, nameof(nextBlockHash)); Guard.NotNull(outputs, nameof(outputs)); lock (this.lockobj) { if ((this.blockHash != null) && (oldBlockHash != this.blockHash)) { this.logger.LogDebug("{0}:'{1}'", nameof(this.blockHash), this.blockHash); this.logger.LogTrace("(-)[BLOCKHASH_MISMATCH]"); throw new InvalidOperationException("Invalid oldBlockHash"); } this.blockHash = nextBlockHash; long utxoSkipDisk = 0; var rewindData = new RewindData(oldBlockHash); Dictionary <OutPoint, int> indexItems = null; if (this.rewindDataIndexCache != null) { indexItems = new Dictionary <OutPoint, int>(); } foreach (UnspentOutput output in outputs) { if (!this.cachedUtxoItems.TryGetValue(output.OutPoint, out CacheItem cacheItem)) { // Add outputs to cache, this will happen for two cases // 1. if a cached item was evicted // 2. for new outputs that are added if (output.CreatedFromBlock) { // if the output is indicate that it was added from a block // There is no need to spend an extra call to disk. this.logger.LogDebug("New Outpoint '{0}' created.", output.OutPoint); cacheItem = new CacheItem() { ExistInInner = false, IsDirty = false, OutPoint = output.OutPoint, Coins = null }; } else { // This can happen if the cached item was evicted while // the block was being processed, fetch the output again from disk. this.logger.LogDebug("Outpoint '{0}' is not found in cache, creating it.", output.OutPoint); FetchCoinsResponse result = this.coindb.FetchCoins(new[] { output.OutPoint }); this.performanceCounter.AddMissCount(1); UnspentOutput unspentOutput = result.UnspentOutputs.Single().Value; cacheItem = new CacheItem() { ExistInInner = unspentOutput.Coins != null, IsDirty = false, OutPoint = unspentOutput.OutPoint, Coins = unspentOutput.Coins }; } this.cachedUtxoItems.Add(cacheItem.OutPoint, cacheItem); this.cacheSizeBytes += cacheItem.GetSize; this.logger.LogDebug("CacheItem added to the cache during save '{0}'.", cacheItem.OutPoint); } // If output.Coins is null this means the utxo needs to be deleted // otherwise this is a new utxo and we store it to cache. if (output.Coins == null) { // DELETE COINS // In cases of an output spent in the same block // it wont exist in cash or in disk so its safe to remove it if (cacheItem.Coins == null) { if (cacheItem.ExistInInner) { throw new InvalidOperationException(string.Format("Missmtch between coins in cache and in disk for output {0}", cacheItem.OutPoint)); } } else { // Handle rewind data this.logger.LogDebug("Create restore outpoint '{0}' in OutputsToRestore rewind data.", cacheItem.OutPoint); rewindData.OutputsToRestore.Add(new RewindDataOutput(cacheItem.OutPoint, cacheItem.Coins)); rewindData.TotalSize += cacheItem.GetSize; if (this.rewindDataIndexCache != null && indexItems != null) { indexItems[cacheItem.OutPoint] = this.blockHash.Height; } } // If a spent utxo never made it to disk then no need to keep it in memory. if (!cacheItem.ExistInInner) { this.logger.LogDebug("Utxo '{0}' is not in disk, removing from cache.", cacheItem.OutPoint); this.cachedUtxoItems.Remove(cacheItem.OutPoint); this.cacheSizeBytes -= cacheItem.GetSize; utxoSkipDisk++; if (cacheItem.IsDirty) { this.dirtyCacheCount--; } } else { // Now modify the cached items with the mutated data. this.logger.LogDebug("Mark cache item '{0}' as spent .", cacheItem.OutPoint); this.cacheSizeBytes -= cacheItem.GetScriptSize; cacheItem.Coins = null; // Delete output from cache but keep a the cache // item reference so it will get deleted form disk cacheItem.IsDirty = true; this.dirtyCacheCount++; } } else { // ADD COINS if (cacheItem.Coins != null) { // Allow overrides. // See https://github.com/bitcoin/bitcoin/blob/master/src/coins.cpp#L94 bool allowOverride = cacheItem.Coins.IsCoinbase && output.Coins != null; if (!allowOverride) { throw new InvalidOperationException(string.Format("New coins override coins in cache or store, for output '{0}'", cacheItem.OutPoint)); } this.logger.LogDebug("Coin override alllowed for utxo '{0}'.", cacheItem.OutPoint); // Deduct the current script size form the // total cache size, it will be added again later. this.cacheSizeBytes -= cacheItem.GetScriptSize; // Clear this in order to calculate the cache size // this will get set later when overridden cacheItem.Coins = null; } // Handle rewind data // New trx so it needs to be deleted if a rewind happens. this.logger.LogDebug("Adding output '{0}' to TransactionsToRemove rewind data.", cacheItem.OutPoint); rewindData.OutputsToRemove.Add(cacheItem.OutPoint); rewindData.TotalSize += cacheItem.GetSize; // Put in the cache the new UTXOs. this.logger.LogDebug("Mark cache item '{0}' as new .", cacheItem.OutPoint); cacheItem.Coins = output.Coins; this.cacheSizeBytes += cacheItem.GetScriptSize; // Mark the cache item as dirty so it get persisted // to disk and not evicted form cache cacheItem.IsDirty = true; this.dirtyCacheCount++; } } this.performanceCounter.AddUtxoSkipDiskCount(utxoSkipDisk); if (this.rewindDataIndexCache != null && indexItems.Any()) { this.rewindDataIndexCache.SaveAndEvict(this.blockHash.Height, indexItems); } // Add the most recent rewind data to the cache. this.cachedRewindData.Add(this.blockHash.Height, rewindData); this.rewindDataSizeBytes += rewindData.TotalSize; // Remove rewind data form the back of a moving window. // The closer we get to the tip we keep a longer rewind data window. // Anything bellow last checkpoint we keep the minimal of 10 // (random low number) rewind data items. // Beyond last checkpoint: // - For POS we keep a window of MaxReorg. // - For POW we keep 100 items (possibly better is an algo that grows closer to tip) // A moving window of information needed to rewind the node to a previous block. // When cache is flushed the rewind data will allow to rewind the node up to the // number of rewind blocks. // TODO: move rewind data to use block store. // Rewind data can go away all together if the node uses the blocks in block store // to get the rewind information, blockstore persists much more frequent then coin cache // So using block store for rewinds is not entirely impossible. int rewindDataWindow = this.CalculateRewindWindow(); int rewindToRemove = this.blockHash.Height - (int)rewindDataWindow; if (this.cachedRewindData.TryGetValue(rewindToRemove, out RewindData delete)) { this.logger.LogDebug("Remove rewind data height '{0}' from cache.", rewindToRemove); this.cachedRewindData.Remove(rewindToRemove); this.rewindDataSizeBytes -= delete.TotalSize; } } }
/// <inheritdoc /> public void SaveChanges(IList <UnspentOutputs> unspentOutputs, IEnumerable <TxOut[]> originalOutputs, uint256 oldBlockHash, uint256 nextBlockHash, int height, List <RewindData> rewindDataList = null) { Guard.NotNull(oldBlockHash, nameof(oldBlockHash)); Guard.NotNull(nextBlockHash, nameof(nextBlockHash)); Guard.NotNull(unspentOutputs, nameof(unspentOutputs)); lock (this.lockobj) { if (this.blockHash != null && oldBlockHash != this.blockHash) { this.logger.LogDebug("{0}:'{1}'", nameof(this.blockHash), this.blockHash); this.logger.LogTrace("(-)[BLOCKHASH_MISMATCH]"); throw new InvalidOperationException("Invalid oldBlockHash"); } this.blockHeight = height; this.blockHash = nextBlockHash; var rewindData = new RewindData(oldBlockHash); var indexItems = new Dictionary <OutPoint, int>(); foreach (var unspent in unspentOutputs) { if (!this.cachedUtxoItems.TryGetValue(unspent.TransactionId, out var cacheItem)) { // This can happen very rarely in the case where we fetch items from // disk but immediately call the Evict method which then removes the cached item(s). this.logger.LogDebug("Outputs of transaction ID '{0}' are not found in cache, creating them.", unspent.TransactionId); var result = this.inner.FetchCoins(new[] { unspent.TransactionId }); var unspentOutput = result.UnspentOutputs[0]; cacheItem = new CacheItem(); cacheItem.ExistInInner = unspentOutput != null; cacheItem.IsDirty = false; cacheItem.UnspentOutputs = unspentOutput?.Clone(); this.cachedUtxoItems.TryAdd(unspent.TransactionId, cacheItem); this.logger.LogDebug("CacheItem added to the cache during save '{0}'.", cacheItem.UnspentOutputs); } // If cacheItem.UnspentOutputs is null this means the trx was not stored in the disk, // that means the trx (and UTXO) is new and all the UTXOs need to be stored in cache // otherwise we store to cache only the UTXO that have been spent. if (cacheItem.UnspentOutputs != null) { // To handle rewind we'll need to restore the original outputs, // so we clone it and save it in rewind data. var clone = unspent.Clone(); // We take the original items that are in cache and put them in rewind data. clone.Outputs = cacheItem.UnspentOutputs.Outputs.ToArray(); this.logger.LogDebug("Modifying transaction '{0}' in OutputsToRestore rewind data.", unspent.TransactionId); rewindData.OutputsToRestore.Add(clone); this.logger.LogDebug("Cache item before spend {0}:'{1}'.", nameof(cacheItem.UnspentOutputs), cacheItem.UnspentOutputs); // Now modify the cached items with the mutated data. cacheItem.UnspentOutputs.Spend(unspent); this.logger.LogDebug("Cache item after spend {0}:'{1}'.", nameof(cacheItem.UnspentOutputs), cacheItem.UnspentOutputs); if (this.rewindDataIndexCache != null) { for (var i = 0; i < unspent.Outputs.Length; i++) { // Only push to rewind data index UTXOs that are spent. // Checks against array length are needed in case problem like 3965 on VSTS appears. if (i < clone.Outputs.Length && unspent.Outputs[i] == null && clone.Outputs[i] != null) { var key = new OutPoint(unspent.TransactionId, i); indexItems[key] = this.blockHeight; } } } } else { // New trx so it needs to be deleted if a rewind happens. this.logger.LogDebug("Adding transaction '{0}' to TransactionsToRemove rewind data.", unspent.TransactionId); rewindData.TransactionsToRemove.Add(unspent.TransactionId); // Put in the cache the new UTXOs. this.logger.LogDebug("Setting {0} to {1}: '{2}'.", nameof(cacheItem.UnspentOutputs), nameof(unspent), unspent); cacheItem.UnspentOutputs = unspent; } cacheItem.IsDirty = true; // Inner does not need to know pruned unspent that it never saw. if (cacheItem.UnspentOutputs.IsPrunable && !cacheItem.ExistInInner) { this.logger.LogDebug( "Outputs of transaction ID '{0}' are prunable and not in underlaying coinview, removing from cache.", unspent.TransactionId); this.cachedUtxoItems.Remove(unspent.TransactionId); } } if (this.rewindDataIndexCache != null && indexItems.Any()) { this.rewindDataIndexCache.Save(indexItems); this.rewindDataIndexCache.Flush(this.blockHeight); } this.cachedRewindDataIndex.Add(this.blockHeight, rewindData); } }