Skip to content

Commit 4186d3f

Browse files
committed
Expose estimaterawfee
Track information the ranges of fee rates that were used to calculate the fee estimates (the last range of fee rates in which the data points met the threshold and the first to fail) and provide an RPC call to return this information.
1 parent 2681153 commit 4186d3f

File tree

4 files changed

+192
-11
lines changed

4 files changed

+192
-11
lines changed

src/policy/fees.cpp

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ class TxConfirmStats
107107
* @param nBlockHeight the current block height
108108
*/
109109
double EstimateMedianVal(int confTarget, double sufficientTxVal,
110-
double minSuccess, bool requireGreater, unsigned int nBlockHeight) const;
110+
double minSuccess, bool requireGreater, unsigned int nBlockHeight,
111+
EstimationResult *result = nullptr) const;
111112

112113
/** Return the max number of confirms we're tracking */
113114
unsigned int GetMaxConfirms() const { return confAvg.size(); }
@@ -186,7 +187,7 @@ void TxConfirmStats::UpdateMovingAverages()
186187
// returns -1 on error conditions
187188
double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
188189
double successBreakPoint, bool requireGreater,
189-
unsigned int nBlockHeight) const
190+
unsigned int nBlockHeight, EstimationResult *result) const
190191
{
191192
// Counters for a bucket (or range of buckets)
192193
double nConf = 0; // Number of tx's confirmed within the confTarget
@@ -215,6 +216,9 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
215216
bool foundAnswer = false;
216217
unsigned int bins = unconfTxs.size();
217218
bool newBucketRange = true;
219+
bool passing = true;
220+
EstimatorBucket passBucket;
221+
EstimatorBucket failBucket;
218222

219223
// Start counting from highest(default) or lowest feerate transactions
220224
for (int bucket = startbucket; bucket >= 0 && bucket <= maxbucketindex; bucket += step) {
@@ -237,14 +241,30 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
237241

238242
// Check to see if we are no longer getting confirmed at the success rate
239243
if ((requireGreater && curPct < successBreakPoint) || (!requireGreater && curPct > successBreakPoint)) {
244+
if (passing == true) {
245+
// First time we hit a failure record the failed bucket
246+
unsigned int failMinBucket = std::min(curNearBucket, curFarBucket);
247+
unsigned int failMaxBucket = std::max(curNearBucket, curFarBucket);
248+
failBucket.start = failMinBucket ? buckets[failMinBucket - 1] : 0;
249+
failBucket.end = buckets[failMaxBucket];
250+
failBucket.withinTarget = nConf;
251+
failBucket.totalConfirmed = totalNum;
252+
failBucket.inMempool = extraNum;
253+
passing = false;
254+
}
240255
continue;
241256
}
242257
// Otherwise update the cumulative stats, and the bucket variables
243258
// and reset the counters
244259
else {
260+
failBucket = EstimatorBucket(); // Reset any failed bucket, currently passing
245261
foundAnswer = true;
262+
passing = true;
263+
passBucket.withinTarget = nConf;
246264
nConf = 0;
265+
passBucket.totalConfirmed = totalNum;
247266
totalNum = 0;
267+
passBucket.inMempool = extraNum;
248268
extraNum = 0;
249269
bestNearBucket = curNearBucket;
250270
bestFarBucket = curFarBucket;
@@ -260,8 +280,8 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
260280
// Find the bucket with the median transaction and then report the average feerate from that bucket
261281
// This is a compromise between finding the median which we can't since we don't save all tx's
262282
// and reporting the average which is less accurate
263-
unsigned int minBucket = bestNearBucket < bestFarBucket ? bestNearBucket : bestFarBucket;
264-
unsigned int maxBucket = bestNearBucket > bestFarBucket ? bestNearBucket : bestFarBucket;
283+
unsigned int minBucket = std::min(bestNearBucket, bestFarBucket);
284+
unsigned int maxBucket = std::max(bestNearBucket, bestFarBucket);
265285
for (unsigned int j = minBucket; j <= maxBucket; j++) {
266286
txSum += txCtAvg[j];
267287
}
@@ -275,13 +295,37 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
275295
break;
276296
}
277297
}
298+
299+
passBucket.start = minBucket ? buckets[minBucket-1] : 0;
300+
passBucket.end = buckets[maxBucket];
301+
}
302+
303+
// If we were passing until we reached last few buckets with insufficient data, then report those as failed
304+
if (passing && !newBucketRange) {
305+
unsigned int failMinBucket = std::min(curNearBucket, curFarBucket);
306+
unsigned int failMaxBucket = std::max(curNearBucket, curFarBucket);
307+
failBucket.start = failMinBucket ? buckets[failMinBucket - 1] : 0;
308+
failBucket.end = buckets[failMaxBucket];
309+
failBucket.withinTarget = nConf;
310+
failBucket.totalConfirmed = totalNum;
311+
failBucket.inMempool = extraNum;
278312
}
279313

