Merge #10543: Change API to estimaterawfee
5e3b7b5
Improve error reporting for estimaterawfee (Alex Morcos)1fafd70
Add function to report highest estimate target tracked per horizon (Alex Morcos)9c85b91
Change API to estimaterawfee (Alex Morcos) Tree-SHA512: e624c6e7967e9e48abe49f5818bd674e5710e571cc093029d2f90d39fdfba3c1f30e83bf89f6dce97052b59a7d9636a64642ccfb26effd149c417d0afbed0c0b
This commit is contained in:
commit
b27b004532
5 changed files with 115 additions and 56 deletions
|
@ -40,6 +40,7 @@ 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; }
|
||||
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; }
|
||||
std::string ToString() const;
|
||||
|
||||
|
|
|
@ -16,6 +16,19 @@
|
|||
|
||||
static constexpr double INF_FEERATE = 1e99;
|
||||
|
||||
std::string StringForFeeEstimateHorizon(FeeEstimateHorizon horizon) {
|
||||
static const std::map<FeeEstimateHorizon, std::string> horizon_strings = {
|
||||
{FeeEstimateHorizon::SHORT_HALFLIFE, "short"},
|
||||
{FeeEstimateHorizon::MED_HALFLIFE, "medium"},
|
||||
{FeeEstimateHorizon::LONG_HALFLIFE, "long"},
|
||||
};
|
||||
auto horizon_string = horizon_strings.find(horizon);
|
||||
|
||||
if (horizon_string == horizon_strings.end()) return "unknown";
|
||||
|
||||
return horizon_string->second;
|
||||
}
|
||||
|
||||
std::string StringForFeeReason(FeeReason reason) {
|
||||
static const std::map<FeeReason, std::string> fee_reason_strings = {
|
||||
{FeeReason::NONE, "None"},
|
||||
|
@ -685,7 +698,7 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
|
|||
break;
|
||||
}
|
||||
default: {
|
||||
return CFeeRate(0);
|
||||
throw std::out_of_range("CBlockPoicyEstimator::estimateRawFee unknown FeeEstimateHorizon");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -704,6 +717,24 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
|
|||
return CFeeRate(median);
|
||||
}
|
||||
|
||||
unsigned int CBlockPolicyEstimator::HighestTargetTracked(FeeEstimateHorizon horizon) const
|
||||
{
|
||||
switch (horizon) {
|
||||
case FeeEstimateHorizon::SHORT_HALFLIFE: {
|
||||
return shortStats->GetMaxConfirms();
|
||||
}
|
||||
case FeeEstimateHorizon::MED_HALFLIFE: {
|
||||
return feeStats->GetMaxConfirms();
|
||||
}
|
||||
case FeeEstimateHorizon::LONG_HALFLIFE: {
|
||||
return longStats->GetMaxConfirms();
|
||||
}
|
||||
default: {
|
||||
throw std::out_of_range("CBlockPoicyEstimator::HighestTargetTracked unknown FeeEstimateHorizon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int CBlockPolicyEstimator::BlockSpan() const
|
||||
{
|
||||
if (firstRecordedHeight == 0) return 0;
|
||||
|
|
|
@ -74,6 +74,8 @@ enum FeeEstimateHorizon {
|
|||
LONG_HALFLIFE = 2
|
||||
};
|
||||
|
||||
std::string StringForFeeEstimateHorizon(FeeEstimateHorizon horizon);
|
||||
|
||||
/* Enumeration of reason for returned fee estimate */
|
||||
enum class FeeReason {
|
||||
NONE,
|
||||
|
@ -223,6 +225,9 @@ public:
|
|||
/** Empty mempool transactions on shutdown to record failure to confirm for txs still in mempool */
|
||||
void FlushUnconfirmed(CTxMemPool& pool);
|
||||
|
||||
/** Calculation of highest target that estimates are tracked for */
|
||||
unsigned int HighestTargetTracked(FeeEstimateHorizon horizon) const;
|
||||
|
||||
private:
|
||||
unsigned int nBestSeenHeight;
|
||||
unsigned int firstRecordedHeight;
|
||||
|
|
|
@ -117,7 +117,6 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "estimatesmartfee", 1, "conservative" },
|
||||
{ "estimaterawfee", 0, "nblocks" },
|
||||
{ "estimaterawfee", 1, "threshold" },
|
||||
{ "estimaterawfee", 2, "horizon" },
|
||||
{ "prioritisetransaction", 1, "dummy" },
|
||||
{ "prioritisetransaction", 2, "fee_delta" },
|
||||
{ "setban", 2, "bantime" },
|
||||
|
|
|
@ -838,9 +838,9 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
|
|||
|
||||
UniValue estimaterawfee(const JSONRPCRequest& request)
|
||||
{
|
||||
if (request.fHelp || request.params.size() < 1|| request.params.size() > 3)
|
||||
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
|
||||
throw std::runtime_error(
|
||||
"estimaterawfee nblocks (threshold horizon)\n"
|
||||
"estimaterawfee nblocks (threshold)\n"
|
||||
"\nWARNING: This interface is unstable and may disappear or change!\n"
|
||||
"\nWARNING: This is an advanced API call that is tightly coupled to the specific\n"
|
||||
" implementation of fee estimation. The parameters it can be called with\n"
|
||||
|
@ -849,72 +849,95 @@ UniValue estimaterawfee(const JSONRPCRequest& request)
|
|||
"confirmation within nblocks blocks if possible. Uses virtual transaction size as defined\n"
|
||||
"in BIP 141 (witness data is discounted).\n"
|
||||
"\nArguments:\n"
|
||||
"1. nblocks (numeric)\n"
|
||||
"1. nblocks (numeric) Confirmation target in blocks (1 - 1008)\n"
|
||||
"2. threshold (numeric, optional) The proportion of transactions in a given feerate range that must have been\n"
|
||||
" confirmed within nblocks in order to consider those feerates as high enough and proceed to check\n"
|
||||
" lower buckets. Default: 0.95\n"
|
||||
"3. horizon (numeric, optional) How long a history of estimates to consider. 0=short, 1=medium, 2=long.\n"
|
||||
" Default: 1\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
|
||||
" \"decay\" : x.x, (numeric) exponential decay (per block) for historical moving average of confirmation data\n"
|
||||
" \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n"
|
||||
" \"pass\" : { (json object) information about the lowest range of feerates to succeed in meeting the threshold\n"
|
||||
" \"startrange\" : x.x, (numeric) start of feerate range\n"
|
||||
" \"endrange\" : x.x, (numeric) end of feerate range\n"
|
||||
" \"withintarget\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed within target\n"
|
||||
" \"totalconfirmed\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed at any point\n"
|
||||
" \"inmempool\" : x.x, (numeric) current number of txs in mempool in the feerate range unconfirmed for at least target blocks\n"
|
||||
" \"leftmempool\" : x.x, (numeric) number of txs over history horizon in the feerate range that left mempool unconfirmed after target\n"
|
||||
" }\n"
|
||||
" \"fail\" : { ... } (json object) information about the highest range of feerates to fail to meet the threshold\n"
|
||||
" \"short\" : { (json object, optional) estimate for short time horizon\n"
|
||||
" \"feerate\" : x.x, (numeric, optional) estimate fee-per-kilobyte (in BTC)\n"
|
||||
" \"decay\" : x.x, (numeric) exponential decay (per block) for historical moving average of confirmation data\n"
|
||||
" \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n"
|
||||
" \"pass\" : { (json object, optional) information about the lowest range of feerates to succeed in meeting the threshold\n"
|
||||
" \"startrange\" : x.x, (numeric) start of feerate range\n"
|
||||
" \"endrange\" : x.x, (numeric) end of feerate range\n"
|
||||
" \"withintarget\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed within target\n"
|
||||
" \"totalconfirmed\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed at any point\n"
|
||||
" \"inmempool\" : x.x, (numeric) current number of txs in mempool in the feerate range unconfirmed for at least target blocks\n"
|
||||
" \"leftmempool\" : x.x, (numeric) number of txs over history horizon in the feerate range that left mempool unconfirmed after target\n"
|
||||
" },\n"
|
||||
" \"fail\" : { ... }, (json object, optional) information about the highest range of feerates to fail to meet the threshold\n"
|
||||
" \"errors\": [ str... ] (json array of strings, optional) Errors encountered during processing\n"
|
||||
" },\n"
|
||||
" \"medium\" : { ... }, (json object, optional) estimate for medium time horizon\n"
|
||||
" \"long\" : { ... } (json object) estimate for long time horizon\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"A negative feerate is returned if no answer can be given.\n"
|
||||
"Results are returned for any horizon which tracks blocks up to the confirmation target.\n"
|
||||
"\nExample:\n"
|
||||
+ HelpExampleCli("estimaterawfee", "6 0.9 1")
|
||||
+ HelpExampleCli("estimaterawfee", "6 0.9")
|
||||
);
|
||||
|
||||
RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM, UniValue::VNUM}, true);
|
||||
RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
|
||||
int nBlocks = request.params[0].get_int();
|
||||
double threshold = 0.95;
|
||||
if (!request.params[1].isNull())
|
||||
threshold = request.params[1].get_real();
|
||||
FeeEstimateHorizon horizon = FeeEstimateHorizon::MED_HALFLIFE;
|
||||
if (!request.params[2].isNull()) {
|
||||
int horizonInt = request.params[2].get_int();
|
||||
if (horizonInt < 0 || horizonInt > 2) {
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid horizon for fee estimates");
|
||||
} else {
|
||||
horizon = (FeeEstimateHorizon)horizonInt;
|
||||
}
|
||||
if (nBlocks < 1 || (unsigned int)nBlocks > ::feeEstimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE)) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid nblocks");
|
||||
}
|
||||
double threshold = 0.95;
|
||||
if (!request.params[1].isNull()) {
|
||||
threshold = request.params[1].get_real();
|
||||
}
|
||||
if (threshold < 0 || threshold > 1) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid threshold");
|
||||
}
|
||||
UniValue result(UniValue::VOBJ);
|
||||
CFeeRate feeRate;
|
||||
EstimationResult buckets;
|
||||
feeRate = ::feeEstimator.estimateRawFee(nBlocks, threshold, horizon, &buckets);
|
||||
|
||||
result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
|
||||
result.push_back(Pair("decay", buckets.decay));
|
||||
result.push_back(Pair("scale", (int)buckets.scale));
|
||||
UniValue passbucket(UniValue::VOBJ);
|
||||
passbucket.push_back(Pair("startrange", round(buckets.pass.start)));
|
||||
passbucket.push_back(Pair("endrange", round(buckets.pass.end)));
|
||||
passbucket.push_back(Pair("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0));
|
||||
passbucket.push_back(Pair("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0));
|
||||
passbucket.push_back(Pair("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0));
|
||||
passbucket.push_back(Pair("leftmempool", round(buckets.pass.leftMempool * 100.0) / 100.0));
|
||||
result.push_back(Pair("pass", passbucket));
|
||||
UniValue failbucket(UniValue::VOBJ);
|
||||
failbucket.push_back(Pair("startrange", round(buckets.fail.start)));
|
||||
failbucket.push_back(Pair("endrange", round(buckets.fail.end)));
|
||||
failbucket.push_back(Pair("withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0));
|
||||
failbucket.push_back(Pair("totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0));
|
||||
failbucket.push_back(Pair("inmempool", round(buckets.fail.inMempool * 100.0) / 100.0));
|
||||
failbucket.push_back(Pair("leftmempool", round(buckets.fail.leftMempool * 100.0) / 100.0));
|
||||
result.push_back(Pair("fail", failbucket));
|
||||
UniValue result(UniValue::VOBJ);
|
||||
|
||||
for (FeeEstimateHorizon horizon : {FeeEstimateHorizon::SHORT_HALFLIFE, FeeEstimateHorizon::MED_HALFLIFE, FeeEstimateHorizon::LONG_HALFLIFE}) {
|
||||
CFeeRate feeRate;
|
||||
EstimationResult buckets;
|
||||
|
||||
// Only output results for horizons which track the target
|
||||
if ((unsigned int)nBlocks > ::feeEstimator.HighestTargetTracked(horizon)) continue;
|
||||
|
||||
feeRate = ::feeEstimator.estimateRawFee(nBlocks, threshold, horizon, &buckets);
|
||||
UniValue horizon_result(UniValue::VOBJ);
|
||||
UniValue errors(UniValue::VARR);
|
||||
UniValue passbucket(UniValue::VOBJ);
|
||||
passbucket.push_back(Pair("startrange", round(buckets.pass.start)));
|
||||
passbucket.push_back(Pair("endrange", round(buckets.pass.end)));
|
||||
passbucket.push_back(Pair("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0));
|
||||
passbucket.push_back(Pair("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0));
|
||||
passbucket.push_back(Pair("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0));
|
||||
passbucket.push_back(Pair("leftmempool", round(buckets.pass.leftMempool * 100.0) / 100.0));
|
||||
UniValue failbucket(UniValue::VOBJ);
|
||||
failbucket.push_back(Pair("startrange", round(buckets.fail.start)));
|
||||
failbucket.push_back(Pair("endrange", round(buckets.fail.end)));
|
||||
failbucket.push_back(Pair("withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0));
|
||||
failbucket.push_back(Pair("totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0));
|
||||
failbucket.push_back(Pair("inmempool", round(buckets.fail.inMempool * 100.0) / 100.0));
|
||||
failbucket.push_back(Pair("leftmempool", round(buckets.fail.leftMempool * 100.0) / 100.0));
|
||||
|
||||
// CFeeRate(0) is used to indicate error as a return value from estimateRawFee
|
||||
if (feeRate != CFeeRate(0)) {
|
||||
horizon_result.push_back(Pair("feerate", ValueFromAmount(feeRate.GetFeePerK())));
|
||||
horizon_result.push_back(Pair("decay", buckets.decay));
|
||||
horizon_result.push_back(Pair("scale", (int)buckets.scale));
|
||||
horizon_result.push_back(Pair("pass", passbucket));
|
||||
// buckets.fail.start == -1 indicates that all buckets passed, there is no fail bucket to output
|
||||
if (buckets.fail.start != -1) horizon_result.push_back(Pair("fail", failbucket));
|
||||
} else {
|
||||
// Output only information that is still meaningful in the event of error
|
||||
horizon_result.push_back(Pair("decay", buckets.decay));
|
||||
horizon_result.push_back(Pair("scale", (int)buckets.scale));
|
||||
horizon_result.push_back(Pair("fail", failbucket));
|
||||
errors.push_back("Insufficient data or no feerate found which meets threshold");
|
||||
horizon_result.push_back(Pair("errors",errors));
|
||||
}
|
||||
result.push_back(Pair(StringForFeeEstimateHorizon(horizon), horizon_result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -932,7 +955,7 @@ static const CRPCCommand commands[] =
|
|||
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} },
|
||||
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} },
|
||||
|
||||
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} },
|
||||
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold"} },
|
||||
};
|
||||
|
||||
void RegisterMiningRPCCommands(CRPCTable &t)
|
||||
|
|
Loading…
Reference in a new issue