Skip to content

Commit cc3f14b

Browse files
committed
Move output reductions for fee to after coin selection
Simplifies CreateTransactionInternal without changing behavior. Removes the pick_new_inputs variable by moving the subtract fee from amount implementation to later in the loop to where it is possible to calculate the fee for the transaction. This allows the fee to be subtracted from the outputs within a single iteration, instead of calculating the fee in the first iteration, and subtracting the fee in the second. This also removes another scenario where a second iteration of the loop finds a smaller input set (and thus smaller fees than the first iteration) with no change and so a third iteration of the loop is done in order to make a change output that contains the excess fees. To handle these cases, we always create a change output which contains the difference between selected input values and the recipient amounts. Once the transaction fee is calculated, the change output is reduced (in the normal case) or the recipient amounts are reduced (in the subtract fee from amount case). All of this is done in a single iteration of the loop.
1 parent d97d25d commit cc3f14b

File tree

1 file changed

+94
-130
lines changed

1 file changed

+94
-130
lines changed

src/wallet/wallet.cpp

Lines changed: 94 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,7 +2834,6 @@ bool CWallet::CreateTransactionInternal(
28342834

28352835
CMutableTransaction txNew;
28362836
FeeCalculation feeCalc;
2837-
CAmount nFeeNeeded;
28382837
std::pair<int64_t, int64_t> tx_sizes;
28392838
int nBytes;
28402839
{
@@ -2919,7 +2918,6 @@ bool CWallet::CreateTransactionInternal(
29192918
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;
29202919

29212920
nFeeRet = 0;
2922-
bool pick_new_inputs = true;
29232921
CAmount nValueIn = 0;
29242922

29252923
// BnB selector is the only selector used when this is true.
@@ -2932,7 +2930,6 @@ bool CWallet::CreateTransactionInternal(
29322930
nChangePosInOut = nChangePosRequest;
29332931
txNew.vin.clear();
29342932
txNew.vout.clear();
2935-
bool fFirst = true;
29362933

29372934
CAmount nValueToSelect = nValue;
29382935
if (nSubtractFeeFromAmount == 0)
@@ -2946,169 +2943,136 @@ bool CWallet::CreateTransactionInternal(
29462943
{
29472944
CTxOut txout(recipient.nAmount, recipient.scriptPubKey);
29482945

2949-
if (recipient.fSubtractFeeFromAmount)
2950-
{
2951-
assert(nSubtractFeeFromAmount != 0);
2952-
txout.nValue -= nFeeRet / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient
2953-
2954-
if (fFirst) // first receiver pays the remainder not divisible by output count
2955-
{
2956-
fFirst = false;
2957-
txout.nValue -= nFeeRet % nSubtractFeeFromAmount;
2958-
}
2959-
}
2960-
// Include the fee cost for outputs. Note this is only used for BnB right now
2946+
// Include the fee cost for outputs.
29612947
if (!coin_selection_params.m_subtract_fee_outputs) {
29622948
coin_selection_params.tx_noinputs_size += ::GetSerializeSize(txout, PROTOCOL_VERSION);
29632949
}
29642950

29652951
if (IsDust(txout, chain().relayDustFee()))
29662952
{
2967-
if (recipient.fSubtractFeeFromAmount && nFeeRet > 0)
2968-
{
2969-
if (txout.nValue < 0)
2970-
error = _("The transaction amount is too small to pay the fee");
2971-
else
2972-
error = _("The transaction amount is too small to send after the fee has been deducted");
2973-
}
2974-
else
2975-
error = _("Transaction amount too small");
2953+
error = _("Transaction amount too small");
29762954
return false;
29772955
}
29782956
txNew.vout.push_back(txout);
29792957
}
29802958

29812959
// Choose coins to use
29822960
bool bnb_used = false;
2983-
if (pick_new_inputs) {
2984-
nValueIn = 0;
2985-
setCoins.clear();
2986-
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used))
2987-
{
2988-
// If BnB was used, it was the first pass. No longer the first pass and continue loop with knapsack.
2989-
if (bnb_used) {
2990-
coin_selection_params.use_bnb = false;
2991-
continue;
2992-
}
2993-
else {
2994-
error = _("Insufficient funds");
2995-
return false;
2996-
}
2961+
nValueIn = 0;
2962+
setCoins.clear();
2963+
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used))
2964+
{
2965+
// If BnB was used, it was the first pass. No longer the first pass and continue loop with knapsack.
2966+
if (bnb_used) {
2967+
coin_selection_params.use_bnb = false;
2968+
continue;
2969+
}
2970+
else {
2971+
error = _("Insufficient funds");
2972+
return false;
29972973
}
2998-
} else {
2999-
bnb_used = false;
30002974
}
30012975

3002-
const CAmount change_and_fee = nValueIn - nValueToSelect;
3003-
if (change_and_fee > 0)
3004-
{
3005-
// Fill a vout to ourself
3006-
CTxOut newTxOut(change_and_fee, scriptChange);
3007-
3008-
// Never create dust outputs; if we would, just
3009-
// add the dust to the fee.
3010-
// The change_and_fee when BnB is used is always going to go to fees.
3011-
if (IsDust(newTxOut, coin_selection_params.m_discard_feerate) || bnb_used)
3012-
{
3013-
nChangePosInOut = -1;
3014-
nFeeRet += change_and_fee;
3015-
}
3016-
else
3017-
{
3018-
if (nChangePosInOut == -1)
3019-
{
3020-
// Insert change txn at random position:
3021-
nChangePosInOut = GetRandInt(txNew.vout.size()+1);
3022-
}
3023-
else if ((unsigned int)nChangePosInOut > txNew.vout.size())
3024-
{
3025-
error = _("Change index out of range");
3026-
return false;
3027-
}
2976+
// Always make a change output
2977+
// We will reduce the fee from this change output later, and remove the output if it is too small.
2978+
const CAmount change_and_fee = nValueIn - nValue;
2979+
assert(change_and_fee >= 0);
2980+
CTxOut newTxOut(change_and_fee, scriptChange);
30282981

3029-
std::vector<CTxOut>::iterator position = txNew.vout.begin()+nChangePosInOut;
3030-
txNew.vout.insert(position, newTxOut);
3031-
}
3032-
} else {
3033-
nChangePosInOut = -1;
2982+
if (nChangePosInOut == -1)
2983+
{
2984+
// Insert change txn at random position:
2985+
nChangePosInOut = GetRandInt(txNew.vout.size()+1);
30342986
}
2987+
else if ((unsigned int)nChangePosInOut > txNew.vout.size())
2988+
{
2989+
error = _("Change index out of range");
2990+
return false;
2991+
}
2992+
2993+
assert(nChangePosInOut != -1);
2994+
auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut);
30352995

30362996
// Dummy fill vin for maximum size estimation
30372997
//
30382998
for (const auto& coin : setCoins) {
30392999
txNew.vin.push_back(CTxIn(coin.outpoint,CScript()));
30403000
}
30413001

3002+
// Calculate the transaction fee
30423003
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly);
30433004
nBytes = tx_sizes.first;
30443005
if (nBytes < 0) {
30453006
error = _("Signing transaction failed");
30463007
return false;
30473008
}
3009+
nFeeRet = coin_selection_params.m_effective_feerate.GetFee(nBytes);
30483010

3049-
nFeeNeeded = coin_selection_params.m_effective_feerate.GetFee(nBytes);
3050-
if (nFeeRet >= nFeeNeeded) {
3051-
// Reduce fee to only the needed amount if possible. This
3052-
// prevents potential overpayment in fees if the coins
3053-
// selected to meet nFeeNeeded result in a transaction that
3054-
// requires less fee than the prior iteration.
3055-
3056-
// If we have no change and a big enough excess fee, then
3057-
// try to construct transaction again only without picking
3058-
// new inputs. We now know we only need the smaller fee
3059-
// (because of reduced tx size) and so we should add a
3060-
// change output. Only try this once.
3061-
if (nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs) {
3062-
unsigned int tx_size_with_change = nBytes + coin_selection_params.change_output_size + 2; // Add 2 as a buffer in case increasing # of outputs changes compact size
3063-
CAmount fee_needed_with_change = coin_selection_params.m_effective_feerate.GetFee(tx_size_with_change);
3064-
CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate);
3065-
if (nFeeRet >= fee_needed_with_change + minimum_value_for_change) {
3066-
pick_new_inputs = false;
3067-
nFeeRet = fee_needed_with_change;
3068-
continue;
3069-
}
3070-
}
3071-
3072-
// If we have change output already, just increase it
3073-
if (nFeeRet > nFeeNeeded && nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) {
3074-
CAmount extraFeePaid = nFeeRet - nFeeNeeded;
3075-
std::vector<CTxOut>::iterator change_position = txNew.vout.begin()+nChangePosInOut;
3076-
change_position->nValue += extraFeePaid;
3077-
nFeeRet -= extraFeePaid;
3078-
}
3079-
break; // Done, enough fee included.
3080-
}
3081-
else if (!pick_new_inputs) {
3082-
// This shouldn't happen, we should have had enough excess
3083-
// fee to pay for the new output and still meet nFeeNeeded
3084-
// Or we should have just subtracted fee from recipients and
3085-
// nFeeNeeded should not have changed
3086-
error = _("Transaction fee and change calculation failed");
3087-
return false;
3011+
// Subtract fee from the change output if not subtrating it from recipient outputs
3012+
CAmount fee_needed = nFeeRet;
3013+
if (nSubtractFeeFromAmount == 0) {
3014+
change_position->nValue -= fee_needed;
30883015
}
30893016

3090-
// Try to reduce change to include necessary fee
3091-
if (nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) {
3092-
CAmount additionalFeeNeeded = nFeeNeeded - nFeeRet;
3093-
std::vector<CTxOut>::iterator change_position = txNew.vout.begin()+nChangePosInOut;
3094-
// Only reduce change if remaining amount is still a large enough output.
3095-
if (change_position->nValue >= MIN_FINAL_CHANGE + additionalFeeNeeded) {
3096-
change_position->nValue -= additionalFeeNeeded;
3097-
nFeeRet += additionalFeeNeeded;
3098-
break; // Done, able to increase fee from change
3099-
}
3017+
// We want to drop the change to fees if:
3018+
// 1. The change output would be dust
3019+
// 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)
3020+
CAmount change_amount = change_position->nValue;
3021+
if (IsDust(*change_position, coin_selection_params.m_discard_feerate) || change_amount <= coin_selection_params.m_cost_of_change)
3022+
{
3023+
nChangePosInOut = -1;
3024+
change_amount = 0;
3025+
txNew.vout.erase(change_position);
3026+
3027+
// Because we have dropped this change, the tx size and required fee will be different, so let's recalculate those
3028+
tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), this, coin_control.fAllowWatchOnly);
3029+
nBytes = tx_sizes.first;
3030+
fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
31003031
}
31013032

3102-
// If subtracting fee from recipients, we now know what fee we
3103-
// need to subtract, we have no reason to reselect inputs
3104-
if (nSubtractFeeFromAmount > 0) {
3105-
pick_new_inputs = false;
3033+
// If the fee is covered, there's no need to loop or subtract from recipients
3034+
if (fee_needed <= change_and_fee - change_amount) {
3035+
nFeeRet = change_and_fee - change_amount;
3036+
break;
31063037
}
31073038

3108-
// Include more fee and try again.
3109-
nFeeRet = nFeeNeeded;
3110-
coin_selection_params.use_bnb = false;
3111-
continue;
3039+
// Reduce output values for subtractFeeFromAmount
3040+
if (nSubtractFeeFromAmount != 0) {
3041+
CAmount to_reduce = fee_needed + change_amount - change_and_fee;
3042+
int i = 0;
3043+
bool fFirst = true;
3044+
for (const auto& recipient : vecSend)
3045+
{
3046+
if (i == nChangePosInOut) {
3047+
++i;
3048+
}
3049+
CTxOut& txout = txNew.vout[i];
3050+
3051+
if (recipient.fSubtractFeeFromAmount)
3052+
{
3053+
txout.nValue -= to_reduce / nSubtractFeeFromAmount; // Subtract fee equally from each selected recipient
3054+
3055+
if (fFirst) // first receiver pays the remainder not divisible by output count
3056+
{
3057+
fFirst = false;
3058+
txout.nValue -= to_reduce % nSubtractFeeFromAmount;
3059+
}
3060+
3061+
// Error if this output is reduced to be below dust
3062+
if (IsDust(txout, chain().relayDustFee())) {
3063+
if (txout.nValue < 0) {
3064+
error = _("The transaction amount is too small to pay the fee");
3065+
} else {
3066+
error = _("The transaction amount is too small to send after the fee has been deducted");
3067+
}
3068+
return false;
3069+
}
3070+
}
3071+
++i;
3072+
}
3073+
nFeeRet = fee_needed;
3074+
break; // The fee has been deducted from the recipients, nothing left to do here
3075+
}
31123076
}
31133077

31143078
// Give up if change keypool ran out and change is required
@@ -3170,8 +3134,8 @@ bool CWallet::CreateTransactionInternal(
31703134
reservedest.KeepDestination();
31713135
fee_calc_out = feeCalc;
31723136

3173-
WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Needed:%d Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n",
3174-
nFeeRet, nBytes, nFeeNeeded, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
3137+
WalletLogPrintf("Fee Calculation: Fee:%d Bytes:%u Tgt:%d (requested %d) Reason:\"%s\" Decay %.5f: Estimation: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n",
3138+
nFeeRet, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
31753139
feeCalc.est.pass.start, feeCalc.est.pass.end,
31763140
(feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) > 0.0 ? 100 * feeCalc.est.pass.withinTarget / (feeCalc.est.pass.totalConfirmed + feeCalc.est.pass.inMempool + feeCalc.est.pass.leftMempool) : 0.0,
31773141
feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool,

0 commit comments

Comments
 (0)