New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better fee estimates #10199

Merged
merged 15 commits into from May 17, 2017

Conversation

@morcos
Member

morcos commented Apr 12, 2017

This is a substantial improvement of fee estimation code built off #9942.
Although in many cases the estimates will not appear to differ by much from previous estimates, they will handle some situations much better.

A longer writeup of the original algorithm and the changes in this PR can be found here:
https://gist.github.com/morcos/d3637f015bc4e607e1fd10d8351e9f41

A summary of major changes:

  • Estimates can now be given for up to a 1008 block target.
  • Estimates are tracked on 3 different time horizons which allows for both the longer targets and lets the estimates adjust more quickly to changes in conditions.
  • estimatefee is deprecated in favor of using only estimatesmartfee (already used by the GUI)
  • estimateSmartFee now requires a 60% success rate at target/2, an 85% rate at target and a 95% rate at 2*target.
  • estimates are by default conservative which requires the 95% rate for 2*target to be met at longer time horizons as well, but by choosing non-conservative estimates, the estimates are drastically reduced during periods of less transaction activity (such as the weekend).
  • estimates are smarter about making sure enough data has been gathered in order to return a valid estimate (at least twice the number of blocks as the requested target much have been recorded)
  • Fee rate buckets are half as big leading to a bit more precision.
  • estimaterawfee is added so that customized logic can be implemented to analyze the raw data and calculate estimates.
  • Transactions which leave the mempool due to eviction or other non-confirmed reasons now count as failures. These are also saved at shutdown.

A summary of open issues:

  • Estimates are not backwards compatible. Currently when updating to this patch, any old fee estimates will be discarded and your node will have to be running for a while to provide new estimates. It is a possibility to add logic to return the old estimates for targets 2-25 until the new estimates have gathered enough data.
  • estimatefee is probably a roughly similar calculation to what it used to be, but I've made no effort to make sure the quality of the estimates it gives have not degraded, so infrastructure which doesn't support estimatesmartfee should update.
  • The GUI needs to be updated to access the new range of estimates. I would suggest providing options for targets of: 2,4,6,12,24,48,144,504,1008
@jonasschnelli

This comment has been minimized.

Member

jonasschnelli commented Apr 13, 2017

Great!
Concept ACK.

The GUI needs to be updated to access the new range of estimates. I would suggest providing options for targets of: 2,4,6,12,24,48,144,504,1008

I'll have a look at this soon.

@jonasschnelli

This comment has been minimized.

Member

jonasschnelli commented Apr 19, 2017

ping @crwatkins (he presented some fee estimation overview at the wallet standards group meeting in Berlin this month).

"\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"
" and the results it returns will change if the internal implementaion changes.\n"

This comment has been minimized.

@jlopp

jlopp Apr 19, 2017

Contributor

Nit: implementation

@jlopp

This comment has been minimized.

Contributor

jlopp commented Apr 19, 2017

I'm not sure if this is a problem specific to your code, though requiring a leading 0 seems like a bug:

bitcoin-cli estimaterawfee 10 .85 0
error: Error parsing JSON:.85
@sipa

This comment has been minimized.

Member

sipa commented Apr 19, 2017

@jlopp .85 is not valid JSON.

int nBlocks = request.params[0].get_int();
double threshold = 0.95;
if (!request.params[1].isNull())
threshold = request.params[1].get_real();

This comment has been minimized.

@jlopp

jlopp Apr 19, 2017

Contributor

Would prefer more explicit range bounding on the threshold with an "Invalid threshold" error response; it currently allows numbers < 0 and > 1 and just returns a -1 feerate.

This comment has been minimized.

@morcos

morcos Apr 20, 2017

Member

The reason I didn't bother with that is I felt estimaterawfee should be a very low level feature that doesn't need all the user friendly bells and whistles.. But it could certainly be cleaned up to add that either in this PR or a follow on..

