Refactor CoinGrinder tests #36255

pull murchandamus wants to merge 11 commits into bitcoin:master from murchandamus:2026-09-refactor-coingrinder-tests changing 2 files +194 −274
  1. murchandamus commented at 10:55 PM on September 14, 2026: member

    This PR migrates the CoinGrinder tests from coinselector_tests.cpp to the new coin selection test suite coinselection_tests.cpp where we use effective values, actual feerates, and avoid setting coin selection parameters that never appear in production.

    Most of the tests are exact replications of the originals in the new framework, just the CoinGrinder exhaustion test was slightly refactored to simplify: it was using different weights for every single UTXO, but just two different weights are sufficient to replicate the relevant behavior for the test.

    Each commit migrates one test to simplify review.

  2. refactor: Move `is_eff_value` in MakeCoin to back
    The `is_eff_value` parameter is never used with a non-default value, so
    moving it to the back of the parameters allows dropping the mention of
    its default value in many instances.
    9a0964378d
  3. tests: Check amount and size of equivalent inputs 29b47fbd62
  4. test: migrate CoinGrinder insufficient funds coverage c147d3e3af
  5. test: migrate CoinGrinder maximum weight coverage 1856acc212
  6. test: migrate CoinGrinder bounded weight selection coverage a3c07a50f7
  7. test: simplify BnB custom input setup 8b6a75edc8
  8. test: migrate CoinGrinder lighter input preference coverage 4e7a419642
  9. test: migrate CoinGrinder mixed weight coverage a6e70bc34a
  10. test: migrate CoinGrinder cloned input coverage 1d626e503a
  11. test: migrate CoinGrinder tiny input coverage 86d8c7ed1a
  12. test: migrate CoinGrinder attempt limit coverage 08ee40f1de
  13. DrahtBot commented at 10:55 PM on September 14, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36255.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK l0rinc, yancyribbens

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35511 (RFC: consensus: Make CAmount a class by hodlinator)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • The inputs size exceeds the maximum weight -> The input size exceeds the maximum weight [“inputs size” is grammatically incorrect and reads awkwardly]

    <sup>2026-09-14 22:56:14</sup>

  14. in src/wallet/test/coinselection_tests.cpp:232 in 9a0964378d
     228 | @@ -229,7 +229,7 @@ BOOST_AUTO_TEST_CASE(bnb_exhaustion_with_solution_test)
     229 |      // A hard case with no exact-match solution: BnB must still report that the algorithm did not complete once the
     230 |      // search is pushed into the attempt limit, even though it finds a solution within cost_of_change of the target.
     231 |      for (size_t i = 0; i < 19; ++i) {
     232 | -        utxo_pool.push_back(MakeCoin(100'000 + i, /*is_eff_value=*/true, default_cs_params));
    


    l0rinc commented at 11:14 PM on September 14, 2026:

    9a09643 refactor: Move is_eff_value in MakeCoin to back:

    Here we can remove the default_cs_params as well now, right?

  15. murchandamus marked this as a draft on Sep 14, 2026
  16. murchandamus commented at 11:16 PM on September 14, 2026: member

    Made available as a PR, because I was discussing it with a colleague who wanted to provide feedback, but there are still a couple minor things that I want to tweak anyway.

  17. in src/wallet/test/coinselection_tests.cpp:93 in 29b47fbd62
      88 | @@ -89,22 +89,21 @@ static void AddDuplicateCoins(std::vector<OutputGroup>& utxo_pool, int count, in
      89 |  }
      90 |  
      91 |  /** Check if SelectionResult a is equivalent to SelectionResult b.
      92 | - * Two results are equivalent if they are composed of the same input values, even if they have different inputs (i.e., same value, different prevout) */
      93 | -static bool HaveEquivalentValues(const SelectionResult& a, const SelectionResult& b)
      94 | + * Two results are equivalent if they are composed of inputs with the same amounts and sizes, even if they have different inputs (i.e., different prevouts). */
      95 | +static bool HaveEquivalentInputs(const SelectionResult& a, const SelectionResult& b)
    


    l0rinc commented at 11:38 PM on September 14, 2026:

    29b47fb tests: Check amount and size of equivalent inputs:

    The previous implementation was indeed hard to understand. This is basically a permutation check of a subset of fields, and since we have very few elements here (given it's test code), we could either simplify further to:

      static bool HaveEquivalentInputs(const SelectionResult& a, const SelectionResult& b)
      {
          return std::ranges::is_permutation(a.GetInputSet(), b.GetInputSet(), [](auto& x, auto& y) {
              return x->txout.nValue == y->txout.nValue && x->input_bytes == y->input_bytes;
          });
      }
    

    or if you don't find this readable we could extract the common parts that prepare the final comparable objects and compare those directly at the call site:

    /** The (amount, input size) pairs of a result's inputs in sorted order, which identify a selection regardless of the prevouts of its inputs. */
    static std::vector<std::pair<CAmount, int>> SortedInputs(const SelectionResult& result)
    {
        std::vector<std::pair<CAmount, int>> inputs;
        for (auto& coin : result.GetInputSet()) {
            inputs.emplace_back(coin->txout.nValue, coin->input_bytes);
        }
        std::ranges::sort(inputs);
        return inputs;
    }
    

    and in the assertion we can have

    BOOST_TEST(SortedInputs(*result) == SortedInputs(expected_result));
    

    directly.

  18. DrahtBot added the label CI failed on Sep 14, 2026
  19. DrahtBot commented at 11:55 PM on September 14, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task No wallet: https://github.com/bitcoin/bitcoin/actions/runs/34906476628/job/104184206297</sub> <sub>LLM reason (✨ experimental): CI failed because clang++-17 crashed (segmentation fault, exit code 139) while compiling src/ipc/libmultiprocess/src/mp/util.cpp.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  20. in src/wallet/test/coinselection_tests.cpp:63 in 9a0964378d
      59 | @@ -60,7 +60,7 @@ static CoinSelectionParams init_cs_params(int eff_feerate = 5000)
      60 |  static const CoinSelectionParams default_cs_params = init_cs_params();
      61 |  
      62 |  /** Make one OutputGroup with a single UTXO that either has a given effective value (default) or a given amount (`is_eff_value = false`). */
      63 | -static OutputGroup MakeCoin(const CAmount& amount, bool is_eff_value = true, CoinSelectionParams cs_params = default_cs_params, int custom_spending_vsize = P2WPKH_INPUT_VSIZE)
      64 | +static OutputGroup MakeCoin(const CAmount& amount, CoinSelectionParams cs_params = default_cs_params, int custom_spending_vsize = P2WPKH_INPUT_VSIZE, bool is_eff_value = true)
    


    l0rinc commented at 6:29 AM on September 15, 2026:

    9a09643 refactor: Move is_eff_value in MakeCoin to back:

    Since every caller uses effective values, could we remove is_eff_value altogether?

  21. in src/wallet/test/coinselection_tests.cpp:264 in c147d3e3af
     260 | @@ -261,6 +261,26 @@ BOOST_AUTO_TEST_CASE(bnb_feerate_sensitivity_test)
     261 |      TestBnBSuccess("Prefer two light inputs over two heavy inputs at high feerates", high_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{3 * CENT, 10 * CENT}, /*expected_attempts=*/9, high_feerate_params);
     262 |  }
     263 |  
     264 | +static void TestCGFail(std::string test_title, std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target)
    


    l0rinc commented at 6:30 AM on September 15, 2026:

    c147d3e test: migrate CoinGrinder insufficient funds coverage:

    Assertions inside these helpers report the helper's source line, so test_title has to identify the individual scenario. Could we organize the scenarios into named Boost test cases and share the repeated assertions through small macros, as in the headers-sync tests? That would keep failure locations at each call site and make the test categories independently runnable. Cases with the same setup and checks can share a small table of inputs and expectations, while cases with different assertion flows can remain separate.

  22. in src/wallet/test/coinselection_tests.cpp:285 in a3c07a50f7
     280 | +
     281 | +    const auto result{CoinGrinder(utxo_pool, selection_target, CENT, max_selection_weight)};
     282 | +
     283 | +    BOOST_CHECK_MESSAGE(result, "Falsy result in CoinGrinder-Success: " + test_title);
     284 | +    BOOST_CHECK_MESSAGE(HaveEquivalentInputs(expected_result, *result), strprintf("Result mismatch in CoinGrinder-Success: %s. Expected %s, but got %s", test_title, InputAmountsToString(expected_result), InputAmountsToString(*result)));
     285 | +    BOOST_CHECK_MESSAGE(result->GetSelectedValue() == expected_amount, strprintf( "Selected amount mismatch in CoinGrinder-Success: %s. Expected %d, but got %d", test_title, expected_amount, result->GetSelectedValue()));
    


    l0rinc commented at 6:32 AM on September 15, 2026:

    a3c07a5 test: migrate CoinGrinder bounded weight selection coverage:

    HaveEquivalentInputs already compares the input values, and GetSelectedValue() just sums them. Could we drop expected_amount, its accumulation, and this assertion? For the remaining scalar checks, Boost already prints the operands:

    BOOST_CHECK_LE(result->GetWeight(), max_selection_weight);
    BOOST_CHECK_EQUAL(result->GetSelectionsEvaluated(), expected_attempts);
    

    I would keep the weight check because GetWeight() reads separately maintained state. The same simplifications apply to the BnB helper.

  23. in src/wallet/test/coinselection_tests.cpp:317 in a3c07a50f7
     309 | @@ -292,6 +310,19 @@ BOOST_AUTO_TEST_CASE(coin_grinder_max_weight_test)
     310 |      }
     311 |  }
     312 |  
     313 | +BOOST_AUTO_TEST_CASE(coin_grinder_lowest_weight_below_limit_test)
     314 | +{
     315 | +    {
     316 | +        std::vector<OutputGroup> utxo_pool;
     317 | +        AddDuplicateCoins(utxo_pool, /*count=*/60, /*amount=*/0.33 * COIN);
    


    l0rinc commented at 6:32 AM on September 15, 2026:

    a3c07a5 test: migrate CoinGrinder bounded weight selection coverage:

    Could we use integer amounts throughout the migrated tests, e.g. 33 * CENT here and 2'533 * CENT for the target below? This expresses the intended satoshi amounts directly and avoids floating-point multiplication followed by conversion to CAmount.

  24. in src/wallet/test/coinselection_tests.cpp:369 in 1d626e503a
     364 | +        std::vector<OutputGroup> utxo_pool;
     365 | +        for (CAmount amount : {4 * COIN, 3 * COIN, 2 * COIN, 1 * COIN}) {
     366 | +            utxo_pool.push_back(MakeCoin(amount, /*custom_spending_vsize=*/100));
     367 | +        }
     368 | +        for (int j = 0; j < 100; ++j) {
     369 | +            utxo_pool.push_back(MakeCoin(8 * COIN, /*custom_spending_vsize=*/1000));
    


    l0rinc commented at 6:35 AM on September 15, 2026:

    1d626e5 test: migrate CoinGrinder cloned input coverage:

    Could AddDuplicateCoins take an optional input size so these four loops become four calls to the existing helper? We could also build expected_inputs once and initialize utxo_pool from it before adding the heavier clones, which avoids constructing the four expected coins twice.

  25. l0rinc changes_requested
  26. l0rinc commented at 6:47 AM on September 15, 2026: contributor

    Concept ACK

    I left a few suggestions to make the test categories independently runnable (each one exercising multiple scenarios, grouping by category), untangling the separate test cases and simplified the helpers, assertions, and amount expressions. I've implemented them in https://github.com/l0rinc/bitcoin/pull/297/commits (see last commit) - feel free to pick and choose and split and fold them back into your commits if you agree with the direction.

    Note that the CI failure seems unrelated.

  27. in src/wallet/test/coinselection_tests.cpp:99 in 08ee40f1de
      98 |  }
      99 |  
     100 |  /** Check if SelectionResult a is equivalent to SelectionResult b.
     101 | - * Two results are equivalent if they are composed of the same input values, even if they have different inputs (i.e., same value, different prevout) */
     102 | -static bool HaveEquivalentValues(const SelectionResult& a, const SelectionResult& b)
     103 | + * Two results are equivalent if they are composed of inputs with the same amounts and sizes, even if they have different inputs (i.e., different prevouts). */
    


    yancyribbens commented at 10:20 AM on September 15, 2026:
     * Two results are equivalent if they are composed of inputs with the same amounts and sizes, even if they are composed from different outputs (i.e., different prevouts). */
    
  28. in src/wallet/test/coinselection_tests.cpp:291 in 1856acc212
     286 | +{
     287 | +    {
     288 | +        std::vector<OutputGroup> utxo_pool;
     289 | +        AddDuplicateCoins(utxo_pool, /*count=*/10, /*amount=*/1 * COIN);
     290 | +        AddDuplicateCoins(utxo_pool, /*count=*/10, /*amount=*/2 * COIN);
     291 | +        TestCGFail("Exceed max weight", utxo_pool, /*selection_target=*/29.5L * COIN, /*max_selection_weight=*/1000, /*expect_max_weight_exceeded=*/true);
    


    yancyribbens commented at 10:42 AM on September 15, 2026:

    This is a strange max_selection_weight. It would be better to use real values, like max transaction weight of of 400_000 I think, although that's unrelated to this change.

  29. in src/wallet/test/coinselector_tests.cpp:984 in 86d8c7ed1a
     975 | @@ -976,32 +976,6 @@ BOOST_AUTO_TEST_CASE(coin_grinder_tests)
     976 |              /*avoid_partial=*/false,
     977 |      };
     978 |  
     979 | -    {
     980 | -        // #################################################################################################################
     981 | -        // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
     982 | -        // #################################################################################################################
     983 | -        CAmount target =  1.9L * COIN;
     984 | -        int max_selection_weight = 40000; // WU
    


    yancyribbens commented at 10:49 AM on September 15, 2026:

    Unrelated to this change, but why choose a set and target of 40000? I had thought at first this was 400000 which would make more sense, so I wonder if this was a typo.

  30. yancyribbens commented at 10:56 AM on September 15, 2026: contributor

    Concept ACK. Thanks for following up on this. It would be nice to see the coinselector test file removed entirely in the future since having multiple test files has caused confusion. Also minor nit, but your older commit messages have different capitalization than your newer messages.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-15 21:50 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me