9
9
10
10
from decimal import Decimal
11
11
12
+ from test_framework .blocktools import COINBASE_MATURITY
12
13
from test_framework .test_framework import BitcoinTestFramework
13
14
from test_framework .util import (
14
15
assert_equal ,
@@ -28,6 +29,20 @@ def get_utxo_of_value(self, from_tx_id, search_value):
28
29
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 } " ))
29
30
30
31
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 ):
31
46
self .log .info ("Send tx from which to conflict outputs later" )
32
47
txid_conflict_from_1 = self .nodes [0 ].sendtoaddress (self .nodes [0 ].getnewaddress (), Decimal ("10" ))
33
48
txid_conflict_from_2 = self .nodes [0 ].sendtoaddress (self .nodes [0 ].getnewaddress (), Decimal ("10" ))
@@ -123,5 +138,272 @@ def run_test(self):
123
138
assert_equal (former_conflicted ["confirmations" ], 1 )
124
139
assert_equal (former_conflicted ["blockheight" ], 217 )
125
140
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
+
126
408
if __name__ == '__main__' :
127
409
TxConflicts ().main ()
0 commit comments