Skip to content

Commit 3810e97

Browse files
committed
Rewrite estimateSmartFee
Change the logic of estimateSmartFee to check a 60% threshold at half the target, a 85% threshold at the target and a 95% threshold at double the target. Always check the shortest time horizon possible and ensure that estimates are monotonically decreasing. Add a conservative mode, which makes sure that the 95% threshold is also met at longer time horizons as well.
1 parent c7447ec commit 3810e97

File tree

6 files changed

+104
-22
lines changed

6 files changed

+104
-22
lines changed

src/policy/fees.cpp

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -658,31 +658,107 @@ CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThr
658658
return CFeeRate(median);
659659
}
660660

661-
CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const
661+
662+
/** Return a fee estimate at the required successThreshold from the shortest
663+
* time horizon which tracks confirmations up to the desired target. If
664+
* checkShorterHorizon is requested, also allow short time horizon estimates
665+
* for a lower target to reduce the given answer */
666+
double CBlockPolicyEstimator::estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon) const
667+
{
668+
double estimate = -1;
669+
if (confTarget >= 1 && confTarget <= longStats->GetMaxConfirms()) {
670+
// Find estimate from shortest time horizon possible
671+
if (confTarget <= shortStats->GetMaxConfirms()) { // short horizon
672+
estimate = shortStats->EstimateMedianVal(confTarget, SUFFICIENT_TXS_SHORT, successThreshold, true, nBestSeenHeight);
673+
}
674+
else if (confTarget <= feeStats->GetMaxConfirms()) { // medium horizon
675+
estimate = feeStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
676+
}
677+
else { // long horizon
678+
estimate = longStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
679+
}
680+
if (checkShorterHorizon) {
681+
// If a lower confTarget from a more recent horizon returns a lower answer use it.
682+
if (confTarget > feeStats->GetMaxConfirms()) {
683+
double medMax = feeStats->EstimateMedianVal(feeStats->GetMaxConfirms(), SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
684+
if (medMax > 0 && (estimate == -1 || medMax < estimate))
685+
estimate = medMax;
686+
}
687+
if (confTarget > shortStats->GetMaxConfirms()) {
688+
double shortMax = shortStats->EstimateMedianVal(shortStats->GetMaxConfirms(), SUFFICIENT_TXS_SHORT, successThreshold, true, nBestSeenHeight);
689+
if (shortMax > 0 && (estimate == -1 || shortMax < estimate))
690+
estimate = shortMax;
691+
}
692+
}
693+
}
694+
return estimate;
695+
}
696+
697+
double CBlockPolicyEstimator::estimateConservativeFee(unsigned int doubleTarget) const
698+
{
699+
double estimate = -1;
700+
if (doubleTarget <= shortStats->GetMaxConfirms()) {
701+
estimate = feeStats->EstimateMedianVal(doubleTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
702+
}
703+
if (doubleTarget <= feeStats->GetMaxConfirms()) {
704+
double longEstimate = longStats->EstimateMedianVal(doubleTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
705+
if (longEstimate > estimate) {
706+
estimate = longEstimate;
707+
}
708+
}
709+
return estimate;
710+
}
711+
712+
CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool, bool conservative) const
662713
{
663714
if (answerFoundAtTarget)
664715
*answerFoundAtTarget = confTarget;
665716

666717
double median = -1;
667-
668718
{
669719
LOCK(cs_feeEstimator);
670720

671721
// Return failure if trying to analyze a target we're not tracking
672-
if (confTarget <= 0 || (unsigned int)confTarget > feeStats->GetMaxConfirms())
722+
if (confTarget <= 0 || (unsigned int)confTarget > longStats->GetMaxConfirms())
673723
return CFeeRate(0);
674724

675725
// It's not possible to get reasonable estimates for confTarget of 1
676726
if (confTarget == 1)
677727
confTarget = 2;
678728

679-
while (median < 0 && (unsigned int)confTarget <= feeStats->GetMaxConfirms()) {
680-
median = feeStats->EstimateMedianVal(confTarget++, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
729+
assert(confTarget > 0); //estimateCombinedFee and estimateConservativeFee take unsigned ints
730+
731+
/** true is passed to estimateCombined fee for target/2 and target so
732+
* that we check the max confirms for shorter time horizons as well.
733+
* This is necessary to preserve monotonically increasing estimates.
734+
* For non-conservative estimates we do the same thing for 2*target, but
735+
* for conservative estimates we want to skip these shorter horizons
736+
* checks for 2*target becuase we are taking the max over all time
737+
* horizons so we already have monotonically increasing estimates and
738+
* the purpose of conservative estimates is not to let short term
739+
* fluctuations lower our estimates by too much.
740+
*/
741+
double halfEst = estimateCombinedFee(confTarget/2, HALF_SUCCESS_PCT, true);
742+
double actualEst = estimateCombinedFee(confTarget, SUCCESS_PCT, true);
743+
double doubleEst = estimateCombinedFee(2 * confTarget, DOUBLE_SUCCESS_PCT, !conservative);
744+
median = halfEst;
745+
if (actualEst > median) {
746+
median = actualEst;
747+
}
748+
if (doubleEst > median) {
749+
median = doubleEst;
750+
}
751+
752+
if (conservative || median == -1) {
753+
double consEst = estimateConservativeFee(2 * confTarget);
754+
if (consEst > median) {
755+
median = consEst;
756+
}
681757
}
682758
} // Must unlock cs_feeEstimator before taking mempool locks
683759

684760
if (answerFoundAtTarget)
685-
*answerFoundAtTarget = confTarget - 1;
761+
*answerFoundAtTarget = confTarget;
686762

687763
// If mempool is limiting txs , return at least the min feerate from the mempool
688764
CAmount minPoolFee = pool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000).GetFeePerK();
@@ -695,6 +771,7 @@ CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoun
695771
return CFeeRate(median);
696772
}
697773

774+
698775
bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
699776
{
700777
try {

src/policy/fees.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class CBlockPolicyEstimator
155155
* confTarget blocks. If no answer can be given at confTarget, return an
156156
* estimate at the lowest target where one can be given.
157157
*/
158-
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const;
158+
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool, bool conservative = true) const;
159159

160160
/** Return a specific fee estimate calculation with a given success threshold and time horizon.
161161
*/
@@ -199,6 +199,8 @@ class CBlockPolicyEstimator
199199
/** Process a transaction confirmed in a block*/
200200
bool processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry);
201201

202+
double estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon) const;
203+
double estimateConservativeFee(unsigned int doubleTarget) const;
202204
};
203205

204206
class FeeFilterRounder

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
106106
{ "getrawmempool", 0, "verbose" },
107107
{ "estimatefee", 0, "nblocks" },
108108
{ "estimatesmartfee", 0, "nblocks" },
109+
{ "estimatesmartfee", 1, "conservative" },
109110
{ "estimaterawfee", 0, "nblocks" },
110111
{ "estimaterawfee", 1, "threshold" },
111112
{ "estimaterawfee", 2, "horizon" },

src/rpc/mining.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -828,16 +828,20 @@ UniValue estimatefee(const JSONRPCRequest& request)
828828

829829
UniValue estimatesmartfee(const JSONRPCRequest& request)
830830
{
831-
if (request.fHelp || request.params.size() != 1)
831+
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
832832
throw std::runtime_error(
833-
"estimatesmartfee nblocks\n"
833+
"estimatesmartfee nblocks (conservative)\n"
834834
"\nWARNING: This interface is unstable and may disappear or change!\n"
835835
"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
836836
"confirmation within nblocks blocks if possible and return the number of blocks\n"
837837
"for which the estimate is valid. Uses virtual transaction size as defined\n"
838838
"in BIP 141 (witness data is discounted).\n"
839839
"\nArguments:\n"
840-
"1. nblocks (numeric)\n"
840+
"1. nblocks (numeric)\n"
841+
"2. conservative (bool, optional, default=true) Whether to return a more conservative estimate which\n"
842+
" also satisfies a longer history. A conservative estimate potentially returns a higher\n"
843+
" feerate and is more likely to be sufficient for the desired target, but is not as\n"
844+
" responsive to short term drops in the prevailing fee market\n"
841845
"\nResult:\n"
842846
"{\n"
843847
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
@@ -854,10 +858,15 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
854858
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM));
855859

856860
int nBlocks = request.params[0].get_int();
861+
bool conservative = true;
862+
if (request.params.size() > 1 && !request.params[1].isNull()) {
863+
RPCTypeCheckArgument(request.params[1], UniValue::VBOOL);
864+
conservative = request.params[1].get_bool();
865+
}
857866

858867
UniValue result(UniValue::VOBJ);
859868
int answerFound;
860-
CFeeRate feeRate = ::feeEstimator.estimateSmartFee(nBlocks, &answerFound, ::mempool);
869+
CFeeRate feeRate = ::feeEstimator.estimateSmartFee(nBlocks, &answerFound, ::mempool, conservative);
861870
result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
862871
result.push_back(Pair("blocks", answerFound));
863872
return result;
@@ -951,7 +960,7 @@ static const CRPCCommand commands[] =
951960
{ "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} },
952961

953962
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} },
954-
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks"} },
963+
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} },
955964

