Skip to content

Commit 79804fe

Browse files
committed
[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.
1 parent 5c73645 commit 79804fe

File tree

6 files changed

+47
-16
lines changed

6 files changed

+47
-16
lines changed

doc/release-notes-16377.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
RPC changes
2+
-----------
3+
- The `walletcreatefundedpsbt` RPC call will now fail with
4+
`Insufficient funds` when inputs are manually selected but are not enough to cover
5+
the outputs and fee. Additional inputs can automatically be added through the
6+
new `add_inputs` option.

src/wallet/coincontrol.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ void CCoinControl::SetNull()
1010
{
1111
destChange = CNoDestination();
1212
m_change_type.reset();
13+
m_add_inputs = true;
1314
fAllowOtherInputs = false;
1415
fAllowWatchOnly = false;
1516
m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS);
@@ -23,4 +24,3 @@ void CCoinControl::SetNull()
2324
m_min_depth = DEFAULT_MIN_DEPTH;
2425
m_max_depth = DEFAULT_MAX_DEPTH;
2526
}
26-

src/wallet/coincontrol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class CCoinControl
2626
CTxDestination destChange;
2727
//! Override the default change type if set, ignored if destChange is set
2828
Optional<OutputType> m_change_type;
29+
//! If false, only selected inputs are used
30+
bool m_add_inputs;
2931
//! If false, allows unselected inputs, but requires all selected inputs be used
3032
bool fAllowOtherInputs;
3133
//! Includes watch only addresses which are solvable

src/wallet/rpcwallet.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3016,13 +3016,12 @@ static UniValue listunspent(const JSONRPCRequest& request)
30163016
return results;
30173017
}
30183018

3019-
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options)
3019+
void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& fee_out, int& change_position, UniValue options, CCoinControl& coinControl)
30203020
{
30213021
// Make sure the results are valid at least up to the most recent block
30223022
// the user could have gotten from another RPC command prior to now
30233023
pwallet->BlockUntilSyncedToCurrentChain();
30243024

3025-
CCoinControl coinControl;
30263025
change_position = -1;
30273026
bool lockUnspents = false;
30283027
UniValue subtractFeeFromOutputs;
@@ -3037,6 +3036,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
30373036
RPCTypeCheckArgument(options, UniValue::VOBJ);
30383037
RPCTypeCheckObj(options,
30393038
{
3039+
{"add_inputs", UniValueType(UniValue::VBOOL)},
30403040
{"changeAddress", UniValueType(UniValue::VSTR)},
30413041
{"changePosition", UniValueType(UniValue::VNUM)},
30423042
{"change_type", UniValueType(UniValue::VSTR)},
@@ -3050,6 +3050,10 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
30503050
},
30513051
true, true);
30523052

3053+
if (options.exists("add_inputs") ) {
3054+
coinControl.m_add_inputs = options["add_inputs"].get_bool();
3055+
}
3056+
30533057
if (options.exists("changeAddress")) {
30543058
CTxDestination dest = DecodeDestination(options["changeAddress"].get_str());
30553059

@@ -3224,7 +3228,8 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
32243228

32253229
CAmount fee;
32263230
int change_position;
3227-
FundTransaction(pwallet, tx, fee, change_position, request.params[1]);
3231+
CCoinControl coin_control;
3232+
FundTransaction(pwallet, tx, fee, change_position, request.params[1], coin_control);
32283233

32293234
UniValue result(UniValue::VOBJ);
32303235
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
@@ -4146,10 +4151,10 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
41464151
}
41474152

41484153
RPCHelpMan{"walletcreatefundedpsbt",
4149-
"\nCreates and funds a transaction in the Partially Signed Transaction format. Inputs will be added if supplied inputs are not enough\n"
4154+
"\nCreates and funds a transaction in the Partially Signed Transaction format.\n"
41504155
"Implements the Creator and Updater roles.\n",
41514156
{
4152-
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The inputs",
4157+
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The inputs. Leave empty to add inputs automatically. See add_inputs option.",
41534158
{
41544159
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
41554160
{
@@ -4180,6 +4185,7 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
41804185
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
41814186
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
41824187
{
4188+
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
41834189
{"changeAddress", RPCArg::Type::STR_HEX, /* default */ "pool address", "The bitcoin address to receive the change"},
41844190
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
41854191
{"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)
42374243
rbf = replaceable_arg.isTrue();
42384244
}
42394245
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
4240-
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3]);
4246+
CCoinControl coin_control;
4247+
// Automatically select coins, unless at least one is manually selected. Can
4248+
// be overriden by options.add_inputs.
4249+
coin_control.m_add_inputs = rawTx.vin.size() == 0;
4250+
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], coin_control);
42414251

42424252
// Make a blank psbt
42434253
PartiallySignedTransaction psbtx(rawTx);

src/wallet/wallet.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,6 +2149,11 @@ void CWallet::AvailableCoins(interfaces::Chain::Lock& locked_chain, std::vector<
21492149
}
21502150

21512151
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
2152+
// Only consider selected coins if add_inputs is false
2153+
if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(entry.first, i))) {
2154+
continue;
2155+
}
2156+
21522157
if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount)
21532158
continue;
21542159

test/functional/rpc_psbt.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from decimal import Decimal
99
from test_framework.test_framework import BitcoinTestFramework
1010
from test_framework.util import (
11+
assert_approx,
1112
assert_equal,
1213
assert_greater_than,
1314
assert_raises_rpc_error,
@@ -80,6 +81,13 @@ def run_test(self):
8081
# Create and fund a raw tx for sending 10 BTC
8182
psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']
8283

84+
# If inputs are specified, do not automatically add more:
85+
utxo1 = self.nodes[0].listunspent()[0]
86+
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
87+
88+
psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt']
89+
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2)
90+
8391
# Node 1 should not be able to add anything to it but still return the psbtx same as before
8492
psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt']
8593
assert_equal(psbtx1, psbtx)
@@ -137,13 +145,13 @@ def run_test(self):
137145
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
138146

139147
# feeRate of 0.1 BTC / KB produces a total fee slightly below -maxtxfee (~0.05280000):
140-
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})
141-
assert_greater_than(res["fee"], 0.05)
142-
assert_greater_than(0.06, res["fee"])
148+
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})
149+
assert_approx(res["fee"], 0.055, 0.005)
143150

144151
# feeRate of 10 BTC / KB produces a total fee well above -maxtxfee
145152
# previously this was silently capped at -maxtxfee
146-
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})
153+
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})
154+
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})
147155

148156
# partially sign multisig things with node 1
149157
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,23 +229,23 @@ def run_test(self):
221229
# replaceable arg
222230
block_height = self.nodes[0].getblockcount()
223231
unspent = self.nodes[0].listunspent()[0]
224-
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)
232+
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)
225233
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
226234
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
227235
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
228236
assert "bip32_derivs" not in psbt_in
229237
assert_equal(decoded_psbt["tx"]["locktime"], block_height+2)
230238

231239
# Same construction with only locktime set and RBF explicitly enabled
232-
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height, {"replaceable": True}, True)
240+
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)
233241
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
234242
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
235243
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
236244
assert "bip32_derivs" in psbt_in
237245
assert_equal(decoded_psbt["tx"]["locktime"], block_height)
238246

239247
# Same construction without optional arguments
240-
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
248+
psbtx_info = self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
241249
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
242250
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
243251
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
@@ -246,7 +254,7 @@ def run_test(self):
246254

247255
# Same construction without optional arguments, for a node with -walletrbf=0
248256
unspent1 = self.nodes[1].listunspent()[0]
249-
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height)
257+
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height, {"add_inputs": True})
250258
decoded_psbt = self.nodes[1].decodepsbt(psbtx_info["psbt"])
251259
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
252260
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
@@ -257,7 +265,7 @@ def run_test(self):
257265
self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"changeAddress":self.nodes[1].getnewaddress()}, False)
258266

259267
# Regression test for 14473 (mishandling of already-signed witness transaction):
260-
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
268+
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], 0, {"add_inputs": True})
261269
complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"])
262270
double_processed_psbt = self.nodes[0].walletprocesspsbt(complete_psbt["psbt"])
263271
assert_equal(complete_psbt, double_processed_psbt)

0 commit comments

Comments
 (0)