1 #!/usr/bin/env python3
2 # Copyright (c) 2023-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 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 for tx in unspents:
150 assert_equal(tx["amount"], 25)
151 152 # tx1 spends unspent[0] and unspent[1]
153 raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[1]], outputs=[{bob.getnewaddress() : 49.9999}])
154 tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex']
155 156 # tx2 spends unspent[1] and unspent[2], conflicts with tx1
157 raw_tx = alice.createrawtransaction(inputs=[unspents[1], unspents[2]], outputs=[{bob.getnewaddress() : 49.99}])
158 tx2 = alice.signrawtransactionwithwallet(raw_tx)['hex']
159 160 # tx3 spends unspent[2], conflicts with tx2
161 raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{bob.getnewaddress() : 24.9899}])
162 tx3 = alice.signrawtransactionwithwallet(raw_tx)['hex']
163 164 # broadcast tx1
165 tx1_txid = alice.sendrawtransaction(tx1)
166 167 assert_equal(alice.listunspent(), [unspents[2]])
168 assert_equal(alice.getbalance(), 25)
169 170 # broadcast tx2, replaces tx1 in mempool
171 tx2_txid = alice.sendrawtransaction(tx2)
172 173 # Check that unspent[0] is now available because the transaction spending it has been replaced in the mempool
174 assert_equal(alice.listunspent(), [unspents[0]])
175 assert_equal(alice.getbalance(), 25)
176 177 assert_equal(alice.gettransaction(tx1_txid)["mempoolconflicts"], [tx2_txid])
178 179 self.log.info("Test scenario where a mempool conflict is removed")
180 181 # broadcast tx3, replaces tx2 in mempool
182 # Now that tx1's conflict has been removed, tx1 is now
183 # not conflicted, and instead is inactive until it is
184 # rebroadcasted. Now unspent[0] is not available, because
185 # tx1 is no longer conflicted.
186 alice.sendrawtransaction(tx3)
187 188 assert_equal(alice.gettransaction(tx1_txid)["mempoolconflicts"], [])
189 assert tx1_txid not in self.nodes[0].getrawmempool()
190 191 # now all of alice's outputs should be considered spent
192 # unspent[0]: spent by inactive tx1
193 # unspent[1]: spent by inactive tx1
194 # unspent[2]: spent by active tx3
195 assert_equal(alice.listunspent(), [])
196 assert_equal(alice.getbalance(), 0)
197 198 # Clean up for next test
199 bob.sendall([self.nodes[2].getnewaddress()])
200 self.generate(self.nodes[2], 1)
201 202 alice.unloadwallet()
203 204 def test_mempool_and_block_conflicts(self):
205 self.nodes[0].createwallet("alice_2")
206 alice = self.nodes[0].get_wallet_rpc("alice_2")
207 bob = self.nodes[1]
208 209 self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(3)])
210 self.generate(self.nodes[2], 1)
211 212 self.log.info("Test a scenario where a transaction has both a block conflict and a mempool conflict")
213 unspents = [{"txid" : element["txid"], "vout" : element["vout"]} for element in alice.listunspent()]
214 215 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
216 217 # alice and bob nodes are disconnected so that transactions can be
218 # created by alice, but broadcasted from bob so that alice's wallet
219 # doesn't know about them
220 self.disconnect_nodes(0, 1)
221 222 # Sends funds to bob
223 raw_tx = alice.createrawtransaction(inputs=[unspents[0]], outputs=[{bob.getnewaddress() : 24.99999}])
224 raw_tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex']
225 tx1_txid = bob.sendrawtransaction(raw_tx1) # broadcast original tx spending unspents[0] only to bob
226 227 # create a conflict to previous tx (also spends unspents[0]), but don't broadcast, sends funds back to alice
228 raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[2]], outputs=[{alice.getnewaddress() : 49.999}])
229 tx1_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
230 231 # Sends funds to bob
232 raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{bob.getnewaddress() : 24.9999}])
233 raw_tx2 = alice.signrawtransactionwithwallet(raw_tx)['hex']
234 tx2_txid = bob.sendrawtransaction(raw_tx2) # broadcast another original tx spending unspents[1] only to bob
235 236 # create a conflict to previous tx (also spends unspents[1]), but don't broadcast, sends funds to alice
237 raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{alice.getnewaddress() : 24.9999}])
238 tx2_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
239 240 bob_unspents = [{"txid" : element, "vout" : 0} for element in [tx1_txid, tx2_txid]]
241 242 # tx1 and tx2 are now in bob's mempool, and they are unconflicted, so bob has these funds
243 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99989000"))
244 245 # spend both of bob's unspents, child tx of tx1 and tx2
246 raw_tx = bob.createrawtransaction(inputs=[bob_unspents[0], bob_unspents[1]], outputs=[{bob.getnewaddress() : 49.999}])
247 raw_tx3 = bob.signrawtransactionwithwallet(raw_tx)['hex']
248 tx3_txid = bob.sendrawtransaction(raw_tx3) # broadcast tx only to bob
249 250 # alice knows about 0 txs, bob knows about 3
251 assert_equal(len(alice.getrawmempool()), 0)
252 assert_equal(len(bob.getrawmempool()), 3)
253 254 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99900000"))
255 256 # bob broadcasts tx_1 conflict
257 tx1_conflict_txid = bob.sendrawtransaction(tx1_conflict)
258 assert_equal(len(alice.getrawmempool()), 0)
259 assert_equal(len(bob.getrawmempool()), 2) # tx1_conflict kicks out both tx1, and its child tx3
260 261 assert tx2_txid in bob.getrawmempool()
262 assert tx1_conflict_txid in bob.getrawmempool()
263 264 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [tx1_conflict_txid])
265 assert_equal(bob.gettransaction(tx2_txid)["mempoolconflicts"], [])
266 assert_equal(bob.gettransaction(tx3_txid)["mempoolconflicts"], [tx1_conflict_txid])
267 268 # check that tx3 is now conflicted, so the output from tx2 can now be spent
269 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
270 271 # we will be disconnecting this block in the future
272 alice.sendrawtransaction(tx2_conflict)
273 assert_equal(len(alice.getrawmempool()), 1) # currently alice's mempool is only aware of tx2_conflict
274 # 11 blocks are mined so that when they are invalidated, tx_2
275 # does not get put back into the mempool
276 blk = self.generate(self.nodes[0], 11, sync_fun=self.no_op)[0]
277 assert_equal(len(alice.getrawmempool()), 0) # tx2_conflict is now mined
278 279 self.connect_nodes(0, 1)
280 self.sync_blocks()
281 assert_equal(alice.getbestblockhash(), bob.getbestblockhash())
282 283 # now that tx2 has a block conflict, tx1_conflict should be the only tx in bob's mempool
284 assert tx1_conflict_txid in bob.getrawmempool()
285 assert_equal(len(bob.getrawmempool()), 1)
286 287 # tx3 should now also be block-conflicted by tx2_conflict
288 assert_equal(bob.gettransaction(tx3_txid)["confirmations"], -11)
289 # bob has no pending funds, since tx1, tx2, and tx3 are all conflicted
290 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
291 bob.invalidateblock(blk) # remove tx2_conflict
292 # bob should still have no pending funds because tx1 and tx3 are still conflicted, and tx2 has not been re-broadcast
293 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
294 assert_equal(len(bob.getrawmempool()), 1)
295 # check that tx3 is no longer block-conflicted
296 assert_equal(bob.gettransaction(tx3_txid)["confirmations"], 0)
297 298 bob.sendrawtransaction(raw_tx2)
299 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
300 301 # create a conflict to previous tx (also spends unspents[2]), but don't broadcast, sends funds back to alice
302 raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{alice.getnewaddress() : 24.99}])
303 tx1_conflict_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
304 305 bob.sendrawtransaction(tx1_conflict_conflict) # kick tx1_conflict out of the mempool
306 bob.sendrawtransaction(raw_tx1) #re-broadcast tx1 because it is no longer conflicted
307 308 # Now bob has pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet
309 bob_bal = bob.getbalances()["mine"]
310 assert_equal(bob_bal["untrusted_pending"], -bob_bal["nonmempool"])
311 312 bob.sendrawtransaction(raw_tx3)
313 assert_equal(len(bob.getrawmempool()), 4) # The mempool contains: tx1, tx2, tx1_conflict_conflict, tx3
314 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("49.99900000"))
315 316 # Clean up for next test
317 bob.reconsiderblock(blk)
318 assert_equal(alice.getbestblockhash(), bob.getbestblockhash())
319 self.sync_mempools()
320 self.generate(self.nodes[2], 1)
321 322 alice.unloadwallet()
323 324 def test_descendants_with_mempool_conflicts(self):
325 self.nodes[0].createwallet("alice_3")
326 alice = self.nodes[0].get_wallet_rpc("alice_3")
327 328 self.nodes[2].send(outputs=[{alice.getnewaddress() : 25} for _ in range(2)])
329 self.generate(self.nodes[2], 1)
330 331 self.nodes[1].createwallet("bob_1")
332 bob = self.nodes[1].get_wallet_rpc("bob_1")
333 334 self.nodes[2].createwallet("carol")
335 carol = self.nodes[2].get_wallet_rpc("carol")
336 337 self.log.info("Test a scenario where a transaction's parent has a mempool conflict")
338 339 unspents = alice.listunspent()
340 assert_equal(len(unspents), 2)
341 for tx in unspents:
342 assert_equal(tx["amount"], 25)
343 344 assert_equal(alice.getrawmempool(), [])
345 346 # Alice spends first utxo to bob in tx1
347 raw_tx = alice.createrawtransaction(inputs=[unspents[0]], outputs=[{bob.getnewaddress() : 24.9999}])
348 tx1 = alice.signrawtransactionwithwallet(raw_tx)['hex']
349 tx1_txid = alice.sendrawtransaction(tx1)
350 351 self.sync_mempools()
352 353 assert_equal(alice.getbalance(), 25)
354 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
355 356 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [])
357 358 raw_tx = bob.createrawtransaction(inputs=[bob.listunspent(minconf=0)[0]], outputs=[{carol.getnewaddress() : 24.999}])
359 # Bob creates a child to tx1
360 tx1_child = bob.signrawtransactionwithwallet(raw_tx)['hex']
361 tx1_child_txid = bob.sendrawtransaction(tx1_child)
362 363 self.sync_mempools()
364 365 # Currently neither tx1 nor tx1_child should have any conflicts
366 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [])
367 assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [])
368 assert tx1_txid in bob.getrawmempool()
369 assert tx1_child_txid in bob.getrawmempool()
370 assert_equal(len(bob.getrawmempool()), 2)
371 372 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
373 assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("24.99900000"))
374 375 # Alice spends first unspent again, conflicting with tx1
376 raw_tx = alice.createrawtransaction(inputs=[unspents[0], unspents[1]], outputs=[{carol.getnewaddress() : 49.99}])
377 tx1_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
378 tx1_conflict_txid = alice.sendrawtransaction(tx1_conflict)
379 380 self.sync_mempools()
381 382 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
383 assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("49.99000000"))
384 385 assert tx1_txid not in bob.getrawmempool()
386 assert tx1_child_txid not in bob.getrawmempool()
387 assert tx1_conflict_txid in bob.getrawmempool()
388 assert_equal(len(bob.getrawmempool()), 1)
389 390 # Now both tx1 and tx1_child are conflicted by tx1_conflict
391 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [tx1_conflict_txid])
392 assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [tx1_conflict_txid])
393 394 # Now create a conflict to tx1_conflict, so that it gets kicked out of the mempool
395 raw_tx = alice.createrawtransaction(inputs=[unspents[1]], outputs=[{carol.getnewaddress() : 24.9895}])
396 tx1_conflict_conflict = alice.signrawtransactionwithwallet(raw_tx)['hex']
397 tx1_conflict_conflict_txid = alice.sendrawtransaction(tx1_conflict_conflict)
398 399 self.sync_mempools()
400 401 # Now that tx1_conflict has been removed, both tx1 and tx1_child
402 assert_equal(bob.gettransaction(tx1_txid)["mempoolconflicts"], [])
403 assert_equal(bob.gettransaction(tx1_child_txid)["mempoolconflicts"], [])
404 405 # Both tx1 and tx1_child are still not in the mempool because they have not be re-broadcasted
406 assert tx1_txid not in bob.getrawmempool()
407 assert tx1_child_txid not in bob.getrawmempool()
408 assert tx1_conflict_txid not in bob.getrawmempool()
409 assert tx1_conflict_conflict_txid in bob.getrawmempool()
410 assert_equal(len(bob.getrawmempool()), 1)
411 412 assert_equal(alice.getbalance(), 0)
413 assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
414 assert_equal(carol.getbalances()["mine"]["untrusted_pending"], Decimal("24.98950000"))
415 416 # Both tx1 and tx1_child can now be re-broadcasted
417 bob.sendrawtransaction(tx1)
418 bob.sendrawtransaction(tx1_child)
419 assert_equal(len(bob.getrawmempool()), 3)
420 421 alice.unloadwallet()
422 bob.unloadwallet()
423 carol.unloadwallet()
424 425 if __name__ == '__main__':
426 TxConflicts(__file__).main()
427