Skip to content

Commit adf7843

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#21786: wallet: ensure sat/vB feerates are in range (mantissa of 3)
847288d test: fee rate values that cannot be represented as sat/vB (Jon Atack) 06a90fa rpc: for sat/vB fee rates, limit ParseFixedPoint decimals to 3 (Jon Atack) 0742c78 rpc: enable passing decimals to AmountFromValue, add doxygen (Jon Atack) 8ce3ef5 test: ParseFixedPoint with 3 decimals for sat/vB fee rates (Jon Atack) b503327 test: type error and out of range fee rates where missing (Jon Atack) c5fd434 test: explicit fee rates with invalid amounts (Jon Atack) ea6f76b test: improve zero-value explicit fee rate coverage (Jon Atack) Pull request description: - Improve/close gaps in existing test coverage before making the change - Enable passing `decimals` to `ParseFixedPoint()` when calling `AmountFromValue()` - Limit explicit fee rates in sat/vB passed in by users to 3 decimals, and raise otherwise - Add regression test coverage Closes #20534. ACKs for top commit: MarcoFalke: review ACK 847288d 🔷 Tree-SHA512: c539d07ae9b21c0d6c8ea460beb9c8dad5559445518aace560abc3c05c588907bae189b6fd7602b3b397de4a42356136c3ec6f960d3dcf2d5d16377aef4ab5a2
2 parents 2a22d90 + 847288d commit adf7843

File tree

9 files changed

+128
-40
lines changed

9 files changed

+128
-40
lines changed

src/rpc/util.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ void RPCTypeCheckObj(const UniValue& o,
7474
}
7575
}
7676

77-
CAmount AmountFromValue(const UniValue& value)
77+
CAmount AmountFromValue(const UniValue& value, int decimals)
7878
{
7979
if (!value.isNum() && !value.isStr())
8080
throw JSONRPCError(RPC_TYPE_ERROR, "Amount is not a number or string");
8181
CAmount amount;
82-
if (!ParseFixedPoint(value.getValStr(), 8, &amount))
82+
if (!ParseFixedPoint(value.getValStr(), decimals, &amount))
8383
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
8484
if (!MoneyRange(amount))
8585
throw JSONRPCError(RPC_TYPE_ERROR, "Amount out of range");

src/rpc/util.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ extern uint256 ParseHashO(const UniValue& o, std::string strKey);
7777
extern std::vector<unsigned char> ParseHexV(const UniValue& v, std::string strName);
7878
extern std::vector<unsigned char> ParseHexO(const UniValue& o, std::string strKey);
7979

80-
extern CAmount AmountFromValue(const UniValue& value);
80+
/**
81+
* Validate and return a CAmount from a UniValue number or string.
82+
*
83+
* @param[in] value UniValue number or string to parse.
84+
* @param[in] decimals Number of significant digits (default: 8).
85+
* @returns a CAmount if the various checks pass.
86+
*/
87+
extern CAmount AmountFromValue(const UniValue& value, int decimals = 8);
8188

8289
using RPCArgList = std::vector<std::pair<std::string, UniValue>>;
8390
extern std::string HelpExampleCli(const std::string& methodname, const std::string& args);

src/test/util_tests.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,15 @@ BOOST_AUTO_TEST_CASE(test_ParseFixedPoint)
17591759
BOOST_CHECK(!ParseFixedPoint("1.1e", 8, &amount));
17601760
BOOST_CHECK(!ParseFixedPoint("1.1e-", 8, &amount));
17611761
BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount));
1762+
1763+
// Test with 3 decimal places for fee rates in sat/vB.
1764+
BOOST_CHECK(ParseFixedPoint("0.001", 3, &amount));
1765+
BOOST_CHECK_EQUAL(amount, CAmount{1});
1766+
BOOST_CHECK(!ParseFixedPoint("0.0009", 3, &amount));
1767+
BOOST_CHECK(!ParseFixedPoint("31.00100001", 3, &amount));
1768+
BOOST_CHECK(!ParseFixedPoint("31.0011", 3, &amount));
1769+
BOOST_CHECK(!ParseFixedPoint("31.99999999", 3, &amount));
1770+
BOOST_CHECK(!ParseFixedPoint("31.999999999999999999999", 3, &amount));
17621771
}
17631772

17641773
static void TestOtherThread(fs::path dirname, std::string lockname, bool *result)

