Skip to content

Commit 69c5aa8

Browse files
committed
merge bitcoin#21359: include_unsafe option for fundrawtransaction
1 parent 169dce7 commit 69c5aa8

File tree

8 files changed

+101
-12
lines changed

8 files changed

+101
-12
lines changed

doc/release-notes-21359.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Wallet
2+
------
3+
4+
- The `fundrawtransaction`, `send` and `walletcreatefundedpsbt` RPCs now support an `include_unsafe` option
5+
that when `true` allows using unsafe inputs to fund the transaction.
6+
Note that the resulting transaction may become invalid if one of the unsafe inputs disappears.
7+
If that happens, the transaction must be funded with different inputs and republished.

src/wallet/coincontrol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class CCoinControl
4040
CTxDestination destChange = CNoDestination();
4141
//! If false, only selected inputs are used
4242
bool m_add_inputs = true;
43+
//! If false, only safe inputs will be used
44+
bool m_include_unsafe_inputs = false;
4345
//! If false, allows unselected inputs, but requires all selected inputs be used if fAllowOtherInputs is true (default)
4446
bool fAllowOtherInputs = false;
4547
//! If false, only include as many inputs as necessary to fulfill a coin selection request. Only usable together with fAllowOtherInputs

src/wallet/coinselection.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ struct CoinEligibilityFilter
6565
/** Minimum number of confirmations for outputs that we sent to ourselves.
6666
* We may use unconfirmed UTXOs sent from ourselves, e.g. change outputs. */
6767
const int conf_mine;
68-
/** Minimum number of confirmations for outputs received from a different
69-
* wallet. We never spend unconfirmed foreign outputs as we cannot rely on these funds yet. */
68+
/** Minimum number of confirmations for outputs received from a different wallet. */
7069
const int conf_theirs;
7170
/** Maximum number of unconfirmed ancestors aggregated across all UTXOs in an OutputGroup. */
7271
const uint64_t max_ancestors;

