20 #include <boost/test/unit_test.hpp> 24 BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup)
31 #define RANDOM_REPEATS 5 33 typedef std::set<std::shared_ptr<COutput>>
CoinSet;
40 static void add_coin(
const CAmount& nValue,
int nInput, std::vector<COutput>&
set)
43 tx.
vout.resize(nInput + 1);
44 tx.
vout[nInput].nValue = nValue;
46 set.emplace_back(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, -1,
true,
true,
true, 0,
false, 0);
52 tx.
vout.resize(nInput + 1);
53 tx.
vout[nInput].nValue = nValue;
55 COutput output(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, -1,
true,
true,
true, 0,
false, 0);
57 group.Insert(std::make_shared<COutput>(output), 0, 0);
64 tx.
vout.resize(nInput + 1);
65 tx.
vout[nInput].nValue = nValue;
67 std::shared_ptr<COutput> coin = std::make_shared<COutput>(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, 148,
true,
true,
true, 0,
false, fee);
69 group.Insert(coin, 0, 0);
70 coin->long_term_fee = long_term_fee;
78 tx.
vout.resize(nInput + 1);
79 tx.
vout[nInput].nValue = nValue;
89 const auto& txout = wtx.
tx->vout.at(nInput);
90 available_coins.
Add(
OutputType::BECH32, {
COutPoint(wtx.
GetHash(), nInput), txout, nAge, custom_size == 0 ?
CalculateMaximumSignedInputSize(txout, &
wallet,
nullptr) : custom_size,
true,
true,
true, wtx.
GetTxTime(), fIsFromMe, feerate});
98 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
104 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
111 std::vector<CAmount> a_amts;
112 std::vector<CAmount> b_amts;
114 a_amts.push_back(coin->txout.nValue);
117 b_amts.push_back(coin->txout.nValue);
119 std::sort(a_amts.begin(), a_amts.end());
120 std::sort(b_amts.begin(), b_amts.end());
122 std::pair<std::vector<CAmount>::iterator, std::vector<CAmount>::iterator>
ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin());
123 return ret.first == a_amts.end() &&
ret.second == b_amts.end();
130 [](
const std::shared_ptr<COutput>& a,
const std::shared_ptr<COutput>& b) {
131 return a->outpoint == b->outpoint;
133 return ret.first == a.GetInputSet().end() &&
ret.second == b.
GetInputSet().end();
140 for (
int i = 0; i < utxos; ++i) {
141 target +=
CAmount{1} << (utxos+i);
148 inline std::vector<OutputGroup>&
GroupCoins(
const std::vector<COutput>& available_coins,
bool subtract_fee_outputs =
false)
150 static std::vector<OutputGroup> static_groups;
151 static_groups.clear();
152 for (
auto& coin : available_coins) {
153 static_groups.emplace_back();
155 group.Insert(std::make_shared<COutput>(coin), 0, 0);
156 group.m_subtract_fee_outputs = subtract_fee_outputs;
158 return static_groups;
176 static_groups =
GroupOutputs(
wallet, available_coins, coin_selection_params, {{filter}})[filter];
186 wallet->SetupDescriptorScriptPubKeyMans();
195 std::vector<COutput> utxo_pool;
217 expected_result.
Clear();
225 expected_result.
Clear();
234 expected_result.
Clear();
238 expected_result.
Clear();
246 expected_result.
Clear();
250 expected_result.
Clear();
262 expected_result.
Clear();
266 expected_result.
Clear();
287 for (
int i = 0; i < 50000; ++i) {
300 for (
int i = 5; i <= 20; ++i) {
304 for (
int i = 0; i < 100; ++i) {
320 coin_selection_params_bnb.m_change_fee = coin_selection_params_bnb.m_effective_feerate.
GetFee(coin_selection_params_bnb.change_output_size);
321 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;
322 coin_selection_params_bnb.min_viable_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size);
329 add_coin(available_coins, *
wallet, 1, coin_selection_params_bnb.m_effective_feerate);
330 available_coins.
All().at(0).input_bytes = 40;
334 available_coins.
Clear();
335 add_coin(available_coins, *
wallet, 1 *
CENT, coin_selection_params_bnb.m_effective_feerate);
336 available_coins.
All().at(0).input_bytes = 40;
347 coin_selection_params_bnb.m_effective_feerate =
CFeeRate(0);
348 add_coin(available_coins, *
wallet, 5 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
349 add_coin(available_coins, *
wallet, 3 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
350 add_coin(available_coins, *
wallet, 2 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
353 COutput select_coin = available_coins.
All().at(0);
356 selected_input.
Insert(select_coin, coin_selection_params_bnb.m_subtract_fee_outputs);
360 const auto result10 =
SelectCoins(*
wallet, available_coins, selected_input, 10 *
CENT, coin_control, coin_selection_params_bnb);
370 coin_selection_params_bnb.m_effective_feerate =
CFeeRate(5000);
371 coin_selection_params_bnb.m_long_term_feerate =
CFeeRate(3000);
374 CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(68);
375 add_coin(available_coins, *
wallet, 10 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
376 add_coin(available_coins, *
wallet, 9 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
377 add_coin(available_coins, *
wallet, 1 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
379 expected_result.
Clear();
382 const auto result11 =
SelectCoins(*
wallet, available_coins, {}, 10 *
CENT, coin_control, coin_selection_params_bnb);
384 available_coins.
Clear();
387 coin_selection_params_bnb.m_effective_feerate =
CFeeRate(3000);
388 coin_selection_params_bnb.m_long_term_feerate =
CFeeRate(5000);
391 input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(68);
392 add_coin(available_coins, *
wallet, 10 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
393 add_coin(available_coins, *
wallet, 9 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
394 add_coin(available_coins, *
wallet, 1 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
396 expected_result.
Clear();
399 const auto result12 =
SelectCoins(*
wallet, available_coins, {}, 10 *
CENT, coin_control, coin_selection_params_bnb);
401 available_coins.
Clear();
404 coin_selection_params_bnb.m_effective_feerate =
CFeeRate(5000);
405 coin_selection_params_bnb.m_long_term_feerate =
CFeeRate(3000);
408 input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(68);
409 add_coin(available_coins, *
wallet, 10 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
410 add_coin(available_coins, *
wallet, 9 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
411 add_coin(available_coins, *
wallet, 1 *
CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
413 expected_result.
Clear();
416 coin_control.m_allow_other_inputs =
true;
417 COutput select_coin = available_coins.
All().at(1);
418 coin_control.Select(select_coin.
outpoint);
420 selected_input.
Insert(select_coin, coin_selection_params_bnb.m_subtract_fee_outputs);
422 const auto result13 =
SelectCoins(*
wallet, available_coins, selected_input, 10 *
CENT, coin_control, coin_selection_params_bnb);
433 add_coin(available_coins, *
wallet, 10 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
434 add_coin(available_coins, *
wallet, 9 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
435 add_coin(available_coins, *
wallet, 8 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
437 add_coin(available_coins, *
wallet, 3 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
438 add_coin(available_coins, *
wallet, 1 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
443 BOOST_REQUIRE(!no_res);
447 add_coin(available_coins, *
wallet, 5 *
CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24,
false, 0,
true);
450 expected_result.
Clear();
477 params.m_subtract_fee_outputs =
true;
478 params.m_change_fee = params.m_effective_feerate.
GetFee(params.change_output_size);
479 params.m_cost_of_change = params.m_discard_feerate.GetFee(params.change_spend_size) + params.m_change_fee;
480 params.m_min_change_target = params.m_cost_of_change + 1;
483 add_coin(available_coins, *
wallet,
COIN + params.m_cost_of_change, params.m_effective_feerate, 6,
true, 0,
true);
484 add_coin(available_coins, *
wallet, 0.5 *
COIN + params.m_cost_of_change, params.m_effective_feerate, 6,
true, 0,
true);
485 add_coin(available_coins, *
wallet, 0.5 *
COIN, params.m_effective_feerate, 6,
true, 0,
true);
508 available_coins.
Clear();
577 available_coins.
Clear();
635 available_coins.
Clear();
667 available_coins.
Clear();
668 for (
int j = 0; j < 20; j++)
680 available_coins.
Clear();
691 available_coins.
Clear();
702 available_coins.
Clear();
722 available_coins.
Clear();
724 for (uint16_t j = 0; j < 676; j++)
732 if (amt - 2000 <
CENT) {
734 uint16_t returnSize = std::ceil((2000.0 +
CENT)/amt);
735 CAmount returnValue = amt * returnSize;
748 available_coins.
Clear();
749 for (
int i2 = 0; i2 < 100; i2++)
812 for (
int i = 0; i < 1000; i++)
829 std::default_random_engine generator;
830 std::exponential_distribution<double> distribution (100);
834 for (
int i = 0; i < 100; ++i)
840 for (
int j = 0; j < 1000; ++j)
842 CAmount val = distribution(generator)*10000000;
865 cs_params.m_cost_of_change = 1;
866 cs_params.min_viable_change = 1;
868 const auto result =
SelectCoins(*
wallet, available_coins, {}, target, cc, cs_params);
870 BOOST_CHECK_GE(result->GetSelectedValue(), target);
877 const CAmount change_cost{125};
881 const CAmount excess{in_amt - fee * 2 - target};
892 selection1.ComputeAndSetWaste(0, change_cost, 0);
897 add_coin(1 *
COIN, 1, selection2, fee * 2, fee - fee_diff);
898 add_coin(2 *
COIN, 2, selection2, fee * 2, fee - fee_diff);
899 selection2.ComputeAndSetWaste(0, change_cost, 0);
900 BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste());
907 selection3.ComputeAndSetWaste(0, change_cost, 0);
909 BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste());
915 add_coin(1 *
COIN, 1, selection_nochange1, fee, fee - fee_diff);
916 add_coin(2 *
COIN, 2, selection_nochange1, fee, fee - fee_diff);
917 selection_nochange1.ComputeAndSetWaste(0, 0, 0);
923 add_coin(1 *
COIN, 1, selection_nochange2, fee, fee + fee_diff);
924 add_coin(2 *
COIN, 2, selection_nochange2, fee, fee + fee_diff);
925 selection_nochange2.ComputeAndSetWaste(0, 0, 0);
927 BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste());
935 selection.ComputeAndSetWaste(0, change_cost, 0);
944 selection.ComputeAndSetWaste(0, 0, 0);
950 const CAmount exact_target{in_amt - fee * 2};
954 selection.ComputeAndSetWaste(0, 0, 0);
961 const CAmount new_change_cost{fee_diff * 2};
964 selection.ComputeAndSetWaste(0, new_change_cost, 0);
970 const CAmount new_target{in_amt - fee * 2 - fee_diff * 2};
974 selection.ComputeAndSetWaste(0, 0, 0);
982 const CAmount target_waste1{-2 * fee_diff};
985 selection.ComputeAndSetWaste(0, 0, 0);
992 const CAmount large_fee_diff{90};
993 const CAmount target_waste2{-2 * large_fee_diff + change_cost};
994 add_coin(1 *
COIN, 1, selection, fee, fee + large_fee_diff);
995 add_coin(2 *
COIN, 2, selection, fee, fee + large_fee_diff);
996 selection.ComputeAndSetWaste(0, change_cost, 0);
1005 const CAmount min_viable_change{200};
1006 const CAmount change_cost{125};
1015 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
1017 for (
size_t i = 0; i < inputs.size(); ++i) {
1018 inputs[i]->ApplyBumpFee(20*(i+1));
1021 selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
1022 CAmount expected_waste = fee_diff * -2 + change_cost + 60;
1025 selection.SetBumpFeeDiscount(30);
1026 selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
1027 expected_waste = fee_diff * -2 + change_cost + 60 - 30;
1037 CAmount changeless_target = 3 *
COIN - 2 * fee - 100;
1041 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
1043 for (
size_t i = 0; i < inputs.size(); ++i) {
1044 inputs[i]->ApplyBumpFee(20*(i+1));
1047 selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
1048 CAmount expected_waste = fee_diff * -2 + 60 + 40;
1051 selection.SetBumpFeeDiscount(30);
1052 selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
1053 expected_waste = fee_diff * -2 + 60 - 30 + 70;
1060 const int input_bytes = 148;
1063 const int nInput = 0;
1067 tx.
vout[nInput].nValue = nValue;
1070 COutput output1(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, input_bytes,
true,
true,
true, 0,
false, feerate);
1071 const CAmount expected_ev1 = 9852;
1075 COutput output2(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, -1,
true,
true,
true, 0,
false, feerate);
1079 COutput output3(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, input_bytes,
true,
true,
true, 0,
false,
CFeeRate(100000));
1080 const CAmount expected_ev3 = -4800;
1085 COutput output4(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, input_bytes,
true,
true,
true, 0,
false, fees);
1089 COutput output5(
COutPoint(tx.
GetHash(), nInput), tx.
vout.at(nInput), 1, -1,
true,
true,
true, 0,
false, 0);
1134 int max_weight = 10
'000; // high enough to not fail for this reason. 1135 const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1136 CoinsResult available_coins; 1137 for (int j = 0; j < 10; ++j) { 1138 add_coin(available_coins, wallet, CAmount(1 * COIN)); 1139 add_coin(available_coins, wallet, CAmount(2 * COIN)); 1141 return available_coins; 1144 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds" 1148 // ########################### 1149 // 2) Test max weight exceeded 1150 // ########################### 1151 CAmount target = 29.5L * COIN; 1152 int max_weight = 3000; 1153 const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1154 CoinsResult available_coins; 1155 for (int j = 0; j < 10; ++j) { 1156 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true); 1157 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true); 1159 return available_coins; 1162 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); 1166 // ############################################################################################################### 1167 // 3) Test selection when some coins surpass the max allowed weight while others not. --> must find a good solution 1168 // ################################################################################################################ 1169 CAmount target = 25.33L * COIN; 1170 int max_weight = 10'000;
1173 for (
int j = 0; j < 60; ++j) {
1176 for (
int i = 0; i < 10; i++) {
1179 return available_coins;
1183 size_t expected_attempts = 37;
1184 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts,
strprintf(
"Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1192 int max_weight = 400
'000; // WU 1193 const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1194 CoinsResult available_coins; 1195 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 148); 1196 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68); 1197 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68); 1198 return available_coins; 1200 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1201 add_coin(1 * COIN, 1, expected_result); 1202 add_coin(1 * COIN, 2, expected_result); 1203 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1204 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1205 size_t expected_attempts = 3; 1206 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1210 // ############################################################################################################### 1211 // 5) Test finding a solution in a UTXO pool with mixed weights 1212 // ################################################################################################################ 1213 CAmount target = 30L * COIN; 1214 int max_weight = 400'000;
1217 for (
int j = 0; j < 5; ++j) {
1225 return available_coins;
1234 size_t expected_attempts = 92;
1235 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts,
strprintf(
"Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1243 int max_weight = 400
'000; // WU 1244 const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1245 CoinsResult available_coins; 1246 // Expected Result: 4 + 3 + 2 + 1 = 10 BTC at 400 vB 1247 add_coin(available_coins, wallet, CAmount(4 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1248 add_coin(available_coins, wallet, CAmount(3 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1249 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1250 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1251 // Distracting clones: 1252 for (int j = 0; j < 100; ++j) { 1253 add_coin(available_coins, wallet, CAmount(8 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); 1255 for (int j = 0; j < 100; ++j) { 1256 add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800); 1258 for (int j = 0; j < 100; ++j) { 1259 add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600); 1261 for (int j = 0; j < 100; ++j) { 1262 add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400); 1264 return available_coins; 1266 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1267 add_coin(4 * COIN, 0, expected_result); 1268 add_coin(3 * COIN, 0, expected_result); 1269 add_coin(2 * COIN, 0, expected_result); 1270 add_coin(1 * COIN, 0, expected_result); 1271 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1272 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1273 size_t expected_attempts = 38; 1274 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1278 // ################################################################################################################# 1279 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead 1280 // ################################################################################################################# 1281 CAmount target = 1.9L * COIN; 1282 int max_weight = 40000; // WU 1283 const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1284 CoinsResult available_coins; 1285 add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500); 1286 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); 1287 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); 1288 for (int j = 0; j < 100; ++j) { 1289 // make a 100 unique coins only differing by one sat 1290 add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110); 1292 return available_coins; 1294 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1295 add_coin(1 * COIN, 1, expected_result); 1296 add_coin(1 * COIN, 2, expected_result); 1297 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1298 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1299 size_t expected_attempts = 7; 1300 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1304 static util::Result<SelectionResult> SelectCoinsSRD(const CAmount& target, 1305 const CoinSelectionParams& cs_params, 1306 const node::NodeContext& m_node, 1308 std::function<CoinsResult(CWallet&)> coin_setup) 1310 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 1311 CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors 1312 Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups; 1313 return SelectCoinsSRD(group.positive_group, target, cs_params.m_change_fee, cs_params.rng_fast, max_weight); 1316 BOOST_AUTO_TEST_CASE(srd_tests) 1319 // 1) Insufficient funds, select all provided coins and fail. 1320 // 2) Exceeded max weight, coin selection always surpasses the max allowed weight. 1321 // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not) 1323 FastRandomContext rand; 1324 CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial
' flag. 1326 /*change_output_size=*/34, 1327 /*change_spend_size=*/68, 1328 /*min_change_target=*/CENT, 1329 /*effective_feerate=*/CFeeRate(0), 1330 /*long_term_feerate=*/CFeeRate(0), 1331 /*discard_feerate=*/CFeeRate(0), 1332 /*tx_noinputs_size=*/10 + 34, // static header size + output size 1333 /*avoid_partial=*/false, 1337 // ######################################################### 1338 // 1) Insufficient funds, select all provided coins and fail 1339 // ######################################################### 1340 CAmount target = 49.5L * COIN; 1341 int max_weight = 10000; // high enough to not fail for this reason. 1342 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1343 CoinsResult available_coins; 1344 for (int j = 0; j < 10; ++j) { 1345 add_coin(available_coins, wallet, CAmount(1 * COIN)); 1346 add_coin(available_coins, wallet, CAmount(2 * COIN)); 1348 return available_coins; 1351 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds" 1355 // ########################### 1356 // 2) Test max weight exceeded 1357 // ########################### 1358 CAmount target = 49.5L * COIN; 1359 int max_weight = 3000; 1360 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1361 CoinsResult available_coins; 1362 for (int j = 0; j < 10; ++j) { 1363 /* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */ 1364 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(0), 144, false, 0, true); 1365 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true); 1367 return available_coins; 1370 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); 1374 // ################################################################################################################ 1375 // 3) Test selection when some coins surpass the max allowed weight while others not. --> must find a good solution 1376 // ################################################################################################################ 1377 CAmount target = 25.33L * COIN; 1378 int max_weight = 10000; // WU 1379 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) { 1380 CoinsResult available_coins; 1381 for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU 1382 add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(0), 144, false, 0, true); 1384 for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU 1385 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true); 1387 return available_coins; 1393 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) 1395 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 1396 auto available_coins = coin_setup(*wallet); 1398 LOCK(wallet->cs_wallet); 1399 auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/ {}, target, cc, cs_params); 1401 const auto signedTxSize = 10 + 34 + 68 * result->GetInputSet().size(); // static header size + output size + inputs size (P2WPKH) 1402 BOOST_CHECK_LE(signedTxSize * WITNESS_SCALE_FACTOR, MAX_STANDARD_TX_WEIGHT); 1404 BOOST_CHECK_GE(result->GetSelectedValue(), target); 1409 static bool has_coin(const CoinSet& set, CAmount amount) 1411 return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin->GetEffectiveValue() == amount; }); 1414 BOOST_AUTO_TEST_CASE(check_max_weight) 1416 const CAmount target = 49.5L * COIN; 1419 FastRandomContext rand; 1420 CoinSelectionParams cs_params{ 1422 /*change_output_size=*/34, 1423 /*change_spend_size=*/68, 1424 /*min_change_target=*/CENT, 1425 /*effective_feerate=*/CFeeRate(0), 1426 /*long_term_feerate=*/CFeeRate(0), 1427 /*discard_feerate=*/CFeeRate(0), 1428 /*tx_noinputs_size=*/10 + 34, // static header size + output size 1429 /*avoid_partial=*/false, 1434 // The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs 1435 // Then tries to spend 49.5 BTC 1436 // The 50.0 BTC output should be selected, because the transaction would otherwise be too large 1438 // Perform selection 1440 const auto result = select_coins( 1441 target, cs_params, cc, [&](CWallet& wallet) { 1442 CoinsResult available_coins; 1443 for (int j = 0; j < 1515; ++j) { 1444 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true); 1447 add_coin(available_coins, wallet, CAmount(50 * COIN), CFeeRate(0), 144, false, 0, true); 1448 return available_coins; 1452 BOOST_CHECK(result); 1453 // Verify that only the 50 BTC UTXO was selected 1454 const auto& selection_res = result->GetInputSet(); 1455 BOOST_CHECK(selection_res.size() == 1); 1456 BOOST_CHECK((*selection_res.begin())->GetEffectiveValue() == 50 * COIN); 1462 // The actor starts with 400x 0.0625 BTC and 2000x 0.025 BTC (75.0 BTC total) unspent outputs 1463 // Then tries to spend 49.5 BTC 1464 // A combination of coins should be selected, such that the created transaction is not too large 1466 // Perform selection 1467 const auto result = select_coins( 1468 target, cs_params, cc, [&](CWallet& wallet) { 1469 CoinsResult available_coins; 1470 for (int j = 0; j < 400; ++j) { 1471 add_coin(available_coins, wallet, CAmount(0.0625 * COIN), CFeeRate(0), 144, false, 0, true); 1473 for (int j = 0; j < 2000; ++j) { 1474 add_coin(available_coins, wallet, CAmount(0.025 * COIN), CFeeRate(0), 144, false, 0, true); 1476 return available_coins; 1480 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.0625 * COIN))); 1481 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.025 * COIN))); 1487 // The actor starts with 1515x 0.033 BTC (49.995 BTC total) unspent outputs 1488 // No results should be returned, because the transaction would be too large 1490 // Perform selection 1491 const auto result = select_coins( 1492 target, cs_params, cc, [&](CWallet& wallet) { 1493 CoinsResult available_coins; 1494 for (int j = 0; j < 1515; ++j) { 1495 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true); 1497 return available_coins; 1502 // 1515 inputs * 68 bytes = 103,020 bytes 1503 // 103,020 bytes * 4 = 412,080 weight, which is above the MAX_STANDARD_TX_WEIGHT of 400,000 1504 BOOST_CHECK(!result); 1508 BOOST_AUTO_TEST_CASE(SelectCoins_effective_value_test) 1510 // Test that the effective value is used to check whether preset inputs provide sufficient funds when subtract_fee_outputs is not used. 1511 // This test creates a coin whose value is higher than the target but whose effective value is lower than the target. 1512 // The coin is selected using coin control, with m_allow_other_inputs = false. SelectCoins should fail due to insufficient funds. 1514 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 1516 CoinsResult available_coins; 1518 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy"); 1519 add_coin(available_coins, *dummyWallet, 100000); // 0.001 BTC 1522 CAmount target{99900}; // 0.000999 BTC 1524 FastRandomContext rand; 1525 CoinSelectionParams cs_params{ 1527 /*change_output_size=*/34, 1528 /*change_spend_size=*/148, 1529 /*min_change_target=*/1000, 1530 /*effective_feerate=*/CFeeRate(3000), 1531 /*long_term_feerate=*/CFeeRate(1000), 1532 /*discard_feerate=*/CFeeRate(1000), 1533 /*tx_noinputs_size=*/0, 1534 /*avoid_partial=*/false, 1537 cc.m_allow_other_inputs = false; 1538 COutput output = available_coins.All().at(0); 1539 cc.SetInputWeight(output.outpoint, 148); 1540 cc.Select(output.outpoint).SetTxOut(output.txout); 1542 LOCK(wallet->cs_wallet); 1543 const auto preset_inputs = *Assert(FetchSelectedInputs(*wallet, cc, cs_params)); 1544 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint}); 1546 const auto result = SelectCoins(*wallet, available_coins, preset_inputs, target, cc, cs_params); 1547 BOOST_CHECK(!result); 1550 BOOST_FIXTURE_TEST_CASE(wallet_coinsresult_test, BasicTestingSetup) 1552 // Test case to verify CoinsResult object sanity. 1553 CoinsResult available_coins; 1555 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy"); 1557 // Add some coins to 'available_coins
' 1558 for (int i=0; i<10; i++) { 1559 add_coin(available_coins, *dummyWallet, 1 * COIN); 1565 // By trying to erase two elements from the 'available_coins
' object. 1566 std::unordered_set<COutPoint, SaltedOutpointHasher> outs_to_remove; 1567 const auto& coins = available_coins.All(); 1568 for (int i = 0; i < 2; i++) { 1569 outs_to_remove.emplace(coins[i].outpoint); 1571 available_coins.Erase(outs_to_remove); 1573 // Check that the elements were actually removed. 1574 const auto& updated_coins = available_coins.All(); 1575 for (const auto& out: outs_to_remove) { 1576 auto it = std::find_if(updated_coins.begin(), updated_coins.end(), [&out](const COutput &coin) { 1577 return coin.outpoint == out; 1579 BOOST_CHECK(it == updated_coins.end()); 1581 // And verify that no extra element were removed 1582 BOOST_CHECK_EQUAL(available_coins.Size(), 8); 1586 BOOST_AUTO_TEST_SUITE_END() 1587 } // namespace wallet
COutPoint outpoint
The outpoint identifying this UTXO.
std::unique_ptr< interfaces::Chain > chain
std::set< std::shared_ptr< COutput > > CoinSet
Stores several 'Groups' whose were mapped by output type.
static bool EquivalentResult(const SelectionResult &a, const SelectionResult &b)
Check if SelectionResult a is equivalent to SelectionResult b.
std::vector< COutput > All() const
Concatenate and return all COutputs as one vector.
void AddInput(const OutputGroup &group)
util::Result< SelectionResult > SelectCoinsBnB(std::vector< OutputGroup > &utxo_pool, const CAmount &selection_target, const CAmount &cost_of_change, int max_weight)
static std::unique_ptr< CWallet > NewWallet(const node::NodeContext &m_node, const std::string &wallet_name="")
std::vector< OutputGroup > & GroupCoins(const std::vector< COutput > &available_coins, bool subtract_fee_outputs=false)
State of transaction not confirmed or conflicting with a known block and not in the mempool...
int64_t GetTxTime() const
static bool EqualResult(const SelectionResult &a, const SelectionResult &b)
Check if this selection is equal to another one.
std::map< OutputType, std::vector< COutput > > coins
int64_t CAmount
Amount in satoshis (Can be negative)
COutputs available for spending, stored by OutputType.
A transaction with a bunch of additional info that only the owner cares about.
NodeContext struct containing references to chain state and connection state.
CAmount m_min_change_target
Mininmum change to target in Knapsack solver and CoinGrinder: select coins to cover the payment and a...
static void add_coin(const CAmount &nValue, int nInput, std::vector< COutput > &set)
int CalculateMaximumSignedInputSize(const CTxOut &txout, const COutPoint outpoint, const SigningProvider *provider, bool can_grind_r, const CCoinControl *coin_control)
util::Result< SelectionResult > SelectCoins(const CWallet &wallet, CoinsResult &available_coins, const PreSelectedInputs &pre_set_inputs, const CAmount &nTargetValue, const CCoinControl &coin_control, const CoinSelectionParams &coin_selection_params)
Select all coins from coin_control, and if coin_control 'm_allow_other_inputs=true', call 'AutomaticCoinSelection' to select a set of coins such that nTargetValue - pre_set_inputs.total_amount is met.
const std::set< std::shared_ptr< COutput > > & GetInputSet() const
Get m_selected_inputs.
Indicate that this wallet supports DescriptorScriptPubKeyMan.
static void ApproximateBestSubset(FastRandomContext &insecure_rand, const std::vector< OutputGroup > &groups, const CAmount &nTotalLower, const CAmount &nTargetValue, std::vector< char > &vfBest, CAmount &nBest, int iterations=1000)
Find a subset of the OutputGroups that is at least as large as, but as close as possible to...
util::Result< SelectionResult > KnapsackSolver(std::vector< OutputGroup > &groups, const CAmount &nTargetValue, CAmount change_target, FastRandomContext &rng, int max_weight)
A CWallet maintains a set of transactions and balances, and provides the ability to create new transa...
A group of UTXOs paid to the same output script.
Txid GetHash() const
Compute the hash of this CMutableTransaction.
An outpoint - a combination of a transaction hash and an index n into its vout.
static const CoinEligibilityFilter filter_standard_extra(6, 6, 0)
std::vector< CTxOut > vout
#define WITH_LOCK(cs, code)
Run code while locking a mutex.
Parameters for one iteration of Coin Selection.
CScript GetScriptForDestination(const CTxDestination &dest)
Generate a Bitcoin scriptPubKey for the given CTxDestination.
static CTransactionRef MakeTransactionRef(Tx &&txIn)
Parameters for filtering which OutputGroups we may use in coin selection.
std::vector< OutputGroup > mixed_group
const Txid & GetHash() const LIFETIMEBOUND
static constexpr int32_t MAX_STANDARD_TX_WEIGHT
The maximum weight for transactions we're willing to relay/mine.
static const CoinEligibilityFilter filter_standard(1, 6, 0)
std::unique_ptr< WalletDatabase > CreateMockableWalletDatabase(MockableData records)
void Erase(const std::unordered_set< COutPoint, SaltedOutpointHasher > &coins_to_remove)
#define BOOST_CHECK_EQUAL(v1, v2)
std::vector< OutputGroup > & KnapsackGroupOutputs(const CoinsResult &available_coins, CWallet &wallet, const CoinEligibilityFilter &filter)
bool m_allow_other_inputs
If true, the selection process can add extra unselected inputs from the wallet while requires all sel...
Fee rate in satoshis per kilovirtualbyte: CAmount / kvB.
util::Result< SelectionResult > CoinGrinder(std::vector< OutputGroup > &utxo_pool, const CAmount &selection_target, CAmount change_target, int max_weight)
bilingual_str ErrorString(const Result< T > &result)
CAmount GetFee(uint32_t num_bytes) const
Return the fee in satoshis for the given vsize in vbytes.
A mutable version of CTransaction.
static constexpr CAmount CENT
std::shared_ptr< CWallet > wallet
void Add(OutputType type, const COutput &out)
BOOST_AUTO_TEST_CASE(bnb_search_test)
A UTXO under consideration for use in funding a new transaction.
FilteredOutputGroups GroupOutputs(const CWallet &wallet, const CoinsResult &coins, const CoinSelectionParams &coin_sel_params, const std::vector< SelectionFilter > &filters, std::vector< OutputGroup > &ret_discarded_groups)
uint64_t randrange(uint64_t range) noexcept
Generate a random integer in the range [0..range).
PreselectedInput & Select(const COutPoint &outpoint)
Lock-in the given output for spending.
#define Assert(val)
Identity function.
static CAmount make_hard_case(int utxos, std::vector< COutput > &utxo_pool)
#define BOOST_CHECK(expr)
static const CoinEligibilityFilter filter_confirmed(1, 1, 0)
static constexpr CAmount COIN
The amount of satoshis in one BTC.