setfeerate
RPC in sat/vB and makes settxfee
a hidden RPC per the plan described in #20484 (comment).
setfeerate
RPC in sat/vB and makes settxfee
a hidden RPC per the plan described in #20484 (comment).
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
sat/vB
instead of sat/B
?
CFeeRate
. I updated the PR descpription to clarify that (thanks!)
2405+ result = "Fee rate for transactions with this wallet successfully set to " + amount_str;
2406+ }
2407+
2408+ UniValue obj(UniValue::VOBJ);
2409+ obj.pushKV("wallet_name", wallet.GetName());
2410+ obj.pushKV("fee_rate", UniValue(UniValue::VNUM, wallet.m_pay_tx_fee.SatsToString()));
ValueFromFeeRate
106@@ -107,7 +107,7 @@ def run_test(self):
107 assert_equal(stats[self.max_stat_pos]['height'], self.start_height + self.max_stat_pos)
108
109 for i in range(self.max_stat_pos+1):
110- self.log.info('Checking block %d\n' % (i))
111+ self.log.info('Checking block %d' % (i))
2384+ if (!rpc_wallet) return NullUniValue;
2385+ CWallet& wallet = *rpc_wallet;
2386+
2387+ LOCK(wallet.cs_wallet);
2388+
2389+ CFeeRate amount{AmountFromValue(request.params[0]), COIN};
(CAmount fee_rate, FeeEstimateMode mode)
and added unit tests.
git diff 14c5e50 2176a3
.
41@@ -42,3 +42,7 @@ std::string CFeeRate::ToString(const FeeEstimateMode& fee_estimate_mode) const
42 default: return strprintf("%d.%08d %s/kvB", nSatoshisPerK / COIN, nSatoshisPerK % COIN, CURRENCY_UNIT);
43 }
44 }
45+
46+std::string CFeeRate::SatsToString() const {
style nit d6643ef609bf442fe38dd8a1335dd7c580f1fdd7:
could clang-format?
SatsToString()
function in favor of making the existing ToString()
more flexible
110+ BOOST_CHECK(CFeeRate(CAmount(123456789), FeeEstimateMode::SAT_VB) == CFeeRate(1234));
111+ // ...with invalid unit values returns CFeeRate(0)
112+ BOOST_CHECK(CFeeRate(CAmount{123}, FeeEstimateMode::UNSET) == CFeeRate{0});
113+ BOOST_CHECK(CFeeRate(CAmount{123}, std::numeric_limits<size_t>::max()) == CFeeRate{0});
114+ BOOST_CHECK(CFeeRate(CAmount{123}, 0) == CFeeRate{0});
115+ BOOST_CHECK(CFeeRate(CAmount{123}, -1) == CFeeRate{0});
style nit in 4cccf2d88377a9f813ab6d63889cbb5136c31aad
Might be good to make this impossible to compile instead of a runtime-just-return-0. Can be achieved via constexpr if
(and some template magic) or by having a CFeeRate::FromSatB(amount)
and FromBtcKb(amount)
helper.
CFeeRate(CAmount, size_t)
one, so it was mistake to add them here. Will move them into the tests for that constructor and see if anything needs to be done there; normally the ctor should just set nSatoshisPerK
to 0 in that case.
BOOST_CHECK(CFeeRate(CAmount{123}, FeeEstimateMode::UNSET) == CFeeRate{0})
line)
40@@ -40,6 +41,7 @@ int ParseSighashString(const UniValue& sighash);
41
42 // core_write.cpp
43 UniValue ValueFromAmount(const CAmount& amount);
44+UniValue ValueFromFeeRate(const CFeeRate& fee_rate);
in commit 3c92185b95764f5c3536637e5923316cd9c2d8b0:
Might be good if the method name included the unit (sat/b) to avoid calling this in a btc/kb context
6@@ -7,35 +7,31 @@
7
2354+{
2355+ return RPCHelpMan{
2356+ "setfeerate",
2357+ "\nSet the transaction fee rate in " + CURRENCY_ATOM + "/vB for this wallet.\n"
2358+ "Overrides the global -paytxfee configuration option.\n"
2359+ "Can be deactivated by passing 0 as the fee rate, in which case automatic fee selection will be used by default.\n",
c6e9bfd1bd9d6a65673fbe1c278a7c7ada35273d
Worth noting that the new settings is not persisted?
2409+ result = "Fee rate for transactions with this wallet successfully set to " + amount_str;
2410+ }
2411+ CHECK_NONFATAL(result.empty() != error.empty());
2412+
2413+ UniValue obj{UniValue::VOBJ};
2414+ obj.pushKV("wallet_name", wallet.GetName());
c6e9bfd1bd9d6a65673fbe1c278a7c7ada35273d
Why return wallet name? What about returning previous effective value?
I think we should establish a guideline first on what input parameters to return. I don’t think we’ve done this in the past and this could lead to user confusion: “What am I supposed to do with the return parameter that is equal to the input parameter? Assert that they are the same? Ignore?”
If this is something useful to return for every wallet RPC, then all wallet RPCs should be updated.
Thanks @promag and @MarcoFalke for the review feedback. Updates in latest push:
Check for and raise “Invalid amount” if the passed value is non-representable by CFeeRate (lesson learned from #20546)
Fix the error messages if the current value is unset, to indicate a current value of “0 (unset)” instead of “0.000 sat/vB”
Make sure the tests check passing amounts with a string as well as a number, and update the coverage for the first two items
Update the settxfee and setfeerate helps with @promag’s suggestion
2397+ const std::string amount_str{amount.ToString(FeeEstimateMode::SAT_VB)};
2398+ const std::string current_setting{strprintf("The current setting of %s for this wallet remains unchanged.", wallet.m_pay_tx_fee == zero ? "0 (unset)" : wallet.m_pay_tx_fee.ToString(FeeEstimateMode::SAT_VB))};
2399+ std::string result, error;
2400+
2401+ if (amount == zero) {
2402+ if (request.params[0].get_real() != 0) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
After #20546 this will go away:
0- const CFeeRate amount{CFeeRate::FromSatB(AmountFromValue(request.params[0]))};
1+ const CFeeRate amount{FeeRateFromValueInSatB(request.params[0])};
2@@ -2399,7 +2399,6 @@ static RPCHelpMan setfeerate()
3
4 if (amount == zero) {
5- if (request.params[0].get_real() != 0) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
6 wallet.m_pay_tx_fee = amount;
2310@@ -2311,10 +2311,12 @@ static RPCHelpMan listlockunspent()
2311 static RPCHelpMan settxfee()
2312 {
2313 return RPCHelpMan{"settxfee",
2314- "\nSet the transaction fee per kB for this wallet. Overrides the global -paytxfee command line parameter.\n"
2315- "Can be deactivated by passing 0 as the fee. In that case automatic fee selection will be used by default.\n",
2316+ "\n(Superseded by the setfeerate RPC in sat/vB, which it is recommended to use instead.)\n"
deprecated
instead of superseded
be more consistent with previous RPC API “changes”?
Code review ACK 73a0f02, needs to squash some commits. Also add release notes?
Rebased, updated settxfee help with @jonasschnelli’s suggestion, added release notes per @promag’s suggestion. Sorry for the delay in updating, this was waiting since a month on the improvements and fixes in #20546 but I’ve had to move forward without them. I think this is done. Please re-ACK if you can.
updates tests that call settxfee to call setfeerate instead;
settxfee functionality still has test coverage in
- wallet_bumpfee.py
- wallet_create_tx.py
- wallet_multiwallet.py
also fixes two fundrawtransaction tests that had off-by-one naming errors
and tightens up one of the two
Make settxfee a hidden rpc to avoid confusion for new users and guide them
to use the setfeerate rpc in sat/vB, while allowing existing scripts based
on settxfee in BTC/kvB to continue using it without breaking.
A hidden RPC means that `bitcoin-cli help settxfee` continues to display
the help documentation while removing it from the results of the general
`bitcoin-cli help`.
Updates the release note.
per PR 20403 review feedback by MarcoFalke
38@@ -39,6 +39,8 @@ class CFeeRate
39 // We've previously had bugs creep in from silent double->int conversion...
40 static_assert(std::is_integral<I>::value, "CFeeRate should be used without floats");
41 }
42+ static CFeeRate FromSatB(CAmount fee_rate);
43+ static CFeeRate FromBtcKb(CAmount fee_rate);
(nFeePaid * 1e3 / 1e8) == (nFeePaid / 1e5)
and not (nFeePaid * 1e8 / 1e3) == (nFeePaid / 1e5)
43- default: return strprintf("%d.%08d %s/kvB", nSatoshisPerK / COIN, nSatoshisPerK % COIN, CURRENCY_UNIT);
44+ if (with_units) {
45+ switch (mode) {
46+ case FeeEstimateMode::SAT_VB:
47+ return strprintf("%d.%03d %s/vB", nSatoshisPerK / 1000, nSatoshisPerK % 1000, CURRENCY_ATOM);
48+ default:
FeeEstimateMode
isn’t passed by mistake?
145+ BOOST_CHECK_EQUAL(CFeeRate(1).ToString(FeeEstimateMode::SAT_VB, /* with_units */ false), "0.001");
146+ BOOST_CHECK_EQUAL(CFeeRate(70).ToString(FeeEstimateMode::SAT_VB, /* with_units */ false), "0.070");
147+ BOOST_CHECK_EQUAL(CFeeRate(3141).ToString(FeeEstimateMode::SAT_VB, /* with_units */ false), "3.141");
148+ BOOST_CHECK_EQUAL(CFeeRate(10002).ToString(FeeEstimateMode::SAT_VB, /* with_units */ false), "10.002");
149+ BOOST_CHECK_EQUAL(CFeeRate(3141).ToString(FeeEstimateMode::BTC_KVB, /* with_units */ false), "0.00003141");
150+ BOOST_CHECK_EQUAL(CFeeRate(10002).ToString(FeeEstimateMode::BTC_KVB, /* with_units */ false), "0.00010002");
215@@ -216,6 +216,29 @@ BOOST_AUTO_TEST_CASE(rpc_parse_monetary_values)
216 BOOST_CHECK_THROW(AmountFromValue(ValueFromString("93e+9")), UniValue); //overflow error
217 }
218
219+BOOST_AUTO_TEST_CASE(rpc_parse_fee_rate_values)
CFeeRate
various constructors and ToString()
already covered by other unit tests? Unit tests have a non-zero maintenance cost and I’m missing the point of such duplication. The ValueFromFeeRate
fn is pretty trivial and I believe the test should be trivial as well.
41- switch (fee_estimate_mode) {
42- case FeeEstimateMode::SAT_VB: return strprintf("%d.%03d %s/vB", nSatoshisPerK / 1000, nSatoshisPerK % 1000, CURRENCY_ATOM);
43- default: return strprintf("%d.%08d %s/kvB", nSatoshisPerK / COIN, nSatoshisPerK % COIN, CURRENCY_UNIT);
44+ if (with_units) {
45+ switch (mode) {
46+ case FeeEstimateMode::SAT_VB:
2387+ CWallet& wallet = *rpc_wallet;
2388+
2389+ LOCK(wallet.cs_wallet);
2390+
2391+ const CFeeRate amount{CFeeRate::FromSatB(AmountFromValue(request.params[0]))};
2392+ const CFeeRate relay_min_feerate{wallet.chain().relayMinFee().GetFeePerK()};
GetFeePerK()
call here and below?
105+ BOOST_CHECK(CFeeRate::FromSatB(CAmount(0)) == CFeeRate(0));
106+ BOOST_CHECK(CFeeRate::FromSatB(CAmount(99999)) == CFeeRate(0));
107+ BOOST_CHECK(CFeeRate::FromSatB(CAmount(100000)) == CFeeRate(1));
108+ BOOST_CHECK(CFeeRate::FromSatB(CAmount(100001)) == CFeeRate(1));
109+ BOOST_CHECK(CFeeRate::FromSatB(CAmount(2690000)) == CFeeRate(26));
110+ BOOST_CHECK(CFeeRate::FromSatB(CAmount(123456789)) == CFeeRate(1234));
718@@ -717,7 +719,7 @@ def test_option_feerate(self):
719
720 result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee)
721 btc_kvb_to_sat_vb = 100000 # (1e5)
197@@ -197,7 +198,7 @@ def run_test(self):
198 # Send 10 BTC normal
199 address = self.nodes[0].getnewaddress("test")
200 fee_per_byte = Decimal('0.001') / 1000
201- self.nodes[2].settxfee(fee_per_byte * 1000)
0 fee_sat_per_byte = 100
1 self.nodes[2].setfeerate(fee_sat_per_byte)
718@@ -717,7 +719,7 @@ def test_option_feerate(self):
719
720 result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by settxfee)
0 result = node.fundrawtransaction(rawtx) # uses self.min_relay_tx_fee (set by setfeerate)
818@@ -817,9 +819,9 @@ def test_option_subtract_fee_from_outputs(self):
819 rawtx = self.nodes[3].createrawtransaction(inputs, outputs)
820
821 # Test subtract fee from outputs with feeRate (BTC/kvB)
822- result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by settxfee)
823+ result = [self.nodes[3].fundrawtransaction(rawtx), # uses self.min_relay_tx_fee (set by setfeerate)
824 self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": []}), # empty subtraction list
825- self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by settxfee)
826+ self.nodes[3].fundrawtransaction(rawtx, {"subtractFeeFromOutputs": [0]}), # uses self.min_relay_tx_fee (set by setfeerate)
337+ rbfid = spend_one_input(rbf_node, dest_address)
338+ fee_rate = 25
339+ test_response(requested=fee_rate, expected=fee_rate, msg="Fee rate for transactions with this wallet successfully set to 25.000 sat/vB")
340+ bumped_tx = rbf_node.bumpfee(rbfid)
341+ actual_feerate = bumped_tx["fee"] * COIN / rbf_node.getrawtransaction(bumped_tx["txid"], True)["vsize"]
342+ assert_greater_than(Decimal("0.01"), abs(fee_rate - actual_feerate))
0.01
? In the test case with settxfee
we have a different value of 0.00001000
. Should we have a testcase-wise constant?
Even 0.01 is too small for this. Signatures can be 71-73 bytes long, but even 72 bytes throws this off.
0 bumped_txdetails = rbf_node.getrawtransaction(bumped_tx["txid"], True)
1 allow_for_bytes_offset = len(bumped_txdetails['vout']) * 2 # potentially up to 2 bytes per output
2 actual_fee = bumped_tx["fee"] * COIN
3 assert_approx(actual_fee, fee_rate * bumped_txdetails['vsize'], fee_rate * allow_for_bytes_offset)
321+ self.log.info("Test setfeerate")
322+
323+ def test_response(*, wallet="RBF wallet", requested=0, expected=0, error=None, msg):
324+ assert_equal(rbf_node.setfeerate(requested), {"wallet_name": wallet, "fee_rate": expected, ("error" if error else "result"): msg})
325+
326+ # Test setfeerate with too high/low values returns expected errors
bumpfee
RPC. I believe it’s better to have it in a separate test. wallet_bumpfee.py
is already huge
340+ bumped_tx = rbf_node.bumpfee(rbfid)
341+ actual_feerate = bumped_tx["fee"] * COIN / rbf_node.getrawtransaction(bumped_tx["txid"], True)["vsize"]
342+ assert_greater_than(Decimal("0.01"), abs(fee_rate - actual_feerate))
343+ test_response(msg="Fee rate for transactions with this wallet successfully unset. By default, automatic fee selection will be used.")
344+
345+ # Test setfeerate with a different -maxtxfee
setfeerate
test, not bumpfee
test
27@@ -27,6 +28,11 @@ UniValue ValueFromAmount(const CAmount& amount)
28 strprintf("%s%d.%08d", sign ? "-" : "", quotient, remainder));
29 }
30
31+UniValue ValueFromFeeRate(const CFeeRate& fee_rate, FeeEstimateMode mode)
ValuefromAmount
in appropriate places ?
215@@ -216,7 +216,7 @@ static void SetFeeEstimateMode(const CWallet& wallet, CCoinControl& cc, const Un
216 if (!estimate_mode.isNull() && estimate_mode.get_str() != "unset") {
217 throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot specify both estimate_mode and fee_rate");
218 }
219- cc.m_feerate = CFeeRate(AmountFromValue(fee_rate), COIN);
220+ cc.m_feerate = CFeeRate::FromSatB(AmountFromValue(fee_rate));
In commit “wallet: update rpcwallet to CFeeRate named constructors” (c72083c2d56237f8fbd3a82e3ff395f0cdb8fd57)
Wow wow. I see that this is doing the right thing, but these constructor names seem absurdly misleading! The AmountFromValue
function converts a floating point number to a fixed-point representation of that number multiplied by 10e8
. So even though the fee_rate
univalue argument has units of sat/B, the AmountFromValue
integer return value is no longer in the same units, because it has been multiplied by 100000000. So FromSatB
should really be called CFeeRate::FromSatB_Fixed_Point_e8
or something like that because it is taking sat/B values with a very specific, atypical number representation.
An analogous thing is happening with FromBtcKb
below. It doesn’t really take a BTC/Kb argument. That’s basically false advertising. What it really takes is a BTC/Kb*10e8 argument which is the same as a sat/Kb argument, so this constructor would be more accurately called CFeeRate::FromSatKb
I guess I’m having a negative reaction to these constructor names, but I do think this could be cleaned up pretty easily. I think it would be better if CFeeRate
class had no involvement with univalue parsing or fixed point representations, and if it didn’t (mis)use the CAmount type as a way to represent rates when CAmount
is used almost everywhere else only to represent quantities.
My suggestion to be less confusing / misleading would be to drop the two new CFeeRate
constructors, and just add standalone RPC util functions:
0CFeeRate FeeRateFromSatB(const UniValue& value);
1CFeeRate FeeRateFromBtcKb(const UniValue& value);
Then call these to simplify CFeeRate::FromSatB(AmountFromValue(feerate))
as FeeRateFromSatB(feerate)
here and in setfeerate
and to simplify CFeeRate::FromBtcKb(AmountFromValue(feerate))
as FeeRateFromBtcKb(feerate)
in settxfee
Wow wow. I see that this is doing the right thing, but these constructor names seem absurdly misleading!
The current code in master isn’t exactly clear right now either:
0 /** Constructor for a fee rate in satoshis per kvB (sat/kvB).
1 *
2 * Passing a num_bytes value of COIN (1e8) returns a fee rate in satoshis per vB (sat/vB),
3 * e.g. (nFeePaid * 1e8 / 1e3) == (nFeePaid / 1e5),
4 * where 1e5 is the ratio to convert from BTC/kvB to sat/vB.
5 */
6 CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes);
I think it would be good to figure out how to clear this up before introducing further features that depend on this.
Wow wow. I see that this is doing the right thing, but these constructor names seem absurdly misleading!
The current code in master isn’t exactly clear right now either:
0 /** Constructor for a fee rate in satoshis per kvB (sat/kvB). 1 * 2 * Passing a num_bytes value of COIN (1e8) returns a fee rate in satoshis per vB (sat/vB), 3 * e.g. (nFeePaid * 1e8 / 1e3) == (nFeePaid / 1e5), 4 * where 1e5 is the ratio to convert from BTC/kvB to sat/vB. 5 */ 6 CFeeRate(const CAmount& nFeePaid, uint32_t num_bytes);
I think it would be good to figure out how to clear this up before introducing further features that depend on this.
Ok, seems best to close this then. It’s almost a year old and needs to be done differently. I think using sat/vB is still a popular request from users and plan to try again a bit later.
85+ node = self.nodes[0]
86+
87+ def test_response(*, requested=0, expected=0, error=None, msg):
88+ assert_equal(node.setfeerate(requested), {"wallet_name": "test_wallet", "fee_rate": expected, ("error" if error else "result"): msg})
89+
90+ # Test setfeerate with 10.0001 (CFeeRate rounding), "10.001" and "4" sat/vB
setfeerate
test
🐙 This pull request conflicts with the target branch and needs rebase.
Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a “draft”.
Started review (will update list below with progress). I got a little sidetracked on the cfeerate constructor names, but overall this seems like a very nice, well thought out PR