Skip to content

Commit 84b4ffe

Browse files
committed
wallet: remove fetch pre-selected-inputs responsibility from SelectCoins
In the transaction creation process, in order to select which coins the new transaction will spend, we first obtain all the available coins known by the wallet, which means walking-through the wallet txes map, gathering the ones that fulfill certain spendability requirements in a vector. This coins vector is then provided to the Coin Selection process, which first checks if the user has manually selected any input (which could be internal, aka known by the wallet, or external), and if it does, it fetches them by searching each of them inside the wallet and/or inside the Coin Control external tx data. Then, after finding the pre-selected-inputs and gathering them in a vector, the Coin Selection process walks-through the entire available coins vector once more just to erase coins that are in both vectors. So the Coin Selection process doesn’t pick them twice (duplicate inputs inside the same transaction). Now, a new method, `FetchCoins` will be responsible for: 1) Lookup the user pre-selected-inputs (which can be internal or external). 2) And, fetch the available coins in the wallet (excluding the already fetched ones). This new flow allows us to never include the pre-selected-inputs inside the available coins vector in the first place. So, Coin Selection can perform its main responsibility without mixing it with having to fetch internal/external coins nor any slow and unneeded duplicate inputs verification Improvements: 1) If any pre-selected-input lookup fail, the process will return the error right away. (Before, the wallet was fetching all the wallet available coins, walking through the entire txes map, and then failing for an invalid pre-selected-input inside SelectCoins) 2) The pre-selected-inputs lookup failure causes are properly described now on the return error. (before, we were just returning an "Insufficient Funds" error even if the failure was due a not solvable external input) 3) Faster Coin Selection, no longer need to "remove the pre-set inputs from the available coins vector so that Coin Selection doesn't pick them" (which meant to loop-over the entire available coins vector at coin selection time, erasing coins that were pre-selected). Now, the available coins vector, which is built after the pre-selected-inputs fetching, doesn't include the already selected inputs in the first place. 4) Faster transaction creation for transactions that only use manually selected inputs. We now will return early, as soon as we finish fetching the pre-selected-inputs and not perform the resources expensive calculation of walking-through the entire wallet txes map to obtain the available coins (coins that we will not use).
1 parent 23448ca commit 84b4ffe

File tree

6 files changed

+88
-51
lines changed

6 files changed

+88
-51
lines changed

src/wallet/spend.cpp

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,31 @@ BResult<CoinsResult> FetchSelectedInputs(const CWallet& wallet, const CCoinContr
153153
return result;
154154
}
155155

156+
BResult<CoinsFund> FetchCoins(const CWallet& wallet, const CCoinControl& coin_control,
157+
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
158+
{
159+
CoinsFund result;
160+
161+
// Fetch manually select coins
162+
if (coin_control.HasSelected()) {
163+
result.pre_selected_inputs = FetchSelectedInputs(wallet, coin_control, coin_selection_params);
164+
if (!result.pre_selected_inputs) return result.pre_selected_inputs.GetError();
165+
166+
// If no other inputs are allowed, return early
167+
if (!coin_control.m_allow_other_inputs) return result;
168+
}
169+
170+
// Fetch wallet available coins
171+
result.available_coins = AvailableCoins(wallet,
172+
&coin_control,
173+
coin_selection_params.m_effective_feerate,
174+
1, /*nMinimumAmount*/
175+
MAX_MONEY, /*nMaximumAmount*/
176+
MAX_MONEY, /*nMinimumSumAmount*/
177+
0); /*nMaximumCount*/
178+
return result;
179+
}
180+
156181
CoinsResult AvailableCoins(const CWallet& wallet,
157182
const CCoinControl* coinControl,
158183
std::optional<CFeeRate> feerate,
@@ -240,7 +265,9 @@ CoinsResult AvailableCoins(const CWallet& wallet,
240265
if (output.nValue < nMinimumAmount || output.nValue > nMaximumAmount)
241266
continue;
242267

243-
if (coinControl && coinControl->HasSelected() && !coinControl->m_allow_other_inputs && !coinControl->IsSelected(outpoint))
268+
// Skip manually selected coins (the caller can fetch them directly)
269+
// If available and manually selected coins are required, use `FetchCoins`
270+
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
244271
continue;
245272

246273
if (wallet.IsLockedCoin(outpoint))
@@ -575,24 +602,17 @@ std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, cons
575602
return best_result;
576603
}
577604

578-
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
605+
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsFund& coins, const CAmount& nTargetValue, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
579606
{
580607
CAmount value_to_select = nTargetValue;
581608

609+
// Add the manually selected coins first
582610
OutputGroup preset_inputs(coin_selection_params);
583-
584-
// calculate value from preset inputs and store them
585-
std::set<COutPoint> preset_coins;
586-
587-
const BResult<CoinsResult>& pre_selected_inputs = FetchSelectedInputs(wallet, coin_control, coin_selection_params);
588-
if (!pre_selected_inputs) return std::nullopt;
589-
else {
590-
const auto& inputs = pre_selected_inputs.GetObj();
611+
if (coins.pre_selected_inputs) {
612+
const auto& inputs = coins.pre_selected_inputs.GetObj();
591613
for (const auto& coin : inputs.all()) {
592-
preset_coins.insert(coin.outpoint);
593-
/* Set ancestors and descendants to 0 as they don't matter for preset inputs since no actual selection is being done.
594-
* positive_only is set to false because we want to include all preset inputs, even if they are dust.
595-
*/
614+
// Set ancestors and descendants to 0 as they don't matter for preset inputs since no actual selection is being done.
615+
// positive_only is set to false because we want to include all preset inputs, even if they are dust.
596616
preset_inputs.Insert(coin, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false);
597617

598618
// Decrease the target amount
@@ -613,21 +633,13 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
613633
return result;
614634
}
615635

616-
// remove preset inputs from coins so that Coin Selection doesn't pick them.
617-
if (coin_control.HasSelected()) {
618-
available_coins.legacy.erase(remove_if(available_coins.legacy.begin(), available_coins.legacy.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.legacy.end());
619-
available_coins.P2SH_segwit.erase(remove_if(available_coins.P2SH_segwit.begin(), available_coins.P2SH_segwit.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.P2SH_segwit.end());
620-
available_coins.bech32.erase(remove_if(available_coins.bech32.begin(), available_coins.bech32.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.bech32.end());
621-
available_coins.bech32m.erase(remove_if(available_coins.bech32m.begin(), available_coins.bech32m.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.bech32m.end());
622-
available_coins.other.erase(remove_if(available_coins.other.begin(), available_coins.other.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.other.end());
623-
}
624-
625636
unsigned int limit_ancestor_count = 0;
626637
unsigned int limit_descendant_count = 0;
627638
wallet.chain().getPackageLimits(limit_ancestor_count, limit_descendant_count);
628639
const size_t max_ancestors = (size_t)std::max<int64_t>(1, limit_ancestor_count);
629640
const size_t max_descendants = (size_t)std::max<int64_t>(1, limit_descendant_count);
630641
const bool fRejectLongChains = gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS);
642+
CoinsResult& available_coins = coins.available_coins;
631643

632644
// form groups from remaining coins; note that preset coins will not
633645
// automatically have their associated (same address) coins included
@@ -908,17 +920,13 @@ static BResult<CreatedTransactionResult> CreateTransactionInternal(
908920
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
909921
CAmount selection_target = recipients_sum + not_input_fees;
910922

911-
// Get available coins
912-
auto available_coins = AvailableCoins(wallet,
913-
&coin_control,
914-
coin_selection_params.m_effective_feerate,
915-
1, /*nMinimumAmount*/
916-
MAX_MONEY, /*nMaximumAmount*/
917-
MAX_MONEY, /*nMinimumSumAmount*/
918-
0); /*nMaximumCount*/
923+
// Fetch manually selected inputs and wallet available coins
924+
BResult<CoinsFund> res_coins = FetchCoins(wallet, coin_control, coin_selection_params);
925+
if (!res_coins) return res_coins.GetError();
926+
CoinsFund coins = res_coins.ReleaseObj();
919927

920928
// Choose coins to use
921-
std::optional<SelectionResult> result = SelectCoins(wallet, available_coins, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
929+
std::optional<SelectionResult> result = SelectCoins(wallet, coins, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
922930
if (!result) {
923931
return _("Insufficient funds");
924932
}

src/wallet/spend.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,35 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
125125
std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins,
126126
const CoinSelectionParams& coin_selection_params);
127127

128+
/**
129+
* Struct containing the wallet's available coins vector ('AvailableCoins' result)
130+
* and the user manually selected inputs whose can be internal (from the wallet)
131+
* or external.
132+
*/
133+
struct CoinsFund
134+
{
135+
// Coins manually selected by the user
136+
// Empty if no coin was selected
137+
BResult<CoinsResult> pre_selected_inputs;
138+
139+
// Available coins in the wallet
140+
// (excluding the manually selected coins)
141+
CoinsResult available_coins;
142+
};
143+
128144
/**
129145
* Select a set of coins such that nTargetValue is met and at least
130146
* all coins from coin_control are selected; never select unconfirmed coins if they are not ours
131147
* param@[in] wallet The wallet which provides data necessary to spend the selected coins
132-
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
148+
* param@[in] coins The struct of available coins (organized by OutputType) and the manually selected
149+
* inputs who must be part of the result.
133150
* param@[in] nTargetValue The target value
134151
* param@[in] coin_selection_params Parameters for this coin selection such as feerates, whether to avoid partial spends,
135152
* and whether to subtract the fee from the outputs.
136153
* returns If successful, a SelectionResult containing the selected coins
137154
* If failed, a nullopt.
138155
*/
139-
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control,
156+
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsFund& coins, const CAmount& nTargetValue, const CCoinControl& coin_control,
140157
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
141158

142159
struct CreatedTransactionResult

src/wallet/test/coinselector_tests.cpp

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -330,16 +330,21 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
330330
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
331331
wallet->SetupDescriptorScriptPubKeyMans();
332332

333-
CoinsResult available_coins;
334-
333+
CoinsFund coins_fund;
334+
CoinsResult& available_coins = coins_fund.available_coins;
335335
add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
336336
add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
337337
add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
338+
339+
// Mix manually selected input with available coins
338340
CCoinControl coin_control;
339341
coin_control.m_allow_other_inputs = true;
340-
coin_control.Select(available_coins.all().at(0).outpoint);
342+
CoinsResult pre_selected_inputs;
343+
pre_selected_inputs.bech32.emplace_back(available_coins.bech32.at(0));
344+
coins_fund.pre_selected_inputs = pre_selected_inputs;
345+
available_coins.bech32.erase(available_coins.bech32.begin()); // not include manually selected coin in available vector
341346
coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
342-
const auto result10 = SelectCoins(*wallet, available_coins, 10 * CENT, coin_control, coin_selection_params_bnb);
347+
const auto result10 = SelectCoins(*wallet, coins_fund, 10 * CENT, coin_control, coin_selection_params_bnb);
343348
BOOST_CHECK(result10);
344349
}
345350
{
@@ -349,7 +354,8 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
349354
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
350355
wallet->SetupDescriptorScriptPubKeyMans();
351356

352-
CoinsResult available_coins;
357+
CoinsFund coins_fund;
358+
CoinsResult& available_coins = coins_fund.available_coins;
353359

354360
// single coin should be selected when effective fee > long term fee
355361
coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
@@ -362,7 +368,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
362368
expected_result.Clear();
363369
add_coin(10 * CENT, 2, expected_result);
364370
CCoinControl coin_control;
365-
const auto result11 = SelectCoins(*wallet, available_coins, 10 * CENT, coin_control, coin_selection_params_bnb);
371+
const auto result11 = SelectCoins(*wallet, coins_fund, 10 * CENT, coin_control, coin_selection_params_bnb);
366372
BOOST_CHECK(EquivalentResult(expected_result, *result11));
367373
available_coins.clear();
368374

@@ -377,7 +383,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
377383
expected_result.Clear();
378384
add_coin(9 * CENT, 2, expected_result);
379385
add_coin(1 * CENT, 2, expected_result);
380-
const auto result12 = SelectCoins(*wallet, available_coins, 10 * CENT, coin_control, coin_selection_params_bnb);
386+
const auto result12 = SelectCoins(*wallet, coins_fund, 10 * CENT, coin_control, coin_selection_params_bnb);
381387
BOOST_CHECK(EquivalentResult(expected_result, *result12));
382388
available_coins.clear();
383389

@@ -393,8 +399,12 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
393399
add_coin(9 * CENT, 2, expected_result);
394400
add_coin(1 * CENT, 2, expected_result);
395401
coin_control.m_allow_other_inputs = true;
396-
coin_control.Select(available_coins.all().at(1).outpoint); // pre select 9 coin
397-
const auto result13 = SelectCoins(*wallet, available_coins, 10 * CENT, coin_control, coin_selection_params_bnb);
402+
CoinsResult pre_selected_inputs;
403+
pre_selected_inputs.bech32.emplace_back(available_coins.bech32.at(1)); // pre select 9 coin
404+
coins_fund.pre_selected_inputs = pre_selected_inputs;
405+
available_coins.bech32.erase(std::next(available_coins.bech32.begin())); // erase pre-selected coin from the available coins vector
406+
407+
const auto result13 = SelectCoins(*wallet, coins_fund, 10 * CENT, coin_control, coin_selection_params_bnb);
398408
BOOST_CHECK(EquivalentResult(expected_result, *result13));
399409
}
400410
}
@@ -750,14 +760,14 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
750760
// Run this test 100 times
751761
for (int i = 0; i < 100; ++i)
752762
{
753-
CoinsResult available_coins;
763+
CoinsFund coins;
754764
CAmount balance{0};
755765

756766
// Make a wallet with 1000 exponentially distributed random inputs
757767
for (int j = 0; j < 1000; ++j)
758768
{
759769
CAmount val = distribution(generator)*10000000;
760-
add_coin(available_coins, *wallet, val);
770+
add_coin(coins.available_coins, *wallet, val);
761771
balance += val;
762772
}
763773

@@ -780,7 +790,7 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
780790
/*avoid_partial=*/ false,
781791
};
782792
CCoinControl cc;
783-
const auto result = SelectCoins(*wallet, available_coins, target, cc, cs_params);
793+
const auto result = SelectCoins(*wallet, coins, target, cc, cs_params);
784794
BOOST_CHECK(result);
785795
BOOST_CHECK_GE(result->GetSelectedValue(), target);
786796
}

test/functional/rpc_fundrawtransaction.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,12 @@ def test_two_vin_two_vout(self):
405405

406406
def test_invalid_input(self):
407407
self.log.info("Test fundrawtxn with an invalid vin")
408-
inputs = [ {'txid' : "1c7f966dab21119bac53213a2bc7532bff1fa844c124fd750a7d0b1332440bd1", 'vout' : 0} ] #invalid vin!
408+
txid = "1c7f966dab21119bac53213a2bc7532bff1fa844c124fd750a7d0b1332440bd1"
409+
vout = 0
410+
inputs = [ {'txid' : txid, 'vout' : vout} ] #invalid vin!
409411
outputs = { self.nodes[0].getnewaddress() : 1.0}
410412
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
411-
assert_raises_rpc_error(-4, "Insufficient funds", self.nodes[2].fundrawtransaction, rawtx)
413+
assert_raises_rpc_error(-4, "Not found pre-selected input COutPoint(%s, %i)" % (txid[0:10], vout), self.nodes[2].fundrawtransaction, rawtx)
412414

413415
def test_fee_p2pkh(self):
414416
"""Compare fee of a standard pubkeyhash transaction."""
@@ -1010,7 +1012,7 @@ def test_external_inputs(self):
10101012

10111013
# An external input without solving data should result in an error
10121014
raw_tx = wallet.createrawtransaction([ext_utxo], {self.nodes[0].getnewaddress(): ext_utxo["amount"] / 2})
1013-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.fundrawtransaction, raw_tx)
1015+
assert_raises_rpc_error(-4, "Not solvable pre-selected input COutPoint(%s, %s)" % (ext_utxo["txid"][0:10], ext_utxo["vout"]), wallet.fundrawtransaction, raw_tx)
10141016

10151017
# Error conditions
10161018
assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, {"solving_data": {"pubkeys":["not a pubkey"]}})

test/functional/rpc_psbt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ def test_psbt_input_keys(psbt_input, keys):
650650
ext_utxo = self.nodes[0].listunspent(addresses=[addr])[0]
651651

652652
# An external input without solving data should result in an error
653-
assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 15})
653+
assert_raises_rpc_error(-4, "Not solvable pre-selected input COutPoint(%s, %s)" % (ext_utxo["txid"][0:10], ext_utxo["vout"]), wallet.walletcreatefundedpsbt, [ext_utxo], {self.nodes[0].getnewaddress(): 15})
654654

655655
# But funding should work when the solving data is provided
656656
psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data": {"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"], addr_info["embedded"]["embedded"]["scriptPubKey"]]}})

test/functional/wallet_send.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def run_test(self):
508508
ext_utxo = ext_fund.listunspent(addresses=[addr])[0]
509509

510510
# An external input without solving data should result in an error
511-
self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, expect_error=(-4, "Insufficient funds"))
511+
self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, expect_error=(-4, "Not solvable pre-selected input COutPoint(%s, %s)" % (ext_utxo["txid"][0:10], ext_utxo["vout"])))
512512

513513
# But funding should work when the solving data is provided
514514
res = self.test_send(from_wallet=ext_wallet, to_wallet=self.nodes[0], amount=15, inputs=[ext_utxo], add_inputs=True, psbt=True, include_watching=True, solving_data={"pubkeys": [addr_info['pubkey']], "scripts": [addr_info["embedded"]["scriptPubKey"], addr_info["embedded"]["embedded"]["scriptPubKey"]]})

0 commit comments

Comments
 (0)