Skip to content

Commit a264cd2

Browse files
committed
Add Gutter Guard Selector
Does a random selection limited to three more output groups than largest-first selection would select to fund the transaction. If the limit is exceeded during selection, the output group with the lowest effective value is discarded.
1 parent ffb0216 commit a264cd2

File tree

3 files changed

+89
-1
lines changed

3 files changed

+89
-1
lines changed

src/wallet/coinselection.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,73 @@ util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utx
246246
return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error();
247247
}
248248

249+
util::Result<SelectionResult> SelectCoinsGG(std::vector<OutputGroup>& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng,
250+
int max_weight)
251+
{
252+
SelectionResult result(target_value, SelectionAlgorithm::GG);
253+
std::priority_queue<OutputGroup, std::vector<OutputGroup>, MinOutputGroupComparator> heap;
254+
255+
// Include change for Gutter Guard Selector as we want to avoid making really small change if the selection just
256+
// barely meets the target.
257+
target_value += CHANGE_LOWER + change_fee;
258+
259+
// Use largest-first selection to determine minimum count of necessary output groups
260+
std::sort(utxo_pool.begin(), utxo_pool.end(), descending);
261+
CAmount selected_lf_amount = 0;
262+
size_t lf_group_count = 0;
263+
264+
for (const OutputGroup& group : utxo_pool) {
265+
selected_lf_amount += group.GetSelectionAmount();
266+
lf_group_count++;
267+
if (selected_lf_amount >= target_value) {
268+
break;
269+
}
270+
}
271+
272+
// Gutter Guard selection
273+
size_t gg_group_limit = lf_group_count + 3;
274+
275+
std::vector<size_t> indexes;
276+
indexes.resize(utxo_pool.size());
277+
std::iota(indexes.begin(), indexes.end(), 0);
278+
Shuffle(indexes.begin(), indexes.end(), rng);
279+
280+
CAmount selected_eff_value = 0;
281+
int weight = 0;
282+
bool max_tx_weight_exceeded = false;
283+
for (const size_t i : indexes) {
284+
// Select random additional group
285+
const OutputGroup& group = utxo_pool.at(i);
286+
Assume(group.GetSelectionAmount() > 0);
287+
heap.push(group);
288+
selected_eff_value += group.GetSelectionAmount();
289+
weight += group.m_weight;
290+
291+
if (weight > max_weight) {
292+
// Store error in case no useful result is found
293+
max_tx_weight_exceeded = true;
294+
}
295+
296+
while (!heap.empty() && (weight > max_weight || heap.size() > gg_group_limit)) {
297+
// Drop output group with lowest effective value
298+
const OutputGroup& to_remove_group = heap.top();
299+
selected_eff_value -= to_remove_group.GetSelectionAmount();
300+
weight -= to_remove_group.m_weight;
301+
heap.pop();
302+
}
303+
304+
if (selected_eff_value >= target_value) {
305+
// Success, return heap content
306+
while (!heap.empty()) {
307+
result.AddInput(heap.top());
308+
heap.pop();
309+
}
310+
return result;
311+
}
312+
}
313+
return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error();
314+
}
315+
249316
/** Find a subset of the OutputGroups that is at least as large as, but as close as possible to, the
250317
* target amount; solve subset sum.
251318
* param@[in] groups OutputGroups to choose from, sorted by value in descending order.
@@ -602,6 +669,7 @@ std::string GetAlgorithmName(const SelectionAlgorithm algo)
602669
case SelectionAlgorithm::BNB: return "bnb";
603670
case SelectionAlgorithm::KNAPSACK: return "knapsack";
604671
case SelectionAlgorithm::SRD: return "srd";
672+
case SelectionAlgorithm::GG: return "gg";
605673
case SelectionAlgorithm::MANUAL: return "manual";
606674
// No default case to allow for compiler to warn
607675
}

src/wallet/coinselection.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ enum class SelectionAlgorithm : uint8_t
311311
BNB = 0,
312312
KNAPSACK = 1,
313313
SRD = 2,
314-
MANUAL = 3,
314+
GG = 3,
315+
MANUAL = 4,
315316
};
316317

317318
std::string GetAlgorithmName(const SelectionAlgorithm algo);
@@ -442,6 +443,21 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
442443
util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng,
443444
int max_weight);
444445

446+
/** Select coins by Gutter Guard Selector. Limits the number of unnecessary
447+
* OutputGroups to three. OutputGroups are selected randomly from the eligible
448+
* output groups until the target is satisfied, if the permitted count of
449+
* OutputGroups is exceeded, the output group with the smallest amount is
450+
* dropped from the input set.
451+
*
452+
* @param[in] utxo_pool The positive effective value OutputGroups eligible for selection
453+
* @param[in] target_value The target value to select for
454+
* @param[in] rng The randomness source to shuffle coins
455+
* @param[in] max_weight The maximum allowed weight for a selection result to be valid
456+
* @returns If successful, a valid SelectionResult, otherwise, util::Error
457+
*/
458+
util::Result<SelectionResult> SelectCoinsGG(std::vector<OutputGroup>& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng,
459+
int max_weight);
460+
445461
// Original coin selection algorithm as a fallback
446462
util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
447463
CAmount change_target, FastRandomContext& rng, int max_weight);

src/wallet/spend.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,10 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
709709
results.push_back(*srd_result);
710710
} else append_error(srd_result);
711711

712+
if (auto gg_result{SelectCoinsGG(groups.positive_group, nTargetValue, coin_selection_params.m_change_fee, coin_selection_params.rng_fast, max_inputs_weight)}) {
713+
results.push_back(*gg_result);
714+
} else append_error(gg_result);
715+
712716
if (results.empty()) {
713717
// No solution found, retrieve the first explicit error (if any).
714718
// future: add 'severity level' to errors so the worst one can be retrieved instead of the first one.

0 commit comments

Comments
 (0)