Merge 4ea3a8b71d into merged_master (Bitcoin PR bitcoin/bitcoin#25806)

This commit is contained in:
Tom Trevethan 2025-04-09 14:10:58 +00:00
commit bff78b8a4e
9 changed files with 504 additions and 177 deletions

View file

@ -185,7 +185,8 @@ BITCOIN_TESTS += \
wallet/test/ismine_tests.cpp \
wallet/test/rpc_util_tests.cpp \
wallet/test/scriptpubkeyman_tests.cpp \
wallet/test/walletload_tests.cpp
wallet/test/walletload_tests.cpp \
wallet/test/group_outputs_tests.cpp
# ELEMENTS: disable PSBT test
# wallet/test/psbt_wallet_tests.cpp

View file

@ -77,10 +77,11 @@ static void CoinSelection(benchmark::Bench& bench)
/*tx_noinputs_size=*/ 0,
/*avoid_partial=*/ false,
};
auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard];
bench.run([&] {
CAmountMap mapValue;
mapValue[::policyAsset] = 1003 * COIN;
auto result = AttemptSelection(wallet, mapValue, filter_standard, available_coins, coin_selection_params, /*allow_mixed_output_types=*/true);
auto result = AttemptSelection(mapValue, group, coin_selection_params, /*allow_mixed_output_types=*/true);
assert(result);
assert(result->GetSelectedValue() == mapValue);
assert(result->GetInputSet().size() == 2);
@ -109,7 +110,7 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>
CWalletTx wtx(MakeTransactionRef(tx), TxStateInactive{});
COutput output(COutPoint(wtx.GetHash(), nInput), wtx.tx->vout.at(nInput), /*depth=*/ 0, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ true, /*fees=*/ 0);
set.emplace_back();
set.back().Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false);
set.back().Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*descendants=*/ 0);
}
// Copied from src/wallet/test/coinselector_tests.cpp
static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool)

View file

