From 62642cfe7618ba3228e91dec071a777cac868ff7 Mon Sep 17 00:00:00 2001 From: Matthew Mitchell Date: Wed, 22 Nov 2023 19:41:07 +0000 Subject: [PATCH] Add optimal coin selection constructor as reasonable default --- coinlib/lib/src/tx/coin_selection.dart | 51 +++++++++++++++++++++++- coinlib/test/tx/coin_selection_test.dart | 46 ++++++++++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/coinlib/lib/src/tx/coin_selection.dart b/coinlib/lib/src/tx/coin_selection.dart index e97f24d..e5d1874 100644 --- a/coinlib/lib/src/tx/coin_selection.dart +++ b/coinlib/lib/src/tx/coin_selection.dart @@ -123,6 +123,48 @@ class CoinSelection { } + /// A useful default coin selection algorithm. + /// Currently this will first select candidates at random until the required + /// input amount is reached. If the resulting transaction is too large or not + /// enough funds have been reached it will fall back to adding the largest + /// input values first. + factory CoinSelection.optimal({ + int version = Transaction.currentVersion, + required Iterable candidates, + required Iterable recipients, + required Program changeProgram, + required BigInt feePerKb, + required BigInt minFee, + required BigInt minChange, + int locktime = 0, + }) { + + final randomSelection = CoinSelection.random( + version: version, + candidates: candidates, + recipients: recipients, + changeProgram: changeProgram, + feePerKb: feePerKb, + minFee: minFee, + minChange: minChange, + locktime: locktime, + ); + + return randomSelection.tooLarge || !randomSelection.enoughFunds + ? CoinSelection.largestFirst( + version: version, + candidates: candidates, + recipients: recipients, + changeProgram: changeProgram, + feePerKb: feePerKb, + minFee: minFee, + minChange: minChange, + locktime: locktime, + ) + : randomSelection; + + } + /// A simple selection algorithm that selects inputs from the [candidates] /// in the order that they are given until the required amount has been /// reached. If there are not enough coins, all shall be selected and @@ -130,6 +172,9 @@ class CoinSelection { /// If [randomise] is set to true, the order of inputs shall be randomised /// after being selected. This is useful for candidates that are not already /// randomised as it may avoid giving clues to the algorithm being used. + /// The algorithm will only take upto 6800 candidates by default to avoid + /// taking too long and due to size limitations. This can be changed with + /// [maxCandidates]. factory CoinSelection.inOrderUntilEnough({ int version = Transaction.currentVersion, required Iterable candidates, @@ -138,8 +183,9 @@ class CoinSelection { required BigInt feePerKb, required BigInt minFee, required BigInt minChange, - bool randomise = false, int locktime = 0, + bool randomise = false, + int maxCandidates = 6800, }) { CoinSelection trySelection(Iterable selected) @@ -154,7 +200,8 @@ class CoinSelection { locktime: locktime, ); - final list = candidates.toList(); + // Restrict number of candidates due to size limitation and for efficiency + final list = candidates.take(maxCandidates).toList(); CoinSelection selection = trySelection([]); for (int i = 0; i < list.length; i++) { diff --git a/coinlib/test/tx/coin_selection_test.dart b/coinlib/test/tx/coin_selection_test.dart index 3251e5c..65fb140 100644 --- a/coinlib/test/tx/coin_selection_test.dart +++ b/coinlib/test/tx/coin_selection_test.dart @@ -273,7 +273,6 @@ void main() { }); void expectSelectedValues(CoinSelection selection, List values) { - print(selection.selected.map((candidate) => candidate.value.toInt())); expect( selection.selected.map((candidate) => candidate.value.toInt()), unorderedEquals(values), @@ -364,6 +363,51 @@ void main() { }); + test(".optimal()", () { + + CoinSelection getOptimal(List candidates, int outValue) + => CoinSelection.optimal( + version: 1234, + candidates: candidates.map((value) => candidateForValue(value)), + recipients: [outputForValue(outValue)], + changeProgram: changeProgram, + feePerKb: feePerKb, minFee: minFee, minChange: minChange, + locktime: 0xabcd1234, + ); + + // Defaults to random where possible + { + final selected = getOptimal(candidates, coin~/2).selected; + expect(selected.length, 1); + expect(selected[0].value.toInt(), isIn(candidates)); + } + + // Fallback to largestFirst where needed + // Create a long list of small inputs that would lead to a too large + // transaction with only a few larger inputs able to satisfy the transaction. + // Create lots of inputs to reduce probability of randomly selecting + // larger inputs. + { + final selection = getOptimal( + [ + ...List.filled(1000, coin*100), + ...List.filled(100000, coin), + ], coin*100000, + ); + expect(selection.tooLarge, false); + expect(selection.enoughFunds, true); + expect(selection.version, 1234); + expect(selection.locktime, 0xabcd1234); + expect( + selection.selected.where( + (candidate) => candidate.value.toInt() == coin*100, + ).length, + isNonZero, + ); + } + + }); + }); }