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