public void TestReplayBlock() { using (var simulator = new MainnetSimulator()) { simulator.AddBlockRange(0, 9999); simulator.WaitForUpdate(); using (var chainState = simulator.CoreDaemon.GetChainState()) { Assert.AreEqual(9999, chainState.Chain.Height); for (var blockHeight = 0; blockHeight <= chainState.Chain.Height; blockHeight++) { var blockHash = chainState.Chain.Blocks[blockHeight].Hash; var expectedTransactions = simulator.BlockProvider.GetBlock(blockHeight).Transactions; var actualTransactions = BlockReplayer.ReplayBlock(simulator.CoreDaemon.CoreStorage, chainState, blockHash, replayForward: true) .ToEnumerable().ToList(); CollectionAssert.AreEqual( expectedTransactions.Select(x => x.Hash).ToList(), actualTransactions.Select(x => x.Transaction.Hash).ToList(), $"Transactions differ at block {blockHeight:N0}"); } } } }
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 ReplayBlockExample() { // create example core daemon BlockProvider embeddedBlocks; IStorageManager storageManager; using (var coreDaemon = CreateExampleDaemon(out embeddedBlocks, out storageManager, maxHeight: 999)) using (embeddedBlocks) using (storageManager) { // start a chain at the genesis block to represent the processed progress var processedChain = Chain.CreateForGenesisBlock(coreDaemon.ChainParams.GenesisChainedHeader).ToBuilder(); // a dictionary of public key script hashes can be created for any addresses of interest, allowing for quick checking var scriptHashesOfInterest = new HashSet <UInt256>(); // retrieve a chainstate to replay blocks with using (var chainState = coreDaemon.GetChainState()) { // enumerate the steps needed to take the currently processed chain towards the current chainstate foreach (var pathElement in processedChain.NavigateTowards(chainState.Chain)) { // retrieve the next block to replay and whether to replay forwards, or backwards for a re-org var replayForward = pathElement.Item1 > 0; var replayBlock = pathElement.Item2; // begin replaying the transactions in the replay block // if this is a re-org, the transactions will be replayed in reverse block order var replayTxes = BlockReplayer.ReplayBlock(coreDaemon.CoreStorage, chainState, replayBlock.Hash, replayForward); // prepare the tx scanner var txScanner = new ActionBlock <ValidatableTx>( validatableTx => { // the transaction being replayed var tx = validatableTx.Transaction; // the previous tx outputs for each of the replay transaction's inputs var prevTxOutputs = validatableTx.PrevTxOutputs; // scan the replay transaction's inputs if (!validatableTx.IsCoinbase) { for (var inputIndex = 0; inputIndex < tx.Inputs.Length; inputIndex++) { var input = tx.Inputs[inputIndex]; var inputPrevTxOutput = validatableTx.PrevTxOutputs[inputIndex]; // check if the input's previous transaction output is of interest var inputPrevTxOutputPublicScriptHash = new UInt256(SHA256Static.ComputeHash(inputPrevTxOutput.ScriptPublicKey)); if (scriptHashesOfInterest.Contains(inputPrevTxOutputPublicScriptHash)) { if (replayForward) { /* An output for an address of interest is being spent. */ } else { /* An output for an address of interest is being "unspent", on re-org. */ } } } } // scan the replay transaction's outputs for (var outputIndex = 0; outputIndex < tx.Outputs.Length; outputIndex++) { var output = tx.Outputs[outputIndex]; // check if the output is of interest var outputPublicScriptHash = new UInt256(SHA256Static.ComputeHash(output.ScriptPublicKey)); if (scriptHashesOfInterest.Contains(outputPublicScriptHash)) { if (replayForward) { /* An output for an address of interest is being minted. */ } else { /* An output for an address of interest is being "unminted", on re-org. */ } } } }); // hook up and wait for the tx scanner replayTxes.LinkTo(txScanner, new DataflowLinkOptions { PropagateCompletion = true }); txScanner.Completion.Wait(); // a wallet would now commit its progress /* * walletDatabase.CurrentBlock = replayBlock.Hash; * walletDatabase.Commit(); */ // TODO: after successfully committing, a wallet would notify CoreDaemon of its current progress // TODO: CoreDaemon will use this information in order to determine how far in the current chainstate it is safe to prune // TODO: with this in place, if a wallet suffers a failure to commit it can just replay the block // TODO: wallets can also remain disconnected from CoreDaemon, and just replay blocks to catch up when they are reconnected // update the processed chain so that the next step towards the current chainstate can be taken if (replayForward) { processedChain.AddBlock(replayBlock); } else { processedChain.RemoveBlock(replayBlock); } } } logger.Info("Processed chain height: {0:N0}", processedChain.Height); } }
public void TestReplayBlockRollback() { using (var daemon = new TestDaemon()) { // create a new keypair to spend to var toKeyPair = daemon.TxManager.CreateKeyPair(); var toPrivateKey = toKeyPair.Item1; var toPublicKey = toKeyPair.Item2; // add block 1 var block0 = daemon.GenesisBlock; var block1 = daemon.MineAndAddEmptyBlock(); // add some blocks so coinbase is mature to spend Block lastBlock = null; for (var i = 0; i < 100; i++) { lastBlock = daemon.MineAndAddEmptyBlock(); } // add block 2, spending from block 1 var spendTx1 = daemon.TxManager.CreateSpendTransaction(block1.Transactions[0], 0, (byte)ScriptHashType.SIGHASH_ALL, block1.Transactions[0].OutputValue(), daemon.CoinbasePrivateKey, daemon.CoinbasePublicKey, toPublicKey); var block2Unmined = daemon.CreateEmptyBlock(lastBlock.Hash) .CreateWithAddedTransactions(spendTx1); var block2 = daemon.MineAndAddBlock(block2Unmined); // add some blocks so coinbase is mature to spend for (var i = 0; i < 100; i++) { lastBlock = daemon.MineAndAddEmptyBlock(); } // add block 3, spending from block 2 var spendTx2 = daemon.TxManager.CreateSpendTransaction(block2.Transactions[1], 0, (byte)ScriptHashType.SIGHASH_ALL, block2.Transactions[1].OutputValue(), toPrivateKey, toPublicKey, toPublicKey); var block3Unmined = daemon.CreateEmptyBlock(lastBlock.Hash) .CreateWithAddedTransactions(spendTx2); var block3 = daemon.MineAndAddBlock(block3Unmined); // replay all blocks up to block 3 daemon.WaitForUpdate(); using (var chainState = daemon.CoreDaemon.GetChainState()) { Assert.AreEqual(203, chainState.Chain.Height); var replayTransactions = new List <ValidatableTx>(); foreach (var blockHash in chainState.Chain.Blocks.Select(x => x.Hash)) { replayTransactions.AddRange(BlockReplayer.ReplayBlock(daemon.CoreStorage, chainState, blockHash, replayForward: true) .ToEnumerable()); } // verify all transactions were replayed Assert.AreEqual(206, replayTransactions.Count); Assert.AreEqual(block0.Transactions[0].Hash, replayTransactions[0].Transaction.Hash); Assert.AreEqual(block1.Transactions[0].Hash, replayTransactions[1].Transaction.Hash); Assert.AreEqual(block2.Transactions[0].Hash, replayTransactions[102].Transaction.Hash); Assert.AreEqual(block2.Transactions[1].Hash, replayTransactions[103].Transaction.Hash); Assert.AreEqual(block3.Transactions[0].Hash, replayTransactions[204].Transaction.Hash); Assert.AreEqual(block3.Transactions[1].Hash, replayTransactions[205].Transaction.Hash); } // mark block 2 invalid, it will be rolled back daemon.CoreStorage.MarkBlockInvalid(block2.Hash, daemon.CoreDaemon.TargetChain); daemon.WaitForUpdate(); // replay rollback of block 3 using (var chainState = daemon.CoreDaemon.GetChainState()) { Assert.AreEqual(101, chainState.Chain.Height); var replayTransactions = new List <ValidatableTx>( BlockReplayer.ReplayBlock(daemon.CoreStorage, chainState, block3.Hash, replayForward: false) .ToEnumerable()); // verify transactions were replayed Assert.AreEqual(2, replayTransactions.Count); Assert.AreEqual(block3.Transactions[1].Hash, replayTransactions[0].Transaction.Hash); Assert.AreEqual(block3.Transactions[0].Hash, replayTransactions[1].Transaction.Hash); // verify correct previous output was replayed (block 3 tx 1 spent block 2 tx 1) Assert.AreEqual(1, replayTransactions[0].PrevTxOutputs.Length); CollectionAssert.AreEqual(block2.Transactions[1].Outputs[0].ScriptPublicKey, replayTransactions[0].PrevTxOutputs[0].ScriptPublicKey); // verify correct previous output was replayed (block 3 tx 0 spends nothing, coinbase) Assert.AreEqual(0, replayTransactions[1].PrevTxOutputs.Length); } // replay rollback of block 2 using (var chainState = daemon.CoreDaemon.GetChainState()) { Assert.AreEqual(101, chainState.Chain.Height); var replayTransactions = new List <ValidatableTx>( BlockReplayer.ReplayBlock(daemon.CoreStorage, chainState, block2.Hash, replayForward: false) .ToEnumerable()); // verify transactions were replayed Assert.AreEqual(2, replayTransactions.Count); Assert.AreEqual(block2.Transactions[1].Hash, replayTransactions[0].Transaction.Hash); Assert.AreEqual(block2.Transactions[0].Hash, replayTransactions[1].Transaction.Hash); // verify correct previous output was replayed (block 2 tx 1 spent block 1 tx 0) Assert.AreEqual(1, replayTransactions[0].PrevTxOutputs.Length); CollectionAssert.AreEqual(block1.Transactions[0].Outputs[0].ScriptPublicKey, replayTransactions[0].PrevTxOutputs[0].ScriptPublicKey); // verify correct previous output was replayed (block 2 tx 0 spends nothing, coinbase) Assert.AreEqual(0, replayTransactions[1].PrevTxOutputs.Length); } } }