Skip to content

Commit 0185939

Browse files
committed
Implement Branch and Bound coin selection in a new file
Create a new file for coin selection logic and implement the BnB algorithm in it.
1 parent f84fed8 commit 0185939

File tree

5 files changed

+184
-0
lines changed

5 files changed

+184
-0
lines changed

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ BITCOIN_CORE_H = \
172172
wallet/wallet.h \
173173
wallet/walletdb.h \
174174
wallet/walletutil.h \
175+
wallet/coinselection.h \
175176
warnings.h \
176177
zmq/zmqabstractnotifier.h \
177178
zmq/zmqconfig.h\
@@ -253,6 +254,7 @@ libbitcoin_wallet_a_SOURCES = \
253254
wallet/wallet.cpp \
254255
wallet/walletdb.cpp \
255256
wallet/walletutil.cpp \
257+
wallet/coinselection.cpp \
256258
$(BITCOIN_CORE_H)
257259

258260
# crypto primitives library

src/wallet/coinselection.cpp

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) 2017 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <wallet/coinselection.h>
6+
#include <util.h>
7+
#include <utilmoneystr.h>
8+
9+
// Descending order comparator
10+
struct {
11+
bool operator()(const CInputCoin& a, const CInputCoin& b) const
12+
{
13+
return a.effective_value > b.effective_value;
14+
}
15+
} descending;
16+
17+
/*
18+
* This is the Branch and Bound Coin Selection algorithm designed by Murch. It searches for an input
19+
* set that can pay for the spending target and does not exceed the spending target by more than the
20+
* cost of creating and spending a change output. The algorithm uses a depth-first search on a binary
21+
* tree. In the binary tree, each node corresponds to the inclusion or the omission of a UTXO. UTXOs
22+
* are sorted by their effective values and the trees is explored deterministically per the inclusion
23+
* branch first. At each node, the algorithm checks whether the selection is within the target range.
24+
* While the selection has not reached the target range, more UTXOs are included. When a selection's
25+
* value exceeds the target range, the complete subtree deriving from this selection can be omitted.
26+
* At that point, the last included UTXO is deselected and the corresponding omission branch explored
27+
* instead. The search ends after the complete tree has been searched or after a limited number of tries.
28+
*
29+
* The search continues to search for better solutions after one solution has been found. The best
30+
* solution is chosen by minimizing the waste metric. The waste metric is defined as the cost to
31+
* spend the current inputs at the given fee rate minus the long term expected cost to spend the
32+
* inputs, plus the amount the selection exceeds the spending target:
33+
*
34+
* waste = selectionTotal - target + inputs × (currentFeeRate - longTermFeeRate)
35+
*
36+
* The algorithm uses two additional optimizations. A lookahead keeps track of the total value of
37+
* the unexplored UTXOs. A subtree is not explored if the lookahead indicates that the target range
38+
* cannot be reached. Further, it is unnecessary to test equivalent combinations. This allows us
39+
* to skip testing the inclusion of UTXOs that match the effective value and waste of an omitted
40+
* predecessor.
41+
*
42+
* The Branch and Bound algorithm is described in detail in Murch's Master Thesis:
43+
* https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
44+
*
45+
* @param const std::vector<CInputCoin>& utxo_pool The set of UTXOs that we are choosing from.
46+
* These UTXOs will be sorted in descending order by effective value and the CInputCoins'
47+
* values are their effective values.
48+
* @param const CAmount& target_value This is the value that we want to select. It is the lower
49+
* bound of the range.
50+
* @param const CAmount& cost_of_change This is the cost of creating and spending a change output.
51+
* This plus target_value is the upper bound of the range.
52+
* @param std::set<CInputCoin>& out_set -> This is an output parameter for the set of CInputCoins
53+
* that have been selected.
54+
* @param CAmount& value_ret -> This is an output parameter for the total value of the CInputCoins
55+
* that were selected.
56+
* @param CAmount not_input_fees -> The fees that need to be paid for the outputs and fixed size
57+
* overhead (version, locktime, marker and flag)
58+
*/
59+
60+
static const size_t TOTAL_TRIES = 100000;
61+
62+
bool SelectCoinsBnB(std::vector<CInputCoin>& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret, CAmount not_input_fees)
63+
{
64+
out_set.clear();
65+
CAmount curr_value = 0;
66+
67+
std::vector<bool> curr_selection; // select the utxo at this index
68+
curr_selection.reserve(utxo_pool.size());
69+
CAmount actual_target = not_input_fees + target_value;
70+
71+
// Calculate curr_available_value
72+
CAmount curr_available_value = 0;
73+
for (const CInputCoin& utxo : utxo_pool) {
74+
// Assert that this utxo is not negative. It should never be negative, effective value calculation should have removed it
75+
assert(utxo.effective_value > 0);
76+
curr_available_value += utxo.effective_value;
77+
}
78+
if (curr_available_value < actual_target) {
79+
return false;
80+
}
81+
82+
// Sort the utxo_pool
83+
std::sort(utxo_pool.begin(), utxo_pool.end(), descending);
84+
85+
CAmount curr_waste = 0;
86+
std::vector<bool> best_selection;
87+
CAmount best_waste = MAX_MONEY;
88+
89+
// Depth First search loop for choosing the UTXOs
90+
for (size_t i = 0; i < TOTAL_TRIES; ++i) {
91+
// Conditions for starting a backtrack
92+
bool backtrack = false;
93+
if (curr_value + curr_available_value < actual_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
94+
curr_value > actual_target + cost_of_change || // Selected value is out of range, go back and try other branch
95+
(curr_waste > best_waste && (utxo_pool.at(0).fee - utxo_pool.at(0).long_term_fee) > 0)) { // Don't select things which we know will be more wasteful if the waste is increasing
96+
backtrack = true;
97+
} else if (curr_value >= actual_target) { // Selected value is within range
98+
curr_waste += (curr_value - actual_target); // This is the excess value which is added to the waste for the below comparison
99+
// Adding another UTXO after this check could bring the waste down if the long term fee is higher than the current fee.
100+
// However we are not going to explore that because this optimization for the waste is only done when we have hit our target
101+
// value. Adding any more UTXOs will be just burning the UTXO; it will go entirely to fees. Thus we aren't going to
102+
// explore any more UTXOs to avoid burning money like that.
103+
if (curr_waste <= best_waste) {
104+
best_selection = curr_selection;
105+
best_selection.resize(utxo_pool.size());
106+
best_waste = curr_waste;
107+
}
108+
curr_waste -= (curr_value - actual_target); // Remove the excess value as we will be selecting different coins now
109+
backtrack = true;
110+
}
111+
112+
// Backtracking, moving backwards
113+
if (backtrack) {
114+
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
115+
while (!curr_selection.empty() && !curr_selection.back()) {
116+
curr_selection.pop_back();
117+
curr_available_value += utxo_pool.at(curr_selection.size()).effective_value;
118+
};
119+
120+
if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched
121+
break;
122+
}
123+
124+
// Output was included on previous iterations, try excluding now.
125+
curr_selection.back() = false;
126+
CInputCoin& utxo = utxo_pool.at(curr_selection.size() - 1);
127+
curr_value -= utxo.effective_value;
128+
curr_waste -= utxo.fee - utxo.long_term_fee;
129+
} else { // Moving forwards, continuing down this branch
130+
CInputCoin& utxo = utxo_pool.at(curr_selection.size());
131+
132+
// Remove this utxo from the curr_available_value utxo amount
133+
curr_available_value -= utxo.effective_value;
134+
135+
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. Since the ratio of fee to
136+
// long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same.
137+
if (!curr_selection.empty() && !curr_selection.back() &&
138+
utxo.effective_value == utxo_pool.at(curr_selection.size() - 1).effective_value &&
139+
utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) {
140+
curr_selection.push_back(false);
141+
} else {
142+
// Inclusion branch first (Largest First Exploration)
143+
curr_selection.push_back(true);
144+
curr_value += utxo.effective_value;
145+
curr_waste += utxo.fee - utxo.long_term_fee;
146+
}
147+
}
148+
}
149+
150+
// Check for solution
151+
if (best_selection.empty()) {
152+
return false;
153+
}
154+
155+
// Set output set
156+
value_ret = 0;
157+
for (size_t i = 0; i < best_selection.size(); ++i) {
158+
if (best_selection.at(i)) {
159+
out_set.insert(utxo_pool.at(i));
160+
value_ret += utxo_pool.at(i).txout.nValue;
161+
}
162+
}
163+
164+
return true;
165+
}

src/wallet/coinselection.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2017 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_COINSELECTION_H
6+
#define BITCOIN_COINSELECTION_H
7+
8+
#include <amount.h>
9+
#include <primitives/transaction.h>
10+
#include <random.h>
11+
#include <wallet/wallet.h>
12+
13+
bool SelectCoinsBnB(std::vector<CInputCoin>& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret, CAmount not_input_fees);
14+
15+
#endif // BITCOIN_COINSELECTION_H

src/wallet/wallet.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <checkpoints.h>
99
#include <chain.h>
1010
#include <wallet/coincontrol.h>
11+
#include <wallet/coinselection.h>
1112
#include <consensus/consensus.h>
1213
#include <consensus/validation.h>
1314
#include <fs.h>

src/wallet/wallet.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ class CInputCoin {
509509

510510
outpoint = COutPoint(walletTx->GetHash(), i);
511511
txout = walletTx->tx->vout[i];
512+
effective_value = txout.nValue;
512513
}
513514

514515
COutPoint outpoint;

0 commit comments

Comments
 (0)