Merge bitcoin/bitcoin#25806: wallet: group outputs only once, decouple it from Coin Selection

6a302d40df wallet: single output groups filtering and grouping process (furszy)
bd91ed1cb2 wallet: unify outputs grouping process (furszy)
55962001da test: coinselector_tests refactor, use CoinsResult instead of plain std::vector (furszy)
34f54a0a3a wallet: decouple outputs grouping process from each ChooseSelectionResult (furszy)
461f0821a2 refactor: make OutputGroup::m_outputs field a vector of shared_ptr (furszy)
d8e749bb84 test: wallet, add coverage for outputs grouping process (furszy)
06ec8f9928 wallet: make OutputGroup "positive_only" filter explicit (furszy)

Pull request description:

  The idea originates from https://github.com/bitcoin/bitcoin/pull/24845#issuecomment-1130310321.

  Note:
  For clarity, it's recommended to start reviewing from the end result to understand the structure of the flow.

  #### GroupOutputs function rationale:
  If "Avoid Partial Spends" is enabled, the function gathers outputs with the same script together inside a container. So Coin Selection can treats them as if them were just one possible input and either select them all or not select them.

  #### How the Inputs Fetch + Selection process roughly works:

  ```
  1. Fetch user’s manually selected inputs.
  2. Fetch wallet available coins (walks through the entire wallet txes map) and insert them into a set of vectors (each vector store outputs from a single type).
  3. Coin Selection Process:
     Call `AttemptSelection` 8 times. Each of them expands the coin eligibility filter (accepting a larger subset of coins in the calculation) until it founds a solutions or completely fails if no solutions gets founds after the 8 rounds.

     Each `AttemptSelection` call performs the following actions:
       - For each output type supported by the wallet (P2SH, P2PK, P2WPKH, P2WSH and a combination of all of them):
         Call ‘ChooseSelectionResult’ providing the respective, filtered by type, coins vector. Which:
             I. Groups the outputs vector twice (one for positive only and a second one who includes the negative ones as well).
                - GroupOutputs walks-through the entire inputted coins vector one time at least, + more if we are avoiding partial spends, to generate a vector of OutputGroups.
             II. Then performs every coin selection algorithm using the recently created vector of OutputGroup: (1) BnB, (2) knapsack and (3) SRD.
             III. Then returns the best solution out of them.
  ```

  We perform the general operation of gathering outputs, with the same script, into a single container inside:
  Each coins selection attempt (8 times —> each coin eligibility filter), for each of the outputs vector who were filtered by type (plus another one joining all the outputs as well if needed), twice (one for the positive only outputs effective value and a second one for all of them).

  So, in the worst case scenario where no solution is found after the 8 Coin Selection attempts, the `GroupOutputs` function is called 80 times (8 * 5 * 2).

  #### Improvements:

  This proposal streamlines the process so that the output groups, filtered by coin eligibility and type, are created in a single loop outside of the Coin Selection Process.

  The new process is as follows:

  ```
  1. Fetch user’s manually selected inputs.
  2. Fetch wallet available coins.
  3. Group outputs by each coin eligibility filter and each different output type found.
  4. Coin Selection Process:
     Call AttemptSelection 8 times. Each of them expands the coin eligibility filter (accepting different output groups) until it founds a solutions or completely fails if no solutions gets founds after the 8 rounds.

     Each ‘AttemptSelection’ call performs the following actions:
        - For each output type supported by the wallet (P2SH, P2PK, P2WPKH, P2WSH and all of them):
            A. Call ‘ChooseSelectionResult’ providing the respective, filtered by type, output group. Which:
               I. Performs every coin selection algorithm using the provided vector of OutputGroup: (1) BnB, (2) knapsack and (3) SRD.
               II. Then returns the best solution out of them.
  ```

  Extra Note:
  The next steps after this PR will be to:
  1) Merge `AvailableCoins` and `GroupOutputs` processes.
  2) Skip entire coin selection rounds if no new coins are added into the subsequent round.
  3) Remove global feerates from the OutputGroup class.
  4) Remove secondary "grouped" tx creation from `CreateTransactionInternal` by running Coin Selection results over the aps grouped outputs vs non-aps ones.