src/wallet/rpcwallet.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ static void SetFeeEstimateMode(const CWallet& wallet, CCoinControl& cc, const Un
216216
if (!estimate_mode.isNull() && estimate_mode.get_str() != "unset") {
217217
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and fee_rate");
218218
}
219-
cc.m_feerate = CFeeRate(AmountFromValue(fee_rate), COIN);
219+
// Fee rates in sat/vB cannot represent more than 3 significant digits.
220+
cc.m_feerate = CFeeRate{AmountFromValue(fee_rate, /* decimals */ 3)};
220221
if (override_min_fee) cc.fOverrideFeeRate = true;
221222
// Default RBF to true for explicit fee_rate, if unset.
222223
if (!cc.m_signal_bip125_rbf) cc.m_signal_bip125_rbf = true;

test/functional/rpc_fundrawtransaction.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""Test the fundrawtransaction RPC."""
66

77
from decimal import Decimal
8+
from itertools import product
9+
810
from test_framework.descriptors import descsum_create
911
from test_framework.test_framework import BitcoinTestFramework
1012
from test_framework.util import (
@@ -723,17 +725,16 @@ def test_option_feerate(self):
723725
result2 = node.fundrawtransaction(rawtx, {"feeRate": 2 * self.min_relay_tx_fee})
724726
result3 = node.fundrawtransaction(rawtx, {"fee_rate": 10 * btc_kvb_to_sat_vb * self.min_relay_tx_fee})
725727
result4 = node.fundrawtransaction(rawtx, {"feeRate": str(10 * self.min_relay_tx_fee)})
726-
# Test that funding non-standard "zero-fee" transactions is valid.
727-
result5 = self.nodes[3].fundrawtransaction(rawtx, {"fee_rate": 0})
728-
result6 = self.nodes[3].fundrawtransaction(rawtx, {"feeRate": 0})
729728

730729
result_fee_rate = result['fee'] * 1000 / count_bytes(result['hex'])
731730
assert_fee_amount(result1['fee'], count_bytes(result1['hex']), 2 * result_fee_rate)
732731
assert_fee_amount(result2['fee'], count_bytes(result2['hex']), 2 * result_fee_rate)
733732
assert_fee_amount(result3['fee'], count_bytes(result3['hex']), 10 * result_fee_rate)
734733
assert_fee_amount(result4['fee'], count_bytes(result4['hex']), 10 * result_fee_rate)
735-
assert_fee_amount(result5['fee'], count_bytes(result5['hex']), 0)
736-
assert_fee_amount(result6['fee'], count_bytes(result6['hex']), 0)
734+
735+
# Test that funding non-standard "zero-fee" transactions is valid.
736+
for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]):
737+
assert_equal(self.nodes[3].fundrawtransaction(rawtx, {param: zero_value})["fee"], 0)
737738

738739
# With no arguments passed, expect fee of 141 satoshis.
739740
assert_approx(node.fundrawtransaction(rawtx)["fee"], vexp=0.00000141, vspan=0.00000001)
@@ -767,11 +768,16 @@ def test_option_feerate(self):
767768
node.fundrawtransaction, rawtx, {param: -1, "add_inputs": True})
768769
assert_raises_rpc_error(-3, "Amount is not a number or string",
769770
node.fundrawtransaction, rawtx, {param: {"foo": "bar"}, "add_inputs": True})
771+
# Test fee rate values that don't pass fixed-point parsing checks.
772+
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
773+
assert_raises_rpc_error(-3, "Invalid amount", node.fundrawtransaction, rawtx, {param: invalid_value, "add_inputs": True})
774+
# Test fee_rate values that cannot be represented in sat/vB.
775+
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
770776
assert_raises_rpc_error(-3, "Invalid amount",
771-
node.fundrawtransaction, rawtx, {param: "", "add_inputs": True})
777+
node.fundrawtransaction, rawtx, {"fee_rate": invalid_value, "add_inputs": True})
772778

773779
self.log.info("Test min fee rate checks are bypassed with fundrawtxn, e.g. a fee_rate under 1 sat/vB is allowed")
774-
node.fundrawtransaction(rawtx, {"fee_rate": 0.99999999, "add_inputs": True})
780+
node.fundrawtransaction(rawtx, {"fee_rate": 0.999, "add_inputs": True})
775781
node.fundrawtransaction(rawtx, {"feeRate": 0.00000999, "add_inputs": True})
776782

777783
self.log.info("- raises RPC error if both feeRate and fee_rate are passed")

test/functional/rpc_psbt.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"""
77

88
from decimal import Decimal
9+
from itertools import product
10+
911
from test_framework.test_framework import BitcoinTestFramework
1012
from test_framework.util import (
1113
assert_approx,
@@ -193,14 +195,14 @@ def run_test(self):
193195
assert_approx(res2["fee"], 0.055, 0.005)
194196

195197
self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed, e.g. a fee_rate under 1 sat/vB is allowed")
196-
res3 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": "0.99999999", "add_inputs": True})
198+
res3 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": "0.999", "add_inputs": True})
197199
assert_approx(res3["fee"], 0.00000381, 0.0000001)
198200
res4 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"feeRate": 0.00000999, "add_inputs": True})
199201
assert_approx(res4["fee"], 0.00000381, 0.0000001)
200202