956965
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} },
957966
};

src/test/policyestimator_tests.cpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,6 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
8383
BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0));
8484
BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() < 9*baseRate.GetFeePerK() + deltaFee);
8585
BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() > 9*baseRate.GetFeePerK() - deltaFee);
86-
int answerFound;
87-
BOOST_CHECK(feeEst.estimateSmartFee(1, &answerFound, mpool) == feeEst.estimateFee(2) && answerFound == 2);
88-
BOOST_CHECK(feeEst.estimateSmartFee(2, &answerFound, mpool) == feeEst.estimateFee(2) && answerFound == 2);
89-
BOOST_CHECK(feeEst.estimateSmartFee(4, &answerFound, mpool) == feeEst.estimateFee(4) && answerFound == 4);
90-
BOOST_CHECK(feeEst.estimateSmartFee(8, &answerFound, mpool) == feeEst.estimateFee(8) && answerFound == 8);
9186
}
9287
}
9388

@@ -143,10 +138,8 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
143138
mpool.removeForBlock(block, ++blocknum);
144139
}
145140

146-
int answerFound;
147141
for (int i = 1; i < 10;i++) {
148142
BOOST_CHECK(feeEst.estimateFee(i) == CFeeRate(0) || feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee);
149-
BOOST_CHECK(feeEst.estimateSmartFee(i, &answerFound, mpool).GetFeePerK() > origFeeEst[answerFound-1] - deltaFee);
150143
}
151144

152145
// Mine all those transactions
@@ -194,7 +187,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
194187
mpool.TrimToSize(1);
195188
BOOST_CHECK(mpool.GetMinFee(1).GetFeePerK() > feeV[5]);
196189
for (int i = 1; i < 10; i++) {
197-
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= feeEst.estimateFee(i).GetFeePerK());
190+
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= feeEst.estimateRawFee(i, 0.85, FeeEstimateHorizon::MED_HALFLIFE).GetFeePerK());
198191
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= mpool.GetMinFee(1).GetFeePerK());
199192
}
200193
}

test/functional/smartfees.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ def check_estimates(node, fees_seen, max_invalid, print_estimates = True):
116116
for i,e in enumerate(all_estimates): # estimate is for i+1
117117
if e >= 0:
118118
valid_estimate = True
119-
# estimatesmartfee should return the same result
120-
assert_equal(node.estimatesmartfee(i+1)["feerate"], e)
119+
if i >= 13: # for n>=14 estimatesmartfee(n/2) should be at least as high as estimatefee(n)
120+
assert(node.estimatesmartfee((i+1)//2)["feerate"] > float(e) - delta)
121121

122122
else:
123123
invalid_estimates += 1

0 commit comments

Comments
 (0)