wallet_transactiontime_rescan.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2018-2022 The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test transaction time during old block rescanning
   6  """
   7  
   8  import concurrent.futures
   9  import time
  10  
  11  from test_framework.authproxy import JSONRPCException
  12  from test_framework.blocktools import COINBASE_MATURITY
  13  from test_framework.test_framework import LimenkaTestFramework
  14  from test_framework.util import (
  15      assert_equal,
  16      assert_raises_rpc_error,
  17      set_node_times,
  18  )
  19  from test_framework.wallet_util import (
  20      get_generate_key,
  21  )
  22  
  23  
  24  class TransactionTimeRescanTest(LimenkaTestFramework):
  25      def add_options(self, parser):
  26          self.add_wallet_options(parser)
  27  
  28      def set_test_params(self):
  29          self.setup_clean_chain = False
  30          self.num_nodes = 3
  31          self.extra_args = [["-keypool=400"],
  32                             ["-keypool=400"],
  33                             []
  34                            ]
  35  
  36      def skip_test_if_missing_module(self):
  37          self.skip_if_no_wallet()
  38  
  39      def run_test(self):
  40          self.log.info('Prepare nodes and wallet')
  41  
  42          minernode = self.nodes[0]  # node used to mine BTC and create transactions
  43          usernode = self.nodes[1]  # user node with correct time
  44          restorenode = self.nodes[2]  # node used to restore user wallet and check time determination in ComputeSmartTime (wallet.cpp)
  45  
  46          # time constant
  47          cur_time = int(time.time())
  48          ten_days = 10 * 24 * 60 * 60
  49  
  50          # synchronize nodes and time
  51          self.sync_all()
  52          set_node_times(self.nodes, cur_time)
  53  
  54          # prepare miner wallet
  55          minernode.createwallet(wallet_name='default')
  56          miner_wallet = minernode.get_wallet_rpc('default')
  57          m1 = miner_wallet.getnewaddress()
  58  
  59          # prepare the user wallet with 3 watch only addresses
  60          wo1 = usernode.getnewaddress()
  61          wo2 = usernode.getnewaddress()
  62          wo3 = usernode.getnewaddress()
  63  
  64          usernode.createwallet(wallet_name='wo', disable_private_keys=True)
  65          wo_wallet = usernode.get_wallet_rpc('wo')
  66  
  67          wo_wallet.importaddress(wo1)
  68          wo_wallet.importaddress(wo2)
  69          wo_wallet.importaddress(wo3)
  70  
  71          self.log.info('Start transactions')
  72  
  73          # check blockcount
  74          assert_equal(minernode.getblockcount(), 200)
  75  
  76          # generate some btc to create transactions and check blockcount
  77          initial_mine = COINBASE_MATURITY + 1
  78          self.generatetoaddress(minernode, initial_mine, m1)
  79          assert_equal(minernode.getblockcount(), initial_mine + 200)
  80  
  81          # synchronize nodes and time
  82          self.sync_all()
  83          set_node_times(self.nodes, cur_time + ten_days)
  84          # send 10 btc to user's first watch-only address
  85          self.log.info('Send 10 btc to user')
  86          miner_wallet.sendtoaddress(wo1, 10)
  87  
  88          # generate blocks and check blockcount
  89          self.generatetoaddress(minernode, COINBASE_MATURITY, m1)
  90          assert_equal(minernode.getblockcount(), initial_mine + 300)
  91  
  92          # synchronize nodes and time
  93          self.sync_all()
  94          set_node_times(self.nodes, cur_time + ten_days + ten_days)
  95          # send 5 btc to our second watch-only address
  96          self.log.info('Send 5 btc to user')
  97          miner_wallet.sendtoaddress(wo2, 5)
  98  
  99          # generate blocks and check blockcount
 100          self.generatetoaddress(minernode, COINBASE_MATURITY, m1)
 101          assert_equal(minernode.getblockcount(), initial_mine + 400)
 102  
 103          # synchronize nodes and time
 104          self.sync_all()
 105          set_node_times(self.nodes, cur_time + ten_days + ten_days + ten_days)
 106          # send 1 btc to our third watch-only address
 107          self.log.info('Send 1 btc to user')
 108          miner_wallet.sendtoaddress(wo3, 1)
 109  
 110          # generate more blocks and check blockcount
 111          self.generatetoaddress(minernode, COINBASE_MATURITY, m1)
 112          assert_equal(minernode.getblockcount(), initial_mine + 500)
 113  
 114          self.log.info('Check user\'s final balance and transaction count')
 115          assert_equal(wo_wallet.getbalance(), 16)
 116          assert_equal(len(wo_wallet.listtransactions()), 3)
 117  
 118          self.log.info('Check transaction times')
 119          for tx in wo_wallet.listtransactions():
 120              if tx['address'] == wo1:
 121                  assert_equal(tx['blocktime'], cur_time + ten_days)
 122                  assert_equal(tx['time'], cur_time + ten_days)
 123              elif tx['address'] == wo2:
 124                  assert_equal(tx['blocktime'], cur_time + ten_days + ten_days)
 125                  assert_equal(tx['time'], cur_time + ten_days + ten_days)
 126              elif tx['address'] == wo3:
 127                  assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days)
 128                  assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days)
 129  
 130          # restore user wallet without rescan
 131          self.log.info('Restore user wallet on another node without rescan')
 132          restorenode.createwallet(wallet_name='wo', disable_private_keys=True)
 133          restorewo_wallet = restorenode.get_wallet_rpc('wo')
 134  
 135          # for descriptor wallets, the test framework maps the importaddress RPC to the
 136          # importdescriptors RPC (with argument 'timestamp'='now'), which always rescans
 137          # blocks of the past 2 hours, based on the current MTP timestamp; in order to avoid
 138          # importing the last address (wo3), we advance the time further and generate 10 blocks
 139          if self.options.descriptors:
 140              set_node_times(self.nodes, cur_time + ten_days + ten_days + ten_days + ten_days)
 141              self.generatetoaddress(minernode, 10, m1)
 142  
 143          restorewo_wallet.importaddress(wo1, rescan=False)
 144          restorewo_wallet.importaddress(wo2, rescan=False)
 145          restorewo_wallet.importaddress(wo3, rescan=False)
 146  
 147          # check user has 0 balance and no transactions
 148          assert_equal(restorewo_wallet.getbalance(), 0)
 149          assert_equal(len(restorewo_wallet.listtransactions()), 0)
 150  
 151          # proceed to rescan, first with an incomplete one, then with a full rescan
 152          self.log.info('Rescan last history part')
 153          restorewo_wallet.rescanblockchain(initial_mine + 350)
 154          self.log.info('Rescan all history')
 155          restorewo_wallet.rescanblockchain()
 156  
 157          self.log.info('Check user\'s final balance and transaction count after restoration')
 158          assert_equal(restorewo_wallet.getbalance(), 16)
 159          assert_equal(len(restorewo_wallet.listtransactions()), 3)
 160  
 161          self.log.info('Check transaction times after restoration')
 162          for tx in restorewo_wallet.listtransactions():
 163              if tx['address'] == wo1:
 164                  assert_equal(tx['blocktime'], cur_time + ten_days)
 165                  assert_equal(tx['time'], cur_time + ten_days)
 166              elif tx['address'] == wo2:
 167                  assert_equal(tx['blocktime'], cur_time + ten_days + ten_days)
 168                  assert_equal(tx['time'], cur_time + ten_days + ten_days)
 169              elif tx['address'] == wo3:
 170                  assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days)
 171                  assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days)
 172  
 173  
 174          self.log.info('Test handling of invalid parameters for rescanblockchain')
 175          assert_raises_rpc_error(-8, "Invalid start_height", restorewo_wallet.rescanblockchain, -1, 10)
 176          assert_raises_rpc_error(-8, "Invalid stop_height", restorewo_wallet.rescanblockchain, 1, -1)
 177          assert_raises_rpc_error(-8, "stop_height must be greater than start_height", restorewo_wallet.rescanblockchain, 20, 10)
 178  
 179          self.log.info("Test `rescanblockchain` fails when wallet is encrypted and locked")
 180          usernode.createwallet(wallet_name="enc_wallet", passphrase="passphrase")
 181          enc_wallet = usernode.get_wallet_rpc("enc_wallet")
 182          assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", enc_wallet.rescanblockchain)
 183  
 184          if not self.options.descriptors:
 185              self.log.info("Test rescanning an encrypted wallet")
 186              hd_seed = get_generate_key().privkey
 187  
 188              usernode.createwallet(wallet_name="temp_wallet", blank=True, descriptors=False)
 189              temp_wallet = usernode.get_wallet_rpc("temp_wallet")
 190              temp_wallet.sethdseed(seed=hd_seed)
 191  
 192              for i in range(399):
 193                  temp_wallet.getnewaddress()
 194  
 195              self.generatetoaddress(usernode, COINBASE_MATURITY + 1, temp_wallet.getnewaddress())
 196              self.generatetoaddress(usernode, COINBASE_MATURITY + 1, temp_wallet.getnewaddress())
 197  
 198              minernode.createwallet("encrypted_wallet", blank=True, passphrase="passphrase", descriptors=False)
 199              encrypted_wallet = minernode.get_wallet_rpc("encrypted_wallet")
 200  
 201              encrypted_wallet.walletpassphrase("passphrase", 99999)
 202              encrypted_wallet.sethdseed(seed=hd_seed)
 203  
 204              with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
 205                  with minernode.assert_debug_log(expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"], timeout=5):
 206                      rescanning = thread.submit(encrypted_wallet.rescanblockchain)
 207  
 208                  # set the passphrase timeout to 1 to test that the wallet remains unlocked during the rescan
 209                  minernode.cli("-rpcwallet=encrypted_wallet").walletpassphrase("passphrase", 1)
 210  
 211                  try:
 212                      minernode.cli("-rpcwallet=encrypted_wallet").walletlock()
 213                  except JSONRPCException as e:
 214                      assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet." in e.error["message"]
 215  
 216                  try:
 217                      minernode.cli("-rpcwallet=encrypted_wallet").walletpassphrasechange("passphrase", "newpassphrase")
 218                  except JSONRPCException as e:
 219                      assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase." in e.error["message"]
 220  
 221                  assert_equal(rescanning.result(), {"start_height": 0, "stop_height": 803})
 222  
 223              assert_equal(encrypted_wallet.getbalance(), temp_wallet.getbalance())
 224  
 225  if __name__ == '__main__':
 226      TransactionTimeRescanTest(__file__).main()
 227