rpc: update estimatefeesmartfee to use forecaster_man

- Given a confirmation target, we use fee estimator module that call all
available fee estimator forcasters and return the lowest fee rate that if
a transaction use will likely confirm in a given confirmation target.

- This also provides backward compatibility

Co-authored-by: willcl-ark <will@256k1.dev>
This commit is contained in:
ismaelsadeeq 2023-08-18 09:00:37 +01:00
parent 821d58a825
commit 88aad65959
No known key found for this signature in database
GPG key ID: 0E3908F364989888
7 changed files with 65 additions and 29 deletions

View file

@ -731,6 +731,7 @@ ForecastResult CBlockPolicyEstimator::ForecastFeeRate(int target, bool conservat
FeeCalculation feeCalcConservative;
CFeeRate feerate{estimateSmartFee(target, &feeCalcConservative, conservative)};
result.current_block_height = feeCalcConservative.bestheight;
result.returned_target = feeCalcConservative.returnedTarget;
if (feerate == CFeeRate(0)) {
result.m_error = "Insufficient data or no feerate found";
return result;

View file

@ -33,6 +33,9 @@ struct ForecastResult {
//! The chain tip at which the forecast was made.
unsigned int current_block_height{0};
//! The confirmation target the forecast was made for.
unsigned int returned_target{0};
std::optional<std::string> m_error; ///< Optional error message.
/**

View file

@ -47,6 +47,7 @@ ForecastResult MemPoolForecaster::ForecastFeeRate(int target, bool conservative)
return result;
}
result.returned_target = target;
LogDebug(BCLog::MEMPOOL,
"%s: Block height %s, Block template 25th percentile fee rate: %s %s/kvB, "
"50th percentile fee rate: %s %s/kvB, 75th percentile fee rate: %s %s/kvB, "

View file

@ -259,6 +259,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getrawmempool", 1, "mempool_sequence" },
{ "getorphantxs", 0, "verbosity" },
{ "estimatesmartfee", 0, "conf_target" },
{ "estimatesmartfee", 2, "block_policy_only" },
{ "estimaterawfee", 0, "conf_target" },
{ "estimaterawfee", 1, "threshold" },
{ "prioritisetransaction", 1, "dummy" },

View file

@ -9,6 +9,7 @@
#include <policy/feerate.h>
#include <policy/fees/block_policy_estimator.h>
#include <policy/fees/forecaster_man.h>
#include <policy/fees/forecaster_util.h>
#include <rpc/protocol.h>
#include <rpc/request.h>
#include <rpc/server.h>
@ -39,6 +40,7 @@ static RPCHelpMan estimatesmartfee()
{"conf_target", RPCArg::Type::NUM, RPCArg::Optional::NO, "Confirmation target in blocks (1 - 1008)"},
{"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"economical"}, "The fee estimate mode.\n"
+ FeeModesDetail(std::string("default mode will be used"))},
{"block_policy_only", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to use block policy estimator only.\n"}
},
RPCResult{
RPCResult::Type::OBJ, "", "",
@ -48,6 +50,7 @@ static RPCHelpMan estimatesmartfee()
{
{RPCResult::Type::STR, "", "error"},
}},
{RPCResult::Type::STR, "forecaster", /*optional=*/true, "the forecaster that provide the fee rate.\n"},
{RPCResult::Type::NUM, "blocks", "block number where estimate was found\n"
"The request target will be clamped between 2 and the highest target\n"
"fee estimation is able to return based on how long it has been running.\n"
@ -60,36 +63,63 @@ static RPCHelpMan estimatesmartfee()
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
CBlockPolicyEstimator& block_policy_fee_estimator = *(EnsureAnyForecasterMan(request.context).GetBlockPolicyEstimator());
FeeRateForecasterManager& forecaster_man = EnsureAnyForecasterMan(request.context);
const NodeContext& node = EnsureAnyNodeContext(request.context);
const CTxMemPool& mempool = EnsureMemPool(node);
CHECK_NONFATAL(mempool.m_opts.signals)->SyncWithValidationInterfaceQueue();
unsigned int max_target = block_policy_fee_estimator.MaximumTarget();
unsigned int max_target = forecaster_man.MaximumTarget();
unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
// Parse estimation mode
bool conservative = false;
if (!request.params[1].isNull()) {
FeeEstimateMode fee_mode;
if (!FeeModeFromString(request.params[1].get_str(), fee_mode)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, InvalidEstimateModeErrorMessage());
}
if (fee_mode == FeeEstimateMode::CONSERVATIVE) conservative = true;
conservative = (fee_mode == FeeEstimateMode::CONSERVATIVE);
}
UniValue result(UniValue::VOBJ);
UniValue errors(UniValue::VARR);
FeeCalculation feeCalc;
CFeeRate feeRate{block_policy_fee_estimator.estimateSmartFee(conf_target, &feeCalc, conservative)};
if (feeRate != CFeeRate(0)) {
CFeeRate min_mempool_feerate{mempool.GetMinFee()};
CFeeRate min_relay_feerate{mempool.m_opts.min_relay_feerate};
feeRate = std::max({feeRate, min_mempool_feerate, min_relay_feerate});
result.pushKV("feerate", ValueFromAmount(feeRate.GetFeePerK()));
} else {
errors.push_back("Insufficient data or no feerate found");
result.pushKV("errors", std::move(errors));
// Use block policy only if requested
bool use_block_policy_only = false;
if (!request.params[2].isNull()) {
use_block_policy_only = request.params[2].get_bool();
}
result.pushKV("blocks", feeCalc.returnedTarget);
CFeeRate feerate;
std::vector<std::string> errs;
if (use_block_policy_only) {
FeeCalculation feeCalc;
feerate = forecaster_man.GetBlockPolicyEstimator()->estimateSmartFee(conf_target, &feeCalc, conservative);
if (feerate == CFeeRate(0)) {
errors.push_back("Insufficient data or no feerate found");
}
result.pushKV("blocks", feeCalc.returnedTarget);
} else {
auto results = forecaster_man.ForecastFeeRateFromForecasters(conf_target, conservative);
feerate = CFeeRate(results.first.feerate.fee, results.first.feerate.size);
if (feerate != CFeeRate(0)) {
result.pushKV("forecaster", forecastTypeToString(results.first.forecaster));
}
result.pushKV("blocks", results.first.returned_target);
errs = results.second;
}
if (feerate != CFeeRate(0)) {
CFeeRate min_mempool_feerate = mempool.GetMinFee();
CFeeRate min_relay_feerate = mempool.m_opts.min_relay_feerate;
feerate = std::max({feerate, min_mempool_feerate, min_relay_feerate});
result.pushKV("feerate", ValueFromAmount(feerate.GetFeePerK()));
}
// Add any additional errors from forecasters
for (const auto& err : errs) {
errors.push_back(err);
}
result.pushKV("errors", std::move(errors));
return result;
},
};

View file

@ -88,11 +88,11 @@ def check_raw_estimates(node, fees_seen):
def check_smart_estimates(node, fees_seen):
"""Call estimatesmartfee and verify that the estimates meet certain invariants."""
"""Call estimatesmartfee a, "economical", Truend verify that the estimates meet certain invariants."""
delta = 1.0e-6 # account for rounding error
last_feerate = float(max(fees_seen))
all_smart_estimates = [node.estimatesmartfee(i) for i in range(1, 26)]
all_smart_estimates = [node.estimatesmartfee(i, "economical", True) for i in range(1, 26)]
mempoolMinFee = node.getmempoolinfo()["mempoolminfee"]
minRelaytxFee = node.getmempoolinfo()["minrelaytxfee"]
for i, e in enumerate(all_smart_estimates): # estimate is for i+1
@ -130,9 +130,9 @@ def make_tx(wallet, utxo, feerate):
)
def check_fee_estimates_btw_modes(node, expected_conservative, expected_economical):
fee_est_conservative = node.estimatesmartfee(1, estimate_mode="conservative")['feerate']
fee_est_economical = node.estimatesmartfee(1, estimate_mode="economical")['feerate']
fee_est_default = node.estimatesmartfee(1)['feerate']
fee_est_conservative = node.estimatesmartfee(1, estimate_mode="conservative", block_policy_only=True)['feerate']
fee_est_economical = node.estimatesmartfee(1, estimate_mode="economical", block_policy_only=True)['feerate']
fee_est_default = node.estimatesmartfee(1, estimate_mode="economical", block_policy_only=True)['feerate'] # Default is economical.
assert_equal(fee_est_conservative, expected_conservative)
assert_equal(fee_est_economical, expected_economical)
assert_equal(fee_est_default, expected_economical)
@ -239,7 +239,7 @@ class EstimateFeeTest(BitcoinTestFramework):
check_estimates(self.nodes[1], self.fees_per_kb)
def test_feerate_mempoolminfee(self):
high_val = 3 * self.nodes[1].estimatesmartfee(1)["feerate"]
high_val = 3 * self.nodes[1].estimatesmartfee(1, "economical", True)["feerate"]
self.restart_node(1, extra_args=[f"-minrelaytxfee={high_val}"])
check_estimates(self.nodes[1], self.fees_per_kb)
self.restart_node(1)
@ -300,16 +300,16 @@ class EstimateFeeTest(BitcoinTestFramework):
# Only 10% of the transactions were really confirmed with a low feerate,
# the rest needed to be RBF'd. We must return the 90% conf rate feerate.
high_feerate_kvb = Decimal(high_feerate) / COIN * 10 ** 3
est_feerate = node.estimatesmartfee(2)["feerate"]
est_feerate = node.estimatesmartfee(2, "economical", True)["feerate"]
assert_equal(est_feerate, high_feerate_kvb)
def test_old_fee_estimate_file(self):
# Get the initial fee rate while node is running
fee_rate = self.nodes[0].estimatesmartfee(1)["feerate"]
fee_rate = self.nodes[0].estimatesmartfee(1, "economical", True)["feerate"]
# Restart node to ensure fee_estimate.dat file is read
self.restart_node(0)
assert_equal(self.nodes[0].estimatesmartfee(1)["feerate"], fee_rate)
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", True)["feerate"], fee_rate)
fee_dat = self.nodes[0].chain_path / "fee_estimates.dat"
@ -320,7 +320,7 @@ class EstimateFeeTest(BitcoinTestFramework):
# Start node and ensure the fee_estimates.dat file was not read
self.start_node(0)
assert_equal(self.nodes[0].estimatesmartfee(1)["errors"], ["Insufficient data or no feerate found"])
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", True)["errors"], ["Insufficient data or no feerate found"])
def test_estimate_dat_is_flushed_periodically(self):
@ -377,7 +377,7 @@ class EstimateFeeTest(BitcoinTestFramework):
def test_acceptstalefeeestimates_option(self):
# Get the initial fee rate while node is running
fee_rate = self.nodes[0].estimatesmartfee(1)["feerate"]
fee_rate = self.nodes[0].estimatesmartfee(1, "economical", True)["feerate"]
self.stop_node(0)
@ -389,7 +389,7 @@ class EstimateFeeTest(BitcoinTestFramework):
# Restart node with -acceptstalefeeestimates option to ensure fee_estimate.dat file is read
self.start_node(0,extra_args=["-acceptstalefeeestimates"])
assert_equal(self.nodes[0].estimatesmartfee(1)["feerate"], fee_rate)
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", True)["feerate"], fee_rate)
def clear_estimates(self):
self.log.info("Restarting node with fresh estimation")
@ -400,7 +400,7 @@ class EstimateFeeTest(BitcoinTestFramework):
self.connect_nodes(0, 1)
self.connect_nodes(0, 2)
self.sync_blocks()
assert_equal(self.nodes[0].estimatesmartfee(1)["errors"], ["Insufficient data or no feerate found"])
assert_equal(self.nodes[0].estimatesmartfee(1, "economical", True)["errors"], ["Insufficient data or no feerate found"])
def broadcast_and_mine(self, broadcaster, miner, feerate, count):
"""Broadcast and mine some number of transactions with a specified fee rate."""

View file

@ -33,7 +33,7 @@ class EstimateFeeTest(BitcoinTestFramework):
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimaterawfee, 1, 'foo')
# extra params
assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', 1)
assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', True, 1)
assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee, 1, 1, 1)
# max value of 1008 per src/policy/fees.h