280-
LogPrint(BCLog::ESTIMATEFEE, "%3d: For conf success %s %4.2f need feerate %s: %12.5g from buckets %8g - %8g Cur Bucket stats %6.2f%% %8.1f/(%.1f+%d mempool)\n",
281-
confTarget, requireGreater ? ">" : "<", successBreakPoint,
282-
requireGreater ? ">" : "<", median, buckets[minBucket], buckets[maxBucket],
283-
100 * nConf / (totalNum + extraNum), nConf, totalNum, extraNum);
314+
LogPrint(BCLog::ESTIMATEFEE, "FeeEst: %d %s%.0f%% decay %.5f: need feerate: %g from (%g - %g) %.2f%% %.1f/(%.1f+%d mem) Fail: (%g - %g) %.2f%% %.1f/(%.1f+%d mem)\n",
315+
confTarget, requireGreater ? ">" : "<", 100.0 * successBreakPoint, decay,
316+
median, passBucket.start, passBucket.end,
317+
100 * passBucket.withinTarget / (passBucket.totalConfirmed + passBucket.inMempool),
318+
passBucket.withinTarget, passBucket.totalConfirmed, passBucket.inMempool,
319+
failBucket.start, failBucket.end,
320+
100 * failBucket.withinTarget / (failBucket.totalConfirmed + failBucket.inMempool),
321+
failBucket.withinTarget, failBucket.totalConfirmed, failBucket.inMempool);
284322

323+
324+
if (result) {
325+
result->pass = passBucket;
326+
result->fail = failBucket;
327+
result->decay = decay;
328+
}
285329
return median;
286330
}
287331

