diff options
Diffstat (limited to 'src/wallet/spend.cpp')
-rw-r--r-- | src/wallet/spend.cpp | 408 |
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}; |