mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-02-09 02:59:31 +08:00
FUZZ: Test that BnB finds best solution
BnB’s solution is the input set with the lowest waste score, excluding any supersets of other solution candidates. This fuzz test compares a brute force search with the BnB result to ensure that BnB succeeds.
This commit is contained in:
@@ -217,6 +217,132 @@ FUZZ_TARGET(coin_grinder_is_optimal)
|
||||
assert(!result_cg);
|
||||
}
|
||||
|
||||
FUZZ_TARGET(bnb_finds_min_waste)
|
||||
{
|
||||
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
|
||||
|
||||
FastRandomContext fast_random_context{ConsumeUInt256(fuzzed_data_provider)};
|
||||
CoinSelectionParams coin_params{fast_random_context};
|
||||
coin_params.m_subtract_fee_outputs = false;
|
||||
// Set effective feerate up to 10'000'000 sats per kvB (10'000 sat/vB).
|
||||
coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, 10'000'000), 1'000};
|
||||
coin_params.m_long_term_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, 10'000'000), 1'000};
|
||||
coin_params.m_discard_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, 10'000'000), 1'000};
|
||||
coin_params.m_cost_of_change = ConsumeMoney(fuzzed_data_provider);
|
||||
|
||||
coin_params.change_output_size = fuzzed_data_provider.ConsumeIntegralInRange(1, MAX_SCRIPT_SIZE);
|
||||
coin_params.m_change_fee = coin_params.m_effective_feerate.GetFee(coin_params.change_output_size);
|
||||
coin_params.change_spend_size = fuzzed_data_provider.ConsumeIntegralInRange<int>(41, 1000);
|
||||
const auto change_spend_fee{coin_params.m_discard_feerate.GetFee(coin_params.change_spend_size)};
|
||||
coin_params.m_cost_of_change = coin_params.m_change_fee + change_spend_fee;
|
||||
CScript change_out_script = CScript() << std::vector<unsigned char>(coin_params.change_output_size, OP_TRUE);
|
||||
const auto dust{GetDustThreshold(CTxOut{/*nValueIn=*/0, change_out_script}, coin_params.m_discard_feerate)};
|
||||
coin_params.min_viable_change = std::max(change_spend_fee + 1, dust);
|
||||
|
||||
// Create some coins
|
||||
CAmount max_spendable{0};
|
||||
int next_locktime{0};
|
||||
// Too many output groups (>17?) would make it possible to generate UTXO
|
||||
// pool and target combinations that cannot be completely searched by BnB
|
||||
// before running into the attempt limit (see BnB "Exhaust..." test). The
|
||||
// brute force search also gets exponentially more expensive with bigger
|
||||
// UTXO pools.
|
||||
// Choose 1–16 of 16 provides ample fuzzing space.
|
||||
static constexpr unsigned max_output_groups{16};
|
||||
std::vector<OutputGroup> group_pos;
|
||||
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), max_output_groups)
|
||||
{
|
||||
// With maximum m_effective_feerate 10'000 s/vB and n_input_bytes = 20'000 B, input_fee <= MAX_MONEY.
|
||||
const int n_input_bytes{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 20'000)};
|
||||
const CAmount input_fee = coin_params.m_effective_feerate.GetFee(n_input_bytes);
|
||||
// Ensure that each UTXO has at least an effective value of 1 sat
|
||||
const CAmount eff_value{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY + group_pos.size() - max_spendable - max_output_groups)};
|
||||
const CAmount amount{eff_value + input_fee};
|
||||
std::vector<COutput> temp_utxo_pool;
|
||||
|
||||
AddCoin(amount, /*n_input=*/0, n_input_bytes, ++next_locktime, temp_utxo_pool, coin_params.m_effective_feerate);
|
||||
max_spendable += eff_value;
|
||||
|
||||
auto output_group = OutputGroup(coin_params);
|
||||
output_group.Insert(std::make_shared<COutput>(temp_utxo_pool.at(0)), /*ancestors=*/0, /*descendants=*/0);
|
||||
group_pos.push_back(output_group);
|
||||
}
|
||||
size_t num_groups = group_pos.size();
|
||||
assert(num_groups <= max_output_groups);
|
||||
|
||||
// Only choose targets below max_spendable
|
||||
const CAmount target{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, std::max(CAmount{1}, max_spendable - coin_params.m_cost_of_change))};
|
||||
|
||||
// Brute force optimal solution (lowest waste, but cannot be superset of another solution)
|
||||
std::vector<uint32_t> solutions;
|
||||
int best_waste{std::numeric_limits<int>::max()};
|
||||
int best_weight{std::numeric_limits<int>::max()};
|
||||
for (uint32_t pattern = 1; (pattern >> num_groups) == 0; ++pattern) {
|
||||
// BnB does not permit adding more inputs to a solution, i.e. a superset of a solution cannot ever be a solution.
|
||||
// The search pattern guarantees that any superset will only be visited after all its subsets have been traversed.
|
||||
bool is_superset = false;
|
||||
for (uint32_t sol : solutions) {
|
||||
if ((pattern & sol) == sol) {
|
||||
is_superset = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_superset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CAmount subset_amount{0};
|
||||
CAmount subset_waste{0};
|
||||
int subset_weight{0};
|
||||
for (unsigned i = 0; i < num_groups; ++i) {
|
||||
if ((pattern >> i) & 1) {
|
||||
subset_amount += group_pos[i].GetSelectionAmount();
|
||||
subset_waste += group_pos[i].fee - group_pos[i].long_term_fee;
|
||||
subset_weight += group_pos[i].m_weight;
|
||||
}
|
||||
}
|
||||
if (subset_amount >= target && subset_amount <= target + coin_params.m_cost_of_change) {
|
||||
solutions.push_back(pattern);
|
||||
// Add the excess (overselection that gets dropped to fees) to waste score
|
||||
CAmount excess = subset_amount - target;
|
||||
subset_waste += excess;
|
||||
SelectionResult result_bf(target, SelectionAlgorithm::MANUAL);
|
||||
|
||||
for (unsigned i = 0; i < num_groups; ++i) {
|
||||
if ((pattern >> i) & 1) {
|
||||
result_bf.AddInput(group_pos[i]);
|
||||
}
|
||||
}
|
||||
if (subset_waste < best_waste) {
|
||||
best_waste = subset_waste;
|
||||
result_bf.RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
|
||||
assert(result_bf.GetWaste() == best_waste);
|
||||
best_weight = subset_weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int high_max_selection_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(best_weight, std::numeric_limits<int>::max());
|
||||
auto result_bnb = SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, high_max_selection_weight);
|
||||
|
||||
if (!solutions.size() || !result_bnb) {
|
||||
// Either both BnB and Brute Force find a solution or neither does.
|
||||
assert(!result_bnb == !solutions.size());
|
||||
} else {
|
||||
// If brute forcing found a solution with an acceptable weight, BnB must find at least one solution with at most 16 output groups
|
||||
assert(result_bnb);
|
||||
result_bnb->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
|
||||
assert(result_bnb->GetWeight() <= high_max_selection_weight);
|
||||
assert(result_bnb->GetSelectedEffectiveValue() >= target);
|
||||
assert(result_bnb->GetSelectedEffectiveValue() <= target + coin_params.m_cost_of_change);
|
||||
assert(best_waste <= result_bnb->GetWaste());
|
||||
if (result_bnb->GetAlgoCompleted()) {
|
||||
// If BnB exhausted the search space, it must return an optimal solution (tied on waste score)
|
||||
assert(best_waste == result_bnb->GetWaste());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CoinSelectionAlgorithm {
|
||||
BNB,
|
||||
SRD,
|
||||
|
||||
Reference in New Issue
Block a user