201203
self.log.info("Test min fee rate checks with walletcreatefundedpsbt are bypassed and that funding non-standard 'zero-fee' transactions is valid")
202-
for param in ["fee_rate", "feeRate"]:
203-
assert_equal(self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {param: 0, "add_inputs": True})["fee"], 0)
204+
for param, zero_value in product(["fee_rate", "feeRate"], [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]):
205+
assert_equal(0, self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {param: zero_value, "add_inputs": True})["fee"])
204206

205207
self.log.info("Test invalid fee rate settings")
206208
for param, value in {("fee_rate", 100000), ("feeRate", 1)}:
@@ -210,8 +212,14 @@ def run_test(self):
210212
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: -1, "add_inputs": True})
211213
assert_raises_rpc_error(-3, "Amount is not a number or string",
212214
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: {"foo": "bar"}, "add_inputs": True})
215+
# Test fee rate values that don't pass fixed-point parsing checks.
216+
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
217+
assert_raises_rpc_error(-3, "Invalid amount",
218+
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: invalid_value, "add_inputs": True})
219+
# Test fee_rate values that cannot be represented in sat/vB.
220+
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
213221
assert_raises_rpc_error(-3, "Invalid amount",
214-
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {param: "", "add_inputs": True})
222+
self.nodes[1].walletcreatefundedpsbt, inputs, outputs, 0, {"fee_rate": invalid_value, "add_inputs": True})
215223

216224
self.log.info("- raises RPC error if both feeRate and fee_rate are passed")
217225
assert_raises_rpc_error(-8, "Cannot specify both fee_rate (sat/vB) and feeRate (BTC/kvB)",

test/functional/wallet_basic.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from test_framework.wallet_util import test_address
1717

18+
NOT_A_NUMBER_OR_STRING = "Amount is not a number or string"
1819
OUT_OF_RANGE = "Amount out of range"
1920

2021

@@ -264,12 +265,25 @@ def run_test(self):
264265
# Test setting explicit fee rate just below the minimum.
265266
self.log.info("Test sendmany raises 'fee rate too low' if fee_rate of 0.99999999 is passed")
266267
assert_raises_rpc_error(-6, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)",
267-
self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.99999999)
268-
269-
self.log.info("Test sendmany raises if fee_rate of 0 or -1 is passed")
270-
assert_raises_rpc_error(-6, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)",
271-
self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0)
268+
self.nodes[2].sendmany, amounts={address: 10}, fee_rate=0.999)
269+
270+
self.log.info("Test sendmany raises if an invalid fee_rate is passed")
271+
# Test fee_rate with zero values.
272+
msg = "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)"
273+
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
274+
assert_raises_rpc_error(-6, msg, self.nodes[2].sendmany, amounts={address: 1}, fee_rate=zero_value)
275+
msg = "Invalid amount"
276+
# Test fee_rate values that don't pass fixed-point parsing checks.
277+
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
278+
assert_raises_rpc_error(-3, msg, self.nodes[2].sendmany, amounts={address: 1.0}, fee_rate=invalid_value)
279+
# Test fee_rate values that cannot be represented in sat/vB.
280+
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
281+
assert_raises_rpc_error(-3, msg, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=invalid_value)
282+
# Test fee_rate out of range (negative number).
272283
assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=-1)
284+
# Test type error.
285+
for invalid_value in [True, {"foo": "bar"}]:
286+
assert_raises_rpc_error(-3, NOT_A_NUMBER_OR_STRING, self.nodes[2].sendmany, amounts={address: 10}, fee_rate=invalid_value)
273287

274288
self.log.info("Test sendmany raises if an invalid conf_target or estimate_mode is passed")
275289
for target, mode in product([-1, 0, 1009], ["economical", "conservative"]):
@@ -447,12 +461,25 @@ def run_test(self):
447461
# Test setting explicit fee rate just below the minimum.
448462
self.log.info("Test sendtoaddress raises 'fee rate too low' if fee_rate of 0.99999999 is passed")
449463
assert_raises_rpc_error(-6, "Fee rate (0.999 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)",
450-
self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.99999999)
451-
452-
self.log.info("Test sendtoaddress raises if fee_rate of 0 or -1 is passed")
453-
assert_raises_rpc_error(-6, "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)",
454-
self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=0)
464+
self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=0.999)
465+
466+
self.log.info("Test sendtoaddress raises if an invalid fee_rate is passed")
467+
# Test fee_rate with zero values.
468+
msg = "Fee rate (0.000 sat/vB) is lower than the minimum fee rate setting (1.000 sat/vB)"
469+
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
470+
assert_raises_rpc_error(-6, msg, self.nodes[2].sendtoaddress, address=address, amount=1, fee_rate=zero_value)
471+
msg = "Invalid amount"
472+
# Test fee_rate values that don't pass fixed-point parsing checks.
473+
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
474+
assert_raises_rpc_error(-3, msg, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=invalid_value)
475+
# Test fee_rate values that cannot be represented in sat/vB.
476+
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
477+
assert_raises_rpc_error(-3, msg, self.nodes[2].sendtoaddress, address=address, amount=10, fee_rate=invalid_value)
478+
# Test fee_rate out of range (negative number).
455479
assert_raises_rpc_error(-3, OUT_OF_RANGE, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=-1)
480+
# Test type error.
481+
for invalid_value in [True, {"foo": "bar"}]:
482+
assert_raises_rpc_error(-3, NOT_A_NUMBER_OR_STRING, self.nodes[2].sendtoaddress, address=address, amount=1.0, fee_rate=invalid_value)
456483

457484
self.log.info("Test sendtoaddress raises if an invalid conf_target or estimate_mode is passed")
458485
for target, mode in product([-1, 0, 1009], ["economical", "conservative"]):

test/functional/wallet_bumpfee.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,24 @@ def test_invalid_parameters(self, rbf_node, peer_node, dest_address):
110110
assert_raises_rpc_error(-8, "Insufficient total fee 0.00000141", rbf_node.bumpfee, rbfid, {"fee_rate": INSUFFICIENT})
111111

112112
self.log.info("Test invalid fee rate settings")
113-
assert_raises_rpc_error(-8, "Insufficient total fee 0.00", rbf_node.bumpfee, rbfid, {"fee_rate": 0})
114113
assert_raises_rpc_error(-4, "Specified or calculated fee 0.141 is too high (cannot be higher than -maxtxfee 0.10",
115114
rbf_node.bumpfee, rbfid, {"fee_rate": TOO_HIGH})
115+
# Test fee_rate with zero values.
116+
msg = "Insufficient total fee 0.00"
117+
for zero_value in [0, 0.000, 0.00000000, "0", "0.000", "0.00000000"]:
118+
assert_raises_rpc_error(-8, msg, rbf_node.bumpfee, rbfid, {"fee_rate": zero_value})
119+
msg = "Invalid amount"
120+
# Test fee_rate values that don't pass fixed-point parsing checks.
121+
for invalid_value in ["", 0.000000001, 1e-09, 1.111111111, 1111111111111111, "31.999999999999999999999"]:
122+
assert_raises_rpc_error(-3, msg, rbf_node.bumpfee, rbfid, {"fee_rate": invalid_value})
123+
# Test fee_rate values that cannot be represented in sat/vB.
124+
for invalid_value in [0.0001, 0.00000001, 0.00099999, 31.99999999, "0.0001", "0.00000001", "0.00099999", "31.99999999"]:
125+
assert_raises_rpc_error(-3, msg, rbf_node.bumpfee, rbfid, {"fee_rate": invalid_value})
126+
# Test fee_rate out of range (negative number).
116127
assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate": -1})
128+
# Test type error.
117129
for value in [{"foo": "bar"}, True]:
118130
assert_raises_rpc_error(-3, "Amount is not a number or string", rbf_node.bumpfee, rbfid, {"fee_rate": value})
119-
assert_raises_rpc_error(-3, "Invalid amount", rbf_node.bumpfee, rbfid, {"fee_rate": ""})
120131

121132
self.log.info("Test explicit fee rate raises RPC error if both fee_rate and conf_target are passed")
122133
assert_raises_rpc_error(-8, "Cannot specify both conf_target and fee_rate. Please provide either a confirmation "

0 commit comments

Comments
 (0)