mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-16 13:01:19 +02:00
Merge 4ea3a8b71d into merged_master (Bitcoin PR bitcoin/bitcoin#25806)
This commit is contained in:
commit
bff78b8a4e
9 changed files with 504 additions and 177 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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: ";
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
234
src/wallet/test/group_outputs_tests.cpp
Normal file
234
src/wallet/test/group_outputs_tests.cpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue