Skip to content

Commit 64f7a19

Browse files
committed
Merge bitcoin/bitcoin#25734: wallet, refactor: #24584 follow-ups
8cd21bb refactor: improve readability for AttemptSelection (josibake) f47ff71 test: only run test for descriptor wallets (josibake) 0760ce0 test: add missing BOOST_ASSERT (josibake) db09aec wallet: switch to new shuffle, erase, push_back (josibake) b6b50b0 scripted-diff: Uppercase function names (josibake) 3f27a2a refactor: add new helper methods (josibake) f5649db refactor: add UNKNOWN OutputType (josibake) Pull request description: This PR is to address follow-ups for #24584, specifically: * Remove redundant, hard-to-read code by adding a new `OutputType` and adding shuffle, erase, and push_back methods for `CoinsResult` * Add missing `BOOST_ASSERT` to unit test * Ensure functional test only runs if using descriptor wallets * Improve readability of `AttemptSelection` by removing triple-nested if statement Note for reviewers: commit `refactor: add new helper methods` should throw an "unused function warning"; the function is used in the next commit. Also, commit `wallet: switch to new shuffle, erase, push_back` will fail to compile, but this is fixed in the next commit with a scripted-diff. the commits are separate like this (code change then scripted-diff) to improve legibility. ACKs for top commit: achow101: ACK 8cd21bb aureleoules: ACK 8cd21bb. LarryRuane: Concept, code review ACK 8cd21bb furszy: utACK 8cd21bb. Left a small, non-blocking, comment. Tree-SHA512: a1bbc5962833e3df4f01a4895d8bd748cc4c608c3f296fd94e8afd8797b8d2e94e7bd44d598bd76fa5c9f5536864f396fcd097348fa0bb190a49a86b0917d60e
2 parents c336f81 + 8cd21bb commit 64f7a19

13 files changed

+180
-163
lines changed

src/bench/coin_selection.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ static void CoinSelection(benchmark::Bench& bench)
5959
wallet::CoinsResult available_coins;
6060
for (const auto& wtx : wtxs) {
6161
const auto txout = wtx->tx->vout.at(0);
62-
available_coins.bech32.emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*spendable=*/true, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/ 0);
62+
available_coins.coins[OutputType::BECH32].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*spendable=*/true, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/ 0);
6363
}
6464

6565
const CoinEligibilityFilter filter_standard(1, 6, 0);

src/outputtype.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ static const std::string OUTPUT_TYPE_STRING_LEGACY = "legacy";
2020
static const std::string OUTPUT_TYPE_STRING_P2SH_SEGWIT = "p2sh-segwit";
2121
static const std::string OUTPUT_TYPE_STRING_BECH32 = "bech32";
2222
static const std::string OUTPUT_TYPE_STRING_BECH32M = "bech32m";
23+
static const std::string OUTPUT_TYPE_STRING_UNKNOWN = "unknown";
2324

