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