@@ -537,13 +581,44 @@ void CBlockPolicyEstimator::processBlock(unsigned int nBlockHeight,
537581

538582
CFeeRate CBlockPolicyEstimator::estimateFee(int confTarget) const
539583
{
584+
// It's not possible to get reasonable estimates for confTarget of 1
585+
if (confTarget <= 1)
586+
return CFeeRate(0);
587+
588+
return estimateRawFee(confTarget, DOUBLE_SUCCESS_PCT, FeeEstimateHorizon::MED_HALFLIFE);
589+
}
590+
591+
CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult* result) const
592+
{
593+
TxConfirmStats* stats;
594+
double sufficientTxs = SUFFICIENT_FEETXS;
595+
switch (horizon) {
596+
case FeeEstimateHorizon::SHORT_HALFLIFE: {
597+
stats = shortStats;
598+
sufficientTxs = SUFFICIENT_TXS_SHORT;
599+
break;
600+
}
601+
case FeeEstimateHorizon::MED_HALFLIFE: {
602+
stats = feeStats;
603+
break;
604+
}
605+
case FeeEstimateHorizon::LONG_HALFLIFE: {
606+
stats = longStats;
607+
break;
608+
}
609+
default: {
610+
return CFeeRate(0);
611+
}
612+
}
613+
540614
LOCK(cs_feeEstimator);
541615
// Return failure if trying to analyze a target we're not tracking
542-
// It's not possible to get reasonable estimates for confTarget of 1
543-
if (confTarget <= 1 || (unsigned int)confTarget > feeStats->GetMaxConfirms())
616+
if (confTarget <= 0 || (unsigned int)confTarget > stats->GetMaxConfirms())
617+
return CFeeRate(0);
618+
if (successThreshold > 1)
544619
return CFeeRate(0);
545620

546-
double median = feeStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
621+
double median = stats->EstimateMedianVal(confTarget, sufficientTxs, successThreshold, true, nBestSeenHeight, result);
547622

548623
if (median < 0)
549624
return CFeeRate(0);

src/policy/fees.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ class TxConfirmStats;
6161
* they've been outstanding.
6262
*/
6363

64+
enum FeeEstimateHorizon {
65+
SHORT_HALFLIFE = 0,
66+
MED_HALFLIFE = 1,
67+
LONG_HALFLIFE = 2
68+
};
69+
70+
struct EstimatorBucket
71+
{
72+
double start = -1;
73+
double end = -1;
74+
double withinTarget = 0;
75+
double totalConfirmed = 0;
76+
double inMempool = 0;
77+
};
78+
79+
struct EstimationResult
80+
{
81+
EstimatorBucket pass;
82+
EstimatorBucket fail;
83+
double decay;
84+
};
85+
6486
/**
6587
* We want to be able to estimate feerates that are needed on tx's to be included in
6688
* a certain number of blocks. Every time a block is added to the best chain, this class records
@@ -90,6 +112,8 @@ class CBlockPolicyEstimator
90112

91113
/** Require an avg of 0.1 tx in the combined feerate bucket per block to have stat significance */
92114
static constexpr double SUFFICIENT_FEETXS = 0.1;
115+
/** Require an avg of 0.5 tx when using short decay since there are fewer blocks considered*/
116+
static constexpr double SUFFICIENT_TXS_SHORT = 0.5;
93117

94118
/** Minimum and Maximum values for tracking feerates
95119
* The MIN_BUCKET_FEERATE should just be set to the lowest reasonable feerate we
@@ -132,6 +156,10 @@ class CBlockPolicyEstimator
132156
*/
133157
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const;
134158

159+
/** Return a specific fee estimate calculation with a given success threshold and time horizon.
160+
*/
161+
CFeeRate estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult *result = nullptr) const;
162+
135163
/** Write estimation data to a file */
136164
bool Write(CAutoFile& fileout) const;
137165

@@ -190,4 +218,5 @@ class FeeFilterRounder
190218
std::set<double> feeset;
191219
FastRandomContext insecure_rand;
192220
};
221+
193222
#endif /*BITCOIN_POLICYESTIMATOR_H */

src/rpc/client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
106106
{ "getrawmempool", 0, "verbose" },
107107
{ "estimatefee", 0, "nblocks" },
108108
{ "estimatesmartfee", 0, "nblocks" },
109+
{ "estimaterawfee", 0, "nblocks" },
110+
{ "estimaterawfee", 1, "threshold" },
111+
{ "estimaterawfee", 2, "horizon" },
109112
{ "prioritisetransaction", 1, "fee_delta" },
110113
{ "setban", 2, "bantime" },
111114
{ "setban", 3, "absolute" },

src/rpc/mining.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,78 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
863863
return result;
864864
}
865865