2425
std::optional<OutputType> ParseOutputType(const std::string& type)
2526
{
@@ -31,6 +32,8 @@ std::optional<OutputType> ParseOutputType(const std::string& type)
3132
return OutputType::BECH32;
3233
} else if (type == OUTPUT_TYPE_STRING_BECH32M) {
3334
return OutputType::BECH32M;
35+
} else if (type == OUTPUT_TYPE_STRING_UNKNOWN) {
36+
return OutputType::UNKNOWN;
3437
}
3538
return std::nullopt;
3639
}
@@ -42,6 +45,7 @@ const std::string& FormatOutputType(OutputType type)
4245
case OutputType::P2SH_SEGWIT: return OUTPUT_TYPE_STRING_P2SH_SEGWIT;
4346
case OutputType::BECH32: return OUTPUT_TYPE_STRING_BECH32;
4447
case OutputType::BECH32M: return OUTPUT_TYPE_STRING_BECH32M;
48+
case OutputType::UNKNOWN: return OUTPUT_TYPE_STRING_UNKNOWN;
4549
} // no default case, so the compiler can warn about missing cases
4650
assert(false);
4751
}
@@ -61,7 +65,8 @@ CTxDestination GetDestinationForKey(const CPubKey& key, OutputType type)
6165
return witdest;
6266
}
6367
}
64-
case OutputType::BECH32M: {} // This function should never be used with BECH32M, so let it assert
68+
case OutputType::BECH32M:
69+
case OutputType::UNKNOWN: {} // This function should never be used with BECH32M or UNKNOWN, so let it assert
6570
} // no default case, so the compiler can warn about missing cases
6671
assert(false);
6772
}
@@ -99,7 +104,8 @@ CTxDestination AddAndGetDestinationForScript(FillableSigningProvider& keystore,
99104
return ScriptHash(witprog);
100105
}
101106
}
102-
case OutputType::BECH32M: {} // This function should not be used for BECH32M, so let it assert
107+
case OutputType::BECH32M:
108+
case OutputType::UNKNOWN: {} // This function should not be used for BECH32M or UNKNOWN, so let it assert
103109
} // no default case, so the compiler can warn about missing cases
104110
assert(false);
105111
}

src/outputtype.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ enum class OutputType {
1919
P2SH_SEGWIT,
2020
BECH32,
2121
BECH32M,
22+
UNKNOWN,
2223
};
2324

2425
static constexpr auto OUTPUT_TYPES = std::array{
2526
OutputType::LEGACY,
2627
OutputType::P2SH_SEGWIT,
2728
OutputType::BECH32,
2829
OutputType::BECH32M,
30+
OutputType::UNKNOWN,
2931
};
3032

3133
std::optional<OutputType> ParseOutputType(const std::string& str);

src/wallet/rpc/coins.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ RPCHelpMan listunspent()
641641
cctl.m_max_depth = nMaxDepth;
642642
cctl.m_include_unsafe_inputs = include_unsafe;
643643
LOCK(pwallet->cs_wallet);
644-
vecOutputs = AvailableCoinsListUnspent(*pwallet, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount).all();
644+
vecOutputs = AvailableCoinsListUnspent(*pwallet, &cctl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount).All();
645645
}
646646

647647
LOCK(pwallet->cs_wallet);