@ -391,14 +391,14 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
// - no groups that are already used in the input set
for (const OutputGroup& g : groups) {
bool add = true;
for (const COutput& c : g.m_outputs) {
for (const std::shared_ptr<wallet::COutput>& c : g.m_outputs) {
auto input_set = result.GetInputSet();
if (input_set.find(c) != input_set.end()) {
add = false;
break;
}
if (c.asset != it->first) {
if (c->asset != it->first) {
add = false;
break;
}
@ -416,8 +416,8 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
if (auto inner_result = KnapsackSolver(inner_groups, it->second, change_target, rng, it->first)) {
auto set = inner_result->GetInputSet();
for (const COutput& ic : set) {
non_policy_effective_value += ic.GetEffectiveValue();
for (const std::shared_ptr<wallet::COutput>& ic : set) {
non_policy_effective_value += ic->GetEffectiveValue();
}
result.AddInput(inner_result.value());
} else {
@ -440,14 +440,14 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
// - no groups that are already used in setCoinsRet
for (const OutputGroup& g : groups) {
bool add = true;
for (const COutput& c : g.m_outputs) {
for (const std::shared_ptr<wallet::COutput>& c : g.m_outputs) {
auto set = result.GetInputSet();
if (set.find(c) != set.end()) {
add = false;
break;
}
if (c.asset != ::policyAsset) {
if (c->asset != ::policyAsset) {
add = false;
break;
}
@ -557,12 +557,9 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
******************************************************************************/
void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only) {
// Filter for positive only here before adding the coin
if (positive_only && output.GetEffectiveValue() <= 0) return;
void OutputGroup::Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants) {
m_outputs.push_back(output);
COutput& coin = m_outputs.back();
auto& coin = *m_outputs.back();
fee += coin.GetFee();
@ -573,7 +570,7 @@ void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descend
m_from_me &= coin.from_me;
m_value += coin.value;
m_depth = std::min(m_depth, output.depth);
m_depth = std::min(m_depth, output->depth);
// ancestors here express the number of ancestors the new coin will end up having, which is
// the sum, rather than the max; this will overestimate in the cases where multiple inputs
// have common ancestors
@ -582,8 +579,8 @@ void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descend
// coin itself; thus, this value is counted as the max, not the sum
m_descendants = std::max(m_descendants, descendants);
if (output.input_bytes > 0) {
m_weight += output.input_bytes * WITNESS_SCALE_FACTOR;
if (output->input_bytes > 0) {
m_weight += output->input_bytes * WITNESS_SCALE_FACTOR;
}
}
@ -598,13 +595,35 @@ CAmount OutputGroup::GetSelectionAmount() const
{
// ELEMENTS: non-policy assets always use `m_value`. Their (negative)
// `effective_value` will be added to the target for the policy asset
if (!m_outputs.empty() && m_outputs[0].asset != ::policyAsset) {
if (!m_outputs.empty() && m_outputs[0]->asset != ::policyAsset) {
return m_value;
}
return m_subtract_fee_outputs ? m_value : effective_value;
}
CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
void OutputGroupTypeMap::Push(const OutputGroup& group, OutputType type, bool insert_positive, bool insert_mixed)
{
if (group.m_outputs.empty()) return;
Groups& groups = groups_by_type[type];
if (insert_positive && group.GetSelectionAmount() > 0) {
groups.positive_group.emplace_back(group);
all_groups.positive_group.emplace_back(group);
}
if (insert_mixed) {
groups.mixed_group.emplace_back(group);
all_groups.mixed_group.emplace_back(group);
}
}
std::optional<Groups> OutputGroupTypeMap::Find(OutputType type)
{
auto it_by_type = groups_by_type.find(type);
if (it_by_type == groups_by_type.end()) return std::nullopt;
return it_by_type->second;
}
CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
{
// This function should not be called with empty inputs as that would mean the selection failed
assert(!inputs.empty());
@ -612,7 +631,8 @@ CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost,
// Always consider the cost of spending an input now vs in the future.
CAmount waste = 0;
CAmount selected_effective_value = 0;
for (const COutput& coin : inputs) {
for (const auto& coin_ptr : inputs) {
const COutput& coin = *coin_ptr;
waste += coin.GetFee() - coin.long_term_fee;
selected_effective_value += use_effective_value ? coin.GetEffectiveValue() : coin.value;
}
@ -632,21 +652,21 @@ CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost,
}
// ELEMENTS:
CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, const CAmountMap& target_map, bool use_effective_value)
CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, const CAmountMap& target_map, bool use_effective_value)
{
// This function should not be called with empty inputs as that would mean the selection failed
assert(!inputs.empty());
// create a map of asset -> coins from the inputs set
std::map<CAsset, std::set<COutput>> coinset_map;
for(auto it = inputs.begin(); it != inputs.end(); ++it) {
auto asset = it->asset;
for(const auto& coin_ptr : inputs) {
auto asset = coin_ptr->asset;
auto search = coinset_map.find(asset);
if (search != coinset_map.end()) {
search->second.insert(*it);
search->second.insert(*coin_ptr);
} else {
std::set<COutput> coinset;
coinset.insert(*it);
coinset.insert(*coin_ptr);
coinset_map.insert({asset, coinset});
}
}
@ -714,12 +734,12 @@ CAmount SelectionResult::GetWaste() const
CAmountMap SelectionResult::GetSelectedValue() const
{
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmountMap{}, [](CAmountMap sum, const auto& coin) { return sum + CAmountMap{{coin.asset, coin.value}}; });
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmountMap{}, [](CAmountMap sum, const auto& coin) { return sum + CAmountMap{{coin->asset, coin->value}}; });
}
CAmountMap SelectionResult::GetSelectedEffectiveValue() const
{
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmountMap{}, [](CAmountMap sum, const auto& coin) { return sum + CAmountMap{{coin.asset, coin.GetEffectiveValue()}}; });
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmountMap{}, [](CAmountMap sum, const auto& coin) { return sum + CAmountMap{{coin->asset, coin->GetEffectiveValue()}}; });
}
void SelectionResult::Clear()
@ -738,14 +758,14 @@ void SelectionResult::AddInput(const OutputGroup& group)
m_weight += group.m_weight;
}
void SelectionResult::AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs)
void SelectionResult::AddInputs(const std::set<std::shared_ptr<COutput>>& inputs, bool subtract_fee_outputs)
{
// As it can fail, combine inputs first
InsertInputs(inputs);
m_use_effective = !subtract_fee_outputs;
m_weight += std::accumulate(inputs.cbegin(), inputs.cend(), 0, [](int sum, const auto& coin) {
return sum + std::max(coin.input_bytes, 0) * WITNESS_SCALE_FACTOR;
return sum + std::max(coin->input_bytes, 0) * WITNESS_SCALE_FACTOR;
});
}
@ -753,7 +773,7 @@ void SelectionResult::AddInputs(const std::set<COutput>& inputs, bool subtract_f
void SelectionResult::AddInput(const SelectionResult& result) {
util::insert(m_selected_inputs, result.GetInputSet());
m_weight += std::accumulate(result.GetInputSet().cbegin(), result.GetInputSet().cend(), 0, [](int sum, const auto& coin) {
return sum + std::max(coin.input_bytes, 0) * WITNESS_SCALE_FACTOR;
return sum + std::max(coin->input_bytes, 0) * WITNESS_SCALE_FACTOR;
});
}
@ -771,14 +791,14 @@ void SelectionResult::Merge(const SelectionResult& other)
m_weight += other.m_weight;
}
const std::set<COutput>& SelectionResult::GetInputSet() const
const std::set<std::shared_ptr<COutput>>& SelectionResult::GetInputSet() const
{
return m_selected_inputs;
}
std::vector<COutput> SelectionResult::GetShuffledInputVector() const
std::vector<std::shared_ptr<COutput>> SelectionResult::GetShuffledInputVector() const
{
std::vector<COutput> coins(m_selected_inputs.begin(), m_selected_inputs.end());
std::vector<std::shared_ptr<COutput>> coins(m_selected_inputs.begin(), m_selected_inputs.end());
Shuffle(coins.begin(), coins.end(), FastRandomContext());
return coins;
}

View file

@ -8,6 +8,7 @@
#include <chainparams.h>
#include <consensus/amount.h>
#include <consensus/consensus.h>
#include <outputtype.h>
#include <policy/feerate.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
@ -196,13 +197,18 @@ struct CoinEligibilityFilter
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) {}
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) {}
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) {}
bool operator<(const CoinEligibilityFilter& other) const {
return std::tie(conf_mine, conf_theirs, max_ancestors, max_descendants, m_include_partial_groups)
< std::tie(other.conf_mine, other.conf_theirs, other.max_ancestors, other.max_descendants, other.m_include_partial_groups);
}
};
/** A group of UTXOs paid to the same output script. */
struct OutputGroup
{
/** The list of UTXOs contained in this output group. */
std::vector<COutput> m_outputs;
std::vector<std::shared_ptr<COutput>> m_outputs;
/** Whether the UTXOs were sent by the wallet to itself. This is relevant because we may want at
* least a certain number of confirmations on UTXOs received from outside wallets while trusting
* our own UTXOs more. */
@ -241,11 +247,37 @@ struct OutputGroup
m_subtract_fee_outputs(params.m_subtract_fee_outputs)
{}
void Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only);
void Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants);
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
CAmount GetSelectionAmount() const;
};
struct Groups {
// Stores 'OutputGroup' containing only positive UTXOs (value > 0).
std::vector<OutputGroup> positive_group;
// Stores 'OutputGroup' which may contain both positive and negative UTXOs.
std::vector<OutputGroup> mixed_group;
};
/** Stores several 'Groups' whose were mapped by output type. */
struct OutputGroupTypeMap
{
// Maps output type to output groups.
std::map<OutputType, Groups> groups_by_type;
// All inserted groups, no type distinction.
Groups all_groups;
// Based on the insert flag; appends group to the 'mixed_group' and, if value > 0, to the 'positive_group'.
// This affects both; the groups filtered by type and the overall groups container.
void Push(const OutputGroup& group, OutputType type, bool insert_positive, bool insert_mixed);
// Retrieves 'Groups' filtered by type
std::optional<Groups> Find(OutputType type);
// Different output types count
size_t TypesCount() { return groups_by_type.size(); }
};
typedef std::map<CoinEligibilityFilter, OutputGroupTypeMap> FilteredOutputGroups;
/** Compute the waste for this result given the cost of change
* and the opportunity cost of spending these inputs now vs in the future.
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
@ -263,7 +295,7 @@ struct OutputGroup
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
* @return The waste
*/
[[nodiscard]] CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
[[nodiscard]] CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
/** Choose a random change target for each transaction to make it harder to fingerprint the Core
@ -296,7 +328,7 @@ struct SelectionResult
{
private:
/** Set of inputs selected by the algorithm to use in the transaction */
std::set<COutput> m_selected_inputs;
std::set<std::shared_ptr<COutput>> m_selected_inputs;
/** The target the algorithm selected for. Equal to the recipient amount plus non-input fees */
CAmountMap m_target;
/** The algorithm used to produce this result */
@ -333,7 +365,7 @@ public:
void Clear();
void AddInput(const OutputGroup& group);
void AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs);
void AddInputs(const std::set<std::shared_ptr<COutput>>& inputs, bool subtract_fee_outputs);
// ELEMENTS
void AddInput(const SelectionResult& result);
@ -350,9 +382,9 @@ public:
void Merge(const SelectionResult& other);
/** Get m_selected_inputs */
const std::set<COutput>& GetInputSet() const;
const std::set<std::shared_ptr<COutput>>& GetInputSet() const;
/** Get the vector of COutputs that will be used to fill in a CTransaction's vin */
std::vector<COutput> GetShuffledInputVector() const;
std::vector<std::shared_ptr<COutput>> GetShuffledInputVector() const;
bool operator<(SelectionResult other) const;