Change file format for fee estimates.
Move buckets and bucketMap to be stored as part of overall serialization of estimator.
Add some placeholder data so file format is only changed once.
Maintain 3 different TxConfirmStats with potential for different decays and scales.
@morcos

This comment has been minimized.

Member

morcos commented Apr 20, 2017

clean rebase after #9942 was merged

@laanwj laanwj added this to the 0.15.0 milestone Apr 20, 2017

@crwatkins

This comment has been minimized.

crwatkins commented Apr 24, 2017

Thanks @jonasschnelli. In Berlin, I noted that at least 50% of the wallets listed on bitcoin.org use Core as a fee estimation source directly or through other servers based on Core and any statistical or algorithmic biases might compound over time because of the large number of clients using the single source. I presented some anecdotal, non-scientific observations about fees and some reasons that they might be artificially high. Some of my concerns about the current esitmatefee were

  • Slow to respond (2.5 day half life)
  • Too high confidence (95%)
  • No long term estimates (only 25 blocks)

I believe that the changes in this PR mitigate some of those concerns, and thus I am in favor of these changes. Nice work!

I'm still a little concerned about so many wallets "competing" using the same identical algorithm. @morcos do you envision some devs (wallets) would use estimaterawfee to tweak the algorithm for their users?

@jlopp

This comment has been minimized.

Contributor

jlopp commented Apr 25, 2017

@crwatkins I can only speak for BitGo, but we run custom compiled versions of Core with our own half life and confidence values. We then use the output as a baseline for a more complex fee algorithm that we adjust upward and downward based upon other data sources. I'm already working on transitioning us to using the estimaterawfee functionality.

@morcos

This comment has been minimized.

Member

morcos commented Apr 25, 2017

