Skip to content

Commit 0f402b9

Browse files
committed
Fix rare edge case of paying too many fees when transaction has no change.
Due to the iterative process of selecting new coins in each loop a new fee is calculated that needs to be met each time. In the typical case if the most recent iteration of the loop produced a much smaller transaction and we have now gathered inputs with too many fees, we can just reduce the change. However in the case where there is no change output, it is possible to end up with a transaction which drastically overpays fees. This commit addresses that case, by creating a change output if the overpayment is large enough to support it, this is accomplished by rerunning the transaction creation loop without selecting new coins. Thanks to instagibbs for working on this as well
1 parent 253cd7e commit 0f402b9

File tree

1 file changed

+40
-13
lines changed

1 file changed

+40
-13
lines changed

src/wallet/wallet.cpp

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2600,8 +2600,12 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
26002600

26012601
scriptChange = GetScriptForDestination(vchPubKey.GetID());
26022602
}
2603+
CTxOut change_prototype_txout(0, scriptChange);
2604+
size_t change_prototype_size = GetSerializeSize(change_prototype_txout, SER_DISK, 0);
26032605

26042606
nFeeRet = 0;
2607+
bool pick_new_inputs = true;
2608+
CAmount nValueIn = 0;
26052609
// Start with no fee and loop until there is enough fee
26062610
while (true)
26072611
{
@@ -2647,15 +2651,18 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
26472651
}
26482652

26492653
// Choose coins to use
2650-
CAmount nValueIn = 0;
2651-
setCoins.clear();
2652-
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coinControl))
2653-
{
2654-
strFailReason = _("Insufficient funds");
2655-
return false;
2654+
if (pick_new_inputs) {
2655+
nValueIn = 0;
2656+
setCoins.clear();
2657+
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coinControl))
2658+
{
2659+
strFailReason = _("Insufficient funds");
2660+
return false;
2661+
}
26562662
}
26572663

26582664
const CAmount nChange = nValueIn - nValueToSelect;
2665+
26592666
if (nChange > 0)
26602667
{
26612668
// Fill a vout to ourself
@@ -2739,16 +2746,30 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
27392746
}
27402747

27412748
if (nFeeRet >= nFeeNeeded) {
2742-
// Reduce fee to only the needed amount if we have change
2743-
// output to increase. This prevents potential overpayment
2744-
// in fees if the coins selected to meet nFeeNeeded result
2745-
// in a transaction that requires less fee than the prior
2746-
// iteration.
2749+
// Reduce fee to only the needed amount if possible. This
2750+
// prevents potential overpayment in fees if the coins
2751+
// selected to meet nFeeNeeded result in a transaction that
2752+
// requires less fee than the prior iteration.
2753+
27472754
// TODO: The case where nSubtractFeeFromAmount > 0 remains
27482755
// to be addressed because it requires returning the fee to
27492756
// the payees and not the change output.
2750-
// TODO: The case where there is no change output remains
2751-
// to be addressed so we avoid creating too small an output.
2757+
2758+
// If we have no change and a big enough excess fee, then
2759+
// try to construct transaction again only without picking
2760+
// new inputs. We now know we only need the smaller fee
2761+
// (because of reduced tx size) and so we should add a
2762+
// change output. Only try this once.
2763+
CAmount fee_needed_for_change = GetMinimumFee(change_prototype_size, currentConfirmationTarget, ::mempool, ::feeEstimator, nullptr, false /* ignoreGlobalPayTxFee */, conservative_estimate);
2764+
CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, ::dustRelayFee);
2765+
CAmount max_excess_fee = fee_needed_for_change + minimum_value_for_change;
2766+
if (nFeeRet > nFeeNeeded + max_excess_fee && nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs) {
2767+
pick_new_inputs = false;
2768+
nFeeRet = nFeeNeeded + fee_needed_for_change;
2769+
continue;
2770+
}
2771+
2772+
// If we have change output already, just increase it
27522773
if (nFeeRet > nFeeNeeded && nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) {
27532774
CAmount extraFeePaid = nFeeRet - nFeeNeeded;
27542775
std::vector<CTxOut>::iterator change_position = txNew.vout.begin()+nChangePosInOut;
@@ -2757,6 +2778,12 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
27572778
}
27582779
break; // Done, enough fee included.
27592780
}
2781+
else if (!pick_new_inputs) {
2782+
// This shouldn't happen, we should have had enough excess
2783+
// fee to pay for the new output and still meet nFeeNeeded
2784+
strFailReason = _("Transaction fee and change calculation failed");
2785+
return false;
2786+
}
27602787

27612788
// Try to reduce change to include necessary fee
27622789
if (nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) {

0 commit comments

Comments
 (0)