-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: return change from SelectionResult #25647
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. |
f5e45c4
to
45ee9d3
Compare
Simulation results here https://github.com/S3RK/coin-selection-simulation/tree/25647-sim/results/25647 |
45ee9d3
to
e763496
Compare
ACK e763496 |
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.
ACK e763496
SelectionResult::m_target should be equal to actual selection target. Selection target is the sum of all recipient amounts plus non input fees. So we need to remove change_fee from the m_target. It's safe because change target is always greater than the change fee, so we can always cover fees if change output is created.
When we have preselected inputs the coin selection search target is reduced by the sum of (effective) values. This causes incorrect m_target value. Create separate instance of SelectionResult for all the preselected inputs and set the target equal to the sum of (effective) values. Target for preselected SelectionResult is equal to the delta for the search target. To get the final SelectionResult with accurate m_target we merge both SelectionResult instances.
e87606d
to
4fef534
Compare
Added one commit c8cf08e and rebased on master again |
if (m_algo == SelectionAlgorithm::MANUAL) { | ||
m_algo = other.m_algo; |
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 the current sources, this code block is a no-op.
the only time when we are entering here is when we merge a manual selection result with another manual selection result.
// The smallest change amount should be: | ||
// 1. at least equal to dust threshold | ||
// 2. at least 1 sat greater than fees to spend it at m_discard_feerate | ||
const auto dust = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate); | ||
const auto change_spend_fee = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size); | ||
coin_selection_params.min_viable_change = std::max(change_spend_fee + 1, 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.
hmm, this doesn't seems to be right:
- The dust amount is taken from -->
GetDustThreshold
which receives the change output, serializes it and uses magic numbers to calculate the input vsize (it's a general minimum input vsize calculation for an output), then callsdiscard_fee.Get(vsize)
with it to get the dust amount.
- The
change_spend_fee
is taken from -->discard_fee.Get(change_spend_size)
, wherechange_spend_size
is the result ofCalculateMaximumSignedInputSize(output)
which is the real vsize calculation of a signed input that spends the change output (we don't hardcode numbers here, we create the input and sign it).
In other words:
The only difference between the dust
and change_spend_fee
variables is the vsize calculation (both are doing m_discard_fee.GetFee(vsize)
), and:
- for
dust
, vsize is calculated from hardcoded numbers to get the minimum theoretical possible size. - for
change_spend_fee
, vsize is calculated creating an input and dummy signing it.
So, dust
will always be smaller than change_spend_fee
right?
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.
Extra note, had a great convo about this topic with @theStack. He mentioned that you might wanted to use wallet.chain().relayDustFee()
instead of coin_selection_params.m_discard_feerate
in the dust amount calculation?
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.
What's implemented here matches the behavior in master. For this PR, I think this is correct.
However we should probably use m_long_term_feerate
for calculating the change spend fee rather than the discard feerate. This would make it match how we calculate "future spends" for all of our inputs.
you might wanted to use
wallet.chain().relayDustFee()
instead ofcoin_selection_params.m_discard_feerate
in the dust amount calculation?
I think discard feerate is correct here. It is the maximum of the longest term fee estimate and the dust relay fee. As the name suggests, it is the feerate that we are willing to discard at, so I think it makes sense to keep it as is.
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.
you might wanted to use
wallet.chain().relayDustFee()
instead ofcoin_selection_params.m_discard_feerate
in the dust amount calculation?I think discard feerate is correct here. It is the maximum of the longest term fee estimate and the dust relay fee. As the name suggests, it is the feerate that we are willing to discard at, so I think it makes sense to keep it as is.
Isn't the point of setting the smallest change amount to "at least equal to dust threshold" to ensure that the created tx can enter our own mempool now, rather than somewhen in the future (considering that we run a node with a possibly customized -dustrelayfee=...
option)? As an example, if one would set -dustrelayfee=...
to a high value and discardfee=0
, a tx with a change output could be created that is rejected from our mempool (due to being IsDust
and therefore !IsStandardTx()
) and thus can't be sent. That's why my assumption was that we want to call GetDustThreshold
with the dustRelayFee instead:
- const auto dust = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate);
+ const auto dust = GetDustThreshold(change_prototype_txout, wallet.chain.relayDustFee());
(Am I missing something?)
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.
As an example, if one would set
-dustrelayfee=...
to a high value anddiscardfee=0
, a tx with a change output could be created that is rejected from our mempool
Then the dustrelayfee is what m_discard_fee
is set to. See GetDiscardRate
in src/wallet/fees.cpp.
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.
As an example, if one would set
-dustrelayfee=...
to a high value anddiscardfee=0
, a tx with a change output could be created that is rejected from our mempoolThen the dustrelayfee is what
m_discard_fee
is set to. SeeGetDiscardRate
in src/wallet/fees.cpp.
Oh right, I completely overlooked this following line of code, d'oh! 🤦♂️
Line 91 in a8f6954
discard_rate = std::max(discard_rate, wallet.chain().relayDustFee()); |
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 believe we should get rid of discard fee rate
and replace it with dust relay fee rate
and long term fee rate
. For this PR I decide to stick to the current implementation to limit the scope. I'll open an issue to deprecate discard fee rate
if there is none yet.
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.
Code review ACK 4fef534
Great step forward, this area needs so much more love.
ACK 4fef534 |
Concept ACK |
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.
Approach ACK.
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.
ACK 4fef534
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.
Sorry for the slow re-review. I don't think these two are blockers if they're issues at all, but I figured it might be good to bring them up.
// - change_fee | ||
const CAmount change = m_use_effective | ||
? GetSelectedEffectiveValue() - m_target - change_fee | ||
: GetSelectedValue() - m_target; |
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 “wallet: add SelectionResult::GetChange” (15e97a6):
If m_target
includes the non_input_fee
, but the recipient is supposed to pay for all fees, shouldn't SFFO branch here rather be GetSelectedValue() - (m_target - non_input_fee)
or GetSelectedValue() - recipients_sum
?
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.
With SFFO non_input_fee
is always set to 0. We can probably make this simpler and cleaner, let's discuss it on the wallet meeting or in some other place.
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.
Ah okay, that explains it. Thanks
// Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the | ||
// transaction at a target feerate. If an attempt fails, more attempts may be made using a more | ||
// permissive CoinEligibilityFilter. | ||
std::optional<SelectionResult> res = [&] { | ||
// Pre-selected inputs already cover the target amount. | ||
if (value_to_select <= 0) return std::make_optional(SelectionResult(nTargetValue, SelectionAlgorithm::MANUAL)); | ||
if (value_to_select <= 0) return std::make_optional(SelectionResult(value_to_select, SelectionAlgorithm::MANUAL)); |
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 “wallet: account for preselected inputs in target” (e3210a7):
This is a question about the commit message:
Sorry, if I'm being dense, but how was “deducting the effective values of the preselected inputs from m_target
” leading to a different result than “creating a separate SelectionResult with the target set to the effective values of the preselected inputs and then starting the selection targeting the delta between that and the search target”?
It sounds to me that in both cases that would be m_target - effective_value_of_preselection
under the hood? I assume there is a subtle difference here, but it's not obvious to me from the description. Is the difference that once we use m_target
and “search target” is now composed differently? Please feel free to ignore if this has been covered elsewhere, and I just missed it.
(Edited to clarify that I'm inquiring about 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.
The newly added Merge
function will add the m_target
s of the two SelectionResults
. In the event that value_to_select <= 0
, using nTargetValue
in this SelectionResult will add
nTargetValueto the
m_targetstored in the preset inputs
SelectionResult`. This is incorrect because the target was already met and we're actually just adding it twice.
Also note how all of the other SelectionResult
s returned by this function all use value_to_select
.
m_target
is not modified after construction, and nTargetValue
is not modified at all in this function - it includes any value already selected by preset inputs.
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.
Answering my own question after talking to S3RK out of band:
The correct m_target
is needed to calculate the change in advance. Previously, the preselection approach was reducing m_target
to reflect that some funds had already been picked, and now storing them in a SelectionResult and combining the two before calculating the change will be operating on basis of the correct m_target
instead.
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.
Ah thanks, @achow101, the case with the negative target makes it even clearer how this is better.
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.
Small extra add to the topic in case you haven't seen it; the pre-set inputs workflow (this stuff) is being decoupled from the coin selection process in #25685. Which should make the flow clearer and less error-prone.
crACK 4fef534 |
Benefits:
In the first three commits we fix the code to accurately track selection target in
SelectionResult::m_target
Then we introduce new variable
min_change
that represents the minimum viable change amountThen we introduce
SelectionResult::GetChange()
which incapsulates dropping change for fee logic and uses correct values ofSelectionResult::m_target
Then we use
SelectionResult::GetChange()
in both tx building and waste calculation codeThis PR is a refactoring and shouldn't change the behaviour.
There is only one known small change (arguably a bug fix). Before we dropped change output if it's smaller than
cost_of_change
after paying change fees. This is incorrect ascost_of_change
already includeschange_fee
.