Skip to content

Commit dc5e20c

Browse files
committed
[doc] coin selection filters by max cluster count, not descendant
Avoid confusion by clarifying the docs and renaming the variables that now hold cluster count rather than descendant count. No behavior change.
1 parent 9c299f6 commit dc5e20c

File tree

7 files changed

+40
-33
lines changed

7 files changed

+40
-33
lines changed

src/bench/coin_selection.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>
107107
tx.vout[nInput].nValue = nValue;
108108
COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/0, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/true, /*fees=*/0);
109109
set.emplace_back();
110-
set.back().Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*descendants=*/ 0);
110+
set.back().Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*cluster_count=*/ 0);
111111
}
112112
// Copied from src/wallet/test/coinselector_tests.cpp
113113
static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool)

src/wallet/coinselection.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
752752
753753
******************************************************************************/
754754

755-
void OutputGroup::Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants) {
755+
void OutputGroup::Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t cluster_count) {
756756
m_outputs.push_back(output);
757757
auto& coin = *m_outputs.back();
758758

@@ -770,9 +770,9 @@ void OutputGroup::Insert(const std::shared_ptr<COutput>& output, size_t ancestor
770770
// the sum, rather than the max; this will overestimate in the cases where multiple inputs
771771
// have common ancestors
772772
m_ancestors += ancestors;
773-
// descendants is the count as seen from the top ancestor, not the descendants as seen from the
774-
// coin itself; thus, this value is counted as the max, not the sum
775-
m_descendants = std::max(m_descendants, descendants);
773+
// This is the maximum cluster count among all outputs. If these outputs are from distinct clusters but spent in the
774+
// same transaction, their clusters will be merged, potentially exceeding the mempool's max cluster count.
775+
m_max_cluster_count = std::max(m_max_cluster_count, cluster_count);
776776

777777
if (output->input_bytes > 0) {
778778
m_weight += output->input_bytes * WITNESS_SCALE_FACTOR;
@@ -783,7 +783,7 @@ bool OutputGroup::EligibleForSpending(const CoinEligibilityFilter& eligibility_f
783783
{
784784
return m_depth >= (m_from_me ? eligibility_filter.conf_mine : eligibility_filter.conf_theirs)
785785
&& m_ancestors <= eligibility_filter.max_ancestors
786-
&& m_descendants <= eligibility_filter.max_descendants;
786+
&& m_max_cluster_count <= eligibility_filter.max_cluster_count;
787787
}
788788

789789
CAmount OutputGroup::GetSelectionAmount() const

src/wallet/coinselection.h

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,19 +207,20 @@ struct CoinEligibilityFilter
207207
const int conf_theirs;
208208
/** Maximum number of unconfirmed ancestors aggregated across all UTXOs in an OutputGroup. */
209209
const uint64_t max_ancestors;
210-
/** Maximum number of descendants that a single UTXO in the OutputGroup may have. */
211-
const uint64_t max_descendants;
210+
/** Maximum cluster count that a single UTXO in the OutputGroup may have. In practice, this filter also caps the
211+
* maximum descendant count, as a transaction's descendant count is never larger than its cluster count. */
212+
const uint64_t max_cluster_count;
212213
/** When avoid_reuse=true and there are full groups (OUTPUT_GROUP_MAX_ENTRIES), whether or not to use any partial groups.*/
213214
const bool m_include_partial_groups{false};
214215

215216
CoinEligibilityFilter() = delete;
216-
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_ancestors) {}
217-
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants) {}
218-
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants, bool include_partial) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants), m_include_partial_groups(include_partial) {}
217+
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_cluster_count(max_ancestors) {}
218+
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_cluster_count) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_cluster_count(max_cluster_count) {}
219+
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_cluster_count, bool include_partial) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_cluster_count(max_cluster_count), m_include_partial_groups(include_partial) {}
219220

220221
bool operator<(const CoinEligibilityFilter& other) const {
221-
return std::tie(conf_mine, conf_theirs, max_ancestors, max_descendants, m_include_partial_groups)
222-
< std::tie(other.conf_mine, other.conf_theirs, other.max_ancestors, other.max_descendants, other.m_include_partial_groups);
222+
return std::tie(conf_mine, conf_theirs, max_ancestors, max_cluster_count, m_include_partial_groups)
223+
< std::tie(other.conf_mine, other.conf_theirs, other.max_ancestors, other.max_cluster_count, other.m_include_partial_groups);
223224
}
224225
};
225226

@@ -239,8 +240,8 @@ struct OutputGroup
239240
/** The aggregated count of unconfirmed ancestors of all UTXOs in this
240241
* group. Not deduplicated and may overestimate when ancestors are shared. */
241242
size_t m_ancestors{0};
242-
/** The maximum count of descendants of a single UTXO in this output group. */
243-
size_t m_descendants{0};
243+
/** The maximum cluster count of a single UTXO in this output group. */
244+
size_t m_max_cluster_count{0};
244245
/** The value of the UTXOs after deducting the cost of spending them at the effective feerate. */
245246
CAmount effective_value{0};
246247
/** The fee to spend these UTXOs at the effective feerate. */
@@ -263,7 +264,7 @@ struct OutputGroup
263264
m_subtract_fee_outputs(params.m_subtract_fee_outputs)
264265
{}
265266

266-
void Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants);
267+
void Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t cluster_count);
267268
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
268269
CAmount GetSelectionAmount() const;
269270
};

src/wallet/spend.cpp

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,8 @@ CoinsResult AvailableCoins(const CWallet& wallet,
405405
if (wtx.truc_child_in_mempool.has_value()) continue;
406406

407407
// this unconfirmed v3 transaction has a parent: spending would create a third generation
408-
size_t ancestors, descendants;
409-
wallet.chain().getTransactionAncestry(wtx.tx->GetHash(), ancestors, descendants);
408+
size_t ancestors, unused_cluster_count;
409+
wallet.chain().getTransactionAncestry(wtx.tx->GetHash(), ancestors, unused_cluster_count);
410410
if (ancestors > 1) continue;
411411
} else {
412412
if (wtx.tx->version == TRUC_VERSION) continue;
@@ -866,11 +866,16 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
866866

867867
util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmount& value_to_select, const CoinSelectionParams& coin_selection_params)
868868
{
869+
// Try to enforce a mixture of cluster limits and ancestor/descendant limits on transactions we create by limiting
870+
// the ancestors and the maximum cluster count of any UTXO we use. We use the ancestor/descendant limits, which are
871+
// lower than the cluster limits, to avoid exceeding any ancestor/descendant limits of legacy nodes. This filter is safe
872+
// because a transaction's ancestor or descendant count cannot be larger than its cluster count.
873+
// TODO: these limits can be relaxed in the future, and we can replace the ancestor filter with a cluster equivalent.
869874
unsigned int limit_ancestor_count = 0;
870875
unsigned int limit_descendant_count = 0;
871876
wallet.chain().getPackageLimits(limit_ancestor_count, limit_descendant_count);
872877
const size_t max_ancestors = (size_t)std::max<int64_t>(1, limit_ancestor_count);
873-
const size_t max_descendants = (size_t)std::max<int64_t>(1, limit_descendant_count);
878+
const size_t max_cluster_count = (size_t)std::max<int64_t>(1, limit_descendant_count);
874879
const bool fRejectLongChains = gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS);
875880

