Browse Source

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
0.15
Wladimir J. van der Laan 7 years ago
parent
commit
b27b004532
No known key found for this signature in database
GPG Key ID: 1E4AED62986CD25D
  1. 1
      src/policy/feerate.h
  2. 33
      src/policy/fees.cpp
  3. 5
      src/policy/fees.h
  4. 1
      src/rpc/client.cpp
  5. 125
      src/rpc/mining.cpp

1
src/policy/feerate.h

@ -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;

33
src/policy/fees.cpp

@ -16,6 +16,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"},
@ -685,7 +698,7 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
break; break;
} }
default: { 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); 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;

5
src/policy/fees.h

@ -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,
@ -223,6 +225,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;

1
src/rpc/client.cpp

@ -117,7 +117,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, "dummy" }, { "prioritisetransaction", 1, "dummy" },
{ "prioritisetransaction", 2, "fee_delta" }, { "prioritisetransaction", 2, "fee_delta" },
{ "setban", 2, "bantime" }, { "setban", 2, "bantime" },

125
src/rpc/mining.cpp

@ -838,9 +838,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"
@ -849,72 +849,95 @@ UniValue estimaterawfee(const JSONRPCRequest& request)
"confirmation within nblocks blocks if possible. Uses virtual transaction size as defined\n" "confirmation within nblocks blocks if possible. Uses virtual transaction size as defined\n"
"in BIP 141 (witness data is discounted).\n" "in BIP 141 (witness data is discounted).\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();
if (nBlocks < 1 || (unsigned int)nBlocks > ::feeEstimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid nblocks");
}
double threshold = 0.95; double threshold = 0.95;
if (!request.params[1].isNull()) if (!request.params[1].isNull()) {
threshold = request.params[1].get_real(); 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 (threshold < 0 || threshold > 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid threshold");
}
UniValue result(UniValue::VOBJ); 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()))); for (FeeEstimateHorizon horizon : {FeeEstimateHorizon::SHORT_HALFLIFE, FeeEstimateHorizon::MED_HALFLIFE, FeeEstimateHorizon::LONG_HALFLIFE}) {
result.push_back(Pair("decay", buckets.decay)); CFeeRate feeRate;
result.push_back(Pair("scale", (int)buckets.scale)); EstimationResult buckets;
UniValue passbucket(UniValue::VOBJ);
passbucket.push_back(Pair("startrange", round(buckets.pass.start))); // Only output results for horizons which track the target
passbucket.push_back(Pair("endrange", round(buckets.pass.end))); if ((unsigned int)nBlocks > ::feeEstimator.HighestTargetTracked(horizon)) continue;
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)); feeRate = ::feeEstimator.estimateRawFee(nBlocks, threshold, horizon, &buckets);
passbucket.push_back(Pair("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0)); UniValue horizon_result(UniValue::VOBJ);
passbucket.push_back(Pair("leftmempool", round(buckets.pass.leftMempool * 100.0) / 100.0)); UniValue errors(UniValue::VARR);
result.push_back(Pair("pass", passbucket)); UniValue passbucket(UniValue::VOBJ);
UniValue failbucket(UniValue::VOBJ); passbucket.push_back(Pair("startrange", round(buckets.pass.start)));
failbucket.push_back(Pair("startrange", round(buckets.fail.start))); passbucket.push_back(Pair("endrange", round(buckets.pass.end)));
failbucket.push_back(Pair("endrange", round(buckets.fail.end))); passbucket.push_back(Pair("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0));
failbucket.push_back(Pair("withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0)); passbucket.push_back(Pair("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0));
failbucket.push_back(Pair("totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0)); passbucket.push_back(Pair("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0));
failbucket.push_back(Pair("inmempool", round(buckets.fail.inMempool * 100.0) / 100.0)); passbucket.push_back(Pair("leftmempool", round(buckets.pass.leftMempool * 100.0) / 100.0));
failbucket.push_back(Pair("leftmempool", round(buckets.fail.leftMempool * 100.0) / 100.0)); UniValue failbucket(UniValue::VOBJ);
result.push_back(Pair("fail", failbucket)); 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;
} }
@ -932,7 +955,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)

Loading…
Cancel
Save