fees: Always round up fee calculated from a feerate

When calculating the fee for a given tx size from a fee rate, we should
always round up to the next satoshi. Otherwise, if we round down (via
truncation), the calculated fee may result in a fee with a feerate
slightly less than targeted.

This is particularly important for coin selection as a slightly lower
feerate than expected can result in a variety of issues.
This commit is contained in:
Andrew Chow 2021-09-10 20:24:44 -04:00
parent 927586990e
commit 0fbaef9676
5 changed files with 14 additions and 9 deletions

View File

@ -7,6 +7,8 @@
#include <tinyformat.h>
#include <cmath>
CFeeRate::CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes)
{
const int64_t nSize{num_bytes};
@ -22,7 +24,9 @@ CAmount CFeeRate::GetFee(uint32_t num_bytes) const
{
const int64_t nSize{num_bytes};
CAmount nFee = nSatoshisPerK * nSize / 1000;
// Be explicit that we're converting from a double to int64_t (CAmount) here.
// We've previously had issues with the silent double->int64_t conversion.
CAmount nFee{static_cast<CAmount>(std::ceil(nSatoshisPerK * nSize / 1000.0))};
if (nFee == 0 && nSize != 0) {
if (nSatoshisPerK > 0) nFee = CAmount(1);

View File

@ -48,6 +48,7 @@ public:
CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes);
/**
* Return the fee in satoshis for the given size in bytes.
* If the calculated fee would have fractional satoshis, then the returned fee will always be rounded up to the nearest satoshi.
*/
CAmount GetFee(uint32_t num_bytes) const;
/**

View File

@ -48,13 +48,13 @@ BOOST_AUTO_TEST_CASE(GetFeeTest)
BOOST_CHECK_EQUAL(feeRate.GetFee(9e3), CAmount(-9e3));
feeRate = CFeeRate(123);
// Truncates the result, if not integer
// Rounds up the result, if not integer
BOOST_CHECK_EQUAL(feeRate.GetFee(0), CAmount(0));
BOOST_CHECK_EQUAL(feeRate.GetFee(8), CAmount(1)); // Special case: returns 1 instead of 0
BOOST_CHECK_EQUAL(feeRate.GetFee(9), CAmount(1));
BOOST_CHECK_EQUAL(feeRate.GetFee(121), CAmount(14));
BOOST_CHECK_EQUAL(feeRate.GetFee(122), CAmount(15));
BOOST_CHECK_EQUAL(feeRate.GetFee(999), CAmount(122));
BOOST_CHECK_EQUAL(feeRate.GetFee(9), CAmount(2));
BOOST_CHECK_EQUAL(feeRate.GetFee(121), CAmount(15));
BOOST_CHECK_EQUAL(feeRate.GetFee(122), CAmount(16));
BOOST_CHECK_EQUAL(feeRate.GetFee(999), CAmount(123));
BOOST_CHECK_EQUAL(feeRate.GetFee(1e3), CAmount(123));
BOOST_CHECK_EQUAL(feeRate.GetFee(9e3), CAmount(1107));

View File

@ -810,10 +810,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
// nDustThreshold = 182 * 3702 / 1000
dustRelayFee = CFeeRate(3702);
// dust:
t.vout[0].nValue = 673 - 1;
t.vout[0].nValue = 674 - 1;
CheckIsNotStandard(t, "dust");
// not dust:
t.vout[0].nValue = 673;
t.vout[0].nValue = 674;
CheckIsStandard(t);
dustRelayFee = CFeeRate(DUST_RELAY_TX_FEE);

View File

@ -179,7 +179,7 @@ class KeyPoolTest(BitcoinTestFramework):
assert_equal("psbt" in res, True)
# create a transaction without change at the maximum fee rate, such that the output is still spendable:
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.0008824})
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.0008823})
assert_equal("psbt" in res, True)
assert_equal(res["fee"], Decimal("0.00009706"))