Skip to content

Commit 7488acc

Browse files
committed
test: Add coin_grinder_tests
1 parent 6cc9a46 commit 7488acc

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

src/wallet/test/coinselector_tests.cpp

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,218 @@ BOOST_AUTO_TEST_CASE(effective_value_test)
10901090
BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
10911091
}
10921092

1093+
static util::Result<SelectionResult> CoinGrinder(const CAmount& target,
1094+
const CoinSelectionParams& cs_params,
1095+
const node::NodeContext& m_node,
1096+
int max_weight,
1097+
std::function<CoinsResult(CWallet&)> coin_setup)
1098+
{
1099+
std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1100+
CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors
1101+
Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups;
1102+
return CoinGrinder(group.positive_group, target, cs_params.m_min_change_target, max_weight);
1103+
}
1104+
1105+
BOOST_AUTO_TEST_CASE(coin_grinder_tests)
1106+
{
1107+
// Test Coin Grinder:
1108+
// 1) Insufficient funds, select all provided coins and fail.
1109+
// 2) Exceeded max weight, coin selection always surpasses the max allowed weight.
1110+
// 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not)
1111+
// 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
1112+
// 5) Test finding a solution in a UTXO pool with mixed weights
1113+
// 6) Test that the lightest solution among many clones is found
1114+
// 7) Lots of tiny UTXOs of different amounts quickly exhausts the search attempts
1115+
1116+
FastRandomContext rand;
1117+
CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag.
1118+
rand,
1119+
/*change_output_size=*/34,
1120+
/*change_spend_size=*/68,
1121+
/*min_change_target=*/CENT,
1122+
/*effective_feerate=*/CFeeRate(5000),
1123+
/*long_term_feerate=*/CFeeRate(2000),
1124+
/*discard_feerate=*/CFeeRate(1000),
1125+
/*tx_noinputs_size=*/10 + 34, // static header size + output size
1126+
/*avoid_partial=*/false,
1127+
};
1128+
1129+
{
1130+
// #########################################################
1131+
// 1) Insufficient funds, select all provided coins and fail
1132+
// #########################################################
1133+
CAmount target = 49.5L * COIN;
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));
1140+
}
1141+
return available_coins;
1142+
});
1143+
BOOST_CHECK(!res);
1144+
BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds"
1145+
}
1146+
1147+
{
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);
1158+
}
1159+
return available_coins;
1160+
});
1161+
BOOST_CHECK(!res);
1162+
BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
1163+
}
1164+
1165+
{
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; // WU
1171+
const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
1172+
CoinsResult available_coins;
1173+
for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU
1174+
add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(5000), 144, false, 0, true);
1175+
}
1176+
for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU
1177+
add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
1178+
}
1179+
return available_coins;
1180+
});
1181+
BOOST_CHECK(res);
1182+
// Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1183+
size_t expected_attempts = 100'000;
1184+
BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1185+
}
1186+
1187+
{
1188+
// #################################################################################################################
1189+
// 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
1190+
// #################################################################################################################
1191+
CAmount target = 1.9L * COIN;
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;
1199+
});
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 = 4;
1206+
BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1207+
}
1208+
1209+
{
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; // WU
1215+
const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
1216+
CoinsResult available_coins;
1217+
for (int j = 0; j < 5; ++j) {
1218+
// Add heavy coins {3, 6, 9, 12, 15}
1219+
add_coin(available_coins, wallet, CAmount((3 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 350);
1220+
// Add medium coins {2, 5, 8, 11, 14}
1221+
add_coin(available_coins, wallet, CAmount((2 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 250);
1222+
// Add light coins {1, 4, 7, 10, 13}
1223+
add_coin(available_coins, wallet, CAmount((1 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 150);
1224+
}
1225+
return available_coins;
1226+
});
1227+
BOOST_CHECK(res);
1228+
SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1229+
add_coin(14 * COIN, 1, expected_result);
1230+
add_coin(13 * COIN, 2, expected_result);
1231+
add_coin(4 * COIN, 3, expected_result);
1232+
BOOST_CHECK(EquivalentResult(expected_result, *res));
1233+
// Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1234+
size_t expected_attempts = 2041;
1235+
BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1236+
}
1237+
1238+
{
1239+
// #################################################################################################################
1240+
// 6) Test that the lightest solution among many clones is found
1241+
// #################################################################################################################
1242+
CAmount target = 9.9L * COIN;
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);
1254+
}
1255+
for (int j = 0; j < 100; ++j) {
1256+
add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800);
1257+
}
1258+
for (int j = 0; j < 100; ++j) {
1259+
add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600);
1260+
}
1261+
for (int j = 0; j < 100; ++j) {
1262+
add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400);
1263+
}
1264+
return available_coins;
1265+
});
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+
// If this takes more attempts, the implementation has regressed
1274+
size_t expected_attempts = 82'815;
1275+
BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1276+
}
1277+
1278+
{
1279+
// #################################################################################################################
1280+
// 7) Lots of tiny UTXOs of different amounts quickly exhausts the search attempts
1281+
// #################################################################################################################
1282+
CAmount target = 1.9L * COIN;
1283+
int max_weight = 40000; // WU
1284+
const auto& res = CoinGrinder(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
1285+
CoinsResult available_coins;
1286+
add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500);
1287+
add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1288+
add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1289+
for (int j = 0; j < 100; ++j) {
1290+
// make a 100 unique coins only differing by one sat
1291+
add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110);
1292+
}
1293+
return available_coins;
1294+
});
1295+
SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1296+
add_coin(1.8 * COIN, 1, expected_result);
1297+
add_coin(1 * COIN, 2, expected_result);
1298+
BOOST_CHECK(EquivalentResult(expected_result, *res));
1299+
// Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1300+
size_t expected_attempts = 100'000;
1301+
BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1302+
}
1303+
}
1304+
10931305
static util::Result<SelectionResult> SelectCoinsSRD(const CAmount& target,
10941306
const CoinSelectionParams& cs_params,
10951307
const node::NodeContext& m_node,
@@ -1149,6 +1361,7 @@ BOOST_AUTO_TEST_CASE(srd_tests)
11491361
const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
11501362
CoinsResult available_coins;
11511363
for (int j = 0; j < 10; ++j) {
1364+
/* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */
11521365
add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(0), 144, false, 0, true);
11531366
add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true);
11541367
}

0 commit comments

Comments
 (0)