Skip to content

Commit 8f30211

Browse files
committed
Merge bitcoin/bitcoin#26643: wallet: Move fee underpayment check to after all fee has been set
798430d wallet: Sanity check fee paid cannot be negative (Andrew Chow) c1a84f1 wallet: Move fee underpayment check to after fee setting (Andrew Chow) e5daf97 wallet: Rename nFeeRet in CreateTransactionInternal to current_fee (Andrew Chow) Pull request description: Currently the fee underpayment check occurs right after we calculate what the transaction's fee should be. However the fee paid by the transaction at that time does not always match. Notably, when doing SFFO, the fee paid at that time will almost always be less than the fee required, which then required having a bypass of the underpayment check that results in SFFO payments going through when they should not. This PR moves the underpayment check to after fees have been finalized so that we always check whether the fee is being underpaid. This removes the exception for SFFO and unifies this behavior for both SFFO and non-SFFO txs. ACKs for top commit: S3RK: Code review ACK 798430d furszy: Code review ACK 798430d glozow: utACK 798430d, code looks correct to me Tree-SHA512: 720e8a3dbdc9937b12ee7881eb2ad58332c9584520da87ef3080e6f9d6220ce8d3bd8b9317b4877e56a229113437340852976db8f64df0d5cc50723fa04b02f0
2 parents a4baf3f + 798430d commit 8f30211

File tree

2 files changed

+34
-14
lines changed

2 files changed

+34
-14
lines changed

src/primitives/transaction.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <ios>
1818
#include <limits>
1919
#include <memory>
20+
#include <numeric>
2021
#include <string>
2122
#include <tuple>
2223
#include <utility>
@@ -280,6 +281,12 @@ inline void SerializeTransaction(const TxType& tx, Stream& s) {
280281
s << tx.nLockTime;
281282
}
282283

284+
template<typename TxType>
285+
inline CAmount CalculateOutputValue(const TxType& tx)
286+
{
287+
return std::accumulate(tx.vout.cbegin(), tx.vout.cend(), CAmount{0}, [](CAmount sum, const auto& txout) { return sum + txout.nValue; });
288+
}
289+
283290

284291
/** The basic transaction that is broadcasted on the network and contained in
285292
* blocks. A transaction can contain multiple inputs and outputs.

src/wallet/spend.cpp

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <interfaces/chain.h>
99
#include <numeric>
1010
#include <policy/policy.h>
11+
#include <primitives/transaction.h>
1112
#include <script/signingprovider.h>
1213
#include <util/check.h>
1314
#include <util/fees.h>
@@ -778,7 +779,6 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
778779
AssertLockHeld(wallet.cs_wallet);
779780

780781
// out variables, to be packed into returned result structure
781-
CAmount nFeeRet;
782782
int nChangePosInOut = change_pos;
783783

784784
FastRandomContext rng_fast;
@@ -960,24 +960,28 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
960960
return util::Error{_("Missing solving data for estimating transaction size")};
961961
}
962962
CAmount fee_needed = coin_selection_params.m_effective_feerate.GetFee(nBytes);
963-
nFeeRet = result->GetSelectedValue() - recipients_sum - change_amount;
963+
const CAmount output_value = CalculateOutputValue(txNew);
964+
Assume(recipients_sum + change_amount == output_value);
965+
CAmount current_fee = result->GetSelectedValue() - output_value;
964966

965-
// The only time that fee_needed should be less than the amount available for fees is when
966-
// we are subtracting the fee from the outputs. If this occurs at any other time, it is a bug.
967-
if (!coin_selection_params.m_subtract_fee_outputs && fee_needed > nFeeRet) {
968-
return util::Error{Untranslated(STR_INTERNAL_BUG("Fee needed > fee paid"))};
967+
// Sanity check that the fee cannot be negative as that means we have more output value than input value
968+
if (current_fee < 0) {
969+
return util::Error{Untranslated(STR_INTERNAL_BUG("Fee paid < 0"))};
969970
}
970971

971972
// If there is a change output and we overpay the fees then increase the change to match the fee needed
972-
if (nChangePosInOut != -1 && fee_needed < nFeeRet) {
973+
if (nChangePosInOut != -1 && fee_needed < current_fee) {
973974
auto& change = txNew.vout.at(nChangePosInOut);
974-
change.nValue += nFeeRet - fee_needed;
975-
nFeeRet = fee_needed;
975+
change.nValue += current_fee - fee_needed;
976+
current_fee = result->GetSelectedValue() - CalculateOutputValue(txNew);
977+
if (fee_needed != current_fee) {
978+
return util::Error{Untranslated(STR_INTERNAL_BUG("Change adjustment: Fee needed != fee paid"))};
979+
}
976980
}
977981

978982
// Reduce output values for subtractFeeFromAmount
979983
if (coin_selection_params.m_subtract_fee_outputs) {
980-
CAmount to_reduce = fee_needed - nFeeRet;
984+
CAmount to_reduce = fee_needed - current_fee;
981985
int i = 0;
982986
bool fFirst = true;
983987
for (const auto& recipient : vecSend)
@@ -1008,7 +1012,16 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
10081012
}
10091013
++i;
10101014
}
1011-
nFeeRet = fee_needed;
1015+
current_fee = result->GetSelectedValue() - CalculateOutputValue(txNew);
1016+
if (fee_needed != current_fee) {
1017+
return util::Error{Untranslated(STR_INTERNAL_BUG("SFFO: Fee needed != fee paid"))};
1018+
}
1019+
}
1020+
1021+
// fee_needed should now always be less than or equal to the current fees that we pay.
1022+
// If it is not, it is a bug.
1023+
if (fee_needed > current_fee) {
1024+
return util::Error{Untranslated(STR_INTERNAL_BUG("Fee needed > fee paid"))};
10121025
}
10131026

10141027
// Give up if change keypool ran out and change is required
@@ -1030,7 +1043,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
10301043
return util::Error{_("Transaction too large")};
10311044
}
10321045

1033-
if (nFeeRet > wallet.m_default_max_tx_fee) {
1046+
if (current_fee > wallet.m_default_max_tx_fee) {
10341047
return util::Error{TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED)};
10351048
}
10361049

@@ -1046,14 +1059,14 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
10461059
reservedest.KeepDestination();
10471060

10481061
wallet.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",
1049-
nFeeRet, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
1062+
current_fee, nBytes, feeCalc.returnedTarget, feeCalc.desiredTarget, StringForFeeReason(feeCalc.reason), feeCalc.est.decay,
10501063
feeCalc.est.pass.start, feeCalc.est.pass.end,
10511064
(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,
10521065
feeCalc.est.pass.withinTarget, feeCalc.est.pass.totalConfirmed, feeCalc.est.pass.inMempool, feeCalc.est.pass.leftMempool,
10531066
feeCalc.est.fail.start, feeCalc.est.fail.end,
10541067
(feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) > 0.0 ? 100 * feeCalc.est.fail.withinTarget / (feeCalc.est.fail.totalConfirmed + feeCalc.est.fail.inMempool + feeCalc.est.fail.leftMempool) : 0.0,
10551068
feeCalc.est.fail.withinTarget, feeCalc.est.fail.totalConfirmed, feeCalc.est.fail.inMempool, feeCalc.est.fail.leftMempool);
1056-
return CreatedTransactionResult(tx, nFeeRet, nChangePosInOut, feeCalc);
1069+
return CreatedTransactionResult(tx, current_fee, nChangePosInOut, feeCalc);
10571070
}
10581071

10591072
util::Result<CreatedTransactionResult> CreateTransaction(

0 commit comments

Comments
 (0)