src/wallet/rpc/spend.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,7 @@ RPCHelpMan sendall()
13821382
total_input_value += tx->tx->vout[input.prevout.n].nValue;
13831383
}
13841384
} else {
1385-
for (const COutput& output : AvailableCoins(*pwallet, &coin_control, fee_rate, /*nMinimumAmount=*/0).all()) {
1385+
for (const COutput& output : AvailableCoins(*pwallet, &coin_control, fee_rate, /*nMinimumAmount=*/0).All()) {
13861386
CHECK_NONFATAL(output.input_bytes > 0);
13871387
if (send_max && fee_rate.GetFee(output.input_bytes) > output.txout.nValue) {
13881388
continue;

src/wallet/scriptpubkeyman.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,6 +1965,11 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_
19651965
desc_prefix = "tr(" + xpub + "/86'";
19661966
break;
19671967
}
1968+
case OutputType::UNKNOWN: {
1969+
// We should never have a DescriptorScriptPubKeyMan for an UNKNOWN OutputType,
1970+
// so if we get to this point something is wrong
1971+
assert(false);
1972+
}
19681973
} // no default case, so the compiler can warn about missing cases
19691974
assert(!desc_prefix.empty());
19701975

src/wallet/spend.cpp

Lines changed: 86 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -79,30 +79,68 @@ TxSize CalculateMaximumSignedTxSize(const CTransaction &tx, const CWallet *walle
7979
return CalculateMaximumSignedTxSize(tx, wallet, txouts, coin_control);
8080
}
8181

82-
uint64_t CoinsResult::size() const
82+
size_t CoinsResult::Size() const
8383
{
84-
return bech32m.size() + bech32.size() + P2SH_segwit.size() + legacy.size() + other.size();
84+
size_t size{0};
85+
for (const auto& it : coins) {
86+
size += it.second.size();
87+
}
88+
return size;
8589
}
8690

87-
std::vector<COutput> CoinsResult::all() const
91+
std::vector<COutput> CoinsResult::All() const
8892
{
8993
std::vector<COutput> all;
90-
all.reserve(this->size());
91-
all.insert(all.end(), bech32m.begin(), bech32m.end());
92-
all.insert(all.end(), bech32.begin(), bech32.end());
93-
all.insert(all.end(), P2SH_segwit.begin(), P2SH_segwit.end());
94-
all.insert(all.end(), legacy.begin(), legacy.end());
95-
all.insert(all.end(), other.begin(), other.end());
94+
all.reserve(coins.size());
95+
for (const auto& it : coins) {
96+
all.insert(all.end(), it.second.begin(), it.second.end());
97+
}
9698
return all;
9799
}
98100

99-
void CoinsResult::clear()
101+
void CoinsResult::Clear() {
102+
coins.clear();
103+
}
104+
105+
void CoinsResult::Erase(std::set<COutPoint>& preset_coins)
100106
{
101-
bech32m.clear();
102-
bech32.clear();
103-
P2SH_segwit.clear();
104-
legacy.clear();
105-
other.clear();
107+
for (auto& it : coins) {
108+
auto& vec = it.second;
109+
auto i = std::find_if(vec.begin(), vec.end(), [&](const COutput &c) { return preset_coins.count(c.outpoint);});
110+
if (i != vec.end()) {
111+
vec.erase(i);
112+
break;
113+
}
114+
}
115+
}
116+
117+
void CoinsResult::Shuffle(FastRandomContext& rng_fast)
118+
{
119+
for (auto& it : coins) {
120+
::Shuffle(it.second.begin(), it.second.end(), rng_fast);
121+
}
122+
}
123+
124+
void CoinsResult::Add(OutputType type, const COutput& out)
125+
{
126+
coins[type].emplace_back(out);
127+
}
128+
129+
static OutputType GetOutputType(TxoutType type, bool is_from_p2sh)
130+
{
131+
switch (type) {
132+
case TxoutType::WITNESS_V1_TAPROOT:
133+
return OutputType::BECH32M;
134+
case TxoutType::WITNESS_V0_KEYHASH:
135+
case TxoutType::WITNESS_V0_SCRIPTHASH:
136+
if (is_from_p2sh) return OutputType::P2SH_SEGWIT;
137+
else return OutputType::BECH32;
138+
case TxoutType::SCRIPTHASH:
139+
case TxoutType::PUBKEYHASH:
140+
return OutputType::LEGACY;
141+
default:
142+
return OutputType::UNKNOWN;
143+
}
106144
}
107145

108146
CoinsResult AvailableCoins(const CWallet& wallet,
@@ -222,51 +260,32 @@ CoinsResult AvailableCoins(const CWallet& wallet,
222260
// Filter by spendable outputs only
223261
if (!spendable && only_spendable) continue;
224262

225-
// When parsing a scriptPubKey, Solver returns the parsed pubkeys or hashes (depending on the script)
226-
// We don't need those here, so we are leaving them in return_values_unused
227-
std::vector<std::vector<uint8_t>> return_values_unused;
228-
TxoutType type;
229-
bool is_from_p2sh{false};
230-
231263
// If the Output is P2SH and spendable, we want to know if it is
232264
// a P2SH (legacy) or one of P2SH-P2WPKH, P2SH-P2WSH (P2SH-Segwit). We can determine
233265
// this from the redeemScript. If the Output is not spendable, it will be classified
234266
// as a P2SH (legacy), since we have no way of knowing otherwise without the redeemScript
267+
CScript script;
268+
bool is_from_p2sh{false};
235269
if (output.scriptPubKey.IsPayToScriptHash() && solvable) {
236-
CScript redeemScript;
237270
CTxDestination destination;
238271
if (!ExtractDestination(output.scriptPubKey, destination))
239272
continue;
240273
const CScriptID& hash = CScriptID(std::get<ScriptHash>(destination));
241-
if (!provider->GetCScript(hash, redeemScript))
274+
if (!provider->GetCScript(hash, script))
242275
continue;
243-
type = Solver(redeemScript, return_values_unused);
244276
is_from_p2sh = true;
245277
} else {
246-
type = Solver(output.scriptPubKey, return_values_unused);
278+
script = output.scriptPubKey;
247279
}
248280

249281
COutput coin(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate);
250-
switch (type) {
251-
case TxoutType::WITNESS_UNKNOWN:
252-
case TxoutType::WITNESS_V1_TAPROOT:
253-
result.bech32m.push_back(coin);
254-
break;
255-
case TxoutType::WITNESS_V0_KEYHASH:
256-
case TxoutType::WITNESS_V0_SCRIPTHASH:
257-
if (is_from_p2sh) {
258-
result.P2SH_segwit.push_back(coin);
259-
break;
260-
}
261-
result.bech32.push_back(coin);
262-
break;
263-
case TxoutType::SCRIPTHASH:
264-
case TxoutType::PUBKEYHASH:
265-
result.legacy.push_back(coin);
266-
break;
267-
default:
268-
result.other.push_back(coin);
269-
};
282+
283+
// When parsing a scriptPubKey, Solver returns the parsed pubkeys or hashes (depending on the script)
284+
// We don't need those here, so we are leaving them in return_values_unused
285+
std::vector<std::vector<uint8_t>> return_values_unused;
286+
TxoutType type;
287+
type = Solver(script, return_values_unused);
288+
result.Add(GetOutputType(type, is_from_p2sh), coin);
270289

271290
// Cache total amount as we go
272291
result.total_amount += output.nValue;
@@ -278,7 +297,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
278297
}
279298

280299
// Checks the maximum number of UTXO's.
281-
if (nMaximumCount > 0 && result.size() >= nMaximumCount) {
300+
if (nMaximumCount > 0 && result.Size() >= nMaximumCount) {
282301
return result;
283302
}
284303
}
@@ -334,7 +353,7 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
334353

335354
std::map<CTxDestination, std::vector<COutput>> result;
336355

337-
for (const COutput& coin : AvailableCoinsListUnspent(wallet).all()) {
356+
for (const COutput& coin : AvailableCoinsListUnspent(wallet).All()) {
338357
CTxDestination address;
339358
if ((coin.spendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.solvable)) &&
340359
ExtractDestination(FindNonChangeParentOutput(wallet, coin.outpoint).scriptPubKey, address)) {
@@ -457,31 +476,25 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
457476
{
458477
// Run coin selection on each OutputType and compute the Waste Metric
459478
std::vector<SelectionResult> results;
460-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.legacy, coin_selection_params)}) {
461-
results.push_back(*result);
462-
}
463-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.P2SH_segwit, coin_selection_params)}) {
464-
results.push_back(*result);
465-
}
466-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.bech32, coin_selection_params)}) {
467-
results.push_back(*result);
468-
}
469-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.bech32m, coin_selection_params)}) {
470-
results.push_back(*result);
479+
for (const auto& it : available_coins.coins) {
480+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, it.second, coin_selection_params)}) {
481+
results.push_back(*result);
482+
}
471483
}
472-
473-
// If we can't fund the transaction from any individual OutputType, run coin selection
474-
// over all available coins, else pick the best solution from the results
475-
if (results.size() == 0) {
476-
if (allow_mixed_output_types) {
477-
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.all(), coin_selection_params)}) {
478-
return result;
479-
}
484+
// If we have at least one solution for funding the transaction without mixing, choose the minimum one according to waste metric
485+
// and return the result
486+
if (results.size() > 0) return *std::min_element(results.begin(), results.end());
487+
488+
// If we can't fund the transaction from any individual OutputType, run coin selection one last time
489+
// over all available coins, which would allow mixing
490+
if (allow_mixed_output_types) {
491+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.All(), coin_selection_params)}) {
492+
return result;
480493
}
481-
return std::optional<SelectionResult>();
482-
};
483-
std::optional<SelectionResult> result{*std::min_element(results.begin(), results.end())};
484-
return result;
494+
}
495+
// Either mixing is not allowed and we couldn't find a solution from any single OutputType, or mixing was allowed and we still couldn't
496+
// find a solution using all available coins
497+
return std::nullopt;
485498
};
486499