866+
UniValue estimaterawfee(const JSONRPCRequest& request)
867+
{
868+
if (request.fHelp || request.params.size() < 1|| request.params.size() > 3)
869+
throw std::runtime_error(
870+
"estimaterawfee nblocks (threshold horizon)\n"
871+
"\nWARNING: This interface is unstable and may disappear or change!\n"
872+
"\nWARNING: This is an advanced API call that is tightly coupled to the specific\n"
873+
" implementation of fee estimation. The parameters it can be called with\n"
874+
" and the results it returns will change if the internal implementation changes.\n"
875+
"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
876+
"confirmation within nblocks blocks if possible. Uses virtual transaction size as defined\n"
877+
"in BIP 141 (witness data is discounted).\n"
878+
"\nArguments:\n"
879+
"1. nblocks (numeric)\n"
880+
"2. threshold (numeric, optional) The proportion of transactions in a given feerate range that must have been\n"
881+
" confirmed within nblocks in order to consider those feerates as high enough and proceed to check\n"
882+
" lower buckets. Default: 0.95\n"
883+
"3. horizon (numeric, optional) How long a history of estimates to consider. 0=short, 1=medium, 2=long.\n"
884+
" Default: 1\n"
885+
"\nResult:\n"
886+
"{\n"
887+
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
888+
" \"decay\" : x.x, (numeric) exponential decay (per block) for historical moving average of confirmation data\n"
889+
" \"pass.\" information about the lowest range of feerates to succeed in meeting the threshold\n"
890+
" \"fail.\" information about the highest range of feerates to fail to meet the threshold\n"
891+
" \"startrange\" : x.x, (numeric) start of feerate range\n"
892+
" \"endrange\" : x.x, (numeric) end of feerate range\n"
893+
" \"withintarget\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed within target\n"
894+
" \"totalconfirmed\" : x.x, (numeric) number of txs over history horizon in the feerate range that were confirmed at any point\n"
895+
" \"inmempool\" : x.x, (numeric) current number of txs in mempool in the feerate range unconfirmed for at least target blocks\n"
896+
"}\n"
897+
"\n"
898+
"A negative feerate is returned if no answer can be given.\n"
899+
"\nExample:\n"
900+
+ HelpExampleCli("estimaterawfee", "6 0.9 1")
901+
);
902+
903+
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VNUM), true);
904+
RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
905+
int nBlocks = request.params[0].get_int();
906+
double threshold = 0.95;
907+
if (!request.params[1].isNull())
908+
threshold = request.params[1].get_real();
909+
FeeEstimateHorizon horizon = FeeEstimateHorizon::MED_HALFLIFE;
910+
if (!request.params[2].isNull()) {
911+
int horizonInt = request.params[2].get_int();
912+
if (horizonInt < 0 || horizonInt > 2) {
913+
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid horizon for fee estimates");
914+
} else {
915+
horizon = (FeeEstimateHorizon)horizonInt;
916+
}
917+
}
918+
UniValue result(UniValue::VOBJ);
919+
CFeeRate feeRate;
920+
EstimationResult buckets;
921+
feeRate = ::feeEstimator.estimateRawFee(nBlocks, threshold, horizon, &buckets);
922+
923+
result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
924+
result.push_back(Pair("decay", buckets.decay));
925+
result.push_back(Pair("pass.startrange", round(buckets.pass.start)));
926+
result.push_back(Pair("pass.endrange", round(buckets.pass.end)));
927+
result.push_back(Pair("pass.withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0));
928+
result.push_back(Pair("pass.totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0));
929+
result.push_back(Pair("pass.inmempool", round(buckets.pass.inMempool * 100.0) / 100.0));
930+
result.push_back(Pair("fail.startrange", round(buckets.fail.start)));
931+
result.push_back(Pair("fail.endrange", round(buckets.fail.end)));
932+
result.push_back(Pair("fail.withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0));
933+
result.push_back(Pair("fail.totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0));
934+
result.push_back(Pair("fail.inmempool", round(buckets.fail.inMempool * 100.0) / 100.0));
935+
return result;
936+
}
937+
866938
static const CRPCCommand commands[] =
867939
{ // category name actor (function) okSafeMode
868940
// --------------------- ------------------------ ----------------------- ----------
@@ -877,6 +949,8 @@ static const CRPCCommand commands[] =
877949

878950
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} },
879951
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks"} },
952+
953+
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} },
880954
};
881955

882956
void RegisterMiningRPCCommands(CRPCTable &t)

0 commit comments

Comments
 (0)