wallet_conflicts.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2023 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 6 """ 7 Test that wallet correctly tracks transactions that have been conflicted by blocks, particularly during reorgs. 8 """ 9 10 from decimal import Decimal 11 12 from test_framework.test_framework import BitcoinTestFramework 13 from test_framework.util import ( 14 assert_equal, 15 ) 16 17 class TxConflicts(BitcoinTestFramework): 18 def set_test_params(self): 19 self.num_nodes = 3 20 21 def skip_test_if_missing_module(self): 22 self.skip_if_no_wallet() 23 24 def get_utxo_of_value(self, from_tx_id, search_value): 25 return next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(from_tx_id)["details"] if tx_out["amount"] == Decimal(f"{search_value}")) 26 27 def run_test(self): 28 """ 29 The following tests check the behavior of the wallet when 30 transaction conflicts are created. These conflicts are created 31 using raw transaction RPCs that double-spend UTXOs and have more 32 fees, replacing the original transaction. 33 """ 34 35 self.test_block_conflicts() 36 self.test_mempool_conflict() 37 self.test_mempool_and_block_conflicts() 38 self.test_descendants_with_mempool_conflicts() 39 40 def test_block_conflicts(self): 41 self.log.info("Send tx from which to conflict outputs later") 42 txid_conflict_from_1 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) 43 txid_conflict_from_2 = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) 44 self.generate(self.nodes[0], 1) 45 self.sync_blocks() 46 47 self.log.info("Disconnect nodes to broadcast conflicts on their respective chains") 48 self.disconnect_nodes(0, 1) 49 self.disconnect_nodes(2, 1) 50 51 self.log.info("Create transactions that conflict with each other") 52 output_A = self.get_utxo_of_value(from_tx_id=txid_conflict_from_1, search_value=10) 53 output_B = self.get_utxo_of_value(from_tx_id=txid_conflict_from_2, search_value=10) 54 55 # First create a transaction that consumes both A and B outputs. 56 # 57 # | tx1 | -----> | | | | 58 # | AB_parent_tx | ----> | Child_Tx | 59 # | tx2 | -----> | | | | 60 # 61 inputs_tx_AB_parent = [{"txid": txid_conflict_from_1, "vout": output_A}, {"txid": txid_conflict_from_2, "vout": output_B}] 62 tx_AB_parent = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_AB_parent, {self.nodes[0].getnewaddress(): Decimal("19.99998")})) 63 64 # Secondly, create two transactions: One consuming output_A, and another one consuming output_B 65 # 66 # | tx1 | -----> | Tx_A_1 | 67 # ---------------- 68 # | tx2 | -----> | Tx_B_1 | 69 # 70 inputs_tx_A_1 = [{"txid": txid_conflict_from_1, "vout": output_A}] 71 inputs_tx_B_1 = [{"txid": txid_conflict_from_2, "vout": output_B}] 72 tx_A_1 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_A_1, {self.nodes[0].getnewaddress(): Decimal("9.99998")})) 73 tx_B_1 = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_B_1, {self.nodes[0].getnewaddress(): Decimal("9.99998")})) 74 75 self.log.info("Broadcast conflicted transaction") 76 txid_AB_parent = self.nodes[0].sendrawtransaction(tx_AB_parent["hex"]) 77 self.generate(self.nodes[0], 1, sync_fun=self.no_op) 78 79 # Now that 'AB_parent_tx' was broadcast, build 'Child_Tx' 80 output_c = self.get_utxo_of_value(from_tx_id=txid_AB_parent, search_value=19.99998) 81 inputs_tx_C_child = [({"txid": txid_AB_parent, "vout": output_c})] 82 83 tx_C_child = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs_tx_C_child, {self.nodes[0].getnewaddress() : Decimal("19.99996")})) 84 tx_C_child_txid = self.nodes[0].sendrawtransaction(tx_C_child["hex"]) 85 self.generate(self.nodes[0], 1, sync_fun=self.no_op) 86 87 self.log.info("Broadcast conflicting tx to node 1 and generate a longer chain") 88 conflicting_txid_A = self.nodes[1].sendrawtransaction(tx_A_1["hex"]) 89 self.generate(self.nodes[1], 4, sync_fun=self.no_op) 90 conflicting_txid_B = self.nodes[1].sendrawtransaction(tx_B_1["hex"]) 91 self.generate(self.nodes[1], 4, sync_fun=self.no_op) 92 93 self.log.info("Connect nodes 0 and 1, trigger reorg and ensure that the tx is effectively conflicted") 94 self.connect_nodes(0, 1) 95 self.sync_blocks([self.nodes[0], self.nodes[1]]) 96 conflicted_AB_tx = self.nodes[0].gettransaction(txid_AB_parent) 97 tx_C_child = self.nodes[0].gettransaction(tx_C_child_txid) 98 conflicted_A_tx = self.nodes[0].gettransaction(conflicting_txid_A) 99 100 self.log.info("Verify, after the reorg, that Tx_A was accepted, and tx_AB and its Child_Tx are conflicting now") 101 # Tx A was accepted, Tx AB was not. 102 assert conflicted_AB_tx["confirmations"] < 0 103 assert conflicted_A_tx["confirmations"] > 0 104 105 # Conflicted tx should have confirmations set to the confirmations of the most conflicting tx 106 assert_equal(-conflicted_AB_tx["confirmations"], conflicted_A_tx["confirmations"]) 107 # Child should inherit conflicted state from parent 108 assert_equal(-tx_C_child["confirmations"], conflicted_A_tx["confirmations"]) 109 # Check the confirmations of the conflicting transactions 110 assert_equal(conflicted_A_tx["confirmations"], 8) 111 assert_equal(self.nodes[0].gettransaction(conflicting_txid_B)["confirmations"], 4) 112 113 self.log.info("Now generate a longer chain that does not contain any tx") 114 # Node2 chain without conflicts 115 self.generate(self.nodes[2], 15, sync_fun=self.no_op) 116 117 # Connect node0 and node2 and wait reorg 118 self.connect_nodes(0, 2) 119 self.sync_blocks() 120 conflicted = self.nodes[0].gettransaction(txid_AB_parent) 121 tx_C_child = self.nodes[0].gettransaction(tx_C_child_txid) 122 123 self.log.info("Test that formerly conflicted transaction are inactive after reorg") 124 # Former conflicted tx should be unconfirmed as it hasn't been yet rebroadcast 125 assert_equal(conflicted["confirmations"], 0) 126 # Former conflicted child tx should be unconfirmed as it hasn't been rebroadcast 127 assert_equal(tx_C_child["confirmations"], 0) 128 # Rebroadcast former conflicted tx and check it confirms smoothly 129 self.nodes[2].sendrawtransaction(conflicted["hex"]) 130 self.generate(self.nodes[2], 1) 131 self.sync_blocks() 132 former_conflicted = self.nodes[0].gettransaction(txid_AB_parent) 133 assert_equal(former_conflicted["confirmations"], 1) 134 assert_equal(former_conflicted["blockheight"], 217) 135 136 def test_mempool_conflict(self): 137 self.nodes[0].createwallet("alice") 138 alice = self.nodes[0].get_wallet_rpc("alice") 139 140 bob = self.nodes[1] 141 142 self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(3)]) 143 self.generate(self.nodes[2], 1) 144 145 self.log.info("Test a scenario where a transaction has a mempool conflict") 146 147 unspents = alice.listunspent() 148 assert_equal(len(unspents), 3) 149 assert all([tx["amount"] == 25 for tx in unspents]) 150 151 # tx1 spends unspent[0] and unspent[1] 152 raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[1]], outputs=[{bob.getnewaddress() : 49.9999}]) 153 tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex'] 154 155 # tx2 spends unspent[1] and unspent[2], conflicts with tx1 156 raw_tx = alice.createrawtransaction(inputs=[unspents[1], unspents[2]], outputs=[{bob.getnewaddress() : 49.99}]) 157 tx2 = alice.signrawtransactionwithwallet(raw_tx)['hex'] 158 159 # tx3 spends unspent[2], conflicts with tx2 160 raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{bob.getnewaddress() : 24.9899}]) 161 tx3 = alice.signrawtransactionwithwallet(raw_tx)['hex'] 162 163 # broadcast tx1 164 tx1_txid = alice.sendrawtransaction(tx1) 165 166 assert_equal(alice.listunspent(), [unspents[2]]) 167 assert_equal(alice.getbalance(), 25) 168 169 # broadcast tx2, replaces tx1 in mempool 170 tx2_txid = alice.sendrawtransaction(tx2) 171 172 # Check that unspent[0] is now available because the transaction spending it has been replaced in the mempool 173 assert_equal(alice.listunspent(), [unspents[0]]) 174 assert_equal(alice.getbalance(), 25) 175 176 assert_equal(alice.gettransaction(tx1_txid)["mempoolconflicts"], [tx2_txid]) 177 178 self.log.info("Test scenario where a mempool conflict is removed") 179 180 # broadcast tx3, replaces tx2 in mempool 181 # Now that tx1's conflict has been removed, tx1 is now 182 # not conflicted, and instead is inactive until it is 183 # rebroadcasted. Now unspent[0] is not available, because 184 # tx1 is no longer conflicted. 185 alice.sendrawtransaction(tx3) 186 187 assert_equal(alice.gettransaction(tx1_txid)["mempoolconflicts"], []) 188 assert tx1_txid not in self.nodes[0].getrawmempool() 189 190 # now all of alice's outputs should be considered spent 191 # unspent[0]: spent by inactive tx1 192 # unspent[1]: spent by inactive tx1 193 # unspent[2]: spent by active tx3 194 assert_equal(alice.listunspent(), []) 195 assert_equal(alice.getbalance(), 0) 196 197 # Clean up for next test 198 bob.sendall([self.nodes[2].getnewaddress()]) 199 self.generate(self.nodes[2], 1) 200 201 alice.unloadwallet() 202 203 def test_mempool_and_block_conflicts(self): 204 self.nodes[0].createwallet("alice_2") 205 alice = self.nodes[0].get_wallet_rpc("alice_2") 206 bob = self.nodes[1] 207 208 self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(3)]) 209 self.generate(self.nodes[2], 1) 210 211 self.log.info("Test a scenario where a transaction has both a block conflict and a mempool conflict") 212 unspents = [{"txid" : element["txid"], "vout" : element["vout"]} for element in alice.listunspent()] 213 214 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 215 216 # alice and bob nodes are disconnected so that transactions can be 217 # created by alice, but broadcasted from bob so that alice's wallet 218 # doesn't know about them 219 self.disconnect_nodes(0, 1) 220 221 # Sends funds to bob 222 raw_tx = alice.createrawtransaction(inputs=[unspents[0]], outputs=[{bob.getnewaddress() : 24.99999}]) 223 raw_tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex'] 224 tx1_txid = bob.sendrawtransaction(raw_tx1) # broadcast original tx spending unspents[0] only to bob 225 226 # create a conflict to previous tx (also spends unspents[0]), but don't broadcast, sends funds back to alice 227 raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[2]], outputs=[{alice.getnewaddress() : 49.999}]) 228 tx1_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex'] 229 230 # Sends funds to bob 231 raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{bob.getnewaddress() : 24.9999}]) 232 raw_tx2 = alice.signrawtransactionwithwallet(raw_tx)['hex'] 233 tx2_txid = bob.sendrawtransaction(raw_tx2) # broadcast another original tx spending unspents[1] only to bob 234 235 # create a conflict to previous tx (also spends unspents[1]), but don't broadcast, sends funds to alice 236 raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{alice.getnewaddress() : 24.9999}]) 237 tx2_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex'] 238 239 bob_unspents = [{"txid" : element, "vout" : 0} for element in [tx1_txid, tx2_txid]] 240 241 # tx1 and tx2 are now in bob's mempool, and they are unconflicted, so bob has these funds 242 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99989000")) 243 244 # spend both of bob's unspents, child tx of tx1 and tx2 245 raw_tx = bob.createrawtransaction(inputs=[bob_unspents[0], bob_unspents[1]], outputs=[{bob.getnewaddress() : 49.999}]) 246 raw_tx3 = bob.signrawtransactionwithwallet(raw_tx)['hex'] 247 tx3_txid = bob.sendrawtransaction(raw_tx3) # broadcast tx only to bob 248 249 # alice knows about 0 txs, bob knows about 3 250 assert_equal(len(alice.getrawmempool()), 0) 251 assert_equal(len(bob.getrawmempool()), 3) 252 253 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99900000")) 254 255 # bob broadcasts tx_1 conflict 256 tx1_conflict_txid = bob.sendrawtransaction(tx1_conflict) 257 assert_equal(len(alice.getrawmempool()), 0) 258 assert_equal(len(bob.getrawmempool()), 2) # tx1_conflict kicks out both tx1, and its child tx3 259 260 assert tx2_txid in bob.getrawmempool() 261 assert tx1_conflict_txid in bob.getrawmempool() 262 263 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [tx1_conflict_txid]) 264 assert_equal(bob.gettransaction(tx2_txid)["mempoolconflicts"], []) 265 assert_equal(bob.gettransaction(tx3_txid)["mempoolconflicts"], [tx1_conflict_txid]) 266 267 # check that tx3 is now conflicted, so the output from tx2 can now be spent 268 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000")) 269 270 # we will be disconnecting this block in the future 271 alice.sendrawtransaction(tx2_conflict) 272 assert_equal(len(alice.getrawmempool()), 1) # currently alice's mempool is only aware of tx2_conflict 273 # 11 blocks are mined so that when they are invalidated, tx_2 274 # does not get put back into the mempool 275 blk = self.generate(self.nodes[0], 11, sync_fun=self.no_op)[0] 276 assert_equal(len(alice.getrawmempool()), 0) # tx2_conflict is now mined 277 278 self.connect_nodes(0, 1) 279 self.sync_blocks() 280 assert_equal(alice.getbestblockhash(), bob.getbestblockhash()) 281 282 # now that tx2 has a block conflict, tx1_conflict should be the only tx in bob's mempool 283 assert tx1_conflict_txid in bob.getrawmempool() 284 assert_equal(len(bob.getrawmempool()), 1) 285 286 # tx3 should now also be block-conflicted by tx2_conflict 287 assert_equal(bob.gettransaction(tx3_txid)["confirmations"], -11) 288 # bob has no pending funds, since tx1, tx2, and tx3 are all conflicted 289 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 290 bob.invalidateblock(blk) # remove tx2_conflict 291 # bob should still have no pending funds because tx1 and tx3 are still conflicted, and tx2 has not been re-broadcast 292 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 293 assert_equal(len(bob.getrawmempool()), 1) 294 # check that tx3 is no longer block-conflicted 295 assert_equal(bob.gettransaction(tx3_txid)["confirmations"], 0) 296 297 bob.sendrawtransaction(raw_tx2) 298 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000")) 299 300 # create a conflict to previous tx (also spends unspents[2]), but don't broadcast, sends funds back to alice 301 raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{alice.getnewaddress() : 24.99}]) 302 tx1_conflict_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex'] 303 304 bob.sendrawtransaction(tx1_conflict_conflict) # kick tx1_conflict out of the mempool 305 bob.sendrawtransaction(raw_tx1) #re-broadcast tx1 because it is no longer conflicted 306 307 # Now bob has no pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet 308 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 309 310 bob.sendrawtransaction(raw_tx3) 311 assert_equal(len(bob.getrawmempool()), 4) # The mempool contains: tx1, tx2, tx1_conflict_conflict, tx3 312 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99900000")) 313 314 # Clean up for next test 315 bob.reconsiderblock(blk) 316 assert_equal(alice.getbestblockhash(), bob.getbestblockhash()) 317 self.sync_mempools() 318 self.generate(self.nodes[2], 1) 319 320 alice.unloadwallet() 321 322 def test_descendants_with_mempool_conflicts(self): 323 self.nodes[0].createwallet("alice_3") 324 alice = self.nodes[0].get_wallet_rpc("alice_3") 325 326 self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(2)]) 327 self.generate(self.nodes[2], 1) 328 329 self.nodes[1].createwallet("bob_1") 330 bob = self.nodes[1].get_wallet_rpc("bob_1") 331 332 self.nodes[2].createwallet("carol") 333 carol = self.nodes[2].get_wallet_rpc("carol") 334 335 self.log.info("Test a scenario where a transaction's parent has a mempool conflict") 336 337 unspents = alice.listunspent() 338 assert_equal(len(unspents), 2) 339 assert all([tx["amount"] == 25 for tx in unspents]) 340 341 assert_equal(alice.getrawmempool(), []) 342 343 # Alice spends first utxo to bob in tx1 344 raw_tx = alice.createrawtransaction(inputs=[unspents[0]], outputs=[{bob.getnewaddress() : 24.9999}]) 345 tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex'] 346 tx1_txid = alice.sendrawtransaction(tx1) 347 348 self.sync_mempools() 349 350 assert_equal(alice.getbalance(), 25) 351 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000")) 352 353 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], []) 354 355 raw_tx = bob.createrawtransaction(inputs=[bob.listunspent(minconf=0)[0]], outputs=[{carol.getnewaddress() : 24.999}]) 356 # Bob creates a child to tx1 357 tx1_child = bob.signrawtransactionwithwallet(raw_tx)['hex'] 358 tx1_child_txid = bob.sendrawtransaction(tx1_child) 359 360 self.sync_mempools() 361 362 # Currently neither tx1 nor tx1_child should have any conflicts 363 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], []) 364 assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], []) 365 assert tx1_txid in bob.getrawmempool() 366 assert tx1_child_txid in bob.getrawmempool() 367 assert_equal(len(bob.getrawmempool()), 2) 368 369 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 370 assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("24.99900000")) 371 372 # Alice spends first unspent again, conflicting with tx1 373 raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[1]], outputs=[{carol.getnewaddress() : 49.99}]) 374 tx1_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex'] 375 tx1_conflict_txid = alice.sendrawtransaction(tx1_conflict) 376 377 self.sync_mempools() 378 379 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 380 assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("49.99000000")) 381 382 assert tx1_txid not in bob.getrawmempool() 383 assert tx1_child_txid not in bob.getrawmempool() 384 assert tx1_conflict_txid in bob.getrawmempool() 385 assert_equal(len(bob.getrawmempool()), 1) 386 387 # Now both tx1 and tx1_child are conflicted by tx1_conflict 388 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [tx1_conflict_txid]) 389 assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [tx1_conflict_txid]) 390 391 # Now create a conflict to tx1_conflict, so that it gets kicked out of the mempool 392 raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{carol.getnewaddress() : 24.9895}]) 393 tx1_conflict_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex'] 394 tx1_conflict_conflict_txid = alice.sendrawtransaction(tx1_conflict_conflict) 395 396 self.sync_mempools() 397 398 # Now that tx1_conflict has been removed, both tx1 and tx1_child 399 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], []) 400 assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], []) 401 402 # Both tx1 and tx1_child are still not in the mempool because they have not be re-broadcasted 403 assert tx1_txid not in bob.getrawmempool() 404 assert tx1_child_txid not in bob.getrawmempool() 405 assert tx1_conflict_txid not in bob.getrawmempool() 406 assert tx1_conflict_conflict_txid in bob.getrawmempool() 407 assert_equal(len(bob.getrawmempool()), 1) 408 409 assert_equal(alice.getbalance(), 0) 410 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) 411 assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("24.98950000")) 412 413 # Both tx1 and tx1_child can now be re-broadcasted 414 bob.sendrawtransaction(tx1) 415 bob.sendrawtransaction(tx1_child) 416 assert_equal(len(bob.getrawmempool()), 3) 417 418 alice.unloadwallet() 419 bob.unloadwallet() 420 carol.unloadwallet() 421 422 if __name__ == '__main__': 423 TxConflicts(__file__).main()