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