-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Optimize coin selection by dropping BnB upper limit #23367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
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. |
6546386
to
3f5fdfe
Compare
As discussed, @achow101 ran his simulation with this modification. It did find 10% more changeless solutions and an overall decrease in fee expenditure, but there were a few outcomes where the final input set that was used to fund transactions caused change output to be created that just exceeded the dust limit. Edit: I see that you appear to always drop everything below |
Just to clarify, dropping to fees behaviour is not changed. I just moved it higher and passed the threshold to coin selection so waste calculation is based on the same tx structure as transaction building. What's new is that in case of BnB we will never created change now. We already limit max absolute fee by I tend to think that whenever we have a solution with huge excess from BnB, we should get the same solution but with change instead of excess found by other selection algorithms. The problem is that SRD has min change of COIN/100/2, and knapsack is not deterministic AFAIK. Maybe need to try to prove this more formally? Or consider adding another algo in the mix that will deterministically produce a solution with change |
56591f6
to
26f86fd
Compare
ccbd009
to
6a798dc
Compare
2d68643
to
71e3635
Compare
ISTM what this is really showing is that our calculation for the upper bound is incorrect. It's a bit overkill to remove the upper bound entirely; rather we should fix the calculation such that it finds the correct upper bound for what we are willing to throw away. |
71e3635
to
df9918c
Compare
Removing upper bound allows to find more changeless solutions. Most of them would be terrible due to huge excess and hence waste, so another solution would be chosen. But in some cases excess higher than upper bound would be economically optimal based on waste metric.
df9918c
to
2dc0471
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this PR should be broken up into multiple. There are a few commits which could stand alone and it would be nice to have them first. I think that the main "Return change from SelectionResult" commit (and some of the things that lead up to it) should be in its own PR, with this one building on top of that.
I have a few issues with the approach taken for GetChange
- there is an inline comment for discussing that.
|
||
for (const CTxIn& txin : tx.vin) { | ||
// if it's not in the wallet and corresponding UTXO is found than select as external output | ||
const auto& outPoint = txin.prevout; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 5f035ef "wallet: do not count wallet utxos as external"
nit: don't use camelCase
BOOST_CHECK(result10); | ||
result10->ComputeAndSetWaste(coin_selection_params_bnb); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In ea7056b "test: verify waste for coinselection"
This doesn't compile
result10->ComputeAndSetWaste(coin_selection_params_bnb); | |
result10->ComputeAndSetWaste(coin_selection_params_bnb.m_cost_of_change); |
BOOST_CHECK(result10); | ||
result10->ComputeAndSetWaste(coin_selection_params_bnb); | ||
BOOST_CHECK(result10->GetWaste() == -68 * 3); // - (long_term_fee * input) * 3coins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In ea7056b "test: verify waste for coinselection"
This fails.
struct TxSize { | ||
int64_t vsize{-1}; | ||
int64_t weight{-1}; | ||
}; | ||
|
||
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, const CCoinControl* coin_control = nullptr); | ||
int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoint, const SigningProvider* pwallet, const CCoinControl* coin_control = nullptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 803ae6e "wallet: unify max signature logic"
nit: Did these have to move?
@@ -32,9 +32,10 @@ class CInputCoin { | |||
effective_value = txout.nValue; | |||
} | |||
|
|||
CInputCoin(const CTransactionRef& tx, unsigned int i, int input_bytes) : CInputCoin(tx, i) | |||
CInputCoin(const CTransactionRef& tx, unsigned int i, int input_bytes, int input_weight) : CInputCoin(tx, i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 7d839eb "wallet: CInputCoin store weight"
Since the callers have to be changed to use TxSize
anyways, why not just take a TxSize
here rather than the two components individually?
{ | ||
m_input_bytes = input_bytes; | ||
m_input_weight = input_weight; | ||
m_isSegwit = isSegwit; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 3b7d2d7 "WIP. Add CInputCoint::m_isSegwit"
nit: snake_case is preferred.
// WU required to store input counter | ||
// 1 vbyte for the default case already counted in non_input_size | ||
int weight = (GetSizeOfCompactSize(m_selected_inputs.size()) - 1)*4; | ||
bool hasWitnessInputs = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6423998 "WIP. Return change from SelectionResult"
nit: snake_case is preferred
@@ -492,6 +499,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec | |||
*/ | |||
preset_inputs.Insert(coin, 0, false, 0, 0, false); | |||
} | |||
// TODO: value_to_select -= preset_inputs.GetSelectionAmount(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6423998 "WIP. Return change from SelectionResult"
This is currently unnecessary because in the loop above, the values of preset inputs are subtracted from value_to_select
.
const auto dust = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate); | ||
// TODO: disambiguate between min_change and m_cost_of_change | ||
if (dust > coin_selection_params.m_cost_of_change) coin_selection_params.m_cost_of_change = dust; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6423998 "WIP. Return change from SelectionResult"
const auto dust = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate); | |
// TODO: disambiguate between min_change and m_cost_of_change | |
if (dust > coin_selection_params.m_cost_of_change) coin_selection_params.m_cost_of_change = dust; | |
coin_selection_params.m_cost_of_change = std::max(GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate), coin_selection_params.m_cost_of_change); |
CAmount SelectionResult::GetChange(const CoinSelectionParams& coin_selection_params) const | ||
{ | ||
if (m_force_no_change) return 0; | ||
|
||
CAmount change = GetSelectedValue() - m_target; | ||
// TODO: rename var maybe? | ||
if (!m_use_effective) { // opposite of m_subtract_fee_outputs | ||
// m_target already includes not_input_fees | ||
// but as they are payed by recipients we can add them back to our change | ||
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size); | ||
change += not_input_fees; | ||
} else { | ||
const auto size = GetVirtualTransactionSize(GetInputsWeight(), 0, 0); | ||
change -= coin_selection_params.m_effective_feerate.GetFee(size); | ||
} | ||
assert(change >= 0); | ||
|
||
if (change <= coin_selection_params.m_cost_of_change) { // TODO: less or less_or_equal | ||
change = 0; | ||
} | ||
|
||
return change; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6423998 "WIP. Return change from SelectionResult"
I feel like all of the size calculation stuff in this function is unnecessary. We don't need GetChange
to be exact down to the satoshi. Rather it just needs to be close enough to the real change (and essentially be whether there is going to be a change output) and its exact value can be adjusted later in CreateTransaction
once we know the real size of the transaction. That adjustment doesn't need to account for dust because that should be taken care of here.
It also seems like it is not necessary to calculate the change on the fly like this. Rather the coin selection algorithms should already know what the change is based on their parameters when they selected. While we don't know the fees exactly due to rolling them into the target, we do know how much was selected and what the input fees should be, so we can calculate the change in the SelectCoins*
functions and just set it in SelectionResult
.
🐙 This pull request conflicts with the target branch and needs rebase. Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft". |
This approach drops huge amounts to fees in the cases introduced in #24580. In the short-term I'll work on just increasing the boundary for BnB search to improve efficiency of coin selection. |
Requires #22019 and #22949
Removing upper bound allows to find more changeless solutions. Most of them would be terrible due to huge excess and hence waste, so another solution would be chosen. But in some cases excess higher than upper bound would be
economically optimal based on waste metric.
This change also makes transaction building and waste calculation consistent. Specifically it eliminates two sources of discrepancy:
As a result we should see more changeless solutions which is good for: privacy, utxo set, total fees paid.