-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: Refactor OutputGroups to handle fees and spending eligibility on grouping #20040
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
OutputGroup will handle the fee and effective value computations inside of Insert. It now needs to take the effective feerate and long term feerates as arguments to its constructor.
Instead of filtering after the OutputGroups have been made, do it as they are being made.
98cb8a1
to
4b15eae
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. |
4d86d14
to
d4b8b8d
Compare
CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_fee, size_t tx_noinputs_size) : use_bnb(use_bnb), change_output_size(change_output_size), change_spend_size(change_spend_size), effective_fee(effective_fee), tx_noinputs_size(tx_noinputs_size) {} | ||
bool m_avoid_partial_spends = false; | ||
|
||
CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_fee, size_t tx_noinputs_size, bool avoid_partial) : |
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.
musing: this constructor only seems useful for tests
Instead of hacking OutputGroup::m_ancestors to discourage the inclusion of partial groups via the eligibility filter, add a parameter to the eligibility filter that indicates whether we want to include the group. Then for those partial groups, don't return them in GroupOutputs if we indicate they aren't desired.
d4b8b8d
to
f6b3052
Compare
ACK f6b3052 |
cc @xekyo |
src/wallet/coinselection.cpp
Outdated
@@ -302,6 +302,16 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group | |||
|
|||
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants) { | |||
m_outputs.push_back(output); | |||
CInputCoin& coin = m_outputs.back(); | |||
coin.m_fee = coin.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(coin.m_input_bytes); |
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 think I have asked this before, but why can m_input_bytes
ever be below zero here? Perhaps it would be good to have a comment to explain that.
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.
m_input_bytes
is initialized to -1
to indicate that it hasn't been calculated 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.
But this is at the point where we calculate the effective value of UTXOs, so we need to know the size. Why would we want to mitigate a missing size here rather than throwing?
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.
Since the intention is to not change behavior in this PR, I think I will leave this as is for now.
Additionally I don't think it is guaranteed that when we add a CInputCoin
to an OutputGroup
that we do know the size. It could be for a preset input or an input not in the wallet (there is a PR for that) where we add those coins to an OutputGroup
and just don't use the effective value calculation. In those instances, the m_input_bytes
may not be set.
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.
A comment would still be a good idea :)
src/wallet/wallet.cpp
Outdated
// This is for if each output gets it's own OutputGroup | ||
OutputGroup coin(effective_feerate, long_term_feerate); | ||
coin.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants); | ||
if (coin.EligibleForSpending(filter)) groups.push_back(coin); |
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.
Wouldn't this mean that if you got a tiny and a large UTXO associated with the same address that you would potentially form a OutputGroup with just the large coin? Shouldn't the group rather be ineligible as a whole to avoid the partial spend?
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.
This would make the wallet more vulnerable to dust attacks. An attacker could them lock out a user from their funds by sending dust to an already used address.
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.
Yeah, I guess dust should get ignored altogether, but at higher fee rates this could in the worst-case even affect a small amount and a slightly larger amount, which should be prohibited by the partial spending restriction.
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only) { | ||
// Compute the effective value first | ||
const CAmount coin_fee = output.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(output.m_input_bytes); | ||
const CAmount ev = output.txout.nValue - coin_fee; |
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 think that this will result in the OutputGroup accepting uneconomic inputs in the case that the recipient is paying the fees. Should we perhaps rather filter by whether the UTXO are uneconomic, but just calculate with the value instead of the effective value for the case of the recipient paying the fees?
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.
Given that the recipient paying the fees is typically used when sweeping the wallet, I don't think it really matters.
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.
Oh, I didn't realize that it was just used for that. Wouldn't it be much easier to implement that as a "send everything": no coin selection, just sum up everything, deduct fees and pay the recipient address that?
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 the most common use case I think, but not the only one.
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.
It may be worth considering an explicit sweepwallet
rpc since I don't think the expectations for "recipient pays fees" and "empty my wallets and send as much as you can" necessarily match. Although, maybe in both cases it would be appropriate not to use uneconomic UTXOs. ;)
Code Review ACK f6b3052 |
Disclaimer: |
I've added a commit to rewrite |
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.
Just reviewed 97256ca
It's much clearer, thank you also for the illustrative comments. Have a few naming suggestions.
Rewrite OutputGroups so that the logic is easier to follow and understand. There is a slight behavior change as OutputGroups will be grouped by scriptPubKey rather than CTxDestination as before. This should have no effect on users as all addresses are a CTxDestination. However by using scriptPubKeys, we can correctly group outputs which fall into the NoDestination case. But we also shouldn't have any NoDestination outputs.
97256ca
to
5d45976
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.
Code review ACK: 5d45976
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only) { | ||
// Compute the effective value first | ||
const CAmount coin_fee = output.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(output.m_input_bytes); | ||
const CAmount ev = output.txout.nValue - coin_fee; |
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.
It may be worth considering an explicit sweepwallet
rpc since I don't think the expectations for "recipient pays fees" and "empty my wallets and send as much as you can" necessarily match. Although, maybe in both cases it would be appropriate not to use uneconomic UTXOs. ;)
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 5d45976
Nice refactoring! I had a bunch of suggestions for small improvements but I don't consider them blocking for a merge.
One more comment: The behavior change in 416d74f could be noted a little more explicitly I think. Before this change, the OutputGroup
s are filled up and then filtered for positive-only later, sometimes then using groups that are not filled completely in coin selection. Now, since the non-positive coins are never inserted we will have full groups with only positive values. This will lead to different results in certain cases. I think this is an improvement and I don't see any issues arise from it but it wouldn't hurt to mention it in the commit message as it is in 5d45976.
void Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants); | ||
std::vector<CInputCoin>::iterator Discard(const CInputCoin& output); | ||
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const; | ||
|
||
//! Update the OutputGroup's fee, long_term_fee, and effective_value based on the given feerates |
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 99b399a
nit: Maybe add a similar comment to Insert
that this updates the fees now if you retouch.
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants) { | ||
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only) { | ||
// Compute the effective value first | ||
const CAmount coin_fee = output.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(output.m_input_bytes); |
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 416d74f
nit: I think you could skip both intermediary vars (coin_fee
, ev
) here and instead set the coin
members here and use them in the following lines without hurting readability.
src/wallet/wallet.h
Outdated
@@ -841,7 +841,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati | |||
bool IsSpentKey(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); | |||
void SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); | |||
|
|||
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool single_coin, const size_t max_ancestors, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter) const; | |||
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool single_coin, const size_t max_ancestors, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const; |
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 416d74f
nit: maybe make positive_only
const as well?
@@ -2489,8 +2489,8 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm | |||
(m_spend_zero_conf_change && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) || | |||
(m_spend_zero_conf_change && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) || | |||
(m_spend_zero_conf_change && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) || | |||
(m_spend_zero_conf_change && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) || | |||
(m_spend_zero_conf_change && !fRejectLongChains && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max()), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)); | |||
(m_spend_zero_conf_change && SelectCoinsMinConf(value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */), vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used)) || |
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 f6b3052
Now since max_descendants and partial_groups are decoupled it could be discussed if it stays like this or if this coin selection step is split up into one which only tries with the old config (leaving out partial groups) and then one after that adds partial groups. But it can be left for a follow-up if that change is desired.
CTxDestination dst; | ||
CInputCoin input_coin = output.GetInputCoin(); | ||
if (separate_coins) { | ||
// Single coin means no grouping. Each COutput gets its own OutputGroup. |
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 5d45976
nit: update comment "Single coin" => "Separate coin"
continue; | ||
} | ||
// If the OutputGroup is not eligible, don't add it | ||
|
||
// Check the OutputGroup's eligibility. Only add the eligible ones. | ||
if (positive_only && group.effective_value <= 0) continue; |
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 5d45976
same as above, I think this is not necessary since Insert takes care of this now.
size_t ancestors, descendants; | ||
chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); | ||
CInputCoin input_coin = output.GetInputCoin(); | ||
CScript spk = input_coin.txout.scriptPubKey; |
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 5d45976
nit: spk
seems to be only used in the line below so I would drop it.
@@ -42,21 +42,19 @@ static void CoinSelection(benchmark::Bench& bench) | |||
} | |||
addCoin(3 * COIN, wallet, wtxs); | |||
|
|||
// Create groups | |||
std::vector<OutputGroup> groups; | |||
// Create coins |
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 6148a8a
nit: This comment isn't that helpful, I would suggest something like "Prepare coins in a format that can be passed to SelectCoinsMinConf()"
src/wallet/wallet.cpp
Outdated
@@ -2381,6 +2381,8 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil | |||
temp.m_confirm_target = 1008; | |||
CFeeRate long_term_feerate = GetMinimumFeeRate(*this, temp, &feeCalc); | |||
|
|||
std::vector<OutputGroup> groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, eligibility_filter.max_ancestors); |
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 6148a8a
This line seems to be the same in both if-else blocks so it could be moved to the beginning of the function before the if
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, never mind, it becomes necessary later when the positive_only
param is added
src/wallet/coinselection.cpp
Outdated
@@ -302,6 +302,16 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group | |||
|
|||
void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants) { | |||
m_outputs.push_back(output); | |||
CInputCoin& coin = m_outputs.back(); | |||
coin.m_fee = coin.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(coin.m_input_bytes); |
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.
A comment would still be a good idea :)
@fjahr Since this now has two ACKs, I'm going to leave this as-is for now. |
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.
Light utACK 5d45976
Nice cleanup thanks Andrew!
… spending eligibility on grouping
Even after #17458, we still deal with setting fees of an
OutputGroup
and filtering theOutputGroup
outside of the struct. We currently make all of theOutputGroup
s inSelectCoins
and then copy and modify them within eachSelectCoinsMinConf
scenario. This PR changes this to constructing theOutputGroup
s within theSelectCoinsMinConf
so that the scenario can be taken into account during the group construction. Furthermore, setting of fees and filtering for effective value is moved intoOutputGroup::Insert
itself so that we don't add undesirable outputs to anOutputGroup
rather than deleting them afterwards.To facilitate fee calculation and effective value filtering during
OutputGroup::Insert
,OutputGroup
now takes the feerates in its constructor and computes the fees and effective value for each output duringInsert
.While removing
OutputGroup
s in accordance with theCoinEligibilityFilter
still requires creating theOutputGroup
s first, we can do that within the function that makes them -GroupOutput
s.