@crwatkins Yes that was the basic idea of including estimaterawfee (that and it's useful for debugging). The exact strategy you use for determining what fee you want to put on a transaction depends on so many factors that its hard to have a one size fits all solution, but thats exactly what Bitcoin Core has to try to ship. So rather than attempt to defend why the algorithm I picked is the one algorithm to rule them all, I thought it would also make sense to expose a way to examine all the collected data and let people build their own algorithms if they'd like to.

@crwatkins

This comment has been minimized.

crwatkins commented Apr 25, 2017

@morcos Exclellent. Thanks!

@Leviathn

This comment has been minimized.

Leviathn commented Apr 25, 2017

Reviewing

@TheBlueMatt

Looks good. With the exception of the fallback during upgrade and the one strange sharp edge LGTM. Needs testing because the code being correct isnt really enough, but no doubt better than today's estimates.

// Start counting from highest(default) or lowest feerate transactions
for (int bucket = startbucket; bucket >= 0 && bucket <= maxbucketindex; bucket += step) {
if (newBucketRange) {

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

Can you update the commit message from "Track start of new bucket range more carefully" to something more descriptive.

" \"leftmempool\" : x.x, (numeric) number of txs over history horizon in the feerate range that left mempool unconfirmed after target\n"
"}\n"
"\n"
"A negative value is returned if no answer can be given.\n"

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

nit: "A negative feerate is returned" (other values are zero, so might as well just specify feerate here).

100 * nConf / (totalNum + extraNum), nConf, totalNum, extraNum);
// If we were passing until we reached last few buckets with insufficient data, then report those as failed
if (passing && !newBucketRange) {
unsigned int failMinBucket = curNearBucket < curFarBucket ? curNearBucket : curFarBucket;

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

nit: std::min/max here like you did above?

std::vector<std::vector<int> > curBlockConf; // curBlockConf[Y][X]
std::vector<std::vector<double>> confAvg; // confAvg[Y][X]
std::vector<std::vector<double>> failAvg; // future use

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

Should probably update this comment in the commit that puts failAvg to use.

std::map<double, unsigned int> tempMap;
std::unique_ptr<TxConfirmStats> tempFeeStats(new TxConfirmStats(tempBuckets, tempMap, MAX_BLOCK_CONFIRMS, tempDecay));
tempFeeStats->Read(filein, nVersionThatWrote, tempNum);

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

As discussed it'd be nice to use this chunk and tempFeeStats to just generate one fee estimate, cache that, and then start fresh (as you do) but use the cached fee estimate while our new buckets fill.

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

leaving for a later improvement

@@ -465,6 +469,8 @@ void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, boo
mapMemPoolTxs[hash].blockHeight = txHeight;
mapMemPoolTxs[hash].bucketIndex = feeStats->NewTx(txHeight, (double)feeRate.GetFeePerK());
shortStats->NewTx(txHeight, (double)feeRate.GetFeePerK());

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

nit: might be nice to assert that the bucketIndex is the same across all three calls here?

/** Write estimation data to a file */
bool Write(CAutoFile& fileout) const;
/** Read estimation data from a file */
bool Read(CAutoFile& filein);
/** Empty mempool transactions on shutdown to record failure to confirm for txs still in mempool */
void ClearInMempool(CTxMemPool& pool);

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

nit: maybe a better name for this would be FlushFailedPreShutdown()?

@@ -448,7 +448,7 @@ void CTxMemPool::removeUnchecked(txiter it, MemPoolRemovalReason reason)
mapLinks.erase(it);
mapTx.erase(it);
nTransactionsUpdated++;
if (minerPolicyEstimator) {minerPolicyEstimator->removeTx(hash);}
if (minerPolicyEstimator) {minerPolicyEstimator->removeTx(hash, false);}

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

nit: it seems kinda strange that the "inBlock" flag is always set to false, even though it is sometimes called when the transaction was, indeed, in a block. Not a bug because it is a dummy call in fees.cpp, but maybe the API should stay the same and have the flag filled in inside of fees.cpp.

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

This will eventually be cleaned up when fee estimation listens to CValidationInterface

return CFeeRate(0);
if ((unsigned int)confTarget > maxUsableEstimate) {
confTarget = maxUsableEstimate;

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

This seems strange to me. Most of our APIs will already gracefully fail if you pass in too high a conf target, I'd rather we do the same here (same in GUI).

This comment has been minimized.

@morcos

morcos May 4, 2017

Member

The design of estimateSmartFee is to be used by the wallet to intelligently as possible automatically put a fee on a transaction being sent. In the GUI, the slider clearly indicates what target the fee is being returned for. For the RPC call estimatesmartfee, this is also clearly returned. The difficulty comes with automatic estimation that happens from a sendtoaddress or sendmany RPC call. If in the future we add a way to select a transaction confirm target with the desired RPC call, perhaps it would make sense to fail, but for now it seems to me that its more important for the transaction to go out, even if its paying a fee for a much faster target.

fileout << CLIENT_VERSION; // version that wrote the file
fileout << nBestSeenHeight;
if (BlockSpan() > HistoricalBlockSpan()/2) {

This comment has been minimized.

@TheBlueMatt

TheBlueMatt Apr 28, 2017

Contributor

This seems like a sharp edge - you can restart a node and end up getting a fee estimate much higher than 30 seconds ago before you shut down. Maybe change the /2 to historicalBest more recent than OLDEST_ESTIMATE_HISTORY*3/4 or so? Would still have the issue but at least its less likely to be hit for those who regularly run a node just during the day or so?

This comment has been minimized.

@morcos

morcos May 4, 2017

Member

After discussion, I agree that this logic could be more intelligent, but I think that can be left for a later improvement.

@instagibbs

utACK 57e71c8 provided conservative is defined to the user

If I find the time I'll do some real-world testing once the buckets fill up.

@@ -53,13 +55,17 @@ class TxConfirmStats
double decay;
unsigned int scale;

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

Add description?

@@ -68,7 +74,8 @@ class TxConfirmStats
* @param maxConfirms max number of confirms to track
* @param decay how much to decay the historical moving average per block
*/
TxConfirmStats(const std::vector<double>& defaultBuckets, unsigned int maxConfirms, double decay);
TxConfirmStats(const std::vector<double>& defaultBuckets, const std::map<double, unsigned int>& defaultBucketMap,

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

add description above?

/** Estimate feerate needed to get be included in a block within confTarget
* blocks. If no answer can be given at confTarget, return an estimate at
* the closest target where one can be given. 'conservative' estimates are
* valid over longer time horizons also.

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

teeny nit: "also" unneeded

confTarget = maxUsableEstimate;
}
assert(confTarget > 0); //estimateCombinedFee and estimateConservativeFee take unsigned ints

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

I think this is impossible to hit, due to confTarget <= 0 check and maxUsableEstimate <= 1 check. (though this might make sense as belt and suspenders)

"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
"confirmation within nblocks blocks if possible and return the number of blocks\n"
"for which the estimate is valid. Uses virtual transaction size as defined\n"
"in BIP 141 (witness data is discounted).\n"
"\nArguments:\n"
"1. nblocks (numeric)\n"
"1. nblocks (numeric)\n"
"2. conservative (bool, optional, default=true) Whether to return a more conservative estimate calculated from a longer history\n"

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

It's not clear to me as the API reader what "conservative" means here, depending on if I'm worrying about over-paying or waiting too long, or being conservative in the amount of data I'm using to estimate.

suggested: "Whether to return a possibly higher feerate estimate calculated from a longer history"

" \"decay\" : x.x, (numeric) exponential decay for historical moving average of confirmation data\n"
" \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n"
" \"pass.\" information about the last range of feerates to succeed in meeting the threshold\n"
" \"fail.\" information about the first range of feerates to fail to meet the threshold\n"

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

nit: s/first/highest/

" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
" \"decay\" : x.x, (numeric) exponential decay for historical moving average of confirmation data\n"
" \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n"
" \"pass.\" information about the last range of feerates to succeed in meeting the threshold\n"

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

nit: s/last/lowest/

" \"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 total\n"

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

s/total/that were confirmed at any point/

# estimatesmartfee should return the same result
assert_equal(node.estimatesmartfee(i+1)["feerate"], e)
if i >= 13: # for n>=14 estimatesmartfee(n/2) should be at least as high as estimatefee(n)
assert(node.estimatesmartfee((i+1)//2)["feerate"] > float(e) - delta)

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

could also do a quick check that the transitive property is true for conservative estimates

/** Require an avg of 0.1 tx in the combined feerate bucket per block to have stat significance */
static constexpr double SUFFICIENT_FEETXS = 0.1;
/** Require an avg of 0.5 tx when using short decay since there are less blocks considered*/

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

grammar nit: s/less/fewer/

{
if (request.fHelp || request.params.size() < 1|| request.params.size() > 3)
throw std::runtime_error(
"estimaterawfee nblocks\n"

This comment has been minimized.

@instagibbs

instagibbs May 1, 2017

Member

missing threshold

median = doubleEst;
}
if (conservative) {

This comment has been minimized.

@TheBlueMatt

TheBlueMatt May 1, 2017

Contributor

I believe we may want to add a check that the previous code actually got a result here. And, generally, that all of the four estimate*Fee calls here actually succeed. It seems super strange to accept only one result as fine if the rest fail (eg on startup you may get a response from short horizon on SUFFICIENT_TXS_SHORT, but not on medium/long, also @sdaftuar saw a case this morning where he had an offline node for a while which succeeded in getting a result for conservative but not for non-conservative for a 3 conftarget, I believe because estimateConservativeFee gave a result from the medium/long horizons when the short horizon could not).

This comment has been minimized.

@morcos

morcos May 3, 2017

Member

After offline discussion, it appears the bigger issue is what to do when the short time horizon estimates have decayed to not being able to give you an answer. It seems like what happens in conservative mode is probably the right thing where you take advantage of the fact that the longer time horizon estimates have not decayed yet. We probably want the ability to utilize this as a fall back in non-conservative mode as well. This can happen either from shutting down a node for a couple of days which means the short time horizon will decay too much, or even just from losing network activity from something like a laptop over the weekend.
As it is, I think its probably still a strict improvement to existing estimates, but could use further refinement.

if (!conservative) {
// When we aren't using conservative estimates, if a lower confTarget from a more recent horizon returns a lower answer use it.
double medMax = feeStats->EstimateMedianVal(feeStats->GetMaxConfirms(), SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
if (estimate == -1 || medMax < estimate) {

This comment has been minimized.

@morcos

morcos May 3, 2017

Member

in all 3 of these replacement evaluations (also line 709 and line 722), we probably don't want to do the replacement if the new estimate is -1, should be rare.

@morcos

This comment has been minimized.

Member

morcos commented May 9, 2017

Addressed the comments in fixup commmits e95eca7 (smarterfee.ver5) and then squashed --> 9a34419 (smarterfee.ver5.squash)

@sipa

Code review ACK, with some nits. I haven't reviewed the changes to the estimation algorithm itself.

@@ -580,10 +587,15 @@ bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
{
try {
LOCK(cs_feeEstimator);
fileout << 139900; // version required to read: 0.13.99 or later
fileout << 149900; // version required to read: 0.14.99 or later

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Any reason to keep this number tied to client versions instead of just an independent "fee estimates file version number"?

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

no reason..., but this also seems simple enough?

@@ -141,19 +207,38 @@ class CBlockPolicyEstimator
/** Classes to track historical data on transaction confirmations */
TxConfirmStats* feeStats;
TxConfirmStats* shortStats;

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Convert these to std::unique_ptrs?

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

This was the change made in #9942, and I couldn't get unique_ptrs to work while also moving the code to the cpp file. I now also have other ideas how to clean up the wonky design of TxConfirmStats inside CBlockPolicyEstimator, but I don't want to do it now and churn this PR.

@@ -159,6 +171,10 @@ class CBlockPolicyEstimator
class FeeFilterRounder
{
private:
static constexpr double MAX_FILTER_FEERATE = 1e7;
static constexpr double FEE_FILTER_SPACING = 1.1;

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Can you add a comment to explain the relation with the CBlockPolicyEstimator::FEE_SPACE constant?

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

yeah, there is no relation.
when I made the filter spacing, I just copied the other one arbitrarily, but then I decided if I was changing the estimate spacing, there was no reason to also make a change to the filter spacing. I can add a comment.

"\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"
" and the results it returns will change if the internal implementaion changes.\n"

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

@jlopp's comment hasn't been addressed yet: "nit: Implementation".

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

oops

if (horizonInt < 0 || horizonInt > 2) {
throw JSONRPCError(RPC_TYPE_ERROR, "Invalid horizon for fee estimates");
}
else {

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Style nit: else on the same line as }

This comment has been minimized.

@morcos
"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"

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

To make the RPC interface less dependent on the specific implementation, perhaps don't make horizon a parameter, and just return information for all applicable horizons for the given value of nblocks?

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

slightly prefer existing, but I could change it if prevailing opinion is otherwise..
estimaterawfee is meant to be implementation dependent

This comment has been minimized.

@sipa

sipa May 17, 2017

Member

The reason for asking is that while the output of this RPC may be implementation dependent, it would be nice if (to the extent possible), its arguments aren't. The specific buckets used seem much more likely to change in other revisions.

Just a nit.

double CBlockPolicyEstimator::estimateCombinedFee(unsigned int confTarget, double successThreshold, bool conservative) const
{
double estimate = -1;

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Can you write this function as a loop over the 3 horizons, instead of a cascaded if/then/else?

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

I can take a pass at rewriting the logic to be a bit clearer, but I'm not sure a loop is the right thing. There are basically 6 different combinations of estimates you might want to check depending on what range your target is in and whether or not you want to check shorter horizons. But the logic might be cleaner if those are just spelled out.

@@ -173,6 +173,10 @@ class CBlockPolicyEstimator
private:
CFeeRate minTrackedFee; //!< Passed to constructor to avoid dependency on main
unsigned int nBestSeenHeight;
unsigned int firstRecordedHeight;
unsigned int historicalFirst;

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Nit: unused until 2 commits later.

This comment has been minimized.

@morcos

morcos May 9, 2017

Member

I wanted to get the file format change over and done with...

unsigned int CBlockPolicyEstimator::MaxUsableEstimate() const
{
// Block spans are divided by 2 to make sure there are enough potential failing data points for the estimate
return std::min(longStats->GetMaxConfirms(), std::max(BlockSpan(), HistoricalBlockSpan()) / 2);

This comment has been minimized.

@sipa

sipa May 9, 2017

Member

Nit: If you'd return which of the two BlockSpans was chosen in a pair or a bool, you wouldn't need to repeat the decision logic in the debug print statement of processBlock.

This comment has been minimized.

@morcos

This comment has been minimized.

@morcos

morcos May 10, 2017

Member

after actually doing this, it seemed uglier, so leaving it as is

morcos added some commits Jan 24, 2017

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.
Make EstimateMedianVal smarter about small failures.
Instead of stopping if it encounters a "sufficient" number of transactions which don't meet the threshold for being confirmed within the target, it keeps looking to add more transactions to see if there is a temporary blip in the data.  This allows a smaller number of required data points.
Change parameters for fee estimation and estimates on all 3 time hori…
…zons.

Make feerate buckets smaller (5% instead of 10%) and make the 3 different horizons have half lifes of 3 hours, 1 day and 1 week respectively.
@morcos

This comment has been minimized.

Member

morcos commented May 10, 2017

OK. I think I addressed remaining comments.
I also tried to clean up the logic of estimateCombinedFee to be a bit clearer (although still not a loop)
I also changed the logic to be smarter about returning an answer if one can be found on any horizon, instead of returning -1.
Fixup commits are squashed.

abe88e4 (smarterfee.ver7) --> 2d2e170 (smarterfee.ver7.squash)

@instagibbs

This comment has been minimized.

Member

instagibbs commented May 12, 2017

re-utACK 2d2e170

result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
result.push_back(Pair("decay", buckets.decay));
result.push_back(Pair("pass.startrange", round(buckets.pass.start)));

This comment has been minimized.

@sipa

sipa May 17, 2017

Member

I haven't seen a '.' inside RPC output field names. Why not bundle these into a pass and fail Object? That's probably easier to parse for external libraries too.

This comment has been minimized.

@morcos

morcos May 17, 2017

Member

ah, that makes sense. i will make this change

"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"

This comment has been minimized.

@sipa

sipa May 17, 2017

Member

The reason for asking is that while the output of this RPC may be implementation dependent, it would be nice if (to the extent possible), its arguments aren't. The specific buckets used seem much more likely to change in other revisions.

Just a nit.

@gmaxwell

ACK.

@morcos

This comment has been minimized.

Member

morcos commented May 17, 2017

Added an extra commit to improve JSON output as per @sipa. Was kind of annoying to squash, so will leave it if that's ok?

@laanwj I think this is reviewed enough that we'd be better off merging and getting some more experimentation with the estimates.. It takes 2 full weeks for the longest estimates to even be exposed.

The biggest open questions are whether I should add an ability to use old estimates during transition and whether we are going to build in GUI support.

The tests could also use some improvement but I don't think they're notably worse than they were.

@sipa

This comment has been minimized.

Member

sipa commented May 17, 2017

utACK 38bc1ec

@instagibbs

This comment has been minimized.

Member

instagibbs commented May 17, 2017

utACK 38bc1ec

@sipa sipa merged commit 38bc1ec into bitcoin:master May 17, 2017

1 check was pending

continuous-integration/travis-ci/pr The Travis CI build is in progress
Details

sipa added a commit that referenced this pull request May 17, 2017

Merge #10199: Better fee estimates
38bc1ec Make more json-like output from estimaterawfee (Alex Morcos)
2d2e170 Comments and improved documentation (Alex Morcos)
ef589f8 minor cleanup: remove unnecessary variable (Alex Morcos)
3ee76d6 Introduce a scale factor (Alex Morcos)
5f1f0c6 Historical block span (Alex Morcos)
aa19b8e Clean up fee estimate debug printing (Alex Morcos)
10f7cbd Track first recorded height (Alex Morcos)
3810e97 Rewrite estimateSmartFee (Alex Morcos)
c7447ec Track failures in fee estimation. (Alex Morcos)
4186d3f Expose estimaterawfee (Alex Morcos)
2681153 minor refactor: explicitly track start of new bucket range and don't update curNearBucket on final loop. (Alex Morcos)
1ba43cc Make EstimateMedianVal smarter about small failures. (Alex Morcos)
d3e30bc Refactor to update moving average on fly (Alex Morcos)
e5007ba Change parameters for fee estimation and estimates on all 3 time horizons. (Alex Morcos)
c0a273f Change file format for fee estimates. (Alex Morcos)

Tree-SHA512: 186e7508d86a1f351bb656edcd84ee9091f5f2706331eda9ee29da9c8eb5bf67b8c1f2abf6662835560e7f613b1377099054f20767f41ddcdbc89c4f9e78946d
@RHavar

This comment has been minimized.

Contributor

RHavar commented May 18, 2017

I tried reading the description from help, and I don't really understand this:

[the rpc command also returns] the number of blocks for which the estimate is valid.

but from the actual return:

"blocks" : n (numeric) block number where estimate was found

Without reading the code, it's not clear what blocks means. Is it the block height when you called estimatestartfee or is it how long it's valid for?

@instagibbs

This comment has been minimized.

Member

instagibbs commented May 18, 2017

@RHavar without actually looking at the code, I believe this refers to the "cheapest bucket" block which that feerate was found.

@RHavar

This comment has been minimized.

Contributor

RHavar commented May 18, 2017

@instagibbs Yeah, you seem right. But I don't think that was inferable from the help information :P

What's the motivation for estimatefee returning a different value than estimatesmartfee ?

$ bitcoin-cli estimatefee 6
0.00254275
$ bitcoin-cli estimatesmartfee 6
{
  "feerate": 0.00208824,
  "blocks": 6
}

?

Anyway, fantastic work. This is a huge improvement, something that has been long needed. Well done @morcos

@RHavar

This comment has been minimized.

Contributor

RHavar commented May 18, 2017

I've updated https://estimatefee.com/ to now use estimatesmartfee $n so if you're interested you can take a look there.

The bitcoin node has only been up a few hours, so it'll probably take a while to collect enough information.

P.S. you kinda broke my domain name lol

@morcos

This comment has been minimized.

Member

morcos commented May 18, 2017

@RHavar you might be right that the description is not the clearest.
blocks refers to the target for which you are getting an estimate, which is an expected number of blocks within which you will be confirmed. Occasionally you ask for an estimate at one target but it returns you an estimate for a different target (b/c it is unable to give you an estimate for the target you asked for)

@RHavar

This comment has been minimized.

Contributor

RHavar commented May 18, 2017

@morcos In that case I'd suggest renaming it from "blocks" to "target" and changing the description to what you wrote above :P

@karel-3d

This comment has been minimized.

Contributor

karel-3d commented Sep 2, 2017

Not sure where to put it, but just FYI

I made a simple tool that looks up smart fees once per 10 minutes and puts it here

https://estimatesmartfee.com/html.html

or

https://estimatesmartfee.com/json.json

If you want to use it for testing or something

@jb55

This comment has been minimized.

Contributor

jb55 commented Oct 26, 2017

also FYIs, I put together a small bash script that uses this for quickly grabbing fees from the command line: https://bitcoin.stackexchange.com/a/61348/1235

thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment