Merge bitcoin/bitcoin#32896: wallet, rpc: add v3 transaction creation and wallet support

5c8bf7b39e doc: add release notes for version 3 transactions (ishaanam)
4ef8065a5e test: add truc wallet tests (ishaanam)
5d932e14db test: extract `bulk_vout` from `bulk_tx` so it can be used by wallet tests (ishaanam)
2cb473d9f2 rpc: Support version 3 transaction creation (Bue-von-hon)
4c20343b4d rpc: Add transaction min standard version parameter (Bue-von-hon)
c5a2d08011 wallet: don't return utxos from multiple truc txs in AvailableCoins (ishaanam)
da8748ad62 wallet: limit v3 tx weight in coin selection (ishaanam)
85c5410615 wallet: mark unconfirmed v3 siblings as mempool conflicts (ishaanam)
0804fc3cb1 wallet: throw error at conflicting tx versions in pre-selected inputs (ishaanam)
cc155226fe wallet: set m_version in coin control to default value (ishaanam)
2e9617664e  wallet: don't include unconfirmed v3 txs with children in available coins (ishaanam)
ec2676becd wallet: unconfirmed ancestors and descendants are always truc (ishaanam)

Pull request description:

  This PR Implements the following:
  - If creating a v3 transaction, `AvailableCoins` doesn't return unconfirmed v2 utxos (and vice versa)
  - `AvailableCoins` doesn't return an unconfirmed v3 utxo if its transaction already has a child
  - If a v3 transaction is kicked out of the mempool by a sibling, mark the sibling as a mempool conflict
  - Throw an error if pre-selected inputs are of the wrong transaction version
  - Allow setting version to 3 manually in `createrawtransaction` (uses commits from #31936)
  - Limits a v3 transaction weight in coin selection

  Closes #31348

  To-Do:
  - [x] Test a v3 sibling conflict kicking out one of our transactions from the mempool
  - [x] Implement separate size limit for TRUC children
  - [x] Test that we can't fund a v2 transaction when everything is v3 unconfirmed
  - [x] Test a v3 sibling conflict being removed from the mempool
  - [x] Test limiting v3 transaction weight in coin selection
  - [x] Simplify tests
  - [x] Add documentation
  - [x] Test that user-input max weight is not overwritten by truc max weight
  - [x] Test v3 in RPCs other than `createrawtransaction`

ACKs for top commit:
  glozow:
    reACK 5c8bf7b39e
  achow101:
    ACK 5c8bf7b39e
  rkrux:
    ACK 5c8bf7b39e

Tree-SHA512: da8aea51c113e193dd0b442eff765bd6b8dc0e5066272d3e52190a223c903f48788795f32c554f268af0d2607b5b8c3985c648879cb176c65540837c05d0abb5
This commit is contained in:
merge-script
2025-08-19 06:00:50 -04:00
25 changed files with 836 additions and 32 deletions

View File

@@ -19,6 +19,7 @@ from test_framework.messages import (
CTxOut,
SEQUENCE_FINAL,
tx_from_hex,
TX_MAX_STANDARD_VERSION,
WITNESS_SCALE_FACTOR,
)
from test_framework.script import (
@@ -666,7 +667,6 @@ SIG_ADD_ZERO = {"failure": {"sign": zero_appender(default_sign)}}
DUST_LIMIT = 600
MIN_FEE = 50000
TX_MAX_STANDARD_VERSION = 3
TX_STANDARD_VERSIONS = [1, 2, TX_MAX_STANDARD_VERSION]
TRUC_MAX_VSIZE = 10000 # test doesn't cover in-mempool spends, so only this limit is hit

View File

@@ -19,6 +19,8 @@ from itertools import product
from test_framework.messages import (
MAX_BIP125_RBF_SEQUENCE,
COIN,
TX_MAX_STANDARD_VERSION,
TX_MIN_STANDARD_VERSION,
CTransaction,
CTxOut,
tx_from_hex,
@@ -254,7 +256,11 @@ class RawTransactionsTest(BitcoinTestFramework):
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])
# Test `createrawtransaction` invalid extra parameters
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 'foo')
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [], {}, 0, False, 2, 3, 'foo')
# Test `createrawtransaction` invalid version parameters
assert_raises_rpc_error(-8, f"Invalid parameter, version out of range({TX_MIN_STANDARD_VERSION}~{TX_MAX_STANDARD_VERSION})", self.nodes[0].createrawtransaction, [], {}, 0, False, TX_MIN_STANDARD_VERSION - 1)
assert_raises_rpc_error(-8, f"Invalid parameter, version out of range({TX_MIN_STANDARD_VERSION}~{TX_MAX_STANDARD_VERSION})", self.nodes[0].createrawtransaction, [], {}, 0, False, TX_MAX_STANDARD_VERSION + 1)
# Test `createrawtransaction` invalid `inputs`
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type array", self.nodes[0].createrawtransaction, 'foo', {})
@@ -334,6 +340,11 @@ class RawTransactionsTest(BitcoinTestFramework):
self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=[{address: 99}, {address2: 99}, {'data': '99'}]),
)
for version in range(TX_MIN_STANDARD_VERSION, TX_MAX_STANDARD_VERSION + 1):
rawtx = self.nodes[2].createrawtransaction(inputs=[{'txid': TXID, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)]), version=version)
tx = tx_from_hex(rawtx)
assert_equal(tx.version, version)
def sendrawtransaction_tests(self):
self.log.info("Test sendrawtransaction with missing input")
inputs = [{'txid': TXID, 'vout': 1}] # won't exist

