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