Merge bitcoin/bitcoin#33160: bench: Add more realistic Coin Selection Bench

0429c503fb bench: Replace Coin Selection bench (Murch)
ec1eefda77 bench: Remove unnecessary wallet parameter (Murch)
e6c4ffb956 bench: Fix type mismatch (Murch)

Pull request description:

  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.

ACKs for top commit:
  l0rinc:
    code review ACK 0429c503fb
  sedited:
    ACK 0429c503fb

Tree-SHA512: 53238d39c8f6d543d80af77e3bb23ab418f2ee266a5ae407fd739c158ca86db553457dcc372b7aa5017f392fb5ae784394cad9edd79b1c0f58ffc32c89e0c306
This commit is contained in:
merge-script
2026-05-20 10:34:59 +02:00

View File

@@ -24,12 +24,13 @@
#include <cassert>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <utility>
#include <vector>
namespace wallet {
static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<std::unique_ptr<CWalletTx>>& wtxs)
static void addCoin(const CAmount& nValue, std::vector<std::unique_ptr<CWalletTx>>& wtxs)
{
static int nextLockTime = 0;
CMutableTransaction tx;
@@ -39,13 +40,13 @@ static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<st
wtxs.push_back(std::make_unique<CWalletTx>(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
}
// Simple benchmark for wallet coin selection. Note that it maybe be necessary
// to build up more complicated scenarios in order to get meaningful
// measurements of performance. From laanwj, "Wallet coin selection is probably
// the hardest, as you need a wider selection of scenarios, just testing the
// same one over and over isn't too useful. Generating random isn't useful
// either for measurements."
// (https://github.com/bitcoin/bitcoin/issues/7883#issuecomment-224807484)
// This benchmark is based on a large diverse UTXO pool. The UTXOs are
// pseudorandomly generated and assigned one of the four relevant output types
// P2PKH, P2SH-P2WPKH, P2WPKH, and P2TR UTXOs.
// 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
// representative coin selection scenario.
static void CoinSelection(benchmark::Bench& bench)
{
const auto test_setup = MakeNoLogFileContext<TestingSetup>();
@@ -53,52 +54,99 @@ static void CoinSelection(benchmark::Bench& bench)
std::vector<std::unique_ptr<CWalletTx>> wtxs;
LOCK(wallet.cs_wallet);
// Add coins.
for (int i = 0; i < 1000; ++i) {
addCoin(1000 * COIN, wallet, wtxs);
}
addCoin(3 * COIN, wallet, wtxs);
// Keep selection deterministic for benchmark stability
FastRandomContext det_rand{/*fDeterministic=*/true};
// Create coins
// Generate coin amounts biased towards smaller amounts
for (int i = 0; i < 400; ++i) {
CAmount amount;
int p{det_rand.randrange(100)};
if (p < 50) {
amount = 10'000 + det_rand.randrange(90'000);
} 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);
} else {
amount = 10'000'000 + det_rand.randrange(90'000'000);
}
addCoin(amount, wtxs);
}
// Create coins from the amounts assigning them various output types
wallet::CoinsResult available_coins;
for (const auto& wtx : wtxs) {
const auto txout = wtx->tx->vout.at(0);
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);
OutputType outtype;
int input_bytes;
int y{det_rand.randrange(100)};
if (y < 35) {
outtype = OutputType::LEGACY;
input_bytes = 148;
} else if (y < 55) {
outtype = OutputType::P2SH_SEGWIT;
input_bytes = 91;
} else if (y < 90) {
outtype = OutputType::BECH32;
input_bytes = 68;
} else {
outtype = OutputType::BECH32M;
input_bytes = 58;
}
CAmount fees = 20 * input_bytes;
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);
}
const CoinEligibilityFilter filter_standard(1, 6, 0);
FastRandomContext rand{};
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 * COIN, group, coin_selection_params, /*allow_mixed_output_types=*/true);
assert(result);
assert(result->GetSelectedValue() == 1003 * COIN);
assert(result->GetInputSet().size() == 2);
});
const CoinEligibilityFilter filter_standard(/*conf_mine=*/1, /*conf_theirs=*/6, /*max_ancestors=*/0);
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));
}
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]);
}
});
}
// Copied from src/wallet/test/coinselector_tests.cpp
static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>& set)
static void add_coin(const 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, /*cluster_count=*/ 0);
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();