ACKs for top commit:
  S3RK:
    ReACK 6a302d4
  achow101:
    ACK 6a302d40df
  theStack:
    re-ACK 6a302d40df 🥥

Tree-SHA512: dff849063be328e7d9c358ec80239a6db2cd6131963b511b83699b95b337d3106263507eaba0119eaac63e6ac21c6c42d187ae23d79d9220b90c323d44b01d24
This commit is contained in:
Andrew Chow 2023-03-06 18:45:50 -05:00
commit 4ea3a8b71d
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41
9 changed files with 473 additions and 146 deletions

View File

@ -179,7 +179,8 @@ BITCOIN_TESTS += \
wallet/test/ismine_tests.cpp \ wallet/test/ismine_tests.cpp \
wallet/test/rpc_util_tests.cpp \ wallet/test/rpc_util_tests.cpp \
wallet/test/scriptpubkeyman_tests.cpp \ wallet/test/scriptpubkeyman_tests.cpp \
wallet/test/walletload_tests.cpp wallet/test/walletload_tests.cpp \
wallet/test/group_outputs_tests.cpp
FUZZ_SUITE_LD_COMMON +=\ FUZZ_SUITE_LD_COMMON +=\
$(SQLITE_LIBS) \ $(SQLITE_LIBS) \

View File

@ -75,8 +75,9 @@ static void CoinSelection(benchmark::Bench& bench)
/*tx_noinputs_size=*/ 0, /*tx_noinputs_size=*/ 0,
/*avoid_partial=*/ false, /*avoid_partial=*/ false,
}; };
auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard];
bench.run([&] { bench.run([&] {
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, available_coins, coin_selection_params, /*allow_mixed_output_types=*/true); auto result = AttemptSelection(1003 * COIN, group, coin_selection_params, /*allow_mixed_output_types=*/true);
assert(result); assert(result);
assert(result->GetSelectedValue() == 1003 * COIN); assert(result->GetSelectedValue() == 1003 * COIN);
assert(result->GetInputSet().size() == 2); assert(result->GetInputSet().size() == 2);
@ -91,7 +92,7 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>
tx.vout[nInput].nValue = nValue; tx.vout[nInput].nValue = nValue;
COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 0, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ true, /*fees=*/ 0); COutput output(COutPoint(tx.GetHash(), nInput), 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.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 // Copied from src/wallet/test/coinselector_tests.cpp
static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool) static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool)

View File

@ -333,12 +333,9 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
******************************************************************************/ ******************************************************************************/
void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only) { void OutputGroup::Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants) {
// Filter for positive only here before adding the coin
if (positive_only && output.GetEffectiveValue() <= 0) return;
m_outputs.push_back(output); m_outputs.push_back(output);
COutput& coin = m_outputs.back(); auto& coin = *m_outputs.back();
fee += coin.GetFee(); fee += coin.GetFee();
@ -358,8 +355,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 // coin itself; thus, this value is counted as the max, not the sum
m_descendants = std::max(m_descendants, descendants); m_descendants = std::max(m_descendants, descendants);
if (output.input_bytes > 0) { if (output->input_bytes > 0) {
m_weight += output.input_bytes * WITNESS_SCALE_FACTOR; m_weight += output->input_bytes * WITNESS_SCALE_FACTOR;
} }
} }
@ -375,7 +372,29 @@ CAmount OutputGroup::GetSelectionAmount() const
return m_subtract_fee_outputs ? m_value : effective_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 // This function should not be called with empty inputs as that would mean the selection failed
assert(!inputs.empty()); assert(!inputs.empty());
@ -383,7 +402,8 @@ CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost,
// Always consider the cost of spending an input now vs in the future. // Always consider the cost of spending an input now vs in the future.
CAmount waste = 0; CAmount waste = 0;
CAmount selected_effective_value = 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; waste += coin.GetFee() - coin.long_term_fee;
selected_effective_value += use_effective_value ? coin.GetEffectiveValue() : coin.txout.nValue; selected_effective_value += use_effective_value ? coin.GetEffectiveValue() : coin.txout.nValue;
} }
@ -431,12 +451,12 @@ CAmount SelectionResult::GetWaste() const
CAmount SelectionResult::GetSelectedValue() const CAmount SelectionResult::GetSelectedValue() const
{ {
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.txout.nValue; }); return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->txout.nValue; });
} }
CAmount SelectionResult::GetSelectedEffectiveValue() const CAmount SelectionResult::GetSelectedEffectiveValue() const
{ {
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.GetEffectiveValue(); }); return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->GetEffectiveValue(); });
} }
void SelectionResult::Clear() void SelectionResult::Clear()
@ -455,14 +475,14 @@ void SelectionResult::AddInput(const OutputGroup& group)
m_weight += group.m_weight; 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 // As it can fail, combine inputs first
InsertInputs(inputs); InsertInputs(inputs);
m_use_effective = !subtract_fee_outputs; m_use_effective = !subtract_fee_outputs;
m_weight += std::accumulate(inputs.cbegin(), inputs.cend(), 0, [](int sum, const auto& coin) { 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;
}); });
} }
@ -480,14 +500,14 @@ void SelectionResult::Merge(const SelectionResult& other)
m_weight += other.m_weight; 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; 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()); Shuffle(coins.begin(), coins.end(), FastRandomContext());
return coins; return coins;
} }

