Skip to content

Commit 6bb5f6d

Browse files
committed
Merge #16377: [rpc] don't automatically append inputs in walletcreatefundedpsbt
e5327f9 [rpc] fundrawtransaction: add_inputs option to control automatic input adding (Sjors Provoost) 79804fe [rpc] walletcreatefundedpsbt: don't automatically append inputs (Sjors Provoost) Pull request description: When the user doesn't specificy inputs, it makes sense to automatically select them. But when the user does specify inputs, `walletcreatefundedpsbt` now fails if the amount is insufficient, unless `addInputs` is set to `true`. Similarly for `fundrawtransaction` if the original transaction already specified inputs, we only add more if `addInputs` is set to `true`. This protects against fat finger mistakes in the amount or fee rate (see also #16257). The behavior is also more similar to GUI coin selection. ACKs for top commit: achow101: ACK e5327f9 meshcollider: utACK e5327f9 Tree-SHA512: d8653b820914396c7c25b0d0a2b7e92de214aa023bc1aa085feb37d3b20fab361ebea90416a7db989f19bdc37e26cf0adfbcb712c80985c87afa67a9bd44fecb
2 parents bd331bd + e5327f9 commit 6bb5f6d

File tree

7 files changed

+67
-20
lines changed

7 files changed

+67
-20
lines changed

doc/release-notes-16377.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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.
7+
8+
- The `fundrawtransaction` RPC now supports `add_inputs` option that when `false`
9+
prevents adding more inputs if necessary and consequently the RPC fails.

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: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2918,13 +2918,12 @@ static UniValue listunspent(const JSONRPCRequest& request)
29182918
return results;
29192919
}
29202920

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

2927-
CCoinControl coinControl;
29282927
change_position = -1;
29292928
bool lockUnspents = false;
29302929
UniValue subtractFeeFromOutputs;
@@ -2939,6 +2938,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
29392938
RPCTypeCheckArgument(options, UniValue::VOBJ);
29402939
RPCTypeCheckObj(options,
29412940
{
2941+
{"add_inputs", UniValueType(UniValue::VBOOL)},
29422942
{"changeAddress", UniValueType(UniValue::VSTR)},
29432943
{"changePosition", UniValueType(UniValue::VNUM)},
29442944
{"change_type", UniValueType(UniValue::VSTR)},
@@ -2952,6 +2952,10 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
29522952
},
29532953
true, true);
29542954