src/wallet/rpcwallet.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3418,6 +3418,7 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
34183418
RPCTypeCheckObj(options,
34193419
{
34203420
{"add_inputs", UniValueType(UniValue::VBOOL)},
3421+
{"include_unsafe", UniValueType(UniValue::VBOOL)},
34213422
{"add_to_wallet", UniValueType(UniValue::VBOOL)},
34223423
{"changeAddress", UniValueType(UniValue::VSTR)},
34233424
{"change_address", UniValueType(UniValue::VSTR)},
@@ -3464,8 +3465,11 @@ void FundTransaction(CWallet* const pwallet, CMutableTransaction& tx, CAmount& f
34643465
lockUnspents = (options.exists("lock_unspents") ? options["lock_unspents"] : options["lockUnspents"]).get_bool();
34653466
}
34663467

3467-
if (options.exists("feeRate"))
3468-
{
3468+
if (options.exists("include_unsafe")) {
3469+
coinControl.m_include_unsafe_inputs = options["include_unsafe"].get_bool();
3470+
}
3471+
3472+
if (options.exists("feeRate")) {
34693473
if (options.exists("conf_target")) {
34703474
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both conf_target and feeRate");
34713475
}
@@ -3530,6 +3534,9 @@ static RPCHelpMan fundrawtransaction()
35303534
{"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}",
35313535
{
35323536
{"add_inputs", RPCArg::Type::BOOL, /* default */ "true", "For a transaction with existing inputs, automatically include more if they are not enough."},
3537+
{"include_unsafe", RPCArg::Type::BOOL, /* default */ "false", "Include inputs that are not safe to spend (unconfirmed transactions from outside keys and unconfirmed replacement transactions).\n"
3538+
"Warning: the resulting transaction may become invalid if one of the unsafe inputs disappears.\n"
3539+
"If that happens, you will need to fund the transaction with different inputs and republish it."},
35333540
{"changeAddress", RPCArg::Type::STR, /* default */ "pool address", "The Dash address to receive the change"},
35343541
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
35353542
{"includeWatching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only.\n"
@@ -4199,6 +4206,9 @@ static RPCHelpMan send()
41994206
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
42004207
{
42014208
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
4209+
{"include_unsafe", RPCArg::Type::BOOL, /* default */ "false", "Include inputs that are not safe to spend (unconfirmed transactions from outside keys and unconfirmed replacement transactions).\n"
4210+
"Warning: the resulting transaction may become invalid if one of the unsafe inputs disappears.\n"
4211+
"If that happens, you will need to fund the transaction with different inputs and republish it."},
42024212
{"add_to_wallet", RPCArg::Type::BOOL, /* default */ "true", "When false, returns a serialized transaction which will not be added to the wallet or broadcast"},
42034213
{"change_address", RPCArg::Type::STR_HEX, /* default */ "pool address", "The Dash address to receive the change"},
42044214
{"change_position", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
@@ -4533,6 +4543,9 @@ static RPCHelpMan walletcreatefundedpsbt()
45334543
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
45344544
{
45354545
{"add_inputs", RPCArg::Type::BOOL, /* default */ "false", "If inputs are specified, automatically include more if they are not enough."},
4546+
{"include_unsafe", RPCArg::Type::BOOL, /* default */ "false", "Include inputs that are not safe to spend (unconfirmed transactions from outside keys and unconfirmed replacement transactions).\n"
4547+
"Warning: the resulting transaction may become invalid if one of the unsafe inputs disappears.\n"
4548+
"If that happens, you will need to fund the transaction with different inputs and republish it."},
45364549
{"changeAddress", RPCArg::Type::STR_HEX, /* default */ "pool address", "The Dash address to receive the change"},
45374550
{"changePosition", RPCArg::Type::NUM, /* default */ "random", "The index of the change output"},
45384551
{"includeWatching", RPCArg::Type::BOOL, /* default */ "true for watch-only wallets, otherwise false", "Also select inputs which are watch only"},

src/wallet/wallet.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,8 +2932,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
29322932
if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) return true;
29332933

29342934
// Fall back to using zero confirmation change (but with as few ancestors in the mempool as
2935-
// possible) if we cannot fund the transaction otherwise. We never spend unconfirmed
2936-
// outputs received from other wallets.
2935+
// possible) if we cannot fund the transaction otherwise.
29372936
if (m_spend_zero_conf_change) {
29382937
if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) return true;
29392938
if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)),
@@ -2951,6 +2950,14 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
29512950
vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used, nCoinType)) {
29522951
return true;
29532952
}
2953+
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
2954+
// received from other wallets.
2955+
if (coin_control.m_include_unsafe_inputs
2956+
&& SelectCoinsMinConf(value_to_select,
2957+
CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
2958+
vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) {
2959+
return true;
2960+
}
29542961
// Try with unlimited ancestors/descendants. The transaction will still need to meet
29552962
// mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but
29562963
// OutputGroups use heuristics that may overestimate ancestor/descendant counts.
@@ -3473,7 +3480,7 @@ bool CWallet::CreateTransactionInternal(
34733480
{
34743481
CAmount nAmountAvailable{0};
34753482
std::vector<COutput> vAvailableCoins;
3476-
AvailableCoins(vAvailableCoins, true, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0);
3483+
AvailableCoins(vAvailableCoins, !coin_control.m_include_unsafe_inputs, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0);
34773484
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
34783485
coin_selection_params.use_bnb = false; // never use BnB
34793486

test/functional/rpc_fundrawtransaction.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def run_test(self):
9797
self.test_address_reuse()
9898
self.test_option_subtract_fee_from_outputs()
9999
self.test_subtract_fee_with_presets()
100+
self.test_include_unsafe()
100101

101102
def test_change_position(self):
102103
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -815,5 +816,40 @@ def test_subtract_fee_with_presets(self):
815816
signedtx = self.nodes[0].signrawtransactionwithwallet(fundedtx['hex'])
816817
self.nodes[0].sendrawtransaction(signedtx['hex'])
817818

819+
def test_include_unsafe(self):
820+
self.log.info("Test fundrawtxn with unsafe inputs")
821+
822+
self.nodes[0].createwallet("unsafe")
823+
wallet = self.nodes[0].get_wallet_rpc("unsafe")
824+
825+
# We receive unconfirmed funds from external keys (unsafe outputs).
826+
addr = wallet.getnewaddress()
827+
txid1 = self.nodes[2].sendtoaddress(addr, 6)
828+
txid2 = self.nodes[2].sendtoaddress(addr, 4)
829+
self.sync_all()
830+
vout1 = find_vout_for_address(wallet, txid1, addr)
831+
vout2 = find_vout_for_address(wallet, txid2, addr)
832+
833+
# Unsafe inputs are ignored by default.
834+
rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 5}])
835+
assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, rawtx)
836+
837+
# But we can opt-in to use them for funding.
838+
fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True})
839+
tx_dec = wallet.decoderawtransaction(fundedtx['hex'])
840+
assert any([txin['txid'] == txid1 and txin['vout'] == vout1 for txin in tx_dec['vin']])
841+
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
842+
wallet.sendrawtransaction(signedtx['hex'])
843+
844+
# And we can also use them once they're confirmed.
845+
self.nodes[0].generate(1)
846+
rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 3}])
847+
fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True})
848+
tx_dec = wallet.decoderawtransaction(fundedtx['hex'])
849+
assert any([txin['txid'] == txid2 and txin['vout'] == vout2 for txin in tx_dec['vin']])
850+
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
851+
wallet.sendrawtransaction(signedtx['hex'])
852+
853+
818854
if __name__ == '__main__':
819855
RawTransactionsTest().main()

test/functional/rpc_psbt.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ def run_test(self):
202202
# We don't care about the decode result, but decoding must succeed.
203203
self.nodes[0].decodepsbt(double_processed_psbt["psbt"])
204204

205+
# Make sure unsafe inputs are included if specified
206+
self.nodes[2].createwallet(wallet_name="unsafe")
207+
wunsafe = self.nodes[2].get_wallet_rpc("unsafe")
208+
self.nodes[0].sendtoaddress(wunsafe.getnewaddress(), 2)
209+
self.sync_mempools()
210+
assert_raises_rpc_error(-4, "Insufficient funds", wunsafe.walletcreatefundedpsbt, [], [{self.nodes[0].getnewaddress(): 1}])
211+
wunsafe.walletcreatefundedpsbt([], [{self.nodes[0].getnewaddress(): 1}], 0, {"include_unsafe": True})
212+
205213
# BIP 174 Test Vectors
206214

207215
# Check that unknown values are just passed through

test/functional/wallet_send.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ def skip_test_if_missing_module(self):
3131
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
3232
arg_conf_target=None, arg_estimate_mode=None,
3333
conf_target=None, estimate_mode=None, add_to_wallet=None, psbt=None,
34-
inputs=None, add_inputs=None, change_address=None, change_position=None,
34+
inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None,
3535
include_watching=None, locktime=None, lock_unspents=None, subtract_fee_from_outputs=None,
3636
expect_error=None):
3737
assert (amount is None) != (data is None)
3838

39-
from_balance_before = from_wallet.getbalance()
39+
from_balance_before = from_wallet.getbalances()["mine"]["trusted"]
40+
if include_unsafe:
41+
from_balance_before += from_wallet.getbalances()["mine"]["untrusted_pending"]
42+
4043
if to_wallet is None:
4144
assert amount is None
4245
else:
@@ -67,6 +70,8 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
6770
options["inputs"] = inputs
6871
if add_inputs is not None:
6972
options["add_inputs"] = add_inputs
73+
if include_unsafe is not None:
74+
options["include_unsafe"] = include_unsafe
7075
if change_address is not None:
7176
options["change_address"] = change_address
7277
if change_position is not None:
@@ -120,6 +125,10 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
120125
assert not "txid" in res
121126
assert "psbt" in res
122127

128+
from_balance = from_wallet.getbalances()["mine"]["trusted"]
129+
if include_unsafe:
130+
from_balance += from_wallet.getbalances()["mine"]["untrusted_pending"]
131+
123132
if add_to_wallet and not include_watching:
124133
# Ensure transaction exists in the wallet:
125134
tx = from_wallet.gettransaction(res["txid"])
@@ -129,13 +138,13 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
129138
assert tx
130139
if amount:
131140
if subtract_fee_from_outputs:
132-
assert_equal(from_balance_before - from_wallet.getbalance(), amount)
141+
assert_equal(from_balance_before - from_balance, amount)
133142
else:
134-
assert_greater_than(from_balance_before - from_wallet.getbalance(), amount)
143+
assert_greater_than(from_balance_before - from_balance, amount)
135144
else:
136145
assert next((out for out in tx["vout"] if out["scriptPubKey"]["asm"] == "OP_RETURN 35"), None)
137146
else:
138-
assert_equal(from_balance_before, from_wallet.getbalance())
147+
assert_equal(from_balance_before, from_balance)
139148

140149
if to_wallet:
141150
self.sync_mempools()
@@ -367,6 +376,14 @@ def run_test(self):
367376
self.log.info("Subtract fee from output")
368377
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0])
369378

379+
self.log.info("Include unsafe inputs")
380+
self.nodes[1].createwallet(wallet_name="w5")
381+
w5 = self.nodes[1].get_wallet_rpc("w5")
382+
self.test_send(from_wallet=w0, to_wallet=w5, amount=2)
383+
self.test_send(from_wallet=w5, to_wallet=w0, amount=1, expect_error=(-4, "Insufficient funds"))
384+
res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True)
385+
assert res["complete"]
386+
370387

371388
if __name__ == '__main__':
372389
WalletSendTest().main()

0 commit comments

Comments
 (0)