wallet_listsinceblock.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2017-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 the listsinceblock RPC."""
   6  
   7  from test_framework.address import key_to_p2wpkh
   8  from test_framework.blocktools import COINBASE_MATURITY
   9  from test_framework.descriptors import descsum_create
  10  from test_framework.test_framework import LimenkaTestFramework
  11  from test_framework.messages import MAX_BIP125_RBF_SEQUENCE
  12  from test_framework.util import (
  13      assert_array_result,
  14      assert_equal,
  15      assert_raises_rpc_error,
  16  )
  17  from test_framework.wallet_util import generate_keypair
  18  
  19  from decimal import Decimal
  20  
  21  class ListSinceBlockTest(LimenkaTestFramework):
  22      def add_options(self, parser):
  23          self.add_wallet_options(parser)
  24  
  25      def set_test_params(self):
  26          self.num_nodes = 4
  27          self.setup_clean_chain = True
  28          # whitelist peers to speed up tx relay / mempool sync
  29          self.noban_tx_relay = True
  30  
  31      def skip_test_if_missing_module(self):
  32          self.skip_if_no_wallet()
  33  
  34      def run_test(self):
  35          # All nodes are in IBD from genesis, so they'll need the miner (node2) to be an outbound connection, or have
  36          # only one connection. (See fPreferredDownload in net_processing)
  37          self.connect_nodes(1, 2)
  38          self.generate(self.nodes[2], COINBASE_MATURITY + 1)
  39  
  40          self.test_no_blockhash()
  41          self.test_invalid_blockhash()
  42          self.test_reorg()
  43          self.test_cant_read_block()
  44          self.test_double_spend()
  45          self.test_double_send()
  46          self.double_spends_filtered()
  47          self.test_targetconfirmations()
  48          if self.options.descriptors:
  49              self.test_desc()
  50          self.test_send_to_self()
  51          self.test_op_return()
  52          self.test_label()
  53  
  54      def test_no_blockhash(self):
  55          self.log.info("Test no blockhash")
  56          txid = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1)
  57          self.sync_all()
  58          assert_array_result(self.nodes[0].listtransactions(), {"txid": txid}, {
  59              "category": "receive",
  60              "amount": 1,
  61              "confirmations": 0,
  62              "trusted": False,
  63          })
  64  
  65          blockhash, = self.generate(self.nodes[2], 1)
  66          blockheight = self.nodes[2].getblockheader(blockhash)['height']
  67  
  68          txs = self.nodes[0].listtransactions()
  69          assert_array_result(txs, {"txid": txid}, {
  70              "category": "receive",
  71              "amount": 1,
  72              "blockhash": blockhash,
  73              "blockheight": blockheight,
  74              "confirmations": 1,
  75          })
  76          assert_equal(len(txs), 1)
  77          assert "trusted" not in txs[0]
  78  
  79          assert_equal(
  80              self.nodes[0].listsinceblock(),
  81              {"lastblock": blockhash,
  82               "removed": [],
  83               "transactions": txs})
  84          assert_equal(
  85              self.nodes[0].listsinceblock(""),
  86              {"lastblock": blockhash,
  87               "removed": [],
  88               "transactions": txs})
  89  
  90      def test_invalid_blockhash(self):
  91          self.log.info("Test invalid blockhash")
  92          assert_raises_rpc_error(-5, "Block not found", self.nodes[0].listsinceblock,
  93                                  "42759cde25462784395a337460bde75f58e73d3f08bd31fdc3507cbac856a2c4")
  94          assert_raises_rpc_error(-5, "Block not found", self.nodes[0].listsinceblock,
  95                                  "0000000000000000000000000000000000000000000000000000000000000000")
  96          assert_raises_rpc_error(-8, "blockhash must be of length 64 (not 11, for 'invalid-hex')", self.nodes[0].listsinceblock,
  97                                  "invalid-hex")
  98          assert_raises_rpc_error(-8, "blockhash must be hexadecimal string (not 'Z000000000000000000000000000000000000000000000000000000000000000')", self.nodes[0].listsinceblock,
  99                                  "Z000000000000000000000000000000000000000000000000000000000000000")
 100  
 101      def test_targetconfirmations(self):
 102          '''
 103          This tests when the value of target_confirmations exceeds the number of
 104          blocks in the main chain. In this case, the genesis block hash should be
 105          given for the `lastblock` property. If target_confirmations is < 1, then
 106          a -8 invalid parameter error is thrown.
 107          '''
 108          self.log.info("Test target_confirmations")
 109          blockhash, = self.generate(self.nodes[2], 1)
 110          blockheight = self.nodes[2].getblockheader(blockhash)['height']
 111  
 112          assert_equal(
 113              self.nodes[0].getblockhash(0),
 114              self.nodes[0].listsinceblock(blockhash, blockheight + 1)['lastblock'])
 115          assert_equal(
 116              self.nodes[0].getblockhash(0),
 117              self.nodes[0].listsinceblock(blockhash, blockheight + 1000)['lastblock'])
 118          assert_raises_rpc_error(-8, "Invalid parameter",
 119              self.nodes[0].listsinceblock, blockhash, 0)
 120  
 121      def test_reorg(self):
 122          '''
 123          `listsinceblock` did not behave correctly when handed a block that was
 124          no longer in the main chain:
 125  
 126               ab0
 127            /       \
 128          aa1 [tx0]   bb1
 129           |           |
 130          aa2         bb2
 131           |           |
 132          aa3         bb3
 133                       |
 134                      bb4
 135  
 136          Consider a client that has only seen block `aa3` above. It asks the node
 137          to `listsinceblock aa3`. But at some point prior the main chain switched
 138          to the bb chain.
 139  
 140          Previously: listsinceblock would find height=4 for block aa3 and compare
 141          this to height=5 for the tip of the chain (bb4). It would then return
 142          results restricted to bb3-bb4.
 143  
 144          Now: listsinceblock finds the fork at ab0 and returns results in the
 145          range bb1-bb4.
 146  
 147          This test only checks that [tx0] is present.
 148          '''
 149          self.log.info("Test reorg")
 150  
 151          # Split network into two
 152          self.split_network()
 153  
 154          # send to nodes[0] from nodes[2]
 155          senttx = self.nodes[2].sendtoaddress(self.nodes[0].getnewaddress(), 1)
 156  
 157          # generate on both sides
 158          nodes1_last_blockhash = self.generate(self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1]
 159          nodes2_first_blockhash = self.generate(self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0]
 160          self.log.debug("nodes[1] last blockhash = {}".format(nodes1_last_blockhash))
 161          self.log.debug("nodes[2] first blockhash = {}".format(nodes2_first_blockhash))
 162  
 163          self.join_network()
 164  
 165          # listsinceblock(nodes1_last_blockhash) should now include tx as seen from nodes[0]
 166          # and return the block height which listsinceblock now exposes since a5e7795.
 167          transactions = self.nodes[0].listsinceblock(nodes1_last_blockhash)['transactions']
 168          found = next(tx for tx in transactions if tx['txid'] == senttx)
 169          assert_equal(found['blockheight'], self.nodes[0].getblockheader(nodes2_first_blockhash)['height'])
 170  
 171      def test_cant_read_block(self):
 172          self.log.info('Test the RPC error "Can\'t read block from disk"')
 173  
 174          # Split network into two
 175          self.split_network()
 176  
 177          # generate on both sides
 178          nodes1_last_blockhash = self.generate(self.nodes[1], 6, sync_fun=lambda: self.sync_all(self.nodes[:2]))[-1]
 179          self.generate(self.nodes[2], 7, sync_fun=lambda: self.sync_all(self.nodes[2:]))[0]
 180  
 181          self.join_network()
 182  
 183          # Renaming the block file to induce unsuccessful block read
 184          blk_dat = (self.nodes[0].blocks_path / "blk00000.dat")
 185          blk_dat_moved = blk_dat.rename(self.nodes[0].blocks_path / "blk00000.dat.moved")
 186          assert not blk_dat.exists()
 187  
 188          # listsinceblock(nodes1_last_blockhash) should now fail as blocks are not accessible
 189          assert_raises_rpc_error(-32603, "Can't read block from disk",
 190              self.nodes[0].listsinceblock, nodes1_last_blockhash)
 191  
 192          # Restoring block file
 193          blk_dat_moved.rename(self.nodes[0].blocks_path / "blk00000.dat")
 194          assert blk_dat.exists()
 195  
 196      def test_double_spend(self):
 197          '''
 198          This tests the case where the same UTXO is spent twice on two separate
 199          blocks as part of a reorg.
 200  
 201               ab0
 202            /       \
 203          aa1 [tx1]   bb1 [tx2]
 204           |           |
 205          aa2         bb2
 206           |           |
 207          aa3         bb3
 208                       |
 209                      bb4
 210  
 211          Problematic case:
 212  
 213          1. User 1 receives BTC in tx1 from utxo1 in block aa1.
 214          2. User 2 receives BTC in tx2 from utxo1 (same) in block bb1
 215          3. User 1 sees 2 confirmations at block aa3.
 216          4. Reorg into bb chain.
 217          5. User 1 asks `listsinceblock aa3` and does not see that tx1 is now
 218             invalidated.
 219  
 220          Currently the solution to this is to detect that a reorg'd block is
 221          asked for in listsinceblock, and to iterate back over existing blocks up
 222          until the fork point, and to include all transactions that relate to the
 223          node wallet.
 224          '''
 225          self.log.info("Test double spend")
 226  
 227          self.sync_all()
 228  
 229          # share utxo between nodes[1] and nodes[2]
 230          privkey, pubkey = generate_keypair(wif=True)
 231          address = key_to_p2wpkh(pubkey)
 232          self.nodes[2].sendtoaddress(address, 10)
 233          self.generate(self.nodes[2], 6)
 234          self.nodes[2].importprivkey(privkey)
 235          utxos = self.nodes[2].listunspent()
 236          utxo = [u for u in utxos if u["address"] == address][0]
 237          self.nodes[1].importprivkey(privkey)
 238  
 239          # Split network into two
 240          self.split_network()
 241  
 242          # send from nodes[1] using utxo to nodes[0]
 243          change = '%.8f' % (float(utxo['amount']) - 1.0003)
 244          recipient_dict = {
 245              self.nodes[0].getnewaddress(): 1,
 246              self.nodes[1].getnewaddress(): change,
 247          }
 248          utxo_dicts = [{
 249              'txid': utxo['txid'],
 250              'vout': utxo['vout'],
 251          }]
 252          txid1 = self.nodes[1].sendrawtransaction(
 253              self.nodes[1].signrawtransactionwithwallet(
 254                  self.nodes[1].createrawtransaction(utxo_dicts, recipient_dict))['hex'])
 255  
 256          # send from nodes[2] using utxo to nodes[3]
 257          recipient_dict2 = {
 258              self.nodes[3].getnewaddress(): 1,
 259              self.nodes[2].getnewaddress(): change,
 260          }
 261          self.nodes[2].sendrawtransaction(
 262              self.nodes[2].signrawtransactionwithwallet(
 263                  self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict2))['hex'])
 264  
 265          # generate on both sides
 266          lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2]
 267          self.generate(self.nodes[2], 4, sync_fun=self.no_op)
 268  
 269          self.join_network()
 270  
 271          self.sync_all()
 272  
 273          # gettransaction should work for txid1
 274          assert self.nodes[0].gettransaction(txid1)['txid'] == txid1, "gettransaction failed to find txid1"
 275  
 276          # listsinceblock(lastblockhash) should now include txid1, as seen from nodes[0]
 277          lsbres = self.nodes[0].listsinceblock(lastblockhash)
 278          assert any(tx['txid'] == txid1 for tx in lsbres['removed'])
 279  
 280          # but it should not include 'removed' if include_removed=false
 281          lsbres2 = self.nodes[0].listsinceblock(blockhash=lastblockhash, include_removed=False)
 282          assert 'removed' not in lsbres2
 283  
 284      def test_double_send(self):
 285          '''
 286          This tests the case where the same transaction is submitted twice on two
 287          separate blocks as part of a reorg. The former will vanish and the
 288          latter will appear as the true transaction (with confirmations dropping
 289          as a result).
 290  
 291               ab0
 292            /       \
 293          aa1 [tx1]   bb1
 294           |           |
 295          aa2         bb2
 296           |           |
 297          aa3         bb3 [tx1]
 298                       |
 299                      bb4
 300  
 301          Asserted:
 302  
 303          1. tx1 is listed in listsinceblock.
 304          2. It is included in 'removed' as it was removed, even though it is now
 305             present in a different block.
 306          3. It is listed with a confirmation count of 2 (bb3, bb4), not
 307             3 (aa1, aa2, aa3).
 308          '''
 309          self.log.info("Test double send")
 310  
 311          self.sync_all()
 312  
 313          # Split network into two
 314          self.split_network()
 315  
 316          # create and sign a transaction
 317          utxos = self.nodes[2].listunspent()
 318          utxo = utxos[0]
 319          change = '%.8f' % (float(utxo['amount']) - 1.0003)
 320          recipient_dict = {
 321              self.nodes[0].getnewaddress(): 1,
 322              self.nodes[2].getnewaddress(): change,
 323          }
 324          utxo_dicts = [{
 325              'txid': utxo['txid'],
 326              'vout': utxo['vout'],
 327          }]
 328          signedtxres = self.nodes[2].signrawtransactionwithwallet(
 329              self.nodes[2].createrawtransaction(utxo_dicts, recipient_dict))
 330          assert signedtxres['complete']
 331  
 332          signedtx = signedtxres['hex']
 333  
 334          # send from nodes[1]; this will end up in aa1
 335          txid1 = self.nodes[1].sendrawtransaction(signedtx)
 336  
 337          # generate bb1-bb2 on right side
 338          self.generate(self.nodes[2], 2, sync_fun=self.no_op)
 339  
 340          # send from nodes[2]; this will end up in bb3
 341          txid2 = self.nodes[2].sendrawtransaction(signedtx)
 342  
 343          assert_equal(txid1, txid2)
 344  
 345          # generate on both sides
 346          lastblockhash = self.generate(self.nodes[1], 3, sync_fun=self.no_op)[2]
 347          self.generate(self.nodes[2], 2, sync_fun=self.no_op)
 348  
 349          self.join_network()
 350  
 351          self.sync_all()
 352  
 353          # gettransaction should work for txid1
 354          tx1 = self.nodes[0].gettransaction(txid1)
 355          assert_equal(tx1['blockheight'], self.nodes[0].getblockheader(tx1['blockhash'])['height'])
 356  
 357          # listsinceblock(lastblockhash) should now include txid1 in transactions
 358          # as well as in removed
 359          lsbres = self.nodes[0].listsinceblock(lastblockhash)
 360          assert any(tx['txid'] == txid1 for tx in lsbres['transactions'])
 361          assert any(tx['txid'] == txid1 for tx in lsbres['removed'])
 362  
 363          # find transaction and ensure confirmations is valid
 364          for tx in lsbres['transactions']:
 365              if tx['txid'] == txid1:
 366                  assert_equal(tx['confirmations'], 2)
 367  
 368          # the same check for the removed array; confirmations should STILL be 2
 369          for tx in lsbres['removed']:
 370              if tx['txid'] == txid1:
 371                  assert_equal(tx['confirmations'], 2)
 372  
 373      def double_spends_filtered(self):
 374          '''
 375          `listsinceblock` was returning conflicted transactions even if they
 376          occurred before the specified cutoff blockhash
 377          '''
 378          self.log.info("Test spends filtered")
 379          spending_node = self.nodes[2]
 380          dest_address = spending_node.getnewaddress()
 381  
 382          tx_input = dict(
 383              sequence=MAX_BIP125_RBF_SEQUENCE, **next(u for u in spending_node.listunspent()))
 384          rawtx = spending_node.createrawtransaction(
 385              [tx_input], {dest_address: tx_input["amount"] - Decimal("0.00051000"),
 386                           spending_node.getrawchangeaddress(): Decimal("0.00050000")})
 387          signedtx = spending_node.signrawtransactionwithwallet(rawtx)
 388          orig_tx_id = spending_node.sendrawtransaction(signedtx["hex"])
 389          original_tx = spending_node.gettransaction(orig_tx_id)
 390  
 391          double_tx = spending_node.bumpfee(orig_tx_id)
 392  
 393          # check that both transactions exist
 394          block_hash = spending_node.listsinceblock(
 395              spending_node.getblockhash(spending_node.getblockcount()))
 396          original_found = False
 397          double_found = False
 398          for tx in block_hash['transactions']:
 399              if tx['txid'] == original_tx['txid']:
 400                  original_found = True
 401              if tx['txid'] == double_tx['txid']:
 402                  double_found = True
 403          assert_equal(original_found, True)
 404          assert_equal(double_found, True)
 405  
 406          lastblockhash = self.generate(spending_node, 1)[0]
 407  
 408          # check that neither transaction exists
 409          block_hash = spending_node.listsinceblock(lastblockhash)
 410          original_found = False
 411          double_found = False
 412          for tx in block_hash['transactions']:
 413              if tx['txid'] == original_tx['txid']:
 414                  original_found = True
 415              if tx['txid'] == double_tx['txid']:
 416                  double_found = True
 417          assert_equal(original_found, False)
 418          assert_equal(double_found, False)
 419  
 420      def test_desc(self):
 421          """Make sure we can track coins by descriptor."""
 422          self.log.info("Test descriptor lookup by scriptPubKey.")
 423  
 424          # Create a watchonly wallet tracking two multisig descriptors.
 425          multi_a = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YBNjUo96Jxd1u4XKWgnoc7LsA1jz3Yc2NiDbhtfBhaBtemB73n9V5vtJHwU6FVXwggTbeoJWQ1rzdz8ysDuQkpnaHyvnvzR/*,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*))")
 426          multi_b = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*,tpubD6NzVbkrYhZ4Y2RLiuEzNQkntjmsLpPYDm3LTRBYynUQtDtpzeUKAcb9sYthSFL3YR74cdFgF5mW8yKxv2W2CWuZDFR2dUpE5PF9kbrVXNZ/*))")
 427          self.nodes[0].createwallet(wallet_name="wo", descriptors=True, disable_private_keys=True)
 428          wo_wallet = self.nodes[0].get_wallet_rpc("wo")
 429          wo_wallet.importdescriptors([
 430              {
 431                  "desc": multi_a,
 432                  "active": False,
 433                  "timestamp": "now",
 434              },
 435              {
 436                  "desc": multi_b,
 437                  "active": False,
 438                  "timestamp": "now",
 439              },
 440          ])
 441  
 442          # Send a coin to each descriptor.
 443          assert_equal(len(wo_wallet.listsinceblock()["transactions"]), 0)
 444          addr_a = self.nodes[0].deriveaddresses(multi_a, 0)[0]
 445          addr_b = self.nodes[0].deriveaddresses(multi_b, 0)[0]
 446          self.nodes[2].sendtoaddress(addr_a, 1)
 447          self.nodes[2].sendtoaddress(addr_b, 2)
 448          self.generate(self.nodes[2], 1)
 449  
 450          # We can identify on which descriptor each coin was received.
 451          coins = wo_wallet.listsinceblock()["transactions"]
 452          assert_equal(len(coins), 2)
 453          coin_a = next(c for c in coins if c["amount"] == 1)
 454          assert_equal(coin_a["parent_descs"][0], multi_a)
 455          coin_b = next(c for c in coins if c["amount"] == 2)
 456          assert_equal(coin_b["parent_descs"][0], multi_b)
 457  
 458      def test_send_to_self(self):
 459          """We can make listsinceblock output our change outputs."""
 460          self.log.info("Test the inclusion of change outputs in the output.")
 461  
 462          # Create a UTxO paying to one of our change addresses.
 463          block_hash = self.nodes[2].getbestblockhash()
 464          addr = self.nodes[2].getrawchangeaddress()
 465          self.nodes[2].sendtoaddress(addr, 1)
 466  
 467          # If we don't list change, we won't have an entry for it.
 468          coins = self.nodes[2].listsinceblock(blockhash=block_hash)["transactions"]
 469          assert not any(c["address"] == addr for c in coins)
 470  
 471          # Now if we list change, we'll get both the send (to a change address) and
 472          # the actual change.
 473          res = self.nodes[2].listsinceblock(blockhash=block_hash, include_change=True)
 474          coins = [entry for entry in res["transactions"] if entry["category"] == "receive"]
 475          assert_equal(len(coins), 2)
 476          assert any(c["address"] == addr for c in coins)
 477          assert all(self.nodes[2].getaddressinfo(c["address"])["ischange"] for c in coins)
 478  
 479      def test_op_return(self):
 480          """Test if OP_RETURN outputs will be displayed correctly."""
 481          block_hash = self.nodes[2].getbestblockhash()
 482  
 483          raw_tx = self.nodes[2].createrawtransaction([], [{'data': 'aa'}])
 484          funded_tx = self.nodes[2].fundrawtransaction(raw_tx)
 485          signed_tx = self.nodes[2].signrawtransactionwithwallet(funded_tx['hex'])
 486          tx_id = self.nodes[2].sendrawtransaction(signed_tx['hex'])
 487  
 488          op_ret_tx = [tx for tx in self.nodes[2].listsinceblock(blockhash=block_hash)["transactions"] if tx['txid'] == tx_id][0]
 489  
 490          assert 'address' not in op_ret_tx
 491  
 492      def test_label(self):
 493          self.log.info('Test passing "label" argument fetches incoming transactions having the specified label')
 494          new_addr = self.nodes[1].getnewaddress(label="new_addr", address_type="bech32")
 495  
 496          self.nodes[2].sendtoaddress(address=new_addr, amount="0.001")
 497          self.generate(self.nodes[2], 1)
 498  
 499          for label in ["new_addr", ""]:
 500              new_addr_transactions = self.nodes[1].listsinceblock(label=label)["transactions"]
 501              assert_equal(len(new_addr_transactions), 1)
 502              assert_equal(new_addr_transactions[0]["label"], label)
 503              if label == "new_addr":
 504                  assert_equal(new_addr_transactions[0]["address"], new_addr)
 505  
 506  
 507  if __name__ == '__main__':
 508      ListSinceBlockTest(__file__).main()
 509