2955+
if (options.exists("add_inputs") ) {
2956+
coinControl.m_add_inputs = options["add_inputs"].get_bool();
2957+
}
2958+
29552959
if (options.exists("changeAddress")) {
29562960
CTxDestination dest = DecodeDestination(options["changeAddress"].get_str());
29572961

@@ -3039,8 +3043,8 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
30393043
static UniValue fundrawtransaction(const JSONRPCRequest& request)
30403044
{
30413045
RPCHelpMan{"fundrawtransaction",
3042-
"\nAdd inputs to a transaction until it has enough in value to meet its out value.\n"
3043-
"This will not modify existing inputs, and will add at most one change output to the outputs.\n"
3046+
"\nIf the transaction has no inputs, they will be automatically selected to meet its out value.\n"
3047+
"It will add at most one change output to the outputs.\n"
30443048
"No existing outputs will be modified unless \"subtractFeeFromOutputs\" is specified.\n"
30453049
"Note that inputs which were signed may need to be resigned after completion since in/outputs have been added.\n"
30463050
"The inputs added will not be signed, use signrawtransactionwithkey\n"
@@ -3054,6 +3058,7 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
30543058
{"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex string of the raw transaction"},
30553059
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "for backward compatibility: passing in a true instead of an object will result in {\"includeWatching\":true}",
30563060
{
3061+
{"add_inputs", RPCArg::Type::BOOL, /* default */ "true", "For a transaction with existing inputs, automatically include more if they are not enough."},
30573062
{"changeAddress", RPCArg::Type::STR, /* default */ "pool address", "The bitcoin address to receive the change"},
30583063
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
30593064
{"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\"."},
@@ -3123,7 +3128,10 @@ static UniValue fundrawtransaction(const JSONRPCRequest& request)
31233128

31243129
CAmount fee;
31253130
int change_position;
3126-
FundTransaction(pwallet, tx, fee, change_position, request.params[1]);
3131+
CCoinControl coin_control;
3132+
// Automatically select (additional) coins. Can be overriden by options.add_inputs.
3133+
coin_control.m_add_inputs = true;
3134+
FundTransaction(pwallet, tx, fee, change_position, request.params[1], coin_control);
31273135

31283136
UniValue result(UniValue::VOBJ);
31293137
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
@@ -3976,10 +3984,10 @@ UniValue walletprocesspsbt(const JSONRPCRequest& request)
39763984
UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
39773985
{
39783986
RPCHelpMan{"walletcreatefundedpsbt",
3979-
"\nCreates and funds a transaction in the Partially Signed Transaction format. Inputs will be added if supplied inputs are not enough\n"
3987+
"\nCreates and funds a transaction in the Partially Signed Transaction format.\n"
39803988
"Implements the Creator and Updater roles.\n",
39813989
{
3982-
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The inputs",
3990+
{"inputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The inputs. Leave empty to add inputs automatically. See add_inputs option.",
39833991
{
39843992
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
39853993
{
@@ -4010,6 +4018,7 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
40104018
{"locktime", RPCArg::Type::NUM, /* default */ "0", "Raw locktime. Non-0 value also locktime-activates inputs"},
40114019
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
40124020
{
4021+
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
40134022
{"changeAddress", RPCArg::Type::STR_HEX, /* default */ "pool address", "The bitcoin address to receive the change"},
40144023
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
40154024
{"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\"."},
@@ -4071,7 +4080,11 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
40714080
rbf = replaceable_arg.isTrue();
40724081
}
40734082
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf);
4074-
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3]);
4083+
CCoinControl coin_control;
4084+
// Automatically select coins, unless at least one is manually selected. Can
4085+
// be overriden by options.add_inputs.
4086+
coin_control.m_add_inputs = rawTx.vin.size() == 0;
4087+
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], coin_control);
40754088

40764089
// Make a blank psbt
40774090
PartiallySignedTransaction psbtx(rawTx);

src/wallet/wallet.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2160,6 +2160,11 @@ void CWallet::AvailableCoins(std::vector<COutput>& vCoins, bool fOnlySafe, const
21602160
}
21612161

21622162
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
2163+
// Only consider selected coins if add_inputs is false
2164+
if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(entry.first, i))) {
2165+
continue;
2166+
}
2167+
21632168
if (wtx.tx->vout[i].nValue < nMinimumAmount || wtx.tx->vout[i].nValue > nMaximumAmount)
21642169
continue;
21652170

test/functional/rpc_fundrawtransaction.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,11 @@ def test_coin_selection(self):
271271
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
272272
assert_equal("00", dec_tx['vin'][0]['scriptSig']['hex'])
273273

274+
# Should fail without add_inputs:
275+
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
276+
# add_inputs is enabled by default
274277
rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
278+
275279
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
276280
totalOut = 0
277281
matchingOuts = 0
@@ -299,7 +303,10 @@ def test_two_vin(self):
299303
dec_tx = self.nodes[2].decoderawtransaction(rawtx)
300304
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
301305

302-
rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
306+
# Should fail without add_inputs:
307+
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
308+
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})
309+
303310
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
304311
totalOut = 0
305312
matchingOuts = 0
@@ -330,7 +337,10 @@ def test_two_vin_two_vout(self):
330337
dec_tx = self.nodes[2].decoderawtransaction(rawtx)
331338
assert_equal(utx['txid'], dec_tx['vin'][0]['txid'])
332339

333-
rawtxfund = self.nodes[2].fundrawtransaction(rawtx)
340+
# Should fail without add_inputs:
341+
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx, {"add_inputs": False})
342+
rawtxfund = self.nodes[2].fundrawtransaction(rawtx, {"add_inputs": True})
343+
334344
dec_tx = self.nodes[2].decoderawtransaction(rawtxfund['hex'])
335345
totalOut = 0
336346
matchingOuts = 0

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,
@@ -85,6 +86,13 @@ def run_test(self):
8586
# Create and fund a raw tx for sending 10 BTC
8687
psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']
8788

