mirror of https://github.com/bitcoin/bitcoin.git
rpc: Support version 3 transaction creation
Adds v3 support to the following RPCs: - createrawtransaction - createpsbt - send - sendall - walletcreatefundedpsbt Co-authored-by: chungeun-choi <cucuridas@gmail.com> Co-authored-by: dongwook-chan <dongwook.chan@gmail.com> Co-authored-by: sean-k1 <uhs2000@naver.com> Co-authored-by: ishaanam <ishaana.misra@gmail.com>
This commit is contained in:
parent
4c20343b4d
commit
2cb473d9f2
|
@ -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"},
|
||||
|
|
|
@ -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<RPCArg> 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<uint32_t>("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<uint32_t>("version"));
|
||||
|
||||
// Make a blank psbt
|
||||
PartiallySignedTransaction psbtx;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <tinyformat.h>
|
||||
#include <univalue.h>
|
||||
#include <util/rbf.h>
|
||||
#include <util/string.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/translation.h>
|
||||
|
||||
|
@ -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<bool> rbf)
|
||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> 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);
|
||||
|
||||
|
|
|
@ -53,6 +53,6 @@ std::vector<std::pair<CTxDestination, CAmount>> 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<bool> rbf);
|
||||
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf, const uint32_t version);
|
||||
|
||||
#endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H
|
||||
|
|
|
@ -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<int>(););
|
||||
TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
|
||||
TMPL_INST(CheckRequiredOrDefault, uint32_t, CHECK_NONFATAL(maybe_arg)->getInt<uint32_t>(););
|
||||
TMPL_INST(CheckRequiredOrDefault, const std::string&, CHECK_NONFATAL(maybe_arg)->get_str(););
|
||||
|
||||
bool RPCHelpMan::IsValidNumArgs(size_t num_args) const
|
||||
|
|
|
@ -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<uint32_t> m_locktime;
|
||||
//! Caps weight of resulting tx
|
||||
|
|
|
@ -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<uint32_t>("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<int>();
|
||||
}
|
||||
|
||||
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<uint32_t>("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<CRecipient> 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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue