aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/spend.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet/spend.cpp')
-rw-r--r--src/wallet/spend.cpp408
1 files changed, 239 insertions, 169 deletions
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index 3bb3c17119..4548d5f813 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -30,11 +30,11 @@ using interfaces::FoundBlock;
namespace wallet {
static constexpr size_t OUTPUT_GROUP_MAX_ENTRIES{100};
-int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoint, const SigningProvider* provider, const CCoinControl* coin_control)
+int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoint, const SigningProvider* provider, bool can_grind_r, const CCoinControl* coin_control)
{
CMutableTransaction txn;
txn.vin.push_back(CTxIn(outpoint));
- if (!provider || !DummySignInput(*provider, txn.vin[0], txout, coin_control)) {
+ if (!provider || !DummySignInput(*provider, txn.vin[0], txout, can_grind_r, coin_control)) {
return -1;
}
return GetVirtualTransactionInputSize(txn.vin[0]);
@@ -43,7 +43,7 @@ int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoin
int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, const CCoinControl* coin_control)
{
const std::unique_ptr<SigningProvider> provider = wallet->GetSolvingProvider(txout.scriptPubKey);
- return CalculateMaximumSignedInputSize(txout, COutPoint(), provider.get(), coin_control);
+ return CalculateMaximumSignedInputSize(txout, COutPoint(), provider.get(), wallet->CanGrindR(), coin_control);
}
// txouts needs to be in the order of tx.vin
@@ -163,6 +163,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
PreSelectedInputs result;
std::vector<COutPoint> vPresetInputs;
coin_control.ListSelected(vPresetInputs);
+ const bool can_grind_r = wallet.CanGrindR();
for (const COutPoint& outpoint : vPresetInputs) {
int input_bytes = -1;
CTxOut txout;
@@ -181,7 +182,7 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
}
if (input_bytes == -1) {
- input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, &coin_control);
+ input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, can_grind_r, &coin_control);
}
// If available, override calculated size with coin control specified size
@@ -214,6 +215,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
const int min_depth = {coinControl ? coinControl->m_min_depth : DEFAULT_MIN_DEPTH};
const int max_depth = {coinControl ? coinControl->m_max_depth : DEFAULT_MAX_DEPTH};
const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs : true};
+ const bool can_grind_r = wallet.CanGrindR();
std::set<uint256> trusted_parents;
for (const auto& entry : wallet.mapWallet)
@@ -287,7 +289,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
continue;
- if (wallet.IsLockedCoin(outpoint))
+ if (wallet.IsLockedCoin(outpoint) && params.skip_locked)
continue;
if (wallet.IsSpent(outpoint))
@@ -305,10 +307,8 @@ CoinsResult AvailableCoins(const CWallet& wallet,
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
- int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), coinControl);
- // Because CalculateMaximumSignedInputSize just uses ProduceSignature and makes a dummy signature,
- // it is safe to assume that this input is solvable if input_bytes is greater -1.
- bool solvable = input_bytes > -1;
+ int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), can_grind_r, coinControl);
+ bool solvable = provider ? InferDescriptor(output.scriptPubKey, *provider)->IsSolvable() : false;
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
// Filter by spendable outputs only
@@ -362,92 +362,80 @@ CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinContr
return AvailableCoins(wallet, coinControl).GetTotalAmount();
}
-const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransaction& tx, int output)
+const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint& outpoint)
{
AssertLockHeld(wallet.cs_wallet);
- const CTransaction* ptx = &tx;
- int n = output;
+ const CWalletTx* wtx{Assert(wallet.GetWalletTx(outpoint.hash))};
+
+ const CTransaction* ptx = wtx->tx.get();
+ int n = outpoint.n;
while (OutputIsChange(wallet, ptx->vout[n]) && ptx->vin.size() > 0) {
const COutPoint& prevout = ptx->vin[0].prevout;
- auto it = wallet.mapWallet.find(prevout.hash);
- if (it == wallet.mapWallet.end() || it->second.tx->vout.size() <= prevout.n ||
- !wallet.IsMine(it->second.tx->vout[prevout.n])) {
+ const CWalletTx* it = wallet.GetWalletTx(prevout.hash);
+ if (!it || it->tx->vout.size() <= prevout.n ||
+ !wallet.IsMine(it->tx->vout[prevout.n])) {
break;
}
- ptx = it->second.tx.get();
+ ptx = it->tx.get();
n = prevout.n;
}
return ptx->vout[n];
}
-const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint& outpoint)
-{
- AssertLockHeld(wallet.cs_wallet);
- return FindNonChangeParentOutput(wallet, *wallet.GetWalletTx(outpoint.hash)->tx, outpoint.n);
-}
-
std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
{
AssertLockHeld(wallet.cs_wallet);
std::map<CTxDestination, std::vector<COutput>> result;
- for (COutput& coin : AvailableCoinsListUnspent(wallet).All()) {
+ CCoinControl coin_control;
+ // Include watch-only for LegacyScriptPubKeyMan wallets without private keys
+ coin_control.fAllowWatchOnly = wallet.GetLegacyScriptPubKeyMan() && wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
+ CoinFilterParams coins_params;
+ coins_params.only_spendable = false;
+ coins_params.skip_locked = false;
+ for (const COutput& coin : AvailableCoins(wallet, &coin_control, /*feerate=*/std::nullopt, coins_params).All()) {
CTxDestination address;
if ((coin.spendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.solvable)) &&
ExtractDestination(FindNonChangeParentOutput(wallet, coin.outpoint).scriptPubKey, address)) {
- result[address].emplace_back(std::move(coin));
+ result[address].emplace_back(coin);
}
}
-
- std::vector<COutPoint> lockedCoins;
- wallet.ListLockedCoins(lockedCoins);
- // Include watch-only for LegacyScriptPubKeyMan wallets without private keys
- const bool include_watch_only = wallet.GetLegacyScriptPubKeyMan() && wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
- const isminetype is_mine_filter = include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
- for (const COutPoint& output : lockedCoins) {
- auto it = wallet.mapWallet.find(output.hash);
- if (it != wallet.mapWallet.end()) {
- const auto& wtx = it->second;
- int depth = wallet.GetTxDepthInMainChain(wtx);
- if (depth >= 0 && output.n < wtx.tx->vout.size() &&
- wallet.IsMine(wtx.tx->vout[output.n]) == is_mine_filter
- ) {
- CTxDestination address;
- if (ExtractDestination(FindNonChangeParentOutput(wallet, *wtx.tx, output.n).scriptPubKey, address)) {
- const auto out = wtx.tx->vout.at(output.n);
- result[address].emplace_back(
- COutPoint(wtx.GetHash(), output.n), out, depth, CalculateMaximumSignedInputSize(out, &wallet, /*coin_control=*/nullptr), /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ false, wtx.GetTxTime(), CachedTxIsFromMe(wallet, wtx, ISMINE_ALL));
- }
- }
- }
- }
-
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>& ret_discarded_groups)
{
- 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;
-
- 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);
-
- // 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);
+ // Allowing partial spends means no grouping. Each COutput gets its own OutputGroup
+ for (const auto& [type, outputs] : coins.coins) {
+ for (const COutput& output : outputs) {
+ // Get mempool info
+ size_t ancestors, descendants;
+ wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
+
+ // 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);
+
+ // Each filter maps to a different set of groups
+ bool accepted = false;
+ 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);
+ accepted = true;
+ }
+ if (!accepted) ret_discarded_groups.emplace_back(group);
+ }
}
- return groups_out;
+ return filtered_groups;
}
// We want to combine COutputs that have the same scriptPubKey into single OutputGroups
@@ -456,16 +444,11 @@ 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& insert_output = [&](
+ const std::shared_ptr<COutput>& output, OutputType type, size_t ancestors, size_t descendants,
+ ScriptPubKeyToOutgroup& groups_map) {
+ 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
@@ -484,41 +467,84 @@ 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);
- }
-
- // 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;
+ group->Insert(output, ancestors, descendants);
+ };
- // 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;
+ ScriptPubKeyToOutgroup spk_to_groups_map;
+ ScriptPubKeyToOutgroup spk_to_positive_groups_map;
+ for (const auto& [type, outs] : coins.coins) {
+ for (const COutput& output : outs) {
+ 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;
+ const auto& shared_output = std::make_shared<COutput>(output);
+ // Filter for positive only before adding the output
+ if (output.GetEffectiveValue() > 0) {
+ insert_output(shared_output, type, ancestors, descendants, spk_to_positive_groups_map);
}
- // 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);
+ // 'All' groups
+ insert_output(shared_output, type, ancestors, descendants, spk_to_groups_map);
}
}
- 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
+ bool accepted = false;
+ 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);
+ accepted = true;
+ }
+ if (!accepted) ret_discarded_groups.emplace_back(group);
+ }
+ }
+ };
+
+ push_output_groups(spk_to_groups_map, /*positive_only=*/ false);
+ push_output_groups(spk_to_positive_groups_map, /*positive_only=*/ true);
+
+ return filtered_groups;
}
-std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
+FilteredOutputGroups GroupOutputs(const CWallet& wallet,
+ const CoinsResult& coins,
+ const CoinSelectionParams& params,
+ const std::vector<SelectionFilter>& filters)
+{
+ std::vector<OutputGroup> unused;
+ return GroupOutputs(wallet, coins, params, filters, unused);
+}
+
+// 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 CAmount& nTargetValue, 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) {
- if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, it.second, coin_selection_params)}) {
- results.push_back(*result);
- }
+ for (auto& [type, group] : groups.groups_by_type) {
+ auto result{ChooseSelectionResult(nTargetValue, 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.
+ if (result) results.push_back(*result);
}
// If we have at least one solution for funding the transaction without mixing, choose the minimum one according to waste metric
// and return the result
@@ -527,41 +553,37 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
// 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) {
- if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.All(), coin_selection_params)}) {
- return result;
- }
+ if (allow_mixed_output_types && groups.TypesCount() > 1) {
+ 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
// find a solution using all available coins
- return std::nullopt;
+ return util::Error();
};
-std::optional<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.
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(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);
}
// 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(all_groups, nTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
+ if (auto knapsack_result{KnapsackSolver(groups.mixed_group, 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);
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);
results.push_back(*srd_result);
}
if (results.empty()) {
// No solution found
- return std::nullopt;
+ return util::Error();
}
std::vector<SelectionResult> eligible_results;
@@ -571,7 +593,8 @@ std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, cons
});
if (eligible_results.empty()) {
- return std::nullopt;
+ return util::Error{_("The inputs size exceeds the maximum weight. "
+ "Please try sending a smaller amount or manually consolidating your wallet's UTXOs")};
}
// Choose the result with the least waste
@@ -580,15 +603,18 @@ std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, cons
return best_result;
}
-std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const PreSelectedInputs& pre_set_inputs,
- const CAmount& nTargetValue, const CCoinControl& coin_control,
- const CoinSelectionParams& coin_selection_params)
+util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const PreSelectedInputs& pre_set_inputs,
+ const CAmount& nTargetValue, const CCoinControl& coin_control,
+ const CoinSelectionParams& coin_selection_params)
{
// Deduct preset inputs amount from the search target
CAmount selection_target = nTargetValue - pre_set_inputs.total_amount;
// Return if automatic coin selection is disabled, and we don't cover the selection target
- if (!coin_control.m_allow_other_inputs && selection_target > 0) return std::nullopt;
+ if (!coin_control.m_allow_other_inputs && selection_target > 0) {
+ return util::Error{_("The preselected coins total amount does not cover the transaction target. "
+ "Please allow other inputs to be automatically selected or include more coins manually")};
+ }
// Return if we can cover the target only with the preset inputs
if (selection_target <= 0) {
@@ -603,11 +629,11 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
CAmount available_coins_total_amount = coin_selection_params.m_subtract_fee_outputs ? available_coins.GetTotalAmount() :
(available_coins.GetEffectiveTotalAmount().has_value() ? *available_coins.GetEffectiveTotalAmount() : 0);
if (selection_target > available_coins_total_amount) {
- return std::nullopt; // Insufficient funds
+ return util::Error(); // Insufficient funds
}
// Start wallet Coin Selection procedure
- auto op_selection_result = AutomaticCoinSelection(wallet, available_coins, selection_target, coin_control, coin_selection_params);
+ auto op_selection_result = AutomaticCoinSelection(wallet, available_coins, selection_target, coin_selection_params);
if (!op_selection_result) return op_selection_result;
// If needed, add preset inputs to the automatic coin selection result
@@ -622,7 +648,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
return op_selection_result;
}
-std::optional<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 CoinSelectionParams& coin_selection_params)
{
unsigned int limit_ancestor_count = 0;
unsigned int limit_descendant_count = 0;
@@ -631,66 +657,93 @@ std::optional<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coi
const size_t max_descendants = (size_t)std::max<int64_t>(1, limit_descendant_count);
const bool fRejectLongChains = gArgs.GetBoolArg("-walletrejectlongchains", DEFAULT_WALLET_REJECT_LONG_CHAINS);
- // form groups from remaining coins; note that preset coins will not
- // automatically have their associated (same address) coins included
- if (coin_control.m_avoid_partial_spends && available_coins.Size() > OUTPUT_GROUP_MAX_ENTRIES) {
- // Cases where we have 101+ outputs all pointing to the same destination may result in
- // privacy leaks as they will potentially be deterministically sorted. We solve that by
- // explicitly shuffling the outputs before processing
+ // Cases where we have 101+ outputs all pointing to the same destination may result in
+ // privacy leaks as they will potentially be deterministically sorted. We solve that by
+ // explicitly shuffling the outputs before processing
+ if (coin_selection_params.m_avoid_partial_spends && available_coins.Size() > OUTPUT_GROUP_MAX_ENTRIES) {
available_coins.Shuffle(coin_selection_params.rng_fast);
}
// Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the
// transaction at a target feerate. If an attempt fails, more attempts may be made using a more
// permissive CoinEligibilityFilter.
- std::optional<SelectionResult> res = [&] {
- // If possible, fund the transaction with confirmed UTXOs only. Prefer at least six
- // confirmations on outputs received from other wallets and only spend confirmed change.
- if (auto r1{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 6, 0), available_coins, coin_selection_params, /*allow_mixed_output_types=*/false)}) return r1;
- // Allow mixing only if no solution from any single output type can be found
- if (auto r2{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 1, 0), available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) return r2;
-
+ util::Result<SelectionResult> res = [&] {
+ // Place coins eligibility filters on a scope increasing order.
+ std::vector<SelectionFilter> ordered_filters{
+ // If possible, fund the transaction with confirmed UTXOs only. Prefer at least six
+ // confirmations on outputs received from other wallets and only spend confirmed change.
+ {CoinEligibilityFilter(1, 6, 0), /*allow_mixed_output_types=*/false},
+ {CoinEligibilityFilter(1, 1, 0)},
+ };
// Fall back to using zero confirmation change (but with as few ancestors in the mempool as
// possible) if we cannot fund the transaction otherwise.
if (wallet.m_spend_zero_conf_change) {
- if (auto r3{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, 2), available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) return r3;
- if (auto r4{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)),
- available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
- return r4;
- }
- if (auto r5{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2),
- available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
- return r5;
- }
+ ordered_filters.push_back({CoinEligibilityFilter(0, 1, 2)});
+ ordered_filters.push_back({CoinEligibilityFilter(0, 1, std::min(size_t{4}, max_ancestors/3), std::min(size_t{4}, max_descendants/3))});
+ ordered_filters.push_back({CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2)});
// If partial groups are allowed, relax the requirement of spending OutputGroups (groups
// of UTXOs sent to the same address, which are obviously controlled by a single wallet)
// in their entirety.
- if (auto r6{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, /*include_partial=*/true),
- available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
- return r6;
- }
+ ordered_filters.push_back({CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, /*include_partial=*/true)});
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
// received from other wallets.
- if (coin_control.m_include_unsafe_inputs) {
- if (auto r7{AttemptSelection(wallet, value_to_select,
- CoinEligibilityFilter(/*conf_mine=*/0, /*conf_theirs=*/0, max_ancestors-1, max_descendants-1, /*include_partial=*/true),
- available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
- return r7;
- }
+ if (coin_selection_params.m_include_unsafe_inputs) {
+ ordered_filters.push_back({CoinEligibilityFilter(/*conf_mine=*/0, /*conf_theirs*/0, max_ancestors-1, max_descendants-1, /*include_partial=*/true)});
}
// Try with unlimited ancestors/descendants. The transaction will still need to meet
// mempool ancestor/descendant policy to be accepted to mempool and broadcasted, but
// OutputGroups use heuristics that may overestimate ancestor/descendant counts.
if (!fRejectLongChains) {
- if (auto r8{AttemptSelection(wallet, value_to_select,
- CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint64_t>::max(), /*include_partial=*/true),
- available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
- return r8;
- }
+ ordered_filters.push_back({CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max(),
+ std::numeric_limits<uint64_t>::max(),
+ /*include_partial=*/true)});
}
}
- // Coin Selection failed.
- return std::optional<SelectionResult>();
+
+ // Group outputs and map them by coin eligibility filter
+ std::vector<OutputGroup> discarded_groups;
+ FilteredOutputGroups filtered_groups = GroupOutputs(wallet, available_coins, coin_selection_params, ordered_filters, discarded_groups);
+
+ // Check if we still have enough balance after applying filters (some coins might be discarded)
+ CAmount total_discarded = 0;
+ CAmount total_unconf_long_chain = 0;
+ for (const auto& group : discarded_groups) {
+ total_discarded += group.GetSelectionAmount();
+ if (group.m_ancestors >= max_ancestors || group.m_descendants >= max_descendants) total_unconf_long_chain += group.GetSelectionAmount();
+ }
+
+ if (CAmount total_amount = available_coins.GetTotalAmount() - total_discarded < value_to_select) {
+ // Special case, too-long-mempool cluster.
+ if (total_amount + total_unconf_long_chain > value_to_select) {
+ return util::Result<SelectionResult>({_("Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool")});
+ }
+ return util::Result<SelectionResult>(util::Error()); // General "Insufficient Funds"
+ }
+
+ // 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) {
+ 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 {
+ // If any specific error message appears here, then something particularly wrong might have happened.
+ // Save the error and continue the selection process. So if no solutions gets found, we can return
+ // the detailed error to the upper layers.
+ if (HasErrorMsg(res)) res_detailed_errors.emplace_back(res);
+ }
+ }
+
+ // Return right away if we have a detailed error
+ if (!res_detailed_errors.empty()) return res_detailed_errors.front();
+
+
+ // General "Insufficient Funds"
+ return util::Result<SelectionResult>(util::Error());
}();
return res;
@@ -787,6 +840,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
+ coin_selection_params.m_include_unsafe_inputs = coin_control.m_include_unsafe_inputs;
// Set the long term feerate estimate to the wallet's consolidate feerate
coin_selection_params.m_long_term_feerate = wallet.m_consolidate_feerate;
@@ -838,7 +892,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout);
// Get size of spending the change output
- int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &wallet);
+ int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, &wallet, /*coin_control=*/nullptr);
// If the wallet doesn't know how to sign change output, assume p2sh-p2wpkh
// as lower-bound to allow BnB to do it's thing
if (change_spend_size == -1) {
@@ -860,7 +914,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
}
if (feeCalc.reason == FeeReason::FALLBACK && !wallet.m_allow_fallback_fee) {
// eventually allow a fallback fee
- return util::Error{_("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.")};
+ return util::Error{strprintf(_("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable %s."), "-fallbackfee")};
}
// Calculate the cost of change
@@ -901,6 +955,12 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.m_subtract_fee_outputs ? 0 : coin_selection_params.tx_noinputs_size);
CAmount selection_target = recipients_sum + not_input_fees;
+ // This can only happen if feerate is 0, and requested destinations are value of 0 (e.g. OP_RETURN)
+ // and no pre-selected inputs. This will result in 0-input transaction, which is consensus-invalid anyways
+ if (selection_target == 0 && !coin_control.HasSelected()) {
+ return util::Error{_("Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input")};
+ }
+
// Fetch manually selected coins
PreSelectedInputs preset_inputs;
if (coin_control.HasSelected()) {
@@ -917,13 +977,16 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
}
// Choose coins to use
- std::optional<SelectionResult> result = SelectCoins(wallet, available_coins, preset_inputs, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
- if (!result) {
- return util::Error{_("Insufficient funds")};
+ auto select_coins_res = SelectCoins(wallet, available_coins, preset_inputs, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
+ if (!select_coins_res) {
+ // 'SelectCoins' either returns a specific error message or, if empty, means a general "Insufficient funds".
+ const bilingual_str& err = util::ErrorString(select_coins_res);
+ return util::Error{err.empty() ?_("Insufficient funds") : err};
}
- TRACE5(coin_selection, selected_coins, wallet.GetName().c_str(), GetAlgorithmName(result->GetAlgo()).c_str(), result->GetTarget(), result->GetWaste(), result->GetSelectedValue());
+ const SelectionResult& result = *select_coins_res;
+ TRACE5(coin_selection, selected_coins, wallet.GetName().c_str(), GetAlgorithmName(result.GetAlgo()).c_str(), result.GetTarget(), result.GetWaste(), result.GetSelectedValue());
- const CAmount change_amount = result->GetChange(coin_selection_params.min_viable_change, coin_selection_params.m_change_fee);
+ const CAmount change_amount = result.GetChange(coin_selection_params.min_viable_change, coin_selection_params.m_change_fee);
if (change_amount > 0) {
CTxOut newTxOut(change_amount, scriptChange);
if (nChangePosInOut == -1) {
@@ -938,7 +1001,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
}
// 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.
@@ -950,7 +1013,7 @@ 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));
}
DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight());
@@ -963,7 +1026,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
const CAmount output_value = CalculateOutputValue(txNew);
Assume(recipients_sum + change_amount == output_value);
- CAmount current_fee = result->GetSelectedValue() - output_value;
+ CAmount current_fee = result.GetSelectedValue() - output_value;
// Sanity check that the fee cannot be negative as that means we have more output value than input value
if (current_fee < 0) {
@@ -974,7 +1037,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
if (nChangePosInOut != -1 && fee_needed < current_fee) {
auto& change = txNew.vout.at(nChangePosInOut);
change.nValue += current_fee - fee_needed;
- current_fee = result->GetSelectedValue() - CalculateOutputValue(txNew);
+ current_fee = result.GetSelectedValue() - CalculateOutputValue(txNew);
if (fee_needed != current_fee) {
return util::Error{Untranslated(STR_INTERNAL_BUG("Change adjustment: Fee needed != fee paid"))};
}
@@ -1013,7 +1076,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
}
++i;
}
- current_fee = result->GetSelectedValue() - CalculateOutputValue(txNew);
+ current_fee = result.GetSelectedValue() - CalculateOutputValue(txNew);
if (fee_needed != current_fee) {
return util::Error{Untranslated(STR_INTERNAL_BUG("SFFO: Fee needed != fee paid"))};
}
@@ -1097,6 +1160,13 @@ util::Result<CreatedTransactionResult> CreateTransaction(
TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str());
CCoinControl tmp_cc = coin_control;
tmp_cc.m_avoid_partial_spends = true;
+
+ // Re-use the change destination from the first creation attempt to avoid skipping BIP44 indexes
+ const int ungrouped_change_pos = txr_ungrouped.change_pos;
+ if (ungrouped_change_pos != -1) {
+ ExtractDestination(txr_ungrouped.tx->vout[ungrouped_change_pos].scriptPubKey, tmp_cc.destChange);
+ }
+
auto txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, tmp_cc, sign);
// if fee of this alternative one is within the range of the max fee, we use this one
const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee) : false};