89+
# If inputs are specified, do not automatically add more:
90+
utxo1 = self.nodes[0].listunspent()[0]
91+
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[0].walletcreatefundedpsbt, [{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90})
92+
93+
psbtx1 = self.nodes[0].walletcreatefundedpsbt([{"txid": utxo1['txid'], "vout": utxo1['vout']}], {self.nodes[2].getnewaddress():90}, 0, {"add_inputs": True})['psbt']
94+
assert_equal(len(self.nodes[0].decodepsbt(psbtx1)['tx']['vin']), 2)
95+
8896
# Node 1 should not be able to add anything to it but still return the psbtx same as before
8997
psbtx = self.nodes[1].walletprocesspsbt(psbtx1)['psbt']
9098
assert_equal(psbtx1, psbtx)
@@ -152,13 +160,13 @@ def run_test(self):
152160
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
153161

154162
# feeRate of 0.1 BTC / KB produces a total fee slightly below -maxtxfee (~0.05280000):
155-
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})
156-
assert_greater_than(res["fee"], 0.05)
157-
assert_greater_than(0.06, res["fee"])
163+
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})
164+
assert_approx(res["fee"], 0.055, 0.005)
158165

159166
# feeRate of 10 BTC / KB produces a total fee well above -maxtxfee
160167
# previously this was silently capped at -maxtxfee
161-
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})
168+
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})
169+
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})
162170

163171
# partially sign multisig things with node 1
164172
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs={self.nodes[1].getnewaddress():29.99}, options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt']
@@ -239,23 +247,23 @@ def run_test(self):
239247
# replaceable arg
240248
block_height = self.nodes[0].getblockcount()
241249
unspent = self.nodes[0].listunspent()[0]
242-
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)
250+
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)
243251
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
244252
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
245253
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
246254
assert "bip32_derivs" not in psbt_in
247255
assert_equal(decoded_psbt["tx"]["locktime"], block_height+2)
248256

249257
# Same construction with only locktime set and RBF explicitly enabled
250-
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height, {"replaceable": True}, True)
258+
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)
251259
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
252260
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
253261
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
254262
assert "bip32_derivs" in psbt_in
255263
assert_equal(decoded_psbt["tx"]["locktime"], block_height)
256264

257265
# Same construction without optional arguments
258-
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
266+
psbtx_info = self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
259267
decoded_psbt = self.nodes[0].decodepsbt(psbtx_info["psbt"])
260268
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
261269
assert_equal(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
@@ -264,7 +272,7 @@ def run_test(self):
264272

265273
# Same construction without optional arguments, for a node with -walletrbf=0
266274
unspent1 = self.nodes[1].listunspent()[0]
267-
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height)
275+
psbtx_info = self.nodes[1].walletcreatefundedpsbt([{"txid":unspent1["txid"], "vout":unspent1["vout"]}], [{self.nodes[2].getnewaddress():unspent1["amount"]+1}], block_height, {"add_inputs": True})
268276
decoded_psbt = self.nodes[1].decodepsbt(psbtx_info["psbt"])
269277
for tx_in, psbt_in in zip(decoded_psbt["tx"]["vin"], decoded_psbt["inputs"]):
270278
assert_greater_than(tx_in["sequence"], MAX_BIP125_RBF_SEQUENCE)
@@ -275,7 +283,7 @@ def run_test(self):
275283
self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"changeAddress":self.nodes[1].getnewaddress()}, False)
276284

277285
# Regression test for 14473 (mishandling of already-signed witness transaction):
278-
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
286+
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], 0, {"add_inputs": True})
279287
complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"])
280288
double_processed_psbt = self.nodes[0].walletprocesspsbt(complete_psbt["psbt"])
281289
assert_equal(complete_psbt, double_processed_psbt)

0 commit comments

Comments
 (0)