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()