View File

@@ -34,6 +34,9 @@ DEFAULT_MIN_RELAY_TX_FEE = 100
# Default for -incrementalrelayfee in sat/kvB
DEFAULT_INCREMENTAL_RELAY_FEE = 100
TRUC_MAX_VSIZE = 10000
TRUC_CHILD_MAX_VSIZE = 1000
def assert_mempool_contents(test_framework, node, expected=None, sync=True):
"""Assert that all transactions in expected are in the mempool,
and no additional ones exist. 'expected' is an array of

View File

@@ -80,6 +80,9 @@ MAX_OP_RETURN_RELAY = 100_000
DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours
TX_MIN_STANDARD_VERSION = 1
TX_MAX_STANDARD_VERSION = 3
MAGIC_BYTES = {
"mainnet": b"\xf9\xbe\xb4\xd9",
"testnet4": b"\x1c\x16\x3f\x28",

View File

@@ -13,6 +13,7 @@ from test_framework.messages import (
CTxIn,
CTxInWitness,
CTxOut,
ser_compact_size,
sha256,
)
from test_framework.script import (
@@ -35,6 +36,8 @@ from test_framework.script import (
hash160,
)
from test_framework.util import assert_equal
# Maximum number of potentially executed legacy signature operations in validating a transaction.
MAX_STD_LEGACY_SIGOPS = 2_500
@@ -128,6 +131,16 @@ def script_to_p2sh_p2wsh_script(script):
p2shscript = CScript([OP_0, sha256(script)])
return script_to_p2sh_script(p2shscript)
def bulk_vout(tx, target_vsize):
if target_vsize < tx.get_vsize():
raise RuntimeError(f"target_vsize {target_vsize} is less than transaction virtual size {tx.get_vsize()}")
# determine number of needed padding bytes
dummy_vbytes = target_vsize - tx.get_vsize()
# compensate for the increase of the compact-size encoded script length
# (note that the length encoding of the unpadded output script needs one byte)
dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1
tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes)
assert_equal(tx.get_vsize(), target_vsize)
def output_key_to_p2tr_script(key):
assert len(key) == 32

View File

@@ -33,11 +33,9 @@ from test_framework.messages import (
CTxInWitness,
CTxOut,
hash256,
ser_compact_size,
)
from test_framework.script import (
CScript,
OP_1,
OP_NOP,
OP_RETURN,
OP_TRUE,
@@ -45,6 +43,7 @@ from test_framework.script import (
taproot_construct,
)
from test_framework.script_util import (
bulk_vout,
key_to_p2pk_script,
key_to_p2pkh_script,
key_to_p2sh_p2wpkh_script,
@@ -121,17 +120,9 @@ class MiniWallet:
"""Pad a transaction with extra outputs until it reaches a target vsize.
returns the tx
"""
if target_vsize < tx.get_vsize():
raise RuntimeError(f"target_vsize {target_vsize} is less than transaction virtual size {tx.get_vsize()}")
tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN])))
# determine number of needed padding bytes
dummy_vbytes = target_vsize - tx.get_vsize()
# compensate for the increase of the compact-size encoded script length
# (note that the length encoding of the unpadded output script needs one byte)
dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1
tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes)
assert_equal(tx.get_vsize(), target_vsize)
bulk_vout(tx, target_vsize)
def get_balance(self):
return sum(u['value'] for u in self._utxos)

View File

@@ -109,6 +109,7 @@ BASE_SCRIPTS = [
'rpc_psbt.py',
'wallet_fundrawtransaction.py',
'wallet_bumpfee.py',
'wallet_v3_txs.py',
'wallet_backup.py',
'feature_segwit.py --v2transport',
'feature_segwit.py --v1transport',

589
test/functional/wallet_v3_txs.py Executable file
View File

@@ -0,0 +1,589 @@
#!/usr/bin/env python3
# Copyright (c) 2025 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test how the wallet deals with TRUC transactions"""
from decimal import Decimal, getcontext
from test_framework.authproxy import JSONRPCException
from test_framework.messages import (
COIN,
CTransaction,
CTxOut,
)
from test_framework.script import (
CScript,
OP_RETURN
)
from test_framework.script_util import bulk_vout
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_raises_rpc_error,
)
from test_framework.mempool_util import (
TRUC_MAX_VSIZE,
TRUC_CHILD_MAX_VSIZE,
)
# sweep alice and bob's wallets and clear the mempool
def cleanup(func):
def wrapper(self, *args):
try:
self.generate(self.nodes[0], 1)
func(self, *args)
finally:
self.generate(self.nodes[0], 1)
try:
self.alice.sendall([self.charlie.getnewaddress()])
except JSONRPCException as e:
assert "Total value of UTXO pool too low to pay for transaction" in e.error['message']
try:
self.bob.sendall([self.charlie.getnewaddress()])
except JSONRPCException as e:
assert "Total value of UTXO pool too low to pay for transaction" in e.error['message']
self.generate(self.nodes[0], 1)
for wallet in [self.alice, self.bob]:
balance = wallet.getbalances()["mine"]
for balance_type in ["untrusted_pending", "trusted", "immature"]:
assert_equal(balance[balance_type], 0)
assert_equal(self.alice.getrawmempool(), [])
assert_equal(self.bob.getrawmempool(), [])
return wrapper
class WalletV3Test(BitcoinTestFramework):
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def set_test_params(self):
getcontext().prec=10
self.num_nodes = 1
self.setup_clean_chain = True
def send_tx(self, from_wallet, inputs, outputs, version):
raw_tx = from_wallet.createrawtransaction(inputs=inputs, outputs=outputs, version=version)
if inputs == []:
raw_tx = from_wallet.fundrawtransaction(raw_tx, {'include_unsafe' : True})["hex"]
raw_tx = from_wallet.signrawtransactionwithwallet(raw_tx)["hex"]
txid = from_wallet.sendrawtransaction(raw_tx)
return txid
def bulk_tx(self, tx, amount, target_vsize):
tx.vout.append(CTxOut(nValue=(amount * COIN), scriptPubKey=CScript([OP_RETURN])))
bulk_vout(tx, target_vsize)
def run_test_with_swapped_versions(self, test_func):
test_func(2, 3)
test_func(3, 2)
def run_test(self):
self.nodes[0].createwallet("alice")
self.alice = self.nodes[0].get_wallet_rpc("alice")
self.nodes[0].createwallet("bob")
self.bob = self.nodes[0].get_wallet_rpc("bob")
self.nodes[0].createwallet("charlie")
self.charlie = self.nodes[0].get_wallet_rpc("charlie")
self.generatetoaddress(self.nodes[0], 100, self.charlie.getnewaddress())
self.run_test_with_swapped_versions(self.tx_spends_unconfirmed_tx_with_wrong_version)
self.run_test_with_swapped_versions(self.va_tx_spends_confirmed_vb_tx)
self.run_test_with_swapped_versions(self.spend_inputs_with_different_versions)
self.spend_inputs_with_different_versions_default_version()
self.v3_utxos_appear_in_listunspent()
self.truc_tx_with_conflicting_sibling()
self.truc_tx_with_conflicting_sibling_change()
self.v3_tx_evicted_from_mempool_by_sibling()
self.v3_conflict_removed_from_mempool()
self.mempool_conflicts_removed_when_v3_conflict_removed()
self.max_tx_weight()
self.max_tx_child_weight()
self.user_input_weight_not_overwritten()
self.user_input_weight_not_overwritten_v3_child()
self.createpsbt_v3()
self.send_v3()
self.sendall_v3()
self.sendall_with_unconfirmed_v3()
self.walletcreatefundedpsbt_v3()
self.sendall_truc_weight_limit()
self.sendall_truc_child_weight_limit()
self.mix_non_truc_versions()
self.cant_spend_multiple_unconfirmed_truc_outputs()
@cleanup
def tx_spends_unconfirmed_tx_with_wrong_version(self, version_a, version_b):
self.log.info(f"Test unavailable funds when v{version_b} tx spends unconfirmed v{version_a} tx")
outputs = {self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, version_a)
assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
outputs = {self.alice.getnewaddress() : 1.0}
raw_tx_v2 = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=version_b)
assert_raises_rpc_error(
-4,
"Insufficient funds",
self.bob.fundrawtransaction,
raw_tx_v2, {'include_unsafe': True}
)
@cleanup
def va_tx_spends_confirmed_vb_tx(self, version_a, version_b):
self.log.info(f"Test available funds when v{version_b} tx spends confirmed v{version_a} tx")
outputs = {self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, version_a)
assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
outputs = {self.alice.getnewaddress() : 1.0}
self.generate(self.nodes[0], 1)
self.send_tx(self.bob, [], outputs, version_b)
@cleanup
def v3_utxos_appear_in_listunspent(self):
self.log.info("Test that unconfirmed v3 utxos still appear in listunspent")
outputs = {self.alice.getnewaddress() : 2.0}
parent_txid = self.send_tx(self.charlie, [], outputs, 3)
assert_equal(self.alice.listunspent(minconf=0)[0]["txid"], parent_txid)
@cleanup
def truc_tx_with_conflicting_sibling(self):
self.log.info("Test v3 transaction with conflicting sibling")
# unconfirmed v3 tx to alice & bob
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 3)
# alice spends her output with a v3 transaction
alice_unspent = self.alice.listunspent(minconf=0)[0]
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
self.send_tx(self.alice, [alice_unspent], outputs, 3)
# bob tries to spend money
outputs = {self.bob.getnewaddress() : 1.999}
bob_tx = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=3)
assert_raises_rpc_error(
-4,
"Insufficient funds",
self.bob.fundrawtransaction,
bob_tx, {'include_unsafe': True}
)
@cleanup
def truc_tx_with_conflicting_sibling_change(self):
self.log.info("Test v3 transaction with conflicting sibling change")
outputs = {self.alice.getnewaddress() : 8.0}
self.send_tx(self.charlie, [], outputs, 3)
self.generate(self.nodes[0], 1)
# unconfirmed v3 tx to alice & bob
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
self.send_tx(self.alice, [], outputs, 3)
# bob spends his output with a v3 transaction
bob_unspent = self.bob.listunspent(minconf=0)[0]
outputs = {self.bob.getnewaddress() : bob_unspent['amount'] - Decimal(0.00000120)}
self.send_tx(self.bob, [bob_unspent], outputs, 3)
# alice tries to spend money
outputs = {self.alice.getnewaddress() : 1.999}
alice_tx = self.alice.createrawtransaction(inputs=[], outputs=outputs, version=3)
assert_raises_rpc_error(
-4,
"Insufficient funds",
self.alice.fundrawtransaction,
alice_tx, {'include_unsafe': True}
)
@cleanup
def spend_inputs_with_different_versions(self, version_a, version_b):
self.log.info(f"Test spending a pre-selected v{version_a} input with a v{version_b} transaction")
outputs = {self.alice.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, version_a)
# alice spends her output
alice_unspent = self.alice.listunspent(minconf=0)[0]
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
alice_tx = self.alice.createrawtransaction(inputs=[alice_unspent], outputs=outputs, version=version_b)
assert_raises_rpc_error(
-4,
f"Can't spend unconfirmed version {version_a} pre-selected input with a version {version_b} tx",
self.alice.fundrawtransaction,
alice_tx
)
@cleanup
def spend_inputs_with_different_versions_default_version(self):
self.log.info("Test spending a pre-selected v3 input with the default version of transaction")
outputs = {self.alice.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 3)
# alice spends her output
alice_unspent = self.alice.listunspent(minconf=0)[0]
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - Decimal(0.00000120)}
alice_tx = self.alice.createrawtransaction(inputs=[alice_unspent], outputs=outputs) # don't set the version here
assert_raises_rpc_error(
-4,
"Can't spend unconfirmed version 3 pre-selected input with a version 2 tx",
self.alice.fundrawtransaction,
alice_tx
)
@cleanup
def v3_tx_evicted_from_mempool_by_sibling(self):
self.log.info("Test v3 transaction evicted because of conflicting sibling")
# unconfirmed v3 tx to alice & bob
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 3)
# alice spends her output with a v3 transaction
alice_unspent = self.alice.listunspent(minconf=0)[0]
alice_fee = Decimal(0.00000120)
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - alice_fee}
alice_txid = self.send_tx(self.alice, [alice_unspent], outputs, 3)
# bob tries to spend money
bob_unspent = self.bob.listunspent(minconf=0)[0]
outputs = {self.bob.getnewaddress() : bob_unspent['amount'] - Decimal(0.00010120)}
bob_txid = self.send_tx(self.bob, [bob_unspent], outputs, 3)
assert_equal(self.alice.gettransaction(alice_txid)['mempoolconflicts'], [bob_txid])
self.log.info("Test that re-submitting Alice's transaction with a higher fee removes bob's tx as a mempool conflict")
fee_delta = Decimal(0.00030120)
outputs = {self.alice.getnewaddress() : alice_unspent['amount'] - fee_delta}
alice_txid = self.send_tx(self.alice, [alice_unspent], outputs, 3)
assert_equal(self.alice.gettransaction(alice_txid)['mempoolconflicts'], [])
@cleanup
def v3_conflict_removed_from_mempool(self):
self.log.info("Test a v3 conflict being removed")
# send a v2 output to alice and confirm it
txid = self.charlie.sendall([self.alice.getnewaddress()])["txid"]
assert_equal(self.charlie.gettransaction(txid, verbose=True)["decoded"]["version"], 2)
self.generate(self.nodes[0], 1)
# create a v3 tx to alice and bob
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 3)
alice_v2_unspent = self.alice.listunspent(minconf=1)[0]
alice_unspent = self.alice.listunspent(minconf=0, maxconf=0)[0]
# alice spends both of her outputs
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] + alice_unspent['amount'] - Decimal(0.00005120)}
self.send_tx(self.alice, [alice_v2_unspent, alice_unspent], outputs, 3)
# bob can't create a transaction
outputs = {self.bob.getnewaddress() : 1.999}
bob_tx = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=3)
assert_raises_rpc_error(
-4,
"Insufficient funds",
self.bob.fundrawtransaction,
bob_tx, {'include_unsafe': True}
)
# alice fee-bumps her tx so it only spends the v2 utxo
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] - Decimal(0.00015120)}
self.send_tx(self.alice, [alice_v2_unspent], outputs, 2)
# bob can now create a transaction
outputs = {self.bob.getnewaddress() : 1.999}
self.send_tx(self.bob, [], outputs, 3)
@cleanup
def mempool_conflicts_removed_when_v3_conflict_removed(self):
self.log.info("Test that we remove v3 txs from mempool_conflicts correctly")
# send a v2 output to alice and confirm it
txid = self.charlie.sendall([self.alice.getnewaddress()])["txid"]
assert_equal(self.charlie.gettransaction(txid, verbose=True)["decoded"]["version"], 2)
self.generate(self.nodes[0], 1)
# create a v3 tx to alice and bob
outputs = {self.alice.getnewaddress() : 2.0, self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 3)
alice_v2_unspent = self.alice.listunspent(minconf=1)[0]
alice_unspent = self.alice.listunspent(minconf=0, maxconf=0)[0]
# bob spends his utxo
inputs=[]
outputs = {self.bob.getnewaddress() : 1.999}
bob_txid = self.send_tx(self.bob, inputs, outputs, 3)
# alice spends both of her utxos, replacing bob's tx
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] + alice_unspent['amount'] - Decimal(0.00005120)}
alice_txid = self.send_tx(self.alice, [alice_v2_unspent, alice_unspent], outputs, 3)
# bob's tx now has a mempool conflict
assert_equal(self.bob.gettransaction(bob_txid)['mempoolconflicts'], [alice_txid])
# alice fee-bumps her tx so it only spends the v2 utxo
outputs = {self.charlie.getnewaddress() : alice_v2_unspent['amount'] - Decimal(0.00015120)}
self.send_tx(self.alice, [alice_v2_unspent], outputs, 2)
# bob's tx now has non conflicts and can be rebroadcast
bob_tx = self.bob.gettransaction(bob_txid)
assert_equal(bob_tx['mempoolconflicts'], [])
self.bob.sendrawtransaction(bob_tx['hex'])
@cleanup
def max_tx_weight(self):
self.log.info("Test max v3 transaction weight.")
tx = CTransaction()
tx.version = 3 # make this a truc tx
# increase tx weight almost to the max truc size
self.bulk_tx(tx, 5, TRUC_MAX_VSIZE - 100)
assert_raises_rpc_error(
-4,
"The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
self.charlie.fundrawtransaction,
tx.serialize_with_witness().hex(),
{'include_unsafe' : True}
)
tx.version = 2
self.charlie.fundrawtransaction(tx.serialize_with_witness().hex())
@cleanup
def max_tx_child_weight(self):
self.log.info("Test max v3 transaction child weight.")
outputs = {self.alice.getnewaddress() : 10}
self.send_tx(self.charlie, [], outputs, 3)
tx = CTransaction()
tx.version = 3
self.bulk_tx(tx, 5, TRUC_CHILD_MAX_VSIZE - 100)
assert_raises_rpc_error(
-4,
"The inputs size exceeds the maximum weight. Please try sending a smaller amount or manually consolidating your wallet's UTXOs",
self.alice.fundrawtransaction,
tx.serialize_with_witness().hex(),
{'include_unsafe' : True}
)
self.generate(self.nodes[0], 1)
self.alice.fundrawtransaction(tx.serialize_with_witness().hex())
@cleanup
def user_input_weight_not_overwritten(self):
self.log.info("Test that the user-input tx weight is not overwritten by the truc maximum")
tx = CTransaction()
tx.version = 3
self.bulk_tx(tx, 5, int(TRUC_MAX_VSIZE/2))
assert_raises_rpc_error(
-4,
"Maximum transaction weight is less than transaction weight without inputs",
self.charlie.fundrawtransaction,
tx.serialize_with_witness().hex(),
{'include_unsafe' : True, 'max_tx_weight' : int(TRUC_MAX_VSIZE/2)}
)
@cleanup
def user_input_weight_not_overwritten_v3_child(self):
self.log.info("Test that the user-input tx weight is not overwritten by the truc child maximum")
outputs = {self.alice.getnewaddress() : 10}
self.send_tx(self.charlie, [], outputs, 3)
tx = CTransaction()
tx.version = 3
self.bulk_tx(tx, 5, int(TRUC_CHILD_MAX_VSIZE/2))
assert_raises_rpc_error(
-4,
"Maximum transaction weight is less than transaction weight without inputs",
self.alice.fundrawtransaction,
tx.serialize_with_witness().hex(),
{'include_unsafe' : True, 'max_tx_weight' : int(TRUC_CHILD_MAX_VSIZE/2)}
)
self.generate(self.nodes[0], 1)
self.alice.fundrawtransaction(tx.serialize_with_witness().hex())
@cleanup
def createpsbt_v3(self):
self.log.info("Test setting version to 3 with createpsbt")
outputs = {self.alice.getnewaddress() : 10}
psbt = self.charlie.createpsbt(inputs=[], outputs=outputs, version=3)
assert_equal(self.charlie.decodepsbt(psbt)["tx"]["version"], 3)
@cleanup
def send_v3(self):
self.log.info("Test setting version to 3 with send")
outputs = {self.alice.getnewaddress() : 10}
tx_hex = self.charlie.send(outputs=outputs, add_to_wallet=False, version=3)["hex"]
assert_equal(self.charlie.decoderawtransaction(tx_hex)["version"], 3)
@cleanup
def sendall_v3(self):
self.log.info("Test setting version to 3 with sendall")
tx_hex = self.charlie.sendall(recipients=[self.alice.getnewaddress()], version=3, add_to_wallet=False)["hex"]
assert_equal(self.charlie.decoderawtransaction(tx_hex)["version"], 3)
@cleanup
def sendall_with_unconfirmed_v3(self):
self.log.info("Test setting version to 3 with sendall + unconfirmed inputs")
outputs = {self.alice.getnewaddress(): 2.00001 for _ in range(4)}
self.send_tx(self.charlie, [], outputs, 2)
self.generate(self.nodes[0], 1)
unspents = self.alice.listunspent()
# confirmed v2 utxos
outputs = {self.alice.getnewaddress() : 2.0}
confirmed_v2 = self.send_tx(self.alice, [unspents[0]], outputs, 2)
# confirmed v3 utxos
outputs = {self.alice.getnewaddress() : 2.0}
confirmed_v3 = self.send_tx(self.alice, [unspents[1]], outputs, 3)
self.generate(self.nodes[0], 1)
# unconfirmed v2 utxos
outputs = {self.alice.getnewaddress() : 2.0}
unconfirmed_v2 = self.send_tx(self.alice, [unspents[2]], outputs, 2)
# unconfirmed v3 utxos
outputs = {self.alice.getnewaddress() : 2.0}
unconfirmed_v3 = self.send_tx(self.alice, [unspents[3]], outputs, 3)
# Test that the only unconfirmed inputs this v3 tx spends are v3
tx_hex = self.alice.sendall([self.bob.getnewaddress()], version=3, add_to_wallet=False, minconf=0)["hex"]
decoded_tx = self.alice.decoderawtransaction(tx_hex)
decoded_vin_txids = [txin["txid"] for txin in decoded_tx["vin"]]
assert_equal(decoded_tx["version"], 3)
assert confirmed_v3 in decoded_vin_txids
assert confirmed_v2 in decoded_vin_txids
assert unconfirmed_v3 in decoded_vin_txids
assert unconfirmed_v2 not in decoded_vin_txids
# Test that the only unconfirmed inputs this v2 tx spends are v2
tx_hex = self.alice.sendall([self.bob.getnewaddress()], version=2, add_to_wallet=False, minconf=0)["hex"]
decoded_tx = self.alice.decoderawtransaction(tx_hex)
decoded_vin_txids = [txin["txid"] for txin in decoded_tx["vin"]]
assert_equal(decoded_tx["version"], 2)
assert confirmed_v3 in decoded_vin_txids
assert confirmed_v2 in decoded_vin_txids
assert unconfirmed_v2 in decoded_vin_txids
assert unconfirmed_v3 not in decoded_vin_txids
@cleanup
def walletcreatefundedpsbt_v3(self):
self.log.info("Test setting version to 3 with walletcreatefundedpsbt")
outputs = {self.alice.getnewaddress() : 10}
psbt = self.charlie.walletcreatefundedpsbt(inputs=[], outputs=outputs, version=3)["psbt"]
assert_equal(self.charlie.decodepsbt(psbt)["tx"]["version"], 3)
@cleanup
def sendall_truc_weight_limit(self):
self.log.info("Test that sendall follows truc tx weight limit")
self.charlie.sendall([self.alice.getnewaddress() for _ in range(300)], add_to_wallet=False, version=2)
# check that error is only raised if version is 3
assert_raises_rpc_error(
-4,
"Transaction too large" ,
self.charlie.sendall,
[self.alice.getnewaddress() for _ in range(300)],
version=3
)
@cleanup
def sendall_truc_child_weight_limit(self):
self.log.info("Test that sendall follows spending unconfirmed truc tx weight limit")
outputs = {self.charlie.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 3)
self.charlie.sendall([self.alice.getnewaddress() for _ in range(50)], add_to_wallet=False)
assert_raises_rpc_error(
-4,
"Transaction too large" ,
self.charlie.sendall,
[self.alice.getnewaddress() for _ in range(50)],
version=3
)
@cleanup
def mix_non_truc_versions(self):
self.log.info("Test that we can mix non-truc versions when spending an unconfirmed output")
outputs = {self.bob.getnewaddress() : 2.0}
self.send_tx(self.charlie, [], outputs, 1)
assert_equal(self.bob.getbalances()["mine"]["trusted"], 0)
assert_greater_than(self.bob.getbalances()["mine"]["untrusted_pending"], 0)
outputs = {self.alice.getnewaddress() : 1.0}
raw_tx_v2 = self.bob.createrawtransaction(inputs=[], outputs=outputs, version=2)
# does not throw an error
self.bob.fundrawtransaction(raw_tx_v2, {'include_unsafe': True})["hex"]
@cleanup
def cant_spend_multiple_unconfirmed_truc_outputs(self):
self.log.info("Test that we can't spend multiple unconfirmed truc outputs")
outputs = {self.alice.getnewaddress(): 2.00001}
self.send_tx(self.charlie, [], outputs, 3)
self.send_tx(self.charlie, [], outputs, 3)
assert_equal(len(self.alice.listunspent(minconf=0)), 2)
outputs = {self.bob.getnewaddress() : 3.0}
raw_tx = self.alice.createrawtransaction(inputs=[], outputs=outputs, version=3)
assert_raises_rpc_error(
-4,
"Insufficient funds",
self.alice.fundrawtransaction,
raw_tx,
{'include_unsafe' : True}
)
if __name__ == '__main__':
WalletV3Test(__file__).main()