wallet_v3_txs.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2025-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 """Test how the wallet deals with TRUC transactions"""
6
7 from decimal import Decimal, getcontext
8
9 from test_framework.messages import (
10 COIN,
11 CTransaction,
12 CTxOut,
13 )
14 from test_framework.script import (
15 CScript,
16 OP_RETURN
17 )
18
19 from test_framework.script_util import bulk_vout
20
21 from test_framework.blocktools import (
22 create_empty_fork,
23 )
24
25 from test_framework.test_framework import BitcoinTestFramework
26 from test_framework.util import (
27 assert_equal,
28 assert_greater_than,
29 assert_raises_rpc_error,
30 JSONRPCException,
31 )
32
33 from test_framework.mempool_util import (
34 TRUC_MAX_VSIZE,
35 TRUC_CHILD_MAX_VSIZE,
36 )
37
38 # sweep alice and bob's wallets and clear the mempool
39 def cleanup(func):
40 def wrapper(self, *args):
41 self.generate(self.nodes[0], 1)
42 func(self, *args)
43
44 self.generate(self.nodes[0], 1)
45 for wallet in [self.alice, self.bob]:
46 txs = set(tx["txid"] for tx in wallet.listtransactions("*", 1000) if tx["confirmations"] == 0 and not tx["abandoned"])
47 for tx in txs:
48 wallet.abandontransaction(tx)
49 try:
50 wallet.sendall([self.charlie.getnewaddress()])
51 except JSONRPCException as e:
52 assert "Total value of UTXO pool too low to pay for transaction" in e.error['message']
53 self.generate(self.nodes[0], 1)
54
55 for wallet in [self.alice, self.bob]:
56 balance = wallet.getbalances()["mine"]
57 for balance_type in ["untrusted_pending", "trusted", "immature", "nonmempool"]:
58 assert_equal(balance[balance_type], 0)
59
60 assert_equal(self.alice.getrawmempool(), [])
61 assert_equal(self.bob.getrawmempool(), [])
62
63 return wrapper
64
65 class WalletV3Test(BitcoinTestFramework):
66 def skip_test_if_missing_module(self):
67 self.skip_if_no_wallet()
68
69 def set_test_params(self):
70 getcontext().prec=10
71 self.num_nodes = 1
72 self.setup_clean_chain = True
73
74 def send_tx(self, from_wallet, inputs, outputs, version):
75 raw_tx = from_wallet.createrawtransaction(inputs=inputs, outputs=outputs, version=version)
76 if inputs == []:
77 raw_tx = from_wallet.fundrawtransaction(raw_tx, {'include_unsafe' : True})["hex"]
78 raw_tx = from_wallet.signrawtransactionwithwallet(raw_tx)["hex"]
79 txid = from_wallet.sendrawtransaction(raw_tx)
80 return txid
81
82 def bulk_tx(self, tx, amount, target_vsize):
83 tx.vout.append(CTxOut(nValue=(amount * COIN), scriptPubKey=CScript([OP_RETURN])))
84 bulk_vout(tx, target_vsize)
85
86 def run_test_with_swapped_versions(self, test_func):
87 test_func(2, 3)
88 test_func(3, 2)
89
90 def trigger_reorg(self, fork_blocks):
91 """Trigger reorg of the fork blocks."""
92 for block in fork_blocks:
93 self.nodes[0].submitblock(block.serialize().hex())
94 assert_equal(self.nodes[0].getbestblockhash(), fork_blocks[-1].hash_hex)
95
96 def run_test(self):
97 self.nodes[0].createwallet("alice")
98 self.alice = self.nodes[0].get_wallet_rpc("alice")
99
100 self.nodes[0].createwallet("bob")
101 self.bob = self.nodes[0].get_wallet_rpc("bob")
102
103 self.nodes[0].createwallet("charlie")
104 self.charlie = self.nodes[0].get_wallet_rpc("charlie")
105
106 self.generatetoaddress(self.nodes[0], 100, self.charlie.getnewaddress())
107
108 self.run_test_with_swapped_versions(self.tx_spends_unconfirmed_tx_with_wrong_version)
109 self.run_test_with_swapped_versions(self.va_tx_spends_confirmed_vb_tx)
110 self.run_test_with_swapped_versions(self.spend_inputs_with_different_versions)
111 self.spend_inputs_with_different_versions_default_version()
112 self.v3_utxos_appear_in_listunspent()
113 self.truc_tx_with_conflicting_sibling()
114 self.truc_tx_with_conflicting_sibling_change()
115 self.v3_tx_evicted_from_mempool_by_sibling()
116 self.v3_conflict_removed_from_mempool()
117 self.mempool_conflicts_removed_when_v3_conflict_removed()
118 self.max_tx_weight()
119 self.max_tx_child_weight()
120 self.user_input_weight_not_overwritten()
121 self.user_input_weight_not_overwritten_v3_child()
122 self.createpsbt_v3()
123 self.send_v3()
124 self.sendall_v3()
125 self.sendall_with_unconfirmed_v3()
126 self.walletcreatefundedpsbt_v3()
127 self.sendall_truc_weight_limit()
128 self.sendall_truc_child_weight_limit()
129 self.mix_non_truc_versions()
130 self.cant_spend_multiple_unconfirmed_truc_outputs()
131 self.test_spend_third_generation()
132 self.test_coins_availability_reorg()
133
134 @cleanup
135 def tx_spends_unconfirmed_tx_with_wrong_version(self, version_a, version_b):
136 self.log.info(f"Test unavailable funds when v{version_b} tx spends unconfirmed v{version_a} tx")
137
138 outputs = {self.bob.getnewaddress() : 2.0}
139 self.send_tx(self.charlie, [], outputs, version_a)
140
141 assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
142 assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
143
144 outputs = {self.alice.getnewaddress() : 1.0}
145
146 raw_tx_v2 = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=version_b)
147
148 assert_raises_rpc_error(
149 -4,
150 "Insufficient funds",
151 self.bob.fundrawtransaction,
152 raw_tx_v2, {'include_unsafe': True}
153 )
154
155 @cleanup
156 def va_tx_spends_confirmed_vb_tx(self, version_a, version_b):
157 self.log.info(f"Test available funds when v{version_b} tx spends confirmed v{version_a} tx")
158
159 outputs = {self.bob.getnewaddress() : 2.0}
160 self.send_tx(self.charlie, [], outputs, version_a)
161
162 assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
163 assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
164
165 outputs = {self.alice.getnewaddress() : 1.0}
166
167 self.generate(self.nodes[0], 1)
168
169 self.send_tx(self.bob, [], outputs, version_b)
170
171 @cleanup
172 def v3_utxos_appear_in_listunspent(self):
173 self.log.info("Test that unconfirmed v3 utxos still appear in listunspent")
174
175 outputs = {self.alice.getnewaddress() : 2.0}
176 parent_txid = self.send_tx(self.charlie, [], outputs, 3)
177 assert_equal(self.alice.listunspent(minconf=0)[0]["txid"], parent_txid)
178
179 @cleanup
180 def truc_tx_with_conflicting_sibling(self):
181 self.log.info("Test v3 transaction with conflicting sibling")
182
183 # unconfirmed v3 tx to alice & bob
184 outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
185 self.send_tx(self.charlie, [], outputs, 3)
186
187 # alice spends her output with a v3 transaction
188 alice_unspent = self.alice.listunspent(minconf=0)[0]
189 outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
190 self.send_tx(self.alice, [alice_unspent], outputs, 3)
191
192 # bob tries to spend money
193 outputs = {self.bob.getnewaddress() : 1.999}
194 bob_tx = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=3)
195
196 assert_raises_rpc_error(
197 -4,
198 "Insufficient funds",
199 self.bob.fundrawtransaction,
200 bob_tx, {'include_unsafe': True}
201 )
202
203 @cleanup
204 def truc_tx_with_conflicting_sibling_change(self):
205 self.log.info("Test v3 transaction with conflicting sibling change")
206
207 outputs = {self.alice.getnewaddress() : 8.0}
208 self.send_tx(self.charlie, [], outputs, 3)
209
210 self.generate(self.nodes[0], 1)
211
212 # unconfirmed v3 tx to alice & bob
213 outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
214 self.send_tx(self.alice, [], outputs, 3)
215
216 # bob spends his output with a v3 transaction
217 bob_unspent = self.bob.listunspent(minconf=0)[0]
218 outputs = {self.bob.getnewaddress() : bob_unspent['amount'] - Decimal(0.00000120)}
219 self.send_tx(self.bob, [bob_unspent], outputs, 3)
220
221 # alice tries to spend money
222 outputs = {self.alice.getnewaddress() : 1.999}
223 alice_tx = self.alice.createrawtransaction(inputs=[], outputs=outputs, version=3)
224
225 assert_raises_rpc_error(
226 -4,
227 "Insufficient funds",
228 self.alice.fundrawtransaction,
229 alice_tx, {'include_unsafe': True}
230 )
231
232 @cleanup
233 def spend_inputs_with_different_versions(self, version_a, version_b):
234 self.log.info(f"Test spending a pre-selected v{version_a} input with a v{version_b} transaction")
235
236 outputs = {self.alice.getnewaddress() : 2.0}
237 self.send_tx(self.charlie, [], outputs, version_a)
238
239 # alice spends her output
240 alice_unspent = self.alice.listunspent(minconf=0)[0]
241 outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
242 alice_tx = self.alice.createrawtransaction(inputs=[alice_unspent], outputs=outputs, version=version_b)
243
244 assert_raises_rpc_error(
245 -4,
246 f"Can't spend unconfirmed version {version_a} pre-selected input with a version {version_b} tx",
247 self.alice.fundrawtransaction,
248 alice_tx
249 )
250
251 @cleanup
252 def spend_inputs_with_different_versions_default_version(self):
253 self.log.info("Test spending a pre-selected v3 input with the default version of transaction")
254
255 outputs = {self.alice.getnewaddress() : 2.0}
256 self.send_tx(self.charlie, [], outputs, 3)
257
258 # alice spends her output
259 alice_unspent = self.alice.listunspent(minconf=0)[0]
260 outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
261 alice_tx = self.alice.createrawtransaction(inputs=[alice_unspent], outputs=outputs) # don't set the version here
262
263 assert_raises_rpc_error(
264 -4,
265 "Can't spend unconfirmed version 3 pre-selected input with a version 2 tx",
266 self.alice.fundrawtransaction,
267 alice_tx
268 )
269
270 @cleanup
271 def v3_tx_evicted_from_mempool_by_sibling(self):
272 self.log.info("Test v3 transaction evicted because of conflicting sibling")
273
274 # unconfirmed v3 tx to alice & bob
275 outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
276 self.send_tx(self.charlie, [], outputs, 3)
277
278 # alice spends her output with a v3 transaction
279 alice_unspent = self.alice.listunspent(minconf=0)[0]
280 alice_fee = Decimal(0.00000120)
281 outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - alice_fee}
282 alice_txid = self.send_tx(self.alice, [alice_unspent], outputs, 3)
283
284 # bob tries to spend money
285 bob_unspent = self.bob.listunspent(minconf=0)[0]
286 outputs = {self.bob.getnewaddress() : bob_unspent['amount'] - Decimal(0.00010120)}
287 bob_txid = self.send_tx(self.bob, [bob_unspent], outputs, 3)
288
289 assert_equal(self.alice.gettransaction(alice_txid)['mempoolconflicts'], [bob_txid])
290
291 self.log.info("Test that re-submitting Alice's transaction with a higher fee removes bob's tx as a mempool conflict")
292 fee_delta = Decimal(0.00030120)
293 outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - fee_delta}
294 alice_txid = self.send_tx(self.alice, [alice_unspent], outputs, 3)
295 assert_equal(self.alice.gettransaction(alice_txid)['mempoolconflicts'], [])
296
297 @cleanup
298 def v3_conflict_removed_from_mempool(self):
299 self.log.info("Test a v3 conflict being removed")
300 # send a v2 output to alice and confirm it
301 txid = self.charlie.sendall([self.alice.getnewaddress()])["txid"]
302 assert_equal(self.charlie.gettransaction(txid, verbose=True)["decoded"]["version"], 2)
303 self.generate(self.nodes[0], 1)
304 # create a v3 tx to alice and bob
305 outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
306 self.send_tx(self.charlie, [], outputs, 3)
307
308 alice_v2_unspent = self.alice.listunspent(minconf=1)[0]
309 alice_unspent = self.alice.listunspent(minconf=0, maxconf=0)[0]
310
311 # alice spends both of her outputs
312 outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] + alice_unspent['amount'] - Decimal(0.00005120)}
313 self.send_tx(self.alice, [alice_v2_unspent, alice_unspent], outputs, 3)
314 # bob can't create a transaction
315 outputs = {self.bob.getnewaddress() : 1.999}
316 bob_tx = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=3)
317
318 assert_raises_rpc_error(
319 -4,
320 "Insufficient funds",
321 self.bob.fundrawtransaction,
322 bob_tx, {'include_unsafe': True}
323 )
324 # alice fee-bumps her tx so it only spends the v2 utxo
325 outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] - Decimal(0.00015120)}
326 self.send_tx(self.alice, [alice_v2_unspent], outputs, 2)
327 # bob can now create a transaction
328 outputs = {self.bob.getnewaddress() : 1.999}
329 self.send_tx(self.bob, [], outputs, 3)
330
331 @cleanup
332 def mempool_conflicts_removed_when_v3_conflict_removed(self):
333 self.log.info("Test that we remove v3 txs from mempool_conflicts correctly")
334 # send a v2 output to alice and confirm it
335 txid = self.charlie.sendall([self.alice.getnewaddress()])["txid"]
336 assert_equal(self.charlie.gettransaction(txid, verbose=True)["decoded"]["version"], 2)
337 self.generate(self.nodes[0], 1)
338 # create a v3 tx to alice and bob
339 outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
340 self.send_tx(self.charlie, [], outputs, 3)
341
342 alice_v2_unspent = self.alice.listunspent(minconf=1)[0]
343 alice_unspent = self.alice.listunspent(minconf=0, maxconf=0)[0]
344 # bob spends his utxo
345 inputs=[]
346 outputs = {self.bob.getnewaddress() : 1.999}
347 bob_txid = self.send_tx(self.bob, inputs, outputs, 3)
348 # alice spends both of her utxos, replacing bob's tx
349 outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] + alice_unspent['amount'] - Decimal(0.00005120)}
350 alice_txid = self.send_tx(self.alice, [alice_v2_unspent, alice_unspent], outputs, 3)
351 # bob's tx now has a mempool conflict
352 assert_equal(self.bob.gettransaction(bob_txid)['mempoolconflicts'], [alice_txid])
353 # alice fee-bumps her tx so it only spends the v2 utxo
354 outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] - Decimal(0.00015120)}
355 self.send_tx(self.alice, [alice_v2_unspent], outputs, 2)
356 # bob's tx now has non conflicts and can be rebroadcast
357 bob_tx = self.bob.gettransaction(bob_txid)
358 assert_equal(bob_tx['mempoolconflicts'], [])
359 self.bob.sendrawtransaction(bob_tx['hex'])
360
361 @cleanup
362 def max_tx_weight(self):
363 self.log.info("Test max v3 transaction weight.")
364
365 tx = CTransaction()
366 tx.version = 3 # make this a truc tx
367 # increase tx weight almost to the max truc size
368 self.bulk_tx(tx, 5, TRUC_MAX_VSIZE - 100)
369
370 assert_raises_rpc_error(
371 -4,
372 "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
373 self.charlie.fundrawtransaction,
374 tx.serialize_with_witness().hex(),
375 {'include_unsafe' : True}
376 )
377
378 tx.version = 2
379 self.charlie.fundrawtransaction(tx.serialize_with_witness().hex())
380
381 @cleanup
382 def max_tx_child_weight(self):
383 self.log.info("Test max v3 transaction child weight.")
384
385 outputs = {self.alice.getnewaddress() : 10}
386 self.send_tx(self.charlie, [], outputs, 3)
387
388 tx = CTransaction()
389 tx.version = 3
390
391 self.bulk_tx(tx, 5, TRUC_CHILD_MAX_VSIZE - 100)
392
393 assert_raises_rpc_error(
394 -4,
395 "The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
396 self.alice.fundrawtransaction,
397 tx.serialize_with_witness().hex(),
398 {'include_unsafe' : True}
399 )
400
401 self.generate(self.nodes[0], 1)
402 self.alice.fundrawtransaction(tx.serialize_with_witness().hex())
403
404 @cleanup
405 def user_input_weight_not_overwritten(self):
406 self.log.info("Test that the user-input tx weight is not overwritten by the truc maximum")
407
408 tx = CTransaction()
409 tx.version = 3
410
411 self.bulk_tx(tx, 5, int(TRUC_MAX_VSIZE/2))
412
413 assert_raises_rpc_error(
414 -4,
415 "Maximum transaction weight is less than transaction weight without inputs",
416 self.charlie.fundrawtransaction,
417 tx.serialize_with_witness().hex(),
418 {'include_unsafe' : True, 'max_tx_weight' : int(TRUC_MAX_VSIZE/2)}
419 )
420
421 @cleanup
422 def user_input_weight_not_overwritten_v3_child(self):
423 self.log.info("Test that the user-input tx weight is not overwritten by the truc child maximum")
424
425 outputs = {self.alice.getnewaddress() : 10}
426 self.send_tx(self.charlie, [], outputs, 3)
427
428 tx = CTransaction()
429 tx.version = 3
430
431 self.bulk_tx(tx, 5, int(TRUC_CHILD_MAX_VSIZE/2))
432
433 assert_raises_rpc_error(
434 -4,
435 "Maximum transaction weight is less than transaction weight without inputs",
436 self.alice.fundrawtransaction,
437 tx.serialize_with_witness().hex(),
438 {'include_unsafe' : True, 'max_tx_weight' : int(TRUC_CHILD_MAX_VSIZE/2)}
439 )
440
441 self.generate(self.nodes[0], 1)
442 self.alice.fundrawtransaction(tx.serialize_with_witness().hex())
443
444 @cleanup
445 def createpsbt_v3(self):
446 self.log.info("Test setting version to 3 with createpsbt")
447
448 outputs = {self.alice.getnewaddress() : 10}
449 psbt = self.charlie.createpsbt(inputs=[], outputs=outputs, version=3)
450 assert_equal(self.charlie.decodepsbt(psbt)["tx_version"], 3)
451
452 @cleanup
453 def send_v3(self):
454 self.log.info("Test setting version to 3 with send")
455
456 outputs = {self.alice.getnewaddress() : 10}
457 tx_hex = self.charlie.send(outputs=outputs, add_to_wallet=False, version=3)["hex"]
458 assert_equal(self.charlie.decoderawtransaction(tx_hex)["version"], 3)
459
460 @cleanup
461 def sendall_v3(self):
462 self.log.info("Test setting version to 3 with sendall")
463
464 tx_hex = self.charlie.sendall(recipients=[self.alice.getnewaddress()], version=3, add_to_wallet=False)["hex"]
465 assert_equal(self.charlie.decoderawtransaction(tx_hex)["version"], 3)
466
467 @cleanup
468 def sendall_with_unconfirmed_v3(self):
469 self.log.info("Test setting version to 3 with sendall + unconfirmed inputs")
470
471 outputs = {self.alice.getnewaddress(): 2.00001 for _ in range(4)}
472
473 self.send_tx(self.charlie, [], outputs, 2)
474 self.generate(self.nodes[0], 1)
475
476 unspents = self.alice.listunspent()
477
478 # confirmed v2 utxos
479 outputs = {self.alice.getnewaddress() : 2.0}
480 confirmed_v2 = self.send_tx(self.alice, [unspents[0]], outputs, 2)
481
482 # confirmed v3 utxos
483 outputs = {self.alice.getnewaddress() : 2.0}
484 confirmed_v3 = self.send_tx(self.alice, [unspents[1]], outputs, 3)
485
486 self.generate(self.nodes[0], 1)
487
488 # unconfirmed v2 utxos
489 outputs = {self.alice.getnewaddress() : 2.0}
490 unconfirmed_v2 = self.send_tx(self.alice, [unspents[2]], outputs, 2)
491
492 # unconfirmed v3 utxos
493 outputs = {self.alice.getnewaddress() : 2.0}
494 unconfirmed_v3 = self.send_tx(self.alice, [unspents[3]], outputs, 3)
495
496 # Test that the only unconfirmed inputs this v3 tx spends are v3
497 tx_hex = self.alice.sendall([self.bob.getnewaddress()], version=3, add_to_wallet=False, minconf=0)["hex"]
498
499 decoded_tx = self.alice.decoderawtransaction(tx_hex)
500 decoded_vin_txids = [txin["txid"] for txin in decoded_tx["vin"]]
501
502 assert_equal(decoded_tx["version"], 3)
503
504 assert confirmed_v3 in decoded_vin_txids
505 assert confirmed_v2 in decoded_vin_txids
506 assert unconfirmed_v3 in decoded_vin_txids
507 assert unconfirmed_v2 not in decoded_vin_txids
508
509 # Test that the only unconfirmed inputs this v2 tx spends are v2
510 tx_hex = self.alice.sendall([self.bob.getnewaddress()], version=2, add_to_wallet=False, minconf=0)["hex"]
511
512 decoded_tx = self.alice.decoderawtransaction(tx_hex)
513 decoded_vin_txids = [txin["txid"] for txin in decoded_tx["vin"]]
514
515 assert_equal(decoded_tx["version"], 2)
516
517 assert confirmed_v3 in decoded_vin_txids
518 assert confirmed_v2 in decoded_vin_txids
519 assert unconfirmed_v2 in decoded_vin_txids
520 assert unconfirmed_v3 not in decoded_vin_txids
521
522 @cleanup
523 def walletcreatefundedpsbt_v3(self):
524 self.log.info("Test setting version to 3 with walletcreatefundedpsbt")
525
526 outputs = {self.alice.getnewaddress() : 10}
527 psbt = self.charlie.walletcreatefundedpsbt(inputs=[], outputs=outputs, version=3)["psbt"]
528 assert_equal(self.charlie.decodepsbt(psbt)["tx_version"], 3)
529
530 @cleanup
531 def sendall_truc_weight_limit(self):
532 self.log.info("Test that sendall follows truc tx weight limit")
533 self.charlie.sendall([self.alice.getnewaddress() for _ in range(300)], add_to_wallet=False, version=2)
534
535 # check that error is only raised if version is 3
536 assert_raises_rpc_error(
537 -4,
538 "Transaction too large" ,
539 self.charlie.sendall,
540 [self.alice.getnewaddress() for _ in range(300)],
541 version=3
542 )
543
544 @cleanup
545 def sendall_truc_child_weight_limit(self):
546 self.log.info("Test that sendall follows spending unconfirmed truc tx weight limit")
547 outputs = {self.charlie.getnewaddress() : 2.0}
548 self.send_tx(self.charlie, [], outputs, 3)
549
550 self.charlie.sendall([self.alice.getnewaddress() for _ in range(50)], add_to_wallet=False)
551
552 assert_raises_rpc_error(
553 -4,
554 "Transaction too large" ,
555 self.charlie.sendall,
556 [self.alice.getnewaddress() for _ in range(50)],
557 version=3
558 )
559
560 @cleanup
561 def mix_non_truc_versions(self):
562 self.log.info("Test that we can mix non-truc versions when spending an unconfirmed output")
563
564 outputs = {self.bob.getnewaddress() : 2.0}
565 self.send_tx(self.charlie, [], outputs, 1)
566
567 assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
568 assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
569
570 outputs = {self.alice.getnewaddress() : 1.0}
571
572 raw_tx_v2 = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=2)
573
574 # does not throw an error
575 self.bob.fundrawtransaction(raw_tx_v2, {'include_unsafe': True})["hex"]
576
577 @cleanup
578 def cant_spend_multiple_unconfirmed_truc_outputs(self):
579 self.log.info("Test that we can't spend multiple unconfirmed truc outputs")
580
581 outputs = {self.alice.getnewaddress(): 2.00001}
582 self.send_tx(self.charlie, [], outputs, 3)
583 self.send_tx(self.charlie, [], outputs, 3)
584
585 assert_equal(len(self.alice.listunspent(minconf=0)), 2)
586
587 outputs = {self.bob.getnewaddress() : 3.0}
588
589 raw_tx = self.alice.createrawtransaction(inputs=[], outputs=outputs, version=3)
590
591 assert_raises_rpc_error(
592 -4,
593 "Insufficient funds",
594 self.alice.fundrawtransaction,
595 raw_tx,
596 {'include_unsafe' : True}
597 )
598
599 @cleanup
600 def test_spend_third_generation(self):
601 self.log.info("Test that we can't spend an unconfirmed TRUC output that already has an unconfirmed parent")
602
603 # Generation 1: Consolidate all UTXOs into one output using sendall
604 self.charlie.sendall([self.charlie.getnewaddress()], version=3)
605 outputs1 = self.charlie.listunspent(minconf=0)
606 assert_equal(len(outputs1), 1)
607
608 # Generation 2: to ensure no change address is created, do another sendall
609 self.charlie.sendall([self.charlie.getnewaddress()], version=3)
610 outputs2 = self.charlie.listunspent(minconf=0)
611 assert_equal(len(outputs2), 1)
612 total_amount = sum([utxo['amount'] for utxo in outputs2])
613
614 # Generation 3: try to send half of total amount to Alice
615 outputs = {self.alice.getnewaddress(): total_amount / 2}
616 assert_raises_rpc_error(
617 -4,
618 "Insufficient funds",
619 self.charlie.send,
620 outputs,
621 version=3
622 )
623
624 # Also doesn't work with fundrawtransaction
625 raw_tx = self.charlie.createrawtransaction(inputs=[], outputs=outputs, version=3)
626 assert_raises_rpc_error(
627 -4,
628 "Insufficient funds",
629 self.charlie.fundrawtransaction,
630 raw_tx,
631 {'include_unsafe' : True}
632 )
633
634 # Also doesn't work with sendall
635 assert_raises_rpc_error(
636 -6,
637 "Total value of UTXO pool too low to pay for transaction",
638 self.charlie.sendall,
639 [self.alice.getnewaddress()],
640 version=3
641 )
642
643 @cleanup
644 def test_coins_availability_reorg(self):
645 self.log.info("Test coin availability after reorg with v2 parent and truc child")
646
647 # Prep fork blocks
648 fork_blocks = create_empty_fork(self.nodes[0])
649
650 # Send funds to alice so she can create transactions
651 outputs = {self.alice.getnewaddress(): 5.0}
652 self.send_tx(self.charlie, [], outputs, 2)
653 self.generate(self.nodes[0], 1)
654
655 # Alice creates a v2 transaction with 2 outputs
656 alice_unspent = self.alice.listunspent()[0]
657 v2_outputs = [
658 {self.alice.getnewaddress(): 2.5},
659 {self.alice.getnewaddress(): 2.4999},
660 ]
661 v2_txid = self.send_tx(self.alice, [alice_unspent], v2_outputs, 2)
662
663 # Mine the v2 transaction in one block
664 self.generate(self.nodes[0], 1)
665
666 # Get the output from the v2 transaction for chaining
667 v2_utxo = self.alice.listunspent(minconf=1)[0]
668 assert_equal(v2_utxo["txid"], v2_txid)
669
670 # Alice creates a truc child chaining from the v2 utxo
671 truc_outputs = {self.alice.getnewaddress(): v2_utxo["amount"] - Decimal("0.0001")}
672 truc_txid = self.send_tx(self.alice, [v2_utxo], truc_outputs, 3)
673
674 # Mine the truc transaction in a second block
675 self.generate(self.nodes[0], 1)
676
677 # Verify both transactions are confirmed
678 wallet_tx_v2 = self.alice.gettransaction(v2_txid)
679 wallet_tx_truc = self.alice.gettransaction(truc_txid)
680
681 assert_equal(wallet_tx_v2["confirmations"], 2)
682 assert_equal(wallet_tx_truc["confirmations"], 1)
683
684 # Check that their versions are correct
685 assert_equal(self.alice.decoderawtransaction(wallet_tx_v2["hex"])["version"], 2)
686 assert_equal(self.alice.decoderawtransaction(wallet_tx_truc["hex"])["version"], 3)
687
688 # Check listunspent before reorg - should have the truc output
689 unspent_before = self.alice.listunspent()
690 truc_output_txids = [u["txid"] for u in unspent_before]
691 assert truc_txid in truc_output_txids
692
693 # Trigger the reorg
694 self.trigger_reorg(fork_blocks)
695 # The TRUC transaction is now in a cluster of size 3, which is only permitted in a reorg.
696 assert_equal(self.nodes[0].getmempoolcluster(truc_txid)["txcount"], 3)
697
698 # After reorg, both transactions should be back in mempool
699 mempool = self.nodes[0].getrawmempool()
700 assert v2_txid in mempool
701 assert truc_txid in mempool
702
703 # Check listunspent after reorg - the truc output should still appear
704 # as unconfirmed since the transaction is in the mempool
705 unspent_after = self.alice.listunspent(minconf=0)
706 unspent_txids_after = [u["txid"] for u in unspent_after]
707 assert truc_txid in unspent_txids_after
708
709 total_unconfirmed_amount = sum([u["amount"] for u in self.alice.listunspent(minconf=0)])
710 # We cannot create a transaction spending both outputs, regardless of version.
711 output_too_high = {self.bob.getnewaddress(): total_unconfirmed_amount - Decimal("1")}
712 for version in [2, 3]:
713 raw_output_too_high = self.alice.createrawtransaction(inputs=[], outputs=output_too_high, version=version)
714 assert_raises_rpc_error(
715 -4,
716 "Insufficient funds",
717 self.alice.fundrawtransaction,
718 raw_output_too_high,
719 {'include_unsafe' : True}
720 )
721
722 # Now try to create a v2 transaction - this triggers AvailableCoins with
723 # check_version_trucness=true. The v2 parent has truc_child_in_mempool set
724 # because the truc child is in mempool.
725 new_v2_outputs = {self.bob.getnewaddress(): 0.1}
726 raw_v2_child = self.alice.createrawtransaction(inputs=[], outputs=new_v2_outputs, version=2)
727 raw_v2_with_v3_sibling = self.alice.fundrawtransaction(raw_v2_child, {'include_unsafe': True})
728
729 # See that this transaction can be added to mempool
730 signed_raw_v2_with_v3_sibling = self.alice.signrawtransactionwithwallet(raw_v2_with_v3_sibling["hex"])
731 self.alice.sendrawtransaction(signed_raw_v2_with_v3_sibling["hex"])
732
733 # This TRUC transaction is in a cluster of size 4
734 assert_equal(self.nodes[0].getmempoolcluster(truc_txid)["txcount"], 4)
735
736
737
738 if __name__ == '__main__':
739 WalletV3Test(__file__).main()
740