@@ -1090,6 +1090,218 @@ BOOST_AUTO_TEST_CASE(effective_value_test)
1090
1090
BOOST_CHECK_EQUAL (output5.GetEffectiveValue (), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
1091
1091
}
1092
1092
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
+
1093
1305
static util::Result<SelectionResult> SelectCoinsSRD (const CAmount& target,
1094
1306
const CoinSelectionParams& cs_params,
1095
1307
const node::NodeContext& m_node,
@@ -1149,6 +1361,7 @@ BOOST_AUTO_TEST_CASE(srd_tests)
1149
1361
const auto & res = SelectCoinsSRD (target, dummy_params, m_node, max_weight, [&](CWallet& wallet) {
1150
1362
CoinsResult available_coins;
1151
1363
for (int j = 0 ; j < 10 ; ++j) {
1364
+ /* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */
1152
1365
add_coin (available_coins, wallet, CAmount (1 * COIN), CFeeRate (0 ), 144 , false , 0 , true );
1153
1366
add_coin (available_coins, wallet, CAmount (2 * COIN), CFeeRate (0 ), 144 , false , 0 , true );
1154
1367
}
0 commit comments