/tmp/bitcoin/src/wallet/test/coinselector_tests.cpp
Line | Count | Source |
1 | | // Copyright (c) 2017-present 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 <consensus/amount.h> |
6 | | #include <node/context.h> |
7 | | #include <policy/policy.h> |
8 | | #include <primitives/transaction.h> |
9 | | #include <random.h> |
10 | | #include <test/util/common.h> |
11 | | #include <test/util/setup_common.h> |
12 | | #include <util/translation.h> |
13 | | #include <wallet/coincontrol.h> |
14 | | #include <wallet/coinselection.h> |
15 | | #include <wallet/spend.h> |
16 | | #include <wallet/test/util.h> |
17 | | #include <wallet/test/wallet_test_fixture.h> |
18 | | #include <wallet/wallet.h> |
19 | | |
20 | | #include <algorithm> |
21 | | #include <boost/test/unit_test.hpp> |
22 | | #include <random> |
23 | | |
24 | | namespace wallet { |
25 | | BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup) |
26 | | |
27 | | // how many times to run all the tests to have a chance to catch errors that only show up with particular random shuffles |
28 | 808 | #define RUN_TESTS 100 |
29 | | |
30 | | // some tests fail 1% of the time due to bad luck. |
31 | | // we repeat those tests this many times and only complain if all iterations of the test fail |
32 | 1.20k | #define RANDOM_REPEATS 5 |
33 | | |
34 | | static const CoinEligibilityFilter filter_standard(1, 6, 0); |
35 | | static const CoinEligibilityFilter filter_confirmed(1, 1, 0); |
36 | | static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); |
37 | | static int nextLockTime = 0; |
38 | | |
39 | | static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result) |
40 | 51 | { |
41 | 51 | CMutableTransaction tx; |
42 | 51 | tx.vout.resize(nInput + 1); |
43 | 51 | tx.vout[nInput].nValue = nValue; |
44 | 51 | tx.nLockTime = nextLockTime++; // so all transactions get different hashes |
45 | 51 | COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/ 0); |
46 | 51 | OutputGroup group; |
47 | 51 | group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*cluster_count=*/ 0); |
48 | 51 | result.AddInput(group); |
49 | 51 | } |
50 | | |
51 | | static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result, CAmount fee, CAmount long_term_fee) |
52 | 28 | { |
53 | 28 | CMutableTransaction tx; |
54 | 28 | tx.vout.resize(nInput + 1); |
55 | 28 | tx.vout[nInput].nValue = nValue; |
56 | 28 | tx.nLockTime = nextLockTime++; // so all transactions get different hashes |
57 | 28 | std::shared_ptr<COutput> coin = std::make_shared<COutput>(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/148, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fee); |
58 | 28 | OutputGroup group; |
59 | 28 | group.Insert(coin, /*ancestors=*/ 0, /*cluster_count=*/ 0); |
60 | 28 | coin->long_term_fee = long_term_fee; // group.Insert() will modify long_term_fee, so we need to set it afterwards |
61 | 28 | result.AddInput(group); |
62 | 28 | } |
63 | | |
64 | | static void add_coin(CoinsResult& available_coins, CWallet& wallet, const CAmount& nValue, CFeeRate feerate = CFeeRate(0), int nAge = 6*24, bool fIsFromMe = false, int nInput =0, bool spendable = false, int custom_size = 0) |
65 | 116k | { |
66 | 116k | CMutableTransaction tx; |
67 | 116k | tx.nLockTime = nextLockTime++; // so all transactions get different hashes |
68 | 116k | tx.vout.resize(nInput + 1); |
69 | 116k | tx.vout[nInput].nValue = nValue; |
70 | 116k | if (spendable) { |
71 | 6.09k | tx.vout[nInput].scriptPubKey = GetScriptForDestination(*Assert(wallet.GetNewDestination(OutputType::BECH32, ""))); |
72 | 6.09k | } |
73 | 116k | Txid txid = tx.GetHash(); |
74 | | |
75 | 116k | LOCK(wallet.cs_wallet); |
76 | 116k | auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{})); |
77 | 116k | assert(ret.second); |
78 | 116k | CWalletTx& wtx = (*ret.first).second; |
79 | 116k | const auto& txout = wtx.tx->vout.at(nInput); |
80 | 116k | available_coins.Add(OutputType::BECH32, {COutPoint(wtx.GetHash(), nInput), txout, nAge, custom_size == 0 ? CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr) : custom_size, /*solvable=*/true, /*safe=*/true, wtx.GetTxTime(), fIsFromMe, feerate}); |
81 | 116k | } |
82 | | |
83 | | // Helpers |
84 | | std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, |
85 | | CAmount change_target, FastRandomContext& rng) |
86 | 5.60k | { |
87 | 5.60k | auto res{KnapsackSolver(groups, nTargetValue, change_target, rng, MAX_STANDARD_TX_WEIGHT)}; |
88 | 5.60k | return res ? std::optional<SelectionResult>(*res) : std::nullopt; |
89 | 5.60k | } |
90 | | |
91 | | std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change) |
92 | 3 | { |
93 | 3 | auto res{SelectCoinsBnB(utxo_pool, selection_target, cost_of_change, MAX_STANDARD_TX_WEIGHT)}; |
94 | 3 | return res ? std::optional<SelectionResult>(*res) : std::nullopt; |
95 | 3 | } |
96 | | |
97 | | /** Check if SelectionResult a is equivalent to SelectionResult b. |
98 | | * Equivalent means same input values, but maybe different inputs (i.e. same value, different prevout) */ |
99 | | static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b) |
100 | 8 | { |
101 | 8 | std::vector<CAmount> a_amts; |
102 | 8 | std::vector<CAmount> b_amts; |
103 | 51 | for (const auto& coin : a.GetInputSet()) { |
104 | 51 | a_amts.push_back(coin->txout.nValue); |
105 | 51 | } |
106 | 51 | for (const auto& coin : b.GetInputSet()) { |
107 | 51 | b_amts.push_back(coin->txout.nValue); |
108 | 51 | } |
109 | 8 | std::sort(a_amts.begin(), a_amts.end()); |
110 | 8 | std::sort(b_amts.begin(), b_amts.end()); |
111 | | |
112 | 8 | std::pair<std::vector<CAmount>::iterator, std::vector<CAmount>::iterator> ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin()); |
113 | 8 | return ret.first == a_amts.end() && ret.second == b_amts.end(); |
114 | 8 | } |
115 | | |
116 | | /** Check if this selection is equal to another one. Equal means same inputs (i.e same value and prevout) */ |
117 | | static bool EqualResult(const SelectionResult& a, const SelectionResult& b) |
118 | 1.10k | { |
119 | 1.10k | std::pair<OutputSet::iterator, OutputSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(), |
120 | 1.14k | [](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) { |
121 | 1.14k | return a->outpoint == b->outpoint; |
122 | 1.14k | }); |
123 | 1.10k | return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end(); |
124 | 1.10k | } |
125 | | |
126 | | inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& available_coins, bool subtract_fee_outputs = false) |
127 | 2.20k | { |
128 | 2.20k | static std::vector<OutputGroup> static_groups; |
129 | 2.20k | static_groups.clear(); |
130 | 225k | for (auto& coin : available_coins) { |
131 | 225k | static_groups.emplace_back(); |
132 | 225k | OutputGroup& group = static_groups.back(); |
133 | 225k | group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*cluster_count=*/ 0); |
134 | 225k | group.m_subtract_fee_outputs = subtract_fee_outputs; |
135 | 225k | } |
136 | 2.20k | return static_groups; |
137 | 2.20k | } |
138 | | |
139 | | inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& available_coins, CWallet& wallet, const CoinEligibilityFilter& filter) |
140 | 3.40k | { |
141 | 3.40k | FastRandomContext rand{}; |
142 | 3.40k | CoinSelectionParams coin_selection_params{ |
143 | 3.40k | rand, |
144 | 3.40k | /*change_output_size=*/ 0, |
145 | 3.40k | /*change_spend_size=*/ 0, |
146 | 3.40k | /*min_change_target=*/ CENT, |
147 | 3.40k | /*effective_feerate=*/ CFeeRate(0), |
148 | 3.40k | /*long_term_feerate=*/ CFeeRate(0), |
149 | 3.40k | /*discard_feerate=*/ CFeeRate(0), |
150 | 3.40k | /*tx_noinputs_size=*/ 0, |
151 | 3.40k | /*avoid_partial=*/ false, |
152 | 3.40k | }; |
153 | 3.40k | static OutputGroupTypeMap static_groups; |
154 | 3.40k | static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter]; |
155 | 3.40k | return static_groups.all_groups.mixed_group; |
156 | 3.40k | } |
157 | | |
158 | | static std::unique_ptr<CWallet> NewWallet(const node::NodeContext& m_node, const std::string& wallet_name = "") |
159 | 23 | { |
160 | 23 | std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), wallet_name, CreateMockableWalletDatabase()); |
161 | 23 | LOCK(wallet->cs_wallet); |
162 | 23 | wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
163 | 23 | wallet->SetupDescriptorScriptPubKeyMans(); |
164 | 23 | return wallet; |
165 | 23 | } |
166 | | |
167 | | // Branch and bound coin selection tests |
168 | | BOOST_AUTO_TEST_CASE(bnb_search_test) |
169 | 1 | { |
170 | 1 | FastRandomContext rand{}; |
171 | | // Setup |
172 | 1 | std::vector<COutput> utxo_pool; |
173 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB); |
174 | | |
175 | | //////////////////// |
176 | | // Behavior tests // |
177 | | //////////////////// |
178 | | |
179 | | // Make sure that effective value is working in AttemptSelection when BnB is used |
180 | 1 | CoinSelectionParams coin_selection_params_bnb{ |
181 | 1 | rand, |
182 | 1 | /*change_output_size=*/ 31, |
183 | 1 | /*change_spend_size=*/ 68, |
184 | 1 | /*min_change_target=*/ 0, |
185 | 1 | /*effective_feerate=*/ CFeeRate(3000), |
186 | 1 | /*long_term_feerate=*/ CFeeRate(1000), |
187 | 1 | /*discard_feerate=*/ CFeeRate(1000), |
188 | 1 | /*tx_noinputs_size=*/ 0, |
189 | 1 | /*avoid_partial=*/ false, |
190 | 1 | }; |
191 | 1 | coin_selection_params_bnb.m_change_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_output_size); |
192 | 1 | coin_selection_params_bnb.m_cost_of_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size) + coin_selection_params_bnb.m_change_fee; |
193 | 1 | coin_selection_params_bnb.min_viable_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size); |
194 | | |
195 | 1 | { |
196 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
197 | | |
198 | 1 | CoinsResult available_coins; |
199 | | |
200 | 1 | add_coin(available_coins, *wallet, 1, coin_selection_params_bnb.m_effective_feerate); |
201 | 1 | available_coins.All().at(0).input_bytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail |
202 | 1 | BOOST_CHECK(!SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change)); |
203 | | |
204 | | // Test fees subtracted from output: |
205 | 1 | available_coins.Clear(); |
206 | 1 | add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate); |
207 | 1 | available_coins.All().at(0).input_bytes = 40; |
208 | 1 | const auto result9 = SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change); |
209 | 1 | BOOST_CHECK(result9); |
210 | 1 | BOOST_CHECK_EQUAL(result9->GetSelectedValue(), 1 * CENT); |
211 | 1 | } |
212 | | |
213 | 1 | { |
214 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
215 | | |
216 | 1 | CoinsResult available_coins; |
217 | | |
218 | 1 | coin_selection_params_bnb.m_effective_feerate = CFeeRate(0); |
219 | 1 | add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
220 | 1 | add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
221 | 1 | add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
222 | 1 | CCoinControl coin_control; |
223 | 1 | coin_control.m_allow_other_inputs = true; |
224 | 1 | COutput select_coin = available_coins.All().at(0); |
225 | 1 | coin_control.Select(select_coin.outpoint); |
226 | 1 | CoinsResult selected_input; |
227 | 1 | selected_input.Add(OutputType::BECH32, select_coin); |
228 | 1 | available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint}); |
229 | | |
230 | 1 | LOCK(wallet->cs_wallet); |
231 | 1 | const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb); |
232 | 1 | BOOST_CHECK(result10); |
233 | 1 | } |
234 | 1 | { |
235 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
236 | 1 | LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it |
237 | | |
238 | 1 | CoinsResult available_coins; |
239 | | |
240 | | // pre selected coin should be selected even if disadvantageous |
241 | 1 | coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000); |
242 | 1 | coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000); |
243 | | |
244 | | // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount |
245 | 1 | CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*virtual_bytes=*/68); // bech32 input size (default test output type) |
246 | 1 | add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
247 | 1 | add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
248 | 1 | add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
249 | | |
250 | 1 | expected_result.Clear(); |
251 | 1 | add_coin(9 * CENT + input_fee, 2, expected_result); |
252 | 1 | add_coin(1 * CENT + input_fee, 2, expected_result); |
253 | 1 | CCoinControl coin_control; |
254 | 1 | coin_control.m_allow_other_inputs = true; |
255 | 1 | COutput select_coin = available_coins.All().at(1); // pre select 9 coin |
256 | 1 | coin_control.Select(select_coin.outpoint); |
257 | 1 | CoinsResult selected_input; |
258 | 1 | selected_input.Add(OutputType::BECH32, select_coin); |
259 | 1 | available_coins.Erase({(++available_coins.coins[OutputType::BECH32].begin())->outpoint}); |
260 | 1 | const auto result13 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb); |
261 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *result13)); |
262 | 1 | } |
263 | | |
264 | 1 | { |
265 | | // Test bnb max weight exceeded |
266 | | // Inputs set [10, 9, 8, 5, 3, 1], Selection Target = 16 and coin 5 exceeding the max weight. |
267 | | |
268 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
269 | | |
270 | 1 | CoinsResult available_coins; |
271 | 1 | add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
272 | 1 | add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
273 | 1 | add_coin(available_coins, *wallet, 8 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
274 | 1 | add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true, /*custom_size=*/MAX_STANDARD_TX_WEIGHT); |
275 | 1 | add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
276 | 1 | add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
277 | | |
278 | 1 | CAmount selection_target = 16 * CENT; |
279 | 1 | const auto& no_res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs=*/true), |
280 | 1 | selection_target, /*cost_of_change=*/0, MAX_STANDARD_TX_WEIGHT); |
281 | 1 | BOOST_REQUIRE(!no_res); |
282 | 1 | BOOST_CHECK(util::ErrorString(no_res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); |
283 | | |
284 | | // Now add same coin value with a good size and check that it gets selected |
285 | 1 | add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); |
286 | 1 | const auto& res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs=*/true), selection_target, /*cost_of_change=*/0); |
287 | | |
288 | 1 | expected_result.Clear(); |
289 | 1 | add_coin(8 * CENT, 2, expected_result); |
290 | 1 | add_coin(5 * CENT, 2, expected_result); |
291 | 1 | add_coin(3 * CENT, 2, expected_result); |
292 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *res)); |
293 | 1 | } |
294 | 1 | } |
295 | | |
296 | | BOOST_AUTO_TEST_CASE(bnb_sffo_restriction) |
297 | 1 | { |
298 | | // Verify the coin selection process does not produce a BnB solution when SFFO is enabled. |
299 | | // This is currently problematic because it could require a change output. And BnB is specialized on changeless solutions. |
300 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
301 | 1 | WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(300, uint256{})); // set a high block so internal UTXOs are selectable |
302 | | |
303 | 1 | FastRandomContext rand{}; |
304 | 1 | CoinSelectionParams params{ |
305 | 1 | rand, |
306 | 1 | /*change_output_size=*/ 31, // unused value, p2wpkh output size (wallet default change type) |
307 | 1 | /*change_spend_size=*/ 68, // unused value, p2wpkh input size (high-r signature) |
308 | 1 | /*min_change_target=*/ 0, // dummy, set later |
309 | 1 | /*effective_feerate=*/ CFeeRate(3000), |
310 | 1 | /*long_term_feerate=*/ CFeeRate(1000), |
311 | 1 | /*discard_feerate=*/ CFeeRate(1000), |
312 | 1 | /*tx_noinputs_size=*/ 0, |
313 | 1 | /*avoid_partial=*/ false, |
314 | 1 | }; |
315 | 1 | params.m_subtract_fee_outputs = true; |
316 | 1 | params.m_change_fee = params.m_effective_feerate.GetFee(params.change_output_size); |
317 | 1 | params.m_cost_of_change = params.m_discard_feerate.GetFee(params.change_spend_size) + params.m_change_fee; |
318 | 1 | params.m_min_change_target = params.m_cost_of_change + 1; |
319 | | // Add spendable coin at the BnB selection upper bound |
320 | 1 | CoinsResult available_coins; |
321 | 1 | add_coin(available_coins, *wallet, COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true); |
322 | 1 | add_coin(available_coins, *wallet, 0.5 * COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true); |
323 | 1 | add_coin(available_coins, *wallet, 0.5 * COIN, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true); |
324 | | // Knapsack will only find a changeless solution on an exact match to the satoshi, SRD doesn’t look for changeless |
325 | | // If BnB were run, it would produce a single input solution with the best waste score |
326 | 1 | auto result = WITH_LOCK(wallet->cs_wallet, return SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, COIN, /*coin_control=*/{}, params)); |
327 | 1 | BOOST_CHECK(result.has_value()); |
328 | 1 | BOOST_CHECK_NE(result->GetAlgo(), SelectionAlgorithm::BNB); |
329 | 1 | BOOST_CHECK(result->GetInputSet().size() == 2); |
330 | | // We have only considered BnB, SRD, and Knapsack. Test needs to be reevaluated if new algo is added |
331 | 1 | BOOST_CHECK(result->GetAlgo() == SelectionAlgorithm::SRD || result->GetAlgo() == SelectionAlgorithm::KNAPSACK); |
332 | 1 | } |
333 | | |
334 | | BOOST_AUTO_TEST_CASE(knapsack_solver_test) |
335 | 1 | { |
336 | 1 | FastRandomContext rand{}; |
337 | 5.60k | const auto temp1{[&rand](std::vector<OutputGroup>& g, const CAmount& v, CAmount c) { return KnapsackSolver(g, v, c, rand); }}; |
338 | 1 | const auto KnapsackSolver{temp1}; |
339 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
340 | | |
341 | 1 | CoinsResult available_coins; |
342 | | |
343 | | // test multiple times to allow for differences in the shuffle order |
344 | 101 | for (int i = 0; i < RUN_TESTS; i++) |
345 | 100 | { |
346 | 100 | available_coins.Clear(); |
347 | | |
348 | | // with an empty wallet we can't even pay one cent |
349 | 100 | BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1 * CENT, CENT)); |
350 | | |
351 | 100 | add_coin(available_coins, *wallet, 1*CENT, CFeeRate(0), 4); // add a new 1 cent coin |
352 | | |
353 | | // with a new 1 cent coin, we still can't find a mature 1 cent |
354 | 100 | BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1 * CENT, CENT)); |
355 | | |
356 | | // but we can find a new 1 cent |
357 | 100 | const auto result1 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); |
358 | 100 | BOOST_CHECK(result1); |
359 | 100 | BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT); |
360 | | |
361 | 100 | add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin |
362 | | |
363 | | // we can't make 3 cents of mature coins |
364 | 100 | BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 3 * CENT, CENT)); |
365 | | |
366 | | // we can make 3 cents of new coins |
367 | 100 | const auto result2 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 3 * CENT, CENT); |
368 | 100 | BOOST_CHECK(result2); |
369 | 100 | BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 3 * CENT); |
370 | | |
371 | 100 | add_coin(available_coins, *wallet, 5*CENT); // add a mature 5 cent coin, |
372 | 100 | add_coin(available_coins, *wallet, 10*CENT, CFeeRate(0), 3, true); // a new 10 cent coin sent from one of our own addresses |
373 | 100 | add_coin(available_coins, *wallet, 20*CENT); // and a mature 20 cent coin |
374 | | |
375 | | // now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27. total = 38 |
376 | | |
377 | | // we can't make 38 cents only if we disallow new coins: |
378 | 100 | BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 38 * CENT, CENT)); |
379 | | // we can't even make 37 cents if we don't allow new coins even if they're from us |
380 | 100 | BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard_extra), 38 * CENT, CENT)); |
381 | | // but we can make 37 cents if we accept new coins from ourself |
382 | 100 | const auto result3 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 37 * CENT, CENT); |
383 | 100 | BOOST_CHECK(result3); |
384 | 100 | BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT); |
385 | | // and we can make 38 cents if we accept all new coins |
386 | 100 | const auto result4 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 38 * CENT, CENT); |
387 | 100 | BOOST_CHECK(result4); |
388 | 100 | BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 38 * CENT); |
389 | | |
390 | | // try making 34 cents from 1,2,5,10,20 - we can't do it exactly |
391 | 100 | const auto result5 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 34 * CENT, CENT); |
392 | 100 | BOOST_CHECK(result5); |
393 | 100 | BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 35 * CENT); // but 35 cents is closest |
394 | 100 | BOOST_CHECK_EQUAL(result5->GetInputSet().size(), 3U); // the best should be 20+10+5. it's incredibly unlikely the 1 or 2 got included (but possible) |
395 | | |
396 | | // when we try making 7 cents, the smaller coins (1,2,5) are enough. We should see just 2+5 |
397 | 100 | const auto result6 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 7 * CENT, CENT); |
398 | 100 | BOOST_CHECK(result6); |
399 | 100 | BOOST_CHECK_EQUAL(result6->GetSelectedValue(), 7 * CENT); |
400 | 100 | BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U); |
401 | | |
402 | | // when we try making 8 cents, the smaller coins (1,2,5) are exactly enough. |
403 | 100 | const auto result7 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 8 * CENT, CENT); |
404 | 100 | BOOST_CHECK(result7); |
405 | 100 | BOOST_CHECK(result7->GetSelectedValue() == 8 * CENT); |
406 | 100 | BOOST_CHECK_EQUAL(result7->GetInputSet().size(), 3U); |
407 | | |
408 | | // when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10) |
409 | 100 | const auto result8 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 9 * CENT, CENT); |
410 | 100 | BOOST_CHECK(result8); |
411 | 100 | BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 10 * CENT); |
412 | 100 | BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U); |
413 | | |
414 | | // now clear out the wallet and start again to test choosing between subsets of smaller coins and the next biggest coin |
415 | 100 | available_coins.Clear(); |
416 | | |
417 | 100 | add_coin(available_coins, *wallet, 6*CENT); |
418 | 100 | add_coin(available_coins, *wallet, 7*CENT); |
419 | 100 | add_coin(available_coins, *wallet, 8*CENT); |
420 | 100 | add_coin(available_coins, *wallet, 20*CENT); |
421 | 100 | add_coin(available_coins, *wallet, 30*CENT); // now we have 6+7+8+20+30 = 71 cents total |
422 | | |
423 | | // check that we have 71 and not 72 |
424 | 100 | const auto result9 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 71 * CENT, CENT); |
425 | 100 | BOOST_CHECK(result9); |
426 | 100 | BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 72 * CENT, CENT)); |
427 | | |
428 | | // now try making 16 cents. the best smaller coins can do is 6+7+8 = 21; not as good at the next biggest coin, 20 |
429 | 100 | const auto result10 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT); |
430 | 100 | BOOST_CHECK(result10); |
431 | 100 | BOOST_CHECK_EQUAL(result10->GetSelectedValue(), 20 * CENT); // we should get 20 in one coin |
432 | 100 | BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U); |
433 | | |
434 | 100 | add_coin(available_coins, *wallet, 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total |
435 | | |
436 | | // now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, better than the next biggest coin, 20 |
437 | 100 | const auto result11 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT); |
438 | 100 | BOOST_CHECK(result11); |
439 | 100 | BOOST_CHECK_EQUAL(result11->GetSelectedValue(), 18 * CENT); // we should get 18 in 3 coins |
440 | 100 | BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U); |
441 | | |
442 | 100 | add_coin(available_coins, *wallet, 18*CENT); // now we have 5+6+7+8+18+20+30 |
443 | | |
444 | | // and now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, the same as the next biggest coin, 18 |
445 | 100 | const auto result12 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT); |
446 | 100 | BOOST_CHECK(result12); |
447 | 100 | BOOST_CHECK_EQUAL(result12->GetSelectedValue(), 18 * CENT); // we should get 18 in 1 coin |
448 | 100 | BOOST_CHECK_EQUAL(result12->GetInputSet().size(), 1U); // because in the event of a tie, the biggest coin wins |
449 | | |
450 | | // now try making 11 cents. we should get 5+6 |
451 | 100 | const auto result13 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 11 * CENT, CENT); |
452 | 100 | BOOST_CHECK(result13); |
453 | 100 | BOOST_CHECK_EQUAL(result13->GetSelectedValue(), 11 * CENT); |
454 | 100 | BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U); |
455 | | |
456 | | // check that the smallest bigger coin is used |
457 | 100 | add_coin(available_coins, *wallet, 1*COIN); |
458 | 100 | add_coin(available_coins, *wallet, 2*COIN); |
459 | 100 | add_coin(available_coins, *wallet, 3*COIN); |
460 | 100 | add_coin(available_coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents |
461 | 100 | const auto result14 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 95 * CENT, CENT); |
462 | 100 | BOOST_CHECK(result14); |
463 | 100 | BOOST_CHECK_EQUAL(result14->GetSelectedValue(), 1 * COIN); // we should get 1 BTC in 1 coin |
464 | 100 | BOOST_CHECK_EQUAL(result14->GetInputSet().size(), 1U); |
465 | | |
466 | 100 | const auto result15 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 195 * CENT, CENT); |
467 | 100 | BOOST_CHECK(result15); |
468 | 100 | BOOST_CHECK_EQUAL(result15->GetSelectedValue(), 2 * COIN); // we should get 2 BTC in 1 coin |
469 | 100 | BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U); |
470 | | |
471 | | // empty the wallet and start again, now with fractions of a cent, to test small change avoidance |
472 | | |
473 | 100 | available_coins.Clear(); |
474 | 100 | add_coin(available_coins, *wallet, CENT * 1 / 10); |
475 | 100 | add_coin(available_coins, *wallet, CENT * 2 / 10); |
476 | 100 | add_coin(available_coins, *wallet, CENT * 3 / 10); |
477 | 100 | add_coin(available_coins, *wallet, CENT * 4 / 10); |
478 | 100 | add_coin(available_coins, *wallet, CENT * 5 / 10); |
479 | | |
480 | | // try making 1 * CENT from the 1.5 * CENT |
481 | | // we'll get change smaller than CENT whatever happens, so can expect CENT exactly |
482 | 100 | const auto result16 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT); |
483 | 100 | BOOST_CHECK(result16); |
484 | 100 | BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT); |
485 | | |
486 | | // but if we add a bigger coin, small change is avoided |
487 | 100 | add_coin(available_coins, *wallet, 1111*CENT); |
488 | | |
489 | | // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 |
490 | 100 | const auto result17 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); |
491 | 100 | BOOST_CHECK(result17); |
492 | 100 | BOOST_CHECK_EQUAL(result17->GetSelectedValue(), 1 * CENT); // we should get the exact amount |
493 | | |
494 | | // if we add more small coins: |
495 | 100 | add_coin(available_coins, *wallet, CENT * 6 / 10); |
496 | 100 | add_coin(available_coins, *wallet, CENT * 7 / 10); |
497 | | |
498 | | // and try again to make 1.0 * CENT |
499 | 100 | const auto result18 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); |
500 | 100 | BOOST_CHECK(result18); |
501 | 100 | BOOST_CHECK_EQUAL(result18->GetSelectedValue(), 1 * CENT); // we should get the exact amount |
502 | | |
503 | | // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf) |
504 | | // they tried to consolidate 10 50k coins into one 500k coin, and ended up with 50k in change |
505 | 100 | available_coins.Clear(); |
506 | 2.10k | for (int j = 0; j < 20; j++) |
507 | 2.00k | add_coin(available_coins, *wallet, 50000 * COIN); |
508 | | |
509 | 100 | const auto result19 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 500000 * COIN, CENT); |
510 | 100 | BOOST_CHECK(result19); |
511 | 100 | BOOST_CHECK_EQUAL(result19->GetSelectedValue(), 500000 * COIN); // we should get the exact amount |
512 | 100 | BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins |
513 | | |
514 | | // if there's not enough in the smaller coins to make at least 1 * CENT change (0.5+0.6+0.7 < 1.0+1.0), |
515 | | // we need to try finding an exact subset anyway |
516 | | |
517 | | // sometimes it will fail, and so we use the next biggest coin: |
518 | 100 | available_coins.Clear(); |
519 | 100 | add_coin(available_coins, *wallet, CENT * 5 / 10); |
520 | 100 | add_coin(available_coins, *wallet, CENT * 6 / 10); |
521 | 100 | add_coin(available_coins, *wallet, CENT * 7 / 10); |
522 | 100 | add_coin(available_coins, *wallet, 1111 * CENT); |
523 | 100 | const auto result20 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); |
524 | 100 | BOOST_CHECK(result20); |
525 | 100 | BOOST_CHECK_EQUAL(result20->GetSelectedValue(), 1111 * CENT); // we get the bigger coin |
526 | 100 | BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U); |
527 | | |
528 | | // but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = 1.0) |
529 | 100 | available_coins.Clear(); |
530 | 100 | add_coin(available_coins, *wallet, CENT * 4 / 10); |
531 | 100 | add_coin(available_coins, *wallet, CENT * 6 / 10); |
532 | 100 | add_coin(available_coins, *wallet, CENT * 8 / 10); |
533 | 100 | add_coin(available_coins, *wallet, 1111 * CENT); |
534 | 100 | const auto result21 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT); |
535 | 100 | BOOST_CHECK(result21); |
536 | 100 | BOOST_CHECK_EQUAL(result21->GetSelectedValue(), CENT); // we should get the exact amount |
537 | 100 | BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6 |
538 | | |
539 | | // test avoiding small change |
540 | 100 | available_coins.Clear(); |
541 | 100 | add_coin(available_coins, *wallet, CENT * 5 / 100); |
542 | 100 | add_coin(available_coins, *wallet, CENT * 1); |
543 | 100 | add_coin(available_coins, *wallet, CENT * 100); |
544 | | |
545 | | // trying to make 100.01 from these three coins |
546 | 100 | const auto result22 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 10001 / 100, CENT); |
547 | 100 | BOOST_CHECK(result22); |
548 | 100 | BOOST_CHECK_EQUAL(result22->GetSelectedValue(), CENT * 10105 / 100); // we should get all coins |
549 | 100 | BOOST_CHECK_EQUAL(result22->GetInputSet().size(), 3U); |
550 | | |
551 | | // but if we try to make 99.9, we should take the bigger of the two small coins to avoid small change |
552 | 100 | const auto result23 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 9990 / 100, CENT); |
553 | 100 | BOOST_CHECK(result23); |
554 | 100 | BOOST_CHECK_EQUAL(result23->GetSelectedValue(), 101 * CENT); |
555 | 100 | BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U); |
556 | 100 | } |
557 | | |
558 | | // test with many inputs |
559 | 6 | for (CAmount amt=1500; amt < COIN; amt*=10) { |
560 | 5 | available_coins.Clear(); |
561 | | // Create 676 inputs (= (old MAX_STANDARD_TX_SIZE == 100000) / 148 bytes per input) |
562 | 3.38k | for (uint16_t j = 0; j < 676; j++) |
563 | 3.38k | add_coin(available_coins, *wallet, amt); |
564 | | |
565 | | // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. |
566 | 505 | for (int i = 0; i < RUN_TESTS; i++) { |
567 | 500 | const auto result24 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 2000, CENT); |
568 | 500 | BOOST_CHECK(result24); |
569 | | |
570 | 500 | if (amt - 2000 < CENT) { |
571 | | // needs more than one input: |
572 | 300 | uint16_t returnSize = std::ceil((2000.0 + CENT)/amt); |
573 | 300 | CAmount returnValue = amt * returnSize; |
574 | 300 | BOOST_CHECK_EQUAL(result24->GetSelectedValue(), returnValue); |
575 | 300 | BOOST_CHECK_EQUAL(result24->GetInputSet().size(), returnSize); |
576 | 300 | } else { |
577 | | // one input is sufficient: |
578 | 200 | BOOST_CHECK_EQUAL(result24->GetSelectedValue(), amt); |
579 | 200 | BOOST_CHECK_EQUAL(result24->GetInputSet().size(), 1U); |
580 | 200 | } |
581 | 500 | } |
582 | 5 | } |
583 | | |
584 | | // test randomness |
585 | 1 | { |
586 | 1 | available_coins.Clear(); |
587 | 101 | for (int i2 = 0; i2 < 100; i2++) |
588 | 100 | add_coin(available_coins, *wallet, COIN); |
589 | | |
590 | | // Again, we only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. |
591 | 101 | for (int i = 0; i < RUN_TESTS; i++) { |
592 | | // picking 50 from 100 coins doesn't depend on the shuffle, |
593 | | // but does depend on randomness in the stochastic approximation code |
594 | 100 | const auto result25 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT); |
595 | 100 | BOOST_CHECK(result25); |
596 | 100 | const auto result26 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT); |
597 | 100 | BOOST_CHECK(result26); |
598 | 100 | BOOST_CHECK(!EqualResult(*result25, *result26)); |
599 | | |
600 | 100 | int fails = 0; |
601 | 600 | for (int j = 0; j < RANDOM_REPEATS; j++) |
602 | 500 | { |
603 | | // Test that the KnapsackSolver selects randomly from equivalent coins (same value and same input size). |
604 | | // When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice |
605 | | // which will cause it to fail. |
606 | | // To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail |
607 | 500 | const auto result27 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT); |
608 | 500 | BOOST_CHECK(result27); |
609 | 500 | const auto result28 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT); |
610 | 500 | BOOST_CHECK(result28); |
611 | 500 | if (EqualResult(*result27, *result28)) |
612 | 5 | fails++; |
613 | 500 | } |
614 | 100 | BOOST_CHECK_NE(fails, RANDOM_REPEATS); |
615 | 100 | } |
616 | | |
617 | | // add 75 cents in small change. not enough to make 90 cents, |
618 | | // then try making 90 cents. there are multiple competing "smallest bigger" coins, |
619 | | // one of which should be picked at random |
620 | 1 | add_coin(available_coins, *wallet, 5 * CENT); |
621 | 1 | add_coin(available_coins, *wallet, 10 * CENT); |
622 | 1 | add_coin(available_coins, *wallet, 15 * CENT); |
623 | 1 | add_coin(available_coins, *wallet, 20 * CENT); |
624 | 1 | add_coin(available_coins, *wallet, 25 * CENT); |
625 | | |
626 | 101 | for (int i = 0; i < RUN_TESTS; i++) { |
627 | 100 | int fails = 0; |
628 | 600 | for (int j = 0; j < RANDOM_REPEATS; j++) |
629 | 500 | { |
630 | 500 | const auto result29 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT); |
631 | 500 | BOOST_CHECK(result29); |
632 | 500 | const auto result30 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT); |
633 | 500 | BOOST_CHECK(result30); |
634 | 500 | if (EqualResult(*result29, *result30)) |
635 | 7 | fails++; |
636 | 500 | } |
637 | 100 | BOOST_CHECK_NE(fails, RANDOM_REPEATS); |
638 | 100 | } |
639 | 1 | } |
640 | 1 | } |
641 | | |
642 | | BOOST_AUTO_TEST_CASE(ApproximateBestSubset) |
643 | 1 | { |
644 | 1 | FastRandomContext rand{}; |
645 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
646 | | |
647 | 1 | CoinsResult available_coins; |
648 | | |
649 | | // Test vValue sort order |
650 | 1.00k | for (int i = 0; i < 1000; i++) |
651 | 1.00k | add_coin(available_coins, *wallet, 1000 * COIN); |
652 | 1 | add_coin(available_coins, *wallet, 3 * COIN); |
653 | | |
654 | 1 | const auto result = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1003 * COIN, CENT, rand); |
655 | 1 | BOOST_CHECK(result); |
656 | 1 | BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN); |
657 | 1 | BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U); |
658 | 1 | } |
659 | | |
660 | | // Tests that with the ideal conditions, the coin selector will always be able to find a solution that can pay the target value |
661 | | BOOST_AUTO_TEST_CASE(SelectCoins_test) |
662 | 1 | { |
663 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
664 | 1 | LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it |
665 | | |
666 | | // Random generator stuff |
667 | 1 | std::default_random_engine generator; |
668 | 1 | std::exponential_distribution<double> distribution (100); |
669 | 1 | FastRandomContext rand; |
670 | | |
671 | | // Run this test 100 times |
672 | 101 | for (int i = 0; i < 100; ++i) |
673 | 100 | { |
674 | 100 | CoinsResult available_coins; |
675 | 100 | CAmount balance{0}; |
676 | | |
677 | | // Make a wallet with 1000 exponentially distributed random inputs |
678 | 100k | for (int j = 0; j < 1000; ++j) |
679 | 100k | { |
680 | 100k | CAmount val = distribution(generator)*10000000; |
681 | 100k | add_coin(available_coins, *wallet, val); |
682 | 100k | balance += val; |
683 | 100k | } |
684 | | |
685 | | // Generate a random fee rate in the range of 100 - 400 |
686 | 100 | CFeeRate rate(rand.randrange(300) + 100); |
687 | | |
688 | | // Generate a random target value between 1000 and wallet balance |
689 | 100 | CAmount target = rand.randrange(balance - 1000) + 1000; |
690 | | |
691 | | // Perform selection |
692 | 100 | CoinSelectionParams cs_params{ |
693 | 100 | rand, |
694 | 100 | /*change_output_size=*/ 34, |
695 | 100 | /*change_spend_size=*/ 148, |
696 | 100 | /*min_change_target=*/ CENT, |
697 | 100 | /*effective_feerate=*/ CFeeRate(0), |
698 | 100 | /*long_term_feerate=*/ CFeeRate(0), |
699 | 100 | /*discard_feerate=*/ CFeeRate(0), |
700 | 100 | /*tx_noinputs_size=*/ 0, |
701 | 100 | /*avoid_partial=*/ false, |
702 | 100 | }; |
703 | 100 | cs_params.m_cost_of_change = 1; |
704 | 100 | cs_params.min_viable_change = 1; |
705 | 100 | CCoinControl cc; |
706 | 100 | const auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, target, cc, cs_params); |
707 | 100 | BOOST_CHECK(result); |
708 | 100 | BOOST_CHECK_GE(result->GetSelectedValue(), target); |
709 | 100 | } |
710 | 1 | } |
711 | | |
712 | | BOOST_AUTO_TEST_CASE(waste_test) |
713 | 1 | { |
714 | 1 | const CAmount fee{100}; |
715 | 1 | const CAmount min_viable_change{300}; |
716 | 1 | const CAmount change_cost{125}; |
717 | 1 | const CAmount change_fee{30}; |
718 | 1 | const CAmount fee_diff{40}; |
719 | 1 | const CAmount in_amt{3 * COIN}; |
720 | 1 | const CAmount target{2 * COIN}; |
721 | 1 | const CAmount excess{80}; |
722 | 1 | const CAmount exact_target{in_amt - fee * 2}; // Maximum spendable amount after fees: no change, no excess |
723 | | |
724 | | // In the following, we test that the waste is calculated correctly in various scenarios. |
725 | | // Usually, RecalculateWaste would compute change_fee and change_cost on basis of the |
726 | | // change output type, current feerate, and discard_feerate, but we use fixed values |
727 | | // across this test to make the test easier to understand. |
728 | 1 | { |
729 | | // Waste with change is the change cost and difference between fee and long term fee |
730 | 1 | SelectionResult selection1{target, SelectionAlgorithm::MANUAL}; |
731 | 1 | add_coin(1 * COIN, 1, selection1, /*fee=*/fee, /*long_term_fee=*/fee - fee_diff); |
732 | 1 | add_coin(2 * COIN, 2, selection1, fee, fee - fee_diff); |
733 | 1 | selection1.RecalculateWaste(min_viable_change, change_cost, change_fee); |
734 | 1 | BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, selection1.GetWaste()); |
735 | | |
736 | | // Waste will be greater when fee is greater, but long term fee is the same |
737 | 1 | SelectionResult selection2{target, SelectionAlgorithm::MANUAL}; |
738 | 1 | add_coin(1 * COIN, 1, selection2, fee * 2, fee - fee_diff); |
739 | 1 | add_coin(2 * COIN, 2, selection2, fee * 2, fee - fee_diff); |
740 | 1 | selection2.RecalculateWaste(min_viable_change, change_cost, change_fee); |
741 | 1 | BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste()); |
742 | | |
743 | | // Waste with change is the change cost and difference between fee and long term fee |
744 | | // With long term fee greater than fee, waste should be less than when long term fee is less than fee |
745 | 1 | SelectionResult selection3{target, SelectionAlgorithm::MANUAL}; |
746 | 1 | add_coin(1 * COIN, 1, selection3, fee, fee + fee_diff); |
747 | 1 | add_coin(2 * COIN, 2, selection3, fee, fee + fee_diff); |
748 | 1 | selection3.RecalculateWaste(min_viable_change, change_cost, change_fee); |
749 | 1 | BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, selection3.GetWaste()); |
750 | 1 | BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste()); |
751 | 1 | } |
752 | | |
753 | 1 | { |
754 | | // Waste without change is the excess and difference between fee and long term fee |
755 | 1 | SelectionResult selection_nochange1{exact_target - excess, SelectionAlgorithm::MANUAL}; |
756 | 1 | add_coin(1 * COIN, 1, selection_nochange1, fee, fee - fee_diff); |
757 | 1 | add_coin(2 * COIN, 2, selection_nochange1, fee, fee - fee_diff); |
758 | 1 | selection_nochange1.RecalculateWaste(min_viable_change, change_cost, change_fee); |
759 | 1 | BOOST_CHECK_EQUAL(fee_diff * 2 + excess, selection_nochange1.GetWaste()); |
760 | | |
761 | | // Waste without change is the excess and difference between fee and long term fee |
762 | | // With long term fee greater than fee, waste should be less than when long term fee is less than fee |
763 | 1 | SelectionResult selection_nochange2{exact_target - excess, SelectionAlgorithm::MANUAL}; |
764 | 1 | add_coin(1 * COIN, 1, selection_nochange2, fee, fee + fee_diff); |
765 | 1 | add_coin(2 * COIN, 2, selection_nochange2, fee, fee + fee_diff); |
766 | 1 | selection_nochange2.RecalculateWaste(min_viable_change, change_cost, change_fee); |
767 | 1 | BOOST_CHECK_EQUAL(fee_diff * -2 + excess, selection_nochange2.GetWaste()); |
768 | 1 | BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste()); |
769 | 1 | } |
770 | | |
771 | 1 | { |
772 | | // Waste with change and fee == long term fee is just cost of change |
773 | 1 | SelectionResult selection{target, SelectionAlgorithm::MANUAL}; |
774 | 1 | add_coin(1 * COIN, 1, selection, fee, fee); |
775 | 1 | add_coin(2 * COIN, 2, selection, fee, fee); |
776 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
777 | 1 | BOOST_CHECK_EQUAL(change_cost, selection.GetWaste()); |
778 | 1 | } |
779 | | |
780 | 1 | { |
781 | | // Waste without change and fee == long term fee is just the excess |
782 | 1 | SelectionResult selection{exact_target - excess, SelectionAlgorithm::MANUAL}; |
783 | 1 | add_coin(1 * COIN, 1, selection, fee, fee); |
784 | 1 | add_coin(2 * COIN, 2, selection, fee, fee); |
785 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
786 | 1 | BOOST_CHECK_EQUAL(excess, selection.GetWaste()); |
787 | 1 | } |
788 | | |
789 | 1 | { |
790 | | // Waste is 0 when fee == long_term_fee, no change, and no excess |
791 | 1 | SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL}; |
792 | 1 | add_coin(1 * COIN, 1, selection, fee, fee); |
793 | 1 | add_coin(2 * COIN, 2, selection, fee, fee); |
794 | 1 | selection.RecalculateWaste(min_viable_change, change_cost , change_fee); |
795 | 1 | BOOST_CHECK_EQUAL(0, selection.GetWaste()); |
796 | 1 | } |
797 | | |
798 | 1 | { |
799 | | // Waste is 0 when (fee - long_term_fee) == (-cost_of_change), and no excess |
800 | 1 | SelectionResult selection{target, SelectionAlgorithm::MANUAL}; |
801 | 1 | add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); |
802 | 1 | add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); |
803 | 1 | selection.RecalculateWaste(min_viable_change, /*change_cost=*/fee_diff * 2, change_fee); |
804 | 1 | BOOST_CHECK_EQUAL(0, selection.GetWaste()); |
805 | 1 | } |
806 | | |
807 | 1 | { |
808 | | // Waste is 0 when (fee - long_term_fee) == (-excess), no change cost |
809 | 1 | const CAmount new_target{exact_target - /*excess=*/fee_diff * 2}; |
810 | 1 | SelectionResult selection{new_target, SelectionAlgorithm::MANUAL}; |
811 | 1 | add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); |
812 | 1 | add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); |
813 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
814 | 1 | BOOST_CHECK_EQUAL(0, selection.GetWaste()); |
815 | 1 | } |
816 | | |
817 | 1 | { |
818 | | // Negative waste when the long term fee is greater than the current fee and the selected value == target |
819 | 1 | SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL}; |
820 | 1 | const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff)) |
821 | 1 | add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); |
822 | 1 | add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); |
823 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
824 | 1 | BOOST_CHECK_EQUAL(target_waste1, selection.GetWaste()); |
825 | 1 | } |
826 | | |
827 | 1 | { |
828 | | // Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee)) |
829 | 1 | SelectionResult selection{target, SelectionAlgorithm::MANUAL}; |
830 | 1 | const CAmount large_fee_diff{90}; |
831 | 1 | const CAmount target_waste2{-2 * large_fee_diff + change_cost}; |
832 | | // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost |
833 | | // = (2 * 100) - (2 * (100 + 90)) + 125 |
834 | | // = 200 - 380 + 125 = -55 |
835 | 1 | assert(target_waste2 == -55); |
836 | 1 | add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff); |
837 | 1 | add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff); |
838 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
839 | 1 | BOOST_CHECK_EQUAL(target_waste2, selection.GetWaste()); |
840 | 1 | } |
841 | 1 | } |
842 | | |
843 | | |
844 | | BOOST_AUTO_TEST_CASE(bump_fee_test) |
845 | 1 | { |
846 | 1 | const CAmount fee{100}; |
847 | 1 | const CAmount min_viable_change{200}; |
848 | 1 | const CAmount change_cost{125}; |
849 | 1 | const CAmount change_fee{35}; |
850 | 1 | const CAmount fee_diff{40}; |
851 | 1 | const CAmount target{2 * COIN}; |
852 | | |
853 | 1 | { |
854 | 1 | SelectionResult selection{target, SelectionAlgorithm::MANUAL}; |
855 | 1 | add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff); |
856 | 1 | add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); |
857 | 1 | const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector(); |
858 | | |
859 | 3 | for (size_t i = 0; i < inputs.size(); ++i) { |
860 | 2 | inputs[i]->ApplyBumpFee(20*(i+1)); |
861 | 2 | } |
862 | | |
863 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
864 | 1 | CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60; |
865 | 1 | BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); |
866 | | |
867 | 1 | selection.SetBumpFeeDiscount(30); |
868 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
869 | 1 | expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60 - /*group_discount=*/30; |
870 | 1 | BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); |
871 | 1 | } |
872 | | |
873 | 1 | { |
874 | | // Test with changeless transaction |
875 | | // |
876 | | // Bump fees and excess both contribute fully to the waste score, |
877 | | // therefore, a bump fee group discount will not change the waste |
878 | | // score as long as we do not create change in both instances. |
879 | 1 | CAmount changeless_target = 3 * COIN - 2 * fee - 100; |
880 | 1 | SelectionResult selection{changeless_target, SelectionAlgorithm::MANUAL}; |
881 | 1 | add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff); |
882 | 1 | add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); |
883 | 1 | const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector(); |
884 | | |
885 | 3 | for (size_t i = 0; i < inputs.size(); ++i) { |
886 | 2 | inputs[i]->ApplyBumpFee(20*(i+1)); |
887 | 2 | } |
888 | | |
889 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
890 | 1 | CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40; |
891 | 1 | BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); |
892 | | |
893 | 1 | selection.SetBumpFeeDiscount(30); |
894 | 1 | selection.RecalculateWaste(min_viable_change, change_cost, change_fee); |
895 | 1 | expected_waste = fee_diff * -2 + /*bump_fees=*/60 - /*group_discount=*/30 + /*excess = 100 - bump_fees + group_discount*/70; |
896 | 1 | BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); |
897 | 1 | } |
898 | 1 | } |
899 | | |
900 | | BOOST_AUTO_TEST_CASE(effective_value_test) |
901 | 1 | { |
902 | 1 | const int input_bytes = 148; |
903 | 1 | const CFeeRate feerate(1000); |
904 | 1 | const CAmount nValue = 10000; |
905 | 1 | const int nInput = 0; |
906 | | |
907 | 1 | CMutableTransaction tx; |
908 | 1 | tx.vout.resize(1); |
909 | 1 | tx.vout[nInput].nValue = nValue; |
910 | | |
911 | | // standard case, pass feerate in constructor |
912 | 1 | COutput output1(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, feerate); |
913 | 1 | const CAmount expected_ev1 = 9852; // 10000 - 148 |
914 | 1 | BOOST_CHECK_EQUAL(output1.GetEffectiveValue(), expected_ev1); |
915 | | |
916 | | // input bytes unknown (input_bytes = -1), pass feerate in constructor |
917 | 1 | COutput output2(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/ false, feerate); |
918 | 1 | BOOST_CHECK_EQUAL(output2.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1 |
919 | | |
920 | | // negative effective value, pass feerate in constructor |
921 | 1 | COutput output3(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, CFeeRate(100000)); |
922 | 1 | const CAmount expected_ev3 = -4800; // 10000 - 14800 |
923 | 1 | BOOST_CHECK_EQUAL(output3.GetEffectiveValue(), expected_ev3); |
924 | | |
925 | | // standard case, pass fees in constructor |
926 | 1 | const CAmount fees = 148; |
927 | 1 | COutput output4(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fees); |
928 | 1 | BOOST_CHECK_EQUAL(output4.GetEffectiveValue(), expected_ev1); |
929 | | |
930 | | // input bytes unknown (input_bytes = -1), pass fees in constructor |
931 | 1 | COutput output5(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/0); |
932 | 1 | BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1 |
933 | 1 | } |
934 | | |
935 | | static util::Result<SelectionResult> CoinGrinder(const CAmount& target, |
936 | | const CoinSelectionParams& cs_params, |
937 | | const node::NodeContext& m_node, |
938 | | int max_selection_weight, |
939 | | std::function<CoinsResult(CWallet&)> coin_setup) |
940 | 9 | { |
941 | 9 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
942 | 9 | CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors |
943 | 9 | Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups; |
944 | 9 | return CoinGrinder(group.positive_group, target, cs_params.m_min_change_target, max_selection_weight); |
945 | 9 | } |
946 | | |
947 | | BOOST_AUTO_TEST_CASE(coin_grinder_tests) |
948 | 1 | { |
949 | | // Test Coin Grinder: |
950 | | // 1) Insufficient funds, select all provided coins and fail. |
951 | | // 2) Exceeded max weight, coin selection always surpasses the max allowed weight. |
952 | | // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not) |
953 | | // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO |
954 | | // 5) Test finding a solution in a UTXO pool with mixed weights |
955 | | // 6) Test that the lightest solution among many clones is found |
956 | | // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead |
957 | | |
958 | 1 | FastRandomContext rand; |
959 | 1 | CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag. |
960 | 1 | rand, |
961 | 1 | /*change_output_size=*/34, |
962 | 1 | /*change_spend_size=*/68, |
963 | 1 | /*min_change_target=*/CENT, |
964 | 1 | /*effective_feerate=*/CFeeRate(5000), |
965 | 1 | /*long_term_feerate=*/CFeeRate(2000), |
966 | 1 | /*discard_feerate=*/CFeeRate(1000), |
967 | 1 | /*tx_noinputs_size=*/10 + 34, // static header size + output size |
968 | 1 | /*avoid_partial=*/false, |
969 | 1 | }; |
970 | | |
971 | 1 | { |
972 | | // ######################################################### |
973 | | // 1) Insufficient funds, select all provided coins and fail |
974 | | // ######################################################### |
975 | 1 | CAmount target = 49.5L * COIN; |
976 | 1 | int max_selection_weight = 10'000; // high enough to not fail for this reason. |
977 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
978 | 1 | CoinsResult available_coins; |
979 | 11 | for (int j = 0; j < 10; ++j) { |
980 | 10 | add_coin(available_coins, wallet, CAmount(1 * COIN)); |
981 | 10 | add_coin(available_coins, wallet, CAmount(2 * COIN)); |
982 | 10 | } |
983 | 1 | return available_coins; |
984 | 1 | }); |
985 | 1 | BOOST_CHECK(!res); |
986 | 1 | BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds" |
987 | 1 | } |
988 | | |
989 | 1 | { |
990 | | // ########################### |
991 | | // 2) Test max weight exceeded |
992 | | // ########################### |
993 | 1 | CAmount target = 29.5L * COIN; |
994 | 1 | int max_selection_weight = 3000; |
995 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
996 | 1 | CoinsResult available_coins; |
997 | 11 | for (int j = 0; j < 10; ++j) { |
998 | 10 | add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true); |
999 | 10 | add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true); |
1000 | 10 | } |
1001 | 1 | return available_coins; |
1002 | 1 | }); |
1003 | 1 | BOOST_CHECK(!res); |
1004 | 1 | BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); |
1005 | 1 | } |
1006 | | |
1007 | 1 | { |
1008 | | // ############################################################################################################### |
1009 | | // 3) Test that the lowest-weight solution is found when some combinations would exceed the allowed weight |
1010 | | // ################################################################################################################ |
1011 | 1 | CAmount target = 25.33L * COIN; |
1012 | 1 | int max_selection_weight = 10'000; // WU |
1013 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1014 | 1 | CoinsResult available_coins; |
1015 | 61 | for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU |
1016 | 60 | add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(5000), 144, false, 0, true); |
1017 | 60 | } |
1018 | 11 | for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU |
1019 | 10 | add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true); |
1020 | 10 | } |
1021 | 1 | return available_coins; |
1022 | 1 | }); |
1023 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); |
1024 | 11 | for (int i = 0; i < 10; ++i) { |
1025 | 10 | add_coin(2 * COIN, i, expected_result); |
1026 | 10 | } |
1027 | 18 | for (int j = 0; j < 17; ++j) { |
1028 | 17 | add_coin(0.33 * COIN, j + 10, expected_result); |
1029 | 17 | } |
1030 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *res)); |
1031 | | // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. |
1032 | 1 | size_t expected_attempts = 37; |
1033 | 1 | BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); |
1034 | 1 | } |
1035 | | |
1036 | 1 | { |
1037 | | // ################################################################################################################# |
1038 | | // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO |
1039 | | // ################################################################################################################# |
1040 | 1 | CAmount target = 1.9L * COIN; |
1041 | 1 | int max_selection_weight = 400'000; // WU |
1042 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1043 | 1 | CoinsResult available_coins; |
1044 | 1 | add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 148); |
1045 | 1 | add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68); |
1046 | 1 | add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68); |
1047 | 1 | return available_coins; |
1048 | 1 | }); |
1049 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); |
1050 | 1 | add_coin(1 * COIN, 1, expected_result); |
1051 | 1 | add_coin(1 * COIN, 2, expected_result); |
1052 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *res)); |
1053 | | // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. |
1054 | 1 | size_t expected_attempts = 3; |
1055 | 1 | BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); |
1056 | 1 | } |
1057 | | |
1058 | 1 | { |
1059 | | // ############################################################################################################### |
1060 | | // 5) Test finding a solution in a UTXO pool with mixed weights |
1061 | | // ################################################################################################################ |
1062 | 1 | CAmount target = 30L * COIN; |
1063 | 1 | int max_selection_weight = 400'000; // WU |
1064 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1065 | 1 | CoinsResult available_coins; |
1066 | 6 | for (int j = 0; j < 5; ++j) { |
1067 | | // Add heavy coins {3, 6, 9, 12, 15} |
1068 | 5 | add_coin(available_coins, wallet, CAmount((3 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 350); |
1069 | | // Add medium coins {2, 5, 8, 11, 14} |
1070 | 5 | add_coin(available_coins, wallet, CAmount((2 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 250); |
1071 | | // Add light coins {1, 4, 7, 10, 13} |
1072 | 5 | add_coin(available_coins, wallet, CAmount((1 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 150); |
1073 | 5 | } |
1074 | 1 | return available_coins; |
1075 | 1 | }); |
1076 | 1 | BOOST_CHECK(res); |
1077 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); |
1078 | 1 | add_coin(14 * COIN, 1, expected_result); |
1079 | 1 | add_coin(13 * COIN, 2, expected_result); |
1080 | 1 | add_coin(4 * COIN, 3, expected_result); |
1081 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *res)); |
1082 | | // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. |
1083 | 1 | size_t expected_attempts = 92; |
1084 | 1 | BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); |
1085 | 1 | } |
1086 | | |
1087 | 1 | { |
1088 | | // ################################################################################################################# |
1089 | | // 6) Test that the lightest solution among many clones is found |
1090 | | // ################################################################################################################# |
1091 | 1 | CAmount target = 9.9L * COIN; |
1092 | 1 | int max_selection_weight = 400'000; // WU |
1093 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1094 | 1 | CoinsResult available_coins; |
1095 | | // Expected Result: 4 + 3 + 2 + 1 = 10 BTC at 400 vB |
1096 | 1 | add_coin(available_coins, wallet, CAmount(4 * COIN), CFeeRate(5000), 144, false, 0, true, 100); |
1097 | 1 | add_coin(available_coins, wallet, CAmount(3 * COIN), CFeeRate(5000), 144, false, 0, true, 100); |
1098 | 1 | add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 100); |
1099 | 1 | add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 100); |
1100 | | // Distracting clones: |
1101 | 101 | for (int j = 0; j < 100; ++j) { |
1102 | 100 | add_coin(available_coins, wallet, CAmount(8 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); |
1103 | 100 | } |
1104 | 101 | for (int j = 0; j < 100; ++j) { |
1105 | 100 | add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800); |
1106 | 100 | } |
1107 | 101 | for (int j = 0; j < 100; ++j) { |
1108 | 100 | add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600); |
1109 | 100 | } |
1110 | 101 | for (int j = 0; j < 100; ++j) { |
1111 | 100 | add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400); |
1112 | 100 | } |
1113 | 1 | return available_coins; |
1114 | 1 | }); |
1115 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); |
1116 | 1 | add_coin(4 * COIN, 0, expected_result); |
1117 | 1 | add_coin(3 * COIN, 0, expected_result); |
1118 | 1 | add_coin(2 * COIN, 0, expected_result); |
1119 | 1 | add_coin(1 * COIN, 0, expected_result); |
1120 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *res)); |
1121 | | // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. |
1122 | 1 | size_t expected_attempts = 38; |
1123 | 1 | BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); |
1124 | 1 | } |
1125 | | |
1126 | 1 | { |
1127 | | // ################################################################################################################# |
1128 | | // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead |
1129 | | // ################################################################################################################# |
1130 | 1 | CAmount target = 1.9L * COIN; |
1131 | 1 | int max_selection_weight = 40000; // WU |
1132 | 1 | const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1133 | 1 | CoinsResult available_coins; |
1134 | 1 | add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500); |
1135 | 1 | add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); |
1136 | 1 | add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); |
1137 | 101 | for (int j = 0; j < 100; ++j) { |
1138 | | // make a 100 unique coins only differing by one sat |
1139 | 100 | add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110); |
1140 | 100 | } |
1141 | 1 | return available_coins; |
1142 | 1 | }); |
1143 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); |
1144 | 1 | add_coin(1 * COIN, 1, expected_result); |
1145 | 1 | add_coin(1 * COIN, 2, expected_result); |
1146 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *res)); |
1147 | | // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. |
1148 | 1 | size_t expected_attempts = 7; |
1149 | 1 | BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); |
1150 | 1 | } |
1151 | | |
1152 | 1 | { |
1153 | | // ################################################################################################################# |
1154 | | // 8) Test input set that has a solution will not find a solution before reaching the attempt limit |
1155 | | // ################################################################################################################# |
1156 | 1 | CAmount target = 8 * COIN; |
1157 | 1 | int max_selection_weight = 3200; // WU |
1158 | 1 | dummy_params.m_min_change_target = 0; |
1159 | 1 | const auto& result_a = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1160 | 1 | CoinsResult doppelgangers; |
1161 | 19 | for (int i = 0; i < 18; ++i) { |
1162 | 18 | add_coin(doppelgangers, wallet, CAmount(1 * COIN + i), CFeeRate(0), 144, false, 0, true, 96 + i); |
1163 | 18 | } |
1164 | 1 | return doppelgangers; |
1165 | 1 | }); |
1166 | 1 | BOOST_CHECK(result_a); |
1167 | 1 | SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); |
1168 | 9 | for (int i = 0; i < 8; ++i) { |
1169 | 8 | add_coin(1 * COIN + i, 0, expected_result); |
1170 | 8 | } |
1171 | 1 | BOOST_CHECK(EquivalentResult(expected_result, *result_a)); |
1172 | | // Demonstrate a solution is found before the attempts limit is reached. |
1173 | 1 | size_t expected_attempts = 87'525; |
1174 | 1 | BOOST_CHECK_MESSAGE(result_a->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result_a->GetSelectionsEvaluated())); |
1175 | | |
1176 | | // Adding one more doppelganger causes the attempt limit to be reached before finding a solution. |
1177 | 1 | const auto& result_b = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { |
1178 | 1 | CoinsResult doppelgangers; |
1179 | 20 | for (int i = 0; i < 19; ++i) { |
1180 | 19 | add_coin(doppelgangers, wallet, CAmount(1 * COIN + i), CFeeRate(0), 144, false, 0, true, 96 + i); |
1181 | 19 | } |
1182 | 1 | return doppelgangers; |
1183 | 1 | }); |
1184 | 1 | BOOST_CHECK(!result_b); |
1185 | 1 | } |
1186 | 1 | } |
1187 | | |
1188 | | static util::Result<SelectionResult> select_coins(const CAmount& target, const CoinSelectionParams& cs_params, const CCoinControl& cc, std::function<CoinsResult(CWallet&)> coin_setup, const node::NodeContext& m_node) |
1189 | 3 | { |
1190 | 3 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
1191 | 3 | auto available_coins = coin_setup(*wallet); |
1192 | | |
1193 | 3 | LOCK(wallet->cs_wallet); |
1194 | 3 | auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/ {}, target, cc, cs_params); |
1195 | 3 | if (result) { |
1196 | 2 | const auto signedTxSize = 10 + 34 + 68 * result->GetInputSet().size(); // static header size + output size + inputs size (P2WPKH) |
1197 | 2 | BOOST_CHECK_LE(signedTxSize * WITNESS_SCALE_FACTOR, MAX_STANDARD_TX_WEIGHT); |
1198 | | |
1199 | 2 | BOOST_CHECK_GE(result->GetSelectedValue(), target); |
1200 | 2 | } |
1201 | 3 | return result; |
1202 | 3 | } |
1203 | | |
1204 | | static bool has_coin(const OutputSet& set, CAmount amount) |
1205 | 3 | { |
1206 | 633 | return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin->GetEffectiveValue() == amount; }); |
1207 | 3 | } |
1208 | | |
1209 | | BOOST_AUTO_TEST_CASE(check_max_selection_weight) |
1210 | 1 | { |
1211 | 1 | const CAmount target = 49.5L * COIN; |
1212 | 1 | CCoinControl cc; |
1213 | | |
1214 | 1 | FastRandomContext rand; |
1215 | 1 | CoinSelectionParams cs_params{ |
1216 | 1 | rand, |
1217 | 1 | /*change_output_size=*/34, |
1218 | 1 | /*change_spend_size=*/68, |
1219 | 1 | /*min_change_target=*/CENT, |
1220 | 1 | /*effective_feerate=*/CFeeRate(0), |
1221 | 1 | /*long_term_feerate=*/CFeeRate(0), |
1222 | 1 | /*discard_feerate=*/CFeeRate(0), |
1223 | 1 | /*tx_noinputs_size=*/10 + 34, // static header size + output size |
1224 | 1 | /*avoid_partial=*/false, |
1225 | 1 | }; |
1226 | | |
1227 | 1 | int max_weight = MAX_STANDARD_TX_WEIGHT - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size); |
1228 | 1 | { |
1229 | | // Scenario 1: |
1230 | | // The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs |
1231 | | // Then tries to spend 49.5 BTC |
1232 | | // The 50.0 BTC output should be selected, because the transaction would otherwise be too large |
1233 | | |
1234 | | // Perform selection |
1235 | | |
1236 | 1 | const auto result = select_coins( |
1237 | 1 | target, cs_params, cc, [&](CWallet& wallet) { |
1238 | 1 | CoinsResult available_coins; |
1239 | 1.51k | for (int j = 0; j < 1515; ++j) { |
1240 | 1.51k | add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true); |
1241 | 1.51k | } |
1242 | | |
1243 | 1 | add_coin(available_coins, wallet, CAmount(50 * COIN), CFeeRate(0), 144, false, 0, true); |
1244 | 1 | return available_coins; |
1245 | 1 | }, |
1246 | 1 | m_node); |
1247 | | |
1248 | 1 | BOOST_CHECK(result); |
1249 | | // Verify that the 50 BTC UTXO was selected, and result is below max_weight |
1250 | 1 | BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(50 * COIN))); |
1251 | 1 | BOOST_CHECK_LE(result->GetWeight(), max_weight); |
1252 | 1 | } |
1253 | | |
1254 | 1 | { |
1255 | | // Scenario 2: |
1256 | | |
1257 | | // The actor starts with 400x 0.0625 BTC and 2000x 0.025 BTC (75.0 BTC total) unspent outputs |
1258 | | // Then tries to spend 49.5 BTC |
1259 | | // A combination of coins should be selected, such that the created transaction is not too large |
1260 | | |
1261 | | // Perform selection |
1262 | 1 | const auto result = select_coins( |
1263 | 1 | target, cs_params, cc, [&](CWallet& wallet) { |
1264 | 1 | CoinsResult available_coins; |
1265 | 401 | for (int j = 0; j < 400; ++j) { |
1266 | 400 | add_coin(available_coins, wallet, CAmount(0.0625 * COIN), CFeeRate(0), 144, false, 0, true); |
1267 | 400 | } |
1268 | 2.00k | for (int j = 0; j < 2000; ++j) { |
1269 | 2.00k | add_coin(available_coins, wallet, CAmount(0.025 * COIN), CFeeRate(0), 144, false, 0, true); |
1270 | 2.00k | } |
1271 | 1 | return available_coins; |
1272 | 1 | }, |
1273 | 1 | m_node); |
1274 | | |
1275 | 1 | BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.0625 * COIN))); |
1276 | 1 | BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.025 * COIN))); |
1277 | 1 | BOOST_CHECK_LE(result->GetWeight(), max_weight); |
1278 | 1 | } |
1279 | | |
1280 | 1 | { |
1281 | | // Scenario 3: |
1282 | | |
1283 | | // The actor starts with 1515x 0.033 BTC (49.995 BTC total) unspent outputs |
1284 | | // No results should be returned, because the transaction would be too large |
1285 | | |
1286 | | // Perform selection |
1287 | 1 | const auto result = select_coins( |
1288 | 1 | target, cs_params, cc, [&](CWallet& wallet) { |
1289 | 1 | CoinsResult available_coins; |
1290 | 1.51k | for (int j = 0; j < 1515; ++j) { |
1291 | 1.51k | add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true); |
1292 | 1.51k | } |
1293 | 1 | return available_coins; |
1294 | 1 | }, |
1295 | 1 | m_node); |
1296 | | |
1297 | | // No results |
1298 | | // 1515 inputs * 68 bytes = 103,020 bytes |
1299 | | // 103,020 bytes * 4 = 412,080 weight, which is above the MAX_STANDARD_TX_WEIGHT of 400,000 |
1300 | 1 | BOOST_CHECK(!result); |
1301 | 1 | } |
1302 | 1 | } |
1303 | | |
1304 | | BOOST_AUTO_TEST_CASE(SelectCoins_effective_value_test) |
1305 | 1 | { |
1306 | | // Test that the effective value is used to check whether preset inputs provide sufficient funds when subtract_fee_outputs is not used. |
1307 | | // This test creates a coin whose value is higher than the target but whose effective value is lower than the target. |
1308 | | // The coin is selected using coin control, with m_allow_other_inputs = false. SelectCoins should fail due to insufficient funds. |
1309 | | |
1310 | 1 | std::unique_ptr<CWallet> wallet = NewWallet(m_node); |
1311 | | |
1312 | 1 | CoinsResult available_coins; |
1313 | 1 | { |
1314 | 1 | std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy"); |
1315 | 1 | add_coin(available_coins, *dummyWallet, 100000); // 0.001 BTC |
1316 | 1 | } |
1317 | | |
1318 | 1 | CAmount target{99900}; // 0.000999 BTC |
1319 | | |
1320 | 1 | FastRandomContext rand; |
1321 | 1 | CoinSelectionParams cs_params{ |
1322 | 1 | rand, |
1323 | 1 | /*change_output_size=*/34, |
1324 | 1 | /*change_spend_size=*/148, |
1325 | 1 | /*min_change_target=*/1000, |
1326 | 1 | /*effective_feerate=*/CFeeRate(3000), |
1327 | 1 | /*long_term_feerate=*/CFeeRate(1000), |
1328 | 1 | /*discard_feerate=*/CFeeRate(1000), |
1329 | 1 | /*tx_noinputs_size=*/0, |
1330 | 1 | /*avoid_partial=*/false, |
1331 | 1 | }; |
1332 | 1 | CCoinControl cc; |
1333 | 1 | cc.m_allow_other_inputs = false; |
1334 | 1 | COutput output = available_coins.All().at(0); |
1335 | 1 | cc.SetInputWeight(output.outpoint, 148); |
1336 | 1 | cc.Select(output.outpoint).SetTxOut(output.txout); |
1337 | | |
1338 | 1 | LOCK(wallet->cs_wallet); |
1339 | 1 | const auto preset_inputs = *Assert(FetchSelectedInputs(*wallet, cc, cs_params)); |
1340 | 1 | available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint}); |
1341 | | |
1342 | 1 | const auto result = SelectCoins(*wallet, available_coins, preset_inputs, target, cc, cs_params); |
1343 | 1 | BOOST_CHECK(!result); |
1344 | 1 | } |
1345 | | |
1346 | | BOOST_FIXTURE_TEST_CASE(wallet_coinsresult_test, BasicTestingSetup) |
1347 | 1 | { |
1348 | | // Test case to verify CoinsResult object sanity. |
1349 | 1 | CoinsResult available_coins; |
1350 | 1 | { |
1351 | 1 | std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy"); |
1352 | | |
1353 | | // Add some coins to 'available_coins' |
1354 | 11 | for (int i=0; i<10; i++) { |
1355 | 10 | add_coin(available_coins, *dummyWallet, 1 * COIN); |
1356 | 10 | } |
1357 | 1 | } |
1358 | | |
1359 | 1 | { |
1360 | | // First test case, check that 'CoinsResult::Erase' function works as expected. |
1361 | | // By trying to erase two elements from the 'available_coins' object. |
1362 | 1 | std::unordered_set<COutPoint, SaltedOutpointHasher> outs_to_remove; |
1363 | 1 | const auto& coins = available_coins.All(); |
1364 | 3 | for (int i = 0; i < 2; i++) { |
1365 | 2 | outs_to_remove.emplace(coins[i].outpoint); |
1366 | 2 | } |
1367 | 1 | available_coins.Erase(outs_to_remove); |
1368 | | |
1369 | | // Check that the elements were actually removed. |
1370 | 1 | const auto& updated_coins = available_coins.All(); |
1371 | 2 | for (const auto& out: outs_to_remove) { |
1372 | 16 | auto it = std::find_if(updated_coins.begin(), updated_coins.end(), [&out](const COutput &coin) { |
1373 | 16 | return coin.outpoint == out; |
1374 | 16 | }); |
1375 | 2 | BOOST_CHECK(it == updated_coins.end()); |
1376 | 2 | } |
1377 | | // And verify that no extra element were removed |
1378 | | BOOST_CHECK_EQUAL(available_coins.Size(), 8); |
1379 | 1 | } |
1380 | 1 | } |
1381 | | |
1382 | | BOOST_AUTO_TEST_SUITE_END() |
1383 | | } // namespace wallet |