diff --git a/src/wallet/test/fuzz/coinselection.cpp b/src/wallet/test/fuzz/coinselection.cpp index ed77befac2b..cceb17fd8f9 100644 --- a/src/wallet/test/fuzz/coinselection.cpp +++ b/src/wallet/test/fuzz/coinselection.cpp @@ -12,6 +12,7 @@ #include #include +#include #include namespace wallet { @@ -216,8 +217,14 @@ FUZZ_TARGET(coin_grinder_is_optimal) assert(!result_cg); } -FUZZ_TARGET(coinselection) -{ +enum class CoinSelectionAlgorithm { + BNB, + SRD, + KNAPSACK, +}; + +template +void FuzzCoinSelectionAlgorithm(std::span buffer) { SeedRandomStateForTest(SeedRand::ZEROS); FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; std::vector utxo_pool; @@ -249,66 +256,72 @@ FUZZ_TARGET(coinselection) std::vector group_pos; GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/true, group_pos); - std::vector group_all; - GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/false, group_all); - - for (const OutputGroup& group : group_all) { - const CoinEligibilityFilter filter{fuzzed_data_provider.ConsumeIntegral(), fuzzed_data_provider.ConsumeIntegral(), fuzzed_data_provider.ConsumeIntegral()}; - (void)group.EligibleForSpending(filter); - } int max_selection_weight = fuzzed_data_provider.ConsumeIntegralInRange(0, std::numeric_limits::max()); - // Run coinselection algorithms - auto result_bnb = coin_params.m_subtract_fee_outputs ? util::Error{Untranslated("BnB disabled when SFFO is enabled")} : - SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, max_selection_weight); - if (result_bnb) { - assert(result_bnb->GetChange(coin_params.min_viable_change, coin_params.m_change_fee) == 0); - assert(result_bnb->GetSelectedValue() >= target); - assert(result_bnb->GetWeight() <= max_selection_weight); - (void)result_bnb->GetShuffledInputVector(); - (void)result_bnb->GetInputSet(); + std::optional result; + + if constexpr (Algorithm == CoinSelectionAlgorithm::BNB) { + if (!coin_params.m_subtract_fee_outputs) { + auto result_bnb = SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, max_selection_weight); + if (result_bnb) { + result = *result_bnb; + assert(result_bnb->GetChange(coin_params.min_viable_change, coin_params.m_change_fee) == 0); + assert(result_bnb->GetSelectedValue() >= target); + assert(result_bnb->GetWeight() <= max_selection_weight); + (void)result_bnb->GetShuffledInputVector(); + (void)result_bnb->GetInputSet(); + } + } } - auto result_srd = SelectCoinsSRD(group_pos, target, coin_params.m_change_fee, fast_random_context, max_selection_weight); - if (result_srd) { - assert(result_srd->GetSelectedValue() >= target); - assert(result_srd->GetChange(CHANGE_LOWER, coin_params.m_change_fee) > 0); // Demonstrate that SRD creates change of at least CHANGE_LOWER - assert(result_srd->GetWeight() <= max_selection_weight); - result_srd->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee); - (void)result_srd->GetShuffledInputVector(); - (void)result_srd->GetInputSet(); + if constexpr (Algorithm == CoinSelectionAlgorithm::SRD) { + auto result_srd = SelectCoinsSRD(group_pos, target, coin_params.m_change_fee, fast_random_context, max_selection_weight); + if (result_srd) { + result = *result_srd; + assert(result_srd->GetSelectedValue() >= target); + assert(result_srd->GetChange(CHANGE_LOWER, coin_params.m_change_fee) > 0); + assert(result_srd->GetWeight() <= max_selection_weight); + result_srd->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee); + (void)result_srd->GetShuffledInputVector(); + (void)result_srd->GetInputSet(); + } } - CAmount change_target{GenerateChangeTarget(target, coin_params.m_change_fee, fast_random_context)}; - auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context, max_selection_weight); - if (result_knapsack) { - assert(result_knapsack->GetSelectedValue() >= target); - assert(result_knapsack->GetWeight() <= max_selection_weight); - result_knapsack->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee); - (void)result_knapsack->GetShuffledInputVector(); - (void)result_knapsack->GetInputSet(); - } + if constexpr (Algorithm == CoinSelectionAlgorithm::KNAPSACK) { + std::vector group_all; + GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/false, group_all); - // If the total balance is sufficient for the target and we are not using - // effective values, Knapsack should always find a solution (unless the selection exceeded the max tx weight). - if (total_balance >= target && subtract_fee_outputs && !HasErrorMsg(result_knapsack)) { - assert(result_knapsack); + for (const OutputGroup& group : group_all) { + const CoinEligibilityFilter filter{fuzzed_data_provider.ConsumeIntegral(), fuzzed_data_provider.ConsumeIntegral(), fuzzed_data_provider.ConsumeIntegral()}; + (void)group.EligibleForSpending(filter); + } + + CAmount change_target{GenerateChangeTarget(target, coin_params.m_change_fee, fast_random_context)}; + auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context, max_selection_weight); + // If the total balance is sufficient for the target and we are not using + // effective values, Knapsack should always find a solution (unless the selection exceeded the max tx weight). + if (total_balance >= target && subtract_fee_outputs && !HasErrorMsg(result_knapsack)) { + assert(result_knapsack); + } + if (result_knapsack) { + result = *result_knapsack; + assert(result_knapsack->GetSelectedValue() >= target); + assert(result_knapsack->GetWeight() <= max_selection_weight); + result_knapsack->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee); + (void)result_knapsack->GetShuffledInputVector(); + (void)result_knapsack->GetInputSet(); + } } std::vector utxos; - std::vector> results; - results.emplace_back(std::move(result_srd)); - results.emplace_back(std::move(result_knapsack)); - results.emplace_back(std::move(result_bnb)); CAmount new_total_balance{CreateCoins(fuzzed_data_provider, utxos, coin_params, next_locktime)}; if (new_total_balance > 0) { std::set> new_utxo_pool; for (const auto& utxo : utxos) { new_utxo_pool.insert(std::make_shared(utxo)); } - for (auto& result : results) { - if (!result) continue; + if (result) { const auto weight{result->GetWeight()}; result->AddInputs(new_utxo_pool, subtract_fee_outputs); assert(result->GetWeight() > weight); @@ -319,8 +332,7 @@ FUZZ_TARGET(coinselection) CAmount manual_balance{CreateCoins(fuzzed_data_provider, manual_inputs, coin_params, next_locktime)}; if (manual_balance == 0) return; auto manual_selection{ManualSelection(manual_inputs, manual_balance, coin_params.m_subtract_fee_outputs)}; - for (auto& result : results) { - if (!result) continue; + if (result) { const CAmount old_target{result->GetTarget()}; const std::set> input_set{result->GetInputSet()}; const int old_weight{result->GetWeight()}; @@ -331,4 +343,16 @@ FUZZ_TARGET(coinselection) } } +FUZZ_TARGET(coinselection_bnb) { + FuzzCoinSelectionAlgorithm(buffer); +} + +FUZZ_TARGET(coinselection_srd) { + FuzzCoinSelectionAlgorithm(buffer); +} + +FUZZ_TARGET(coinselection_knapsack) { + FuzzCoinSelectionAlgorithm(buffer); +} + } // namespace wallet