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