|
11 | 11 | #include <test/util/setup_common.h>
|
12 | 12 | #include <wallet/coinselection.h>
|
13 | 13 |
|
| 14 | +#include <numeric> |
14 | 15 | #include <vector>
|
15 | 16 |
|
16 | 17 | namespace wallet {
|
@@ -134,6 +135,87 @@ FUZZ_TARGET(coin_grinder)
|
134 | 135 | }
|
135 | 136 | }
|
136 | 137 |
|
| 138 | +FUZZ_TARGET(coin_grinder_is_optimal) |
| 139 | +{ |
| 140 | + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; |
| 141 | + |
| 142 | + FastRandomContext fast_random_context{ConsumeUInt256(fuzzed_data_provider)}; |
| 143 | + CoinSelectionParams coin_params{fast_random_context}; |
| 144 | + coin_params.m_subtract_fee_outputs = false; |
| 145 | + // Set effective feerate up to MAX_MONEY sats per 1'000'000 vB (2'100'000'000 sat/vB = 21'000 BTC/kvB). |
| 146 | + coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, MAX_MONEY), 1'000'000}; |
| 147 | + coin_params.m_min_change_target = ConsumeMoney(fuzzed_data_provider); |
| 148 | + |
| 149 | + // Create some coins |
| 150 | + CAmount max_spendable{0}; |
| 151 | + int next_locktime{0}; |
| 152 | + static constexpr unsigned max_output_groups{16}; |
| 153 | + std::vector<OutputGroup> group_pos; |
| 154 | + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), max_output_groups) |
| 155 | + { |
| 156 | + // With maximum m_effective_feerate and n_input_bytes = 1'000'000, input_fee <= MAX_MONEY. |
| 157 | + const int n_input_bytes{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 1'000'000)}; |
| 158 | + // Only make UTXOs with positive effective value |
| 159 | + const CAmount input_fee = coin_params.m_effective_feerate.GetFee(n_input_bytes); |
| 160 | + // Ensure that each UTXO has at least an effective value of 1 sat |
| 161 | + const CAmount eff_value{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY - max_spendable - max_output_groups + group_pos.size())}; |
| 162 | + const CAmount amount{eff_value + input_fee}; |
| 163 | + std::vector<COutput> temp_utxo_pool; |
| 164 | + |
| 165 | + AddCoin(amount, /*n_input=*/0, n_input_bytes, ++next_locktime, temp_utxo_pool, coin_params.m_effective_feerate); |
| 166 | + max_spendable += eff_value; |
| 167 | + |
| 168 | + auto output_group = OutputGroup(coin_params); |
| 169 | + output_group.Insert(std::make_shared<COutput>(temp_utxo_pool.at(0)), /*ancestors=*/0, /*descendants=*/0); |
| 170 | + group_pos.push_back(output_group); |
| 171 | + } |
| 172 | + size_t num_groups = group_pos.size(); |
| 173 | + assert(num_groups <= max_output_groups); |
| 174 | + |
| 175 | + // Only choose targets below max_spendable |
| 176 | + const CAmount target{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, std::max(CAmount{1}, max_spendable - coin_params.m_min_change_target))}; |
| 177 | + |
| 178 | + // Brute force optimal solution |
| 179 | + CAmount best_amount{MAX_MONEY}; |
| 180 | + int best_weight{std::numeric_limits<int>::max()}; |
| 181 | + for (uint32_t pattern = 1; (pattern >> num_groups) == 0; ++pattern) { |
| 182 | + CAmount subset_amount{0}; |
| 183 | + int subset_weight{0}; |
| 184 | + for (unsigned i = 0; i < num_groups; ++i) { |
| 185 | + if ((pattern >> i) & 1) { |
| 186 | + subset_amount += group_pos[i].GetSelectionAmount(); |
| 187 | + subset_weight += group_pos[i].m_weight; |
| 188 | + } |
| 189 | + } |
| 190 | + if ((subset_amount >= target + coin_params.m_min_change_target) && (subset_weight < best_weight || (subset_weight == best_weight && subset_amount < best_amount))) { |
| 191 | + best_weight = subset_weight; |
| 192 | + best_amount = subset_amount; |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + if (best_weight < std::numeric_limits<int>::max()) { |
| 197 | + // Sufficient funds and acceptable weight: CoinGrinder should find at least one solution |
| 198 | + int high_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(best_weight, std::numeric_limits<int>::max()); |
| 199 | + |
| 200 | + auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, high_max_weight); |
| 201 | + assert(result_cg); |
| 202 | + assert(result_cg->GetWeight() <= high_max_weight); |
| 203 | + assert(result_cg->GetSelectedEffectiveValue() >= target + coin_params.m_min_change_target); |
| 204 | + assert(best_weight < result_cg->GetWeight() || (best_weight == result_cg->GetWeight() && best_amount <= result_cg->GetSelectedEffectiveValue())); |
| 205 | + if (result_cg->GetAlgoCompleted()) { |
| 206 | + // If CoinGrinder exhausted the search space, it must return the optimal solution |
| 207 | + assert(best_weight == result_cg->GetWeight()); |
| 208 | + assert(best_amount == result_cg->GetSelectedEffectiveValue()); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + // CoinGrinder cannot ever find a better solution than the brute-forced best, or there is none in the first place |
| 213 | + int low_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, best_weight - 1); |
| 214 | + auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, low_max_weight); |
| 215 | + // Max_weight should have been exceeded, or there were insufficient funds |
| 216 | + assert(!result_cg); |
| 217 | +} |
| 218 | + |
137 | 219 | FUZZ_TARGET(coinselection)
|
138 | 220 | {
|
139 | 221 | FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
|
|
0 commit comments