876881
// Cases where we have 101+ outputs all pointing to the same destination may result in
@@ -895,20 +900,21 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
895900
// possible) if we cannot fund the transaction otherwise.
896901
if (wallet.m_spend_zero_conf_change) {
897902
ordered_filters.push_back({CoinEligibilityFilter(0, 1, 2)});
898-
ordered_filters.push_back({CoinEligibilityFilter(0, 1, std::min(size_t{4}, max_ancestors/3), std::min(size_t{4}, max_descendants/3))});
899-
ordered_filters.push_back({CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2)});
903+
ordered_filters.push_back({CoinEligibilityFilter(0, 1, std::min(size_t{4}, max_ancestors/3), std::min(size_t{4}, max_cluster_count/3))});
904+
ordered_filters.push_back({CoinEligibilityFilter(0, 1, max_ancestors/2, max_cluster_count/2)});
900905
// If partial groups are allowed, relax the requirement of spending OutputGroups (groups
901906
// of UTXOs sent to the same address, which are obviously controlled by a single wallet)
902907
// in their entirety.
903-
ordered_filters.push_back({CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, /*include_partial=*/true)});
908+
ordered_filters.push_back({CoinEligibilityFilter(0, 1, max_ancestors-1, max_cluster_count-1, /*include_partial=*/true)});
904909
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
905910
// received from other wallets.
906911
if (coin_selection_params.m_include_unsafe_inputs) {
907-
ordered_filters.push_back({CoinEligibilityFilter(/*conf_mine=*/0, /*conf_theirs*/0, max_ancestors-1, max_descendants-1, /*include_partial=*/true)});
912+
ordered_filters.push_back({CoinEligibilityFilter(/*conf_mine=*/0, /*conf_theirs*/0, max_ancestors-1, max_cluster_count-1, /*include_partial=*/true)});
908913
}
909-
// Try with unlimited ancestors/descendants. The transaction will still need to meet
910-
// mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but
911-
// OutputGroups use heuristics that may overestimate ancestor/descendant counts.
914+
// Try with unlimited ancestors/clusters. The transaction will still need to meet
915+
// local mempool policy (i.e. cluster limits) to be accepted to mempool and broadcasted, and
916+
// limits of other nodes (e.g. ancestor/descendant limits) to propagate, but OutputGroups
917+
// use heuristics that may overestimate.
912918
if (!fRejectLongChains) {
913919
ordered_filters.push_back({CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max(),
914920
std::numeric_limits<uint64_t>::max(),
@@ -925,7 +931,7 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
925931
CAmount total_unconf_long_chain = 0;
926932
for (const auto& group : discarded_groups) {
927933
total_discarded += group.GetSelectionAmount();
928-
if (group.m_ancestors >= max_ancestors || group.m_descendants >= max_descendants) total_unconf_long_chain += group.GetSelectionAmount();
934+
if (group.m_ancestors >= max_ancestors || group.m_max_cluster_count >= max_cluster_count) total_unconf_long_chain += group.GetSelectionAmount();
929935
}
930936

931937
if (CAmount total_amount = available_coins.GetTotalAmount() - total_discarded < value_to_select) {

src/wallet/test/coinselection_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ static OutputGroup MakeCoin(const CAmount& amount, bool is_eff_value = true, Coi
5353
tx.vout[0].nValue = amount + int(is_eff_value) * fees;
5454
tx.nLockTime = next_lock_time++; // so all transactions get different hashes
5555
OutputGroup group(cs_params);
56-
group.Insert(std::make_shared<COutput>(COutPoint(tx.GetHash(), 0), tx.vout.at(0), /*depth=*/1, /*input_bytes=*/custom_spending_vsize, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/fees), /*ancestors=*/0, /*descendants=*/0);
56+
group.Insert(std::make_shared<COutput>(COutPoint(tx.GetHash(), 0), tx.vout.at(0), /*depth=*/1, /*input_bytes=*/custom_spending_vsize, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/fees), /*ancestors=*/0, /*cluster_count=*/0);
5757
return group;
5858
}
5959

src/wallet/test/coinselector_tests.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
4545
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
4646
COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/ 0);
4747
OutputGroup group;
48-
group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*descendants=*/ 0);
48+
group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*cluster_count=*/ 0);
4949
result.AddInput(group);
5050
}
5151

@@ -57,7 +57,7 @@ static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result,
5757
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
5858
std::shared_ptr<COutput> coin = std::make_shared<COutput>(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/148, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fee);
5959
OutputGroup group;
60-
group.Insert(coin, /*ancestors=*/ 0, /*descendants=*/ 0);
60+
group.Insert(coin, /*ancestors=*/ 0, /*cluster_count=*/ 0);
6161
coin->long_term_fee = long_term_fee; // group.Insert() will modify long_term_fee, so we need to set it afterwards
6262
result.AddInput(group);
6363
}
@@ -131,7 +131,7 @@ inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& availabl
131131
for (auto& coin : available_coins) {
132132
static_groups.emplace_back();
133133
OutputGroup& group = static_groups.back();
134-
group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*descendants=*/ 0);
134+
group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*cluster_count=*/ 0);
135135
group.m_subtract_fee_outputs = subtract_fee_outputs;
136136
}
137137
return static_groups;

src/wallet/test/fuzz/coinselection.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ static void GroupCoins(FuzzedDataProvider& fuzzed_data_provider, const std::vect
3333
bool valid_outputgroup{false};
3434
for (auto& coin : coins) {
3535
if (!positive_only || (positive_only && coin.GetEffectiveValue() > 0)) {
36-
output_group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/0, /*descendants=*/0);
36+
output_group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/0, /*cluster_count=*/0);
3737
}
3838
// If positive_only was specified, nothing was inserted, leading to an empty output group
3939
// that would be invalid for the BnB algorithm
@@ -167,7 +167,7 @@ FUZZ_TARGET(coin_grinder_is_optimal)
167167
max_spendable += eff_value;
168168

169169
auto output_group = OutputGroup(coin_params);
170-
output_group.Insert(std::make_shared<COutput>(temp_utxo_pool.at(0)), /*ancestors=*/0, /*descendants=*/0);
170+
output_group.Insert(std::make_shared<COutput>(temp_utxo_pool.at(0)), /*ancestors=*/0, /*cluster_count=*/0);
171171
group_pos.push_back(output_group);
172172
}
173173
size_t num_groups = group_pos.size();

0 commit comments

Comments
 (0)