Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/qt/sendcoinsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,9 @@ bool SendCoinsDialog::send(const QList<SendCoinsRecipient>& recipients, QString&

updateCoinControlState();

prepareStatus = model->prepareTransaction(*m_current_transaction, *m_coin_control);
CCoinControl coin_control = *m_coin_control;
coin_control.m_allow_other_inputs = !coin_control.HasSelected(); // future, could introduce a checkbox to customize this value.
prepareStatus = model->prepareTransaction(*m_current_transaction, coin_control);

// process prepareStatus and on error generate message shown to user
processSendCoinsReturn(prepareStatus,
Expand Down
8 changes: 7 additions & 1 deletion src/test/util/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ const std::string ADDRESS_BCRT1_UNSPENDABLE = "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqq
#ifdef ENABLE_WALLET
std::string getnewaddress(CWallet& w)
{
return EncodeDestination(*Assert(w.GetNewDestination("")));
return EncodeDestination(getNewDestination(w, OutputType::LEGACY));
}

CTxDestination getNewDestination(CWallet& w, OutputType /* output_type */)
{
// Dash only supports LEGACY output type (no SegWit)
return *Assert(w.GetNewDestination(""));
}

// void importaddress(CWallet& wallet, const std::string& address)
Expand Down
5 changes: 4 additions & 1 deletion src/test/util/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef BITCOIN_TEST_UTIL_WALLET_H
#define BITCOIN_TEST_UTIL_WALLET_H

#include <outputtype.h>
#include <string>

namespace wallet {
Expand All @@ -20,8 +21,10 @@ extern const std::string ADDRESS_BCRT1_UNSPENDABLE;

/** Import the address to the wallet */
void importaddress(wallet::CWallet& wallet, const std::string& address);
/** Returns a new address from the wallet */
/** Returns a new encoded destination from the wallet (hardcoded to BECH32) */
std::string getnewaddress(wallet::CWallet& w);
/** Returns a new destination, of an specific type, from the wallet */
CTxDestination getNewDestination(wallet::CWallet& w, OutputType output_type);


#endif // BITCOIN_TEST_UTIL_WALLET_H
2 changes: 1 addition & 1 deletion src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CCoinControl
bool m_include_unsafe_inputs = false;
//! If true, the selection process can add extra unselected inputs from the wallet
//! while requires all selected inputs be used
bool m_allow_other_inputs = false;
bool m_allow_other_inputs = true;
//! If false, only include as many inputs as necessary to fulfill a coin selection request. Only usable together with m_allow_other_inputs
bool fRequireAllInputs = true;
//! Includes watch only addresses which are solvable
Expand Down
16 changes: 16 additions & 0 deletions src/wallet/coinselection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,22 @@ void SelectionResult::AddInput(const OutputGroup& group)
m_use_effective = !group.m_subtract_fee_outputs;
}

void SelectionResult::AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs)
{
util::insert(m_selected_inputs, inputs);
m_use_effective = !subtract_fee_outputs;
}

void SelectionResult::Merge(const SelectionResult& other)
{
m_target += other.m_target;
m_use_effective |= other.m_use_effective;
if (m_algo == SelectionAlgorithm::MANUAL) {
m_algo = other.m_algo;
}
util::insert(m_selected_inputs, other.m_selected_inputs);
}

const std::set<COutput>& SelectionResult::GetInputSet() const
{
return m_selected_inputs;
Expand Down
8 changes: 6 additions & 2 deletions src/wallet/coinselection.h
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,9 @@ struct SelectionResult

public:
/** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */
const CAmount m_target;
CAmount m_target;
/** The algorithm used to produce this result */
const SelectionAlgorithm m_algo;
SelectionAlgorithm m_algo;

explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
: m_target(target), m_algo(algo) {}
Expand All @@ -299,6 +299,10 @@ struct SelectionResult
void Clear();

void AddInput(const OutputGroup& group);
void AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs);

/** Combine another SelectionResult into this one */
void Merge(const SelectionResult& other);

/** Calculates and stores the waste for this selection via GetSelectionWaste */
void ComputeAndSetWaste(CAmount change_cost);
Expand Down
194 changes: 89 additions & 105 deletions src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,46 @@ void CoinsResult::clear()
other.clear();
}

