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.cpp117
1 files changed, 47 insertions, 70 deletions
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index 001acd04e2..92cf771358 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -502,32 +502,20 @@ std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, cons
// Vector of results. We will choose the best one based on waste.
std::vector<SelectionResult> results;
- // Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
- std::vector<OutputGroup> positive_groups = GroupOutputs(wallet, available_coins, coin_selection_params, eligibility_filter, true /* positive_only */);
+ 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)}) {
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, false /* positive_only */);
- CAmount target_with_change = nTargetValue;
- // While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
- // So we need to include that for KnapsackSolver and SRD as well, as we are expecting to create a change output.
- if (!coin_selection_params.m_subtract_fee_outputs) {
- target_with_change += coin_selection_params.m_change_fee;
- }
- if (auto knapsack_result{KnapsackSolver(all_groups, target_with_change, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
- knapsack_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
+ 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)}) {
+ 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);
}
- // Include change for SRD as we want to avoid making really small change if the selection just
- // barely meets the target. Just use the lower bound change target instead of the randomly
- // generated one, since SRD will result in a random change amount anyway; avoid making the
- // target needlessly large.
- const CAmount srd_target = target_with_change + CHANGE_LOWER;
- if (auto srd_result{SelectCoinsSRD(positive_groups, srd_target, coin_selection_params.rng_fast)}) {
- srd_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
+ if (auto srd_result{SelectCoinsSRD(positive_groups, 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);
}
@@ -603,7 +591,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
SelectionResult result(nTargetValue, SelectionAlgorithm::MANUAL);
result.AddInput(preset_inputs);
if (result.GetSelectedValue() < nTargetValue) return std::nullopt;
- result.ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
+ result.ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
return result;
}
@@ -628,12 +616,15 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
available_coins.Shuffle(coin_selection_params.rng_fast);
}
+ SelectionResult preselected(preset_inputs.GetSelectionAmount(), SelectionAlgorithm::MANUAL);
+ preselected.AddInput(preset_inputs);
+
// 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 (value_to_select <= 0) return std::make_optional(SelectionResult(value_to_select, 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.
@@ -687,9 +678,9 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
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);
+ res->Merge(preselected);
+ if (res->GetAlgo() == SelectionAlgorithm::MANUAL) {
+ res->ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
}
return res;
@@ -803,7 +794,6 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
coin_selection_params.m_subtract_fee_outputs = true;
}
}
- coin_selection_params.m_min_change_target = GenerateChangeTarget(std::floor(recipients_sum / vecSend.size()), rng_fast);
// Create change script that will be used if we need change
CScript scriptChange;
@@ -872,6 +862,15 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
coin_selection_params.m_change_fee = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size);
coin_selection_params.m_cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_change_fee;
+ coin_selection_params.m_min_change_target = GenerateChangeTarget(std::floor(recipients_sum / vecSend.size()), coin_selection_params.m_change_fee, rng_fast);
+
+ // The smallest change amount should be:
+ // 1. at least equal to dust threshold
+ // 2. at least 1 sat greater than fees to spend it at m_discard_feerate
+ const auto dust = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate);
+ const auto change_spend_fee = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size);
+ coin_selection_params.min_viable_change = std::max(change_spend_fee + 1, dust);
+
// vouts to the payees
if (!coin_selection_params.m_subtract_fee_outputs) {
coin_selection_params.tx_noinputs_size = 10; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 witness overhead (dummy, flag, stack size)
@@ -910,25 +909,22 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
if (!result) {
return util::Error{_("Insufficient funds")};
}
- TRACE5(coin_selection, selected_coins, wallet.GetName().c_str(), GetAlgorithmName(result->m_algo).c_str(), result->m_target, result->GetWaste(), result->GetSelectedValue());
-
- // Always make a change output
- // We will reduce the fee from this change output later, and remove the output if it is too small.
- const CAmount change_and_fee = result->GetSelectedValue() - recipients_sum;
- assert(change_and_fee >= 0);
- CTxOut newTxOut(change_and_fee, scriptChange);
-
- if (nChangePosInOut == -1) {
- // Insert change txn at random position:
- nChangePosInOut = rng_fast.randrange(txNew.vout.size() + 1);
- }
- else if ((unsigned int)nChangePosInOut > txNew.vout.size()) {
- return util::Error{_("Transaction change output index out of range")};
+ 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);
+ if (change_amount > 0) {
+ CTxOut newTxOut(change_amount, scriptChange);
+ if (nChangePosInOut == -1) {
+ // Insert change txn at random position:
+ nChangePosInOut = rng_fast.randrange(txNew.vout.size() + 1);
+ } else if ((unsigned int)nChangePosInOut > txNew.vout.size()) {
+ return util::Error{_("Transaction change output index out of range")};
+ }
+ txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut);
+ } else {
+ nChangePosInOut = -1;
}
- assert(nChangePosInOut != -1);
- auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut);
-
// Shuffle selected coins and fill in final vin
std::vector<COutput> selected_coins = result->GetShuffledInputVector();
@@ -952,42 +948,23 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
if (nBytes == -1) {
return util::Error{_("Missing solving data for estimating transaction size")};
}
- nFeeRet = coin_selection_params.m_effective_feerate.GetFee(nBytes);
-
- // Subtract fee from the change output if not subtracting it from recipient outputs
- CAmount fee_needed = nFeeRet;
- if (!coin_selection_params.m_subtract_fee_outputs) {
- change_position->nValue -= fee_needed;
- }
-
- // We want to drop the change to fees if:
- // 1. The change output would be dust
- // 2. The change is within the (almost) exact match window, i.e. it is less than or equal to the cost of the change output (cost_of_change)
- CAmount change_amount = change_position->nValue;
- if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change)
- {
- nChangePosInOut = -1;
- change_amount = 0;
- txNew.vout.erase(change_position);
-
- // Because we have dropped this change, the tx size and required fee will be different, so let's recalculate those
- tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control);
- nBytes = tx_sizes.vsize;
- fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
- }
+ CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
+ nFeeRet = result->GetSelectedValue() - recipients_sum - change_amount;
- // The only time that fee_needed should be less than the amount available for fees (in change_and_fee - change_amount) is when
+ // The only time that fee_needed should be less than the amount available for fees is when
// we are subtracting the fee from the outputs. If this occurs at any other time, it is a bug.
- assert(coin_selection_params.m_subtract_fee_outputs || fee_needed <= change_and_fee - change_amount);
+ assert(coin_selection_params.m_subtract_fee_outputs || fee_needed <= nFeeRet);
- // Update nFeeRet in case fee_needed changed due to dropping the change output
- if (fee_needed <= change_and_fee - change_amount) {
- nFeeRet = change_and_fee - change_amount;
+ // If there is a change output and we overpay the fees then increase the change to match the fee needed
+ if (nChangePosInOut != -1 && fee_needed < nFeeRet) {
+ auto& change = txNew.vout.at(nChangePosInOut);
+ change.nValue += nFeeRet - fee_needed;
+ nFeeRet = fee_needed;
}
// Reduce output values for subtractFeeFromAmount
if (coin_selection_params.m_subtract_fee_outputs) {
- CAmount to_reduce = fee_needed + change_amount - change_and_fee;
+ CAmount to_reduce = fee_needed - nFeeRet;
int i = 0;
bool fFirst = true;
for (const auto& recipient : vecSend)