/// <summary> /// Processes raw bitcoin blocks /// - decodes the block, transactions, output scripts, and output addresses /// </summary> /// <param name="data">raw block byte array</param> public override void DoWork(byte[] data) { // attempt to decode the block (also decodes transactions, output scripts, and addresses) Block block; try { block = new Block(data) { FirstSeen = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), }; } catch (Exception e) { Console.WriteLine("Unable to decode block!"); return; } Console.WriteLine("Received Block with " + block.Transactions.Length + " transactions.\n" + " Previous Block: " + block.Header.PrevBlockHash + "\n" + " Block Hash: " + block.BlockHash); // Add block data to internal db, and check for re-org Program.Database.EnqueueTask(new DatabaseWrite(block), 0); // check all transactions foreach (var transaction in block.Transactions) { // check all outputs of the transaction SubscriptionCheck.CheckForSubscription(transaction); } }
/// <summary> /// Processes raw bitcoin transactions /// - decodes the transaction, output scripts, and output addresses /// </summary> /// <param name="data">raw transaction byte array</param> public override void DoWork(byte[] data) { // hex version of the transaction var txHex = ByteToHex.ByteArrayToHex(data); // attempt to decode the transaction (also decodes output scripts and addresses) Transaction transaction; try { transaction = new Transaction(data) { FirstSeen = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), LastUpdated = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; } catch (Exception e) { Console.WriteLine("Unable to decode transaction: \n" + txHex); return; } // check all outputs of the transaction SubscriptionCheck.CheckForSubscription(transaction); /* * Console.WriteLine("Received Transaction with " + transaction.Outputs.Length + " outputs and " + transaction.Inputs.Length + " inputs. HasWitness = " + + (transaction.HasWitness ? "YES" : "NO") + + ". Output Scripts:"); + + + // iterate over each output in the transaction + foreach (var output in transaction.Outputs) + { + // write out the raw output script as hex + Console.WriteLine(ByteToHex.ByteArrayToHex(output.Script)); + var script = new Script(output.Script); + var dataCount = 0; + // write out the ASM version of the output + foreach (var opCode in script.OpCodes) + if (opCode == OpCodeType.OP_DATA) + Console.Write(" " + ByteToHex.ByteArrayToHex(script.DataChunks[dataCount++])); + else + Console.Write(" " + opCode); + Console.WriteLine(); + // write out the type and address (if the output is a known payment type) + Console.WriteLine(" Type = " + output.Type + (output.Address == "" ? "" : ". Address = " + output.Address)); + } + Console.WriteLine(); */ }
private static void Main(string[] args) { // get internal litedb database filename from app settings var databaseFileName = ConfigurationManager.AppSettings["LiteDBFileName"]; // init the database Database = new DatabaseConsumer(databaseFileName); // get saved subscriptions Subscriptions = Database.GetSubscriptions().ToList(); // get RPC connection params from app settings var rpcURL = ConfigurationManager.AppSettings["RPCURI"]; var rpcUsername = ConfigurationManager.AppSettings["RPCUsername"]; var rpcPassword = ConfigurationManager.AppSettings["RPCPassword"]; // init RPC connection RPCClient = new RPCClient(new Uri(rpcURL), rpcUsername, rpcPassword); // get info for the last block that was processed var lastBlock = Database.GetLastBlock(); var blockCount = RPCClient.GetBlockCount(); // write out of the current block height, and last processed block height Console.Write("Current block: " + blockCount + ". " + (lastBlock == null ? " No previous block data found." : " Last block processed: " + lastBlock.Height)); // if we already have block data, fetch transactions since the last seen block if (lastBlock != null && lastBlock.Height < blockCount) { Console.WriteLine(". processing " + (blockCount - lastBlock.Height) + " blocks..."); // record how long it takes to process the block data var stopWatch = new Stopwatch(); stopWatch.Start(); // look at all blocks from the last block that was processed until the current height for (var blockIndex = lastBlock.Height; blockIndex <= blockCount; blockIndex++) { // fetch raw block data by height var blockHash = RPCClient.GetBlockHash(blockIndex); var blockData = RPCClient.GetBlockData(blockHash); // decode the block var block = new Block(ByteToHex.StringToByteArray(blockData)) { FirstSeen = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; // add the block to the database Database.EnqueueTask(new DatabaseWrite(block), 0); // process all transactions that occured in the block foreach (var transaction in block.Transactions) { // does this transaction contain an output we are watching? SubscriptionCheck.CheckForSubscription(transaction); } } stopWatch.Stop(); var elapsed = stopWatch.Elapsed; Console.Write("Processed blocks in " + $"{elapsed.Hours:00}:{elapsed.Minutes:00}:{elapsed.Seconds:00}.{elapsed.Milliseconds / 10:00}"); } Console.WriteLine(); // get websocket listen address/port from app settings var websocketListen = ConfigurationManager.AppSettings["WebSocketListen"]; // start websocket server WebSocketServer = new WebSocket.Server(websocketListen); // get ZMQ server address from app settings var zmqServerTX = ConfigurationManager.AppSettings["ZMQPublisherRawTX"]; var zmqServerBlock = ConfigurationManager.AppSettings["ZMQPublisherRawBlock"]; // start ZMQ subscribers new ZMQ.Subscriber(zmqServerTX, "rawtx", new TXConsumer()); new ZMQ.Subscriber(zmqServerBlock, "rawblock", new BlockConsumer()); // skip scanning the mempool if there is no saved block data yet (TODO: maybe it should still scan?) if (lastBlock != null) { // fetch the mempool var memPool = RPCClient.GetMemPool(); Console.WriteLine("Mempool contains " + memPool.Length + " transactions; processing..."); // record how long it takes to process the mempool var stopWatch2 = new Stopwatch(); stopWatch2.Start(); // process all mempool transactions foreach (var txid in memPool) { var rawTransaction = RPCClient.GetRawTransaction(txid); var transaction = new Transaction(rawTransaction) { FirstSeen = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), LastUpdated = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; // does this transaction contain an output we are watching? SubscriptionCheck.CheckForSubscription(transaction); } stopWatch2.Stop(); var elapsed2 = stopWatch2.Elapsed; Console.WriteLine("Processed mempool in " + $"{elapsed2.Hours:00}:{elapsed2.Minutes:00}:{elapsed2.Seconds:00}.{elapsed2.Milliseconds / 10:00}"); } else { Console.WriteLine("Skipping mempool scan on first run."); } }
private void HandleBlock(Block block) { // have we seen this block before? var blockSearch = _blocks.FindOne(x => x.BlockHash == block.BlockHash); if (blockSearch != null) { // yes; we've seen the block before. So, no need to handle it. return; } // is this the first block we've stored? blockSearch = _blocks.FindOne(x => x.IsChainTip); if (blockSearch == null) { // yes; so, just store it without PrevHash check // use JSON RPC to fetch block height var blockHeight = Program.RPCClient.GetBlockHeight(block.BlockHash); block.Height = blockHeight; // mark as best known chain tip _chainTipHash = block.BlockHash; block.IsChainTip = true; // save to db _blocks.Insert(block); return; } // no; so, check if we have the previous block in the chain blockSearch = _blocks.FindOne(x => x.BlockHash == block.Header.PrevBlockHash); if (blockSearch == null) { // we are missing the prev block; this shouldn't happen... // queue blocks backwards until we find a prev-block we have, and track the missing blocks var missingBlocks = new List <Block> { block }; var prevBlockHash = block.Header.PrevBlockHash; // loop until we have already have the previous block while (!_blocks.Exists(x => x.BlockHash == prevBlockHash)) { var prevBlockData = Program.RPCClient.GetBlockData(block.Header.PrevBlockHash); var prevBlock = new Block(ByteToHex.StringToByteArray(prevBlockData)); missingBlocks.Add(prevBlock); prevBlockHash = prevBlock.Header.PrevBlockHash; } // missingBlocks is now an ordered list of blocks we are missing - queue them in reverse missingBlocks.Reverse(); foreach (var missingBlock in missingBlocks) { Program.Database.EnqueueTask(new DatabaseWrite(missingBlock), 0); } // discard (don't save) this block, as it will be re-processed in order return; } // we already have the previous block in the chain... // so, this new block's height is prevblock height + 1 block.Height = blockSearch.Height + 1; var chainTipBlock = _blocks.FindOne(x => x.IsChainTip); // check if the prevHash block is our chaintip if (_chainTipHash != block.Header.PrevBlockHash) { // no; so, there was a re-org! // we need to invalidate transaction inclusions, back to the forking block var orphanedBlock = chainTipBlock; var newChainBlock = block; var orphanedBlocks = new List <Block> { orphanedBlock }; var newChainBlocks = new List <Block> { newChainBlock }; // step backwards on each chain in turn until the two sides of the fork are at the same height while (orphanedBlock.Height > newChainBlock.Height) { orphanedBlock = _blocks.FindOne(x => x.BlockHash == orphanedBlock.Header.PrevBlockHash); orphanedBlocks.Add(orphanedBlock); } while (orphanedBlock.Height < newChainBlock.Height) { newChainBlock = _blocks.FindOne(x => x.BlockHash == newChainBlock.Header.PrevBlockHash); newChainBlocks.Add(newChainBlock); } // orphaned chain and new chain are the same height now // step back both chains at the same time until we have a matching prevBlockHash while (orphanedBlock.Header.PrevBlockHash != newChainBlock.Header.PrevBlockHash) { orphanedBlock = _blocks.FindOne(x => x.BlockHash == orphanedBlock.Header.PrevBlockHash); orphanedBlocks.Add(orphanedBlock); newChainBlock = _blocks.FindOne(x => x.BlockHash == newChainBlock.Header.PrevBlockHash); newChainBlocks.Add(newChainBlock); } // prevBlockHash is now the forking block; // roll-back transaction inclusions var transactions = _transactions.Find(x => x.IncludedAtBlockHeight >= orphanedBlock.Height); foreach (var transaction in transactions) { transaction.IncludedAtBlockHeight = 0; transaction.IncludedInBlockHex = ""; } // mark all blocks on the orphaned side as orphaned, and vice-versa foreach (var blk in orphanedBlocks) { blk.Orphaned = true; } // this is needed in the case of re-re-orgs foreach (var blk in newChainBlocks) { blk.Orphaned = false; } // we need to re-scan transactions in higher blocks // (skip the transactions in this block itself, as they will be queued behind this insert) // for most re-orgs, this won't actually have anything to process foreach (var blk in newChainBlocks.Where(x => x.BlockHash != block.BlockHash)) { // check all transactions in the block foreach (var transaction in block.Transactions) { SubscriptionCheck.CheckForSubscription(transaction); } } // re-org is handled, and this is the new chaintip, so fall thru to insert the block normally } // this is a regular block insert - we have previous block, and the previous block is our last known chaintip chainTipBlock.IsChainTip = false; _blocks.Update(chainTipBlock); block.IsChainTip = true; _chainTipHash = block.BlockHash; // save to db _blocks.Insert(block); }