Adds a Coin Selection benchmark that doesn’t just test a worst case of one of the algorithms but exercises coin selection to to select inputs for a variety of different targets from a large number of UTXOs.
bench: Add more realistic Coin Selection Bench #33160
pull murchandamus wants to merge 3 commits into bitcoin:master from murchandamus:2025-08-improve-coinselection-bench changing 1 files +71 −34-
murchandamus commented at 2:17 AM on August 9, 2025: member
- DrahtBot added the label Tests on Aug 9, 2025
-
DrahtBot commented at 2:18 AM on August 9, 2025: contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
Code Coverage & Benchmarks
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33160.
<!--021abf342d371248e50ceaed478a90ca-->
Reviews
See the guideline for information on the review process.
Type Reviewers Concept ACK l0rinc If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--174a7506f384e20aa4161008e828411d-->
Conflicts
Reviewers, this pull request conflicts with the following ones:
- #33034 (wallet: Store transactions in a separate sqlite table by achow101)
- #33032 (wallet, test: Replace MockableDatabase with in-memory SQLiteDatabase by achow101)
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
LLM Linter (✨ experimental)
Possible places where named args for integral literals may be used (e.g.
func(x, /*named_arg=*/0)in C++, andfunc(x, named_arg=0)in Python):COutPoint(wtx->GetHash(), 0)insrc/bench/coin_selection.cpp
<sup>2026-04-07 22:25:35</sup>
- DrahtBot added the label CI failed on Aug 9, 2025
-
DrahtBot commented at 3:18 AM on August 9, 2025: contributor
<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. <sub>Task
tidy: https://github.com/bitcoin/bitcoin/runs/47721081968</sub> <sub>LLM reason (✨ experimental): clang-tidy detected a performance-inefficient vector operation error, causing the CI to fail.</sub><details><summary>Hints</summary>
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
</details>
- murchandamus force-pushed on Aug 18, 2025
-
in src/bench/coin_selection.cpp:123 in 2dfb6d5ebd outdated
118 | + // Add coins. 119 | + for (int i = 0; i < 400; ++i) { 120 | + int x{det_rand.randrange(100)}; 121 | + if (x < 50) { 122 | + // 0.0001–0.001 COIN 123 | + addCoin(det_rand.randrange(90'000) + 10'000, wallet, wtxs);
l0rinc commented at 8:47 PM on August 26, 2025:we don't actually need the wallet here, right?
murchandamus commented at 8:43 PM on March 9, 2026:Removed the wallet argument from addCoin(…)
in src/bench/coin_selection.cpp:159 in 2dfb6d5ebd outdated
154 | + // 0.1–1 COIN 155 | + targets.push_back(det_rand.randrange(90'000'000) + 10'000'000); 156 | + } 157 | + 158 | + // Allow actual randomness for selection 159 | + FastRandomContext rand{};
l0rinc commented at 9:09 PM on August 26, 2025:we want deterministic randomness for benchmarks, otherwise it's hard to know what we're actually measuring:
FastRandomContext rand{/*fDeterministic=*/true};
murchandamus commented at 7:40 PM on March 9, 2026:Taken
in src/bench/coin_selection.cpp:153 in 2dfb6d5ebd outdated
148 | + available_coins.coins[OutputType::BECH32M].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); 149 | + } 150 | + } 151 | + 152 | + std::vector<CAmount> targets; 153 | + for (int i = 0; i < 100; ++i) {
l0rinc commented at 9:10 PM on August 26, 2025:we could reserve this to avoid the build failure:
std::vector<CAmount> targets; targets.reserve(10); for (size_t i{0}; i < targets.capacity(); ++i) { targets.push_back(rand_range(rng, 0.1_btc, 1_btc)); }Note that I have reduced the target count to 10 since the benchmark was very slow otherwise
murchandamus commented at 7:49 PM on March 9, 2026:I reduced it to ten targets and added the reserve statement.
l0rinc commented at 3:26 PM on March 13, 2026:I realized that looping to
targets.capacity()is not a stable way to cap this benchmark at 10 targets, since the reservation is often rounded up to powers of two. We should extract the target count to a constant instead:constexpr size_t NUM_TARGETS{10}; std::vector<CAmount> targets; targets.reserve(NUM_TARGETS); for (size_t i{0}; i < NUM_TARGETS; ++i) { targets.push_back(10'000'000 + det_rand.randrange(90'000'000)); }Which would also allow us to make the benchmark average out the batch:
bench.batch(NUM_TARGETS).run([&] {in src/bench/coin_selection.cpp:102 in 2dfb6d5ebd outdated
95 | @@ -99,6 +96,90 @@ static void CoinSelection(benchmark::Bench& bench) 96 | }); 97 | } 98 | 99 | +// This benchmark is based on a UTXO pool composed of 400 UTXOs. The UTXOs are 100 | +// pseudorandomly generated to be of the four relevant output types P2PKH, 101 | +// P2SH-P2WPKH, P2WPKH, and P2TR UTXOs, and fall in the range of 10'000 sats to 102 | +// 1 ₿ with larger amounts being more likely.
l0rinc commented at 9:13 PM on August 26, 2025:I'd avoid the
Narrow No-Break Spacechars from docs if possible, they're rendered differently on different mediums
murchandamus commented at 6:50 PM on March 9, 2026:Okay, replaced with regular spaces.
in src/bench/coin_selection.cpp:116 in 2dfb6d5ebd outdated
111 | + std::vector<std::unique_ptr<CWalletTx>> wtxs; 112 | + LOCK(wallet.cs_wallet); 113 | + 114 | + // Use arbitrary static seed for generating a pseudorandom scenario 115 | + uint256 arb_seed = uint256("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 116 | + FastRandomContext det_rand{arb_seed};
l0rinc commented at 9:15 PM on August 26, 2025:Same, seems simpler to just leave it to deterministic instead of hard-coding a confusing seed
FastRandomContext det_rand{/*fDeterministic=*/true};note: can we reuse this for the random source below as well?
murchandamus commented at 7:03 PM on March 9, 2026:Took your suggestion.
in src/bench/coin_selection.cpp:141 in 2dfb6d5ebd outdated
136 | + // Create coins 137 | + wallet::CoinsResult available_coins; 138 | + for (const auto& wtx : wtxs) { 139 | + const auto txout = wtx->tx->vout.at(0); 140 | + int y{det_rand.randrange(100)}; 141 | + if (y < 35) {
l0rinc commented at 9:18 PM on August 26, 2025:we could dedup considerable here for better signal-to-noise ratio - it's a lot of work to find the differences between the values:
for (const auto& wtx : wtxs) { const auto txout{wtx->tx->vout.at(0)}; const int input_bytes{CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr)}; const COutput output{(COutPoint{wtx->GetHash(), 0}), txout, /*depth=*/6 * 24, input_bytes, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0}; if (int y{rng.randrange(100)}; y < 35) { available_coins.coins[OutputType::LEGACY].push_back(output); } else if (y < 55) { available_coins.coins[OutputType::P2SH_SEGWIT].push_back(output); } else if (y < 90) { available_coins.coins[OutputType::BECH32].push_back(output); } else { available_coins.coins[OutputType::BECH32M].push_back(output); } }or even more thoroghly:
for (const auto& wtx : wtxs) { const int p{rand_percentage(rng)}; auto val{p < 35 ? OutputType::LEGACY : p < 55 ? OutputType::P2SH_SEGWIT : p < 90 ? OutputType::BECH32 : OutputType::BECH32M}; const auto txout{wtx->tx->vout.at(0)}; const int input_bytes{CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr)}; const COutput output{COutPoint{wtx->GetHash(), 0}, txout, /*depth=*/6 * 24, input_bytes, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0}; available_coins.coins[val].emplace_back(output); }
murchandamus commented at 8:24 PM on March 9, 2026:Nice idea. I realized that the weight was actually -1 everywhere and instead assigned weight and fees as intended.
in src/bench/coin_selection.cpp:142 in 2dfb6d5ebd outdated
137 | + wallet::CoinsResult available_coins; 138 | + for (const auto& wtx : wtxs) { 139 | + const auto txout = wtx->tx->vout.at(0); 140 | + int y{det_rand.randrange(100)}; 141 | + if (y < 35) { 142 | + available_coins.coins[OutputType::LEGACY].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);
l0rinc commented at 9:22 PM on August 26, 2025:Please rebase,
spendablewas removed since: https://github.com/bitcoin/bitcoin/commit/6a7aa015747e2634fe5a4b2f7fa0d104eb75c796#diff-38f1a8db124a979cb6dd76ce263f7aae0053d6967ee909e6356115fa0402dc8cL78
murchandamus commented at 7:49 PM on March 9, 2026:I removed the
spendablearguments.in src/bench/coin_selection.cpp:155 in 2dfb6d5ebd outdated
150 | + } 151 | + 152 | + std::vector<CAmount> targets; 153 | + for (int i = 0; i < 100; ++i) { 154 | + // 0.1–1 COIN 155 | + targets.push_back(det_rand.randrange(90'000'000) + 10'000'000);
l0rinc commented at 9:30 PM on August 26, 2025:nit: since you're also mentioning the minimum in the comment first, we might as well put the fixed size first here
targets.push_back(10'000'000 + det_rand.randrange(90'000'000));Alternatively, to avoid all the 0s, consider:
targets.push_back(COIN / 10 + rng.randrange(COIN / 9 * 10));This would unify it with the rest of the usages.
this has come up multiple times, we should really add a
randrangehelper with min/max values - but for now we can just do that here to make the test itself as clear as possible (we have a lot of comments, indicating that the code needed extra explanation, so let's clear up the code instead and remove comments)namespace { constexpr CAmount operator""_sat(uint64_t v) { return v; } constexpr CAmount operator""_ksat(uint64_t v) { return v * 1'000; } constexpr CAmount operator""_btc(uint64_t v) { return v * COIN; } constexpr CAmount operator""_btc(long double v) { return std::llround(v * COIN); } // TODO if you don't trust doubles this could also work constexpr CAmount operator""_btc(const char* s, size_t n) { CAmount a{0}; assert(ParseFixedPoint(std::string{s, n}, 8, &a)); return a; } int rand_percentage(FastRandomContext& rng) { return rng.randrange(101); } CAmount rand_range(FastRandomContext& rng, CAmount lo, CAmount hi) { return lo + rng.randrange(hi - lo + 1); } } // namespacewhich would simplify the above to
targets.push_back(rand_range(rng, 0.1_btc, 1_btc));
murchandamus commented at 8:29 PM on March 9, 2026:I switched the minimum and random part as suggested. I find the straight-up sat amounts more readable than mixing multiple units or the suffix construction, so I left that as was.
in src/bench/coin_selection.cpp:184 in 2dfb6d5ebd outdated
179 | + } 180 | + }); 181 | +} 182 | + 183 | // Copied from src/wallet/test/coinselector_tests.cpp 184 | static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>& set)
l0rinc commented at 9:35 PM on August 26, 2025:nInputshould beuint32_totherwise we'd have a narrowing conversion inOutPoint{tx.GetHash(), nInput}
murchandamus commented at 8:29 PM on March 9, 2026:Changed to uint32_t
in src/bench/coin_selection.cpp:132 in 76a4a4b86c outdated
127 | + } else if (x < 95) { 128 | + // 0.01–0.1 COIN 129 | + addCoin(det_rand.randrange(9'000'000) + 1'000'000, wallet, wtxs); 130 | + } else { 131 | + // 0.1–1 COIN 132 | + addCoin(det_rand.randrange(90'000'000) + 10'000'000, wallet, wtxs);
l0rinc commented at 10:13 PM on August 26, 2025:we could also separate the parts that change from the parts that don't, to improve readability by reducing number of moving parts:
for (int i{0}; i < 400; ++i) { CAmount val; if (int p{rand_percentage(rng)}; p < 50) { val = rand_range(rng, 10_ksat, 100_ksat); } else if (p < 75) { val = rand_range(rng, 100_ksat, 1_Msat); } else if (p < 95) { val = rand_range(rng, 1_Msat, 10_Msat); } else { val = rand_range(rng, 0.1_btc, 1_btc); } addCoin(val, wtxs); }or
for (int i{0}; i < 400; ++i) { const int p{rand_percentage(rng)}; const auto val{p < 50 ? rand_range(rng, 10_ksat, 100_ksat) : p < 75 ? rand_range(rng, 100_ksat, 1_Msat) : p < 95 ? rand_range(rng, 1_Msat, 10_Msat) : rand_range(rng, 0.1_btc, 1_btc)}; addCoin(val, wtxs); }
murchandamus commented at 8:35 PM on March 9, 2026:Took a variant of the first
in src/bench/coin_selection.cpp:212 in 2dfb6d5ebd outdated
204 | @@ -205,16 +205,12 @@ static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool) 205 | 206 | static void BnBExhaustion(benchmark::Bench& bench) 207 | { 208 | - // Setup 209 | std::vector<OutputGroup> utxo_pool; 210 | 211 | + CAmount target = make_hard_case(17, utxo_pool); 212 | bench.run([&] { 213 | // Benchmark
l0rinc commented at 10:47 PM on August 26, 2025:we don't have to keep this comment either in 2dfb6d5ebdb579f58244534835f027cd11187ae3
murchandamus commented at 8:30 PM on March 9, 2026:Removed the comment
in src/bench/coin_selection.cpp:213 in 2dfb6d5ebd outdated
210 | 211 | + CAmount target = make_hard_case(17, utxo_pool); 212 | bench.run([&] { 213 | // Benchmark 214 | - CAmount target = make_hard_case(17, utxo_pool); 215 | SelectCoinsBnB(utxo_pool, target, 0, MAX_STANDARD_TX_WEIGHT); // Should exhaust
l0rinc commented at 10:47 PM on August 26, 2025:Should exhaust- can we assert that instead of adding a dead comment?
murchandamus commented at 8:32 PM on March 9, 2026:We currently don’t have access to whether the search concluded or was interrupted early. I propose that we return it with the changes in #32150, and when that gets adopted, we could check here.
in src/bench/coin_selection.cpp:104 in 76a4a4b86c outdated
95 | @@ -99,6 +96,90 @@ static void CoinSelection(benchmark::Bench& bench) 96 | }); 97 | } 98 | 99 | +// This benchmark is based on a UTXO pool composed of 400 UTXOs. The UTXOs are 100 | +// pseudorandomly generated to be of the four relevant output types P2PKH, 101 | +// P2SH-P2WPKH, P2WPKH, and P2TR UTXOs, and fall in the range of 10'000 sats to 102 | +// 1 ₿ with larger amounts being more likely. 103 | +// This UTXO pool is used to run coin selection for 100 pseudorandom selection 104 | +// targets from 0.1–2 ₿. Altogether, this gives us a deterministic benchmark
l0rinc commented at 10:55 PM on August 26, 2025:where are we adding 2 btc? Could we avoid values from the comments, they're just adding a maintenance burden when we're forgetting updating them
murchandamus commented at 7:01 PM on March 9, 2026:I rewrote the paragraph avoiding numbers.
in src/bench/coin_selection.cpp:176 in 76a4a4b86c outdated
171 | + }; 172 | + auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]; 173 | + 174 | + bench.run([&] { 175 | + for (const auto& target : targets) { 176 | + auto result = AttemptSelection(wallet.chain(), target, group, coin_selection_params, /*allow_mixed_output_types=*/true);
l0rinc commented at 11:18 PM on August 26, 2025:For the record, Knapsack and CoinsBnB are really slow (with debug build to avoid all the inlining), making this whole benchmark one of our heaviest - could we reduce some of the iterations? <img alt="image" src="https://github.com/user-attachments/assets/6c3c59ef-3fad-4128-b1c8-ec4c40df1dcb" />
murchandamus commented at 7:39 PM on March 9, 2026:Reduced to ten iterations as you suggested.
in src/bench/coin_selection.cpp:163 in 76a4a4b86c outdated
158 | + // Allow actual randomness for selection 159 | + FastRandomContext rand{}; 160 | + const CoinEligibilityFilter filter_standard(1, 6, 0); 161 | + const CoinSelectionParams coin_selection_params{ 162 | + rand, 163 | + /*change_output_size=*/ 31,
l0rinc commented at 11:56 PM on August 26, 2025:nit: let's unify the comment styles across the file
murchandamus commented at 8:35 PM on March 9, 2026:I replaced all instances of the space between the comment and value in this document. Please let me know if there was something else that I missed.
l0rinc changes_requestedl0rinc commented at 12:00 AM on August 27, 2025: contributorConcept ACK, the existing test were all quite trivial compared to this new one. I left a ton of comments, for simplicity here's the final code that I'm suggesting, feel free to pick and choose from it.
<details> <summary>Details</summary>
// Copyright (c) 2012-2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <bench/bench.h> #include <consensus/amount.h> #include <interfaces/chain.h> #include <node/context.h> #include <outputtype.h> #include <policy/feerate.h> #include <policy/policy.h> #include <primitives/transaction.h> #include <random.h> #include <sync.h> #include <util/result.h> #include <wallet/coinselection.h> #include <wallet/spend.h> #include <wallet/test/util.h> #include <wallet/transaction.h> #include <wallet/wallet.h> #include <cassert> #include <map> #include <memory> #include <set> #include <utility> #include <vector> #include <cmath> using node::NodeContext; using wallet::AttemptSelection; using wallet::CHANGE_LOWER; using wallet::COutput; using wallet::CWallet; using wallet::CWalletTx; using wallet::CoinEligibilityFilter; using wallet::CoinSelectionParams; using wallet::CreateMockableWalletDatabase; using wallet::OutputGroup; using wallet::SelectCoinsBnB; using wallet::TxStateInactive; namespace { constexpr CAmount operator""_sat(uint64_t v) { return v; } constexpr CAmount operator""_ksat(uint64_t v) { return v * 1'000; } constexpr CAmount operator""_btc(uint64_t v) { return v * COIN; } constexpr CAmount operator""_btc(long double v) { return std::llround(v * COIN); } int rand_percentage(FastRandomContext& rng) { return rng.randrange(100); } CAmount rand_range(FastRandomContext& rng, CAmount lo, CAmount hi) { return lo + rng.randrange(hi - lo + 1); } } // namespace static void addCoin(CAmount nValue, std::vector<std::unique_ptr<CWalletTx>>& wtxs) { static int nextLockTime = 0; CMutableTransaction tx; tx.nLockTime = nextLockTime++; // so all transactions get different hashes tx.vout.resize(1); tx.vout[0].nValue = nValue; wtxs.push_back(std::make_unique<CWalletTx>(MakeTransactionRef(std::move(tx)), TxStateInactive{})); } // Simple benchmark for wallet coin selection that exercises a worst-case // scenario for Knapsack: All UTXOs are necessary, but it is not an exact // match, so the only eligible input set is only discovered on the second pass // after all random walks fail to produce a solution. static void KnapsackWorstCase(benchmark::Bench& bench) { NodeContext node; auto chain{interfaces::MakeChain(node)}; CWallet wallet(chain.get(), "", CreateMockableWalletDatabase()); std::vector<std::unique_ptr<CWalletTx>> wtxs; LOCK(wallet.cs_wallet); for (int i{0}; i < 1000; ++i) { addCoin(1000_btc, wtxs); } addCoin(3_btc, wtxs); // Create coins wallet::CoinsResult available_coins; for (const auto& wtx : wtxs) { const auto txout{wtx->tx->vout.at(0)}; const int input_bytes{CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr)}; const COutput output{COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, input_bytes, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0}; available_coins.coins[OutputType::BECH32].emplace_back(output); } const CoinEligibilityFilter filter_standard(1, 6, 0); FastRandomContext rand{/*fDeterministic=*/true}; const CoinSelectionParams coin_selection_params{ rand, /*change_output_size=*/34, /*change_spend_size=*/148, /*min_change_target=*/CHANGE_LOWER, /*effective_feerate=*/CFeeRate(20'000), /*long_term_feerate=*/CFeeRate(10'000), /*discard_feerate=*/CFeeRate(3000), /*tx_noinputs_size=*/0, /*avoid_partial=*/false, }; auto group{wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]}; bench.run([&] { auto result{AttemptSelection(wallet.chain(), 1002.99_btc, group, coin_selection_params, /*allow_mixed_output_types=*/true)}; assert(result); assert(result->GetSelectedValue() == 1003_btc); assert(result->GetInputSet().size() == 2); }); } // This benchmark is based on a UTXO pool composed of 400 UTXOs. The UTXOs are // pseudorandomly generated to be of the four relevant output types P2PKH, // P2SH-P2WPKH, P2WPKH, and P2TR UTXOs, and fall in the range of 10'000 sats to // 1₿ with larger amounts being more likely. // This UTXO pool is used to run coin selection for 100 pseudorandom selection // targets from 0.1–2₿. Altogether, this gives us a deterministic benchmark // with a hopefully somewhat representative coin selection scenario. static void CoinSelectionOnDiverseWallet(benchmark::Bench& bench) { FastRandomContext rng{/*fDeterministic=*/true}; NodeContext node; auto chain{interfaces::MakeChain(node)}; CWallet wallet(chain.get(), "", CreateMockableWalletDatabase()); LOCK(wallet.cs_wallet); std::vector<std::unique_ptr<CWalletTx>> wtxs; wtxs.reserve(400); for (size_t i{0}; i < wtxs.capacity(); ++i) { const int p{rand_percentage(rng)}; const auto val{p < 50 ? rand_range(rng, 10_ksat, 100_ksat) : p < 75 ? rand_range(rng, 100_ksat, 1000_ksat) : p < 95 ? rand_range(rng, 1000_ksat, 1_btc) : rand_range(rng, 0.1_btc, 1_btc)}; addCoin(val, wtxs); } // Create coins wallet::CoinsResult available_coins; for (const auto& wtx : wtxs) { const int p{rand_percentage(rng)}; auto val{p < 35 ? OutputType::LEGACY : p < 55 ? OutputType::P2SH_SEGWIT : p < 90 ? OutputType::BECH32 : OutputType::BECH32M}; const auto txout{wtx->tx->vout.at(0)}; const int input_bytes{CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr)}; const COutput output{COutPoint{wtx->GetHash(), 0}, txout, /*depth=*/6 * 24, input_bytes, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0}; available_coins.coins[val].emplace_back(output); } std::vector<CAmount> targets; targets.reserve(10); for (size_t i{0}; i < targets.capacity(); ++i) { targets.push_back(rand_range(rng, 0.1_btc, 1_btc)); } const CoinSelectionParams coin_selection_params{ rng, /*change_output_size=*/31, /*change_spend_size=*/68, /*min_change_target=*/CHANGE_LOWER, /*effective_feerate=*/CFeeRate(20'000), /*long_term_feerate=*/CFeeRate(10'000), /*discard_feerate=*/CFeeRate(3000), /*tx_noinputs_size=*/72, /*avoid_partial=*/false, }; const CoinEligibilityFilter filter_standard(1, 6, 0); auto group{wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]}; bench.run([&] { for (const auto& target : targets) { auto result{AttemptSelection(wallet.chain(), target, group, coin_selection_params, /*allow_mixed_output_types=*/true)}; assert(result && result->GetSelectedValue() >= target); } }); } static void add_coin(CAmount nValue, uint32_t nInput, std::vector<OutputGroup>& set) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; COutput output(COutPoint{tx.GetHash(), nInput}, tx.vout.at(nInput), /*depth=*/0, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/true, /*fees=*/0); set.emplace_back(); set.back().Insert(std::make_shared<COutput>(output), /*ancestors=*/0, /*descendants=*/0); } static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool) { utxo_pool.clear(); CAmount target{0}; for (int i{0}; i < utxos; ++i) { target += 1_sat << (utxos + i); add_coin(1_sat << (utxos + i), 2 * i, utxo_pool); add_coin((1_sat << (utxos + i)) + (1_sat << (utxos - 1 - i)), 2 * i + 1, utxo_pool); } return target; } static void BnBExhaustion(benchmark::Bench& bench) { std::vector<OutputGroup> utxo_pool; auto target{make_hard_case(17, utxo_pool)}; bench.run([&] { SelectCoinsBnB(utxo_pool, target, 0, MAX_STANDARD_TX_WEIGHT); // Should exhaust }); } BENCHMARK(KnapsackWorstCase, benchmark::PriorityLevel::HIGH); BENCHMARK(CoinSelectionOnDiverseWallet, benchmark::PriorityLevel::HIGH); BENCHMARK(BnBExhaustion, benchmark::PriorityLevel::HIGH);</details>
maflcko removed the label CI failed on Sep 26, 2025DrahtBot added the label Needs rebase on Dec 9, 2025murchandamus force-pushed on Mar 8, 2026DrahtBot added the label CI failed on Mar 8, 2026DrahtBot commented at 8:53 PM on March 8, 2026: contributor<!--85328a0da195eb286784d51f73fa0af9-->
🚧 At least one of the CI tasks failed. <sub>Task
test ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/22828405687/job/66212445052</sub> <sub>LLM reason (✨ experimental): Compilation errors in bench_bitcoin (coin_selection.cpp) caused the build to fail.</sub><details><summary>Hints</summary>
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
</details>
DrahtBot removed the label Needs rebase on Mar 8, 2026murchandamus commented at 8:57 PM on March 9, 2026: memberThanks for your comments, @l0rinc.
murchandamus force-pushed on Mar 9, 2026murchandamus force-pushed on Mar 9, 2026murchandamus force-pushed on Mar 9, 2026murchandamus force-pushed on Mar 9, 2026murchandamus force-pushed on Mar 9, 2026DrahtBot removed the label CI failed on Mar 9, 2026murchandamus force-pushed on Mar 12, 2026murchandamus marked this as ready for review on Mar 12, 2026murchandamus requested review from l0rinc on Mar 12, 2026in src/bench/coin_selection.cpp:78 in 8b0ab66156
76 | - available_coins.coins[OutputType::BECH32].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/ 0); 77 | + available_coins.coins[OutputType::BECH32].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0); 78 | } 79 | 80 | const CoinEligibilityFilter filter_standard(1, 6, 0); 81 | FastRandomContext rand{};
l0rinc commented at 3:40 PM on March 13, 2026:FastRandomContext rand{/*fDeterministic=*/true};
murchandamus commented at 9:32 PM on March 13, 2026:Taken
in src/bench/coin_selection.cpp:77 in 8b0ab66156
75 | const auto txout = wtx->tx->vout.at(0); 76 | - available_coins.coins[OutputType::BECH32].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/ 0); 77 | + available_coins.coins[OutputType::BECH32].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0); 78 | } 79 | 80 | const CoinEligibilityFilter filter_standard(1, 6, 0);
l0rinc commented at 3:44 PM on March 13, 2026:Could add helpers to a few more places:
const CoinEligibilityFilter filter_standard(/*conf_mine=*/1, /*conf_theirs=*/6, /*max_ancestors=*/0);
murchandamus commented at 9:30 PM on March 13, 2026:Taken
in src/bench/coin_selection.cpp:101 in 8b0ab66156
95 | @@ -99,15 +96,103 @@ static void CoinSelection(benchmark::Bench& bench) 96 | }); 97 | } 98 | 99 | +// This benchmark is based on a UTXO pool composed of hundreds of UTXOs. The 100 | +// UTXOs are pseudorandomly generated to be of the four relevant output types 101 | +// P2PKH, P2SH-P2WPKH, P2WPKH, and P2TR UTXOs.
l0rinc commented at 3:54 PM on March 13, 2026:nit: the benchmark doesn't actually generate UTXOs of those script types:
// The UTXOs are pseudorandomly generated and assigned one of the four // relevant output types: P2PKH, P2SH-P2WPKH, P2WPKH, and P2TR.
murchandamus commented at 9:30 PM on March 13, 2026:Taken
in src/bench/coin_selection.cpp:180 in 8b0ab66156 outdated
175 | + }; 176 | + auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]; 177 | + 178 | + bench.run([&] { 179 | + for (const auto& target : targets) { 180 | + auto result = AttemptSelection(wallet.chain(), target, group, coin_selection_params, /*allow_mixed_output_types=*/true);
l0rinc commented at 4:03 PM on March 13, 2026:nit: is
/*allow_mixed_output_types=*/falsemaybe more accurate here? We basically always return at https://github.com/bitcoin/bitcoin/blob/3a222507fd4d61c4ac8b4506343a345d66ee76b1/src/wallet/spend.cpp#L716 (is that deliberate?), which means that https://github.com/bitcoin/bitcoin/blob/3a222507fd4d61c4ac8b4506343a345d66ee76b1/src/wallet/spend.cpp#L716-L723 is basically never executed.
murchandamus commented at 8:29 PM on March 13, 2026:Given the random targets and random UTXOs, it would be possible for all output types by themselves not to have sufficient funds to produce a solution. In that case, we would fall back to mix the UTXOs and select from all of them together. This is very unlikely with 400 UTXOs, but still seems correct to me.
l0rinc commented at 5:59 PM on April 9, 2026:Can you please double-check? Changing the above to:
if (allow_mixed_output_types && groups.TypesCount() > 1) { throw "ChooseSelectionResult(chain, nTargetValue, groups.all_groups, coin_selection_params)"; }still passes, so it's not theoretic.
in src/bench/coin_selection.cpp:122 in 8b0ab66156
117 | + for (int i = 0; i < 400; ++i) { 118 | + CAmount amount; 119 | + int p{det_rand.randrange(100)}; 120 | + if (p < 50) { 121 | + amount = 10'000 + det_rand.randrange(90'000); 122 | + } else if (p < 75) {
l0rinc commented at 4:04 PM on March 13, 2026:} else if (p < 75) {in src/bench/coin_selection.cpp:187 in 8b0ab66156
182 | + assert(result->GetSelectedValue() >= target); 183 | + } 184 | + }); 185 | +} 186 | + 187 | // Copied from src/wallet/test/coinselector_tests.cpp
l0rinc commented at 4:04 PM on March 13, 2026:are these
Copied fromcomments useful?
murchandamus commented at 9:17 PM on March 13, 2026:Removed.
in src/bench/coin_selection.cpp:162 in 8b0ab66156
157 | + targets.reserve(10); 158 | + for (size_t i{0}; i < targets.capacity(); ++i) { 159 | + targets.push_back(10'000'000 + det_rand.randrange(90'000'000)); 160 | + } 161 | + 162 | + // Allow actual randomness for selection
l0rinc commented at 4:06 PM on March 13, 2026:// Keep selection deterministic for benchmark stability
murchandamus commented at 10:20 PM on March 13, 2026:Put above
det_randin src/bench/coin_selection.cpp:92 in 8b0ab66156
l0rinc commented at 4:17 PM on March 13, 2026:slightly unrelated: we could maybe change floating point here to avoid potential weirdness:
auto result = AttemptSelection(wallet.chain(), 1002 * COIN + 99 * CENT, group, coin_selection_params, /*allow_mixed_output_types=*/true);or
auto result = AttemptSelection(wallet.chain(), 100'299'000'000, group, coin_selection_params, /*allow_mixed_output_types=*/true);
murchandamus commented at 9:20 PM on March 13, 2026:Thanks, looking at the old benchmark again, I realized that I was mistaken in what it did. It had 1000 × 1000 BTC and 1 × 3 BTC, and then looked that 1003 would be selected via 1000 + 3.
I don’t think it’s an interesting case to keep around next to the new benchmark, so I propose removing it.
l0rinc approvedl0rinc commented at 4:27 PM on March 13, 2026: contributorI'm mostly okay with the change, left a few more comments
<details><summary>All combined suggestions</summary>
diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp --- a/src/bench/coin_selection.cpp (revision 67544d3b713f945e6c9fa826aa081a1b966f458a) +++ b/src/bench/coin_selection.cpp (date 1773418910077) @@ -12,6 +12,7 @@ #include <primitives/transaction.h> #include <random.h> #include <sync.h> +#include <test/util/setup_common.h> #include <util/result.h> #include <wallet/coinselection.h> #include <wallet/spend.h> @@ -74,8 +75,8 @@ available_coins.coins[OutputType::BECH32].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/0); } - const CoinEligibilityFilter filter_standard(1, 6, 0); - FastRandomContext rand{}; + const CoinEligibilityFilter filter_standard(/*conf_mine=*/1, /*conf_theirs=*/6, /*max_ancestors=*/0); + FastRandomContext rand{/*fDeterministic=*/true}; const CoinSelectionParams coin_selection_params{ rand, /*change_output_size=*/34, @@ -89,7 +90,7 @@ }; auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]; bench.run([&] { - auto result = AttemptSelection(wallet.chain(), 1002.99 * COIN, group, coin_selection_params, /*allow_mixed_output_types=*/true); + auto result = AttemptSelection(wallet.chain(), 1002 * COIN + 99 * CENT, group, coin_selection_params, /*allow_mixed_output_types=*/false); assert(result); assert(result->GetSelectedValue() == 1003 * COIN); assert(result->GetInputSet().size() == 2); @@ -97,8 +98,8 @@ } // This benchmark is based on a UTXO pool composed of hundreds of UTXOs. The -// UTXOs are pseudorandomly generated to be of the four relevant output types -// P2PKH, P2SH-P2WPKH, P2WPKH, and P2TR UTXOs. +// UTXOs are pseudorandomly generated and assigned one of the four relevant +// output types: P2PKH, P2SH-P2WPKH, P2WPKH, and P2TR. // Smaller amounts are more likely to be generated than larger amounts. This // UTXO pool is used to run coin selection for pseudorandom selection targets. // Altogether, this gives us a deterministic benchmark with a somewhat @@ -119,7 +120,7 @@ int p{det_rand.randrange(100)}; if (p < 50) { amount = 10'000 + det_rand.randrange(90'000); - } else if (p < 75) { + } else if (p < 75) { amount = 100'000 + det_rand.randrange(900'000); } else if (p < 95) { amount = 1'000'000 + det_rand.randrange(9'000'000); @@ -153,15 +154,16 @@ available_coins.coins[outtype].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, /*input_bytes=*/input_bytes, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/fees); } + constexpr size_t NUM_TARGETS{10}; std::vector<CAmount> targets; - targets.reserve(10); - for (size_t i{0}; i < targets.capacity(); ++i) { + targets.reserve(NUM_TARGETS); + for (size_t i{0}; i < NUM_TARGETS; ++i) { targets.push_back(10'000'000 + det_rand.randrange(90'000'000)); } - // Allow actual randomness for selection + // Keep selection deterministic for benchmark stability FastRandomContext rand{/*fDeterministic=*/true}; - const CoinEligibilityFilter filter_standard(1, 6, 0); + const CoinEligibilityFilter filter_standard(/*conf_mine=*/1, /*conf_theirs=*/6, /*max_ancestors=*/0); const CoinSelectionParams coin_selection_params{ rand, /*change_output_size=*/31, @@ -175,7 +177,7 @@ }; auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]; - bench.run([&] { + bench.batch(NUM_TARGETS).run([&] { for (const auto& target : targets) { auto result = AttemptSelection(wallet.chain(), target, group, coin_selection_params, /*allow_mixed_output_types=*/true); assert(result); @@ -184,7 +186,6 @@ }); } -// Copied from src/wallet/test/coinselector_tests.cpp static void add_coin(const CAmount& nValue, uint32_t nInput, std::vector<OutputGroup>& set) { CMutableTransaction tx; @@ -194,7 +195,6 @@ set.emplace_back(); set.back().Insert(std::make_shared<COutput>(output), /*ancestors=*/0, /*cluster_count=*/0); } -// Copied from src/wallet/test/coinselector_tests.cpp static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool) { utxo_pool.clear(); @@ -213,7 +213,7 @@ CAmount target = make_hard_case(17, utxo_pool); bench.run([&] { - (void)SelectCoinsBnB(utxo_pool, target, /*cost_of_change=*/0, MAX_STANDARD_TX_WEIGHT); // Should exhaust + (void)SelectCoinsBnB(utxo_pool, target, /*cost_of_change=*/0, MAX_STANDARD_TX_WEIGHT); }); }</details>
murchandamus force-pushed on Mar 13, 2026murchandamus commented at 10:07 PM on March 13, 2026: memberTook almost all of your suggestion (unresolved where I didn’t or had comments).
Spent more time staring at it, and was left wondering what the old Coin Selection benchmark was adding at this point, so I instead restructured the PR to replace it.
in src/bench/coin_selection.cpp:107 in 6a9ac95c80 outdated
108 | + } else { 109 | + outtype = OutputType::BECH32M; 110 | + input_bytes = 58; 111 | + } 112 | + CAmount fees = 20 * input_bytes; 113 | + available_coins.coins[outtype].emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, /*input_bytes=*/input_bytes, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/fees);
murchandamus commented at 10:24 PM on March 13, 2026:Now with weight and fees! :)
DrahtBot added the label Needs rebase on Apr 6, 2026a7c5615087bench: Fix type mismatch
and cleanup some comments Co-authored-by: l0rinc <pap.lorinc@gmail.com>
3ae4f5d0c3bench: Remove unnecessary wallet parameter
Co-authored-by: l0rinc <pap.lorinc@gmail.com>
3b27f0b797bench: Replace Coin Selection bench
Co-authored-by: l0rinc <pap.lorinc@gmail.com>
murchandamus force-pushed on Apr 7, 2026murchandamus commented at 10:26 PM on April 7, 2026: memberRebased after conflict with #34208
DrahtBot added the label CI failed on Apr 7, 2026DrahtBot removed the label Needs rebase on Apr 7, 2026DrahtBot removed the label CI failed on Apr 9, 2026in src/bench/coin_selection.cpp:102 in a7c5615087
98 | @@ -99,17 +99,16 @@ static void CoinSelection(benchmark::Bench& bench) 99 | }); 100 | } 101 | 102 | -// Copied from src/wallet/test/coinselector_tests.cpp 103 | -static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>& set) 104 | +static void add_coin(const CAmount& nValue, uint32_t nInput, std::vector<OutputGroup>& set)
l0rinc commented at 4:07 PM on April 9, 2026:a7c5615 bench: Fix type mismatch:
nit: we could add
#include <cstdint>nit2: we could explain in the commit message why this was needed, someting like:
The BnB helper takes an input index that is passed straight to
COutPoint. Useuint32_tthere so the benchmark does not rely on an implicit narrowing conversion.in src/bench/coin_selection.cpp:133 in 3b27f0b797
150 | + targets.push_back(10'000'000 + det_rand.randrange(90'000'000)); 151 | + } 152 | + 153 | + bench.batch(NUM_TARGETS).run([&] { 154 | + for (const auto& target : targets) { 155 | + auto result = AttemptSelection(wallet.chain(), target, group, coin_selection_params, /*allow_mixed_output_types=*/true);
l0rinc commented at 5:12 PM on April 9, 2026:3b27f0b bench: Replace Coin Selection bench:
It seems
groupis mutated during the run (e.g. https://github.com/bitcoin/bitcoin/blob/141fbe4d530b51345e62dee1348e82d8a0406ffc/src/wallet/coinselection.cpp#L114 and https://github.com/bitcoin/bitcoin/blob/fa2670bd4b5b199c417011942228ba87d1613030/src/wallet/coinselection.cpp#L665) so we can't reuse it between runs - since the first iteration would be different from the rest this way.in src/bench/coin_selection.cpp:111 in 3b27f0b797
114 | } 115 | 116 | - const CoinEligibilityFilter filter_standard(1, 6, 0); 117 | - FastRandomContext rand{}; 118 | + const CoinEligibilityFilter filter_standard(/*conf_mine=*/1, /*conf_theirs=*/6, /*max_ancestors=*/0); 119 | const CoinSelectionParams coin_selection_params{
l0rinc commented at 5:17 PM on April 9, 2026:3b27f0b bench: Replace Coin Selection bench:
It seems some of the important parameters are not exposed through the constructor (e.g.
m_change_fee,min_viable_change,m_cost_of_change, maybe others), we should likely set these as well to have meaningful measurements. Maybe we could use https://github.com/bitcoin/bitcoin/blob/0831173c0171de33f95b96324db4041c4799b163/src/wallet/spend.cpp#L1063 to set these automatically, not sure.in src/bench/coin_selection.cpp:131 in 3b27f0b797
148 | + targets.reserve(NUM_TARGETS); 149 | + for (size_t i{0}; i < NUM_TARGETS; ++i) { 150 | + targets.push_back(10'000'000 + det_rand.randrange(90'000'000)); 151 | + } 152 | + 153 | + bench.batch(NUM_TARGETS).run([&] {
l0rinc commented at 6:01 PM on April 9, 2026:3b27f0b bench: Replace Coin Selection bench:
We could avoid the mutation (making each iteration slightly different) and the other mentioned problems with the new
setupmethod - this is what I came up with locally:diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp --- a/src/bench/coin_selection.cpp (revision 158569b595ea25c17750ab07c20d489803662971) +++ b/src/bench/coin_selection.cpp (date 1775757474430) @@ -22,6 +22,7 @@ #include <cassert> #include <map> #include <memory> +#include <optional> #include <set> #include <utility> #include <vector> @@ -108,18 +109,6 @@ } const CoinEligibilityFilter filter_standard(/*conf_mine=*/1, /*conf_theirs=*/6, /*max_ancestors=*/0); - const CoinSelectionParams coin_selection_params{ - det_rand, - /*change_output_size=*/31, - /*change_spend_size=*/68, - /*min_change_target=*/CHANGE_LOWER, - /*effective_feerate=*/CFeeRate(20'000), - /*long_term_feerate=*/CFeeRate(10'000), - /*discard_feerate=*/CFeeRate(3000), - /*tx_noinputs_size=*/72, - /*avoid_partial=*/false, - }; - auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard]; constexpr size_t NUM_TARGETS{10}; std::vector<CAmount> targets; @@ -128,13 +117,35 @@ targets.push_back(10'000'000 + det_rand.randrange(90'000'000)); } - bench.batch(NUM_TARGETS).run([&] { - for (const auto& target : targets) { - auto result = AttemptSelection(wallet.chain(), target, group, coin_selection_params, /*allow_mixed_output_types=*/true); - assert(result); - assert(result->GetSelectedValue() >= target); - } - }); + std::optional<FastRandomContext> rng; + std::optional<CoinSelectionParams> params; + std::vector<wallet::OutputGroupTypeMap> groups; + bench.batch(NUM_TARGETS).unit("selection").epochIterations(1) + .setup([&] { + rng.emplace(/*fDeterministic=*/true); + params.emplace(*rng); + + params->change_output_size = 31; + params->change_spend_size = 68; + params->m_min_change_target = CHANGE_LOWER; + params->m_effective_feerate = CFeeRate{20'000}; + params->m_long_term_feerate = CFeeRate{10'000}; + params->m_discard_feerate = CFeeRate{3000}; + params->tx_noinputs_size = 72; + params->m_avoid_partial_spends = false; + + params->m_change_fee = params->m_effective_feerate.GetFee(params->change_output_size); + params->min_viable_change = params->m_discard_feerate.GetFee(params->change_spend_size); + params->m_cost_of_change = params->min_viable_change + params->m_change_fee; + + groups.assign(NUM_TARGETS, wallet::GroupOutputs(wallet, available_coins, *params, {{filter_standard}})[filter_standard]); + }) + .run([&] { + for (size_t i{0}; i < NUM_TARGETS; ++i) { + auto result{AttemptSelection(wallet.chain(), targets[i], groups[i], *params, /*allow_mixed_output_types=*/true)}; + assert(result && result->GetSelectedValue() >= targets[i]); + } + }); } static void add_coin(const CAmount& nValue, uint32_t nInput, std::vector<OutputGroup>& set)l0rinc changes_requestedDrahtBot added the label Needs rebase on Apr 19, 2026DrahtBot commented at 10:09 AM on April 19, 2026: contributor<!--cf906140f33d8803c4a75a2196329ecb-->
🐙 This pull request conflicts with the target branch and needs rebase.
ContributorsLabels
github-metadata-mirror
This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-26 18:12 UTC
This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me