diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 391e1209323175..4f9a0d1dabd9da 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -246,6 +246,79 @@ util::Result SelectCoinsSRD(const std::vector& utx return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error(); } +util::Result SelectCoinsGG(std::vector& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng, + int max_weight) +{ + SelectionResult result(target_value, SelectionAlgorithm::PF); + std::priority_queue, MinOutputGroupComparator> heap; + + // Include change for PF as we want to avoid making really small change if the selection just + // barely meets the target. + target_value += CHANGE_LOWER + change_fee; + + std::sort(utxo_pool.begin(), utxo_pool.end(), descending); + CAmount selected_lf_amount = 0; + size_t lf_group_count = 0; + + for (const OutputGroup& group : utxo_pool) { + selected_lf_amount += group.GetSelectionAmount(); + lf_group_count++; + if (selected_lf_amount >= target_value) { + break; + } + } + + size_t gg_group_limit = lf_group_count + 3; + + std::vector indexes; + indexes.resize(utxo_pool.size()); + std::iota(indexes.begin(), indexes.end(), 0); + Shuffle(indexes.begin(), indexes.end(), rng); + + CAmount selected_eff_value = 0; + int weight = 0; + bool max_tx_weight_exceeded = false; + for (const size_t i : indexes) { + const OutputGroup& group = utxo_pool.at(i); + Assume(group.GetSelectionAmount() > 0); + + // Add group to selection + heap.push(group); + selected_eff_value += group.GetSelectionAmount(); + weight += group.m_weight; + + // If the selection weight exceeds the maximum allowed size, remove the least valuable inputs until we + // are below max weight. + if (weight > max_weight) { + max_tx_weight_exceeded = true; // mark it in case we don't find any useful result. + do { + const OutputGroup& to_remove_group = heap.top(); + selected_eff_value -= to_remove_group.GetSelectionAmount(); + weight -= to_remove_group.m_weight; + heap.pop(); + } while (!heap.empty() && weight > max_weight); + } + + while (!heap.empty() && heap.size() > gg_group_limit) { + const OutputGroup& to_remove_group = heap.top(); + selected_eff_value -= to_remove_group.GetSelectionAmount(); + weight -= to_remove_group.m_weight; + heap.pop(); + } + + // Now check if we are above the target + if (selected_eff_value >= target_value) { + // Result found, add it. + while (!heap.empty()) { + result.AddInput(heap.top()); + heap.pop(); + } + return result; + } + } + return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error(); +} + /** Find a subset of the OutputGroups that is at least as large as, but as close as possible to, the * target amount; solve subset sum. * param@[in] groups OutputGroups to choose from, sorted by value in descending order. @@ -602,6 +675,7 @@ std::string GetAlgorithmName(const SelectionAlgorithm algo) case SelectionAlgorithm::BNB: return "bnb"; case SelectionAlgorithm::KNAPSACK: return "knapsack"; case SelectionAlgorithm::SRD: return "srd"; + case SelectionAlgorithm::PF: return "pf"; case SelectionAlgorithm::MANUAL: return "manual"; // No default case to allow for compiler to warn } diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 20b2461c041896..5915d04a7171ea 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -311,7 +311,8 @@ enum class SelectionAlgorithm : uint8_t BNB = 0, KNAPSACK = 1, SRD = 2, - MANUAL = 3, + GG = 3, + MANUAL = 4, }; std::string GetAlgorithmName(const SelectionAlgorithm algo); @@ -442,6 +443,21 @@ util::Result SelectCoinsBnB(std::vector& utxo_pool util::Result SelectCoinsSRD(const std::vector& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng, int max_weight); +/** Select coins by Gutter Guard Selector. Limits the number of unnecessary + * OutputGroups to three. OutputGroups are selected randomly from the eligible + * output groups until the target is satisfied, if the permitted count of + * OutputGroups is exceeded, the output group with the smallest amount is + * dropped from the input set. + * + * @param[in] utxo_pool The positive effective value OutputGroups eligible for selection + * @param[in] target_value The target value to select for + * @param[in] rng The randomness source to shuffle coins + * @param[in] max_weight The maximum allowed weight for a selection result to be valid + * @returns If successful, a valid SelectionResult, otherwise, util::Error + */ +util::Result SelectCoinsGG(std::vector& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng, + int max_weight); + // Original coin selection algorithm as a fallback util::Result KnapsackSolver(std::vector& groups, const CAmount& nTargetValue, CAmount change_target, FastRandomContext& rng, int max_weight); diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 35583642a5ae9e..450da47de0f8bf 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -709,6 +709,10 @@ util::Result ChooseSelectionResult(interfaces::Chain& chain, co results.push_back(*srd_result); } else append_error(srd_result); + if (auto gg_result{SelectCoinsGG(groups.positive_group, nTargetValue, coin_selection_params.m_change_fee, coin_selection_params.rng_fast, max_inputs_weight)}) { + results.push_back(*gg_result); + } else append_error(gg_result); + if (results.empty()) { // No solution found, retrieve the first explicit error (if any). // future: add 'severity level' to errors so the worst one can be retrieved instead of the first one.