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();
        }
Esempio n. 2
0
        /// <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);
            }
        }
Esempio n. 3
0
        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);
        }
Esempio n. 5
0
        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);
        }
Esempio n. 6
0
        /// <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);
        }
Esempio n. 7
0
        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);
            }
        }
Esempio n. 9
0
        /// <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);
            }
        }
Esempio n. 10
0
        /// <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);
        }
Esempio n. 11
0
        /// <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;
            }
        }
Esempio n. 12
0
        /// <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);
        }
Esempio n. 13
0
        /// <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;
                }
            }
        }
Esempio n. 14
0
        /// <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;
                    }
                }
            }
        }
Esempio n. 16
0
        /// <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);
        }
Esempio n. 17
0
        /// <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;
                }
            }
        }
Esempio n. 18
0
        /// <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);
            }
        }