487500
std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins, const CoinSelectionParams& coin_selection_params)
@@ -592,11 +605,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
592605

593606
// remove preset inputs from coins so that Coin Selection doesn't pick them.
594607
if (coin_control.HasSelected()) {
595-
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());
596-
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());
597-
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());
598-
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());
599-
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());
608+
available_coins.Erase(preset_coins);
600609
}
601610

602611
unsigned int limit_ancestor_count = 0;
@@ -608,15 +617,11 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
608617

609618
// form groups from remaining coins; note that preset coins will not
610619
// automatically have their associated (same address) coins included
611-
if (coin_control.m_avoid_partial_spends && available_coins.size() > OUTPUT_GROUP_MAX_ENTRIES) {
620+
if (coin_control.m_avoid_partial_spends && available_coins.Size() > OUTPUT_GROUP_MAX_ENTRIES) {
612621
// Cases where we have 101+ outputs all pointing to the same destination may result in
613622
// privacy leaks as they will potentially be deterministically sorted. We solve that by
614623
// explicitly shuffling the outputs before processing
615-
Shuffle(available_coins.legacy.begin(), available_coins.legacy.end(), coin_selection_params.rng_fast);
616-
Shuffle(available_coins.P2SH_segwit.begin(), available_coins.P2SH_segwit.end(), coin_selection_params.rng_fast);
617-
Shuffle(available_coins.bech32.begin(), available_coins.bech32.end(), coin_selection_params.rng_fast);
618-
Shuffle(available_coins.bech32m.begin(), available_coins.bech32m.end(), coin_selection_params.rng_fast);
619-
Shuffle(available_coins.other.begin(), available_coins.other.end(), coin_selection_params.rng_fast);
624+
available_coins.Shuffle(coin_selection_params.rng_fast);
620625
}
621626

622627
// Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the

src/wallet/spend.h

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,22 @@ TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* walle
3434
* This struct is really just a wrapper around OutputType vectors with a convenient
3535
* method for concatenating and returning all COutputs as one vector.
3636
*
37-
* clear(), size() methods are implemented so that one can interact with
38-
* the CoinsResult struct as if it was a vector
37+
* Size(), Clear(), Erase(), Shuffle(), and Add() methods are implemented to
38+
* allow easy interaction with the struct.
3939
*/
4040
struct CoinsResult {
41-
/** Vectors for each OutputType */
42-
std::vector<COutput> legacy;
43-
std::vector<COutput> P2SH_segwit;
44-
std::vector<COutput> bech32;
45-
std::vector<COutput> bech32m;
46-
47-
/** Other is a catch-all for anything that doesn't match the known OutputTypes */
48-
std::vector<COutput> other;
41+
std::map<OutputType, std::vector<COutput>> coins;
4942

5043
/** Concatenate and return all COutputs as one vector */
51-
std::vector<COutput> all() const;
44+
std::vector<COutput> All() const;
5245

5346
/** The following methods are provided so that CoinsResult can mimic a vector,
5447
* i.e., methods can work with individual OutputType vectors or on the entire object */
55-
uint64_t size() const;
56-
void clear();
48+
size_t Size() const;
49+
void Clear();
50+
void Erase(std::set<COutPoint>& preset_coins);
51+
void Shuffle(FastRandomContext& rng_fast);
52+
void Add(OutputType type, const COutput& out);
5753

5854
/** Sum of all available coins */
5955
CAmount total_amount{0};

0 commit comments

Comments
 (0)