Merge 20391 via rpc_setfeerate-28+knots

This commit is contained in:
Luke Dashjr 2025-03-05 03:27:08 +00:00
commit a22e586743
12 changed files with 215 additions and 1 deletions

View File

@ -13,6 +13,7 @@
class CBlock;
class CBlockHeader;
class CFeeRate;
class CScript;
class CTransaction;
struct CMutableTransaction;
@ -41,6 +42,7 @@ bool DecodeHexBlockHeader(CBlockHeader&, const std::string& hex_header);
// core_write.cpp
UniValue ValueFromAmount(const CAmount amount);
UniValue ValueFromFeeRate(const CFeeRate& fee_rate);
std::string FormatScript(const CScript& script);
std::string EncodeHexTx(const CTransaction& tx);
std::string SighashToStr(unsigned char sighash_type);

View File

@ -9,6 +9,7 @@
#include <consensus/consensus.h>
#include <consensus/validation.h>
#include <key_io.h>
#include <policy/feerate.h>
#include <script/descriptor.h>
#include <script/script.h>
#include <script/solver.h>
@ -36,6 +37,11 @@ UniValue ValueFromAmount(const CAmount amount)
strprintf("%s%d.%08d", amount < 0 ? "-" : "", quotient, remainder));
}
UniValue ValueFromFeeRate(const CFeeRate& fee_rate)
{
return UniValue(UniValue::VNUM, fee_rate.SatsToString());
}
std::string FormatScript(const CScript& script)
{
std::string ret;

View File

@ -43,3 +43,7 @@ std::string CFeeRate::ToString(const FeeEstimateMode& fee_estimate_mode) const
default: return strprintf("%d.%08d %s/kvB", nSatoshisPerK / COIN, nSatoshisPerK % COIN, CURRENCY_UNIT);
}
}
std::string CFeeRate::SatsToString() const {
return strprintf("%d.%03d", nSatoshisPerK / 1000, nSatoshisPerK % 1000);
}

View File

@ -68,7 +68,10 @@ public:
friend bool operator>=(const CFeeRate& a, const CFeeRate& b) { return a.nSatoshisPerK >= b.nSatoshisPerK; }
friend bool operator!=(const CFeeRate& a, const CFeeRate& b) { return a.nSatoshisPerK != b.nSatoshisPerK; }
CFeeRate& operator+=(const CFeeRate& a) { nSatoshisPerK += a.nSatoshisPerK; return *this; }
/** Return the fee rate in sat/vB or BTC/kvB, with units, as a string. */
std::string ToString(const FeeEstimateMode& fee_estimate_mode = FeeEstimateMode::BTC_KVB) const;
/** Return the fee rate in sat/vB, without units, as a string. */
std::string SatsToString() const;
friend CFeeRate operator*(const CFeeRate& f, int a) { return CFeeRate(a * f.nSatoshisPerK); }
friend CFeeRate operator*(int a, const CFeeRate& f) { return CFeeRate(a * f.nSatoshisPerK); }

View File

@ -47,6 +47,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendtoaddress", 8, "avoid_reuse" },
{ "sendtoaddress", 9, "fee_rate"},
{ "sendtoaddress", 10, "verbose"},
{ "setfeerate", 0, "amount" },
{ "settxfee", 0, "amount" },
{ "sethdseed", 0, "newkeypool" },
{ "getreceivedbyaddress", 1, "minconf" },

View File

@ -139,4 +139,12 @@ BOOST_AUTO_TEST_CASE(ToStringTest)
BOOST_CHECK_EQUAL(feeRate.ToString(FeeEstimateMode::SAT_VB), "0.001 sat/vB");
}
BOOST_AUTO_TEST_CASE(SatsToStringTest)
{
BOOST_CHECK_EQUAL(CFeeRate(1).SatsToString(), "0.001");
BOOST_CHECK_EQUAL(CFeeRate(70).SatsToString(), "0.070");
BOOST_CHECK_EQUAL(CFeeRate(3141).SatsToString(), "3.141");
BOOST_CHECK_EQUAL(CFeeRate(10002).SatsToString(), "10.002");
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -311,6 +311,17 @@ BOOST_AUTO_TEST_CASE(rpc_parse_monetary_values)
BOOST_CHECK_THROW(AmountFromValue(ValueFromString("93e+9")), UniValue); //overflow error
}
BOOST_AUTO_TEST_CASE(rpc_parse_fee_rate_values)
{
// Test ValueFromFeeRate() and CFeeRate()
// ...using default CFeeRate constructor
BOOST_CHECK_EQUAL(ValueFromFeeRate(CFeeRate(AmountFromValue(0.00001234))).get_real(), 1.234);
BOOST_CHECK_EQUAL(ValueFromFeeRate(CFeeRate(AmountFromValue(0.1234))).get_real(), 12340.000);
BOOST_CHECK_EQUAL(ValueFromFeeRate(CFeeRate(AmountFromValue(1234))).get_real(), 123400000.000);
// ...using CFeeRate constructor with bytes 1000
BOOST_CHECK_EQUAL(ValueFromFeeRate(CFeeRate(AmountFromValue(0.00001234), 1000)).get_real(), 1.234);
}
BOOST_AUTO_TEST_CASE(rpc_ban)
{
BOOST_CHECK_NO_THROW(CallRPC(std::string("clearbanned")));

View File

@ -433,6 +433,80 @@ RPCHelpMan sendmany()
};
}
RPCHelpMan setfeerate()
{
return RPCHelpMan{
"setfeerate",
"\nSet the transaction fee rate in " + CURRENCY_ATOM + "/vB for this wallet.\n"
"Overrides the global -paytxfee configuration option. Like -paytxfee, it is not persisted after bitcoind shutdown/restart.\n"
"Can be deactivated by passing 0 as the fee rate, in which case automatic fee selection will be used by default.\n",
{
{"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The transaction fee rate in " + CURRENCY_ATOM + "/vB to set (0 to unset)"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "wallet_name", "Name of the wallet the fee rate setting applies to"},
{RPCResult::Type::NUM, "fee_rate", "Fee rate in " + CURRENCY_ATOM + "/vB for the wallet after this operation"},
{RPCResult::Type::STR, "result", /* optional */ true, "Description of result, if successful"},
{RPCResult::Type::STR, "error", /* optional */ true, "Description of error, if any"},
},
},
RPCExamples{
""
"\nSet a fee rate of 1 " + CURRENCY_ATOM + "/vB\n"
+ HelpExampleCli("setfeerate", "1") +
"\nSet a fee rate of 3.141 " + CURRENCY_ATOM + "/vB\n"
+ HelpExampleCli("setfeerate", "3.141") +
"\nSet a fee rate of 7.75 " + CURRENCY_ATOM + "/vB with named arguments\n"
+ HelpExampleCli("-named setfeerate", "amount=\"7.75\"") +
"\nSet a fee rate of 25 " + CURRENCY_ATOM + "/vB with the RPC\n"
+ HelpExampleRpc("setfeerate", "25")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
std::shared_ptr<CWallet> const rpc_wallet{GetWalletForJSONRPCRequest(request)};
if (!rpc_wallet) return NullUniValue;
CWallet& wallet = *rpc_wallet;
LOCK(wallet.cs_wallet);
const CFeeRate amount{AmountFromValue(request.params[0]), COIN /* sat/vB */};
const CFeeRate relay_min_feerate{wallet.chain().relayMinFee().GetFeePerK()};
const CFeeRate wallet_min_feerate{wallet.m_min_fee.GetFeePerK()};
const CFeeRate wallet_max_feerate{wallet.m_default_max_tx_fee, 1000 /* BTC/kvB */};
const CFeeRate zero{CFeeRate{0}};
const std::string amount_str{amount.ToString(FeeEstimateMode::SAT_VB)};
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))};
std::string result, error;
if (amount == zero) {
if (request.params[0].get_real() != 0) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid amount");
wallet.m_pay_tx_fee = amount;
result = "Fee rate for transactions with this wallet successfully unset. By default, automatic fee selection will be used.";
} else if (amount < relay_min_feerate) {
error = strprintf("The requested fee rate of %s cannot be less than the minimum relay fee rate of %s. %s", amount_str, relay_min_feerate.ToString(FeeEstimateMode::SAT_VB), current_setting);
} else if (amount < wallet_min_feerate) {
error = strprintf("The requested fee rate of %s cannot be less than the wallet min fee rate of %s. %s", amount_str, wallet_min_feerate.ToString(FeeEstimateMode::SAT_VB), current_setting);
} else if (amount > wallet_max_feerate) {
error = strprintf("The requested fee rate of %s cannot be greater than the wallet max fee rate of %s. %s", amount_str, wallet_max_feerate.ToString(FeeEstimateMode::SAT_VB), current_setting);
} else {
wallet.m_pay_tx_fee = amount;
result = "Fee rate for transactions with this wallet successfully set to " + amount_str;
}
CHECK_NONFATAL(result.empty() != error.empty());
UniValue obj{UniValue::VOBJ};
obj.pushKV("wallet_name", wallet.GetName());
obj.pushKV("fee_rate", ValueFromFeeRate(wallet.m_pay_tx_fee));
if (error.empty()) {
obj.pushKV("result", result);
} else {
obj.pushKV("error", error);
}
return obj;
},
};
}
RPCHelpMan settxfee()
{
return RPCHelpMan{"settxfee",

View File

@ -1075,6 +1075,7 @@ RPCHelpMan encryptwallet();
// spend
RPCHelpMan sendtoaddress();
RPCHelpMan sendmany();
RPCHelpMan setfeerate();
RPCHelpMan settxfee();
RPCHelpMan fundrawtransaction();
RPCHelpMan bumpfee();
@ -1156,6 +1157,7 @@ Span<const CRPCCommand> GetWalletRPCCommands()
{"wallet", &sendtoaddress},
{"wallet", &sethdseed},
{"wallet", &setlabel},
{"wallet", &setfeerate},
{"wallet", &settxfee},
{"wallet", &setwalletflag},
{"wallet", &signmessage},

View File

@ -19,10 +19,12 @@ from test_framework.blocktools import (
COINBASE_MATURITY,
)
from test_framework.messages import (
COIN,
MAX_BIP125_RBF_SEQUENCE,
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_approx,
assert_equal,
assert_fee_amount,
assert_greater_than,
@ -62,6 +64,7 @@ class BumpFeeTest(BitcoinTestFramework):
"-mintxfee=0.00002",
"-addresstype=bech32",
] for i in range(self.num_nodes)]
self.wallet_names = [self.default_wallet_name, "RBF wallet"]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
@ -106,6 +109,7 @@ class BumpFeeTest(BitcoinTestFramework):
test_bumpfee_metadata(self, rbf_node, dest_address)
test_locked_wallet_fails(self, rbf_node, dest_address)
test_change_script_match(self, rbf_node, dest_address)
test_setfeerate(self, rbf_node, dest_address)
test_settxfee(self, rbf_node, dest_address)
test_maxtxfee_fails(self, rbf_node, dest_address)
# These tests wipe out a number of utxos that are expected in other tests
@ -532,6 +536,44 @@ def test_dust_to_fee(self, rbf_node, dest_address):
self.clear_mempool()
def test_setfeerate(self, rbf_node, dest_address):
self.log.info("Test setfeerate")
def test_response(*, wallet="RBF wallet", requested=0, expected=0, error=None, msg):
assert_equal(rbf_node.setfeerate(requested), {"wallet_name": wallet, "fee_rate": expected, ("error" if error else "result"): msg})
# Test setfeerate with too high/low values returns expected errors
new = Decimal("10000.001")
test_response(requested=new, error=True, msg=f"The requested fee rate of {new} sat/vB cannot be greater than the wallet max fee rate of 10000.000 sat/vB. The current setting of 0 (unset) for this wallet remains unchanged.")
new = Decimal("0.999")
test_response(requested=new, error=True, msg=f"The requested fee rate of {new} sat/vB cannot be less than the minimum relay fee rate of 1.000 sat/vB. The current setting of 0 (unset) for this wallet remains unchanged.")
fee_rate = Decimal("2.001")
test_response(requested=fee_rate, expected=fee_rate, msg=f"Fee rate for transactions with this wallet successfully set to {fee_rate} sat/vB")
new = Decimal("1.999")
test_response(requested=new, expected=fee_rate, error=True, msg=f"The requested fee rate of {new} sat/vB cannot be less than the wallet min fee rate of 2.000 sat/vB. The current setting of {fee_rate} sat/vB for this wallet remains unchanged.")
# Test setfeerate with valid values returns expected results
rbfid = spend_one_input(rbf_node, dest_address)
fee_rate = 25
test_response(requested=fee_rate, expected=fee_rate, msg="Fee rate for transactions with this wallet successfully set to 25.000 sat/vB")
bumped_tx = rbf_node.bumpfee(rbfid)
bumped_txdetails = rbf_node.getrawtransaction(bumped_tx["txid"], True)
allow_for_bytes_offset = len(bumped_txdetails['vout']) * 2 # potentially up to 2 bytes per output
actual_fee = bumped_tx["fee"] * COIN
assert_approx(actual_fee, fee_rate * bumped_txdetails['vsize'], fee_rate * allow_for_bytes_offset)
test_response(msg="Fee rate for transactions with this wallet successfully unset. By default, automatic fee selection will be used.")
# Test setfeerate with a different -maxtxfee
self.restart_node(1, ["-maxtxfee=0.000025"] + self.extra_args[1])
new = "2.501"
test_response(requested=new, error=True, msg=f"The requested fee rate of {new} sat/vB cannot be greater than the wallet max fee rate of 2.500 sat/vB. The current setting of 0 (unset) for this wallet remains unchanged.")
self.restart_node(1, self.extra_args[1])
rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
self.connect_nodes(1, 0)
self.clear_mempool()
def test_settxfee(self, rbf_node, dest_address):
self.log.info('Test settxfee')
assert_raises_rpc_error(-8, "txfee cannot be less than min relay tx fee", rbf_node.settxfee, Decimal('0.000005'))

View File

@ -3,6 +3,8 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from decimal import Decimal
from test_framework.messages import (
tx_from_hex,
)
@ -37,6 +39,7 @@ class CreateTxWalletTest(BitcoinTestFramework):
self.test_tx_size_too_large()
self.test_create_too_long_mempool_chain()
self.test_version3()
self.test_setfeerate()
def test_anti_fee_sniping(self):
self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled')
@ -55,6 +58,7 @@ class CreateTxWalletTest(BitcoinTestFramework):
# More than 10kB of outputs, so that we hit -maxtxfee with a high feerate
outputs = {self.nodes[0].getnewaddress(address_type='bech32'): 0.000025 for _ in range(400)}
raw_tx = self.nodes[0].createrawtransaction(inputs=[], outputs=outputs)
msg = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)"
for fee_setting in ['-minrelaytxfee=0.01', '-mintxfee=0.01', '-paytxfee=0.01']:
self.log.info('Check maxtxfee in combination with {}'.format(fee_setting))
@ -85,6 +89,12 @@ class CreateTxWalletTest(BitcoinTestFramework):
)
self.nodes[0].settxfee(0)
self.log.info('Check maxtxfee in combination with setfeerate (sat/vB)')
self.nodes[0].setfeerate(1000)
assert_raises_rpc_error(-6, msg, self.nodes[0].sendmany, dummy="", amounts=outputs)
assert_raises_rpc_error(-4, msg, self.nodes[0].fundrawtransaction, hexstring=raw_tx)
self.nodes[0].setfeerate(0)
def test_create_too_long_mempool_chain(self):
self.log.info('Check too-long mempool chain error')
df_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
@ -127,6 +137,51 @@ class CreateTxWalletTest(BitcoinTestFramework):
assert_equal(tx_current_version.version, 2)
wallet_v3.unloadwallet()
def test_setfeerate(self):
self.log.info("Test setfeerate")
self.restart_node(0, extra_args=["-mintxfee=0.00003141"]) # 3.141 sat/vB
node = self.nodes[0]
def test_response(*, requested=0, expected=0, error=None, msg):
assert_equal(node.setfeerate(requested), {"wallet_name": self.default_wallet_name, "fee_rate": expected, ("error" if error else "result"): msg})
# Test setfeerate with 10.0001 (CFeeRate rounding), "10.001" and "4" sat/vB
test_response(requested=10.0001, expected=10, msg="Fee rate for transactions with this wallet successfully set to 10.000 sat/vB")
assert_equal(node.getwalletinfo()["paytxfee"], Decimal("0.00010000"))
test_response(requested="10.001", expected=Decimal("10.001"), msg="Fee rate for transactions with this wallet successfully set to 10.001 sat/vB")
assert_equal(node.getwalletinfo()["paytxfee"], Decimal("0.00010001"))
test_response(requested="4", expected=4, msg="Fee rate for transactions with this wallet successfully set to 4.000 sat/vB")
assert_equal(node.getwalletinfo()["paytxfee"], Decimal("0.00004000"))
# Test setfeerate with too-high/low values returns expected errors
test_response(requested=Decimal("10000.001"), expected=4, error=True, msg="The requested fee rate of 10000.001 sat/vB cannot be greater than the wallet max fee rate of 10000.000 sat/vB. The current setting of 4.000 sat/vB for this wallet remains unchanged.")
test_response(requested=Decimal("0.999"), expected=4, error=True, msg="The requested fee rate of 0.999 sat/vB cannot be less than the minimum relay fee rate of 1.000 sat/vB. The current setting of 4.000 sat/vB for this wallet remains unchanged.")
test_response(requested=Decimal("3.140"), expected=4, error=True, msg="The requested fee rate of 3.140 sat/vB cannot be less than the wallet min fee rate of 3.141 sat/vB. The current setting of 4.000 sat/vB for this wallet remains unchanged.")
assert_equal(node.getwalletinfo()["paytxfee"], Decimal("0.00004000"))
# Test setfeerate to 3.141 sat/vB
test_response(requested=3.141, expected=Decimal("3.141"), msg="Fee rate for transactions with this wallet successfully set to 3.141 sat/vB")
assert_equal(node.getwalletinfo()["paytxfee"], Decimal("0.00003141"))
# Test setfeerate with values non-representable by CFeeRate
for invalid_value in [0.00000001, 0.0009, 0.00099999]:
assert_raises_rpc_error(-3, "Invalid amount", node.setfeerate, amount=invalid_value)
# Test setfeerate with values rejected by ParseFixedPoint() called in AmountFromValue()
for invalid_value in ["", 0.000000001, "1.111111111", 11111111111]:
assert_raises_rpc_error(-3, "Invalid amount", node.setfeerate, amount=invalid_value)
# Test deactivating setfeerate
test_response(msg="Fee rate for transactions with this wallet successfully unset. By default, automatic fee selection will be used.")
assert_equal(node.getwalletinfo()["paytxfee"], 0)
# Test currently-unset setfeerate with too-high/low values returns expected errors
test_response(requested=Decimal("10000.001"), error=True, msg="The requested fee rate of 10000.001 sat/vB cannot be greater than the wallet max fee rate of 10000.000 sat/vB. The current setting of 0 (unset) for this wallet remains unchanged.")
assert_equal(node.getwalletinfo()["paytxfee"], 0)
test_response(requested=Decimal("0.999"), error=True, msg="The requested fee rate of 0.999 sat/vB cannot be less than the minimum relay fee rate of 1.000 sat/vB. The current setting of 0 (unset) for this wallet remains unchanged.")
test_response(requested=Decimal("3.140"), error=True, msg="The requested fee rate of 3.140 sat/vB cannot be less than the wallet min fee rate of 3.141 sat/vB. The current setting of 0 (unset) for this wallet remains unchanged.")
assert_equal(node.getwalletinfo()["paytxfee"], 0)
if __name__ == '__main__':
CreateTxWalletTest(__file__).main()

View File

@ -250,12 +250,18 @@ class MultiWalletTest(BitcoinTestFramework):
assert_equal(batch[0]["result"]["chain"], self.chain)
assert_equal(batch[1]["result"]["walletname"], "w1")
self.log.info('Check for per-wallet settxfee call')
self.log.info('Test per-wallet setfeerate and settxfee calls')
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
assert_equal(w2.getwalletinfo()['paytxfee'], 0)
w2.setfeerate(200)
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
assert_equal(w2.getwalletinfo()['paytxfee'], Decimal('0.00200000'))
w2.settxfee(0.001)
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
assert_equal(w2.getwalletinfo()['paytxfee'], Decimal('0.00100000'))
w1.setfeerate(30)
assert_equal(w1.getwalletinfo()['paytxfee'], Decimal('0.00030000'))
assert_equal(w2.getwalletinfo()['paytxfee'], Decimal('0.00100000'))
self.log.info("Test dynamic wallet loading")