public FetchCoinsResponse FetchCoins(OutPoint[] utxos) { FetchCoinsResponse res = new FetchCoinsResponse(); using (DBreeze.Transactions.Transaction transaction = this.CreateTransaction()) { transaction.SynchronizeTables("BlockHash", "Coins"); transaction.ValuesLazyLoadingIsOn = false; using (new StopwatchDisposable(o => this.performanceCounter.AddQueryTime(o))) { this.performanceCounter.AddQueriedEntities(utxos.Length); foreach (OutPoint outPoint in utxos) { Row <byte[], byte[]> row = transaction.Select <byte[], byte[]>("Coins", outPoint.ToBytes()); Coins outputs = row.Exists ? this.dBreezeSerializer.Deserialize <Utilities.Coins>(row.Value) : null; this.logger.LogTrace("Outputs for '{0}' were {1}.", outPoint, outputs == null ? "NOT loaded" : "loaded"); res.UnspentOutputs.Add(outPoint, new UnspentOutput(outPoint, outputs)); } } } return(res); }
/// <inheritdoc /> public async Task <UnspentOutputs> GetUnspentTransactionAsync(uint256 trxid) { CoinViews.FetchCoinsResponse response = null; if (this.UTXOSet != null) { response = await this.UTXOSet.FetchCoinsAsync(new[] { trxid }).ConfigureAwait(false); } return(response?.UnspentOutputs?.SingleOrDefault()); }
/// <inheritdoc /> public FetchCoinsResponse FetchCoins(OutPoint[] utxos) { Guard.NotNull(utxos, nameof(utxos)); var result = new FetchCoinsResponse(); var missedOutpoint = new List <OutPoint>(); lock (this.lockobj) { foreach (OutPoint outPoint in utxos) { if (!this.cachedUtxoItems.TryGetValue(outPoint, out CacheItem cache)) { this.logger.LogDebug("Utxo '{0}' not found in cache.", outPoint); missedOutpoint.Add(outPoint); } else { this.logger.LogDebug("Utxo '{0}' found in cache, UTXOs:'{1}'.", outPoint, cache.Coins); result.UnspentOutputs.Add(outPoint, new UnspentOutput(outPoint, cache.Coins)); } } this.performanceCounter.AddMissCount(missedOutpoint.Count); this.performanceCounter.AddHitCount(utxos.Length - missedOutpoint.Count); if (missedOutpoint.Count > 0) { this.logger.LogDebug("{0} cache missed transaction needs to be loaded from underlying CoinView.", missedOutpoint.Count); FetchCoinsResponse fetchedCoins = this.Inner.FetchCoins(missedOutpoint.ToArray()); foreach (var unspentOutput in fetchedCoins.UnspentOutputs) { result.UnspentOutputs.Add(unspentOutput.Key, unspentOutput.Value); var cache = new CacheItem() { ExistInInner = unspentOutput.Value.Coins != null, IsDirty = false, OutPoint = unspentOutput.Key, Coins = unspentOutput.Value.Coins }; this.logger.LogDebug("CacheItem added to the cache, UTXO '{0}', Coin:'{1}'.", cache.OutPoint, cache.Coins); this.cachedUtxoItems.Add(cache.OutPoint, cache); this.cacheSizeBytes += cache.GetSize; } } // Check if we need to evict items form the cache. // This happens every time data is fetched fomr coindb this.TryEvictCacheLocked(); } return(result); }
/// <inheritdoc /> public uint256 GetTipHash(CancellationToken cancellationToken = default(CancellationToken)) { if (this.blockHash == null) { FetchCoinsResponse response = this.FetchCoins(new uint256[0], cancellationToken); this.innerBlockHash = response.BlockHash; this.blockHash = this.innerBlockHash; } return(this.blockHash); }
/// <inheritdoc /> public async Task <uint256> GetTipHashAsync(CancellationToken cancellationToken = default(CancellationToken)) { if (this.blockHash == null) { FetchCoinsResponse response = await this.FetchCoinsAsync(new uint256[0], cancellationToken).ConfigureAwait(false); this.innerBlockHash = response.BlockHash; this.blockHash = this.innerBlockHash; } return(this.blockHash); }
/// <inheritdoc /> public FetchCoinsResponse FetchCoins(OutPoint[] txIds) { Guard.NotNull(txIds, nameof(txIds)); using (this.lockobj.LockRead()) { var result = new FetchCoinsResponse(); for (int i = 0; i < txIds.Length; i++) { var output = this.unspents.TryGet(txIds[i]); if (output != null) { result.UnspentOutputs.Add(output.OutPoint, output); } } return(result); } }
public FetchCoinsResponse FetchCoins(OutPoint[] utxos) { FetchCoinsResponse res = new FetchCoinsResponse(); using (new StopwatchDisposable(o => this.performanceCounter.AddQueryTime(o))) { this.performanceCounter.AddQueriedEntities(utxos.Length); foreach (OutPoint outPoint in utxos) { byte[] row = this.leveldb.Get(new byte[] { coinsTable }.Concat(outPoint.ToBytes()).ToArray()); Coins outputs = row != null?this.dBreezeSerializer.Deserialize <Coins>(row) : null; this.logger.LogTrace("Outputs for '{0}' were {1}.", outPoint, outputs == null ? "NOT loaded" : "loaded"); res.UnspentOutputs.Add(outPoint, new UnspentOutput(outPoint, outputs)); } } return(res); }
public FetchCoinsResponse FetchCoins(OutPoint[] utxos) { FetchCoinsResponse res = new FetchCoinsResponse(); using (var session = this.db.NewSession()) { using (new StopwatchDisposable(o => this.performanceCounter.AddQueryTime(o))) { this.performanceCounter.AddQueriedEntities(utxos.Length); Types.StoreInput input = new Types.StoreInput(); Types.StoreOutput output = new Types.StoreOutput(); Types.StoreContext context = new Types.StoreContext(); var readKey = new Types.StoreKey { tableType = "Coins" }; foreach (OutPoint outPoint in utxos) { output.value = null; readKey.key = outPoint.ToBytes(); var addStatus = session.Read(ref readKey, ref input, ref output, context, 1); if (addStatus == Status.PENDING) { session.CompletePending(true); context.FinalizeRead(ref addStatus, ref output); } Utilities.Coins outputs = addStatus == Status.OK ? this.dBreezeSerializer.Deserialize <Utilities.Coins>(output.value.value) : null; this.logger.LogDebug("Outputs for '{0}' were {1}.", outPoint, outputs == null ? "NOT loaded" : "loaded"); res.UnspentOutputs.Add(outPoint, new UnspentOutput(outPoint, outputs)); } } } return(res); }
/// <inheritdoc /> public Task <FetchCoinsResponse> FetchCoinsAsync(uint256[] txIds, CancellationToken cancellationToken = default(CancellationToken)) { Task <FetchCoinsResponse> task = Task.Run(() => { this.logger.LogTrace("({0}.{1}:{2})", nameof(txIds), nameof(txIds.Length), txIds?.Length); FetchCoinsResponse res = null; using (DBreeze.Transactions.Transaction transaction = this.dbreeze.GetTransaction()) { transaction.SynchronizeTables("BlockHash", "Coins"); transaction.ValuesLazyLoadingIsOn = false; using (new StopwatchDisposable(o => this.PerformanceCounter.AddQueryTime(o))) { uint256 blockHash = this.GetTipHash(transaction); var result = new UnspentOutputs[txIds.Length]; this.PerformanceCounter.AddQueriedEntities(txIds.Length); int i = 0; foreach (uint256 input in txIds) { Row <byte[], Coins> row = transaction.Select <byte[], Coins>("Coins", input.ToBytes(false)); UnspentOutputs outputs = row.Exists ? new UnspentOutputs(input, row.Value) : null; this.logger.LogTrace("Outputs for '{0}' were {1}.", input, outputs == null ? "NOT loaded" : "loaded"); result[i++] = outputs; } res = new FetchCoinsResponse(result, blockHash); } } this.logger.LogTrace("(-):*.{0}='{1}',*.{2}.{3}={4}", nameof(res.BlockHash), res.BlockHash, nameof(res.UnspentOutputs), nameof(res.UnspentOutputs.Length), res.UnspentOutputs.Length); return(res); }, cancellationToken); return(task); }
/// <inheritdoc /> public void CacheCoins(OutPoint[] utxos) { lock (this.lockobj) { var missedOutpoint = new List <OutPoint>(); foreach (OutPoint outPoint in utxos) { if (!this.cachedUtxoItems.TryGetValue(outPoint, out CacheItem cache)) { this.logger.LogDebug("Prefetch Utxo '{0}' not found in cache.", outPoint); missedOutpoint.Add(outPoint); } } this.performanceCounter.AddCacheMissCount(missedOutpoint.Count); this.performanceCounter.AddCacheHitCount(utxos.Length - missedOutpoint.Count); if (missedOutpoint.Count > 0) { FetchCoinsResponse fetchedCoins = this.Inner.FetchCoins(missedOutpoint.ToArray()); foreach (var unspentOutput in fetchedCoins.UnspentOutputs) { var cache = new CacheItem() { ExistInInner = unspentOutput.Value.Coins != null, IsDirty = false, OutPoint = unspentOutput.Key, Coins = unspentOutput.Value.Coins }; this.logger.LogDebug("Prefetch CacheItem added to the cache, UTXO: '{0}', Coin:'{1}'.", cache.OutPoint, cache.Coins); this.cachedUtxoItems.Add(cache.OutPoint, cache); this.cacheSizeBytes += cache.GetSize; } } } }
/// <summary> /// Retrieves the block hash of the current tip of the coinview. /// </summary> /// <returns>Block hash of the current tip of the coinview.</returns> public async Task <uint256> GetBlockHashAsync() { FetchCoinsResponse response = await this.FetchCoinsAsync(new uint256[0]).ConfigureAwait(false); return(response.BlockHash); }
/// <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 chaced 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 cashe item was evicted while // the block was being processed, fetch the outut 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 crurrent 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 sie // this will get set later when overriden 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; } }
/// <inheritdoc /> public async Task <FetchCoinsResponse> FetchCoinsAsync(uint256[] txIds, CancellationToken cancellationToken = default(CancellationToken)) { Guard.NotNull(txIds, nameof(txIds)); this.logger.LogTrace("({0}.{1}:{2})", nameof(txIds), nameof(txIds.Length), txIds.Length); FetchCoinsResponse result = null; var outputs = new UnspentOutputs[txIds.Length]; var miss = new List <int>(); var missedTxIds = new List <uint256>(); using (await this.lockobj.LockAsync(cancellationToken).ConfigureAwait(false)) { for (int i = 0; i < txIds.Length; i++) { CacheItem cache; if (!this.unspents.TryGetValue(txIds[i], out cache)) { this.logger.LogTrace("Cache missed for transaction ID '{0}'.", txIds[i]); miss.Add(i); missedTxIds.Add(txIds[i]); } else { this.logger.LogTrace("Cache hit for transaction ID '{0}'.", txIds[i]); outputs[i] = cache.UnspentOutputs == null ? null : cache.UnspentOutputs.IsPrunable ? null : cache.UnspentOutputs.Clone(); } } this.PerformanceCounter.AddMissCount(miss.Count); this.PerformanceCounter.AddHitCount(txIds.Length - miss.Count); } this.logger.LogTrace("{0} cache missed transaction needs to be loaded from underlying CoinView.", missedTxIds.Count); FetchCoinsResponse fetchedCoins = await this.Inner.FetchCoinsAsync(missedTxIds.ToArray(), cancellationToken).ConfigureAwait(false); using (await this.lockobj.LockAsync(cancellationToken).ConfigureAwait(false)) { uint256 innerblockHash = fetchedCoins.BlockHash; if (this.blockHash == null) { Debug.Assert(this.unspents.Count == 0); this.innerBlockHash = innerblockHash; this.blockHash = this.innerBlockHash; } for (int i = 0; i < miss.Count; i++) { int index = miss[i]; UnspentOutputs unspent = fetchedCoins.UnspentOutputs[i]; outputs[index] = unspent; var cache = new CacheItem(); cache.ExistInInner = unspent != null; cache.IsDirty = false; cache.UnspentOutputs = unspent; cache.OriginalOutputs = unspent?.Outputs.ToArray(); this.unspents.TryAdd(txIds[index], cache); } result = new FetchCoinsResponse(outputs, this.blockHash); } int cacheEntryCount = this.CacheEntryCount; if (cacheEntryCount > this.MaxItems) { this.logger.LogTrace("Cache is full now with {0} entries, evicting ...", cacheEntryCount); await this.EvictAsync().ConfigureAwait(false); } this.logger.LogTrace("(-):*.{0}='{1}',*.{2}.{3}={4}", nameof(result.BlockHash), result.BlockHash, nameof(result.UnspentOutputs), nameof(result.UnspentOutputs.Length), result.UnspentOutputs.Length); return(result); }
/// <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.LogTrace("{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 (UnspentOutputs unspent in unspentOutputs) { if (!this.cachedUtxoItems.TryGetValue(unspent.TransactionId, out CacheItem 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.LogTrace("Outputs of transaction ID '{0}' are not found in cache, creating them.", unspent.TransactionId); FetchCoinsResponse result = this.inner.FetchCoins(new[] { unspent.TransactionId }); UnspentOutputs 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.LogTrace("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. UnspentOutputs 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.LogTrace("Modifying transaction '{0}' in OutputsToRestore rewind data.", unspent.TransactionId); rewindData.OutputsToRestore.Add(clone); this.logger.LogTrace("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.LogTrace("Cache item after spend {0}:'{1}'.", nameof(cacheItem.UnspentOutputs), cacheItem.UnspentOutputs); } else { // New trx so it needs to be deleted if a rewind happens. this.logger.LogTrace("Adding transaction '{0}' to TransactionsToRemove rewind data.", unspent.TransactionId); rewindData.TransactionsToRemove.Add(unspent.TransactionId); // Put in the cache the new UTXOs. this.logger.LogTrace("Setting {0} to {1}: '{2}'.", nameof(cacheItem.UnspentOutputs), nameof(unspent), unspent); cacheItem.UnspentOutputs = unspent; } cacheItem.IsDirty = true; if (this.rewindDataIndexCache != null) { for (int i = 0; i < unspent.Outputs.Length; i++) { var key = new OutPoint(unspent.TransactionId, i); indexItems[key] = this.blockHeight; } } // Inner does not need to know pruned unspent that it never saw. if (cacheItem.UnspentOutputs.IsPrunable && !cacheItem.ExistInInner) { this.logger.LogTrace("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); } }
/// <inheritdoc /> public FetchCoinsResponse FetchCoins(uint256[] txIds, CancellationToken cancellationToken = default(CancellationToken)) { Guard.NotNull(txIds, nameof(txIds)); FetchCoinsResponse result = null; var outputs = new UnspentOutputs[txIds.Length]; var miss = new List <int>(); var missedTxIds = new List <uint256>(); lock (this.lockobj) { for (int i = 0; i < txIds.Length; i++) { CacheItem cache; if (!this.cachedUtxoItems.TryGetValue(txIds[i], out cache)) { this.logger.LogTrace("Transaction '{0}' not found in cache.", txIds[i]); miss.Add(i); missedTxIds.Add(txIds[i]); } else { this.logger.LogTrace("Transaction '{0}' found in cache, UTXOs:'{1}'.", txIds[i], cache.UnspentOutputs); outputs[i] = cache.UnspentOutputs == null ? null : cache.UnspentOutputs.IsPrunable ? null : cache.UnspentOutputs.Clone(); } } this.performanceCounter.AddMissCount(miss.Count); this.performanceCounter.AddHitCount(txIds.Length - miss.Count); FetchCoinsResponse fetchedCoins = null; if (missedTxIds.Count > 0 || this.blockHash == null) { this.logger.LogTrace("{0} cache missed transaction needs to be loaded from underlying CoinView.", missedTxIds.Count); fetchedCoins = this.Inner.FetchCoins(missedTxIds.ToArray(), cancellationToken); } if (this.blockHash == null) { uint256 innerblockHash = fetchedCoins.BlockHash; Debug.Assert(this.cachedUtxoItems.Count == 0); this.innerBlockHash = innerblockHash; this.blockHash = this.innerBlockHash; } for (int i = 0; i < miss.Count; i++) { int index = miss[i]; UnspentOutputs unspent = fetchedCoins.UnspentOutputs[i]; outputs[index] = unspent; var cache = new CacheItem(); cache.ExistInInner = unspent != null; cache.IsDirty = false; cache.UnspentOutputs = unspent?.Clone(); this.logger.LogTrace("CacheItem added to the cache, Transaction Id '{0}', UTXO:'{1}'.", txIds[index], cache.UnspentOutputs); this.cachedUtxoItems.TryAdd(txIds[index], cache); } result = new FetchCoinsResponse(outputs, this.blockHash); int cacheEntryCount = this.cacheEntryCount; if (cacheEntryCount > this.MaxItems) { this.logger.LogTrace("Cache is full now with {0} entries, evicting.", cacheEntryCount); this.EvictLocked(); } } return(result); }
/// <summary> /// Retrieves the block hash of the current tip of the coinview. /// </summary> /// <returns>Block hash of the current tip of the coinview.</returns> public async Task <uint256> GetBlockHashAsync(CancellationToken cancellationToken = default(CancellationToken)) { FetchCoinsResponse response = await this.FetchCoinsAsync(new uint256[0], cancellationToken).ConfigureAwait(false); return(response.BlockHash); }
/// <inheritdoc /> public async Task SaveChangesAsync(IEnumerable <UnspentOutputs> unspentOutputs, IEnumerable <TxOut[]> originalOutputs, uint256 oldBlockHash, uint256 nextBlockHash, List <RewindData> rewindDataList = null) { Guard.NotNull(oldBlockHash, nameof(oldBlockHash)); Guard.NotNull(nextBlockHash, nameof(nextBlockHash)); Guard.NotNull(unspentOutputs, nameof(unspentOutputs)); 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); using (await this.lockobj.LockAsync().ConfigureAwait(false)) { if ((this.blockHash != null) && (oldBlockHash != this.blockHash)) { this.logger.LogTrace("(-)[BLOCKHASH_MISMATCH]"); throw new InvalidOperationException("Invalid oldBlockHash"); } this.blockHash = nextBlockHash; var rewindData = new RewindData(oldBlockHash); foreach (UnspentOutputs unspent in unspentOutputs) { if (!this.cachedUtxoItems.TryGetValue(unspent.TransactionId, out CacheItem 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.LogTrace("Outputs of transaction ID '{0}' are not found in cache, creating them.", unspent.TransactionId); FetchCoinsResponse result = await this.inner.FetchCoinsAsync(new[] { unspent.TransactionId }).ConfigureAwait(false); UnspentOutputs unspentOutput = result.UnspentOutputs[0]; cacheItem = new CacheItem(); cacheItem.ExistInInner = unspentOutput != null; cacheItem.IsDirty = false; cacheItem.UnspentOutputs = unspentOutput?.Clone(); this.cachedUtxoItems.TryAdd(unspent.TransactionId, cacheItem); } else { this.logger.LogTrace("Outputs of transaction ID '{0}' are in cache already, updating them.", unspent.TransactionId); } // 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. UnspentOutputs clone = unspent.Clone(); // We take the original items that are in cache and put them in rewind data. clone.Outputs = cacheItem.UnspentOutputs.Outputs.ToArray(); rewindData.OutputsToRestore.Add(clone); // Now modify the cached items with the mutated data. cacheItem.UnspentOutputs.Spend(unspent); } else { // New trx so it needs to be deleted if a rewind happens. rewindData.TransactionsToRemove.Add(unspent.TransactionId); // Put in the cache the new UTXOs. 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.LogTrace("Outputs of transaction ID '{0}' are prunable and not in underlaying coinview, removing from cache.", unspent.TransactionId); this.cachedUtxoItems.Remove(unspent.TransactionId); } } this.cachedRewindDataList.Add(rewindData); } this.logger.LogTrace("(-)"); }
/// <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 chaced 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 cashe item was evicted while // the block was being processed, fetch the outut again from disk. this.logger.LogDebug("Outpoint '{0}' is not found in cache, creating it.", output.OutPoint); FetchCoinsResponse result = this.inner.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 crurrent 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 sie // this will get set later when overriden 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 togetehr 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. uint rewindDataWindow = 10; if (this.blockHash.Height >= this.lastCheckpointHeight) { if (this.network.Consensus.MaxReorgLength != 0) { rewindDataWindow = this.network.Consensus.MaxReorgLength + 1; } else { // TODO: make the rewind data window a configuration // parameter of evern a network parameter. // For POW assume BTC where a rewind data of 100 is more then enough. rewindDataWindow = 100; } } 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; } } }
public override async Task <FetchCoinsResponse> FetchCoinsAsync(uint256[] txIds) { Guard.NotNull(txIds, nameof(txIds)); FetchCoinsResponse result = null; UnspentOutputs[] outputs = new UnspentOutputs[txIds.Length]; List <int> miss = new List <int>(); List <uint256> missedTxIds = new List <uint256>(); using (this.lockobj.LockRead()) { this.WaitOngoingTasks(); for (int i = 0; i < txIds.Length; i++) { CacheItem cache; if (!this.unspents.TryGetValue(txIds[i], out cache)) { miss.Add(i); missedTxIds.Add(txIds[i]); } else { outputs[i] = cache.UnspentOutputs == null ? null : cache.UnspentOutputs.IsPrunable ? null : cache.UnspentOutputs.Clone(); } } this.PerformanceCounter.AddMissCount(miss.Count); this.PerformanceCounter.AddHitCount(txIds.Length - miss.Count); } var fetchedCoins = await this.Inner.FetchCoinsAsync(missedTxIds.ToArray()).ConfigureAwait(false); using (this.lockobj.LockWrite()) { this.flushing.Wait(); var innerblockHash = fetchedCoins.BlockHash; if (this.blockHash == null) { Debug.Assert(this.unspents.Count == 0); this.innerBlockHash = innerblockHash; this.blockHash = this.innerBlockHash; } for (int i = 0; i < miss.Count; i++) { var index = miss[i]; var unspent = fetchedCoins.UnspentOutputs[i]; outputs[index] = unspent; CacheItem cache = new CacheItem(); cache.ExistInInner = unspent != null; cache.IsDirty = false; cache.UnspentOutputs = unspent; cache.OriginalOutputs = unspent?._Outputs.ToArray(); this.unspents.TryAdd(txIds[index], cache); } result = new FetchCoinsResponse(outputs, this.blockHash); } if (this.CacheEntryCount > this.MaxItems) { this.Evict(); if (this.CacheEntryCount > this.MaxItems) { await this.FlushAsync().ConfigureAwait(false); this.Evict(); } } return(result); }