mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-19 06:43:45 +01:00
[rpc] walletcreatefundedpsbt: don't automatically append inputs
When the user doesn't specificy inputs, it makes sense to automatically select them. But when the user does specify inputs, we now fail if the amount is insufficient, unless addInputs is set to true.
This commit is contained in:
6
doc/release-notes-16377.md
Normal file
6
doc/release-notes-16377.md
Normal file
@@ -0,0 +1,6 @@
|
||||
RPC changes
|
||||
-----------
|
||||
- The `walletcreatefundedpsbt` RPC call will now fail with
|
||||
`Insufficient funds` when inputs are manually selected but are not enough to cover
|
||||
the outputs and fee. Additional inputs can automatically be added through the
|
||||
new `add_inputs` option.
|
||||
@@ -10,6 +10,7 @@ void CCoinControl::SetNull()
|
||||
{
|
||||
destChange = CNoDestination();
|
||||
m_change_type.reset();
|
||||
m_add_inputs = true;
|
||||
fAllowOtherInputs = false;
|
||||
fAllowWatchOnly = false;
|
||||
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
|
||||
@@ -23,4 +24,3 @@ void CCoinControl::SetNull()
|
||||
m_min_depth = DEFAULT_MIN_DEPTH;
|
||||
m_max_depth = DEFAULT_MAX_DEPTH;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ public:
|
||||
CTxDestination destChange;
|
||||
//! Override the default change type if set, ignored if destChange is set
|
||||
Optional<OutputType> m_change_type;
|
||||
//! If false, only selected inputs are used
|
||||
bool m_add_inputs;
|
||||
//! If false, allows unselected inputs, but requires all selected inputs be used
|
||||
bool fAllowOtherInputs;
|
||||
//! Includes watch only addresses which are solvable
|
||||
|
||||
@@ -3016,13 +3016,12 @@ static UniValue listunspent(const JSONRPCRequest& request)
|
||||
return results;
|
||||
}
|
||||
|
||||
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options)
|
||||
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options, CCoinControl& coinControl)
|
||||
{
|
||||
// Make sure the results are valid at least up to the most recent block
|
||||
// the user could have gotten from another RPC command prior to now
|
||||
pwallet->BlockUntilSyncedToCurrentChain();
|
||||
|
||||
CCoinControl coinControl;
|
||||
change_position = -1;
|
||||
bool lockUnspents = false;
|
||||
UniValue subtractFeeFromOutputs;
|
||||
@@ -3037,6 +3036,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
|
||||
RPCTypeCheckArgument(options, UniValue::VOBJ);
|
||||
RPCTypeCheckObj(options,
|
||||
{
|
||||
{"add_inputs", UniValueType(UniValue::VBOOL)},
|
||||
{"changeAddress", UniValueType(UniValue::VSTR)},
|
||||
{"changePosition", UniValueType(UniValue::VNUM)},
|
||||
{"change_type", UniValueType(UniValue::VSTR)},
|
||||
@@ -3050,6 +3050,10 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
|
||||
},
|
||||
true, true);
|
||||
|
||||
if (options.exists("add_inputs") ) {
|
||||
coinControl.m_add_inputs = options["add_inputs"].get_bool();
|
||||
}
|
||||
|
||||
if (options.exists("changeAddress")) {
|
||||
CTxDestination dest = DecodeDestination(options["changeAddress"].get_str());
|
||||
|
||||
@@ -3224,7 +3228,8 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
|
||||
|
||||
CAmount fee;
|
||||
int change_position;
|
||||
FundTransaction(pwallet, tx, fee, change_position, request.params[1]);
|
||||
CCoinControl coin_control;
|
||||
FundTransaction(pwallet, tx, fee, change_position, request.params[1], coin_control);
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
|
||||
@@ -4146,10 +4151,10 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
|
||||
}
|
||||
|
||||
RPCHelpMan{"walletcreatefundedpsbt",
|
||||
"\nCreates and funds a transaction in the Partially Signed Transaction format. Inputs will be added if supplied inputs are not enough\n"
|
||||
"\nCreates and funds a transaction in the Partially Signed Transaction format.\n"
|
||||
"Implements the Creator and Updater roles.\n",
|
||||
{
|
||||
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The inputs",
|
||||
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The inputs. Leave empty to add inputs automatically. See add_inputs option.",
|
||||
{
|
||||
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
|
||||
{
|
||||
@@ -4180,6 +4185,7 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
|
||||
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
|
||||
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
|
||||
{
|
||||
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
|
||||
{"changeAddress", RPCArg::Type::STR_HEX, /* default */ "pool address", "The bitcoin address to receive the change"},
|
||||
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
|
||||
{"change_type", RPCArg::Type::STR, /* default */ "set by -changetype", "The output type to use. Only valid if changeAddress is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},
|
||||
@@ -4237,7 +4243,11 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
|
||||
rbf = replaceable_arg.isTrue();
|
||||
}
|
||||
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
|
||||
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3]);
|
||||
CCoinControl coin_control;
|
||||
// Automatically select coins, unless at least one is manually selected. Can
|
||||
// be overriden by options.add_inputs.
|
||||
coin_control.m_add_inputs = rawTx.vin.size() == 0;
|
||||
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], coin_control);
|
||||
|
||||
// Make a blank psbt
|
||||
PartiallySignedTransaction psbtx(rawTx);
|
||||
|
||||
@@ -2149,6 +2149,11 @@ void CWallet::AvailableCoins(interfaces::Chain::Lock& locked_chain, std::vector<
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
|
||||
// Only consider selected coins if add_inputs is false
|
||||
if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(entry.first, i))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
from decimal import Decimal
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_approx,
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_raises_rpc_error,
|
||||
@@ -80,6 +81,13 @@ class PSBTTest(BitcoinTestFramework):
|
||||
# Create and fund a raw tx for sending 10 BTC
|
||||
psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']
|
||||
|
||||
# If inputs are specified, do not automatically add more:
|
||||
utxo1 = self.nodes[0].listunspent()[0]
|
||||
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
|
||||
|
||||
psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt']
|
||||
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2)
|
||||
|
||||
# Node 1 should not be able to add anything to it but still return the psbtx same as before
|
||||
psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt']
|
||||
assert_equal(psbtx1, psbtx)
|
||||
@@ -137,13 +145,13 @@ class PSBTTest(BitcoinTestFramework):
|
||||
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
|
||||
|
||||
# feeRate of 0.1 BTC / KB produces a total fee slightly below -maxtxfee (~0.05280000):
|
||||
res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 0.1})
|
||||
assert_greater_than(res["fee"], 0.05)
|
||||
assert_greater_than(0.06, res["fee"])
|
||||
res = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 0.1, "add_inputs": True})
|
||||
assert_approx(res["fee"], 0.055, 0.005)
|
||||
|
||||
# feeRate of 10 BTC / KB produces a total fee well above -maxtxfee
|
||||
# previously this was silently capped at -maxtxfee
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10})
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():29.99}, 0, {"feeRate": 10, "add_inputs": True})
|
||||
assert_raises_rpc_error(-4, "Fee exceeds maximum configured by -maxtxfee", self.nodes[1].walletcreatefundedpsbt, [{"txid":txid,"vout":p2wpkh_pos},{"txid":txid,"vout":p2sh_p2wpkh_pos},{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():1}, 0, {"feeRate": 10, "add_inputs": False})
|
||||
|
||||
# partially sign multisig things with node 1
|
||||
psbtx = self.nodes[1].walletcreatefundedpsbt([{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], {self.nodes[1].getnewaddress():29.99})['psbt']
|
||||
@@ -221,7 +229,7 @@ class PSBTTest(BitcoinTestFramework):
|
||||
# replaceable arg
|
||||
block_height = self.nodes[0].getblockcount()
|
||||
unspent = self.nodes[0].listunspent()[0]
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"replaceable": False}, False)
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"replaceable": False, "add_inputs": True}, False)
|
||||
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
|
||||
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
|
||||
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
|
||||
@@ -229,7 +237,7 @@ class PSBTTest(BitcoinTestFramework):
|
||||
assert_equal(decoded_psbt["tx"]["locktime"], block_height+2)
|
||||
|
||||
# Same construction with only locktime set and RBF explicitly enabled
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height, {"replaceable": True}, True)
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height, {"replaceable": True, "add_inputs": True}, True)
|
||||
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
|
||||
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
|
||||
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
|
||||
@@ -237,7 +245,7 @@ class PSBTTest(BitcoinTestFramework):
|
||||
assert_equal(decoded_psbt["tx"]["locktime"], block_height)
|
||||
|
||||
# Same construction without optional arguments
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
|
||||
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
|
||||
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
|
||||
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
|
||||
@@ -246,7 +254,7 @@ class PSBTTest(BitcoinTestFramework):
|
||||
|
||||
# Same construction without optional arguments, for a node with -walletrbf=0
|
||||
unspent1 = self.nodes[1].listunspent()[0]
|
||||
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height)
|
||||
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height, {"add_inputs": True})
|
||||
decoded_psbt = self.nodes[1].decodepsbt(psbtx_info["psbt"])
|
||||
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
|
||||
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
|
||||
@@ -257,7 +265,7 @@ class PSBTTest(BitcoinTestFramework):
|
||||
self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"changeAddress":self.nodes[1].getnewaddress()}, False)
|
||||
|
||||
# Regression test for 14473 (mishandling of already-signed witness transaction):
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
|
||||
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], 0, {"add_inputs": True})
|
||||
complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"])
|
||||
double_processed_psbt = self.nodes[0].walletprocesspsbt(complete_psbt["psbt"])
|
||||
assert_equal(complete_psbt, double_processed_psbt)
|
||||
|
||||
Reference in New Issue
Block a user