public WalletEntry(IImmutableList <MonitoredWalletAddress> addresses, EnumWalletEntryType type, ChainPosition chainPosition, UInt64 value) { this.addresses = addresses; this.type = type; this.chainPosition = chainPosition; this.value = value; }
public void Mint(Transaction tx, ChainedHeader chainedHeader, bool isCoinbase) { // there exist two duplicate coinbases in the blockchain, which the design assumes to be impossible // ignore the first occurrences of these duplicates so that they do not need to later be deleted from the utxo, an unsupported operation // no other duplicates will occur again, it is now disallowed if ((chainedHeader.Height == DUPE_COINBASE_1_HEIGHT && tx.Hash == DUPE_COINBASE_1_HASH) || (chainedHeader.Height == DUPE_COINBASE_2_HEIGHT && tx.Hash == DUPE_COINBASE_2_HASH)) { return; } // verify transaction does not already exist in utxo if (this.chainStateBuilderStorage.ContainsTransaction(tx.Hash)) { // duplicate transaction output this.logger.Warn("Duplicate transaction at block {0:#,##0}, {1}, coinbase".Format2(chainedHeader.Height, chainedHeader.Hash.ToHexNumberString())); throw new ValidationException(chainedHeader.Hash); } // add transaction to the utxo this.chainStateBuilderStorage.AddTransaction(tx.Hash, new UnspentTx(chainedHeader.Hash, tx.Outputs.Count, OutputState.Unspent)); // add transaction outputs to the utxo foreach (var output in tx.Outputs.Select((x, i) => new KeyValuePair <TxOutputKey, TxOutput>(new TxOutputKey(tx.Hash, (UInt32)i), x))) { this.chainStateBuilderStorage.AddOutput(output.Key, output.Value); // MONITOR: MintTxOutput if (this.chainStateMonitor != null) { this.chainStateMonitor.MintTxOutput(ChainPosition.Fake(), output.Key, output.Value, GetOutputScripHash(output.Value), isCoinbase); } } }
private void ScanForEntry(ChainPosition chainPosition, EnumWalletEntryType walletEntryType, TxOutput txOutput, UInt256 outputScriptHash) { var matchingAddresses = ImmutableList.CreateBuilder <MonitoredWalletAddress>(); // test hash addresses List <MonitoredWalletAddress> addresses; if (this.addressesByOutputScriptHash.TryGetValue(outputScriptHash, out addresses)) { matchingAddresses.AddRange(addresses); } // test matcher addresses foreach (var address in this.matcherAddresses) { if (address.Address.MatchesTxOutput(txOutput, outputScriptHash)) { matchingAddresses.Add(address); } } if (matchingAddresses.Count > 0) { var entry = new WalletEntry ( addresses: matchingAddresses.ToImmutable(), type: walletEntryType, chainPosition: chainPosition, value: txOutput.Value ); this.entriesLock.DoWrite(() => { this.logger.Debug("{0,-10} {1,20:#,##0.000_000_00} BTC, Entries: {2:#,##0}".Format2(walletEntryType.ToString() + ":", txOutput.Value / (decimal)(100.MILLION()), this.entries.Count)); this.entries.Add(entry); if (walletEntryType == EnumWalletEntryType.Spend) { this.bitBalance -= entry.BitValue; } else { this.bitBalance += entry.BitValue; } }); var handler = this.OnEntryAdded; if (handler != null) { handler(entry); } } }
private void ScanForEntry(ChainPosition chainPosition, EnumWalletEntryType walletEntryType, TxOutput txOutput, UInt256 outputScriptHash) { var matchingAddresses = ImmutableList.CreateBuilder <MonitoredWalletAddress>(); // test hash addresses List <MonitoredWalletAddress> addresses; if (this.addressesByOutputScriptHash.TryGetValue(outputScriptHash, out addresses)) { matchingAddresses.AddRange(addresses); } // test matcher addresses foreach (var address in this.matcherAddresses) { if (address.Address.MatchesTxOutput(txOutput, outputScriptHash)) { matchingAddresses.Add(address); } } if (matchingAddresses.Count > 0) { var entry = new WalletEntry ( addresses: matchingAddresses.ToImmutable(), type: walletEntryType, chainPosition: chainPosition, value: txOutput.Value ); lock (this.entries) { if (keepEntries) { this.entries.Add(entry); } this.entriesCount++; } this.bitBalance += entry.BitValue * walletEntryType.Direction(); logger.Debug($"{walletEntryType + ":",-10} {txOutput.Value / (decimal)(100.MILLION()),20:#,##0.000_000_00} BTC, Entries: {this.entriesCount:#,##0}"); this.OnEntryAdded?.Invoke(entry); } }
private async Task ScanBlock(ICoreStorage coreStorage, IChainState chainState, ChainedHeader scanBlock, bool forward, CancellationToken cancelToken = default(CancellationToken)) { var replayTxes = BlockReplayer.ReplayBlock(coreStorage, chainState, scanBlock.Hash, forward, cancelToken); var txScanner = new ActionBlock <ValidatableTx>( validatableTx => { var tx = validatableTx.Transaction; var txIndex = validatableTx.Index; if (!validatableTx.IsCoinbase) { for (var inputIndex = 0; inputIndex < tx.Inputs.Length; inputIndex++) { var input = tx.Inputs[inputIndex]; var prevOutput = validatableTx.PrevTxOutputs[inputIndex]; var prevOutputScriptHash = new UInt256(SHA256Static.ComputeHash(prevOutput.ScriptPublicKey)); var chainPosition = ChainPosition.Fake(); var entryType = forward ? EnumWalletEntryType.Spend : EnumWalletEntryType.UnSpend; ScanForEntry(chainPosition, entryType, (TxOutput)prevOutput, prevOutputScriptHash); } } for (var outputIndex = 0; outputIndex < tx.Outputs.Length; outputIndex++) { var output = tx.Outputs[outputIndex]; var outputScriptHash = new UInt256(SHA256Static.ComputeHash(output.ScriptPublicKey)); var chainPosition = ChainPosition.Fake(); var entryType = validatableTx.IsCoinbase ? (forward ? EnumWalletEntryType.Mine : EnumWalletEntryType.UnMine) : (forward ? EnumWalletEntryType.Receive : EnumWalletEntryType.UnReceieve); ScanForEntry(chainPosition, entryType, output, outputScriptHash); } }); replayTxes.LinkTo(txScanner, new DataflowLinkOptions { PropagateCompletion = true }); await txScanner.Completion; }
public void Unmint(Transaction tx, ChainedHeader chainedHeader, bool isCoinbase) { // ignore duplicate coinbases if ((chainedHeader.Height == DUPE_COINBASE_1_HEIGHT && tx.Hash == DUPE_COINBASE_1_HASH) || (chainedHeader.Height == DUPE_COINBASE_2_HEIGHT && tx.Hash == DUPE_COINBASE_2_HASH)) { return; } // check that transaction exists UnspentTx unspentTx; if (!this.chainStateBuilderStorage.TryGetTransaction(tx.Hash, out unspentTx)) { // missing transaction output this.logger.Warn("Missing transaction at block {0:#,##0}, {1}, tx {2}".Format2(chainedHeader.Height, chainedHeader.Hash.ToHexNumberString(), tx.Hash)); throw new ValidationException(chainedHeader.Hash); } //TODO verify blockheight // verify all outputs are unspent before unminting if (!unspentTx.OutputStates.All(x => x == OutputState.Unspent)) { throw new ValidationException(chainedHeader.Hash); } // remove the transaction this.chainStateBuilderStorage.RemoveTransaction(tx.Hash); // remove the transaction outputs for (var outputIndex = 0; outputIndex < tx.Outputs.Count; outputIndex++) { var txOutput = tx.Outputs[outputIndex]; var txOutputKey = new TxOutputKey(tx.Hash, (UInt32)outputIndex); this.chainStateBuilderStorage.RemoveOutput(txOutputKey); // MONITOR: UnspendTxOutput if (this.chainStateMonitor != null) { this.chainStateMonitor.UnmintTxOutput(ChainPosition.Fake(), txOutputKey, txOutput, GetOutputScripHash(txOutput), isCoinbase); } } }
//TODO thread safety //TODO need to rescan utxo when addresses are added as well public void AddAddress(IWalletAddress address) { //TODO add to queue, cannot monitor address until chain position moves var startChainPosition = ChainPosition.Fake(); var monitoredRange = new[] { Tuple.Create(startChainPosition, startChainPosition) }.ToList(); foreach (var outputScriptHash in address.GetOutputScriptHashes()) { List <MonitoredWalletAddress> addresses; if (!this.addressesByOutputScriptHash.TryGetValue(outputScriptHash, out addresses)) { addresses = new List <MonitoredWalletAddress>(); this.addressesByOutputScriptHash.Add(outputScriptHash, addresses); } addresses.Add(new MonitoredWalletAddress(address, monitoredRange)); } if (address.IsMatcher) { this.matcherAddresses.Add(new MonitoredWalletAddress(address, monitoredRange)); } }
public override void SpendTxOutput(ChainPosition chainPosition, ChainedHeader chainedHeader, Transaction tx, TxInput txInput, TxOutputKey txOutputKey, TxOutput txOutput, UInt256 outputScriptHash) { this.ScanForEntry(chainPosition, EnumWalletEntryType.Spend, txOutput, outputScriptHash); }
public override void MintTxOutput(ChainPosition chainPosition, TxOutputKey txOutputKey, TxOutput txOutput, UInt256 outputScriptHash, bool isCoinbase) { this.ScanForEntry(chainPosition, isCoinbase ? EnumWalletEntryType.Mine : EnumWalletEntryType.Receive, txOutput, outputScriptHash); }
public void Unspend(TxInput input, ChainedHeader chainedHeader, Dictionary <UInt256, SpentTx> spentTransactions, Dictionary <TxOutputKey, TxOutput> spentOutputs) { //TODO currently a MissingDataException will get thrown if the rollback information is missing //TODO rollback is still possible if any resurrecting transactions can be found //TODO the network does not allow arbitrary transaction lookup, but if the transactions can be retrieved then this code should allow it //// retrieve rollback information //UInt256 prevTxBlockHash; //if (!spentTransactions.TryGetValue(input.PreviousTxOutputKey.TxHash, out prevTxBlockHash)) //{ // //TODO throw should indicate rollback info is missing // throw new MissingDataException(null); //} // retrieve previous output TxOutput prevTxOutput; if (!spentOutputs.TryGetValue(input.PreviousTxOutputKey, out prevTxOutput)) { throw new Exception("TODO - corruption"); } // retrieve transaction output states, if not found then a fully spent transaction is being resurrected UnspentTx unspentTx; if (!this.chainStateBuilderStorage.TryGetTransaction(input.PreviousTxOutputKey.TxHash, out unspentTx)) { // retrieve spent transaction SpentTx prevSpentTx; if (!spentTransactions.TryGetValue(input.PreviousTxOutputKey.TxHash, out prevSpentTx)) { throw new Exception("TODO - corruption"); } // create fully spent transaction output state unspentTx = new UnspentTx(prevSpentTx.ConfirmedBlockHash, prevSpentTx.OutputCount, OutputState.Spent); } // retrieve previous output index var outputIndex = unchecked ((int)input.PreviousTxOutputKey.TxOutputIndex); if (outputIndex < 0 || outputIndex >= unspentTx.OutputStates.Length) { throw new Exception("TODO - corruption"); } // check that output isn't already considered unspent if (unspentTx.OutputStates[outputIndex] == OutputState.Unspent) { throw new ValidationException(chainedHeader.Hash); } // mark output as unspent this.chainStateBuilderStorage.UpdateTransaction(input.PreviousTxOutputKey.TxHash, unspentTx.SetOutputState(outputIndex, OutputState.Unspent)); // add transaction output back to utxo this.chainStateBuilderStorage.AddOutput(input.PreviousTxOutputKey, prevTxOutput); // MONITOR: UnspendTxOutput if (this.chainStateMonitor != null) { this.chainStateMonitor.UnspendTxOutput(ChainPosition.Fake(), input, input.PreviousTxOutputKey, prevTxOutput, GetOutputScripHash(prevTxOutput)); } }
//TODO with the rollback information that's now being stored, rollback could be down without needing the block private void RollbackUtxo(Block block) { //TODO currently a MissingDataException will get thrown if the rollback information is missing //TODO rollback is still possible if any resurrecting transactions can be found //TODO the network does not allow arbitrary transaction lookup, but if the transactions can be retrieved then this code should allow it //TODO this should be handled by a distinct worker that rebuilds rollback information var spentTransactions = new Dictionary <UInt256, SpentTx>(); spentTransactions.AddRange(this.spentTransactionsCache[block.Hash]); var spentOutputs = new Dictionary <TxOutputKey, TxOutput>(); spentOutputs.AddRange(this.spentOutputsCache[block.Hash]); for (var txIndex = block.Transactions.Count - 1; txIndex >= 1; txIndex--) { var tx = block.Transactions[txIndex]; // MONITOR: BeforeRemoveTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.BeforeRemoveTransaction(ChainPosition.Fake(), tx); } // remove outputs this.Unmint(tx, this.LastBlock, isCoinbase: false); // remove inputs in reverse order for (var inputIndex = tx.Inputs.Count - 1; inputIndex >= 0; inputIndex--) { var input = tx.Inputs[inputIndex]; this.Unspend(input, this.LastBlock, spentTransactions, spentOutputs); } // MONITOR: AfterRemoveTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.AfterRemoveTransaction(ChainPosition.Fake(), tx); } } var coinbaseTx = block.Transactions[0]; // MONITOR: BeforeRemoveTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.BeforeRemoveTransaction(ChainPosition.Fake(), coinbaseTx); } // remove coinbase outputs this.Unmint(coinbaseTx, this.LastBlock, isCoinbase: true); for (var inputIndex = coinbaseTx.Inputs.Count - 1; inputIndex >= 0; inputIndex--) { // MONITOR: UnCoinbaseInput if (this.chainStateMonitor != null) { this.chainStateMonitor.UnCoinbaseInput(ChainPosition.Fake(), coinbaseTx.Inputs[inputIndex]); } } // MONITOR: AfterRemoveTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.AfterRemoveTransaction(ChainPosition.Fake(), coinbaseTx); } }
private void CalculateUtxo(ChainedBlock chainedBlock, out long txCount, out long inputCount) { // don't include genesis block coinbase in utxo if (chainedBlock.Height <= 0) { throw new InvalidOperationException(); } txCount = 1; inputCount = 0; //using (var txInputQueue = new ProducerConsumer<Tuple<Transaction, int, TxInput, int, TxOutput>>()) //using (var validateScriptsTask = Task.Factory.StartNew(() => this.ValidateTransactionScripts(chainedBlock, txInputQueue))) //using (var txOutputQueue = new ProducerConsumer<Tuple<int, ChainPosition, TxOutput>>()) //using (var scannerTask = Task.Factory.StartNew(() => this.ScanTransactions(txOutputQueue, monitors))) //try //{ //TODO apply real coinbase rule // https://github.com/bitcoin/bitcoin/blob/481d89979457d69da07edd99fba451fd42a47f5c/src/core.h#L219 var coinbaseTx = chainedBlock.Transactions[0]; // MONITOR: BeforeAddTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.BeforeAddTransaction(ChainPosition.Fake(), coinbaseTx); } // MONITOR: CoinbaseInput if (this.chainStateMonitor != null) { foreach (var input in coinbaseTx.Inputs) { this.chainStateMonitor.CoinbaseInput(ChainPosition.Fake(), input); } } this.Mint(coinbaseTx, chainedBlock.ChainedHeader, isCoinbase: true); // MONITOR: AfterAddTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.AfterAddTransaction(ChainPosition.Fake(), coinbaseTx); } // check for double spends for (var txIndex = 1; txIndex < chainedBlock.Transactions.Count; txIndex++) { var tx = chainedBlock.Transactions[txIndex]; txCount++; // MONITOR: BeforeAddTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.BeforeAddTransaction(ChainPosition.Fake(), tx); } for (var inputIndex = 0; inputIndex < tx.Inputs.Count; inputIndex++) { var input = tx.Inputs[inputIndex]; inputCount++; this.Spend(txIndex, tx, inputIndex, input, chainedBlock.ChainedHeader); // MEASURE: Input Rate this.stats.inputRateMeasure.Tick(); } this.Mint(tx, chainedBlock.ChainedHeader, isCoinbase: false); // MONITOR: AfterAddTransaction if (this.chainStateMonitor != null) { this.chainStateMonitor.AfterAddTransaction(ChainPosition.Fake(), tx); } // MEASURE: Transaction Rate this.stats.txRateMeasure.Tick(); } //} //finally //{ // // ensure that started tasks always complete using a finally block // // any exceptions will be propagated by Task.WaitAll() // //TODO unwrap aggregation exception into something more specific // txInputQueue.CompleteAdding(); // txOutputQueue.CompleteAdding(); // Task.WaitAll(validateScriptsTask, scannerTask); //} }