/// <summary>Persists rewind data into the repository.</summary> /// <param name="rewindData">The data to be persisted.</param> public void RecordRewindData(AddressIndexerRewindData rewindData) { lock (this.LockObject) { this.addressIndexerRewindData.Upsert(rewindData); } }
/// <summary>Reverts changes made by processing a block with <param name="blockHash"> hash.</param></summary> public void Rewind(uint256 blockHash) { lock (this.LockObject) { AddressIndexerRewindData rewindData = this.addressIndexerRewindData.FindById(blockHash.ToString()); if (rewindData == null) { this.logger.LogTrace("(-)[NOT_FOUND]"); throw new Exception($"Rewind data not found for {blockHash}."); } // Put the spent outputs back into the cache. foreach (OutPointData outPointData in rewindData.SpentOutputs) { this.AddOutPointData(outPointData); } this.addressIndexerRewindData.Delete(rewindData.BlockHash); } }
/// <summary>Processes block that was added or removed from consensus chain.</summary> /// <returns><c>true</c> if block was processed.</returns> private bool ProcessBlock(Block block, ChainedHeader header) { lock (this.lockObject) { // Record outpoints. foreach (Transaction tx in block.Transactions) { for (int i = 0; i < tx.Outputs.Count; i++) { // OP_RETURN outputs and empty outputs cannot be spent and therefore do not need to be put into the cache. if (tx.Outputs[i].IsEmpty || tx.Outputs[i].ScriptPubKey.IsUnspendable) { continue; } var outPoint = new OutPoint(tx, i); var outPointData = new OutPointData() { Outpoint = outPoint.ToString(), ScriptPubKeyBytes = tx.Outputs[i].ScriptPubKey.ToBytes(), Money = tx.Outputs[i].Value }; // TODO: When the outpoint cache is full, adding outpoints singly causes overhead writing evicted entries out to the repository this.outpointsRepository.AddOutPointData(outPointData); } } } // Process inputs. var inputs = new List <TxIn>(); // Collect all inputs excluding coinbases. foreach (TxInList inputsCollection in block.Transactions.Where(x => !x.IsCoinBase).Select(x => x.Inputs)) { inputs.AddRange(inputsCollection); } lock (this.lockObject) { var rewindData = new AddressIndexerRewindData() { BlockHash = header.HashBlock.ToString(), BlockHeight = header.Height, SpentOutputs = new List <OutPointData>() }; foreach (TxIn input in inputs) { OutPoint consumedOutput = input.PrevOut; if (!this.outpointsRepository.TryGetOutPointData(consumedOutput, out OutPointData consumedOutputData)) { this.logger.LogError("Missing outpoint data for {0}.", consumedOutput); this.logger.LogTrace("(-)[MISSING_OUTPOINTS_DATA]"); throw new Exception($"Missing outpoint data for {consumedOutput}"); } Money amountSpent = consumedOutputData.Money; rewindData.SpentOutputs.Add(consumedOutputData); this.outpointsRepository.RemoveOutPointData(consumedOutput); // Transactions that don't actually change the balance just bloat the database. if (amountSpent == 0) { continue; } string address = this.scriptAddressReader.GetAddressFromScriptPubKey(this.network, new Script(consumedOutputData.ScriptPubKeyBytes)); if (string.IsNullOrEmpty(address)) { // This condition need not be logged, as the address reader should be aware of all possible address formats already. continue; } this.ProcessBalanceChangeLocked(header.Height, address, amountSpent, false); } // Process outputs. foreach (Transaction tx in block.Transactions) { foreach (TxOut txOut in tx.Outputs) { Money amountReceived = txOut.Value; // Transactions that don't actually change the balance just bloat the database. if (amountReceived == 0 || txOut.IsEmpty || txOut.ScriptPubKey.IsUnspendable) { continue; } string address = this.scriptAddressReader.GetAddressFromScriptPubKey(this.network, txOut.ScriptPubKey); if (string.IsNullOrEmpty(address)) { // This condition need not be logged, as the address reader should be aware of all // possible address formats already. continue; } this.ProcessBalanceChangeLocked(header.Height, address, amountReceived, true); } } this.outpointsRepository.RecordRewindData(rewindData); this.outpointsRepository.PurgeOldRewindData(this.consensusManager.Tip.Height - this.compactionTriggerDistance); } return(true); }