Skip to content

Commit 24abd83

Browse files
committed
Merge bitcoin/bitcoin#22949: fee: Round up fee calculation to avoid a lower than expected feerate
80dc829 tests: Calculate fees more similarly to CFeeRate::GetFee (Andrew Chow) ce2cc44 tests: Test for assertion when feerate is rounded down (Andrew Chow) 0fbaef9 fees: Always round up fee calculated from a feerate (Andrew Chow) Pull request description: When calculating the fee for a feerate, it is possible that the final calculation will have fractional satoshis. Currently those are ignored via truncation which results in the absolute fee being rounded down. Rounding down is problematic because it results in a feerate that is slightly lower than the feerate represented by the `CFeeRate` object. A slightly lower feerate particularly causes issues for coin selection as it can trigger an assertion error. To avoid potentially underpaying the feerate (and the assertion), always round up the calculated fee. A test is added for the assertion, along with a comment explaining what happens. It is unlikely that a user can trigger this as it requires a very specific set of rounding errors to occur as well as the transaction not needing any change and being right on the lower bound of the exact match window. However I was able to trigger the assertion while running coin selection simulations, albeit after thousands of transactions and with some weird feerates. ACKs for top commit: ryanofsky: Code review ACK 80dc829 promag: Tested ACK 80dc829. lsilva01: tACK 80dc829 meshcollider: utACK 80dc829 Tree-SHA512: fe26684c60f236cab48ea6a4600c141ce766dbe59504ec77595dcbd7fd0b34559acc617007f4f499c9155d8fda0a336954413410ba862b19c765c0cfac79d642
2 parents 23ae793 + 80dc829 commit 24abd83

File tree

7 files changed

+57
-12
lines changed

7 files changed

+57
-12
lines changed

src/policy/feerate.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#include <tinyformat.h>
99

