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:
Wladimir J. van der Laan 2017-07-11 15:28:28 +02:00 committed by Pasta
parent 50ec763075
commit e789608728
No known key found for this signature in database
GPG Key ID: D362C9F7142766AE
5 changed files with 115 additions and 56 deletions

View File

@ -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; }
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; } CFeeRate& operator+=(const CFeeRate& a) { nSatoshisPerK += a.nSatoshisPerK; return *this; }
std::string ToString() const; std::string ToString() const;

View File

@ -15,6 +15,19 @@
static constexpr double INF_FEERATE = 1e99; 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) { std::string StringForFeeReason(FeeReason reason) {
static const std::map<FeeReason, std::string> fee_reason_strings = { static const std::map<FeeReason, std::string> fee_reason_strings = {
{FeeReason::NONE, "None"}, {FeeReason::NONE, "None"},
@ -669,7 +682,7 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
break; break;
} }
default: { default: {
return CFeeRate(0); throw std::out_of_range("CBlockPoicyEstimator::estimateRawFee unknown FeeEstimateHorizon");
} }
} }
@ -688,6 +701,24 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
return CFeeRate(median); 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 unsigned int CBlockPolicyEstimator::BlockSpan() const
{ {
if (firstRecordedHeight == 0) return 0; if (firstRecordedHeight == 0) return 0;

View File

@ -74,6 +74,8 @@ enum FeeEstimateHorizon {
LONG_HALFLIFE = 2 LONG_HALFLIFE = 2
}; };
std::string StringForFeeEstimateHorizon(FeeEstimateHorizon horizon);
/* Enumeration of reason for returned fee estimate */ /* Enumeration of reason for returned fee estimate */
enum class FeeReason { enum class FeeReason {
NONE, NONE,
@ -214,6 +216,9 @@ public:
/** Empty mempool transactions on shutdown to record failure to confirm for txs still in mempool */ /** Empty mempool transactions on shutdown to record failure to confirm for txs still in mempool */
void FlushUnconfirmed(CTxMemPool& pool); void FlushUnconfirmed(CTxMemPool& pool);
/** Calculation of highest target that estimates are tracked for */
unsigned int HighestTargetTracked(FeeEstimateHorizon horizon) const;
private: private:
unsigned int nBestSeenHeight; unsigned int nBestSeenHeight;
unsigned int firstRecordedHeight; unsigned int firstRecordedHeight;

View File

@ -140,7 +140,6 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "estimatesmartfee", 1, "conservative" }, { "estimatesmartfee", 1, "conservative" },
{ "estimaterawfee", 0, "nblocks" }, { "estimaterawfee", 0, "nblocks" },
{ "estimaterawfee", 1, "threshold" }, { "estimaterawfee", 1, "threshold" },
{ "estimaterawfee", 2, "horizon" },
{ "prioritisetransaction", 1, "fee_delta" }, { "prioritisetransaction", 1, "fee_delta" },
{ "setban", 2, "bantime" }, { "setban", 2, "bantime" },
{ "setban", 3, "absolute" }, { "setban", 3, "absolute" },

View File

@ -865,9 +865,9 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
UniValue estimaterawfee(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( 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 interface is unstable and may disappear or change!\n"
"\nWARNING: This is an advanced API call that is tightly coupled to the specific\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" " implementation of fee estimation. The parameters it can be called with\n"
@ -875,72 +875,95 @@ UniValue estimaterawfee(const JSONRPCRequest& request)
"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n" "\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
"confirmation within nblocks blocks if possible.\n" "confirmation within nblocks blocks if possible.\n"
"\nArguments:\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" "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" " confirmed within nblocks in order to consider those feerates as high enough and proceed to check\n"
" lower buckets. Default: 0.95\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" "\nResult:\n"
"{\n" "{\n"
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n" " \"short\" : { (json object, optional) estimate for short time horizon\n"
" \"decay\" : x.x, (numeric) exponential decay (per block) for historical moving average of confirmation data\n" " \"feerate\" : x.x, (numeric, optional) estimate fee-per-kilobyte (in BTC)\n"
" \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n" " \"decay\" : x.x, (numeric) exponential decay (per block) for historical moving average of confirmation data\n"
" \"pass\" : { (json object) information about the lowest range of feerates to succeed in meeting the threshold\n" " \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n"
" \"startrange\" : x.x, (numeric) start of feerate range\n" " \"pass\" : { (json object, optional) information about the lowest range of feerates to succeed in meeting the threshold\n"
" \"endrange\" : x.x, (numeric) end of feerate range\n" " \"startrange\" : x.x, (numeric) start of feerate range\n"
" \"withintarget\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed within target\n" " \"endrange\" : x.x, (numeric) end of feerate range\n"
" \"totalconfirmed\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed at any point\n" " \"withintarget\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed within target\n"
" \"inmempool\" : x.x, (numeric) current number of txs in mempool in the feerate range unconfirmed for at least target blocks\n" " \"totalconfirmed\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed at any point\n"
" \"leftmempool\" : x.x, (numeric) number of txs over history horizon in the feerate range that left mempool unconfirmed after target\n" " \"inmempool\" : x.x, (numeric) current number of txs in mempool in the feerate range unconfirmed for at least target blocks\n"
" }\n" " \"leftmempool\" : x.x, (numeric) number of txs over history horizon in the feerate range that left mempool unconfirmed after target\n"
" \"fail\" : { ... } (json object) information about the highest range of feerates to fail to meet the threshold\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"
"\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" "\nExample:\n"
+ HelpExampleCli("estimaterawfee", "6 0.9 1") + HelpExampleCli("estimaterawfee", "6 0.9")
); );
RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM, UniValue::VNUM}, true); RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM, UniValue::VNUM}, true);
RPCTypeCheckArgument(request.params[0], UniValue::VNUM); RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
int nBlocks = request.params[0].get_int(); int nBlocks = request.params[0].get_int();
double threshold = 0.95; if (nBlocks < 1 || (unsigned int)nBlocks > ::feeEstimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE)) {
if (!request.params[1].isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid nblocks");
threshold = request.params[1].get_real(); }
FeeEstimateHorizon horizon = FeeEstimateHorizon::MED_HALFLIFE; double threshold = 0.95;
if (!request.params[2].isNull()) { if (!request.params[1].isNull()) {
int horizonInt = request.params[2].get_int(); threshold = request.params[1].get_real();
if (horizonInt < 0 || horizonInt > 2) { }
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid horizon for fee estimates"); if (threshold < 0 || threshold > 1) {
} else { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid threshold");
horizon = (FeeEstimateHorizon)horizonInt;
}
} }
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()))); UniValue result(UniValue::VOBJ);
result.push_back(Pair("decay", buckets.decay));
result.push_back(Pair("scale", (int)buckets.scale)); for (FeeEstimateHorizon horizon : {FeeEstimateHorizon::SHORT_HALFLIFE, FeeEstimateHorizon::MED_HALFLIFE, FeeEstimateHorizon::LONG_HALFLIFE}) {
UniValue passbucket(UniValue::VOBJ); CFeeRate feeRate;
passbucket.push_back(Pair("startrange", round(buckets.pass.start))); EstimationResult buckets;
passbucket.push_back(Pair("endrange", round(buckets.pass.end)));
passbucket.push_back(Pair("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0)); // Only output results for horizons which track the target
passbucket.push_back(Pair("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0)); if ((unsigned int)nBlocks > ::feeEstimator.HighestTargetTracked(horizon)) continue;
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)); feeRate = ::feeEstimator.estimateRawFee(nBlocks, threshold, horizon, &buckets);
result.push_back(Pair("pass", passbucket)); UniValue horizon_result(UniValue::VOBJ);
UniValue failbucket(UniValue::VOBJ); UniValue errors(UniValue::VARR);
failbucket.push_back(Pair("startrange", round(buckets.fail.start))); UniValue passbucket(UniValue::VOBJ);
failbucket.push_back(Pair("endrange", round(buckets.fail.end))); passbucket.push_back(Pair("startrange", round(buckets.pass.start)));
failbucket.push_back(Pair("withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0)); passbucket.push_back(Pair("endrange", round(buckets.pass.end)));
failbucket.push_back(Pair("totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0)); passbucket.push_back(Pair("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0));
failbucket.push_back(Pair("inmempool", round(buckets.fail.inMempool * 100.0) / 100.0)); passbucket.push_back(Pair("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0));
failbucket.push_back(Pair("leftmempool", round(buckets.fail.leftMempool * 100.0) / 100.0)); passbucket.push_back(Pair("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0));
result.push_back(Pair("fail", failbucket)); 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; return result;
} }
@ -959,7 +982,7 @@ static const CRPCCommand commands[] =
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} }, { "util", "estimatefee", &estimatefee, true, {"nblocks"} },
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} }, { "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} },
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} }, { "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold"} },
}; };
void RegisterMiningRPCCommands(CRPCTable &t) void RegisterMiningRPCCommands(CRPCTable &t)