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()