// Fetch and validate the coin control selected inputs.
// Coins could be internal (from the wallet) or external.
util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const CCoinControl& coin_control,
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{
PreSelectedInputs result;
std::vector<COutPoint> vPresetInputs;
coin_control.ListSelected(vPresetInputs);
for (const COutPoint& outpoint : vPresetInputs) {
int input_bytes = -1;
CTxOut txout;
if (auto ptr_wtx = wallet.GetWalletTx(outpoint.hash)) {
// Clearly invalid input, fail
if (ptr_wtx->tx->vout.size() <= outpoint.n) {
return util::Error{strprintf(_("Invalid pre-selected input %s"), outpoint.ToString())};
}
txout = ptr_wtx->tx->vout.at(outpoint.n);
input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control);
} else {
// The input is external. We did not find the tx in mapWallet.
if (!coin_control.GetExternalOutput(outpoint, txout)) {
return util::Error{strprintf(_("Not found pre-selected input %s"), outpoint.ToString())};
}
}

if (input_bytes == -1) {
input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, &coin_control);
}

if (input_bytes == -1) {
return util::Error{strprintf(_("Not solvable pre-selected input %s"), outpoint.ToString())}; // Not solvable, can't estimate size for fee
}

/* Set some defaults for depth, spendable, solvable, safe, time, and from_me as these don't matter for preset inputs since no selection is being done. */
COutput output(outpoint, txout, /*depth=*/ 0, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, coin_selection_params.m_effective_feerate);
result.Insert(output, coin_selection_params.m_subtract_fee_outputs);
}
return result;
}

CoinsResult AvailableCoins(const CWallet& wallet,
const CCoinControl* coinControl,
std::optional<CFeeRate> feerate,
Expand Down Expand Up @@ -185,7 +225,8 @@ CoinsResult AvailableCoins(const CWallet& wallet,
if (output.nValue < nMinimumAmount || output.nValue > nMaximumAmount)
continue;

if (coinControl && coinControl->HasSelected() && !coinControl->m_allow_other_inputs && !coinControl->IsSelected(outpoint))
// Skip manually selected coins (the caller can fetch them directly)
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
continue;

if (wallet.IsLockedCoin(outpoint) && nCoinType != CoinType::ONLY_MASTERNODE_COLLATERAL)
Expand Down Expand Up @@ -511,95 +552,42 @@ std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, cons
return best_result;
}

std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
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)
{
// Note: this function should never be used for "always free" tx types like dstx
CoinType nCoinType = coin_control.nCoinType;
CAmount value_to_select = nTargetValue;

OutputGroup preset_inputs(coin_selection_params);
// Deduct preset inputs amount from the search target
CAmount selection_target = nTargetValue - pre_set_inputs.total_amount;

// calculate value from preset inputs and store them
std::set<COutPoint> preset_coins;
// 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;

std::vector<COutPoint> vPresetInputs;
coin_control.ListSelected(vPresetInputs);
for (const COutPoint& outpoint : vPresetInputs) {
int input_bytes = -1;
CTxOut txout;
auto ptr_wtx = wallet.GetWalletTx(outpoint.hash);
if (ptr_wtx) {
// Clearly invalid input, fail
if (ptr_wtx->tx->vout.size() <= outpoint.n) {
return std::nullopt;
}
txout = ptr_wtx->tx->vout.at(outpoint.n);
input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control);
} else {
// The input is external. We did not find the tx in mapWallet.
if (!coin_control.GetExternalOutput(outpoint, txout)) {
return std::nullopt;
}
input_bytes = CalculateMaximumSignedInputSize(txout, outpoint, &coin_control.m_external_provider, &coin_control);
}
if (nCoinType == CoinType::ONLY_FULLY_MIXED) {
// Make sure to include mixed preset inputs only,
// even if some non-mixed inputs were manually selected via CoinControl
if (!wallet.IsFullyMixed(outpoint)) continue;
}
if (input_bytes == -1) {
return std::nullopt; // Not solvable, can't estimate size for fee
}

/* Set some defaults for depth, spendable, solvable, safe, time, and from_me as these don't matter for preset inputs since no selection is being done. */
COutput output(outpoint, txout, /*depth=*/ 0, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, coin_selection_params.m_effective_feerate);
if (coin_selection_params.m_subtract_fee_outputs) {
value_to_select -= output.txout.nValue;
} else {
value_to_select -= output.GetEffectiveValue();
}
preset_coins.insert(outpoint);
/* Set ancestors and descendants to 0 as they don't matter for preset inputs since no actual selection is being done.
* positive_only is set to false because we want to include all preset inputs, even if they are dust.
*/
preset_inputs.Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false);
}

