Skip to content

Commit 11d6459

Browse files
committed
rpc: include_unsafe option for fundrawtransaction
Allow RPC users to opt-in to unsafe inputs when funding a raw transaction. Applications that need to manage a complex RBF flow (such as lightning nodes using anchor outputs) are very limited if they can only use safe inputs. Fixes #21299
1 parent 480bf01 commit 11d6459

File tree

8 files changed

+97
-10
lines changed

8 files changed

+97
-10
lines changed

doc/release-notes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ Wallet
152152
- The `bumpfee` RPC is not available with wallets that have private keys
153153
disabled. `psbtbumpfee` can be used instead. (#20891)
154154

155+
- The `fundrawtransaction`, `send` and `walletcreatefundedpsbt` RPCs now support an `include_unsafe` option
156+
that when `true` allows using unsafe inputs to fund the transaction.
157+
Note that the resulting transaction may become invalid if one of the unsafe inputs disappears.
158+
If that happens, the transaction must be funded with different inputs and republished. (#21359)
159+
155160
GUI changes
156161
-----------
157162

src/wallet/coincontrol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class CCoinControl
2929
std::optional<OutputType> m_change_type;
3030
//! If false, only selected inputs are used
3131
bool m_add_inputs = true;
32+
//! If false, only safe inputs will be used
33+
bool m_include_unsafe_inputs = false;
3234
//! If false, allows unselected inputs, but requires all selected inputs be used
3335
bool fAllowOtherInputs = false;
3436
//! Includes watch only addresses which are solvable

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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3075,6 +3075,7 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
30753075
RPCTypeCheckObj(options,
30763076
{
30773077
{"add_inputs", UniValueType(UniValue::VBOOL)},
3078+
{"include_unsafe", UniValueType(UniValue::VBOOL)},
30783079
{"add_to_wallet", UniValueType(UniValue::VBOOL)},
30793080
{"changeAddress", UniValueType(UniValue::VSTR)},
30803081
{"change_address", UniValueType(UniValue::VSTR)},
@@ -3135,6 +3136,10 @@ void FundTransaction(CWallet& wallet, CMutableTransaction& tx, CAmount& fee_out,
31353136
lockUnspents = (options.exists("lock_unspents") ? options["lock_unspents"] : options["lockUnspents"]).get_bool();
31363137
}
31373138

3139+
if (options.exists("include_unsafe")) {
3140+
coinControl.m_include_unsafe_inputs = options["include_unsafe"].get_bool();
3141+
}
3142+
31383143
if (options.exists("feeRate")) {
31393144
if (options.exists("fee_rate")) {
31403145
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both fee_rate (" + CURRENCY_ATOM + "/vB) and feeRate (" + CURRENCY_UNIT + "/kvB)");
@@ -3205,6 +3210,9 @@ static RPCHelpMan fundrawtransaction()
32053210
{"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}",
32063211
{
32073212
{"add_inputs", RPCArg::Type::BOOL, RPCArg::Default{true}, "For a transaction with existing inputs, automatically include more if they are not enough."},
3213+
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include inputs that are not safe to spend (unconfirmed transactions from outside keys and unconfirmed replacement transactions).\n"
3214+
"Warning: the resulting transaction may become invalid if one of the unsafe inputs disappears.\n"
3215+
"If that happens, you will need to fund the transaction with different inputs and republish it."},
32083216
{"changeAddress", RPCArg::Type::STR, RPCArg::DefaultHint{"pool address"}, "The bitcoin address to receive the change"},
32093217
{"changePosition", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"},
32103218
{"change_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The output type to use. Only valid if changeAddress is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},
@@ -4030,6 +4038,9 @@ static RPCHelpMan send()
40304038
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
40314039
{
40324040
{"add_inputs", RPCArg::Type::BOOL, RPCArg::Default{false}, "If inputs are specified, automatically include more if they are not enough."},
4041+
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include inputs that are not safe to spend (unconfirmed transactions from outside keys and unconfirmed replacement transactions).\n"
4042+
"Warning: the resulting transaction may become invalid if one of the unsafe inputs disappears.\n"
4043+
"If that happens, you will need to fund the transaction with different inputs and republish it."},
40334044
{"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true}, "When false, returns a serialized transaction which will not be added to the wallet or broadcast"},
40344045
{"change_address", RPCArg::Type::STR_HEX, RPCArg::DefaultHint{"pool address"}, "The bitcoin address to receive the change"},
40354046
{"change_position", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"},
@@ -4373,6 +4384,9 @@ static RPCHelpMan walletcreatefundedpsbt()
43734384
{"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "",
43744385
{
43754386
{"add_inputs", RPCArg::Type::BOOL, RPCArg::Default{false}, "If inputs are specified, automatically include more if they are not enough."},
4387+
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include inputs that are not safe to spend (unconfirmed transactions from outside keys and unconfirmed replacement transactions).\n"
4388+
"Warning: the resulting transaction may become invalid if one of the unsafe inputs disappears.\n"
4389+
"If that happens, you will need to fund the transaction with different inputs and republish it."},
43764390
{"changeAddress", RPCArg::Type::STR_HEX, RPCArg::DefaultHint{"pool address"}, "The bitcoin address to receive the change"},
43774391
{"changePosition", RPCArg::Type::NUM, RPCArg::DefaultHint{"random"}, "The index of the change output"},
43784392
{"change_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -changetype"}, "The output type to use. Only valid if changeAddress is not specified. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."},

src/wallet/wallet.cpp

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

25182518
// Fall back to using zero confirmation change (but with as few ancestors in the mempool as
2519-
// possible) if we cannot fund the transaction otherwise. We never spend unconfirmed
2520-
// outputs received from other wallets.
2519+
// possible) if we cannot fund the transaction otherwise.
25212520
if (m_spend_zero_conf_change) {
25222521
if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) return true;
25232522
if (SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)),
@@ -2535,6 +2534,14 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
25352534
vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) {
25362535
return true;
25372536
}
2537+
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
2538+
// received from other wallets.
2539+
if (coin_control.m_include_unsafe_inputs
2540+
&& SelectCoinsMinConf(value_to_select,
2541+
CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
2542+
vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) {
2543+
return true;
2544+
}
25382545
// Try with unlimited ancestors/descendants. The transaction will still need to meet
25392546
// mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but
25402547
// OutputGroups use heuristics that may overestimate ancestor/descendant counts.
@@ -2836,7 +2843,7 @@ bool CWallet::CreateTransactionInternal(
28362843
txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight());
28372844
{
28382845
std::vector<COutput> vAvailableCoins;
2839-
AvailableCoins(vAvailableCoins, true, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0);
2846+
AvailableCoins(vAvailableCoins, !coin_control.m_include_unsafe_inputs, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0);
28402847
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
28412848
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
28422849

test/functional/rpc_fundrawtransaction.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def run_test(self):
9696
self.test_option_subtract_fee_from_outputs()
9797
self.test_subtract_fee_with_presets()
9898
self.test_transaction_too_large()
99+
self.test_include_unsafe()
99100

100101
def test_change_position(self):
101102
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -928,6 +929,40 @@ def test_transaction_too_large(self):
928929
self.nodes[0].generate(10)
929930
assert_raises_rpc_error(-4, "Transaction too large", recipient.fundrawtransaction, rawtx)
930931

932+
def test_include_unsafe(self):
933+
self.log.info("Test fundrawtxn with unsafe inputs")
934+
935+
self.nodes[0].createwallet("unsafe")
936+
wallet = self.nodes[0].get_wallet_rpc("unsafe")
937+
938+
# We receive unconfirmed funds from external keys (unsafe outputs).
939+
addr = wallet.getnewaddress()
940+
txid1 = self.nodes[2].sendtoaddress(addr, 6)
941+
txid2 = self.nodes[2].sendtoaddress(addr, 4)
942+
self.sync_all()
943+
vout1 = find_vout_for_address(wallet, txid1, addr)
944+
vout2 = find_vout_for_address(wallet, txid2, addr)
945+
946+
# Unsafe inputs are ignored by default.
947+
rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 5}])
948+
assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, rawtx)
949+
950+
# But we can opt-in to use them for funding.
951+
fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True})
952+
tx_dec = wallet.decoderawtransaction(fundedtx['hex'])
953+
assert any([txin['txid'] == txid1 and txin['vout'] == vout1 for txin in tx_dec['vin']])
954+
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
955+
wallet.sendrawtransaction(signedtx['hex'])
956+
957+
# And we can also use them once they're confirmed.
958+
self.nodes[0].generate(1)
959+
rawtx = wallet.createrawtransaction([], [{self.nodes[2].getnewaddress(): 3}])
960+
fundedtx = wallet.fundrawtransaction(rawtx, {"include_unsafe": True})
961+
tx_dec = wallet.decoderawtransaction(fundedtx['hex'])
962+
assert any([txin['txid'] == txid2 and txin['vout'] == vout2 for txin in tx_dec['vin']])
963+
signedtx = wallet.signrawtransactionwithwallet(fundedtx['hex'])
964+
wallet.sendrawtransaction(signedtx['hex'])
965+
931966

932967
if __name__ == '__main__':
933968
RawTransactionsTest().main()

test/functional/rpc_psbt.py

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

397+
# Make sure unsafe inputs are included if specified
398+
self.nodes[2].createwallet(wallet_name="unsafe")
399+
wunsafe = self.nodes[2].get_wallet_rpc("unsafe")
400+
self.nodes[0].sendtoaddress(wunsafe.getnewaddress(), 2)
401+
self.sync_mempools()
402+
assert_raises_rpc_error(-4, "Insufficient funds", wunsafe.walletcreatefundedpsbt, [], [{self.nodes[0].getnewaddress(): 1}])
403+
wunsafe.walletcreatefundedpsbt([], [{self.nodes[0].getnewaddress(): 1}], 0, {"include_unsafe": True})
404+
397405
# BIP 174 Test Vectors
398406

399407
# 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
@@ -33,12 +33,15 @@ def skip_test_if_missing_module(self):
3333
def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
3434
arg_conf_target=None, arg_estimate_mode=None, arg_fee_rate=None,
3535
conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None,
36-
inputs=None, add_inputs=None, change_address=None, change_position=None, change_type=None,
36+
inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None,
3737
include_watching=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None,
3838
expect_error=None):
3939
assert (amount is None) != (data is None)
4040

41-
from_balance_before = from_wallet.getbalance()
41+
from_balance_before = from_wallet.getbalances()["mine"]["trusted"]
42+
if include_unsafe:
43+
from_balance_before += from_wallet.getbalances()["mine"]["untrusted_pending"]
44+
4245
if to_wallet is None:
4346
assert amount is None
4447
else:
@@ -71,6 +74,8 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
7174
options["inputs"] = inputs
7275
if add_inputs is not None:
7376
options["add_inputs"] = add_inputs
77+
if include_unsafe is not None:
78+
options["include_unsafe"] = include_unsafe
7479
if change_address is not None:
7580
options["change_address"] = change_address
7681
if change_position is not None:
@@ -133,6 +138,10 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
133138
assert not "txid" in res
134139
assert "psbt" in res
135140

141+
from_balance = from_wallet.getbalances()["mine"]["trusted"]
142+
if include_unsafe:
143+
from_balance += from_wallet.getbalances()["mine"]["untrusted_pending"]
144+
136145
if add_to_wallet and not include_watching:
137146
# Ensure transaction exists in the wallet:
138147
tx = from_wallet.gettransaction(res["txid"])
@@ -143,13 +152,13 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None,
143152
assert tx
144153
if amount:
145154
if subtract_fee_from_outputs:
146-
assert_equal(from_balance_before - from_wallet.getbalance(), amount)
155+
assert_equal(from_balance_before - from_balance, amount)
147156
else:
148-
assert_greater_than(from_balance_before - from_wallet.getbalance(), amount)
157+
assert_greater_than(from_balance_before - from_balance, amount)
149158
else:
150159
assert next((out for out in tx["vout"] if out["scriptPubKey"]["asm"] == "OP_RETURN 35"), None)
151160
else:
152-
assert_equal(from_balance_before, from_wallet.getbalance())
161+
assert_equal(from_balance_before, from_balance)
153162

154163
if to_wallet:
155164
self.sync_mempools()
@@ -440,6 +449,14 @@ def run_test(self):
440449
self.log.info("Subtract fee from output")
441450
self.test_send(from_wallet=w0, to_wallet=w1, amount=1, subtract_fee_from_outputs=[0])
442451

452+
self.log.info("Include unsafe inputs")
453+
self.nodes[1].createwallet(wallet_name="w5")
454+
w5 = self.nodes[1].get_wallet_rpc("w5")
455+
self.test_send(from_wallet=w0, to_wallet=w5, amount=2)
456+
self.test_send(from_wallet=w5, to_wallet=w0, amount=1, expect_error=(-4, "Insufficient funds"))
457+
res = self.test_send(from_wallet=w5, to_wallet=w0, amount=1, include_unsafe=True)
458+
assert res["complete"]
459+
443460

444461
if __name__ == '__main__':
445462
WalletSendTest().main()

0 commit comments

Comments
 (0)