View File

@ -7,6 +7,7 @@
#include <consensus/amount.h> #include <consensus/amount.h>
#include <consensus/consensus.h> #include <consensus/consensus.h>
#include <outputtype.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <random.h> #include <random.h>
@ -192,13 +193,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) : 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) : 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) {} 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. */ /** A group of UTXOs paid to the same output script. */
struct OutputGroup struct OutputGroup
{ {
/** The list of UTXOs contained in this output group. */ /** 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 /** 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 * least a certain number of confirmations on UTXOs received from outside wallets while trusting
* our own UTXOs more. */ * our own UTXOs more. */
@ -237,11 +243,37 @@ struct OutputGroup
m_subtract_fee_outputs(params.m_subtract_fee_outputs) 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; bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
CAmount GetSelectionAmount() 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 /** Compute the waste for this result given the cost of change
* and the opportunity cost of spending these inputs now vs in the future. * 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) * If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
@ -259,7 +291,7 @@ struct OutputGroup
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false). * @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
* @return The waste * @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 /** Choose a random change target for each transaction to make it harder to fingerprint the Core
@ -292,7 +324,7 @@ struct SelectionResult
{ {
private: private:
/** Set of inputs selected by the algorithm to use in the transaction */ /** 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 */ /** The target the algorithm selected for. Equal to the recipient amount plus non-input fees */
CAmount m_target; CAmount m_target;
/** The algorithm used to produce this result */ /** The algorithm used to produce this result */
@ -329,7 +361,7 @@ public:
void Clear(); void Clear();
void AddInput(const OutputGroup& group); 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);
/** Calculates and stores the waste for this selection via GetSelectionWaste */ /** Calculates and stores the waste for this selection via GetSelectionWaste */
void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee); void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee);
@ -344,9 +376,9 @@ public:
void Merge(const SelectionResult& other); void Merge(const SelectionResult& other);
/** Get m_selected_inputs */ /** 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 */ /** 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; bool operator<(SelectionResult other) const;

View File

@ -404,28 +404,37 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
return result; 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) { if (!coin_sel_params.m_avoid_partial_spends) {
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup. // Allowing partial spends means no grouping. Each COutput gets its own OutputGroup
for (const COutput& output : outputs) { for (const auto& [type, outputs] : coins.coins) {
// Skip outputs we cannot spend for (const COutput& output : outputs) {
if (!output.spendable) continue; // Skip outputs we cannot spend
if (!output.spendable) continue;
size_t ancestors, descendants; // Get mempool info
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants); size_t ancestors, descendants;
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
// Make an OutputGroup containing just this output // Create a new group per output and add it to the all groups vector
OutputGroup group{coin_sel_params}; OutputGroup group(coin_sel_params);
group.Insert(output, ancestors, descendants, positive_only); group.Insert(std::make_shared<COutput>(output), ancestors, descendants);
// Check the OutputGroup's eligibility. Only add the eligible ones. // Each filter maps to a different set of groups
if (positive_only && group.GetSelectionAmount() <= 0) continue; for (const auto& sel_filter : filters) {
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group); 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 // We want to combine COutputs that have the same scriptPubKey into single OutputGroups
@ -434,16 +443,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 // 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 // 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. // 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; typedef std::map<std::pair<CScript, OutputType>, std::vector<OutputGroup>> ScriptPubKeyToOutgroup;
for (const auto& output : outputs) { const auto& group_outputs = [](
// Skip outputs we cannot spend const COutput& output, OutputType type, size_t ancestors, size_t descendants,
if (!output.spendable) continue; ScriptPubKeyToOutgroup& groups_map, const CoinSelectionParams& coin_sel_params,
bool positive_only) {
size_t ancestors, descendants; std::vector<OutputGroup>& groups = groups_map[std::make_pair(output.txout.scriptPubKey,type)];
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
CScript spk = output.txout.scriptPubKey;
std::vector<OutputGroup>& groups = spk_to_groups_map[spk];
if (groups.size() == 0) { if (groups.size() == 0) {
// No OutputGroups for this scriptPubKey yet, add one // No OutputGroups for this scriptPubKey yet, add one
@ -462,42 +467,69 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
group = &groups.back(); group = &groups.back();
} }
// Add the output to group // Filter for positive only before adding the output to group
group->Insert(output, ancestors, descendants, positive_only); 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 ScriptPubKeyToOutgroup spk_to_groups_map;
for (const auto& spk_and_groups_pair: spk_to_groups_map) { ScriptPubKeyToOutgroup spk_to_positive_groups_map;
const std::vector<OutputGroup>& groups_per_spk= spk_and_groups_pair.second; 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. size_t ancestors, descendants;
for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) { wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
const OutputGroup& group = *group_it;
// Don't include partial groups if there are full groups too and we don't want partial groups group_outputs(output, type, ancestors, descendants, spk_to_groups_map, coin_sel_params, /*positive_only=*/ false);
if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) { group_outputs(output, type, ancestors, descendants, spk_to_positive_groups_map,
continue; coin_sel_params, /*positive_only=*/ true);
}
// 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);
} }
} }
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 // 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(); } static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins, util::Result<SelectionResult> AttemptSelection(const CAmount& nTargetValue, OutputGroupTypeMap& groups,
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types) const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types)
{ {
// Run coin selection on each OutputType and compute the Waste Metric // Run coin selection on each OutputType and compute the Waste Metric
std::vector<SelectionResult> results; std::vector<SelectionResult> results;
for (const auto& it : available_coins.coins) { for (auto& [type, group] : groups.groups_by_type) {
auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, it.second, coin_selection_params)}; auto result{ChooseSelectionResult(nTargetValue, group, coin_selection_params)};
// If any specific error message appears here, then something particularly wrong happened. // If any specific error message appears here, then something particularly wrong happened.
if (HasErrorMsg(result)) return result; // So let's return the specific error. if (HasErrorMsg(result)) return result; // So let's return the specific error.
// Append the favorable result. // Append the favorable result.
@ -510,32 +542,30 @@ 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 // 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. // over all available coins, which would allow mixing.
// If TypesCount() <= 1, there is nothing to mix. // If TypesCount() <= 1, there is nothing to mix.
if (allow_mixed_output_types && available_coins.TypesCount() > 1) { if (allow_mixed_output_types && groups.TypesCount() > 1) {
return ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.All(), coin_selection_params); return ChooseSelectionResult(nTargetValue, 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 // 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 // find a solution using all available coins
return util::Error(); return util::Error();
}; };
util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins, const CoinSelectionParams& coin_selection_params) util::Result<SelectionResult> ChooseSelectionResult(const CAmount& nTargetValue, Groups& groups, const CoinSelectionParams& coin_selection_params)
{ {
// Vector of results. We will choose the best one based on waste. // Vector of results. We will choose the best one based on waste.
std::vector<SelectionResult> results; std::vector<SelectionResult> results;
std::vector<OutputGroup> positive_groups = GroupOutputs(wallet, available_coins, coin_selection_params, eligibility_filter, /*positive_only=*/true); if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change)}) {
if (auto bnb_result{SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change)}) {
results.push_back(*bnb_result); results.push_back(*bnb_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. // 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); if (auto knapsack_result{KnapsackSolver(groups.mixed_group, nTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue, 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); 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); results.push_back(*knapsack_result);
} }
if (auto srd_result{SelectCoinsSRD(positive_groups, nTargetValue, coin_selection_params.rng_fast)}) { if (auto srd_result{SelectCoinsSRD(groups.positive_group, nTargetValue, 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); 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); results.push_back(*srd_result);
} }
@ -607,11 +637,6 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
return op_selection_result; 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 CAmount& value_to_select, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params) util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmount& value_to_select, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
{ {
unsigned int limit_ancestor_count = 0; unsigned int limit_ancestor_count = 0;
@ -666,12 +691,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. // Walk-through the filters until the solution gets found.
// If no solution is found, return the first detailed error (if any). // If no solution is found, return the first detailed error (if any).
// future: add "error level" so the worst one can be picked instead. // future: add "error level" so the worst one can be picked instead.
std::vector<util::Result<SelectionResult>> res_detailed_errors; std::vector<util::Result<SelectionResult>> res_detailed_errors;
for (const auto& select_filter : ordered_filters) { 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)}) { coin_selection_params, select_filter.allow_mixed_output_types)}) {
return res; // result found return res; // result found
} else { } else {
@ -933,7 +963,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
} }
// Shuffle selected coins and fill in final vin // 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 // The sequence number is set to non-maxint so that DiscourageFeeSniping
// works. // works.
@ -945,7 +975,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
// behavior." // 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}; 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) { for (const auto& coin : selected_coins) {
txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); txNew.vin.push_back(CTxIn(coin->outpoint, CScript(), nSequence));
} }
DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight());