// coin control -> return all selected outputs (we want all selected to go into the transaction for sure)
if (coin_control.HasSelected() && !coin_control.m_allow_other_inputs) {
// Return if we can cover the target only with the preset inputs
if (selection_target <= 0) {
SelectionResult result(nTargetValue, SelectionAlgorithm::MANUAL);
bool all_inputs{coin_control.fRequireAllInputs};
if (!all_inputs) {
// Calculate the smallest set of inputs required to meet nTargetValue from available_coins
bool success{false};
OutputGroup preset_candidates(coin_selection_params);
for (const COutput& out : available_coins.all()) {
if (!out.spendable) continue;
if (preset_coins.count(out.outpoint)) {
preset_candidates.Insert(out, /*ancestors=*/0, /*descendants=*/0, /*positive_only=*/false);
}
if (preset_candidates.GetSelectionAmount() >= nTargetValue) {
result.AddInput(preset_candidates);
success = true;
break;
}
}
// Couldn't meet target, add all inputs
if (!success) all_inputs = true;
}
if (all_inputs) {
result.AddInput(preset_inputs);
}
if (result.GetSelectedValue() < nTargetValue) return std::nullopt;
result.AddInputs(pre_set_inputs.coins, coin_selection_params.m_subtract_fee_outputs);
result.ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
return result;
}

// remove preset inputs from coins so that Coin Selection doesn't pick them.
if (coin_control.HasSelected()) {
available_coins.legacy.erase(remove_if(available_coins.legacy.begin(), available_coins.legacy.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.legacy.end());
available_coins.other.erase(remove_if(available_coins.other.begin(), available_coins.other.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.other.end());
// Start wallet Coin Selection procedure
auto op_selection_result = AutomaticCoinSelection(wallet, available_coins, selection_target, coin_control, coin_selection_params);
if (!op_selection_result) return op_selection_result;

// If needed, add preset inputs to the automatic coin selection result
if (!pre_set_inputs.coins.empty()) {
SelectionResult preselected(pre_set_inputs.total_amount, SelectionAlgorithm::MANUAL);
preselected.AddInputs(pre_set_inputs.coins, coin_selection_params.m_subtract_fee_outputs);
op_selection_result->Merge(preselected);
op_selection_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
}
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)
{
// Note: this function should never be used for "always free" tx types like dstx
CoinType nCoinType = coin_control.nCoinType;

unsigned int limit_ancestor_count = 0;
unsigned int limit_descendant_count = 0;
Expand All @@ -617,13 +605,11 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
Shuffle(available_coins.legacy.begin(), available_coins.legacy.end(), coin_selection_params.rng_fast);
Shuffle(available_coins.other.begin(), available_coins.other.end(), 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 = [&] {
// Pre-selected inputs already cover the target amount.
if (value_to_select <= 0) return std::make_optional(SelectionResult(nTargetValue, SelectionAlgorithm::MANUAL));

// 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, nCoinType)}) return r1;
Expand Down Expand Up @@ -673,14 +659,6 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
return std::optional<SelectionResult>();
}();

if (!res) return std::nullopt;

// Add preset inputs to result
res->AddInput(preset_inputs);
if (res->m_algo == SelectionAlgorithm::MANUAL) {
res->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
}

return res;
}

Expand Down Expand Up @@ -884,23 +862,29 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(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()) {
auto res_fetch_inputs = FetchSelectedInputs(wallet, coin_control, coin_selection_params);
if (!res_fetch_inputs) return util::Error{util::ErrorString(res_fetch_inputs)};
preset_inputs = *res_fetch_inputs;
}

// Get available coins
auto available_coins = AvailableCoins(wallet,
&coin_control,
coin_selection_params.m_effective_feerate,
1, /*nMinimumAmount*/
MAX_MONEY, /*nMaximumAmount*/
MAX_MONEY, /*nMinimumSumAmount*/
0); /*nMaximumCount*/
// Fetch wallet available coins if "other inputs" are
// allowed (coins automatically selected by the wallet)
CoinsResult available_coins;
if (coin_control.m_allow_other_inputs) {
available_coins = AvailableCoins(wallet,
&coin_control,
coin_selection_params.m_effective_feerate,
1, /*nMinimumAmount*/
MAX_MONEY, /*nMaximumAmount*/
MAX_MONEY, /*nMinimumSumAmount*/
0); /*nMaximumCount*/
}

// Choose coins to use
std::optional<SelectionResult> result = SelectCoins(wallet, available_coins, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
std::optional<SelectionResult> result = SelectCoins(wallet, available_coins, preset_inputs, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
if (!result) {
if (coin_control.nCoinType == CoinType::ONLY_NONDENOMINATED) {
return util::Error{_("Unable to locate enough non-denominated funds for this transaction.")};
Expand Down
Loading
Loading