View file

@ -441,28 +441,37 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
return result;
}
std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only)
FilteredOutputGroups GroupOutputs(const CWallet& wallet,
const CoinsResult& coins,
const CoinSelectionParams& coin_sel_params,
const std::vector<SelectionFilter>& filters)
{
std::vector<OutputGroup> groups_out;
FilteredOutputGroups filtered_groups;
if (!coin_sel_params.m_avoid_partial_spends) {
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup.
for (const COutput& output : outputs) {
// Skip outputs we cannot spend
if (!output.spendable) continue;
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup
for (const auto& [type, outputs] : coins.coins) {
for (const COutput& output : outputs) {
// Skip outputs we cannot spend
if (!output.spendable) continue;
size_t ancestors, descendants;
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
// Get mempool info
size_t ancestors, descendants;
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
// Make an OutputGroup containing just this output
OutputGroup group{coin_sel_params};
group.Insert(output, ancestors, descendants, positive_only);
// Create a new group per output and add it to the all groups vector
OutputGroup group(coin_sel_params);
group.Insert(std::make_shared<COutput>(output), ancestors, descendants);
// Check the OutputGroup's eligibility. Only add the eligible ones.
if (positive_only && group.GetSelectionAmount() <= 0) continue;
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
// Each filter maps to a different set of groups
for (const auto& sel_filter : filters) {
const auto& filter = sel_filter.filter;
if (!group.EligibleForSpending(filter)) continue;
filtered_groups[filter].Push(group, type, /*insert_positive=*/true, /*insert_mixed=*/true);
}
}
}
return groups_out;
return filtered_groups;
}
// We want to combine COutputs that have the same scriptPubKey into single OutputGroups
@ -471,16 +480,12 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
// For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput is added
// to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has
// OUTPUT_GROUP_MAX_ENTRIES COutputs, a new OutputGroup is added to the end of the vector.
std::map<CScript, std::vector<OutputGroup>> spk_to_groups_map;
for (const auto& output : outputs) {
// Skip outputs we cannot spend
if (!output.spendable) continue;
size_t ancestors, descendants;
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
CScript spk = output.txout.scriptPubKey;
std::vector<OutputGroup>& groups = spk_to_groups_map[spk];
typedef std::map<std::pair<CScript, OutputType>, std::vector<OutputGroup>> ScriptPubKeyToOutgroup;
const auto& group_outputs = [](
const COutput& output, OutputType type, size_t ancestors, size_t descendants,
ScriptPubKeyToOutgroup& groups_map, const CoinSelectionParams& coin_sel_params,
bool positive_only) {
std::vector<OutputGroup>& groups = groups_map[std::make_pair(output.txout.scriptPubKey,type)];
if (groups.size() == 0) {
// No OutputGroups for this scriptPubKey yet, add one
@ -499,42 +504,69 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
group = &groups.back();
}
// Add the output to group
group->Insert(output, ancestors, descendants, positive_only);
}
// Filter for positive only before adding the output to group
if (!positive_only || output.GetEffectiveValue() > 0) {
group->Insert(std::make_shared<COutput>(output), ancestors, descendants);
}
};
// Now we go through the entire map and pull out the OutputGroups
for (const auto& spk_and_groups_pair: spk_to_groups_map) {
const std::vector<OutputGroup>& groups_per_spk= spk_and_groups_pair.second;
ScriptPubKeyToOutgroup spk_to_groups_map;
ScriptPubKeyToOutgroup spk_to_positive_groups_map;
for (const auto& [type, outs] : coins.coins) {
for (const COutput& output : outs) {
// Skip outputs we cannot spend
if (!output.spendable) continue;
// Go through the vector backwards. This allows for the first item we deal with being the partial group.
for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) {
const OutputGroup& group = *group_it;
size_t ancestors, descendants;
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
// Don't include partial groups if there are full groups too and we don't want partial groups
if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) {
continue;
}
// Check the OutputGroup's eligibility. Only add the eligible ones.
if (positive_only && group.GetSelectionAmount() <= 0) continue;
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
group_outputs(output, type, ancestors, descendants, spk_to_groups_map, coin_sel_params, /*positive_only=*/ false);
group_outputs(output, type, ancestors, descendants, spk_to_positive_groups_map,
coin_sel_params, /*positive_only=*/ true);
}
}
return groups_out;
// Now we go through the entire maps and pull out the OutputGroups
const auto& push_output_groups = [&](const ScriptPubKeyToOutgroup& groups_map, bool positive_only) {
for (const auto& [script, groups] : groups_map) {
// Go through the vector backwards. This allows for the first item we deal with being the partial group.
for (auto group_it = groups.rbegin(); group_it != groups.rend(); group_it++) {
const OutputGroup& group = *group_it;
// Each filter maps to a different set of groups
for (const auto& sel_filter : filters) {
const auto& filter = sel_filter.filter;
if (!group.EligibleForSpending(filter)) continue;
// Don't include partial groups if there are full groups too and we don't want partial groups
if (group_it == groups.rbegin() && groups.size() > 1 && !filter.m_include_partial_groups) {
continue;
}
OutputType type = script.second;
// Either insert the group into the positive-only groups or the mixed ones.
filtered_groups[filter].Push(group, type, positive_only, /*insert_mixed=*/!positive_only);
}
}
}
};
push_output_groups(spk_to_groups_map, /*positive_only=*/ false);
push_output_groups(spk_to_positive_groups_map, /*positive_only=*/ true);
return filtered_groups;
}
// Returns true if the result contains an error and the message is not empty
static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmountMap& mapTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
util::Result<SelectionResult> AttemptSelection(const CAmountMap& mapTargetValue, OutputGroupTypeMap& groups,
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types)
{
// Run coin selection on each OutputType and compute the Waste Metric
std::vector<SelectionResult> results;
for (const auto& it : available_coins.coins) {
auto result{ChooseSelectionResult(wallet, mapTargetValue, eligibility_filter, it.second, coin_selection_params)};
for (auto& [type, group] : groups.groups_by_type) {
auto result{ChooseSelectionResult(mapTargetValue, group, coin_selection_params)};
// If any specific error message appears here, then something particularly wrong happened.
if (HasErrorMsg(result)) return result; // So let's return the specific error.
// Append the favorable result.
@ -548,15 +580,15 @@ util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmo
// If we can't fund the transaction from any individual OutputType, run coin selection one last time
// over all available coins, which would allow mixing
// If TypesCount() <= 1, there is nothing to mix.
if (allow_mixed_output_types && available_coins.TypesCount() > 1) {
return ChooseSelectionResult(wallet, mapTargetValue, eligibility_filter, available_coins.All(), coin_selection_params);
if (allow_mixed_output_types && groups.TypesCount() > 1) {
return ChooseSelectionResult(mapTargetValue, groups.all_groups, coin_selection_params);
}
// Either mixing is not allowed and we couldn't find a solution from any single OutputType, or mixing was allowed and we still couldn't
// find a solution using all available coins
return util::Error();
};
util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmountMap& mapTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins, const CoinSelectionParams& coin_selection_params)
util::Result<SelectionResult> ChooseSelectionResult(const CAmountMap& mapTargetValue, Groups& groups, const CoinSelectionParams& coin_selection_params)
{
// Vector of results. We will choose the best one based on waste.
// std::vector<std::tuple<CAmount, std::set<CInputCoin>, CAmountMap>> results;
@ -565,7 +597,6 @@ util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const
// ELEMENTS: BnB only for policy asset?
if (mapTargetValue.size() == 1) {
// 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, available_coins, coin_selection_params, eligibility_filter, /*positive_only=*/true);
// ELEMENTS:
CAsset asset = mapTargetValue.begin()->first;
@ -578,10 +609,10 @@ util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const
}
// Get output groups that only contain this asset.
std::vector<OutputGroup> asset_groups;
for (const OutputGroup& g : positive_groups) {
for (const OutputGroup& g : groups.positive_group) {
bool add = true;
for (const COutput& c : g.m_outputs) {
if (c.asset != asset) {
for (const std::shared_ptr<wallet::COutput>& c : g.m_outputs) {
if (c->asset != asset) {
add = false;
break;
}
@ -593,7 +624,7 @@ util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const
}
// END ELEMENTS
if (auto bnb_result{SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change)}) {
if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change)}) {
results.push_back(*bnb_result);
}
@ -602,14 +633,13 @@ util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const
// generated one, since SRD will result in a random change amount anyway; avoid making the
// target needlessly large.
const CAmount srd_target = target_with_change + CHANGE_LOWER;
if (auto srd_result{SelectCoinsSRD(positive_groups, srd_target, coin_selection_params.rng_fast)}) {
if (auto srd_result{SelectCoinsSRD(groups.positive_group, srd_target, coin_selection_params.rng_fast)}) {
srd_result->ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
results.push_back(*srd_result);
}
}
// The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here.
std::vector<OutputGroup> all_groups = GroupOutputs(wallet, available_coins, coin_selection_params, eligibility_filter, /*positive_only=*/false);
// While mapTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
CAmountMap mapTargetValue_copy = mapTargetValue;
@ -623,7 +653,7 @@ util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const
if (!coin_selection_params.m_subtract_fee_outputs) {
map_target_with_change[::policyAsset] += coin_selection_params.m_change_fee;
}
if (auto knapsack_result{KnapsackSolver(all_groups, mapTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
if (auto knapsack_result{KnapsackSolver(groups.mixed_group, mapTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
knapsack_result->ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
results.push_back(*knapsack_result);
}
@ -693,11 +723,6 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
return op_selection_result;
}
struct SelectionFilter {
CoinEligibilityFilter filter;
bool allow_mixed_output_types{true};
};
util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmountMap& value_to_select, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
{
unsigned int limit_ancestor_count = 0;
@ -761,12 +786,17 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
}
}
// Group outputs and map them by coin eligibility filter
FilteredOutputGroups filtered_groups = GroupOutputs(wallet, available_coins, coin_selection_params, ordered_filters);
// Walk-through the filters until the solution gets found.
// If no solution is found, return the first detailed error (if any).
// future: add "error level" so the worst one can be picked instead.
std::vector<util::Result<SelectionResult>> res_detailed_errors;
for (const auto& select_filter : ordered_filters) {
if (auto res{AttemptSelection(wallet, value_to_select, select_filter.filter, available_coins,
auto it = filtered_groups.find(select_filter.filter);
if (it == filtered_groups.end()) continue;
if (auto res{AttemptSelection(value_to_select, it->second,
coin_selection_params, select_filter.allow_mixed_output_types)}) {
return res; // result found
} else {
@ -880,16 +910,16 @@ static void resetBlindDetails(BlindDetails* det, bool preserve_output_data = fal
}
}
static bool fillBlindDetails(BlindDetails* det, CWallet* wallet, CMutableTransaction& txNew, std::vector<COutput>& selected_coins, bilingual_str& error) {
static bool fillBlindDetails(BlindDetails* det, CWallet* wallet, CMutableTransaction& txNew, std::vector<std::shared_ptr<COutput>>& selected_coins, bilingual_str& error) {
int num_inputs_blinded = 0;
// Fill in input blinding details
for (const COutput& coin : selected_coins) {
det->i_amount_blinds.push_back(coin.bf_value);
det->i_asset_blinds.push_back(coin.bf_asset);
det->i_assets.push_back(coin.asset);
det->i_amounts.push_back(coin.value);
if (coin.txout.nValue.IsCommitment() || coin.txout.nAsset.IsCommitment()) {
for (const std::shared_ptr<wallet::COutput>& coin : selected_coins) {
det->i_amount_blinds.push_back(coin->bf_value);
det->i_asset_blinds.push_back(coin->bf_asset);
det->i_assets.push_back(coin->asset);
det->i_amounts.push_back(coin->value);
if (coin->txout.nValue.IsCommitment() || coin->txout.nAsset.IsCommitment()) {
num_inputs_blinded++;
}
}
@ -1272,7 +1302,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
if (may_need_blinded_dummy) {
may_need_blinded_dummy = false;
for (const auto& coin : result.GetInputSet()) {
if (!coin.txout.nValue.IsExplicit()) {
if (!coin->txout.nValue.IsExplicit()) {
may_need_blinded_dummy = true;
break;
}
@ -1395,7 +1425,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
// selected_coins = std::vector<CInputCoin>(setCoins.begin(), setCoins.end());
// Shuffle(selected_coins.begin(), selected_coins.end(), FastRandomContext());
// Shuffle selected coins and fill in final vin
std::vector<COutput> selected_coins = result.GetShuffledInputVector();
std::vector<std::shared_ptr<COutput>> selected_coins = result.GetShuffledInputVector();
// The sequence number is set to non-maxint so that DiscourageFeeSniping
// works.
@ -1407,11 +1437,11 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
// behavior."
const uint32_t nSequence{coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf) ? MAX_BIP125_RBF_SEQUENCE : CTxIn::MAX_SEQUENCE_NONFINAL};
for (const auto& coin : selected_coins) {
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence));
txNew.vin.push_back(CTxIn(coin->outpoint, CScript(), nSequence));
if (issuance_details && coin.asset == issuance_details->reissuance_token) {
if (issuance_details && coin->asset == issuance_details->reissuance_token) {
reissuance_index = txNew.vin.size() - 1;
token_blinding = coin.bf_asset;
token_blinding = coin->bf_asset;
}
}
DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight());
@ -1688,10 +1718,10 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
summary += " ";
}
summary += strprintf("#%d: %s [%s] (%s [%s])\n", i,
selected_coins[i].value,
selected_coins[i].txout.nValue.IsExplicit() ? "explicit" : "blinded",
selected_coins[i].asset.GetHex(),
selected_coins[i].txout.nAsset.IsExplicit() ? "explicit" : "blinded"
selected_coins[i]->value,
selected_coins[i]->txout.nValue.IsExplicit() ? "explicit" : "blinded",
selected_coins[i]->asset.GetHex(),
selected_coins[i]->txout.nAsset.IsExplicit() ? "explicit" : "blinded"
);
}
summary += "OUT: ";

View file

@ -111,17 +111,27 @@ const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint&
*/
std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet);
std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only);
struct SelectionFilter {
CoinEligibilityFilter filter;
bool allow_mixed_output_types{true};
};
/**
* Group coins by the provided filters.
*/
FilteredOutputGroups GroupOutputs(const CWallet& wallet,
const CoinsResult& coins,
const CoinSelectionParams& coin_sel_params,
const std::vector<SelectionFilter>& filters);
/**
* Attempt to find a valid input set that preserves privacy by not mixing OutputTypes.
* `ChooseSelectionResult()` will be called on each OutputType individually and the best
* the solution (according to the waste metric) will be chosen. If a valid input cannot be found from any
* single OutputType, fallback to running `ChooseSelectionResult()` over all available coins.
*
* param@[in] wallet The wallet which provides solving data for the coins
* param@[in] nTargetValue The target value
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
* param@[in] groups The grouped outputs mapped by coin eligibility filters
* param@[in] coin_selection_params Parameters for the coin selection
* param@[in] allow_mixed_output_types Relax restriction that SelectionResults must be of the same OutputType
* returns If successful, a SelectionResult containing the input set
@ -129,7 +139,7 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
* result that surpassed the tx max weight size).
*/
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmountMap& mapTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
util::Result<SelectionResult> AttemptSelection(const CAmountMap& mapTargetValue, OutputGroupTypeMap& groups,
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types);
/**
@ -137,23 +147,20 @@ util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmo
* Multiple coin selection algorithms will be run and the input set that produces the least waste
* (according to the waste metric) will be chosen.
*
* param@[in] wallet The wallet which provides solving data for the coins
* param@[in] nTargetValue The target value
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
* param@[in] groups The struct containing the outputs grouped by script and divided by (1) positive only outputs and (2) all outputs (positive + negative).
* param@[in] coin_selection_params Parameters for the coin selection
* returns If successful, a SelectionResult containing the input set
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
* result that surpassed the tx max weight size).
*/
util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmountMap& mapTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins,
const CoinSelectionParams& coin_selection_params);
util::Result<SelectionResult> ChooseSelectionResult(const CAmountMap& mapTargetValue, Groups& groups, const CoinSelectionParams& coin_selection_params);
// User manually selected inputs that must be part of the transaction
struct PreSelectedInputs
{
std::set<COutput> coins;
std::set<std::shared_ptr<COutput>> coins;
// If subtract fee from outputs is disabled, the 'total_amount'
// will be the sum of each output effective value
// instead of the sum of the outputs amount
@ -166,7 +173,7 @@ struct PreSelectedInputs
} else {
total_amount[output.asset] += output.GetEffectiveValue();
}
coins.insert(output);
coins.insert(std::make_shared<COutput>(output));
}
};

View file

@ -29,7 +29,7 @@ BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup)
// we repeat those tests this many times and only complain if all iterations of the test fail
#define RANDOM_REPEATS 5
typedef std::set<COutput> CoinSet;
typedef std::set<std::shared_ptr<COutput>> CoinSet;
// ELEMENTS
static node::NodeContext testNode;
static auto testChain = interfaces::MakeChain(testNode);
@ -59,7 +59,7 @@ static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
CWalletTx wtx(MakeTransactionRef(tx), TxStateInactive{});
COutput output(testWallet, wtx, COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false);
OutputGroup group;
group.Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ true);
group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*descendants=*/ 0);
result.AddInput(group);
}
@ -73,7 +73,7 @@ static void add_coin(const CAmount& nValue, int nInput, CoinSet& set, CAmount fe
// ELEMENTS FIXME: input_bytes size of 1000 seems to work (I picked it arbitrarily), but bitcoin has 148
COutput coin(testWallet, wtx, COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ 1000, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, CFeeRate(fee));
coin.long_term_fee = long_term_fee;
set.insert(coin);
set.insert(std::make_shared<COutput>(coin));
}
static void add_coin(CoinsResult& available_coins, CWallet& wallet, const CAmount& nValue, CFeeRate feerate = CFeeRate(0), int nAge = 6*24, bool fIsFromMe = false, int nInput =0, bool spendable = false)
@ -102,10 +102,10 @@ static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
std::vector<CAmount> a_amts;
std::vector<CAmount> b_amts;
for (const auto& coin : a.GetInputSet()) {
a_amts.push_back(coin.txout.nValue.GetAmount());
a_amts.push_back(coin->txout.nValue.GetAmount());
}
for (const auto& coin : b.GetInputSet()) {
b_amts.push_back(coin.txout.nValue.GetAmount());
b_amts.push_back(coin->txout.nValue.GetAmount());
}
std::sort(a_amts.begin(), a_amts.end());
std::sort(b_amts.begin(), b_amts.end());
@ -118,8 +118,8 @@ static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
static bool EqualResult(const SelectionResult& a, const SelectionResult& b)
{
std::pair<CoinSet::iterator, CoinSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(),
[](const COutput& a, const COutput& b) {
return a.outpoint == b.outpoint;
[](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) {
return a->outpoint == b->outpoint;
});
return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end();
}
@ -142,12 +142,12 @@ inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& availabl
static_groups.clear();
for (auto& coin : available_coins) {
static_groups.emplace_back();
static_groups.back().Insert(coin, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false);
static_groups.back().Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*descendants=*/ 0);
}
return static_groups;
}
inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput>& available_coins, CWallet& wallet, const CoinEligibilityFilter& filter)
inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& available_coins, CWallet& wallet, const CoinEligibilityFilter& filter)
{
FastRandomContext rand{};
CoinSelectionParams coin_selection_params{
@ -161,9 +161,9 @@ inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput>
/*tx_noinputs_size=*/ 0,
/*avoid_partial=*/ false,
};
static std::vector<OutputGroup> static_groups;
static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, filter, /*positive_only=*/false);
return static_groups;
static OutputGroupTypeMap static_groups;
static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter];
return static_groups.all_groups.mixed_group;
}
// Branch and bound coin selection tests
@ -430,25 +430,25 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
available_coins.Clear();
// with an empty wallet we can't even pay one cent
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard), 1 * CENT, CENT));
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1 * CENT, CENT));
add_coin(available_coins, *wallet, 1*CENT, CFeeRate(0), 4); // add a new 1 cent coin
// with a new 1 cent coin, we still can't find a mature 1 cent
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard), 1 * CENT, CENT));
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1 * CENT, CENT));
// but we can find a new 1 cent
const auto result1 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 1 * CENT, CENT);
const auto result1 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
BOOST_CHECK(result1);
BOOST_CHECK_EQUAL(result1->GetSelectedValue()[::policyAsset], 1 * CENT);
add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin
// we can't make 3 cents of mature coins
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard), 3 * CENT, CENT));
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 3 * CENT, CENT));
// we can make 3 cents of new coins
const auto result2 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 3 * CENT, CENT);
const auto result2 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 3 * CENT, CENT);
BOOST_CHECK(result2);
BOOST_CHECK_EQUAL(result2->GetSelectedValue()[::policyAsset], 3 * CENT);
@ -459,38 +459,38 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
// now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27. total = 38
// we can't make 38 cents only if we disallow new coins:
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard), 38 * CENT, CENT));
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 38 * CENT, CENT));
// we can't even make 37 cents if we don't allow new coins even if they're from us
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard_extra), 38 * CENT, CENT));
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard_extra), 38 * CENT, CENT));
// but we can make 37 cents if we accept new coins from ourself
const auto result3 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard), 37 * CENT, CENT);
const auto result3 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 37 * CENT, CENT);
BOOST_CHECK(result3);
BOOST_CHECK_EQUAL(result3->GetSelectedValue()[::policyAsset], 37 * CENT);
// and we can make 38 cents if we accept all new coins
const auto result4 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 38 * CENT, CENT);
const auto result4 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 38 * CENT, CENT);
BOOST_CHECK(result4);
BOOST_CHECK_EQUAL(result4->GetSelectedValue()[::policyAsset], 38 * CENT);
// try making 34 cents from 1,2,5,10,20 - we can't do it exactly
const auto result5 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 34 * CENT, CENT);
const auto result5 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 34 * CENT, CENT);
BOOST_CHECK(result5);
BOOST_CHECK_EQUAL(result5->GetSelectedValue()[::policyAsset], 35 * CENT); // but 35 cents is closest
BOOST_CHECK_EQUAL(result5->GetInputSet().size(), 3U); // the best should be 20+10+5. it's incredibly unlikely the 1 or 2 got included (but possible)
// when we try making 7 cents, the smaller coins (1,2,5) are enough. We should see just 2+5
const auto result6 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 7 * CENT, CENT);
const auto result6 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 7 * CENT, CENT);
BOOST_CHECK(result6);
BOOST_CHECK_EQUAL(result6->GetSelectedValue()[::policyAsset], 7 * CENT);
BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U);
// when we try making 8 cents, the smaller coins (1,2,5) are exactly enough.
const auto result7 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 8 * CENT, CENT);
const auto result7 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 8 * CENT, CENT);
BOOST_CHECK(result7);
BOOST_CHECK(result7->GetSelectedValue()[::policyAsset] == 8 * CENT);
BOOST_CHECK_EQUAL(result7->GetInputSet().size(), 3U);
// when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10)
const auto result8 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 9 * CENT, CENT);
const auto result8 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 9 * CENT, CENT);
BOOST_CHECK(result8);
BOOST_CHECK_EQUAL(result8->GetSelectedValue()[::policyAsset], 10 * CENT);
BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U);
@ -505,12 +505,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 30*CENT); // now we have 6+7+8+20+30 = 71 cents total
// check that we have 71 and not 72
const auto result9 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 71 * CENT, CENT);
const auto result9 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 71 * CENT, CENT);
BOOST_CHECK(result9);
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 72 * CENT, CENT));
BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 72 * CENT, CENT));
// now try making 16 cents. the best smaller coins can do is 6+7+8 = 21; not as good at the next biggest coin, 20
const auto result10 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 16 * CENT, CENT);
const auto result10 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
BOOST_CHECK(result10);
BOOST_CHECK_EQUAL(result10->GetSelectedValue()[::policyAsset], 20 * CENT); // we should get 20 in one coin
BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U);
@ -518,7 +518,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total
// now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, better than the next biggest coin, 20
const auto result11 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 16 * CENT, CENT);
const auto result11 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
BOOST_CHECK(result11);
BOOST_CHECK_EQUAL(result11->GetSelectedValue()[::policyAsset], 18 * CENT); // we should get 18 in 3 coins
BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U);
@ -526,13 +526,13 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 18*CENT); // now we have 5+6+7+8+18+20+30
// and now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, the same as the next biggest coin, 18
const auto result12 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 16 * CENT, CENT);
const auto result12 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
BOOST_CHECK(result12);
BOOST_CHECK_EQUAL(result12->GetSelectedValue()[::policyAsset], 18 * CENT); // we should get 18 in 1 coin
BOOST_CHECK_EQUAL(result12->GetInputSet().size(), 1U); // because in the event of a tie, the biggest coin wins
// now try making 11 cents. we should get 5+6
const auto result13 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 11 * CENT, CENT);
const auto result13 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 11 * CENT, CENT);
BOOST_CHECK(result13);
BOOST_CHECK_EQUAL(result13->GetSelectedValue()[::policyAsset], 11 * CENT);
BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U);
@ -542,12 +542,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 2*COIN);
add_coin(available_coins, *wallet, 3*COIN);
add_coin(available_coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents
const auto result14 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 95 * CENT, CENT);
const auto result14 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 95 * CENT, CENT);
BOOST_CHECK(result14);
BOOST_CHECK_EQUAL(result14->GetSelectedValue()[::policyAsset], 1 * COIN); // we should get 1 BTC in 1 coin
BOOST_CHECK_EQUAL(result14->GetInputSet().size(), 1U);
const auto result15 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 195 * CENT, CENT);
const auto result15 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 195 * CENT, CENT);
BOOST_CHECK(result15);
BOOST_CHECK_EQUAL(result15->GetSelectedValue()[::policyAsset], 2 * COIN); // we should get 2 BTC in 1 coin
BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U);
@ -563,7 +563,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
// try making 1 * CENT from the 1.5 * CENT
// we'll get change smaller than CENT whatever happens, so can expect CENT exactly
const auto result16 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), CENT, CENT);
const auto result16 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT);
BOOST_CHECK(result16);
BOOST_CHECK_EQUAL(result16->GetSelectedValue()[::policyAsset], CENT);
@ -571,7 +571,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 1111*CENT);
// try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5
const auto result17 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 1 * CENT, CENT);
const auto result17 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
BOOST_CHECK(result17);
BOOST_CHECK_EQUAL(result17->GetSelectedValue()[::policyAsset], 1 * CENT); // we should get the exact amount
@ -580,7 +580,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 7 / 10);
// and try again to make 1.0 * CENT
const auto result18 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 1 * CENT, CENT);
const auto result18 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
BOOST_CHECK(result18);
BOOST_CHECK_EQUAL(result18->GetSelectedValue()[::policyAsset], 1 * CENT); // we should get the exact amount
@ -590,7 +590,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
for (int j = 0; j < 20; j++)
add_coin(available_coins, *wallet, 50000 * COIN);
const auto result19 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 500000 * COIN, CENT);
const auto result19 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 500000 * COIN, CENT);
BOOST_CHECK(result19);
BOOST_CHECK_EQUAL(result19->GetSelectedValue()[::policyAsset], 500000 * COIN); // we should get the exact amount
BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins
@ -604,7 +604,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 6 / 10);
add_coin(available_coins, *wallet, CENT * 7 / 10);
add_coin(available_coins, *wallet, 1111 * CENT);
const auto result20 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 1 * CENT, CENT);
const auto result20 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
BOOST_CHECK(result20);
BOOST_CHECK_EQUAL(result20->GetSelectedValue()[::policyAsset], 1111 * CENT); // we get the bigger coin
BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U);
@ -615,7 +615,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 6 / 10);
add_coin(available_coins, *wallet, CENT * 8 / 10);
add_coin(available_coins, *wallet, 1111 * CENT);
const auto result21 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), CENT, CENT);
const auto result21 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT);
BOOST_CHECK(result21);
BOOST_CHECK_EQUAL(result21->GetSelectedValue()[::policyAsset], CENT); // we should get the exact amount
BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6
@ -627,13 +627,13 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 100);
// trying to make 100.01 from these three coins
const auto result22 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), CENT * 10001 / 100, CENT);
const auto result22 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 10001 / 100, CENT);
BOOST_CHECK(result22);
BOOST_CHECK_EQUAL(result22->GetSelectedValue()[::policyAsset], CENT * 10105 / 100); // we should get all coins
BOOST_CHECK_EQUAL(result22->GetInputSet().size(), 3U);
// but if we try to make 99.9, we should take the bigger of the two small coins to avoid small change
const auto result23 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), CENT * 9990 / 100, CENT);
const auto result23 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 9990 / 100, CENT);
BOOST_CHECK(result23);
BOOST_CHECK_EQUAL(result23->GetSelectedValue()[::policyAsset], 101 * CENT);
BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U);
@ -648,7 +648,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
// We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
for (int i = 0; i < RUN_TESTS; i++) {
const auto result24 = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_confirmed), 2000, CENT);
const auto result24 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 2000, CENT);
BOOST_CHECK(result24);
if (amt - 2000 < CENT) {
@ -739,7 +739,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset)
add_coin(available_coins, *wallet, 1000 * COIN);
add_coin(available_coins, *wallet, 3 * COIN);
const auto result = KnapsackSolver(KnapsackGroupOutputs(available_coins.All(), *wallet, filter_standard), 1003 * COIN, CENT, rand);
const auto result = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1003 * COIN, CENT, rand);
BOOST_CHECK(result);
BOOST_CHECK_EQUAL(result->GetSelectedValue()[::policyAsset], 1003 * COIN);
BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U);
@ -958,7 +958,7 @@ static util::Result<SelectionResult> select_coins(const CAmountMap& target, cons
static bool has_coin(const CoinSet& set, CAmount amount)
{
return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin.GetEffectiveValue() == amount; });
return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin->GetEffectiveValue() == amount; });
}
BOOST_AUTO_TEST_CASE(check_max_weight)

