-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: increase BnB upper limit #24752
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
9dcc361
to
de6ef81
Compare
de6ef81
to
f83127e
Compare
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. |
f83127e
to
50831c9
Compare
1561bfe
to
3c106cc
Compare
3c106cc
to
9e9c114
Compare
9e9c114
to
070f9aa
Compare
070f9aa
to
b009a99
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.
Very exciting changes in this PR. It feels like there are still some moving parts, see my comments on the various sections.
@@ -760,7 +760,8 @@ static bool CreateTransactionInternal( | |||
|
|||
// vouts to the payees | |||
if (!coin_selection_params.m_subtract_fee_outputs) { | |||
coin_selection_params.tx_noinputs_size = 11; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 output count, 1 witness overhead (dummy, flag, stack size) | |||
coin_selection_params.tx_noinputs_size = 10; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 witness overhead (dummy, flag, stack size) |
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.
Note for reviewers/myself: It should be 10 vB for non-segwit transactions, and 42 WU for segwit transactions (as the witness adds the two fields marker
and flag
that don't scale with the count of inputs. So, this is correct for non-segwit transactions, but would be short for a segwit transaction.
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.
For clarity. The size required to store output counter added below. So in case of <253 outputs tx_noinputs_size
will be 11vB for both segwit and non-segwit txs. This overshoots the target a bit for non-segwit, but this is much better than selecting not enough coins.
@@ -27,25 +27,25 @@ using interfaces::FoundBlock; | |||
namespace wallet { | |||
static constexpr size_t OUTPUT_GROUP_MAX_ENTRIES{100}; | |||
|
|||
int GetTxSpendSize(const CWallet& wallet, const CWalletTx& wtx, unsigned int out, bool use_max_sig) | |||
int GetTxSpendSize(const CWallet& wallet, const CWalletTx& wtx, unsigned int out, const CCoinControl* coin_control) |
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.
Note to other reviewers: it wasn't obvious to me at first why this replacement is possible, but I found that use_max_sig
is directly calculated from CCoinControl
values. Perhaps this could be mentioned in the commit message.
./src/wallet/wallet.cpp: const bool use_max_sig = coin_control && (coin_control->fAllowWatchOnly || coin_control->IsExternalSelected(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.
Will do
// barely meets the target. Just use the lower bound change target instead of the randomly | ||
// generated one, since SRD will result in a random change amount anyway; avoid making the | ||
// target needlessly large. | ||
const CAmount srd_target = nTargetValue + coin_selection_params.m_change_fee + CHANGE_LOWER; |
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.
I see how this is just a refactor, and doesn't change the result, but it's not clear to me why it's being done. Could you add a sentence of motivation to the commit message?
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.
Will do. The reason is to have the correct value (without CHANGE_LOWER) for SelectionResult.m_target
if (!coin_selection_params.m_subtract_fee_outputs) { | ||
target_with_change += coin_selection_params.m_change_fee; | ||
} | ||
if (auto knapsack_result{KnapsackSolver(all_groups, target_with_change, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) { |
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.
Nice catch!
/** Force no change even if more than enough inputs are selected */ | ||
bool m_force_no_change{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.
force_no_change = false
feels like a potential brain twister. Perhaps it would be better to call this allow_change
and invert the boolean values.
/** Force no change even if more than enough inputs are selected */ | |
bool m_force_no_change{false}; | |
/** Do not allow change even if more than enough inputs are selected */ | |
bool m_allow_change{true}; |
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.
meh.. I'm ambivalent about this. What do others think?
@@ -434,9 +434,17 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec | |||
*/ | |||
preset_inputs.Insert(out, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false); | |||
} | |||
if (preset_inputs.GetSelectionAmount() < nTargetValue) return std::nullopt; |
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.
Wasn't this what we introduced SelectionResult.Merge(…)
above for?
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.
Not sure what did you mean by this. This is the case when the selected inputs are not enough and we just return null value. There's no merging going on.
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.
Right, but if the preset inputs are less than the target, wouldn't we be supposed to select additional inputs and build a transaction? I thought that's what you introduced the merge for.
@@ -572,8 +585,8 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec | |||
if (!res) return std::nullopt; | |||
|
|||
// Add preset inputs to result | |||
res->AddInput(preset_inputs); | |||
if (res->m_algo == SelectionAlgorithm::MANUAL) { | |||
res->Merge(preselected); |
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.
I have the impression that this commit makes changes to how coin selection works with preselected inputs and in addition introduces that SelectionResult returns change. While both changes look worthwhile to me, it would be easier to follow if they were separate commits if not separate PRs. Am I missing some special reason why doing all of this at once is advantageous?
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.
I intended this to not change the behavior. The reason why we need Merge
is to correctly track SelectionResult::m_target
. Right now in case with preselected inputs m_target
will be reduced by the amount of preselected inputs and this leads to incorrect result in SelectionResult::GetChange
I'll split the commits and try to explain better why this is needed
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output. | ||
std::vector<OutputGroup> positive_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, true /* positive_only */); | ||
if (auto bnb_result{SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change)}) { | ||
if (auto bnb_result{SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change + cost_of_extra_input)}) { |
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.
That's an interesting upper bound. Much better than just completely unbounded, but definitely gives some room for bigger excess
amounts than before. Is this still a WIP-value to give you more wiggle room or is this what you finally identified as the appropriate upper bound? Curious about your thought process here.
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.
To fix inefficiencies identified #24580 we need to increase upper limit by the opportunity cost of spending one extra input. We don't know what type that one extra input would be.
The range of possible values is from the cost of p2tr (57.5vb) to the cost of p2pkh (148vb). The chosen value is in the middle of that range.
- If you have only p2pkh inputs in your wallet: the new upper bound is lower than optimal, but still better than current bound as it extends the set of possible BnB solutions.
- If you have only p2tr inputs: the new bound is higher than optimal, but also better than current bound. Same reasoning applies here as for the BnB without upper limit. We produce more BnB solutions and later choose what solution is economically more efficient. But unlike BnB without upper limit we don't risk dropping ridiculous amounts to fees.
An alternative way to pick the value would be to examine available UTXOs and take an average input size or something like this. This is more complicated and I'm not sure if it makes a meaningful difference.
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.
I should probably add a comment to the code once we agree on the value
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.
Perhaps it should just use the default output type, that way it will automatically upgrade as wallets progress to P2TR eventually. The current default seems like a reasonable choice at this time.
🤔 Having the bound definitely feels a lot better than unbounded. A whole input's fee still feels like a lot of additional fees. It's obviously only the worst case, but I'm curious to see how that fares in simulations. Do you need to make more logic changes or do you think I could take it for a spin on a few different scenarios?
🐙 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". |
Update:
FWIU, @S3RK had a similar result with his simulations so far. There was a bug discovered in how the waste is calculated for Knapsack in some instances, but I don't expect this bug to impact the overall trend. More experiments to follow, and it may be interesting to explore the opposite, i.e. see whether reducing the window size in which BnB produces solutions improves results instead. |
I don't think this is the correct interpretation of the data. e.g. for
I see similar picture for Overall it's not clear whether increasing the upper limit is beneficial. I'm closing this now and will send refactoring PRs separately. |
Successor of #23367
Increasing upper bound helps to find more changeless solutions.
This improves coin selection in the cases introduced in #24580.