wallet: Add CoinGrinder coin selection algorithm #27877

pull murchandamus wants to merge 15 commits into bitcoin:master from murchandamus:2023-05-coingrinder changing 5 files +745 −4
  1. murchandamus commented at 10:32 pm on June 13, 2023: contributor

    Please refer to the topic on Delving Bitcoin discussing Gutter Guard/Coingrinder simulation results.

    Adds a coin selection algorithm that minimizes the weight of the input set while creating change.

    Motivations

    • At high feerates, using unnecessary inputs can significantly increase the fees
    • Users are upset when fees are relatively large compared to the amount sent
    • Some users struggle to maintain a sufficient count of UTXOs in their wallet

    Approach

    So far, Bitcoin Core has used a balanced approach to coin selection, where it will generate multiple input set candidates using various coin selection algorithms and pick the least wasteful among their results, but not explicitly minimize the input set weight. Under some circumstances, we do want to minimize the weight of the input set. Sometimes changeless solutions require many or heavy inputs, and there is not always a changeless solution for Branch and Bound to find in the first place. This can cause expensive transactions unnecessarily. Given a wallet with sufficient funds, CoinGrinder will pick the minimal-waste input set for a transaction with a change output. The current implementation only runs CoinGrinder at feerates over 3×long-term-feerate-estimate (by default 30 ṩ/vB), which may be a decent compromise between our goal to reduce costs for the users, but still permit transactions at lower feerates to naturally reduce the wallet’s UTXO pool to curb bloat.

    Trade-offs

    Simulations for my thesis on coin selection (see Section 6.3.2.1 [PDF]) suggest that minimizing the input set for all transactions tends to grind a wallet’s UTXO pool to dust (pun intended): an approach selecting inputs per coin-age-priority (in effect similar to “largest first selection”) on average produced a UTXO pool with 15× the UTXO count as Bitcoin Core’s Knapsack-based Coin Selection then (in 2016). Therefore, I do not recommend running CoinGrinder under all circumstances, but only at extreme feerates or when we have another good reason to minimize the input set for other reasons. In the long-term, we should introduce additional metrics to score different input set candidates, e.g. on basis of their privacy and wallet health impact, to pick from all our coin selection results, but until then, we may want to limit use of CoinGrinder in other ways.

  2. DrahtBot commented at 10:32 pm on June 13, 2023: contributor

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

    Code Coverage

    For detailed information about the code coverage, see the test coverage report.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK achow101, sipa, sr-gi
    Concept ACK jonatack, S3RK, kashifs, ishaanam

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #28985 (Avoid changeless input sets when SFFO is active by murchandamus)
    • #28977 (Add Gutter Guard Selector by murchandamus)

    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.

  3. DrahtBot added the label CI failed on Jun 13, 2023
  4. DrahtBot renamed this:
    Add CoinGrinder coin selection algorithm
    wallet: Add CoinGrinder coin selection algorithm
    on Jun 14, 2023
  5. murchandamus commented at 1:58 pm on June 14, 2023: contributor

    At this point, I’m mostly looking for Concept reviews.

    Some ideas floating around in the context that I’d love comments on:

    • Also run CoinGrinder when the fees surpass some relative portion of the recipient output amounts
    • Only run CoinGrinder if no changeless solution was proposed
  6. t-bast commented at 10:44 am on June 15, 2023: contributor

    Thanks for exploring this, I think this is an interesting idea! I’ll comment based on my specific usage of the wallet for lightning service providers, please bear in mind that this is a very specific use case that is quite different from regular users (see #24795 for early details about this).

    My understanding of the current goals of coin selection are that it tries to find a good balance between minimizing fees and consolidating utxos. The latter actually helps the former: consolidating utxos is usually good for performance, and if you do it when fees are low, you will avoid paying more fees in the future when fees are higher because you’ll be able to use less inputs in your future transactions.

    In the case of a lightning service provider, we’d rather avoid consolidating utxos, and are ready to pay more fees to keep a large enough utxo set to satisfy our users’ needs. The main reasons for that are that:

    • we are using 0-conf (in a setup where the user is trusting us to not double-spend)
    • we are using low fees for those transactions (it’s ok to keep transactions unconfirmed until the mempool clears, this is how we can keep the cost low for users)
    • the change outputs from most of those transactions are unsafe because they may be double-spent by a commitment transaction (and thus generally unusable until the the transaction confirms, except when used for that same user)

    That last point is a consequence of splicing, let me know if you want me to explain that point in more details.

    If we run out of safe utxos, we cannot onboard new users, which may require us to transfer funds from a cold wallet while making sure we’re creating many utxos, at a time where the on-chain fees may be high. Our ideal wallet would maintain a large pool of utxos of various sizes, to ensure we’re always ready to onboard new users or sell liquidity to existing users.

    I understand that this is the opposite of what most end users want and can’t be achieved by the same algorithm: that’s why I would find it very useful to have various coin selection algorithms in bitcoin core, that are tuned to different profiles/scenarios. I like that this algorithm feels like something we could use (at all feerates) to keep a wallet state that makes more sense for us.

  7. murchandamus force-pushed on Jun 15, 2023
  8. murchandamus commented at 8:11 pm on June 15, 2023: contributor

    Changes in last force-push: https://github.com/bitcoin/bitcoin/compare/10621c6c4499ba32ff3b7f0bd11b0351f36982b4..0446986f8dd87db29153618d380d159ec0016c1a

    Thanks for your thoughts, @t-bast. Your comment inspired a small optimization. CoinGrinder has been simplified by keeping only track of the input weight (instead of weight and waste), and minimizing input weight throughout. This reduced the number of parameters necessary to call CoinGrinder, simplified the input set traversal, and allows CoinGrinder to be used at all feerates instead of just high feerates. It is however still only called for feerates above 100 ṩ/vB from spend.cpp at this time.

  9. dergoegge commented at 9:40 am on June 20, 2023: member

    Ran the fuzzer for a bit:

    0fuzz: wallet/test/fuzz/coinselection.cpp:102: void wallet::coin_grinder_fuzz_target(FuzzBufferType): Assertion `result_knapsack->GetWeight() >= result_cg->GetWeight()' failed.
    

    Input to reproduce:

    0$ echo "d3d3/wF383d3d3d3d3d3d3d3d3d5d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3fzd3d3d3d3d7BE
    1Wnd3d3d3d3V3d3d3d3d3d3l6d3d3d3d3d3f///8Fd3d3d3d3d3d3d3d3d1d3d3d3d3d3d3d3d3d3
    2d3d3d3d3d3d3d3d3d3d3d3d3Mnd3d3d3d3d5d3d3d3d3YS5hYWFhYWFhYWFhYWFhYWFhYWFhYUBh
    3YWEuAAAACmFhWWFhYWFhd3d3d3d3d3d3d3d3dzp3d3d3d3d3d3d3d3d3d3d3d/N3d3d3d3d3d3d3
    4d3d3eXd3d3d3d3d3d3d1d3d3d3d3d3d3dzJ3d3d3d3d3eXd3d3d3d3d3d3d1d3d3d3d3d3d3dzJ3
    5d3d3d3d3d3d3d3d3d3d3dyh3d3d3d3d3dxcC+xc=" | base64 -d > coingrinder_crash
    
  10. dergoegge commented at 9:24 am on June 21, 2023: member
    I would like to see if the CI catches the crash, now that #27919 has been merged. @Xekyo would you mind rebasing?
  11. murchandamus force-pushed on Jun 21, 2023
  12. murchandamus commented at 5:07 pm on June 21, 2023: contributor

    @dergoegge: Thanks, I discovered the issue myself over the weekend and was exploring yesterday whether I could come up with another optimization. In the end, I fixed the fuzz test by only comparing the weight with SRD and Knapsack when CoinGrinder actually concluded searching the combination space exhaustively. Since finding the lowest weight input set is an NP-hard problem, it’s clearly expected that we can generate UTXO pools for which CoinGrinder will not be able to finish the search in 100k attempts, it was naïve to expect that the fuzz test wouldn’t eventually find such a case.

    Your crash seed passes now.

    I’m sorry I’m only seeing your comment after pushing the fix.

  13. maflcko commented at 5:10 pm on June 21, 2023: member
    I think you can still rebase to unblock the CI in any case?
  14. murchandamus force-pushed on Jun 21, 2023
  15. murchandamus commented at 5:16 pm on June 21, 2023: contributor

    Rebased on Master.

    Changes since last rebase: SelectionResult now has a record if BnB or CoinGrinder run out of tries and thus have not found the optimal solution.

  16. DrahtBot removed the label CI failed on Jun 21, 2023
  17. DrahtBot added the label Wallet on Jun 21, 2023
  18. DrahtBot added the label Needs rebase on Jun 23, 2023
  19. murchandamus force-pushed on Jun 26, 2023
  20. DrahtBot removed the label Needs rebase on Jun 26, 2023
  21. in src/wallet/coinselection.cpp:286 in 49ec2d4ada outdated
    272+        curr_selection.push_back(utxo_pool_index);
    273+        next_op = operations::add;
    274+    };
    275+
    276+    for (size_t curr_try = 0;;) {
    277+        if (curr_try >= TOTAL_TRIES || next_op == operations::done) {
    


    brunoerg commented at 3:37 pm on September 7, 2023:
    In 49ec2d4ada6d9abb43c2a5f35dd6f2e5666157f9: Is there any case that curr_try may be greater than TOTAL_TRIES?

    murchandamus commented at 5:56 pm on September 7, 2023:
    No, I don’t think so, an equality check here should be functionally equivalent.
  22. in src/wallet/coinselection.cpp:191 in cf14a72859 outdated
    173@@ -174,6 +174,10 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
    174                 curr_selection_weight += utxo.m_weight;
    175             }
    176         }
    177+        if (curr_try >= TOTAL_TRIES - 1) {
    178+            // On last attempt and didn’t break due to full traversal: solution is non-optimal
    179+            result.SetAlgoCompleted(false);
    


    brunoerg commented at 7:23 pm on September 7, 2023:
    In cf14a72859a2eaf42a24a2ea3125bdadde5f6611: Here we’re setting it for BnB but seems we’re not using GetAlgoCompleted for it?

    murchandamus commented at 7:41 pm on September 8, 2023:
    You are right, I’ll have follow up on this

    murchandamus commented at 7:51 pm on December 8, 2023:
    I dropped tracking whether the algorithm was able to exhaustively search the UTXO pool from BnB for now
  23. murchandamus force-pushed on Sep 15, 2023
  24. murchandamus commented at 9:35 pm on September 15, 2023: contributor
    Rebased. Fixed up a couple comments and improved a commit message
  25. murchandamus force-pushed on Sep 26, 2023
  26. DrahtBot added the label CI failed on Sep 26, 2023
  27. DrahtBot removed the label CI failed on Sep 27, 2023
  28. jonatack commented at 8:10 pm on November 30, 2023: contributor
    Concept ACK here or #28977 based on today’s IRC discussion https://bitcoin-irc.chaincode.com/bitcoin-core-dev/2023-11-30#986727.
  29. S3RK commented at 12:32 pm on December 3, 2023: contributor

    Concept ACK.

    The PR description should say that we don’t use hardcoded fee rate value, but rather multiple of long-term-fee-rate. LTFR is an existing user configurable parameter. It’s also a good fit for the purpose of determining when to be more “aggressive” with coin selection, because it captures forward looking fee market expectations.

  30. murchandamus commented at 7:19 pm on December 4, 2023: contributor

    The PR description should say that we don’t use hardcoded fee rate value, but rather multiple of long-term-fee-rate. LTFR is an existing user configurable parameter. It’s also a good fit for the purpose of determining when to be more “aggressive” with coin selection, because it captures forward looking fee market expectations.

    Thanks, I’ve amended the description.

  31. in src/wallet/coinselection.cpp:37 in 313d4df883 outdated
    32+            // Sort lower waste to front on tied effective_value
    33+            if (a.long_term_fee < a.fee) {
    34+                return a.m_weight < b.m_weight;
    35+            } else {
    36+                return a.m_weight > b.m_weight;
    37+            }
    


    achow101 commented at 8:25 pm on December 4, 2023:

    In 313d4df88360edf1bad1f3415166a96f0ee93f0a “opt: Tie-break UTXO sort by waste for BnB”

    This comparison is a little hard to follow as it requires reminding yourself of the effects of the waste score. It wasn’t immediately obvious why the weight matters for this comparison. It seems like it would be simpler to just actually calculate the wastes and compare them.

    0            return (a.fee - a.long_term_fee) > (b.fee - b.long_term_fee);
    

    murchandamus commented at 7:33 pm on December 8, 2023:
    Thanks, great suggestion. Adopted, but with turned around comparison, it needs to be less-than (I checked with a test).
  32. in src/wallet/spend.cpp:588 in ec308d7873 outdated
    584@@ -585,6 +585,13 @@ util::Result<SelectionResult> ChooseSelectionResult(const CAmount& nTargetValue,
    585         results.push_back(*knapsack_result);
    586     } else append_error(knapsack_result);
    587 
    588+    if (coin_selection_params.m_effective_feerate > CFeeRate{coin_selection_params.m_long_term_feerate.GetFee(3000)}) { // Minimize input set for feerates of at least 3×LTFRE (default: 30 ṩ/vB+)
    


    achow101 commented at 8:31 pm on December 4, 2023:

    In ec308d7873f7eb4d597c00708091ede55e9ff5a5 “coinselection: Add CoinGrinder algorithm”

    I wish there was a better way to write feerate*3. This is kinda hard to read.


    murchandamus commented at 7:38 pm on December 8, 2023:
    Added multiplication operator in #29037, and amended here
  33. in src/wallet/coinselection.cpp:274 in ec308d7873 outdated
    269+        OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
    270+        curr_amount += utxo.GetSelectionAmount();
    271+        curr_weight += utxo.m_weight;
    272+        curr_selection.push_back(utxo_pool_index);
    273+        next_op = operations::add;
    274+    };
    


    achow101 commented at 8:50 pm on December 4, 2023:

    In ec308d7873f7eb4d597c00708091ede55e9ff5a5 “coinselection: Add CoinGrinder algorithm”

    Since this lambda is used in only one spot, I think it would be a bit easier to read to just have it inline.


    murchandamus commented at 7:41 pm on December 8, 2023:
    Inlined the add_utxo_at_index function
  34. in src/wallet/coinselection.cpp:264 in 6ba9c1cd10 outdated
    256@@ -257,17 +257,21 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    257     // Sort the utxo_pool
    258     std::sort(utxo_pool.begin(), utxo_pool.end(), descending_effval_weight);
    259     std::vector<CAmount> lookahead(utxo_pool.size());
    260+    std::vector<int> min_tail_weight(utxo_pool.size());
    261 
    262     // Calculate lookahead and check that there are sufficient funds
    263     CAmount total_available = 0;
    264+    int min_group_weight = 2*max_weight;
    


    achow101 commented at 9:20 pm on December 4, 2023:

    In 6ba9c1cd1032c75e618d8ca7b7aee6ebed16d22d “opt: Cut if last addition was minimal weight”

    Why 2x max_weight instead of just max_weight?


    murchandamus commented at 7:47 pm on December 8, 2023:
    I just need this to be as big as the largest or bigger than any of the actual groups’ input weights. Changed to std::numeric_limits<int>::max()
  35. murchandamus force-pushed on Dec 8, 2023
  36. murchandamus commented at 8:05 pm on December 8, 2023: contributor
    Rebased on #29037, addressed comments by @achow101 and @brunoerg
  37. murchandamus commented at 3:03 am on December 9, 2023: contributor
    Edit: Moved simulation result discussion to Delving Bitcoin
  38. murchandamus commented at 3:08 am on December 9, 2023: contributor
    Edit: Moved simulation result discussion to Delving Bitcoin
  39. murchandamus force-pushed on Dec 10, 2023
  40. in src/wallet/coinselection.cpp:212 in 232d3d94fc outdated
    207+ * • ADD:   010100 ⇒ 010110
    208+ *          the UTXO at the index (here 5th) is additionally selected
    209+ * • SHIFT: 010100 ⇒ 010010
    210+ *          the 4th is deselected and the 5th is selected instead
    211+ * • CUT:   010100 ⇒ 001000
    212+ *          the third is deselected, the first is deselected and the second is selected
    


    kashifs commented at 4:14 pm on December 10, 2023:

    For consistency, I think this should read:

    the 4th is deselected, the 2nd is deselected and the 3rd is selected


    murchandamus commented at 4:10 pm on December 22, 2023:
    Thanks, I’ll fix that
  41. in src/wallet/coinselection.cpp:214 in 232d3d94fc outdated
    209+ * • SHIFT: 010100 ⇒ 010010
    210+ *          the 4th is deselected and the 5th is selected instead
    211+ * • CUT:   010100 ⇒ 001000
    212+ *          the third is deselected, the first is deselected and the second is selected
    213+ *
    214+ * @param const std::vector<OutputGroup>& utxo_pool The UTXO groups that we are choosing from.
    


    kashifs commented at 5:40 pm on December 10, 2023:

    This should read:

    @param std::vector<OutputGroup>& utxo_pool The UTXO groups that we are choosing from.


    murchandamus commented at 4:11 pm on December 22, 2023:
    Fixed, thanks
  42. in src/wallet/coinselection.cpp:219 in 232d3d94fc outdated
    214+ * @param const std::vector<OutputGroup>& utxo_pool The UTXO groups that we are choosing from.
    215+ *        These UTXO groups will be sorted in descending order by effective
    216+ *        value, with lower weight preferred as a tie-breaker.
    217+ * @param const CAmount& selection_target This is the minimum amount that we need for the transaction without
    218+ *        considering change.
    219+ * @param CAmount& change_target The minimum budget for creating a change output that we add to the selection_target.
    


    kashifs commented at 5:47 pm on December 10, 2023:

    this should read:

    @param CAmount change_target The minimum budget for creating a change output that we add to the selection_target.


    brunoerg commented at 8:33 pm on December 11, 2023:
    perhaps change_target could be const CAmount&?

    kashifs commented at 11:45 pm on December 13, 2023:
    Perhaps. That would make it consistent with const CAmount& selection_target. It’s my understanding that the documentation should exactly match the function definition. Is there another way to look at this?

    murchandamus commented at 4:13 pm on December 22, 2023:
    Thanks, I’ve amended the documentation
  43. kashifs commented at 8:31 pm on December 17, 2023: contributor
    Concept ACK ef8ed8
  44. DrahtBot requested review from S3RK on Dec 17, 2023
  45. DrahtBot requested review from jonatack on Dec 17, 2023
  46. achow101 referenced this in commit e3847f7ac4 on Dec 20, 2023
  47. murchandamus force-pushed on Dec 22, 2023
  48. murchandamus commented at 4:54 pm on December 22, 2023: contributor
    Rebased and addressed comments by @kashifs. Thanks for the thorough read, @kashifs.
  49. DrahtBot added the label CI failed on Dec 22, 2023
  50. DrahtBot removed the label CI failed on Dec 22, 2023
  51. in src/wallet/coinselection.cpp:198 in 73b87bacf6 outdated
    193+ * searches for the minimum-weight input set producing a change output. The
    194+ * algorithm is closely related to the Branch and Bound algorithm, but will
    195+ * produce a transaction with a change output instead of aiming to create a
    196+ * changeless transaction.
    197+ *
    198+ * This implementation produces a new input set in every loop iteration. It
    


    sipa commented at 9:11 pm on December 29, 2023:
    Perhaps this explanation of the algorithm belongs more inside the body of the function than at the top, as it’s explaining the implementation and not the interface?

    murchandamus commented at 8:28 pm on January 2, 2024:
    I have reduced the description here to the interface and will add the remaining explanations to the corresponding spots of the function.
  52. in src/wallet/coinselection.cpp:201 in 73b87bacf6 outdated
    196+ * changeless transaction.
    197+ *
    198+ * This implementation produces a new input set in every loop iteration. It
    199+ * uses three operations to transition between input set candidates:
    200+ *
    201+ * • ADD: select the UTXO group at the current index into the input set
    


    sipa commented at 9:12 pm on December 29, 2023:

    It took me a while to figure out what the intent of each operation is. It may be worthwhile to document that here too. My belief is:

    • ADD: explore the current prefix further (by selecting the first undecided transaction, ignoring equal-value transactions if the last one was unselected).
    • SHIFT: skip the current prefix (just the selection branch).
    • CUT: skip the current prefix (both selection and non-selection branches).

    It may also be helpful to explain for each of the operations how the “current index” is affected by them.


    murchandamus commented at 9:29 pm on January 2, 2024:
    I have completely rewritten my description. I hope it’s clearer now.
  53. murchandamus commented at 9:30 pm on December 29, 2023: contributor

    In a simulation comparing the master branch with the CoinGrinder branch playing through a scenario that performs 5 005 payments with feerates sampled from 2023, CoinGrinder reduces the total transaction fees by 9.5%.

    Excerpts from the result table:

    Branch Mean UTXO count Total Fees paid Change outputs created Solutions found by
    master 165.36 0.44500219 3020 knapsack: 610 ; srd: 2414 ; bnb: 1981
    CoinGrinder 230.68 0.40248296 3110 knapsack: 520 ; bnb: 1894 ; cg: 1382 ; srd: 1209

    As can be seen, in this scenario in which the vast majority of transactions are created above the long-term feerate estimate, CoinGrinder significantly reduces the overall fee expenditures at the cost of creating change outputs slightly more often, and maintaining a larger UTXO pool. We do see the wallet’s UTXO pool quickly contract whenever the feerates are reduced to lower levels: image

    We can see in a second simulation that uses the same payment scenario but a series of much lower feerates (“Peak and tail”) that the UTXO pool size is only slightly elevated when a larger portion of the transactions is performed at feerates below the long-term feerate estimate. We see that the addition of CoinGrinder reduces the total fees by about 4% due to better performance in the infrequent high feerate payments.

    Branch Mean UTXO count Total Fees paid Change outputs created Solutions found by
    master 32.70 0.10775743 4556 knapsack: 3636 ; srd: 920 ; bnb: 449
    CoinGrinder 34.31 0.10326806 4538 knapsack: 3593 ; srd: 554 ; bnb: 467; cg: 391

    Please see this Delving Bitcoin thread for the complete description of the simulation and results.

  54. in src/wallet/coinselection.cpp:338 in a7e3ad5efe outdated
    333+            curr_amount += utxo.GetSelectionAmount();
    334+            curr_weight += utxo.m_weight;
    335+            curr_selection.push_back(utxo_pool_index);
    336+            next_op = operations::add;
    337+
    338+            // Depending on evaluation of current selection, ADD next, SHIFT latest, or CUT latest and shift prior
    


    sipa commented at 10:08 pm on December 29, 2023:
    Given that any of the {add, shift, cut} states ends with executing an add, and the end of the add operation (the block of code on this line and below) determines what the next iteration will be, I think it would be more natural to move this block to the beginning of the while loop, before the switch case. So then it becomes a processing loop that in every iteration decides what it will be doing (cut, shift, or add), without state carried between iterations? This doesn’t work for “done”, but that could be a boolean instead.

    murchandamus commented at 8:12 pm on January 8, 2024:
    I’ve completely rewritten CoinGrinder to implement this suggestion.
  55. in src/wallet/coinselection.cpp:261 in a7e3ad5efe outdated
    256+    CAmount total_available = 0;
    257+    int min_group_weight = std::numeric_limits<int>::max();
    258+    size_t i = utxo_pool.size();
    259+    while (i > 0) {
    260+        --i;
    261+        lookahead.at(i) = total_available;
    


    sipa commented at 10:15 pm on December 29, 2023:
    I’d avoid the std::vector::at everywhere and use std::vector::operator[] instead, as at involves conditional branches to detect out-of-bounds access (well-predicted ones, but for a tight loop like this I do expect it to still be impactful). It’s generally better to crash than to have UB due to out-of-bounds access, but inside performance-critical algorithms I think it’s worth the extra review effort to make sure there are no out-of-bounds accesses.

    murchandamus commented at 8:11 pm on January 8, 2024:
    Thanks, I replaced all the std::vector::at with std::vector::operator[].
  56. in src/wallet/coinselection.cpp:321 in a7e3ad5efe outdated
    316+            if (utxo_pool_index > 0
    317+                    && (curr_selection.empty() || curr_selection.back() != utxo_pool_index - 1)
    318+                    && utxo_pool.at(utxo_pool_index - 1).GetSelectionAmount() == utxo_pool.at(utxo_pool_index).GetSelectionAmount()
    319+                    && utxo_pool.at(utxo_pool_index - 1).fee == utxo_pool.at(utxo_pool_index).fee) {
    320+                if (utxo_pool_index < utxo_pool.size() - 1) {
    321+                    // Skip if previous UTXO is equivalent and unselected
    


    sipa commented at 10:54 pm on December 29, 2023:
    One complication with the “move decision for add/shift/cut to beginning of loop” approach is that this skipping doesn’t work anymore as-is. I’d suggest turning it into a while loop that increments utxo_pool_index until the skip condition is no longer satisfied.
  57. murchandamus commented at 3:10 pm on January 4, 2024: contributor
    After discussing Pieter’s review comments further with him, I decided to restructure the algorithm proposed in this PR. I’m returning the PR to draft for a few days while I rewrite the commits.
  58. murchandamus marked this as a draft on Jan 4, 2024
  59. murchandamus force-pushed on Jan 8, 2024
  60. murchandamus commented at 9:14 pm on January 8, 2024: contributor

    Addressed @sipa’s comments, and completely restructured the PR. Added two more tests, and counting of the number of attempts used in CoinGrinder to measure the optimizations.

    Ready for review

  61. murchandamus marked this as ready for review on Jan 8, 2024
  62. murchandamus force-pushed on Jan 8, 2024
  63. DrahtBot added the label CI failed on Jan 8, 2024
  64. murchandamus force-pushed on Jan 8, 2024
  65. murchandamus commented at 10:59 pm on January 8, 2024: contributor

    Mea culpa. Fixed the linter issue.

    Ready for review

  66. DrahtBot removed the label CI failed on Jan 8, 2024
  67. murchandamus force-pushed on Jan 9, 2024
  68. murchandamus commented at 0:31 am on January 9, 2024: contributor
    Added missing test title in overview of CoinGrinder tests
  69. in src/wallet/coinselection.cpp:343 in 8858f49880 outdated
    345-                // Exhausted search space before running into attempt limit
    346-                result.SetSelectionsEvaluated(curr_try);
    347-                result.SetAlgoCompleted(true);
    348-                break;
    349+        while (!is_done && should_shift) {
    350+            if (should_shift) {
    


    sipa commented at 4:23 am on January 9, 2024:
    I believe this condition is always true (since it’s inside a while (... && should_shift)).

    murchandamus commented at 2:22 pm on January 9, 2024:
    Ah, oops. Yes of course.
  70. in src/wallet/coinselection.cpp:340 in 840beb3f8f outdated
    336@@ -336,6 +337,9 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    337                 best_selection_weight = curr_weight;
    338                 best_selection_amount = curr_amount;
    339             }
    340+        } else if (!best_selection.empty() && curr_weight + min_tail_weight[curr_selection.back()] * std::ceil((selection_target + change_target - curr_amount) / utxo_pool[curr_selection.back()].GetSelectionAmount()) > best_selection_weight) {
    


    sipa commented at 4:35 am on January 9, 2024:

    I don’t think this std::ceil is doing anything. The / operator is an integer division here, which rounds down. Applying std::ceil to that just converts it to a floating point number. I don’t think this is incorrect, as rounding down is just more conservative, but it’s probably not what you intend.

    To compute $\lceil \frac{a}{b} \rceil$, you can use (a+b-1)/b.

    Also: I don’t think best_selection.empty() is possible at this point; a UTXO was just added. EDIT: I was confusing best_selection with curr_selection.


    murchandamus commented at 6:51 pm on January 9, 2024:

    Thanks, I’ve adopted your proposed change. After more discussion, I realized why this is not correct and dropped the std::ceil(…) from my prior version.

    For context on the !best_selection.empty()): we need to check whether we have found any solution so far, as we would otherwise no longer trigger the “max weight exceeded” failure message.

  71. in src/wallet/coinselection.cpp:300 in 191a6e4d10 outdated
    296@@ -292,7 +297,10 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    297         ++curr_try;
    298 
    299         // EVALUATE current selection, default to EXPLORING the inclusion branch further, else do exactly one SHIFT or CUT.
    300-        if (curr_weight > max_weight) {
    301+        if (curr_amount + lookahead[curr_selection.back()] < selection_target + change_target) {
    


    sipa commented at 4:40 am on January 9, 2024:
    You’re using curr_selection.back() several times in the evaluation logic here; would it make sense to have a auto added_utxo = next_utxo++; variable in the select logic, and then use added_utxo everywhere instead of curr_selection.back()? That makes it also clearer that you don’t need to test for curr_selection.empty().

    murchandamus commented at 2:15 pm on January 9, 2024:

    Good idea.

    I’ve added auto curr_tail = curr_selection.back(); to reference that instead throughout the evaluation block.

  72. in src/wallet/coinselection.cpp:299 in 191a6e4d10 outdated
    296@@ -292,7 +297,10 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    297         ++curr_try;
    298 
    299         // EVALUATE current selection, default to EXPLORING the inclusion branch further, else do exactly one SHIFT or CUT.
    


    sipa commented at 4:41 am on January 9, 2024:
    Since the exploring will always happen (after SHIFT/CUT, possible), perhaps formulate it as “EVALUATE current selection to see whether we can SHIFT or CUT the current selection before EXPLORING further”?

    murchandamus commented at 1:33 pm on January 9, 2024:

    Thanks, I went with

    // EVALUATE current selection: check for solutions and determine whether we can CUT or SHIFT before EXPLORING further

  73. in src/wallet/coinselection.cpp:361 in 8858f49880 outdated
    363+
    364+            // After SHIFTing to an omission branch, the `next_utxo` might have the same value and same weight as the
    365+            // UTXO we just omitted (i.e. it is a "clone"). If so, selecting `next_utxo` would produce an equivalent
    366+            // selection as one we previously evaluated. In that case, increment `next_utxo` until we find a UTXO with a
    367+            // differing amount or weight.
    368+            while (!should_shift && next_utxo > 0
    


    sipa commented at 4:47 am on January 9, 2024:

    I think it’s impossible that next_utxo = 0 here (it was set to curr_selection.back() + 1 above, and the loop only increments it).


    Very nit: when this loop starts, should_shift will always be false, and it can only become true by reaching the else branch below. I think it can be simplified to:

    0while (...
    1        && utxo_pool[next_utxo - 1].fee == utxo_pool[next_utxo],fee) {
    2    if (next_utxo >= utxo_pool.size() - 1) {
    3        // Reached end of UTXO pool skipping clones: SHIFT instead
    4        should_shift = true;
    5        break;
    6    }
    7    // Skip clone: previous UTXO is equivalent and unselected
    8    ++next_utxo;
    9}
    

    murchandamus commented at 2:48 pm on January 9, 2024:

    Thanks, I did some hard staring at this while-loop. I restructured it to use a break as you suggested, and was able to reduce the loop’s condition to:

     0-            while (!should_shift && next_utxo > 0
     1-                    && (curr_selection.empty() || curr_selection.back() != next_utxo - 1)
     2-                    && utxo_pool[next_utxo - 1].GetSelectionAmount() == utxo_pool[next_utxo].GetSelectionAmount()
     3+            while (utxo_pool[next_utxo - 1].GetSelectionAmount() == utxo_pool[next_utxo].GetSelectionAmount()
     4                     && utxo_pool[next_utxo - 1].fee == utxo_pool[next_utxo].fee) {
     5-                if (next_utxo < utxo_pool.size() - 1) {
     6-                    // Skip clone: previous UTXO is equivalent and unselected
     7-                    ++next_utxo;
     8-                } else {
     9+                if (next_utxo >= utxo_pool.size() - 1) {
    10                     // Reached end of UTXO pool skipping clones: SHIFT instead
    11                     should_shift = true;
    12+                    break;
    13                 }
    14+                // Skip clone: previous UTXO is equivalent and unselected
    15+                ++next_utxo;
    16             }
    17         }
    

    Originally, when I introduced clone skipping, I missed that it could only occur after a SHIFT, so there were some unnecessary checks here. Clearly next_utxo has to be greater than 0, since it was just incremented in the SHIFT. We also don’t have to check whether the current_selection.back() is the last element of the UTXO pool, we would have cut if that were the case. So, we only have to check for clones.

    I’m actually just realizing that I might also be able to drop the fee comparison. We already break ties between UTXOs of the same selection amount in favor of less weight, so the preceding UTXO must always have the same fee or a lower fee. While a UTXO with a greater fee (i.e. greater weight) and the same selection amount would not strictly be a clone, it can only lead to a worse input set than the predecessor that we have already evaluated. Gonna ponder and test, will perhaps fix up later today.

  74. murchandamus commented at 7:18 pm on January 9, 2024: contributor
    Addressed @sipa’s comments
  75. murchandamus force-pushed on Jan 9, 2024
  76. DrahtBot added the label CI failed on Jan 9, 2024
  77. DrahtBot commented at 9:33 pm on January 9, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is 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.

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

    Debug: https://github.com/bitcoin/bitcoin/runs/20315937460

  78. murchandamus force-pushed on Jan 9, 2024
  79. DrahtBot removed the label CI failed on Jan 10, 2024
  80. in src/wallet/coinselection.cpp:244 in e38c150f6d outdated
    239+     *
    240+     * Each iteration of CoinGrinder starts by selecting the `next_utxo` and evaluating the current selection. We
    241+     * use three state transactions to progress from the current selection to the next promising selection:
    242+     *
    243+     * - EXPLORE inclusion branch: We do not have sufficient funds yet and the lookahead promises sufficient funds to
    244+     *                             reach the target. We continue with the direct successor as our `next_utxo` for the
    


    murchandamus commented at 2:26 am on January 11, 2024:
    0     *                             reach the target. We continue with the direct successor of our `next_utxo` for the
    

    murchandamus commented at 3:16 am on January 12, 2024:
    Rephrased, especially given that there is no lookahead yet
  81. in src/wallet/coinselection.cpp:300 in e38c150f6d outdated
    295+        if (curr_weight > max_weight) {
    296+            // max_weight exceeded: SHIFT
    297+            max_tx_weight_exceeded = true;
    298+            should_shift  = true;
    299+        } else if (curr_amount >= selection_target + change_target) {
    300+            // Potential solution, adding more weight cannot be better: SHIFT
    


    murchandamus commented at 2:30 am on January 11, 2024:
    0            // Success, adding more weight cannot be better: SHIFT
    

    murchandamus commented at 3:19 am on January 12, 2024:
    Fixed
  82. in src/wallet/coinselection.cpp:28 in e38c150f6d outdated
    24@@ -25,7 +25,7 @@ static util::Result<SelectionResult> ErrorMaxWeightExceeded()
    25                          "Please try sending a smaller amount or manually consolidating your wallet's UTXOs")};
    26 }
    27 
    28-// Descending order comparator
    29+// Sort by descending (effective) value prefer lower waste on tie
    


    ishaanam commented at 9:19 pm on January 11, 2024:

    In e38c150f6dff886c470ba993c1725112c1ee5093 “coinselection: Add CoinGrinder algorithm “:

    Shouldn’t the comment be updated in the previous commit?


    murchandamus commented at 3:08 am on January 12, 2024:
    Good catch, thanks. Fixed.
  83. in src/wallet/spend.cpp:713 in e38c150f6d outdated
    708@@ -709,6 +709,13 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
    709         results.push_back(*knapsack_result);
    710     } else append_error(knapsack_result);
    711 
    712+    if (coin_selection_params.m_effective_feerate > CFeeRate{3 * coin_selection_params.m_long_term_feerate}) { // Minimize input set for feerates of at least 3×LTFRE (default: 30 ṩ/vB+)
    713+        if (auto cg_result{CoinGrinder(groups.positive_group, nTargetValue, coin_selection_params.m_min_change_target, max_inputs_weight)}) {
    


    ishaanam commented at 9:31 pm on January 11, 2024:

    In e38c150f6dff886c470ba993c1725112c1ee5093 “coinselection: Add CoinGrinder algorithm”:

    The comment describing m_min_change_target in CoinSelectionParams should be updated, because currently it describes this variable as being used in Knapsack solver.


    murchandamus commented at 3:27 am on January 12, 2024:
    Good point, thanks for pointing that out.
  84. in src/wallet/coinselection.cpp:456 in e38c150f6d outdated
    301+            should_shift  = true;
    302+            if (curr_weight < best_selection_weight || curr_amount < best_selection_amount) {
    303+                best_selection = curr_selection;
    304+                best_selection_weight = curr_weight;
    305+                best_selection_amount = curr_amount;
    306+            }
    


    ishaanam commented at 9:57 pm on January 11, 2024:

    In e38c150f6dff886c470ba993c1725112c1ee5093 “coinselection: Add CoinGrinder algorithm”:

    Why do the current and selection amounts matter if they would both result in change? Shouldn’t the best selection only be updated if the current selection’s waste is less than the best selection’s waste?


    murchandamus commented at 3:24 am on January 12, 2024:

    Thanks, starting a few commits later we will SHIFT when curr_weight exceeds the best_selection_weight, so the second part of this condition can only trigger on same weight. Since we do not have the SHIFT criteria here yet, this is a bug. Fixed by explicitly checking that weight is tied.

    My argument for preferring an input set with less funds is that the wallet’s confirmed balance is reduced less and therefore the wallet has more confirmed liquidity (that is not constrained by e.g. needing to bump a lower feerate parent transaction) in case this transaction doesn’t get confirmed immediately.

  85. ishaanam commented at 10:28 pm on January 11, 2024: contributor

    Concept ACK

    I’m still reviewing this PR, but I have left some initial review comments.

  86. murchandamus force-pushed on Jan 12, 2024
  87. murchandamus commented at 3:42 pm on January 12, 2024: contributor

    Addressed Ishaana’s review

    Added a test with a pattern of mixed weight inputs to highlight the improvements from optimizations.

  88. DrahtBot added the label CI failed on Jan 12, 2024
  89. DrahtBot commented at 4:38 pm on January 12, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is 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.

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

    Debug: https://github.com/bitcoin/bitcoin/runs/20430984764

  90. murchandamus force-pushed on Jan 12, 2024
  91. murchandamus commented at 7:59 pm on January 12, 2024: contributor
    I’ve added another fuzz target that validates the output of CoinGrinder against a bruteforce search for the smallest weight input set.
  92. murchandamus force-pushed on Jan 12, 2024
  93. murchandamus commented at 9:58 pm on January 12, 2024: contributor
    SFFO is such a pain in the neck.
  94. DrahtBot removed the label CI failed on Jan 12, 2024
  95. murchandamus force-pushed on Jan 12, 2024
  96. murchandamus commented at 11:12 pm on January 12, 2024: contributor
    Fixed bug discovered with the optimality fuzz test: opt: Skip over barren combinations of tiny UTXOs, incorrectly was set to cut, but this could be premature in case that the last selected UTXO was heavier than later UTXOs in the pool.
  97. murchandamus commented at 1:56 am on January 13, 2024: contributor
    CI failure is unrelated, see https://github.com/bitcoin/bitcoin/pull/29243
  98. doc: Document max_weight on BnB aaee65823c
  99. opt: Tie-break UTXO sort by waste for BnB
    Since we are searching for the minimal waste, we sort UTXOs with equal
    effective value by ascending waste to be able to cut barren branches
    earlier.
    89d0956643
  100. murchandamus force-pushed on Jan 15, 2024
  101. murchandamus commented at 2:11 pm on January 15, 2024: contributor
    Rebased on fix for CI issue
  102. DrahtBot added the label CI failed on Jan 18, 2024
  103. DrahtBot removed the label CI failed on Jan 23, 2024
  104. in src/wallet/coinselection.cpp:211 in 180676ed7b outdated
    206+    std::sort(utxo_pool.begin(), utxo_pool.end(), descending);
    207+
    208+    // Check that there are sufficient funds
    209+    CAmount total_available = 0;
    210+    for (const OutputGroup& utxo : utxo_pool) {
    211+        // Assert UTXOs with negative effective value have been filtered
    


    sipa commented at 3:54 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    Nit: non-positive effective value (0 is not allowed either, apparently)


    murchandamus commented at 9:21 pm on January 28, 2024:
    Good catch, fixed.
  105. in src/wallet/coinselection.cpp:215 in 180676ed7b outdated
    210+    for (const OutputGroup& utxo : utxo_pool) {
    211+        // Assert UTXOs with negative effective value have been filtered
    212+        assert(utxo.GetSelectionAmount() > 0);
    213+        total_available += utxo.GetSelectionAmount();
    214+    }
    215+    if (total_available < selection_target + change_target) {
    


    sipa commented at 3:57 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    Not as much in this commit yet, but in the PR overall, almost everywhere the sum selection_target + change_target is used. Maybe it makes sense to have a const auto total_target = selection_target + change_target, and use total_target everywhere?


    murchandamus commented at 9:22 pm on January 28, 2024:
    Sure, makes sense.
  106. in src/wallet/coinselection.cpp:518 in 180676ed7b outdated
    338+            should_shift  = false;
    339+        }
    340+    }
    341+
    342+    if (best_selection.empty()) {
    343+        return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error();
    


    sipa commented at 4:19 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    I don’t believe it’s actually possible to not have a solution at this point, unless the maximum weight is exceeded because the two ways of achieving that are:

    • Not enough funds (but that is checked early in the function)
    • A solution exists, but wasn’t found due to computation limits. However, with a UTXO set of $n$ elements/groups, a solution should always be found within $n$ iterations (and I think the number of iterations should be set at least as high as the number of UTXOs anyway).

    If that is correct, then we can:

    • Drop max_tx_weight_exceeded.
    • Drop this conditional, and make it if (best_selection.empty()) return ErrorMaxWeightExceeded();
    • Drop best_selection_weight, and instead reuse max_weight for that.

    Patch:

     0diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp
     1index 63ba39cd0a0..7c48fc4e0f0 100644
     2--- a/src/wallet/coinselection.cpp
     3+++ b/src/wallet/coinselection.cpp
     4@@ -242,8 +242,6 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
     5     // CoinGrinder tracks selection via the indices of the currently selected UTXOs
     6     std::vector<size_t> best_selection;
     7     CAmount best_selection_amount = MAX_MONEY;
     8-    int best_selection_weight = max_weight; // Tie is fine, because we prefer lower selection amount
     9-    bool max_tx_weight_exceeded = false;
    10 
    11     std::vector<size_t> curr_selection;
    12     CAmount curr_amount = 0;
    13@@ -318,9 +316,7 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    14         if (curr_amount + lookahead[curr_tail] < selection_target + change_target) {
    15             // Insufficient funds with lookahead: CUT
    16             should_cut = true;
    17-        } else if (curr_weight > best_selection_weight) {
    18-            // best_selection_weight is initialized to max_weight
    19-            if (curr_weight > max_weight) max_tx_weight_exceeded = true;
    20+        } else if (curr_weight > max_weight) {
    21             // Worse weight than best solution. More UTXOs only increase weight:
    22             // CUT if last selected group had minimal weight, else SHIFT
    23             if (utxo_pool[curr_tail].m_weight <= min_tail_weight[curr_tail]) {
    24@@ -331,13 +327,13 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    25         } else if (curr_amount >= selection_target + change_target) {
    26             // Success, adding more weight cannot be better: SHIFT
    27             should_shift  = true;
    28-            if (curr_weight < best_selection_weight || (curr_weight == best_selection_weight && curr_amount < best_selection_amount)) {
    29+            if (curr_weight < max_weight || (curr_weight == max_weight && curr_amount < best_selection_amount)) {
    30                 // New lowest weight, or same weight with fewer funds tied up
    31                 best_selection = curr_selection;
    32-                best_selection_weight = curr_weight;
    33+                max_weight = curr_weight;
    34                 best_selection_amount = curr_amount;
    35             }
    36-        } else if (!best_selection.empty() && curr_weight + min_tail_weight[curr_tail] * ((selection_target + change_target - curr_amount) / utxo_pool[curr_tail].GetSelectionAmount()) > best_selection_weight) {
    37+        } else if (!best_selection.empty() && curr_weight + min_tail_weight[curr_tail] * ((selection_target + change_target - curr_amount) / utxo_pool[curr_tail].GetSelectionAmount()) > max_weight) {
    38             // Compare minimal tail weight and last selected amount with the amount missing to gauge whether a better weight is still possible.
    39             if (utxo_pool[curr_tail].m_weight <= min_tail_weight[curr_tail]) {
    40                 should_cut = true;
    41@@ -397,9 +393,7 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    42         }
    43     }
    44 
    45-    if (best_selection.empty()) {
    46-        return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error();
    47-    }
    48+    if (best_selection.empty()) ErrorMaxWeightExceeded();
    49 
    50     for (const size_t& i : best_selection) {
    51         result.AddInput(utxo_pool[i]);
    

    murchandamus commented at 10:05 pm on January 28, 2024:
    This seems correct to me, and I’ve fuzzed it for five hours without failure for good measure. This changes a number of commits, in different ways, still pondering whether it’s best to put into the first implementation or to amend the commit that optimizes handling of max_weight. Will revisit.

    murchandamus commented at 7:44 pm on January 29, 2024:
    I have decided to defer this for the moment, since I do appreciate the clarity of distinguishing between the best_selection_weight and max_weight, and I anticipate that I will have to revisit this in the context of #29264 soon anyway.

    sipa commented at 1:39 pm on January 31, 2024:
    Sounds good.
  107. in src/wallet/coinselection.cpp:220 in 180676ed7b outdated
    215+    if (total_available < selection_target + change_target) {
    216+        // Insufficient funds
    217+        return util::Error();
    218+    }
    219+
    220+    // CoinGrinder tracks selection via the indices of the currently selected UTXOs
    


    sipa commented at 5:33 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    Perhaps add a one-line comment for each of the 8 variables here.


    murchandamus commented at 9:33 pm on January 28, 2024:

    Added descriptions

     0// The current selection and the best input set found so far, stored as the utxo_pool indices of the UTXOs forming them
     1std::vector<size_t> curr_selection;
     2std::vector<size_t> best_selection;
     3
     4// The currently selected effective amount, and the effective amount of the best selection so far
     5CAmount curr_amount = 0;
     6CAmount best_selection_amount = MAX_MONEY;
     7
     8// The weight of the currently selected input set, and the weight of the best selection
     9int curr_weight = 0;
    10int best_selection_weight = std::numeric_limits<int>::max();
    11
    12// Whether the input sets generated during this search have exceeded the maximum transaction weight at any point
    13bool max_tx_weight_exceeded = false;
    14
    15// Index of the next UTXO to consider in utxo_pool
    16size_t next_utxo = 0;
    
  108. in src/wallet/coinselection.cpp:241 in 180676ed7b outdated
    236+     *
    237+     * We can never find a new solution by deselecting a UTXO, because we then revisit a previously evaluated
    238+     * selection. Therefore, we only need to check whether we found a new solution _after adding_ a new UTXO.
    239+     *
    240+     * Each iteration of CoinGrinder starts by selecting the `next_utxo` and evaluating the current selection. We
    241+     * use three state transactions to progress from the current selection to the next promising selection:
    


    sipa commented at 5:34 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    “state transactions” -> “state transitions”?


    murchandamus commented at 9:33 pm on January 28, 2024:
    Hah, thanks, fixed.
  109. in src/wallet/coinselection.cpp:254 in 180676ed7b outdated
    249+     *                                 Evaluation: EXPLORE, next_utxo: 8
    250+     *                                 Next Selection: {0, 5, 7, 8}
    251+     *
    252+     * - SHIFT to omission branch: Adding more UTXOs to the current selection cannot produce a solution that is better
    253+     *                             than the current best, e.g. the max weight is exceeded or the current selection has
    254+     *                             exceeded reached the target.
    


    sipa commented at 5:35 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    exceeded or reached?


    murchandamus commented at 9:37 pm on January 28, 2024:
    Thanks, fixed up the sentence.
  110. in src/wallet/coinselection.cpp:256 in 180676ed7b outdated
    251+     *
    252+     * - SHIFT to omission branch: Adding more UTXOs to the current selection cannot produce a solution that is better
    253+     *                             than the current best, e.g. the max weight is exceeded or the current selection has
    254+     *                             exceeded reached the target.
    255+     *                             We designate our `next_utxo` the one after the tail of our current selection, then
    256+     *                             deselected the tail of our current selection.
    


    sipa commented at 5:35 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    deselected -> deselect?

  111. in src/wallet/test/fuzz/coinselection.cpp:89 in 168957a164 outdated
    83+    std::vector<COutput> utxo_pool;
    84+
    85+    const CAmount target{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY)};
    86+
    87+    FastRandomContext fast_random_context{ConsumeUInt256(fuzzed_data_provider)};
    88+    CoinSelectionParams coin_params{fast_random_context};
    


    sipa commented at 6:07 pm on January 27, 2024:

    In commit “fuzz: Add CoinGrinder fuzz target”

    coin_params.m_min_change_target is left at 0 here. Perhaps leave a comment about why that is ok?


    murchandamus commented at 10:21 pm on January 28, 2024:
    I dropped change_target below and instead assigned coin_params.m_min_change_target, with a descriptive comment, and passed that below to the other algorithms.
  112. in src/wallet/test/fuzz/coinselection.cpp:177 in 4b488b6d08 outdated
    169+    }
    170+    size_t num_utxos = utxo_pool.size();
    171+    assert(num_utxos <= max_utxos);
    172+
    173+    std::vector<OutputGroup> group_pos;
    174+    GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/true, group_pos);
    


    sipa commented at 7:15 pm on January 27, 2024:

    In commit “wallet: Add CoinGrinder coin selection algorithm”:

    Would it make sense to construct the group_pos variable directly from the fuzzer? I think you only need to populate its m_weight and m_effective_value. As you don’t use anything but group_pos, target, and change_target in the actual test, all fuzz information you use to construct other things is effectively wasted.


    murchandamus commented at 11:25 pm on January 28, 2024:
    That would be good, but that would take more time than I have right now. Will leave this open for the moment.

    murchandamus commented at 7:42 pm on January 29, 2024:

    I cleaned up the Optimality fuzz test a bit:

    • I have removed multiple CoinSelectionParams from being fuzzed that are not relevant to testing CoinGrinder’s optimality (SFFO, LTFRE, all change cost related quantities except min_change_target)
    • I now fuzz min_change_target
    • I just create up to 16 UTXOs that are each a separate OutputGroup
    • I now first run the brute force solution, then set a fuzzed max_weight greater than the best_weight and check that CoinGrinder finds the optimal solution, then set a lower max_weight and check that CoinGrinder provides no solution
    • I generate target to be below max_spendable instead of returning early
  113. in src/wallet/test/fuzz/coinselection.cpp:178 in 4b488b6d08 outdated
    173+    std::vector<OutputGroup> group_pos;
    174+    GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/true, group_pos);
    175+    size_t num_groups = group_pos.size();
    176+    assert(num_groups <= num_utxos);
    177+
    178+    // Run coinselection algorithms
    


    sipa commented at 7:17 pm on January 27, 2024:

    In commit “wallet: Add CoinGrinder coin selection algorithm”:

    Only one coin selection algorithm is run here (unless you count the brute forcing).

  114. in src/wallet/test/fuzz/coinselection.cpp:179 in 4b488b6d08 outdated
    174+    GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/true, group_pos);
    175+    size_t num_groups = group_pos.size();
    176+    assert(num_groups <= num_utxos);
    177+
    178+    // Run coinselection algorithms
    179+    CAmount change_target{CHANGE_LOWER + coin_params.m_change_fee}; // In order to ensure that it’s comparable to SRD, we must use the same change_target
    


    sipa commented at 7:17 pm on January 27, 2024:

    In commit “wallet: Add CoinGrinder coin selection algorithm”:

    SRD is not used in this test, so I think you can construct change_target from fuzz data directly.


    murchandamus commented at 10:31 pm on January 28, 2024:

    Okay, replaced with

    0coin_params.m_min_change_target = coin_params.m_change_fee + fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(CHANGE_LOWER, CHANGE_UPPER);
    
  115. in src/wallet/test/fuzz/coinselection.cpp:180 in 4b488b6d08 outdated
    175+    size_t num_groups = group_pos.size();
    176+    assert(num_groups <= num_utxos);
    177+
    178+    // Run coinselection algorithms
    179+    CAmount change_target{CHANGE_LOWER + coin_params.m_change_fee}; // In order to ensure that it’s comparable to SRD, we must use the same change_target
    180+    auto result_cg = CoinGrinder(group_pos, target, change_target, MAX_STANDARD_TX_WEIGHT);
    


    sipa commented at 7:20 pm on January 27, 2024:

    In commit “wallet: Add CoinGrinder coin selection algorithm”:

    I think it would be better if you’d also construct max_weight from the fuzz data (and then take it into account in the brute force loop below).

    As an alternative, run the brute force loop first (without any weight maximum restriction), and then run CG twice, once with a (fuzz-constructed) max_weight >= the brute force best solution (and check that CG finds it) and once with a (fuzz-constructed) max_weight < the brute force best solution (and check that CG does not find it).


    murchandamus commented at 11:22 pm on January 28, 2024:
    Great idea. I’ve amended the fuzz test to exit early if there are insufficient funds, otherwise brute force the solution, and then run CG twice.
  116. in src/wallet/coinselection.cpp:212 in 180676ed7b outdated
    207+
    208+    // Check that there are sufficient funds
    209+    CAmount total_available = 0;
    210+    for (const OutputGroup& utxo : utxo_pool) {
    211+        // Assert UTXOs with negative effective value have been filtered
    212+        assert(utxo.GetSelectionAmount() > 0);
    


    sipa commented at 7:23 pm on January 27, 2024:

    In commit “coinselection: Add CoinGrinder algorithm”

    Negative amounts won’t cause a crash or other UB, I believe (the result will just be suboptimal), so it’s probably better to use Assume here instead of assert.


    murchandamus commented at 9:21 pm on January 28, 2024:
    Adopted. Yeah, OutputGroups with a single negative effective value would simply sort last and be skipped per the lookahead, while OutputGroups with an overall positive effective value but containing some negative effective value UTXOs would just cause some additional iterations up to making the result suboptimal.
  117. in src/wallet/coinselection.cpp:342 in 27359ef470 outdated
    338@@ -338,17 +339,33 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    339             should_shift  = true;
    340         }
    341 
    342-        if (should_shift) {
    343+        while (!is_done && should_shift) {
    


    sipa commented at 7:26 pm on January 27, 2024:

    In commit opt: “Skip evaluation of equivalent input sets”

    This !is_done && here seems unnecessary, because a break; is invoked whenever is_done is set to true.


    murchandamus commented at 11:55 pm on January 28, 2024:
    Good catch, thanks.
  118. murchandamus commented at 0:07 am on January 29, 2024: contributor
    Addressed most of @sipa’s feedback except for two things I’m still mulling over
  119. murchandamus force-pushed on Jan 29, 2024
  120. murchandamus force-pushed on Jan 29, 2024
  121. murchandamus commented at 7:44 pm on January 29, 2024: contributor
    All review feedback is addressed, ready for review.
  122. DrahtBot added the label CI failed on Jan 29, 2024
  123. murchandamus commented at 9:02 pm on January 30, 2024: contributor
    The p2p_v2_earlykeyresponse.py test is unrelated.
  124. in src/wallet/coinselection.cpp:466 in 9ccbef5d48 outdated
    313+                best_selection_weight = curr_weight;
    314+                best_selection_amount = curr_amount;
    315+            }
    316+        }
    317+
    318+        if (curr_try >= TOTAL_TRIES) {
    


    sipa commented at 1:48 pm on January 31, 2024:

    I think you should set the number of tries to be at least some multiple of the utxo_pool size, because (a) without it, you can’t guarantee even considering every UTXO, and (b) the runtime of coin selection overall already contains a component proportional to the size of utxo_pool (simply the cost of constructing the pool in the first place) anyway. So I would suggest something like const auto total_tries = max(TOTAL_TRIES_CG, 10 * utxo_pool.size()); or so.

    It may be worth doing this as a follow-up, as other algorithms may benefit from a similar treatment. Still, I’d suggest using a separate constant TOTAL_TRIES_CG, even if you also set it to 100000 for now, because there is no inherent reason why BNB and CG should have the same iteration count limit (their time per iteration, and certainly their “solving power” per iteration, is possibly quite different).


    murchandamus commented at 7:38 pm on January 31, 2024:

    While I agree that CoinGrinder already does work that scales with the size of the UTXO pool, I would argue that CoinGrinder should never need more than 100'000 tries to find a solution. We can only use up to ~1740 inputs in a standard transaction, so if a wallet can’t scrounge up enough funds after traversing 100'000 candidate input sets, I’d say it has other issues.

    Happy to change both of these in a follow-up, but wouldn’t consider it necessary for this PR to land.

  125. in src/wallet/coinselection.cpp:332 in 9ccbef5d48 outdated
    327+        }
    328+
    329+        if (should_cut) {
    330+            // Neither adding to the current selection nor exploring the omission branch of the last selected UTXO can
    331+            // find any solutions. Redirect to exploring Omission branch of the penultimate selected UTXO (i.e. deselect
    332+            // last selected UTXO, then SHIFT)
    


    sipa commented at 1:49 pm on January 31, 2024:
    Did you mean “then EXPLORE” here?

    murchandamus commented at 7:38 pm on January 31, 2024:
    No, SHIFT is deselect + explore and CUT is deselect + deselect + explore, so CUT is deselect + SHIFT.

    murchandamus commented at 1:04 pm on February 1, 2024:
    That said, your comment indicates that this could be phrased better, since it appears to be difficult to follow. Will improve.

    murchandamus commented at 8:22 pm on February 1, 2024:

    Changed as follows:

    0-            // find any solutions. Redirect to exploring Omission branch of the penultimate selected UTXO (i.e. deselect
    1-            // last selected UTXO, then SHIFT)
    2+            // find any solutions. Redirect to exploring the Omission branch of the penultimate selected UTXO (i.e.
    3+            // set `next_utxo` to one after the penultimate selected, then deselect the last two selected UTXOs)
    
  126. in src/wallet/coinselection.cpp:342 in 9ccbef5d48 outdated
    337+
    338+        if (should_shift) {
    339+            // Set `next_utxo` to one after last selected, then deselect last selected UTXO
    340+            if (curr_selection.empty()) {
    341+                // Exhausted search space before running into attempt limit
    342+                result.SetSelectionsEvaluated(curr_try);
    


    sipa commented at 1:59 pm on January 31, 2024:
    This result.SetSelectionsEvaluated(curr_try); break; is repeated in both exit conditions. The result.SetSelectionsEvaluated(curr_try); can be moved outside of the loop I believe.

    murchandamus commented at 7:40 pm on January 31, 2024:
    Yeah, looks correct. I will move result.SetSelectionsEvaluated(curr_try) out of the loop next time I’ll push. I know there are some other reviews in flight right now.

    murchandamus commented at 8:17 pm on February 1, 2024:
    Moved result.SetSelectionsEvaluated(curr_try) after the loop
  127. in src/wallet/spend.cpp:716 in 9ccbef5d48 outdated
    708@@ -709,6 +709,13 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
    709         results.push_back(*knapsack_result);
    710     } else append_error(knapsack_result);
    711 
    712+    if (coin_selection_params.m_effective_feerate > CFeeRate{3 * coin_selection_params.m_long_term_feerate}) { // Minimize input set for feerates of at least 3×LTFRE (default: 30 ṩ/vB+)
    713+        if (auto cg_result{CoinGrinder(groups.positive_group, nTargetValue, coin_selection_params.m_min_change_target, max_inputs_weight)}) {
    714+            cg_result->ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
    715+            results.push_back(*cg_result);
    716+        } else append_error(cg_result);
    


    sipa commented at 2:01 pm on January 31, 2024:

    Coding style nit:

    0} else {
    1    append_error(cg_result);
    2}
    

    (conditional without braces/indent only allowed if it’s of the if (condition) statement; form)


    murchandamus commented at 7:41 pm on January 31, 2024:
    Okay, will fix when I push next time.

    murchandamus commented at 8:18 pm on February 1, 2024:
    Fixed
  128. in src/wallet/test/fuzz/coinselection.cpp:145 in e8115ac340 outdated
    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+    coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
    


    sipa commented at 3:18 pm on January 31, 2024:

    I’ve experimented a bit with how far the range of inputs can be extended to cover extreme values (not because those extreme values are ones we care about in practice, but because they’re more likely to expose bugs that do affect practical inputs):

     0--- a/src/wallet/test/fuzz/coinselection.cpp
     1+++ b/src/wallet/test/fuzz/coinselection.cpp
     2@@ -142,29 +142,25 @@ FUZZ_TARGET(coin_grinder_is_optimal)
     3     FastRandomContext fast_random_context{ConsumeUInt256(fuzzed_data_provider)};
     4     CoinSelectionParams coin_params{fast_random_context};
     5     coin_params.m_subtract_fee_outputs = false;
     6-    coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)};
     7-    coin_params.m_min_change_target = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY);
     8+    // Set effective feerate up to MAX_MONEY sats per 1000000 vB (21000 BTC per kvB).
     9+    coin_params.m_effective_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, MAX_MONEY)};
    10+    coin_params.m_min_change_target = ConsumeMoney(fuzzed_data_provider);
    11 
    12     // Create some coins
    13-    CAmount total_balance{0};
    14     CAmount max_spendable{0};
    15     int next_locktime{0};
    16     unsigned max_output_groups = 16;
    17     std::vector<OutputGroup> group_pos;
    18     LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), max_output_groups)
    19     {
    20-        const int n_input_bytes{fuzzed_data_provider.ConsumeIntegralInRange<int>(41, 10000)};
    21+        // With maximum m_effective_feerate and n_input_bytes = 1000000, input_fee <= MAX_MONEY.
    22+        const int n_input_bytes{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 1000000)};
    23         // Only make UTXOs with positive effective value
    24         const CAmount input_fee = coin_params.m_effective_feerate.GetFee(n_input_bytes);
    25-        const CAmount amount{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(input_fee + 1, MAX_MONEY)};
    26-        if (total_balance + amount >= MAX_MONEY) {
    27-            break;
    28-        }
    29-
    30+        const CAmount eff_value{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY - max_spendable)};
    31+        const CAmount amount{eff_value + input_fee};
    32         std::vector<COutput> temp_utxo_pool;
    33         AddCoin(amount, /*n_input=*/0, n_input_bytes, ++next_locktime, temp_utxo_pool, coin_params.m_effective_feerate);
    34-        total_balance += amount;
    35-        CAmount eff_value = amount - input_fee;
    36         max_spendable += eff_value;
    37 
    38         auto output_group = OutputGroup(coin_params);
    39@@ -195,9 +191,9 @@ FUZZ_TARGET(coin_grinder_is_optimal)
    40         }
    41     }
    42 
    43-    if (best_weight <= MAX_STANDARD_TX_WEIGHT) {
    44+    if (best_weight < std::numeric_limits<int>::max()) {
    45         // Sufficient funds and acceptable weight: CoinGrinder should find at least one solution
    46-        int high_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(best_weight, MAX_STANDARD_TX_WEIGHT);
    47+        int high_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(best_weight, std::numeric_limits<int>::max());
    48 
    49         auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, high_max_weight);
    50         assert(result_cg);
    51@@ -209,7 +205,7 @@ FUZZ_TARGET(coin_grinder_is_optimal)
    52     }
    53 
    54     // CoinGrinder cannot ever find a better solution than the brute-forced best, or there is none in the first place
    55-    int low_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, std::min(best_weight - 1, MAX_STANDARD_TX_WEIGHT));
    56+    int low_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, best_weight - 1);
    57     auto result_cg = CoinGrinder(group_pos, target, coin_params.m_min_change_target, low_max_weight);
    58     // Max_weight should have been exceeded, or there were insufficient funds
    59     assert(!result_cg);
    

    murchandamus commented at 7:44 pm on January 31, 2024:
    Okay cool, sound great. Will adopt with the next push.

    murchandamus commented at 9:03 pm on February 1, 2024:

    I had to make the following change to ensure that there is a positive effective value:

    0-        const CAmount eff_value{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY - max_spendable)};
    1+        // Ensure that each UTXO has at least an effective value of 1 sat
    2+        const CAmount eff_value{fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(1, MAX_MONEY - max_spendable - max_output_groups + group_pos.size())};
    
  129. sipa commented at 3:25 pm on January 31, 2024: member
    Concept ACK
  130. murchandamus commented at 7:56 pm on January 31, 2024: contributor
    Thanks @sipa, good improvements, will amend when the other review in flight comes back.
  131. in src/wallet/coinselection.cpp:392 in ab7eaaa98b outdated
    387+            }
    388+            next_utxo = curr_selection.back() + 1;
    389+            deselect_last();
    390+            should_shift  = false;
    391+
    392+            // After SHIFTing to an omission branch, the `next_utxo` might have the same value and same weight as the
    


    sipa commented at 7:55 pm on February 1, 2024:

    (From IRL discussion)

    The actually implemented optimization here is actually more powerful than what is described by the comment, because the weight isn’t compared. Due to the fact that that among equal-value utxo groups, the lower weight ones sort first, higher weight ones are even worse, and can also be skipped.


    murchandamus commented at 7:56 pm on February 1, 2024:
    Yep, will update.

    murchandamus commented at 9:13 pm on February 1, 2024:
    Instead of comparing fees, I’m comparing weights here now. I’ll also add a commit to skip larger weights after tie-breaking the sort order with the weight.
  132. in src/wallet/test/coinselector_tests.cpp:1200 in e00d908ce1 outdated
    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+        expected_result.Clear();
    


    sr-gi commented at 8:34 pm on February 1, 2024:

    In commit: test: Add coin_grinder_tests

    I think clearing the expected result here is not necessary. You can get rid of this (and potentially also just move the declaration of expected_result here instead.


    murchandamus commented at 9:38 pm on February 1, 2024:
    Yep, moved declaration down there.
  133. in src/wallet/test/coinselector_tests.cpp:1262 in e00d908ce1 outdated
    1257+            for (int j = 0; j < 100; ++j) {
    1258+                add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400);
    1259+            }
    1260+            return available_coins;
    1261+        });
    1262+        expected_result.Clear();
    


    sr-gi commented at 8:35 pm on February 1, 2024:

    In commit: test: Add coin_grinder_tests

    Same as per point 5)


    murchandamus commented at 9:38 pm on February 1, 2024:
    Done
  134. in src/wallet/coinselection.cpp:213 in b441b2cc02 outdated
    213-    for (const OutputGroup& utxo : utxo_pool) {
    214-        // Assert UTXOs with non-positive effective value have been filtered
    215-        Assume(utxo.GetSelectionAmount() > 0);
    216-        total_available += utxo.GetSelectionAmount();
    217+    size_t i = utxo_pool.size();
    218+    while (i > 0) {
    


    sr-gi commented at 9:14 pm on February 1, 2024:

    In opt: Track remaining effective_value in lookahead:

    I wonder why this gets turned into a descending while loop instead of a descending for loop. Given the total number of iterations is known beforehand a for seems best suited (plus the scope of the counter is reduced to to scope of the loop)


    murchandamus commented at 9:51 pm on February 1, 2024:

    Sure, replaced with:

    0-    size_t i = utxo_pool.size();
    1-    while (i > 0) {
    2-        --i;
    3+    for (int i = utxo_pool.size() - 1 ; i >= 0; --i) {
    
  135. in src/wallet/test/coinselector_tests.cpp:1292 in 89465b0551 outdated
    1287+                // make a 100 unique coins only differing by one sat
    1288+                add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110);
    1289+            }
    1290+            return available_coins;
    1291+        });
    1292+        expected_result.Clear();
    


    sr-gi commented at 9:19 pm on February 1, 2024:

    in test: Exhaust search attempts with tiny UTXOs:

    Same as for cases 5) and 6), clearing is not needed


    murchandamus commented at 9:55 pm on February 1, 2024:
    Absolutely. Fixed.
  136. sr-gi commented at 9:22 pm on February 1, 2024: member

    Concept ACK.

    First pass, I need to dig a bit deeper, but it makes sense to me

  137. murchandamus commented at 10:02 pm on February 1, 2024: contributor

    Addressed feedback by @sipa and @sr-gi.

    I also improved the clone skipping by skipping any UTXOs with same effective value and heavier weight that follow an unselected UTXO.

  138. murchandamus force-pushed on Feb 1, 2024
  139. murchandamus force-pushed on Feb 2, 2024
  140. murchandamus force-pushed on Feb 2, 2024
  141. DrahtBot removed the label CI failed on Feb 2, 2024
  142. in src/wallet/coinselection.cpp:337 in 40f9f7e123 outdated
    223+
    224+    // Calculate lookahead values, min_tail_weights, and check that there are sufficient funds
    225+    CAmount total_available = 0;
    226+    int min_group_weight = std::numeric_limits<int>::max();
    227+    for (size_t i = 0; i < utxo_pool.size(); ++i) {
    228+        size_t index = utxo_pool.size() - 1 - i; // Loop over every element in reverse order
    


    sr-gi commented at 9:23 pm on February 2, 2024:

    Isn’t this equivalent to:

    0for (int i = utxo_pool.size()-1; i >= 0 ; --i) 
    

    I guess it’s a matter of personal taste though, so feel free to disregard


    murchandamus commented at 1:50 pm on February 6, 2024:
    Yes I think that would be equivalent.
  143. murchandamus force-pushed on Feb 4, 2024
  144. murchandamus commented at 9:02 pm on February 4, 2024: contributor
    Added a longer textual explanation on how the algorithm can be thought in terms of the search graph it walks.
  145. in src/wallet/coinselection.cpp:224 in d45da596ac outdated
    219+ *
    220+ *     {} {A} {AB} {ABC} {ABCD} {ABC_} {AB_} {AB_D} {AB__} {A_} {A_C} {A_CD} {A_C_} {A__} {A__D} {A___} {_} {_B} {_BC}
    221+ *     {_BCD} {_BC_} {_B_} {_B_D} {_B__} {__} {__C} {__CD} {__C} {___} {___D} {____}
    222+ *
    223+ * As powersets grow exponentially with the set size, walking the entire tree would quickly get computationally
    224+ * infeasible with growing UTXO pools. We can represent the current state of the search with constant data thanks to
    


    sipa commented at 9:30 pm on February 4, 2024:
    It’s linear, not constant? (curr_selections is $\mathcal{O}(n)$ in the number of UTXOs/groups). I assume that the point you’re trying to make is that it doesn’t scale with the search space (which is exponential in the number of UTXOs), but as-is it can be misunderstood.

    murchandamus commented at 2:41 pm on February 5, 2024:
    Thanks, I’ve improved that sentence.
  146. in src/wallet/coinselection.cpp:234 in d45da596ac outdated
    229+ * visiting {A} and exploring the inclusion branch {AB} and its descendants, the candidate input set in the omission
    230+ * branch {A_} is equivalent to the parent {A} in effective value and weight. While CoinGrinder does need to visit the
    231+ * descendants of the omission branch {A_}, it is unnecessary to evaluate the candidate input set in the omission branch
    232+ * itself. By skipping evaluation of all nodes on an omission branch we reduce the visited nodes to 15:
    233+ *
    234+ *     {} {A} {AB} {ABC} {ABCD} {AB_D} {A_C} {A_CD} {A__D} {_B} {_BC} {_BCD} {_B_D} {__C} {__CD} {__C} {___D} {____}
    


    sipa commented at 9:33 pm on February 4, 2024:
    I think you can drop {}, {____}, and the second {__C} here (you currently have 18, not 15, entries).

    murchandamus commented at 2:48 pm on February 5, 2024:
    Thanks, good catch

    sr-gi commented at 6:16 pm on February 5, 2024:

    I think this should be 16 though, not 15 as the comment claims:

    {} {A} {AB} {ABC} {ABCD} {AB_D} {A_C} {A_CD} {A__D} {_B} {_BC} {_BCD} {_B_D} {__C} {__CD} ~{__C}~{___D} ~{____}~


    murchandamus commented at 1:39 pm on February 6, 2024:
    @sr-gi: I did not count the root node, since we do not add a UTXO in the root node, so I did not count it among the inclusion branch nodes.

    sr-gi commented at 2:22 pm on February 6, 2024:
    Fair
  147. in src/wallet/coinselection.cpp:259 in d45da596ac outdated
    254+ * If a candidate input set in a node has not selected enough to fund a transaction, we continue directly along the next
    255+ * inclusion branch. We call this operation _EXPLORE_. (We go from one inclusion branch to the next inclusion branch:
    256+ * {_B} ⇒ {_BC}.)
    257+ * Further, any prefix that already has selected sufficient effective value to fund the transaction cannot be improved
    258+ * by adding more UTXOs. If for example the candidate input set in {AB} is a valid solution, all potential descendant
    259+ * solutions {ABC}, {ABCD}, and {AB_D} must have a higher weight.
    


    sipa commented at 9:35 pm on February 4, 2024:
    Maybe add, “thus we can SHIFT in this case”.

    murchandamus commented at 2:49 pm on February 5, 2024:
    Added!
  148. in src/wallet/coinselection.cpp:306 in d45da596ac outdated
    286+ *                                                        /                           /             /             /
    287+ * D=[4/2]                                           {A__D}                      {_B_D}        {__CD}        {___D}
    288+ *
    289+ *
    290+ * We implement this tree walk in the following algorithm:
    291+ * 1. Add `next_utxo`
    


    sipa commented at 9:36 pm on February 4, 2024:
    Maybe add that this is the EXPLORE step, or the final addition part of SHIFT/CUT.

    murchandamus commented at 2:50 pm on February 5, 2024:
    Added
  149. in src/wallet/coinselection.cpp:293 in d45da596ac outdated
    288+ *
    289+ *
    290+ * We implement this tree walk in the following algorithm:
    291+ * 1. Add `next_utxo`
    292+ * 2. Evaluate candidate input set
    293+ * 3. Optionally backtrack, and determine `next_utxo`
    


    sipa commented at 9:37 pm on February 4, 2024:
    Maybe add that this corresponds to deciding whether SHIFT or CUT are appropriate.

    murchandamus commented at 3:00 pm on February 5, 2024:
    Rephrased to mention again all three possible state transitions with example
  150. in src/wallet/test/coinselector_tests.cpp:1116 in 91eae8dbab outdated
    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+
    1115+    FastRandomContext rand;
    


    sipa commented at 9:47 pm on February 4, 2024:
    You can use the (test) global g_insecure_rand_ctx instead of creating your own RNG.

    murchandamus commented at 2:03 pm on February 6, 2024:
    I think that applies to pretty much all coinselection tests, so I’ll leave that for a follow-up at the moment.
  151. in src/wallet/coinselection.h:333 in cc134b6b00 outdated
    329@@ -330,6 +330,8 @@ struct SelectionResult
    330     bool m_use_effective{false};
    331     /** The computed waste */
    332     std::optional<CAmount> m_waste;
    333+    /** False if algorithm was cut short by hitting limit of attempts and solution is non-optimal */
    


    sipa commented at 9:49 pm on February 4, 2024:
    Document that this only holds for CG, or alternatively, set it in other algorithms were it applies too (only BNB, I assume)?

    murchandamus commented at 3:06 pm on February 5, 2024:
    I had also set it for BnB originally, but then I got the complaint that it wasn’t being read anywhere yet. I’ll add it when I update BnB accordingly.
  152. in src/wallet/coinselection.cpp:242 in d45da596ac outdated
    223+ * As powersets grow exponentially with the set size, walking the entire tree would quickly get computationally
    224+ * infeasible with growing UTXO pools. We can represent the current state of the search with constant data thanks to
    225+ * visiting the nodes in a deterministic order. We skip visiting as many nodes as possible by recognizing subtrees that
    226+ * cannot yield any better solutions.
    227+ * CoinGrinder is searching for the input set with lowest weight that can fund a transaction, so for example we can only
    228+ * ever find a _better_ candidate input set in a node that adds a UTXO, but never in a node that skips a UTXO. After
    


    sipa commented at 9:51 pm on February 4, 2024:
    Perhaps point out that this makes it a branch-and-bound algorithm (https://en.wikipedia.org/wiki/Branch_and_bound): any branch which can only contain solutions worse than the best solution so far can be skipped.

    murchandamus commented at 2:48 pm on February 5, 2024:
    Sure, added the mention of that.
  153. in src/wallet/test/fuzz/coinselection.cpp:152 in a96b2db8fb outdated
    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+    unsigned max_output_groups = 16;
    


    sipa commented at 10:03 pm on February 4, 2024:
    Nit: static constexpr unsigned MAX_OUTPUT_GROUPS{16};.

    murchandamus commented at 2:05 pm on February 6, 2024:
    Taken
  154. in src/wallet/coinselection.cpp:503 in 97546dfe5c outdated
    482+            // After SHIFTing to an omission branch, the `next_utxo` might have the same effective value as the UTXO we
    483+            // just omitted. Since lower weight is our tiebreaker on UTXOs with equal effective value for sorting, if it
    484+            // ties on the effective value, it _must_ have the same weight (i.e. be a "clone" of the prior UTXO) or a
    485+            // higher weight. If so, selecting `next_utxo` would produce an equivalent or worse selection as one we
    486+            // previously evaluated. In that case, increment `next_utxo` until we find a UTXO with a differing amount.
    487+            while (utxo_pool[next_utxo - 1].GetSelectionAmount() == utxo_pool[next_utxo].GetSelectionAmount()) {
    


    sipa commented at 10:13 pm on February 4, 2024:

    I think you can actually do even better: it suffices that the next UTXO has a weight larger or equal to the first preceding omitted one. This is always the case when they have equal value (due to the tiebreak), but it may extend to further (lower) values too (if those have higher or equal weight).

    EDIT: as we aim for minimal overshoot of target value (after minimizing weight), it only works when the considered next utxo has weight larger than the first omitted one, or has the same value (and equal or larger weight, but that is implied by the sorting). That makes this suggestion much less applicable.

    Feel free to leave this for a follow-up.


    murchandamus commented at 3:05 pm on February 5, 2024:

    I’m not sure I follow. We also SHIFT after finding a solution, so we can’t just go by any preceding omitted one. Also, isn’t that just pre-empting the overweight check in the evaluation?

    Need to think more about that one.


    sipa commented at 5:05 pm on February 6, 2024:

    @murchandamus Let me elaborate for context, but no point in addressing this in this PR (after my edit, I don’t think it’s a particularly impactful improvement even).

    Let’s say the transactions, in order after sorting are A,B,C,D,E,F. Imagine we’ve just processed _BC, and SHIFT after it (so, without clone skipping, the next exploration would be _B_D). If it happens to be the case that D is unambiguously worse than C, then D can be skipped as well. This happens when value(D)=value(C) AND weight(D)>=weight(C) (as your current PR does; the latter condition being implied by sorting), but it also happens when value(D)<value(C) AND weight(D)>weight(C). If we wouldn’t care about minimizing value overshoot, it’d be even possible to skip whenever weight(D)>=weight(C) in general, which would be far more often applicable.

    Ok, so let’s say for any of the reasons above, _B_D was clone-skipped, so we’re now considering E as next_utxo. It suffices to compare E with C, rather than with D, because it’s possible that weight(E)>weight(C), but not weight(E)>weight(D).

    So my belief is that the rule can be that whenever next_utxo is unambiguously worse than the first omitted UTXO without intervening inclusions, it can be skipped, as a generalization of clone skipping. I haven’t tried it, though.

    Also, isn’t that just pre-empting the overweight check in the evaluation?

    Partially, but clone-skipping works on just individual UTXOs, not just when an entire subtree can be skipped. It’s possible that after the clone-skipping another, includable, UTXO still needs to be considered.


    murchandamus commented at 7:58 am on February 9, 2024:

    I see thanks for the explanation. It seems to me like most of these cases should be caught by the optimization to cut instead of shift when we exceed the weight:

    opt: Cut if last addition was minimal weight

    In situations where we have UTXO groups of various weight, we can CUT rather than SHIFT when we exceeded the max_weight or the best selection’s weight while the last step was equal to the minimum weight in the lookahead.

    It would take more time to determine whether this would make another meaningful impact if implemented, so I would like to defer this to a potential follow-up.

  155. in src/wallet/test/coinselector_tests.cpp:1278 in 5055d3d20b outdated
    1267@@ -1267,6 +1268,32 @@ BOOST_AUTO_TEST_CASE(coin_grinder_tests)
    1268         size_t expected_attempts = 42;
    1269         BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
    1270     }
    1271+
    1272+    {
    1273+        // #################################################################################################################
    


    sipa commented at 10:15 pm on February 4, 2024:
    Any reason not to add this test in the very beginning together with the other test cases?

    murchandamus commented at 2:10 pm on February 6, 2024:
    Nope, good point. Folded into test: Add coin_grinder_tests
  156. in src/wallet/coinselection.cpp:451 in ee6ff57bec outdated
    447@@ -448,6 +448,13 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    448                 best_selection_weight = curr_weight;
    449                 best_selection_amount = curr_amount;
    450             }
    451+        } else if (!best_selection.empty() && curr_weight + min_tail_weight[curr_tail] * ((total_target - curr_amount) / utxo_pool[curr_tail].GetSelectionAmount()) > best_selection_weight) {
    


    sipa commented at 10:31 pm on February 4, 2024:

    I’m not entirely convinced it’s impossible to hit overflow in the multiplication here. I’d suggest upcasting first int64_t{min_tail_weight[curr_tail]} * ....

    Also two minor improvements here:

    • you can round up in the division, as you need integral transactions
    • the !best_selection_empty() is unnecessary (best_selection_weight is already initialized to max_weight here, and exceeding that is also a reason to shift/cut).

    Overall that means:

    0} else if (curr_weight + int64_t{min_tail_weight[curr_tail]} * ((total_target - curr_amount + utxo_pool[curr_tail].GetSelectionAmount() - 1) / utxo_pool[curr_tail].GetSelectionAmount()) > best_selection_weight) {`
    

    murchandamus commented at 2:15 pm on February 6, 2024:
    I used the suggestion to upcast and to round up, but I’m leaving the !best_selection.empty() in, as we would otherwise not produce a max_tx_weight_exceeded error.
  157. in src/wallet/coinselection.cpp:266 in ee6ff57bec outdated
    261+ * shifts right by one: {AB} ⇒ {A_C}.)
    262+ * When we reach a leaf node in the last level of the tree, shifting to the omission branch is not possible. Instead we
    263+ * go to the omission branch of the node’s last ancestor on an inclusion branch: from {ABCD}, we go to {AB_D}. From
    264+ * {AB_D}, we go to {A_C}. We refer to this operation as a _CUT_. (The ultimate element in
    265+ * the input set is deselected, and the penultimate element is shifted right by one: {AB_D} ⇒ {A_C}.)
    266+ * If a candidate input set in a node has not selected enough to fund a transaction, we continue directly along the next
    


    sr-gi commented at 4:51 pm on February 5, 2024:
    “has not selected enough to fund a transaction” -> enoguh “elements”, “coins”, …?

    murchandamus commented at 1:41 pm on February 6, 2024:
    Improved to “If a candidate input set in a node has not selected sufficient funds to build the transaction, we continue directly along the next inclusion branch.”
  158. in src/wallet/coinselection.cpp:274 in ee6ff57bec outdated
    269+ * Further, any prefix that already has selected sufficient effective value to fund the transaction cannot be improved
    270+ * by adding more UTXOs. If for example the candidate input set in {AB} is a valid solution, all potential descendant
    271+ * solutions {ABC}, {ABCD}, and {AB_D} must have a higher weight.
    272+ *
    273+ * Given the above UTXO set and a target of 11, using these initial observations, the basic implementation of
    274+* CoinGrinder visits the following 10 nodes:
    


    sr-gi commented at 4:52 pm on February 5, 2024:
    nit: missing whitespace here

    murchandamus commented at 1:47 pm on February 6, 2024:
    Fixed!
  159. murchandamus force-pushed on Feb 6, 2024
  160. murchandamus commented at 2:17 pm on February 6, 2024: contributor
    Responded to feedback from @sipa and @sr-gi
  161. murchandamus force-pushed on Feb 6, 2024
  162. DrahtBot added the label CI failed on Feb 6, 2024
  163. DrahtBot commented at 2:24 pm on February 6, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is 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.

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

    Debug: https://github.com/bitcoin/bitcoin/runs/21275419357

  164. DrahtBot removed the label CI failed on Feb 6, 2024
  165. in src/wallet/test/fuzz/coinselection.cpp:205 in 01e046b7bb outdated
    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+        if (result_cg->GetAlgoCompleted()) {
    


    sipa commented at 6:44 pm on February 6, 2024:

    Only if you do further touchups, but if so, perhaps you can add a test here that if a solution was returned, even if GetAlgoCompleted isn’t true, that result_cg->GetWeight() <= high_max_weight and that result_cg->GetSelectedEffectiveValue() <= target + coin_params.m_min_change_target?

    Also best_weight > result_cg->GetWeight() || (best_weight == result_cg->GetWeight() && best_amount >= result_cg->GetSelectedEffectiveValue()) would be possible to test, though adds less (it failing would just imply the exhaustive search in the fuzz test was wrong).


    murchandamus commented at 9:01 am on February 9, 2024:

    I added all three checks you suggest as I understand them (some inequalities in different direction):

    0+         assert(result_cg->GetWeight() <= high_max_weight);
    1+         assert(result_cg->GetSelectedEffectiveValue() >= target + coin_params.m_min_change_target);
    2+         assert(best_weight < result_cg->GetWeight() || (best_weight == result_cg->GetWeight() && best_amount <= result_cg->GetSelectedEffectiveValue()));
    

    Note that it is possible that CoinGrinder had found the best solution already, but didn’t manage to exhaustively search the combination search. These three checks all pass on a single run of all my fuzz seeds.

  166. in src/wallet/coinselection.cpp:458 in 509564e05e outdated
    454@@ -454,6 +455,13 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
    455                 best_selection_weight = curr_weight;
    456                 best_selection_amount = curr_amount;
    457             }
    458+        } else if (!best_selection.empty() && curr_weight + int64_t{min_tail_weight[curr_tail]} * std::ceil((total_target - curr_amount) / utxo_pool[curr_tail].GetSelectionAmount()) > best_selection_weight) {
    


    sipa commented at 6:56 pm on February 6, 2024:

    std::ceil doesn’t do anything here, the input to it is already a rounded-down integer. The proper way to compute a rounding-up integer division $\lceil\frac{a}{b}\rceil$ is (a + b - 1) / b.

    Also, can you drop the !best_selection.empty() && here?


    murchandamus commented at 9:35 am on February 9, 2024:

    Also, can you drop the !best_selection.empty() && here?

    No, dropping the !best_selection_empty() will prevent CoinGrinder from returning the max_tx_weight_exceeded error where expected. I fixed the rounding up as you described.


    sipa commented at 6:55 pm on February 9, 2024:
    Ah, I see. That’s unfortunate as it means this optimization isn’t available before some solution was found, but probably doesn’t matter much.

    murchandamus commented at 5:32 am on February 10, 2024:
    Mh, it could also be turned on after max_weight was exceeded the first time. :thinking: Might follow-up on this.
  167. in src/wallet/test/coinselector_tests.cpp:1295 in 509564e05e outdated
    1286@@ -1287,11 +1287,11 @@ BOOST_AUTO_TEST_CASE(coin_grinder_tests)
    1287             return available_coins;
    1288         });
    1289         SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
    1290-        add_coin(1.8 * COIN, 1, expected_result);
    1291+        add_coin(1 * COIN, 1, expected_result);
    


    sipa commented at 6:58 pm on February 6, 2024:
    This test is modified in the last commit; it’d be more convincingly improving runtime if the final version of the test was already included in the commit that initially adds it.

    murchandamus commented at 8:14 am on February 9, 2024:
    This change just modifies the expected number of iterations and the expected_result. Before the last optimization CoinGrinder finds a suboptimal solution (1.8 BTC + 1 BTC), because it does not manage to exhaustively search the entire search space. With the “sand skipping” optimization, it skips over the tail of tiny UTXOs. This allows it to search all relevant combinations and find the preferred solution (1 BTC + 1 BTC).

    sipa commented at 6:56 pm on February 9, 2024:
    Oh, I missed that this was just changing the expected result, not the input. Sounds good.
  168. in src/wallet/test/coinselector_tests.cpp:1157 in f770aff872 outdated
    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);
    


    achow101 commented at 9:21 pm on February 6, 2024:

    In f770aff87279ba252f7b8fcadef0e9d1828ca031 “test: Add coin_grinder_tests”

    nit: A comment with the weight calculation like the ones in later test would be nice.


    murchandamus commented at 8:07 am on February 9, 2024:
    I added a comment with the calculation
  169. in src/wallet/test/coinselector_tests.cpp:1184 in f770aff872 outdated
    1179+            return available_coins;
    1180+        });
    1181+        BOOST_CHECK(res);
    1182+        // If this takes more attempts, the implementation has regressed
    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()));
    


    achow101 commented at 9:25 pm on February 6, 2024:

    In f770aff87279ba252f7b8fcadef0e9d1828ca031 “test: Add coin_grinder_tests”

    I think it would be useful to clarify that expected_attempts is the iteration limit.

    The comment suggests that the check should be <= but the check looks for exactly this number of attempts. That seems fragile if there are further optimizations made that improve this.


    murchandamus commented at 8:23 am on February 9, 2024:
    I’ve improved the comment to clarify that this check is both used to highlight the improvements brought about by following optimizations and to catch future regressions.
  170. in src/wallet/test/coinselector_tests.cpp:1235 in f770aff872 outdated
    1225+            return available_coins;
    1226+        });
    1227+        BOOST_CHECK(res);
    1228+        // If this takes more attempts, the implementation has regressed
    1229+        size_t expected_attempts = 2041;
    1230+        BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
    


    achow101 commented at 9:30 pm on February 6, 2024:

    In f770aff87279ba252f7b8fcadef0e9d1828ca031 “test: Add coin_grinder_tests”

    This check implies that there is an exact solution at 2041 attempts, and we should be able to know what that solution is, so can we check for it?


    murchandamus commented at 8:50 am on February 9, 2024:
    Yes, that would be possible. It looks like it would be the “4, 13” from the light coins and the “14” from the medium coins that should get found. I added a check.
  171. in src/wallet/coinselection.cpp:799 in 3e2a97bd4b outdated
    795@@ -794,6 +796,15 @@ void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const
    796     }
    797 }
    798 
    799+void SelectionResult::SetAlgoCompleted(bool algo_completed) {
    


    achow101 commented at 9:32 pm on February 6, 2024:

    In 3e2a97bd4bb275e2f0827a3099a64222c6460ed9 “coinselection: Track whether CG completed”

    style nit: Open brace { should be on a new line for functions.


    murchandamus commented at 8:52 am on February 9, 2024:
    Fixed
  172. in src/wallet/coinselection.cpp:797 in 04a043a973 outdated
    793@@ -514,6 +794,15 @@ void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const
    794     }
    795 }
    796 
    797+void SelectionResult::SetSelectionsEvaluated(size_t attempts) {
    


    achow101 commented at 9:32 pm on February 6, 2024:

    In 04a043a973602bc74043f7c5767bcd55246913cb “coinselection: Add CoinGrinder algorithm”

    style nit: Open brace { should be on a new line for functions.


    murchandamus commented at 8:18 am on February 9, 2024:
    Fixed
  173. coinselection: Add CoinGrinder algorithm
    CoinGrinder is a DFS-based coin selection algorithm that
    deterministically finds the input set with the lowest weight creating a
    change output.
    6cc9a46cd0
  174. test: Add coin_grinder_tests 7488acc646
  175. coinselection: Track whether CG completed
    CoinGrinder may not be able to exhaustively search all potentially
    interesting combinations for large UTXO pools, so we keep track of
    whether the search was terminated by the iteration limit.
    1502231229
  176. fuzz: Add CoinGrinder fuzz target 67df6c629a
  177. fuzz: Test optimality of CoinGrinder
    Co-authored-by: Pieter Wuille <pieter@wuille.net>
    d68bc74fb2
  178. opt: Skip branches with worse weight
    Once we exceed the weight of the current best selection, we can always
    shift as adding more inputs can never yield a better solution.
    5f84f3cc04
  179. opt: Track remaining effective_value in lookahead
    Introduces a dedicated data structure to track the total
    effective_value available in the remaining UTXOs at each index of the
    UTXO pool. In contrast to the approach in BnB, this allows us to
    immediately jump to a lower index instead of visiting every UTXO to add
    back their eff_value to the lookahead.
    407b1e3432
  180. opt: Skip evaluation of equivalent input sets
    When two successive UTXOs match in effective value and weight, we can
    skip the second if the prior is not selected: adding it would create an
    equivalent input set to a previously evaluated.
    
    E.g. if we have three UTXOs with effective values {5, 3, 3} of the same
    weight each, we want to evaluate
    {5, _, _}, {5, 3, _}, {5, 3, 3}, {_, 3, _}, {_, 3, 3},
    but skip {5, _, 3}, and {_, _, 3}, because the first 3 is not selected,
    and we therefore do not need to evaluate the second 3 at the same
    position in the input set.
    
    If we reach the end of the branch, we must SHIFT the previously selected
    UTXO group instead.
    451be19dc1
  181. opt: Tiebreak UTXOs by weight for CoinGrinder 9124c73742
  182. opt: Skip heavier UTXOs with same effective value
    When two successive UTXOs differ in weight but match in effective value,
    we can skip the second if the first is not selected, because all input
    sets we can generate by swapping out a lighter UTXOs with a heavier UTXO
    of matching effective value would be strictly worse.
    5248e2a60d
  183. opt: Cut if last addition was minimal weight
    In situations where we have UTXO groups of various weight, we can CUT
    rather than SHIFT when we exceeded the max_weight or the best
    selection’s weight while the last step was equal to the minimum weight
    in the lookahead.
    1edd2baa37
  184. opt: Skip checking max_weight separately
    Initialize `best_selection_weight` as `max_weight` allows us to skip the
    separate `max_weight` check on every loop.
    b7672c7cdd
  185. opt: Skip over barren combinations of tiny UTXOs
    Given a lot of small amount UTXOs it is possible that the lookahead
    indicates sufficient funds, but any combination of them would push us
    beyond the current best_weight.
    We can estimate a lower bound for the minimal necessary weight to reach
    target from the maximal amount and minimal weight in the tail of the
    UTXO pool: if adding a number of hypothetical UTXOs of this maximum
    amount and minimum weight would not be able to beat `best_weight`, we
    can SHIFT to the omission branch, and CUT if the last selected UTXO is
    not heavier than the minimum weight of the remainder.
    13161ecf03
  186. murchandamus commented at 10:04 am on February 9, 2024: contributor
    Addressed all comments by @sipa and @achow101. Thanks for the review!
  187. murchandamus force-pushed on Feb 9, 2024
  188. achow101 commented at 6:44 pm on February 9, 2024: member
    ACK 13161ecf032b7a850686e5942c12222c8f3d0d52
  189. DrahtBot requested review from ishaanam on Feb 9, 2024
  190. DrahtBot requested review from sipa on Feb 9, 2024
  191. DrahtBot requested review from kashifs on Feb 9, 2024
  192. DrahtBot requested review from sr-gi on Feb 9, 2024
  193. DrahtBot removed review request from kashifs on Feb 9, 2024
  194. DrahtBot requested review from kashifs on Feb 9, 2024
  195. DrahtBot removed review request from kashifs on Feb 9, 2024
  196. DrahtBot requested review from kashifs on Feb 9, 2024
  197. sipa commented at 8:20 pm on February 9, 2024: member
    ACK 13161ecf032b7a850686e5942c12222c8f3d0d52
  198. DrahtBot removed review request from kashifs on Feb 9, 2024
  199. DrahtBot requested review from kashifs on Feb 9, 2024
  200. sr-gi commented at 9:25 pm on February 9, 2024: member
    ACK 13161ec
  201. DrahtBot removed review request from sr-gi on Feb 9, 2024
  202. DrahtBot removed review request from kashifs on Feb 9, 2024
  203. DrahtBot requested review from kashifs on Feb 9, 2024
  204. achow101 merged this on Feb 9, 2024
  205. achow101 closed this on Feb 9, 2024


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: 2024-07-03 10:13 UTC

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