/ test / functional / wallet_reorgsrestore.py
wallet_reorgsrestore.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2019-2022 The Bitcoin Core developers
  3  # Distributed under the MIT software license, see the accompanying
  4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
  5  
  6  """Test tx status in case of reorgs while wallet being shutdown.
  7  
  8  Wallet txn status rely on block connection/disconnection for its
  9  accuracy. In case of reorgs happening while wallet being shutdown
 10  block updates are not going to be received. At wallet loading, we
 11  check against chain if confirmed txn are still in chain and change
 12  their status if block in which they have been included has been
 13  disconnected.
 14  """
 15  
 16  from decimal import Decimal
 17  import shutil
 18  
 19  from test_framework.test_framework import BitcoinTestFramework
 20  from test_framework.util import (
 21          assert_equal,
 22  )
 23  
 24  class ReorgsRestoreTest(BitcoinTestFramework):
 25      def add_options(self, parser):
 26          self.add_wallet_options(parser)
 27  
 28      def set_test_params(self):
 29          self.num_nodes = 3
 30  
 31      def skip_test_if_missing_module(self):
 32          self.skip_if_no_wallet()
 33  
 34      def run_test(self):
 35          # Send a tx from which to conflict outputs later
 36          txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
 37          self.generate(self.nodes[0], 1)
 38  
 39          # Disconnect node1 from others to reorg its chain later
 40          self.disconnect_nodes(0, 1)
 41          self.disconnect_nodes(1, 2)
 42          self.connect_nodes(0, 2)
 43  
 44          # Send a tx to be unconfirmed later
 45          txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
 46          tx = self.nodes[0].gettransaction(txid)
 47          self.generate(self.nodes[0], 4, sync_fun=self.no_op)
 48          self.sync_blocks([self.nodes[0], self.nodes[2]])
 49          tx_before_reorg = self.nodes[0].gettransaction(txid)
 50          assert_equal(tx_before_reorg["confirmations"], 4)
 51  
 52          # Disconnect node0 from node2 to broadcast a conflict on their respective chains
 53          self.disconnect_nodes(0, 2)
 54          nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10"))
 55          inputs = []
 56          inputs.append({"txid": txid_conflict_from, "vout": nA})
 57          outputs_1 = {}
 58          outputs_2 = {}
 59  
 60          # Create a conflicted tx broadcast on node0 chain and conflicting tx broadcast on node1 chain. Both spend from txid_conflict_from
 61          outputs_1[self.nodes[0].getnewaddress()] = Decimal("9.99998")
 62          outputs_2[self.nodes[0].getnewaddress()] = Decimal("9.99998")
 63          conflicted = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_1))
 64          conflicting = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_2))
 65  
 66          conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"])
 67          self.generate(self.nodes[0], 1, sync_fun=self.no_op)
 68          conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"])
 69          self.generate(self.nodes[2], 9, sync_fun=self.no_op)
 70  
 71          # Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted
 72          self.connect_nodes(0, 2)
 73          self.sync_blocks([self.nodes[0], self.nodes[2]])
 74          conflicted = self.nodes[0].gettransaction(conflicted_txid)
 75          conflicting = self.nodes[0].gettransaction(conflicting_txid)
 76          assert_equal(conflicted["confirmations"], -9)
 77          assert_equal(conflicted["walletconflicts"][0], conflicting["txid"])
 78  
 79          # Node0 wallet is shutdown
 80          self.restart_node(0)
 81  
 82          # The block chain re-orgs and the tx is included in a different block
 83          self.generate(self.nodes[1], 9, sync_fun=self.no_op)
 84          self.nodes[1].sendrawtransaction(tx["hex"])
 85          self.generate(self.nodes[1], 1, sync_fun=self.no_op)
 86          self.nodes[1].sendrawtransaction(conflicted["hex"])
 87          self.generate(self.nodes[1], 1, sync_fun=self.no_op)
 88  
 89          # Node0 wallet file is loaded on longest sync'ed node1
 90          self.stop_node(1)
 91          self.nodes[0].backupwallet(self.nodes[0].datadir_path / 'wallet.bak')
 92          shutil.copyfile(self.nodes[0].datadir_path / 'wallet.bak', self.nodes[1].chain_path / self.default_wallet_name / self.wallet_data_filename)
 93          self.start_node(1)
 94          tx_after_reorg = self.nodes[1].gettransaction(txid)
 95          # Check that normal confirmed tx is confirmed again but with different blockhash
 96          assert_equal(tx_after_reorg["confirmations"], 2)
 97          assert tx_before_reorg["blockhash"] != tx_after_reorg["blockhash"]
 98          conflicted_after_reorg = self.nodes[1].gettransaction(conflicted_txid)
 99          # Check that conflicted tx is confirmed again with blockhash different than previously conflicting tx
100          assert_equal(conflicted_after_reorg["confirmations"], 1)
101          assert conflicting["blockhash"] != conflicted_after_reorg["blockhash"]
102  
103  if __name__ == '__main__':
104      ReorgsRestoreTest().main()