View file

@ -29,8 +29,10 @@ static void GroupCoins(FuzzedDataProvider& fuzzed_data_provider, const std::vect
auto output_group = OutputGroup(coin_params);
bool valid_outputgroup{false};
for (auto& coin : coins) {
output_group.Insert(coin, /*ancestors=*/0, /*descendants=*/0, positive_only);
// If positive_only was specified, nothing may have been inserted, leading to an empty output group
if (!positive_only || (positive_only && coin.GetEffectiveValue() > 0)) {
output_group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/0, /*descendants=*/0);
}
// If positive_only was specified, nothing was inserted, leading to an empty output group
// that would be invalid for the BnB algorithm
valid_outputgroup = !positive_only || output_group.GetSelectionAmount() > 0;
if (valid_outputgroup && fuzzed_data_provider.ConsumeBool()) {

View file

@ -0,0 +1,234 @@
// Copyright (c) 2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
#include <test/util/setup_common.h>
#include <wallet/coinselection.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>
#include <boost/test/unit_test.hpp>
namespace wallet {
BOOST_FIXTURE_TEST_SUITE(group_outputs_tests, TestingSetup)
static int nextLockTime = 0;
static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)
{
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockWalletDatabase());
wallet->LoadWallet();
LOCK(wallet->cs_wallet);
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet->SetupDescriptorScriptPubKeyMans();
return wallet;
}
static void addCoin(CoinsResult& coins,
CWallet& wallet,
const CTxDestination& dest,
const CAmount& nValue,
bool is_from_me,
CFeeRate fee_rate = CFeeRate(0),
int depth = 6)
{
CMutableTransaction tx;
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
tx.vout.resize(1);
tx.vout[0].nValue = nValue;
tx.vout[0].scriptPubKey = GetScriptForDestination(dest);
const uint256& txid = tx.GetHash();
LOCK(wallet.cs_wallet);
auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
assert(ret.second);
CWalletTx& wtx = (*ret.first).second;
const auto& txout = wtx.tx->vout.at(0);
coins.Add(*Assert(OutputTypeFromDestination(dest)),
{COutPoint(wtx.GetHash(), 0),
txout,
depth,
CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr),
/*spendable=*/ true,
/*solvable=*/ true,
/*safe=*/ true,
wtx.GetTxTime(),
is_from_me,
fee_rate});
}
CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)
{
return CoinSelectionParams{
rand,
/*change_output_size=*/ 0,
/*change_spend_size=*/ 0,
/*min_change_target=*/ CENT,
/*effective_feerate=*/ CFeeRate(0),
/*long_term_feerate=*/ CFeeRate(0),
/*discard_feerate=*/ CFeeRate(0),
/*tx_noinputs_size=*/ 0,
/*avoid_partial=*/ avoid_partial_spends,
};
}
class GroupVerifier
{
public:
std::shared_ptr<CWallet> wallet{nullptr};
CoinsResult coins_pool;
FastRandomContext rand;
void GroupVerify(const OutputType type,
const CoinEligibilityFilter& filter,
bool avoid_partial_spends,
bool positive_only,
int expected_size)
{
OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];
std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
groups.groups_by_type[type].mixed_group;
BOOST_CHECK_EQUAL(groups_out.size(), expected_size);
}
void GroupAndVerify(const OutputType type,
const CoinEligibilityFilter& filter,
int expected_with_partial_spends_size,
int expected_without_partial_spends_size,
bool positive_only)
{
// First avoid partial spends
GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size);
// Second don't avoid partial spends
GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
}
};
BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
{
const auto& wallet = NewWallet(m_node);
GroupVerifier group_verifier;
group_verifier.wallet = wallet;
const CoinEligibilityFilter& BASIC_FILTER{1, 6, 0};
// #################################################################################
// 10 outputs from different txs going to the same script
// 1) if partial spends is enabled --> must not be grouped
// 2) if partial spends is not enabled --> must be grouped into a single OutputGroup
// #################################################################################
unsigned long GROUP_SIZE = 10;
const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
for (unsigned long i = 0; i < GROUP_SIZE; i++) {
addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);
}
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ GROUP_SIZE,
/*expected_without_partial_spends_size=*/ 1,
/*positive_only=*/ true);
// ####################################################################################
// 3) 10 more UTXO are added with a different script --> must be grouped into a single
// group for avoid partial spends and 10 different output groups for partial spends
// ####################################################################################
const CTxDestination dest2 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
for (unsigned long i = 0; i < GROUP_SIZE; i++) {
addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true);
}
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
/*expected_without_partial_spends_size=*/ 2,
/*positive_only=*/ true);
// ################################################################################
// 4) Now add a negative output --> which will be skipped if "positive_only" is set
// ################################################################################
const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));
BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);
// First expect no changes with "positive_only" enabled
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
/*expected_without_partial_spends_size=*/ 2,
/*positive_only=*/ true);
// Then expect changes with "positive_only" disabled
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
/*expected_without_partial_spends_size=*/ 3,
/*positive_only=*/ false);
// ##############################################################################
// 5) Try to add a non-eligible UTXO (due not fulfilling the min depth target for
// "not mine" UTXOs) --> it must not be added to any group
// ##############################################################################
const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN,
/*is_from_me=*/false, CFeeRate(0), /*depth=*/5);
// Expect no changes from this round and the previous one (point 4)
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
/*expected_without_partial_spends_size=*/ 3,
/*positive_only=*/ false);
// ##############################################################################
// 6) Try to add a non-eligible UTXO (due not fulfilling the min depth target for
// "mine" UTXOs) --> it must not be added to any group
// ##############################################################################
const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN,
/*is_from_me=*/true, CFeeRate(0), /*depth=*/0);
// Expect no changes from this round and the previous one (point 5)
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
/*expected_without_partial_spends_size=*/ 3,
/*positive_only=*/ false);
// ###########################################################################################
// 7) Surpass the OUTPUT_GROUP_MAX_ENTRIES and verify that a second partial group gets created
// ###########################################################################################
const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
uint16_t NUM_SINGLE_ENTRIES = 101;
for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100}
addCoin(group_verifier.coins_pool, *wallet, dest7, 9 * COIN, /*is_from_me=*/true);
}
// Exclude partial groups only adds one more group to the previous test case (point 6)
int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;
group_verifier.GroupAndVerify(OutputType::BECH32,
BASIC_FILTER,
/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
/*expected_without_partial_spends_size=*/ 4,
/*positive_only=*/ false);
// Include partial groups should add one more group inside the "avoid partial spends" count
const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
group_verifier.GroupAndVerify(OutputType::BECH32,
avoid_partial_groups_filter,
/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
/*expected_without_partial_spends_size=*/ 5,
/*positive_only=*/ false);
}
BOOST_AUTO_TEST_SUITE_END()
} // end namespace wallet