diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 0a33ae732a4..1fbe62d3d38 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -119,6 +119,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createrawtransaction", 1, "outputs" }, { "createrawtransaction", 2, "locktime" }, { "createrawtransaction", 3, "replaceable" }, + { "createrawtransaction", 4, "version" }, { "decoderawtransaction", 1, "iswitness" }, { "signrawtransactionwithkey", 1, "privkeys" }, { "signrawtransactionwithkey", 2, "prevtxs" }, @@ -167,6 +168,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "walletcreatefundedpsbt", 3, "solving_data"}, { "walletcreatefundedpsbt", 3, "max_tx_weight"}, { "walletcreatefundedpsbt", 4, "bip32derivs" }, + { "walletcreatefundedpsbt", 5, "version" }, { "walletprocesspsbt", 1, "sign" }, { "walletprocesspsbt", 3, "bip32derivs" }, { "walletprocesspsbt", 4, "finalize" }, @@ -177,6 +179,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createpsbt", 1, "outputs" }, { "createpsbt", 2, "locktime" }, { "createpsbt", 3, "replaceable" }, + { "createpsbt", 4, "version" }, { "combinepsbt", 0, "txs"}, { "joinpsbts", 0, "txs"}, { "finalizepsbt", 1, "extract"}, @@ -213,6 +216,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "send", 4, "replaceable"}, { "send", 4, "solving_data"}, { "send", 4, "max_tx_weight"}, + { "send", 5, "version"}, { "sendall", 0, "recipients" }, { "sendall", 1, "conf_target" }, { "sendall", 3, "fee_rate"}, @@ -230,6 +234,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendall", 4, "conf_target"}, { "sendall", 4, "replaceable"}, { "sendall", 4, "solving_data"}, + { "sendall", 4, "version"}, { "simulaterawtransaction", 0, "rawtxs" }, { "simulaterawtransaction", 1, "options" }, { "simulaterawtransaction", 1, "include_watchonly"}, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index c87741a5389..b7f458ea294 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -53,6 +53,8 @@ using node::GetTransaction; using node::NodeContext; using node::PSBTAnalysis; +static constexpr decltype(CTransaction::version) DEFAULT_RAWTX_VERSION{CTransaction::CURRENT_VERSION}; + static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS) @@ -158,6 +160,7 @@ static std::vector CreateTxDoc() {"locktime", RPCArg::Type::NUM, RPCArg::Default{0}, "Raw locktime. Non-0 value also locktime-activates inputs"}, {"replaceable", RPCArg::Type::BOOL, RPCArg::Default{true}, "Marks this transaction as BIP125-replaceable.\n" "Allows this transaction to be replaced by a transaction with higher fees. If provided, it is an error if explicit sequence numbers are incompatible."}, + {"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_RAWTX_VERSION}, "Transaction version"}, }; } @@ -437,7 +440,7 @@ static RPCHelpMan createrawtransaction() if (!request.params[3].isNull()) { rbf = request.params[3].get_bool(); } - CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf); + CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, self.Arg("version")); return EncodeHexTx(CTransaction(rawTx)); }, @@ -1679,7 +1682,7 @@ static RPCHelpMan createpsbt() if (!request.params[3].isNull()) { rbf = request.params[3].get_bool(); } - CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf); + CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, self.Arg("version")); // Make a blank psbt PartiallySignedTransaction psbtx; diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 17a111ae0bc..2fba6c03bef 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -143,7 +144,7 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in) } } -CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf) +CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf, const uint32_t version) { CMutableTransaction rawTx; @@ -154,6 +155,11 @@ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniVal rawTx.nLockTime = nLockTime; } + if (version < TX_MIN_STANDARD_VERSION || version > TX_MAX_STANDARD_VERSION) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, version out of range(%d~%d)", TX_MIN_STANDARD_VERSION, TX_MAX_STANDARD_VERSION)); + } + rawTx.version = version; + AddInputs(rawTx, inputs_in, rbf); AddOutputs(rawTx, outputs_in); diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index 40d6bbba873..33017e91879 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -53,6 +53,6 @@ std::vector> ParseOutputs(const UniValue& out void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in); /** Create a transaction from univalue parameters */ -CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf); +CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf, const uint32_t version); #endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 5da02b4df4e..0604dec2dc9 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -731,6 +731,7 @@ TMPL_INST(CheckRequiredOrDefault, const UniValue&, *CHECK_NONFATAL(maybe_arg);); TMPL_INST(CheckRequiredOrDefault, bool, CHECK_NONFATAL(maybe_arg)->get_bool();); TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt();); TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt();); +TMPL_INST(CheckRequiredOrDefault, uint32_t, CHECK_NONFATAL(maybe_arg)->getInt();); TMPL_INST(CheckRequiredOrDefault, const std::string&, CHECK_NONFATAL(maybe_arg)->get_str();); bool RPCHelpMan::IsValidNumArgs(size_t num_args) const diff --git a/src/wallet/coincontrol.h b/src/wallet/coincontrol.h index 1cba19af6bf..6431dc86951 100644 --- a/src/wallet/coincontrol.h +++ b/src/wallet/coincontrol.h @@ -21,6 +21,8 @@ namespace wallet { const int DEFAULT_MIN_DEPTH = 0; const int DEFAULT_MAX_DEPTH = 9999999; +const int DEFAULT_WALLET_TX_VERSION = CTransaction::CURRENT_VERSION; + //! Default for -avoidpartialspends static constexpr bool DEFAULT_AVOIDPARTIALSPENDS = false; @@ -110,7 +112,7 @@ public: //! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs FlatSigningProvider m_external_provider; //! Version - uint32_t m_version = CTransaction::CURRENT_VERSION; + uint32_t m_version = DEFAULT_WALLET_TX_VERSION; //! Locktime std::optional m_locktime; //! Caps weight of resulting tx diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index b643183e0ee..dd7c9172d98 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -1276,6 +1276,7 @@ RPCHelpMan send() }, FundTxDoc()), RPCArgOptions{.oneline_description="options"}}, + {"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -1315,8 +1316,9 @@ RPCHelpMan send() ParseOutputs(outputs), InterpretSubtractFeeFromOutputInstructions(options["subtract_fee_from_outputs"], outputs.getKeys()) ); - CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf); CCoinControl coin_control; + coin_control.m_version = self.Arg("version"); + CMutableTransaction rawTx = ConstructTransaction(options["inputs"], request.params[0], options["locktime"], rbf, coin_control.m_version); // Automatically select coins, unless at least one is manually selected. Can // be overridden by options.add_inputs. coin_control.m_allow_other_inputs = rawTx.vin.size() == 0; @@ -1383,6 +1385,7 @@ RPCHelpMan sendall() {"send_max", RPCArg::Type::BOOL, RPCArg::Default{false}, "When true, only use UTXOs that can pay for their own fees to maximize the output amount. When 'false' (default), no UTXO is left behind. send_max is incompatible with providing specific inputs."}, {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Require inputs with at least this many confirmations."}, {"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Require inputs with at most this many confirmations."}, + {"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"}, }, FundTxDoc() ), @@ -1463,6 +1466,10 @@ RPCHelpMan sendall() } } + if (options.exists("version")) { + coin_control.m_version = options["version"].getInt(); + } + if (coin_control.m_version == TRUC_VERSION) { coin_control.m_max_tx_weight = TRUC_MAX_WEIGHT; } else { @@ -1483,7 +1490,7 @@ RPCHelpMan sendall() throw JSONRPCError(RPC_WALLET_ERROR, "Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee."); } - CMutableTransaction rawTx{ConstructTransaction(options["inputs"], recipient_key_value_pairs, options["locktime"], rbf)}; + CMutableTransaction rawTx{ConstructTransaction(options["inputs"], recipient_key_value_pairs, options["locktime"], rbf, coin_control.m_version)}; LOCK(pwallet->cs_wallet); CAmount total_input_value(0); @@ -1501,6 +1508,13 @@ RPCHelpMan sendall() if (!tx || input.prevout.n >= tx->tx->vout.size() || !(pwallet->IsMine(tx->tx->vout[input.prevout.n]) & ISMINE_SPENDABLE)) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input not found. UTXO (%s:%d) is not part of wallet.", input.prevout.hash.ToString(), input.prevout.n)); } + if (pwallet->GetTxDepthInMainChain(*tx) == 0) { + if (tx->tx->version == TRUC_VERSION && coin_control.m_version != TRUC_VERSION) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Can't spend unconfirmed version 3 pre-selected input with a version %d tx", coin_control.m_version)); + } else if (coin_control.m_version == TRUC_VERSION && tx->tx->version != TRUC_VERSION) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Can't spend unconfirmed version %d pre-selected input with a version 3 tx", tx->tx->version)); + } + } total_input_value += tx->tx->vout[input.prevout.n].nValue; } } else { @@ -1749,6 +1763,7 @@ RPCHelpMan walletcreatefundedpsbt() FundTxDoc()), RPCArgOptions{.oneline_description="options"}}, {"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"}, + {"version", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_WALLET_TX_VERSION}, "Transaction version"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -1776,16 +1791,18 @@ RPCHelpMan walletcreatefundedpsbt() UniValue options{request.params[3].isNull() ? UniValue::VOBJ : request.params[3]}; + CCoinControl coin_control; + coin_control.m_version = self.Arg("version"); + const UniValue &replaceable_arg = options["replaceable"]; const bool rbf{replaceable_arg.isNull() ? wallet.m_signal_rbf : replaceable_arg.get_bool()}; - CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf); + CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, coin_control.m_version); UniValue outputs(UniValue::VOBJ); outputs = NormalizeOutputs(request.params[1]); std::vector recipients = CreateRecipients( ParseOutputs(outputs), InterpretSubtractFeeFromOutputInstructions(options["subtractFeeFromOutputs"], outputs.getKeys()) ); - CCoinControl coin_control; // Automatically select coins, unless at least one is manually selected. Can // be overridden by options.add_inputs. coin_control.m_allow_other_inputs = rawTx.vin.size() == 0; diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index b22d5731871..348b2b67e84 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -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 diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 4b93768d79c..d2610a1d79f 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -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 diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 2c815ce596e..ebb306a8234 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -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",