Skip to content

Commit b91c5b9

Browse files
committed
merge bitcoin#22949: Round up fee calculation to avoid a lower than expected feerate
1 parent 05fb900 commit b91c5b9

File tree

6 files changed

+57
-10
lines changed

6 files changed

+57
-10
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
#include <policy/feerate.h>
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
@@ -54,6 +54,7 @@ class CFeeRate
5454

5555
/**
5656
* Return the fee in satoshis for the given size in vbytes.
57+
* If the calculated fee would have fractional satoshis, then the returned fee will always be rounded up to the nearest satoshi.
5758
*/
5859
CAmount GetFee(uint32_t num_bytes) const;
5960

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

test/functional/rpc_fundrawtransaction.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def run_test(self):
132132
self.test_include_unsafe()
133133
self.test_external_inputs()
134134
self.test_22670()
135+
self.test_feerate_rounding()
135136

136137
def test_change_position(self):
137138
"""Ensure setting changePosition in fundraw with an exact match is handled properly."""
@@ -1080,5 +1081,34 @@ def do_fund_send(target):
10801081
do_fund_send(lower_bound)
10811082
do_fund_send(upper_bound)
10821083

1084+
self.restart_node(0, [])
1085+
self.connect_nodes(0, 1)
1086+
self.connect_nodes(0, 2)
1087+
self.connect_nodes(0, 3)
1088+
1089+
def test_feerate_rounding(self):
1090+
self.log.info("Test that rounding of GetFee does not result in an assertion")
1091+
1092+
self.nodes[1].createwallet("roundtest")
1093+
w = self.nodes[1].get_wallet_rpc("roundtest")
1094+
1095+
addr = w.getnewaddress()
1096+
self.nodes[0].sendtoaddress(addr, 1)
1097+
self.generate(self.nodes[0], 1)
1098+
self.sync_all()
1099+
1100+
# 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.
1101+
# At a feerate of 1.85 sat/vb, the input will need a fee of 125.8 sats and the rest 77.7 sats
1102+
# The entire tx fee should be 203.5 sats.
1103+
# Coin selection rounds the fee individually instead of at the end (due to how CFeeRate::GetFee works).
1104+
# If rounding down (which is the incorrect behavior), then the calculated fee will be 125 + 77 = 202.
1105+
# If rounding up, then the calculated fee will be 126 + 78 = 204.
1106+
# In the former case, the calculated needed fee is higher than the actual fee being paid, so an assertion is reached
1107+
# To test this does not happen, we subtract 202 sats from the input value. If working correctly, this should
1108+
# fail with insufficient funds rather than bitcoind asserting.
1109+
rawtx = w.createrawtransaction(inputs=[], outputs=[{self.nodes[0].getnewaddress(): 1 - 0.00000202}])
1110+
assert_raises_rpc_error(-4, "Insufficient funds", w.fundrawtransaction, rawtx, {"fee_rate": 1.85})
1111+
1112+
10831113
if __name__ == '__main__':
10841114
RawTransactionsTest().main()

test/functional/test_framework/util.py

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

4444
def assert_fee_amount(fee, tx_size, feerate_DASH_kvB):
4545
"""Assert the fee is in range."""
46-
feerate_DASH_vB = feerate_DASH_kvB / 1000
47-
target_fee = satoshi_round(tx_size * feerate_DASH_vB)
46+
target_fee = get_fee(tx_size, feerate_DASH_kvB)
4847
if fee < target_fee:
4948
raise AssertionError("Fee of %s DASH too low! (Should be %s DASH)" % (str(fee), str(target_fee)))
5049
# allow the wallet's estimation to be at most 2 bytes off
51-
if fee > (tx_size + 2) * feerate_DASH_vB:
50+
high_fee = get_fee(tx_size + 2, feerate_DASH_kvB)
51+
if fee > high_fee:
5252
raise AssertionError("Fee of %s DASH too high! (Should be %s DASH)" % (str(fee), str(target_fee)))
5353

5454

@@ -242,6 +242,18 @@ def random_bitflip(data):
242242
return bytes(data)
243243

244244

245+
def ceildiv(a, b):
246+
"""Divide 2 ints and round up to next int rather than round down"""
247+
return -(-a // b)
248+
249+
250+
def get_fee(tx_size, feerate_dash_kvb):
251+
"""Calculate the fee in DASH given a feerate is DASH/kvB. Reflects CFeeRate::GetFee"""
252+
feerate_sat_kvb = int(feerate_dash_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
253+
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
254+
return satoshi_round(target_fee_sat / Decimal(1e8)) # Truncate DASH result to nearest sat
255+
256+
245257
def satoshi_round(amount):
246258
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
247259

test/functional/wallet_keypool_hd.py

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

217217
# create a transaction without change at the maximum fee rate, such that the output is still spendable:
218-
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00008824})
218+
res = w2.walletcreatefundedpsbt(inputs=[], outputs=[{destination: 0.00010000}], options={"subtractFeeFromOutputs": [0], "feeRate": 0.00008822})
219219
assert_equal("psbt" in res, True)
220220
assert_equal(res["fee"], Decimal("0.00001694"))
221221

0 commit comments

Comments
 (0)