Skip to content

Commit 5d45976

Browse files
committed
Rewrite OutputGroups to be clearer and to use scriptPubKeys
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.
1 parent f6b3052 commit 5d45976

File tree

2 files changed

+74
-42
lines changed

2 files changed

+74
-42
lines changed

src/wallet/wallet.cpp

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4193,58 +4193,90 @@ bool CWalletTx::IsImmatureCoinBase() const
41934193
return GetBlocksToMaturity() > 0;
41944194
}
41954195

4196-
std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outputs, bool single_coin, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const {
4197-
std::vector<OutputGroup> groups;
4198-
std::map<CTxDestination, OutputGroup> gmap;
4199-
std::set<CTxDestination> full_groups;
4196+
std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const
4197+
{
4198+
std::vector<OutputGroup> groups_out;
42004199

4201-
for (const auto& output : outputs) {
4202-
if (output.fSpendable) {
4203-
CTxDestination dst;
4204-
CInputCoin input_coin = output.GetInputCoin();
4200+
if (separate_coins) {
4201+
// Single coin means no grouping. Each COutput gets its own OutputGroup.
4202+
for (const COutput& output : outputs) {
4203+
// Skip outputs we cannot spend
4204+
if (!output.fSpendable) continue;
42054205

42064206
size_t ancestors, descendants;
42074207
chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants);
4208-
if (!single_coin && ExtractDestination(output.tx->tx->vout[output.i].scriptPubKey, dst)) {
4209-
auto it = gmap.find(dst);
4210-
if (it != gmap.end()) {
4211-
// Limit output groups to no more than OUTPUT_GROUP_MAX_ENTRIES
4212-
// number of entries, to protect against inadvertently creating
4213-
// a too-large transaction when using -avoidpartialspends to
4214-
// prevent breaking consensus or surprising users with a very
4215-
// high amount of fees.
4216-
if (it->second.m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) {
4217-
groups.push_back(it->second);
4218-
it->second = OutputGroup{effective_feerate, long_term_feerate};
4219-
full_groups.insert(dst);
4220-
}
4221-
it->second.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
4222-
} else {
4223-
auto ins = gmap.emplace(dst, OutputGroup{effective_feerate, long_term_feerate});
4224-
ins.first->second.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
4225-
}
4226-
} else {
4227-
// This is for if each output gets it's own OutputGroup
4228-
OutputGroup coin(effective_feerate, long_term_feerate);
4229-
coin.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
4230-
if (positive_only && coin.effective_value <= 0) continue;
4231-
if (coin.m_outputs.size() > 0 && coin.EligibleForSpending(filter)) groups.push_back(coin);
4232-
}
4208+
CInputCoin input_coin = output.GetInputCoin();
4209+
4210+
// Make an OutputGroup containing just this output
4211+
OutputGroup group{effective_feerate, long_term_feerate};
4212+
group.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
4213+
4214+
// Check the OutputGroup's eligibility. Only add the eligible ones.
4215+
if (positive_only && group.effective_value <= 0) continue;
4216+
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
42334217
}
4218+
return groups_out;
42344219
}
4235-
if (!single_coin) {
4236-
for (auto& it : gmap) {
4237-
auto& group = it.second;
4238-
if (full_groups.count(it.first) > 0 && !filter.m_include_partial_groups) {
4239-
// Don't include partial groups if we don't want them
4220+
4221+
// We want to combine COutputs that have the same scriptPubKey into single OutputGroups
4222+
// except when there are more than OUTPUT_GROUP_MAX_ENTRIES COutputs grouped in an OutputGroup.
4223+
// To do this, we maintain a map where the key is the scriptPubKey and the value is a vector of OutputGroups.
4224+
// For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput's CInputCoin is added
4225+
// to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has
4226+
// OUTPUT_GROUP_MAX_ENTRIES CInputCoins, a new OutputGroup is added to the end of the vector.
4227+
std::map<CScript, std::vector<OutputGroup>> spk_to_groups_map;
4228+
for (const auto& output : outputs) {
4229+
// Skip outputs we cannot spend
4230+
if (!output.fSpendable) continue;
4231+
4232+
size_t ancestors, descendants;
4233+
chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants);
4234+
CInputCoin input_coin = output.GetInputCoin();
4235+
CScript spk = input_coin.txout.scriptPubKey;
4236+
4237+
std::vector<OutputGroup>& groups = spk_to_groups_map[spk];
4238+
4239+
if (groups.size() == 0) {
4240+
// No OutputGroups for this scriptPubKey yet, add one
4241+
groups.emplace_back(effective_feerate, long_term_feerate);
4242+
}
4243+
4244+
// Get the last OutputGroup in the vector so that we can add the CInputCoin to it
4245+
// A pointer is used here so that group can be reassigned later if it is full.
4246+
OutputGroup* group = &groups.back();
4247+
4248+
// Check if this OutputGroup is full. We limit to OUTPUT_GROUP_MAX_ENTRIES when using -avoidpartialspends
4249+
// to avoid surprising users with very high fees.
4250+
if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) {
4251+
// The last output group is full, add a new group to the vector and use that group for the insertion
4252+
groups.emplace_back(effective_feerate, long_term_feerate);
4253+
group = &groups.back();
4254+
}
4255+
4256+
// Add the input_coin to group
4257+
group->Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
4258+
}
4259+
4260+
// Now we go through the entire map and pull out the OutputGroups
4261+
for (const auto& spk_and_groups_pair: spk_to_groups_map) {
4262+
const std::vector<OutputGroup>& groups_per_spk= spk_and_groups_pair.second;
4263+
4264+
// Go through the vector backwards. This allows for the first item we deal with being the partial group.
4265+
for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) {
4266+
const OutputGroup& group = *group_it;
4267+
4268+
// Don't include partial groups if there are full groups too and we don't want partial groups
4269+
if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) {
42404270
continue;
42414271
}
4242-
// If the OutputGroup is not eligible, don't add it
4272+
4273+
// Check the OutputGroup's eligibility. Only add the eligible ones.
42434274
if (positive_only && group.effective_value <= 0) continue;
4244-
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups.push_back(group);
4275+
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
42454276
}
42464277
}
4247-
return groups;
4278+
4279+
return groups_out;
42484280
}
42494281

42504282
bool CWallet::IsCrypted() const

src/wallet/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
841841
bool IsSpentKey(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
842842
void SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
843843

844-
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool single_coin, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const;
844+
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const;
845845

846846
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
847847
void LockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

0 commit comments

Comments
 (0)