View File

@ -106,17 +106,27 @@ const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint&
*/ */
std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_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. * 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 * `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 * 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. * 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] 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] groups The grouped outputs mapped by coin eligibility filters
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
* param@[in] coin_selection_params Parameters for the coin selection * 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 * 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 * returns If successful, a SelectionResult containing the input set
@ -124,7 +134,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 * or (2) an specific error message if there was something particularly wrong (e.g. a selection
* result that surpassed the tx max weight size). * result that surpassed the tx max weight size).
*/ */
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins, util::Result<SelectionResult> AttemptSelection(const CAmount& nTargetValue, OutputGroupTypeMap& groups,
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types); const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types);
/** /**
@ -132,23 +142,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 * Multiple coin selection algorithms will be run and the input set that produces the least waste
* (according to the waste metric) will be chosen. * (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] 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] groups The struct containing the outputs grouped by script and divided by (1) positive only outputs and (2) all outputs (positive + negative).
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
* param@[in] coin_selection_params Parameters for the coin selection * param@[in] coin_selection_params Parameters for the coin selection
* returns If successful, a SelectionResult containing the input set * 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") * 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 * or (2) an specific error message if there was something particularly wrong (e.g. a selection
* result that surpassed the tx max weight size). * result that surpassed the tx max weight size).
*/ */
util::Result<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins, util::Result<SelectionResult> ChooseSelectionResult(const CAmount& nTargetValue, Groups& groups, const CoinSelectionParams& coin_selection_params);
const CoinSelectionParams& coin_selection_params);
// User manually selected inputs that must be part of the transaction // User manually selected inputs that must be part of the transaction
struct PreSelectedInputs struct PreSelectedInputs
{ {
std::set<COutput> coins; std::set<std::shared_ptr<COutput>> coins;
// If subtract fee from outputs is disabled, the 'total_amount' // If subtract fee from outputs is disabled, the 'total_amount'
// will be the sum of each output effective value // will be the sum of each output effective value
// instead of the sum of the outputs amount // instead of the sum of the outputs amount
@ -161,7 +168,7 @@ struct PreSelectedInputs
} else { } else {
total_amount += output.GetEffectiveValue(); total_amount += 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 // we repeat those tests this many times and only complain if all iterations of the test fail
#define RANDOM_REPEATS 5 #define RANDOM_REPEATS 5
typedef std::set<COutput> CoinSet; typedef std::set<std::shared_ptr<COutput>> CoinSet;
static const CoinEligibilityFilter filter_standard(1, 6, 0); static const CoinEligibilityFilter filter_standard(1, 6, 0);
static const CoinEligibilityFilter filter_confirmed(1, 1, 0); static const CoinEligibilityFilter filter_confirmed(1, 1, 0);
@ -53,7 +53,7 @@ static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
tx.nLockTime = nextLockTime++; // so all transactions get different hashes tx.nLockTime = nextLockTime++; // so all transactions get different hashes
COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0); COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0);
OutputGroup group; 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); result.AddInput(group);
} }
@ -65,7 +65,7 @@ static void add_coin(const CAmount& nValue, int nInput, CoinSet& set, CAmount fe
tx.nLockTime = nextLockTime++; // so all transactions get different hashes tx.nLockTime = nextLockTime++; // so all transactions get different hashes
COutput coin(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ 148, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, fee); COutput coin(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ 148, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, fee);
coin.long_term_fee = long_term_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) 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)
@ -94,10 +94,10 @@ static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
std::vector<CAmount> a_amts; std::vector<CAmount> a_amts;
std::vector<CAmount> b_amts; std::vector<CAmount> b_amts;
for (const auto& coin : a.GetInputSet()) { for (const auto& coin : a.GetInputSet()) {
a_amts.push_back(coin.txout.nValue); a_amts.push_back(coin->txout.nValue);
} }
for (const auto& coin : b.GetInputSet()) { for (const auto& coin : b.GetInputSet()) {
b_amts.push_back(coin.txout.nValue); b_amts.push_back(coin->txout.nValue);
} }
std::sort(a_amts.begin(), a_amts.end()); std::sort(a_amts.begin(), a_amts.end());
std::sort(b_amts.begin(), b_amts.end()); std::sort(b_amts.begin(), b_amts.end());
@ -110,8 +110,8 @@ static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
static bool EqualResult(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(), std::pair<CoinSet::iterator, CoinSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(),
[](const COutput& a, const COutput& b) { [](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) {
return a.outpoint == b.outpoint; return a->outpoint == b->outpoint;
}); });
return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end(); return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end();
} }
@ -134,12 +134,12 @@ inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& availabl
static_groups.clear(); static_groups.clear();
for (auto& coin : available_coins) { for (auto& coin : available_coins) {
static_groups.emplace_back(); 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; 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{}; FastRandomContext rand{};
CoinSelectionParams coin_selection_params{ CoinSelectionParams coin_selection_params{
@ -153,9 +153,9 @@ inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput>
/*tx_noinputs_size=*/ 0, /*tx_noinputs_size=*/ 0,
/*avoid_partial=*/ false, /*avoid_partial=*/ false,
}; };
static std::vector<OutputGroup> static_groups; static OutputGroupTypeMap static_groups;
static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, filter, /*positive_only=*/false); static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter];
return static_groups; return static_groups.all_groups.mixed_group;
} }
// Branch and bound coin selection tests // Branch and bound coin selection tests
@ -418,25 +418,25 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
available_coins.Clear(); available_coins.Clear();
// with an empty wallet we can't even pay one cent // 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 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 // 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 // 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(result1);
BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT); BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT);
add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin
// we can't make 3 cents of mature coins // 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 // 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(result2);
BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 3 * CENT); BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 3 * CENT);
@ -447,38 +447,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 // 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: // 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 // 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 // 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(result3);
BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT); BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT);
// and we can make 38 cents if we accept all new coins // 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(result4);
BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 38 * CENT); BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 38 * CENT);
// try making 34 cents from 1,2,5,10,20 - we can't do it exactly // 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(result5);
BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 35 * CENT); // but 35 cents is closest BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 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) 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 // 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(result6);
BOOST_CHECK_EQUAL(result6->GetSelectedValue(), 7 * CENT); BOOST_CHECK_EQUAL(result6->GetSelectedValue(), 7 * CENT);
BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U); BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U);
// when we try making 8 cents, the smaller coins (1,2,5) are exactly enough. // 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);
BOOST_CHECK(result7->GetSelectedValue() == 8 * CENT); BOOST_CHECK(result7->GetSelectedValue() == 8 * CENT);
BOOST_CHECK_EQUAL(result7->GetInputSet().size(), 3U); 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) // 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(result8);
BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 10 * CENT); BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 10 * CENT);
BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U); BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U);
@ -493,12 +493,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 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 // 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(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 // 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(result10);
BOOST_CHECK_EQUAL(result10->GetSelectedValue(), 20 * CENT); // we should get 20 in one coin BOOST_CHECK_EQUAL(result10->GetSelectedValue(), 20 * CENT); // we should get 20 in one coin
BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U); BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U);
@ -506,7 +506,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 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 // 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(result11);
BOOST_CHECK_EQUAL(result11->GetSelectedValue(), 18 * CENT); // we should get 18 in 3 coins BOOST_CHECK_EQUAL(result11->GetSelectedValue(), 18 * CENT); // we should get 18 in 3 coins
BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U); BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U);
@ -514,13 +514,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 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 // 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(result12);
BOOST_CHECK_EQUAL(result12->GetSelectedValue(), 18 * CENT); // we should get 18 in 1 coin BOOST_CHECK_EQUAL(result12->GetSelectedValue(), 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 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 // 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(result13);
BOOST_CHECK_EQUAL(result13->GetSelectedValue(), 11 * CENT); BOOST_CHECK_EQUAL(result13->GetSelectedValue(), 11 * CENT);
BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U); BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U);
@ -530,12 +530,12 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 2*COIN); add_coin(available_coins, *wallet, 2*COIN);
add_coin(available_coins, *wallet, 3*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 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(result14);
BOOST_CHECK_EQUAL(result14->GetSelectedValue(), 1 * COIN); // we should get 1 BTC in 1 coin BOOST_CHECK_EQUAL(result14->GetSelectedValue(), 1 * COIN); // we should get 1 BTC in 1 coin
BOOST_CHECK_EQUAL(result14->GetInputSet().size(), 1U); 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(result15);
BOOST_CHECK_EQUAL(result15->GetSelectedValue(), 2 * COIN); // we should get 2 BTC in 1 coin BOOST_CHECK_EQUAL(result15->GetSelectedValue(), 2 * COIN); // we should get 2 BTC in 1 coin
BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U); BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U);
@ -551,7 +551,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
// try making 1 * CENT from the 1.5 * CENT // try making 1 * CENT from the 1.5 * CENT
// we'll get change smaller than CENT whatever happens, so can expect CENT exactly // 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(result16);
BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT); BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT);
@ -559,7 +559,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, 1111*CENT); add_coin(available_coins, *wallet, 1111*CENT);
// try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 // 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(result17);
BOOST_CHECK_EQUAL(result17->GetSelectedValue(), 1 * CENT); // we should get the exact amount BOOST_CHECK_EQUAL(result17->GetSelectedValue(), 1 * CENT); // we should get the exact amount
@ -568,7 +568,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 7 / 10); add_coin(available_coins, *wallet, CENT * 7 / 10);
// and try again to make 1.0 * CENT // 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(result18);
BOOST_CHECK_EQUAL(result18->GetSelectedValue(), 1 * CENT); // we should get the exact amount BOOST_CHECK_EQUAL(result18->GetSelectedValue(), 1 * CENT); // we should get the exact amount
@ -578,7 +578,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
for (int j = 0; j < 20; j++) for (int j = 0; j < 20; j++)
add_coin(available_coins, *wallet, 50000 * COIN); 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(result19);
BOOST_CHECK_EQUAL(result19->GetSelectedValue(), 500000 * COIN); // we should get the exact amount BOOST_CHECK_EQUAL(result19->GetSelectedValue(), 500000 * COIN); // we should get the exact amount
BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins
@ -592,7 +592,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 6 / 10); add_coin(available_coins, *wallet, CENT * 6 / 10);
add_coin(available_coins, *wallet, CENT * 7 / 10); add_coin(available_coins, *wallet, CENT * 7 / 10);
add_coin(available_coins, *wallet, 1111 * CENT); 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(result20);
BOOST_CHECK_EQUAL(result20->GetSelectedValue(), 1111 * CENT); // we get the bigger coin BOOST_CHECK_EQUAL(result20->GetSelectedValue(), 1111 * CENT); // we get the bigger coin
BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U); BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U);
@ -603,7 +603,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 6 / 10); add_coin(available_coins, *wallet, CENT * 6 / 10);
add_coin(available_coins, *wallet, CENT * 8 / 10); add_coin(available_coins, *wallet, CENT * 8 / 10);
add_coin(available_coins, *wallet, 1111 * CENT); 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(result21);
BOOST_CHECK_EQUAL(result21->GetSelectedValue(), CENT); // we should get the exact amount BOOST_CHECK_EQUAL(result21->GetSelectedValue(), CENT); // we should get the exact amount
BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6 BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6
@ -615,13 +615,13 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test)
add_coin(available_coins, *wallet, CENT * 100); add_coin(available_coins, *wallet, CENT * 100);
// trying to make 100.01 from these three coins // 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(result22);
BOOST_CHECK_EQUAL(result22->GetSelectedValue(), CENT * 10105 / 100); // we should get all coins BOOST_CHECK_EQUAL(result22->GetSelectedValue(), CENT * 10105 / 100); // we should get all coins
BOOST_CHECK_EQUAL(result22->GetInputSet().size(), 3U); 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 // 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(result23);
BOOST_CHECK_EQUAL(result23->GetSelectedValue(), 101 * CENT); BOOST_CHECK_EQUAL(result23->GetSelectedValue(), 101 * CENT);
BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U); BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U);
@ -636,7 +636,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. // 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++) { 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); BOOST_CHECK(result24);
if (amt - 2000 < CENT) { if (amt - 2000 < CENT) {
@ -727,7 +727,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset)
add_coin(available_coins, *wallet, 1000 * COIN); add_coin(available_coins, *wallet, 1000 * COIN);
add_coin(available_coins, *wallet, 3 * 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(result);
BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN); BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN);
BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U); BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U);
@ -943,7 +943,7 @@ static util::Result<SelectionResult> select_coins(const CAmount& target, const C
static bool has_coin(const CoinSet& set, CAmount amount) 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) 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); auto output_group = OutputGroup(coin_params);
bool valid_outputgroup{false}; bool valid_outputgroup{false};
for (auto& coin : coins) { for (auto& coin : coins) {
output_group.Insert(coin, /*ancestors=*/0, /*descendants=*/0, positive_only); if (!positive_only || (positive_only && coin.GetEffectiveValue() > 0)) {
// If positive_only was specified, nothing may have been inserted, leading to an empty output group 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 // that would be invalid for the BnB algorithm
valid_outputgroup = !positive_only || output_group.GetSelectionAmount() > 0; valid_outputgroup = !positive_only || output_group.GetSelectionAmount() > 0;
if (valid_outputgroup && fuzzed_data_provider.ConsumeBool()) { 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