/ test / functional / wallet_conflicts.py
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()