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