10+
#include <cmath>
11+
1012
CFeeRate::CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes)
1113
{
1214
const int64_t nSize{num_bytes};
@@ -22,7 +24,9 @@ CAmount CFeeRate::GetFee(uint32_t num_bytes) const
2224
{
2325
const int64_t nSize{num_bytes};
2426

25-
CAmount nFee = nSatoshisPerK * nSize / 1000;
27+
// Be explicit that we're converting from a double to int64_t (CAmount) here.
28+
// We've previously had issues with the silent double->int64_t conversion.
29+
CAmount nFee{static_cast<CAmount>(std::ceil(nSatoshisPerK * nSize / 1000.0))};
2630

2731
if (nFee == 0 && nSize != 0) {
2832
if (nSatoshisPerK > 0) nFee = CAmount(1);

src/policy/feerate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class CFeeRate
4848
CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes);
4949
/**
5050
* Return the fee in satoshis for the given size in bytes.
51+
* If the calculated fee would have fractional satoshis, then the returned fee will always be rounded up to the nearest satoshi.
5152
*/
5253
CAmount GetFee(uint32_t num_bytes) const;
5354
/**

src/test/amount_tests.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ BOOST_AUTO_TEST_CASE(GetFeeTest)
4848
BOOST_CHECK_EQUAL(feeRate.GetFee(9e3), CAmount(-9e3));
4949

5050
feeRate = CFeeRate(123);
51-
// Truncates the result, if not integer
51+
// Rounds up the result, if not integer
5252
BOOST_CHECK_EQUAL(feeRate.GetFee(0), CAmount(0));
5353
BOOST_CHECK_EQUAL(feeRate.GetFee(8), CAmount(1)); // Special case: returns 1 instead of 0
54-
BOOST_CHECK_EQUAL(feeRate.GetFee(9), CAmount(1));
55-
BOOST_CHECK_EQUAL(feeRate.GetFee(121), CAmount(14));
56-
BOOST_CHECK_EQUAL(feeRate.GetFee(122), CAmount(15));
57-
BOOST_CHECK_EQUAL(feeRate.GetFee(999), CAmount(122));
54+
BOOST_CHECK_EQUAL(feeRate.GetFee(9), CAmount(2));
55+
BOOST_CHECK_EQUAL(feeRate.GetFee(121), CAmount(15));
56+
BOOST_CHECK_EQUAL(feeRate.GetFee(122), CAmount(16));
57+
BOOST_CHECK_EQUAL(feeRate.GetFee(999), CAmount(123));
5858
BOOST_CHECK_EQUAL(feeRate.GetFee(1e3), CAmount(123));
5959
BOOST_CHECK_EQUAL(feeRate.GetFee(9e3), CAmount(1107));
6060

src/test/transaction_tests.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,10 +810,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
810810
// nDustThreshold = 182 * 3702 / 1000
811811
dustRelayFee = CFeeRate(3702);
812812
// dust:
813-
t.vout[0].nValue = 673 - 1;
813+
t.vout[0].nValue = 674 - 1;
814814
CheckIsNotStandard(t, "dust");
815815
// not dust:
816-
t.vout[0].nValue = 673;
816+
t.vout[0].nValue = 674;
817817
CheckIsStandard(t);
818818
dustRelayFee = CFeeRate(DUST_RELAY_TX_FEE);
819819

test/functional/rpc_fundrawtransaction.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def run_test(self):
136136
self.test_include_unsafe()
137137
self.test_external_inputs()
138138
self.test_22670()
139+
self.test_feerate_rounding()
139140

140141
def test_change_position(self):
141142
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -1129,6 +1130,33 @@ def do_fund_send(target):
11291130
do_fund_send(upper_bound)
11301131

11311132
self.restart_node(0)
1133+
self.connect_nodes(0, 1)
1134+
self.connect_nodes(0, 2)
1135+
self.connect_nodes(0, 3)
1136+
1137+
def test_feerate_rounding(self):
1138+
self.log.info("Test that rounding of GetFee does not result in an assertion")
1139+
1140+
self.nodes[1].createwallet("roundtest")
1141+
w = self.nodes[1].get_wallet_rpc("roundtest")
1142+
1143+
addr = w.getnewaddress(address_type="bech32")
1144+
self.nodes[0].sendtoaddress(addr, 1)
1145+
self.generate(self.nodes[0], 1)
1146+
self.sync_all()
1147+
1148+
# A P2WPKH input costs 68 vbytes; With a single P2WPKH output, the rest of the tx is 42 vbytes for a total of 110 vbytes.
1149+
# At a feerate of 1.85 sat/vb, the input will need a fee of 125.8 sats and the rest 77.7 sats
1150+
# The entire tx fee should be 203.5 sats.
1151+
# Coin selection rounds the fee individually instead of at the end (due to how CFeeRate::GetFee works).
1152+
# If rounding down (which is the incorrect behavior), then the calculated fee will be 125 + 77 = 202.
1153+
# If rounding up, then the calculated fee will be 126 + 78 = 204.
1154+
# In the former case, the calculated needed fee is higher than the actual fee being paid, so an assertion is reached
1155+
# To test this does not happen, we subtract 202 sats from the input value. If working correctly, this should
1156+
# fail with insufficient funds rather than bitcoind asserting.
1157+
rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(address_type="bech32"): 1 - 0.00000202}])
1158+
assert_raises_rpc_error(-4, "Insufficient funds", w.fundrawtransaction, rawtx, {"fee_rate": 1.85})
1159+
11321160

11331161
if __name__ == '__main__':
11341162
RawTransactionsTest().main()

test/functional/test_framework/util.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ def assert_approx(v, vexp, vspan=0.00001):
3636

3737
def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
3838
"""Assert the fee is in range."""
39-
feerate_BTC_vB = feerate_BTC_kvB / 1000
40-
target_fee = satoshi_round(tx_size * feerate_BTC_vB)
39+
target_fee = get_fee(tx_size, feerate_BTC_kvB)
4140
if fee < target_fee:
4241
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
4342
# allow the wallet's estimation to be at most 2 bytes off
44-
if fee > (tx_size + 2) * feerate_BTC_vB:
43+
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
44+
if fee > high_fee:
4545
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
4646

4747

@@ -218,6 +218,18 @@ def str_to_b64str(string):
218218
return b64encode(string.encode('utf-8')).decode('ascii')
219219

220220

221+
def ceildiv(a, b):
222+
"""Divide 2 ints and round up to next int rather than round down"""
223+
return -(-a // b)
224+
225+
226+
def get_fee(tx_size, feerate_btc_kvb):
227+
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
228+
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
229+
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
230+
return satoshi_round(target_fee_sat / Decimal(1e8)) # Truncate BTC result to nearest sat
231+
232+
221233
def satoshi_round(amount):
222234
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
223235

test/functional/wallet_keypool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def run_test(self):
193193
assert_equal("psbt" in res, True)
194194

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

0 commit comments

Comments
 (0)