Browse Source

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
0.15
Pieter Wuille 7 years ago
parent
commit
318ea50a1c
No known key found for this signature in database
GPG Key ID: A636E97631F767E0
  1. 1
      src/init.cpp
  2. 611
      src/policy/fees.cpp
  3. 190
      src/policy/fees.h
  4. 4
      src/rpc/client.cpp
  5. 103
      src/rpc/mining.cpp
  6. 45
      src/test/policyestimator_tests.cpp
  7. 2
      src/txmempool.cpp
  8. 4
      test/functional/smartfees.py

1
src/init.cpp

@ -215,6 +215,7 @@ void Shutdown()
if (fFeeEstimatesInitialized) if (fFeeEstimatesInitialized)
{ {
::feeEstimator.FlushUnconfirmed(::mempool);
fs::path est_path = GetDataDir() / FEE_ESTIMATES_FILENAME; fs::path est_path = GetDataDir() / FEE_ESTIMATES_FILENAME;
CAutoFile est_fileout(fsbridge::fopen(est_path, "wb"), SER_DISK, CLIENT_VERSION); CAutoFile est_fileout(fsbridge::fopen(est_path, "wb"), SER_DISK, CLIENT_VERSION);
if (!est_fileout.IsNull()) if (!est_fileout.IsNull())

611
src/policy/fees.cpp

@ -14,6 +14,8 @@
#include "txmempool.h" #include "txmempool.h"
#include "util.h" #include "util.h"
static constexpr double INF_FEERATE = 1e99;
/** /**
* We will instantiate an instance of this class to track transactions that were * We will instantiate an instance of this class to track transactions that were
* included in a block. We will lump transactions into a bucket according to their * included in a block. We will lump transactions into a bucket according to their
@ -26,40 +28,43 @@ class TxConfirmStats
{ {
private: private:
//Define the buckets we will group transactions into //Define the buckets we will group transactions into
std::vector<double> buckets; // The upper-bound of the range for the bucket (inclusive) const std::vector<double>& buckets; // The upper-bound of the range for the bucket (inclusive)
std::map<double, unsigned int> bucketMap; // Map of bucket upper-bound to index into all vectors by bucket const std::map<double, unsigned int>& bucketMap; // Map of bucket upper-bound to index into all vectors by bucket
// For each bucket X: // For each bucket X:
// Count the total # of txs in each bucket // Count the total # of txs in each bucket
// Track the historical moving average of this total over blocks // Track the historical moving average of this total over blocks
std::vector<double> txCtAvg; std::vector<double> txCtAvg;
// and calculate the total for the current block to update the moving average
std::vector<int> curBlockTxCt;
// Count the total # of txs confirmed within Y blocks in each bucket // Count the total # of txs confirmed within Y blocks in each bucket
// Track the historical moving average of theses totals over blocks // Track the historical moving average of theses totals over blocks
std::vector<std::vector<double>> confAvg; // confAvg[Y][X] std::vector<std::vector<double>> confAvg; // confAvg[Y][X]
// and calculate the totals for the current block to update the moving averages
std::vector<std::vector<int> > curBlockConf; // curBlockConf[Y][X] // Track moving avg of txs which have been evicted from the mempool
// after failing to be confirmed within Y blocks
std::vector<std::vector<double>> failAvg; // failAvg[Y][X]
// Sum the total feerate of all tx's in each bucket // Sum the total feerate of all tx's in each bucket
// Track the historical moving average of this total over blocks // Track the historical moving average of this total over blocks
std::vector<double> avg; std::vector<double> avg;
// and calculate the total for the current block to update the moving average
std::vector<double> curBlockVal;
// Combine the conf counts with tx counts to calculate the confirmation % for each Y,X // Combine the conf counts with tx counts to calculate the confirmation % for each Y,X
// Combine the total value with the tx counts to calculate the avg feerate per bucket // Combine the total value with the tx counts to calculate the avg feerate per bucket
double decay; double decay;
// Resolution (# of blocks) with which confirmations are tracked
unsigned int scale;
// Mempool counts of outstanding transactions // Mempool counts of outstanding transactions
// For each bucket X, track the number of transactions in the mempool // For each bucket X, track the number of transactions in the mempool
// that are unconfirmed for each possible confirmation value Y // that are unconfirmed for each possible confirmation value Y
std::vector<std::vector<int> > unconfTxs; //unconfTxs[Y][X] std::vector<std::vector<int> > unconfTxs; //unconfTxs[Y][X]
// transactions still unconfirmed after MAX_CONFIRMS for each bucket // transactions still unconfirmed after GetMaxConfirms for each bucket
std::vector<int> oldUnconfTxs; std::vector<int> oldUnconfTxs;
void resizeInMemoryCounters(size_t newbuckets);
public: public:
/** /**
* Create new TxConfirmStats. This is called by BlockPolicyEstimator's * Create new TxConfirmStats. This is called by BlockPolicyEstimator's
@ -68,9 +73,10 @@ public:
* @param maxConfirms max number of confirms to track * @param maxConfirms max number of confirms to track
* @param decay how much to decay the historical moving average per block * @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,
unsigned int maxPeriods, double decay, unsigned int scale);
/** Clear the state of the curBlock variables to start counting for the new block */ /** Roll the circular buffer for unconfirmed txs*/
void ClearCurrent(unsigned int nBlockHeight); void ClearCurrent(unsigned int nBlockHeight);
/** /**
@ -86,7 +92,7 @@ public:
/** Remove a transaction from mempool tracking stats*/ /** Remove a transaction from mempool tracking stats*/
void removeTx(unsigned int entryHeight, unsigned int nBestSeenHeight, void removeTx(unsigned int entryHeight, unsigned int nBestSeenHeight,
unsigned int bucketIndex); unsigned int bucketIndex, bool inBlock);
/** Update our estimates by decaying our historical moving average and updating /** Update our estimates by decaying our historical moving average and updating
with the data gathered from the current block */ with the data gathered from the current block */
@ -104,10 +110,11 @@ public:
* @param nBlockHeight the current block height * @param nBlockHeight the current block height
*/ */
double EstimateMedianVal(int confTarget, double sufficientTxVal, double EstimateMedianVal(int confTarget, double sufficientTxVal,
double minSuccess, bool requireGreater, unsigned int nBlockHeight) const; double minSuccess, bool requireGreater, unsigned int nBlockHeight,
EstimationResult *result = nullptr) const;
/** Return the max number of confirms we're tracking */ /** Return the max number of confirms we're tracking */
unsigned int GetMaxConfirms() const { return confAvg.size(); } unsigned int GetMaxConfirms() const { return scale * confAvg.size(); }
/** Write state of estimation data to a file*/ /** Write state of estimation data to a file*/
void Write(CAutoFile& fileout) const; void Write(CAutoFile& fileout) const;
@ -116,44 +123,47 @@ public:
* Read saved state of estimation data from a file and replace all internal data structures and * Read saved state of estimation data from a file and replace all internal data structures and
* variables with this state. * variables with this state.
*/ */
void Read(CAutoFile& filein); void Read(CAutoFile& filein, int nFileVersion, size_t numBuckets);
}; };
TxConfirmStats::TxConfirmStats(const std::vector<double>& defaultBuckets, TxConfirmStats::TxConfirmStats(const std::vector<double>& defaultBuckets,
unsigned int maxConfirms, double _decay) const std::map<double, unsigned int>& defaultBucketMap,
unsigned int maxPeriods, double _decay, unsigned int _scale)
: buckets(defaultBuckets), bucketMap(defaultBucketMap)
{ {
decay = _decay; decay = _decay;
for (unsigned int i = 0; i < defaultBuckets.size(); i++) { scale = _scale;
buckets.push_back(defaultBuckets[i]); confAvg.resize(maxPeriods);
bucketMap[defaultBuckets[i]] = i; for (unsigned int i = 0; i < maxPeriods; i++) {
}
confAvg.resize(maxConfirms);
curBlockConf.resize(maxConfirms);
unconfTxs.resize(maxConfirms);
for (unsigned int i = 0; i < maxConfirms; i++) {
confAvg[i].resize(buckets.size()); confAvg[i].resize(buckets.size());
curBlockConf[i].resize(buckets.size()); }
unconfTxs[i].resize(buckets.size()); failAvg.resize(maxPeriods);
for (unsigned int i = 0; i < maxPeriods; i++) {
failAvg[i].resize(buckets.size());
} }
oldUnconfTxs.resize(buckets.size());
curBlockTxCt.resize(buckets.size());
txCtAvg.resize(buckets.size()); txCtAvg.resize(buckets.size());
curBlockVal.resize(buckets.size());
avg.resize(buckets.size()); avg.resize(buckets.size());
resizeInMemoryCounters(buckets.size());
} }
// Zero out the data for the current block void TxConfirmStats::resizeInMemoryCounters(size_t newbuckets) {
// newbuckets must be passed in because the buckets referred to during Read have not been updated yet.
unconfTxs.resize(GetMaxConfirms());
for (unsigned int i = 0; i < unconfTxs.size(); i++) {
unconfTxs[i].resize(newbuckets);
}
oldUnconfTxs.resize(newbuckets);
}
// Roll the unconfirmed txs circular buffer
void TxConfirmStats::ClearCurrent(unsigned int nBlockHeight) void TxConfirmStats::ClearCurrent(unsigned int nBlockHeight)
{ {
for (unsigned int j = 0; j < buckets.size(); j++) { for (unsigned int j = 0; j < buckets.size(); j++) {
oldUnconfTxs[j] += unconfTxs[nBlockHeight%unconfTxs.size()][j]; oldUnconfTxs[j] += unconfTxs[nBlockHeight%unconfTxs.size()][j];
unconfTxs[nBlockHeight%unconfTxs.size()][j] = 0; unconfTxs[nBlockHeight%unconfTxs.size()][j] = 0;
for (unsigned int i = 0; i < curBlockConf.size(); i++)
curBlockConf[i][j] = 0;
curBlockTxCt[j] = 0;
curBlockVal[j] = 0;
} }
} }
@ -163,33 +173,38 @@ void TxConfirmStats::Record(int blocksToConfirm, double val)
// blocksToConfirm is 1-based // blocksToConfirm is 1-based
if (blocksToConfirm < 1) if (blocksToConfirm < 1)
return; return;
int periodsToConfirm = (blocksToConfirm + scale - 1)/scale;
unsigned int bucketindex = bucketMap.lower_bound(val)->second; unsigned int bucketindex = bucketMap.lower_bound(val)->second;
for (size_t i = blocksToConfirm; i <= curBlockConf.size(); i++) { for (size_t i = periodsToConfirm; i <= confAvg.size(); i++) {
curBlockConf[i - 1][bucketindex]++; confAvg[i - 1][bucketindex]++;
} }
curBlockTxCt[bucketindex]++; txCtAvg[bucketindex]++;
curBlockVal[bucketindex] += val; avg[bucketindex] += val;
} }
void TxConfirmStats::UpdateMovingAverages() void TxConfirmStats::UpdateMovingAverages()
{ {
for (unsigned int j = 0; j < buckets.size(); j++) { for (unsigned int j = 0; j < buckets.size(); j++) {
for (unsigned int i = 0; i < confAvg.size(); i++) for (unsigned int i = 0; i < confAvg.size(); i++)
confAvg[i][j] = confAvg[i][j] * decay + curBlockConf[i][j]; confAvg[i][j] = confAvg[i][j] * decay;
avg[j] = avg[j] * decay + curBlockVal[j]; for (unsigned int i = 0; i < failAvg.size(); i++)
txCtAvg[j] = txCtAvg[j] * decay + curBlockTxCt[j]; failAvg[i][j] = failAvg[i][j] * decay;
avg[j] = avg[j] * decay;
txCtAvg[j] = txCtAvg[j] * decay;
} }
} }
// returns -1 on error conditions // returns -1 on error conditions
double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal, double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
double successBreakPoint, bool requireGreater, double successBreakPoint, bool requireGreater,
unsigned int nBlockHeight) const unsigned int nBlockHeight, EstimationResult *result) const
{ {
// Counters for a bucket (or range of buckets) // Counters for a bucket (or range of buckets)
double nConf = 0; // Number of tx's confirmed within the confTarget double nConf = 0; // Number of tx's confirmed within the confTarget
double totalNum = 0; // Total number of tx's that were ever confirmed double totalNum = 0; // Total number of tx's that were ever confirmed
int extraNum = 0; // Number of tx's still in mempool for confTarget or longer int extraNum = 0; // Number of tx's still in mempool for confTarget or longer
double failNum = 0; // Number of tx's that were never confirmed but removed from the mempool after confTarget
int periodTarget = (confTarget + scale - 1)/scale;
int maxbucketindex = buckets.size() - 1; int maxbucketindex = buckets.size() - 1;
@ -212,12 +227,21 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
bool foundAnswer = false; bool foundAnswer = false;
unsigned int bins = unconfTxs.size(); unsigned int bins = unconfTxs.size();
bool newBucketRange = true;
bool passing = true;
EstimatorBucket passBucket;
EstimatorBucket failBucket;
// Start counting from highest(default) or lowest feerate transactions // Start counting from highest(default) or lowest feerate transactions
for (int bucket = startbucket; bucket >= 0 && bucket <= maxbucketindex; bucket += step) { for (int bucket = startbucket; bucket >= 0 && bucket <= maxbucketindex; bucket += step) {
if (newBucketRange) {
curNearBucket = bucket;
newBucketRange = false;
}
curFarBucket = bucket; curFarBucket = bucket;
nConf += confAvg[confTarget - 1][bucket]; nConf += confAvg[periodTarget - 1][bucket];
totalNum += txCtAvg[bucket]; totalNum += txCtAvg[bucket];
failNum += failAvg[periodTarget - 1][bucket];
for (unsigned int confct = confTarget; confct < GetMaxConfirms(); confct++) for (unsigned int confct = confTarget; confct < GetMaxConfirms(); confct++)
extraNum += unconfTxs[(nBlockHeight - confct)%bins][bucket]; extraNum += unconfTxs[(nBlockHeight - confct)%bins][bucket];
extraNum += oldUnconfTxs[bucket]; extraNum += oldUnconfTxs[bucket];
@ -226,24 +250,41 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
// (Only count the confirmed data points, so that each confirmation count // (Only count the confirmed data points, so that each confirmation count
// will be looking at the same amount of data and same bucket breaks) // will be looking at the same amount of data and same bucket breaks)
if (totalNum >= sufficientTxVal / (1 - decay)) { if (totalNum >= sufficientTxVal / (1 - decay)) {
double curPct = nConf / (totalNum + extraNum); double curPct = nConf / (totalNum + failNum + extraNum);
// Check to see if we are no longer getting confirmed at the success rate // Check to see if we are no longer getting confirmed at the success rate
if (requireGreater && curPct < successBreakPoint) if ((requireGreater && curPct < successBreakPoint) || (!requireGreater && curPct > successBreakPoint)) {
break; if (passing == true) {
if (!requireGreater && curPct > successBreakPoint) // First time we hit a failure record the failed bucket
break; unsigned int failMinBucket = std::min(curNearBucket, curFarBucket);
unsigned int failMaxBucket = std::max(curNearBucket, curFarBucket);
failBucket.start = failMinBucket ? buckets[failMinBucket - 1] : 0;
failBucket.end = buckets[failMaxBucket];
failBucket.withinTarget = nConf;
failBucket.totalConfirmed = totalNum;
failBucket.inMempool = extraNum;
failBucket.leftMempool = failNum;
passing = false;
}
continue;
}
// Otherwise update the cumulative stats, and the bucket variables // Otherwise update the cumulative stats, and the bucket variables
// and reset the counters // and reset the counters
else { else {
failBucket = EstimatorBucket(); // Reset any failed bucket, currently passing
foundAnswer = true; foundAnswer = true;
passing = true;
passBucket.withinTarget = nConf;
nConf = 0; nConf = 0;
passBucket.totalConfirmed = totalNum;
totalNum = 0; totalNum = 0;
passBucket.inMempool = extraNum;
passBucket.leftMempool = failNum;
failNum = 0;
extraNum = 0; extraNum = 0;
bestNearBucket = curNearBucket; bestNearBucket = curNearBucket;
bestFarBucket = curFarBucket; bestFarBucket = curFarBucket;
curNearBucket = bucket + step; newBucketRange = true;
} }
} }
} }
@ -255,8 +296,8 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
// Find the bucket with the median transaction and then report the average feerate from that bucket // Find the bucket with the median transaction and then report the average feerate from that bucket
// This is a compromise between finding the median which we can't since we don't save all tx's // This is a compromise between finding the median which we can't since we don't save all tx's
// and reporting the average which is less accurate // and reporting the average which is less accurate
unsigned int minBucket = bestNearBucket < bestFarBucket ? bestNearBucket : bestFarBucket; unsigned int minBucket = std::min(bestNearBucket, bestFarBucket);
unsigned int maxBucket = bestNearBucket > bestFarBucket ? bestNearBucket : bestFarBucket; unsigned int maxBucket = std::max(bestNearBucket, bestFarBucket);
for (unsigned int j = minBucket; j <= maxBucket; j++) { for (unsigned int j = minBucket; j <= maxBucket; j++) {
txSum += txCtAvg[j]; txSum += txCtAvg[j];
} }
@ -270,83 +311,109 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
break; break;
} }
} }
passBucket.start = minBucket ? buckets[minBucket-1] : 0;
passBucket.end = buckets[maxBucket];
} }
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", // If we were passing until we reached last few buckets with insufficient data, then report those as failed
confTarget, requireGreater ? ">" : "<", successBreakPoint, if (passing && !newBucketRange) {
requireGreater ? ">" : "<", median, buckets[minBucket], buckets[maxBucket], unsigned int failMinBucket = std::min(curNearBucket, curFarBucket);
100 * nConf / (totalNum + extraNum), nConf, totalNum, extraNum); unsigned int failMaxBucket = std::max(curNearBucket, curFarBucket);
failBucket.start = failMinBucket ? buckets[failMinBucket - 1] : 0;
failBucket.end = buckets[failMaxBucket];
failBucket.withinTarget = nConf;
failBucket.totalConfirmed = totalNum;
failBucket.inMempool = extraNum;
failBucket.leftMempool = failNum;
}
LogPrint(BCLog::ESTIMATEFEE, "FeeEst: %d %s%.0f%% decay %.5f: feerate: %g from (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out) Fail: (%g - %g) %.2f%% %.1f/(%.1f %d mem %.1f out)\n",
confTarget, requireGreater ? ">" : "<", 100.0 * successBreakPoint, decay,
median, passBucket.start, passBucket.end,
100 * passBucket.withinTarget / (passBucket.totalConfirmed + passBucket.inMempool + passBucket.leftMempool),
passBucket.withinTarget, passBucket.totalConfirmed, passBucket.inMempool, passBucket.leftMempool,
failBucket.start, failBucket.end,
100 * failBucket.withinTarget / (failBucket.totalConfirmed + failBucket.inMempool + failBucket.leftMempool),
failBucket.withinTarget, failBucket.totalConfirmed, failBucket.inMempool, failBucket.leftMempool);
if (result) {
result->pass = passBucket;
result->fail = failBucket;
result->decay = decay;
result->scale = scale;
}
return median; return median;
} }
void TxConfirmStats::Write(CAutoFile& fileout) const void TxConfirmStats::Write(CAutoFile& fileout) const
{ {
fileout << decay; fileout << decay;
fileout << buckets; fileout << scale;
fileout << avg; fileout << avg;
fileout << txCtAvg; fileout << txCtAvg;
fileout << confAvg; fileout << confAvg;
fileout << failAvg;
} }
void TxConfirmStats::Read(CAutoFile& filein) void TxConfirmStats::Read(CAutoFile& filein, int nFileVersion, size_t numBuckets)
{ {
// Read data file into temporary variables and do some very basic sanity checking // Read data file and do some very basic sanity checking
std::vector<double> fileBuckets; // buckets and bucketMap are not updated yet, so don't access them
std::vector<double> fileAvg; // If there is a read failure, we'll just discard this entire object anyway
std::vector<std::vector<double> > fileConfAvg; size_t maxConfirms, maxPeriods;
std::vector<double> fileTxCtAvg;
double fileDecay; // The current version will store the decay with each individual TxConfirmStats and also keep a scale factor
size_t maxConfirms; if (nFileVersion >= 149900) {
size_t numBuckets; filein >> decay;
if (decay <= 0 || decay >= 1) {
filein >> fileDecay;
if (fileDecay <= 0 || fileDecay >= 1)
throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)"); throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)");
filein >> fileBuckets; }
numBuckets = fileBuckets.size(); filein >> scale;
if (numBuckets <= 1 || numBuckets > 1000) }
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
filein >> fileAvg; filein >> avg;
if (fileAvg.size() != numBuckets) if (avg.size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in feerate average bucket count"); throw std::runtime_error("Corrupt estimates file. Mismatch in feerate average bucket count");
filein >> fileTxCtAvg; }
if (fileTxCtAvg.size() != numBuckets) filein >> txCtAvg;
if (txCtAvg.size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in tx count bucket count"); throw std::runtime_error("Corrupt estimates file. Mismatch in tx count bucket count");
filein >> fileConfAvg; }
maxConfirms = fileConfAvg.size(); filein >> confAvg;
if (maxConfirms <= 0 || maxConfirms > 6 * 24 * 7) // one week maxPeriods = confAvg.size();
maxConfirms = scale * maxPeriods;
if (maxConfirms <= 0 || maxConfirms > 6 * 24 * 7) { // one week
throw std::runtime_error("Corrupt estimates file. Must maintain estimates for between 1 and 1008 (one week) confirms"); throw std::runtime_error("Corrupt estimates file. Must maintain estimates for between 1 and 1008 (one week) confirms");
for (unsigned int i = 0; i < maxConfirms; i++) { }
if (fileConfAvg[i].size() != numBuckets) for (unsigned int i = 0; i < maxPeriods; i++) {
if (confAvg[i].size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in feerate conf average bucket count"); throw std::runtime_error("Corrupt estimates file. Mismatch in feerate conf average bucket count");
} }
// Now that we've processed the entire feerate estimate data file and not
// thrown any errors, we can copy it to our data structures
decay = fileDecay;
buckets = fileBuckets;
avg = fileAvg;
confAvg = fileConfAvg;
txCtAvg = fileTxCtAvg;
bucketMap.clear();
// Resize the current block variables which aren't stored in the data file
// to match the number of confirms and buckets
curBlockConf.resize(maxConfirms);
for (unsigned int i = 0; i < maxConfirms; i++) {
curBlockConf[i].resize(buckets.size());
} }
curBlockTxCt.resize(buckets.size());
curBlockVal.resize(buckets.size());
unconfTxs.resize(maxConfirms); if (nFileVersion >= 149900) {
for (unsigned int i = 0; i < maxConfirms; i++) { filein >> failAvg;
unconfTxs[i].resize(buckets.size()); if (maxPeriods != failAvg.size()) {
throw std::runtime_error("Corrupt estimates file. Mismatch in confirms tracked for failures");
}
for (unsigned int i = 0; i < maxPeriods; i++) {
if (failAvg[i].size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in one of failure average bucket counts");
}
}
} else {
failAvg.resize(confAvg.size());
for (unsigned int i = 0; i < failAvg.size(); i++) {
failAvg[i].resize(numBuckets);
}
} }
oldUnconfTxs.resize(buckets.size());
for (unsigned int i = 0; i < buckets.size(); i++) // Resize the current block variables which aren't stored in the data file
bucketMap[buckets[i]] = i; // to match the number of confirms and buckets
resizeInMemoryCounters(numBuckets);
LogPrint(BCLog::ESTIMATEFEE, "Reading estimates: %u buckets counting confirms up to %u blocks\n", LogPrint(BCLog::ESTIMATEFEE, "Reading estimates: %u buckets counting confirms up to %u blocks\n",
numBuckets, maxConfirms); numBuckets, maxConfirms);
@ -360,7 +427,7 @@ unsigned int TxConfirmStats::NewTx(unsigned int nBlockHeight, double val)
return bucketindex; return bucketindex;
} }
void TxConfirmStats::removeTx(unsigned int entryHeight, unsigned int nBestSeenHeight, unsigned int bucketindex) void TxConfirmStats::removeTx(unsigned int entryHeight, unsigned int nBestSeenHeight, unsigned int bucketindex, bool inBlock)
{ {
//nBestSeenHeight is not updated yet for the new block //nBestSeenHeight is not updated yet for the new block
int blocksAgo = nBestSeenHeight - entryHeight; int blocksAgo = nBestSeenHeight - entryHeight;
@ -388,6 +455,12 @@ void TxConfirmStats::removeTx(unsigned int entryHeight, unsigned int nBestSeenHe
blockIndex, bucketindex); blockIndex, bucketindex);
} }
} }
if (!inBlock && (unsigned int)blocksAgo >= scale) { // Only counts as a failure if not confirmed for entire period
unsigned int periodsAgo = blocksAgo / scale;
for (size_t i = 0; i < periodsAgo && i < failAvg.size(); i++) {
failAvg[i][bucketindex]++;
}
}
} }
// This function is called from CTxMemPool::removeUnchecked to ensure // This function is called from CTxMemPool::removeUnchecked to ensure
@ -395,12 +468,14 @@ void TxConfirmStats::removeTx(unsigned int entryHeight, unsigned int nBestSeenHe
// tracked. Txs that were part of a block have already been removed in // tracked. Txs that were part of a block have already been removed in
// processBlockTx to ensure they are never double tracked, but it is // processBlockTx to ensure they are never double tracked, but it is
// of no harm to try to remove them again. // of no harm to try to remove them again.
bool CBlockPolicyEstimator::removeTx(uint256 hash) bool CBlockPolicyEstimator::removeTx(uint256 hash, bool inBlock)
{ {
LOCK(cs_feeEstimator); LOCK(cs_feeEstimator);
std::map<uint256, TxStatsInfo>::iterator pos = mapMemPoolTxs.find(hash); std::map<uint256, TxStatsInfo>::iterator pos = mapMemPoolTxs.find(hash);
if (pos != mapMemPoolTxs.end()) { if (pos != mapMemPoolTxs.end()) {
feeStats->removeTx(pos->second.blockHeight, nBestSeenHeight, pos->second.bucketIndex); feeStats->removeTx(pos->second.blockHeight, nBestSeenHeight, pos->second.bucketIndex, inBlock);
shortStats->removeTx(pos->second.blockHeight, nBestSeenHeight, pos->second.bucketIndex, inBlock);
longStats->removeTx(pos->second.blockHeight, nBestSeenHeight, pos->second.bucketIndex, inBlock);
mapMemPoolTxs.erase(hash); mapMemPoolTxs.erase(hash);
return true; return true;
} else { } else {
@ -409,21 +484,28 @@ bool CBlockPolicyEstimator::removeTx(uint256 hash)
} }
CBlockPolicyEstimator::CBlockPolicyEstimator() CBlockPolicyEstimator::CBlockPolicyEstimator()
: nBestSeenHeight(0), trackedTxs(0), untrackedTxs(0) : nBestSeenHeight(0), firstRecordedHeight(0), historicalFirst(0), historicalBest(0), trackedTxs(0), untrackedTxs(0)
{ {
static_assert(MIN_BUCKET_FEERATE > 0, "Min feerate must be nonzero"); static_assert(MIN_BUCKET_FEERATE > 0, "Min feerate must be nonzero");
minTrackedFee = CFeeRate(MIN_BUCKET_FEERATE); size_t bucketIndex = 0;
std::vector<double> vfeelist; for (double bucketBoundary = MIN_BUCKET_FEERATE; bucketBoundary <= MAX_BUCKET_FEERATE; bucketBoundary *= FEE_SPACING, bucketIndex++) {
for (double bucketBoundary = minTrackedFee.GetFeePerK(); bucketBoundary <= MAX_BUCKET_FEERATE; bucketBoundary *= FEE_SPACING) { buckets.push_back(bucketBoundary);
vfeelist.push_back(bucketBoundary); bucketMap[bucketBoundary] = bucketIndex;
} }
vfeelist.push_back(INF_FEERATE); buckets.push_back(INF_FEERATE);
feeStats = new TxConfirmStats(vfeelist, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY); bucketMap[INF_FEERATE] = bucketIndex;
assert(bucketMap.size() == buckets.size());
feeStats = new TxConfirmStats(buckets, bucketMap, MED_BLOCK_PERIODS, MED_DECAY, MED_SCALE);
shortStats = new TxConfirmStats(buckets, bucketMap, SHORT_BLOCK_PERIODS, SHORT_DECAY, SHORT_SCALE);
longStats = new TxConfirmStats(buckets, bucketMap, LONG_BLOCK_PERIODS, LONG_DECAY, LONG_SCALE);
} }
CBlockPolicyEstimator::~CBlockPolicyEstimator() CBlockPolicyEstimator::~CBlockPolicyEstimator()
{ {
delete feeStats; delete feeStats;
delete shortStats;
delete longStats;
} }
void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate) void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate)
@ -457,12 +539,17 @@ void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, boo
CFeeRate feeRate(entry.GetFee(), entry.GetTxSize()); CFeeRate feeRate(entry.GetFee(), entry.GetTxSize());
mapMemPoolTxs[hash].blockHeight = txHeight; mapMemPoolTxs[hash].blockHeight = txHeight;
mapMemPoolTxs[hash].bucketIndex = feeStats->NewTx(txHeight, (double)feeRate.GetFeePerK()); unsigned int bucketIndex = feeStats->NewTx(txHeight, (double)feeRate.GetFeePerK());
mapMemPoolTxs[hash].bucketIndex = bucketIndex;
unsigned int bucketIndex2 = shortStats->NewTx(txHeight, (double)feeRate.GetFeePerK());
assert(bucketIndex == bucketIndex2);
unsigned int bucketIndex3 = longStats->NewTx(txHeight, (double)feeRate.GetFeePerK());
assert(bucketIndex == bucketIndex3);
} }
bool CBlockPolicyEstimator::processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry) bool CBlockPolicyEstimator::processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry)
{ {
if (!removeTx(entry->GetTx().GetHash())) { if (!removeTx(entry->GetTx().GetHash(), true)) {
// This transaction wasn't being tracked for fee estimation // This transaction wasn't being tracked for fee estimation
return false; return false;
} }
@ -482,6 +569,8 @@ bool CBlockPolicyEstimator::processBlockTx(unsigned int nBlockHeight, const CTxM
CFeeRate feeRate(entry->GetFee(), entry->GetTxSize()); CFeeRate feeRate(entry->GetFee(), entry->GetTxSize());
feeStats->Record(blocksToConfirm, (double)feeRate.GetFeePerK()); feeStats->Record(blocksToConfirm, (double)feeRate.GetFeePerK());
shortStats->Record(blocksToConfirm, (double)feeRate.GetFeePerK());
longStats->Record(blocksToConfirm, (double)feeRate.GetFeePerK());
return true; return true;
} }
@ -503,21 +592,32 @@ void CBlockPolicyEstimator::processBlock(unsigned int nBlockHeight,
// of unconfirmed txs to remove from tracking. // of unconfirmed txs to remove from tracking.
nBestSeenHeight = nBlockHeight; nBestSeenHeight = nBlockHeight;
// Clear the current block state and update unconfirmed circular buffer // Update unconfirmed circular buffer
feeStats->ClearCurrent(nBlockHeight); feeStats->ClearCurrent(nBlockHeight);
shortStats->ClearCurrent(nBlockHeight);
longStats->ClearCurrent(nBlockHeight);
// Decay all exponential averages
feeStats->UpdateMovingAverages();
shortStats->UpdateMovingAverages();
longStats->UpdateMovingAverages();
unsigned int countedTxs = 0; unsigned int countedTxs = 0;
// Repopulate the current block states // Update averages with data points from current block
for (unsigned int i = 0; i < entries.size(); i++) { for (unsigned int i = 0; i < entries.size(); i++) {
if (processBlockTx(nBlockHeight, entries[i])) if (processBlockTx(nBlockHeight, entries[i]))
countedTxs++; countedTxs++;
} }
// Update all exponential averages with the current block state if (firstRecordedHeight == 0 && countedTxs > 0) {
feeStats->UpdateMovingAverages(); firstRecordedHeight = nBestSeenHeight;
LogPrint(BCLog::ESTIMATEFEE, "Blockpolicy first recorded height %u\n", firstRecordedHeight);
}
LogPrint(BCLog::ESTIMATEFEE, "Blockpolicy after updating estimates for %u of %u txs in block, since last block %u of %u tracked, new mempool map size %u\n",
countedTxs, entries.size(), trackedTxs, trackedTxs + untrackedTxs, mapMemPoolTxs.size()); LogPrint(BCLog::ESTIMATEFEE, "Blockpolicy estimates updated by %u of %u block txs, since last block %u of %u tracked, mempool map size %u, max target %u from %s\n",
countedTxs, entries.size(), trackedTxs, trackedTxs + untrackedTxs, mapMemPoolTxs.size(),
MaxUsableEstimate(), HistoricalBlockSpan() > BlockSpan() ? "historical" : "current");
trackedTxs = 0; trackedTxs = 0;
untrackedTxs = 0; untrackedTxs = 0;
@ -525,13 +625,44 @@ void CBlockPolicyEstimator::processBlock(unsigned int nBlockHeight,
CFeeRate CBlockPolicyEstimator::estimateFee(int confTarget) const CFeeRate CBlockPolicyEstimator::estimateFee(int confTarget) const
{ {
// It's not possible to get reasonable estimates for confTarget of 1
if (confTarget <= 1)
return CFeeRate(0);
return estimateRawFee(confTarget, DOUBLE_SUCCESS_PCT, FeeEstimateHorizon::MED_HALFLIFE);
}
CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult* result) const
{
TxConfirmStats* stats;
double sufficientTxs = SUFFICIENT_FEETXS;
switch (horizon) {
case FeeEstimateHorizon::SHORT_HALFLIFE: {
stats = shortStats;
sufficientTxs = SUFFICIENT_TXS_SHORT;
break;
}
case FeeEstimateHorizon::MED_HALFLIFE: {
stats = feeStats;
break;
}
case FeeEstimateHorizon::LONG_HALFLIFE: {
stats = longStats;
break;
}
default: {
return CFeeRate(0);
}
}
LOCK(cs_feeEstimator); LOCK(cs_feeEstimator);
// Return failure if trying to analyze a target we're not tracking // Return failure if trying to analyze a target we're not tracking
// It's not possible to get reasonable estimates for confTarget of 1 if (confTarget <= 0 || (unsigned int)confTarget > stats->GetMaxConfirms())
if (confTarget <= 1 || (unsigned int)confTarget > feeStats->GetMaxConfirms()) return CFeeRate(0);
if (successThreshold > 1)
return CFeeRate(0); return CFeeRate(0);
double median = feeStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, MIN_SUCCESS_PCT, true, nBestSeenHeight); double median = stats->EstimateMedianVal(confTarget, sufficientTxs, successThreshold, true, nBestSeenHeight, result);
if (median < 0) if (median < 0)
return CFeeRate(0); return CFeeRate(0);
@ -539,31 +670,148 @@ CFeeRate CBlockPolicyEstimator::estimateFee(int confTarget) const
return CFeeRate(median); return CFeeRate(median);
} }
CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const unsigned int CBlockPolicyEstimator::BlockSpan() const
{
if (firstRecordedHeight == 0) return 0;
assert(nBestSeenHeight >= firstRecordedHeight);
return nBestSeenHeight - firstRecordedHeight;
}
unsigned int CBlockPolicyEstimator::HistoricalBlockSpan() const
{
if (historicalFirst == 0) return 0;
assert(historicalBest >= historicalFirst);
if (nBestSeenHeight - historicalBest > OLDEST_ESTIMATE_HISTORY) return 0;
return historicalBest - historicalFirst;
}
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);
}
/** Return a fee estimate at the required successThreshold from the shortest
* time horizon which tracks confirmations up to the desired target. If
* checkShorterHorizon is requested, also allow short time horizon estimates
* for a lower target to reduce the given answer */
double CBlockPolicyEstimator::estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon) const
{
double estimate = -1;
if (confTarget >= 1 && confTarget <= longStats->GetMaxConfirms()) {
// Find estimate from shortest time horizon possible
if (confTarget <= shortStats->GetMaxConfirms()) { // short horizon
estimate = shortStats->EstimateMedianVal(confTarget, SUFFICIENT_TXS_SHORT, successThreshold, true, nBestSeenHeight);
}
else if (confTarget <= feeStats->GetMaxConfirms()) { // medium horizon
estimate = feeStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
}
else { // long horizon
estimate = longStats->EstimateMedianVal(confTarget, SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
}
if (checkShorterHorizon) {
// If a lower confTarget from a more recent horizon returns a lower answer use it.
if (confTarget > feeStats->GetMaxConfirms()) {
double medMax = feeStats->EstimateMedianVal(feeStats->GetMaxConfirms(), SUFFICIENT_FEETXS, successThreshold, true, nBestSeenHeight);
if (medMax > 0 && (estimate == -1 || medMax < estimate))
estimate = medMax;
}
if (confTarget > shortStats->GetMaxConfirms()) {
double shortMax = shortStats->EstimateMedianVal(shortStats->GetMaxConfirms(), SUFFICIENT_TXS_SHORT, successThreshold, true, nBestSeenHeight);
if (shortMax > 0 && (estimate == -1 || shortMax < estimate))
estimate = shortMax;
}
}
}
return estimate;
}
/** Ensure that for a conservative estimate, the DOUBLE_SUCCESS_PCT is also met
* at 2 * target for any longer time horizons.
*/
double CBlockPolicyEstimator::estimateConservativeFee(unsigned int doubleTarget) const
{
double estimate = -1;
if (doubleTarget <= shortStats->GetMaxConfirms()) {
estimate = feeStats->EstimateMedianVal(doubleTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
}
if (doubleTarget <= feeStats->GetMaxConfirms()) {
double longEstimate = longStats->EstimateMedianVal(doubleTarget, SUFFICIENT_FEETXS, DOUBLE_SUCCESS_PCT, true, nBestSeenHeight);
if (longEstimate > estimate) {
estimate = longEstimate;
}
}
return estimate;
}
/** estimateSmartFee returns the max of the feerates calculated with a 60%
* threshold required at target / 2, an 85% threshold required at target and a
* 95% threshold required at 2 * target. Each calculation is performed at the
* shortest time horizon which tracks the required target. Conservative
* estimates, however, required the 95% threshold at 2 * target be met for any
* longer time horizons also.
*/
CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool, bool conservative) const
{ {
if (answerFoundAtTarget) if (answerFoundAtTarget)
*answerFoundAtTarget = confTarget; *answerFoundAtTarget = confTarget;
double median = -1; double median = -1;
{ {
LOCK(cs_feeEstimator); LOCK(cs_feeEstimator);
// Return failure if trying to analyze a target we're not tracking // Return failure if trying to analyze a target we're not tracking
if (confTarget <= 0 || (unsigned int)confTarget > feeStats->GetMaxConfirms()) if (confTarget <= 0 || (unsigned int)confTarget > longStats->GetMaxConfirms())
return CFeeRate(0); return CFeeRate(0);
// It's not possible to get reasonable estimates for confTarget of 1 // It's not possible to get reasonable estimates for confTarget of 1
if (confTarget == 1) if (confTarget == 1)
confTarget = 2; confTarget = 2;
while (median < 0 && (unsigned int)confTarget <= feeStats->GetMaxConfirms()) { unsigned int maxUsableEstimate = MaxUsableEstimate();
median = feeStats->EstimateMedianVal(confTarget++, SUFFICIENT_FEETXS, MIN_SUCCESS_PCT, true, nBestSeenHeight); if (maxUsableEstimate <= 1)
return CFeeRate(0);
if ((unsigned int)confTarget > maxUsableEstimate) {
confTarget = maxUsableEstimate;
}
assert(confTarget > 0); //estimateCombinedFee and estimateConservativeFee take unsigned ints
/** true is passed to estimateCombined fee for target/2 and target so
* that we check the max confirms for shorter time horizons as well.
* This is necessary to preserve monotonically increasing estimates.
* For non-conservative estimates we do the same thing for 2*target, but
* for conservative estimates we want to skip these shorter horizons
* checks for 2*target becuase we are taking the max over all time
* horizons so we already have monotonically increasing estimates and
* the purpose of conservative estimates is not to let short term
* fluctuations lower our estimates by too much.
*/
double halfEst = estimateCombinedFee(confTarget/2, HALF_SUCCESS_PCT, true);
double actualEst = estimateCombinedFee(confTarget, SUCCESS_PCT, true);
double doubleEst = estimateCombinedFee(2 * confTarget, DOUBLE_SUCCESS_PCT, !conservative);
median = halfEst;
if (actualEst > median) {
median = actualEst;
}
if (doubleEst > median) {
median = doubleEst;
}
if (conservative || median == -1) {
double consEst = estimateConservativeFee(2 * confTarget);
if (consEst > median) {
median = consEst;
}
} }
} // Must unlock cs_feeEstimator before taking mempool locks } // Must unlock cs_feeEstimator before taking mempool locks
if (answerFoundAtTarget) if (answerFoundAtTarget)
*answerFoundAtTarget = confTarget - 1; *answerFoundAtTarget = confTarget;
// If mempool is limiting txs , return at least the min feerate from the mempool // If mempool is limiting txs , return at least the min feerate from the mempool
CAmount minPoolFee = pool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000).GetFeePerK(); CAmount minPoolFee = pool.GetMinFee(GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000).GetFeePerK();
@ -576,14 +824,24 @@ CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, int *answerFoun
return CFeeRate(median); return CFeeRate(median);
} }
bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
{ {
try { try {
LOCK(cs_feeEstimator); 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
fileout << CLIENT_VERSION; // version that wrote the file fileout << CLIENT_VERSION; // version that wrote the file
fileout << nBestSeenHeight; fileout << nBestSeenHeight;
if (BlockSpan() > HistoricalBlockSpan()/2) {
fileout << firstRecordedHeight << nBestSeenHeight;
}
else {
fileout << historicalFirst << historicalBest;
}
fileout << buckets;
feeStats->Write(fileout); feeStats->Write(fileout);
shortStats->Write(fileout);
longStats->Write(fileout);
} }
catch (const std::exception&) { catch (const std::exception&) {
LogPrintf("CBlockPolicyEstimator::Write(): unable to write policy estimator data (non-fatal)\n"); LogPrintf("CBlockPolicyEstimator::Write(): unable to write policy estimator data (non-fatal)\n");
@ -596,27 +854,104 @@ bool CBlockPolicyEstimator::Read(CAutoFile& filein)
{ {
try { try {
LOCK(cs_feeEstimator); LOCK(cs_feeEstimator);
int nVersionRequired, nVersionThatWrote, nFileBestSeenHeight; int nVersionRequired, nVersionThatWrote;
unsigned int nFileBestSeenHeight, nFileHistoricalFirst, nFileHistoricalBest;
filein >> nVersionRequired >> nVersionThatWrote; filein >> nVersionRequired >> nVersionThatWrote;
if (nVersionRequired > CLIENT_VERSION) if (nVersionRequired > CLIENT_VERSION)
return error("CBlockPolicyEstimator::Read(): up-version (%d) fee estimate file", nVersionRequired); return error("CBlockPolicyEstimator::Read(): up-version (%d) fee estimate file", nVersionRequired);
// Read fee estimates file into temporary variables so existing data
// structures aren't corrupted if there is an exception.
filein >> nFileBestSeenHeight; filein >> nFileBestSeenHeight;
feeStats->Read(filein);
nBestSeenHeight = nFileBestSeenHeight; if (nVersionThatWrote < 149900) {
// Read the old fee estimates file for temporary use, but then discard. Will start collecting data from scratch.
// decay is stored before buckets in old versions, so pre-read decay and pass into TxConfirmStats constructor
double tempDecay;
filein >> tempDecay;
if (tempDecay <= 0 || tempDecay >= 1)
throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)");
std::vector<double> tempBuckets;
filein >> tempBuckets;
size_t tempNum = tempBuckets.size();
if (tempNum <= 1 || tempNum > 1000)
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
std::map<double, unsigned int> tempMap;
std::unique_ptr<TxConfirmStats> tempFeeStats(new TxConfirmStats(tempBuckets, tempMap, MED_BLOCK_PERIODS, tempDecay, 1));
tempFeeStats->Read(filein, nVersionThatWrote, tempNum);
// if nVersionThatWrote < 139900 then another TxConfirmStats (for priority) follows but can be ignored. // if nVersionThatWrote < 139900 then another TxConfirmStats (for priority) follows but can be ignored.
tempMap.clear();
for (unsigned int i = 0; i < tempBuckets.size(); i++) {
tempMap[tempBuckets[i]] = i;
} }
catch (const std::exception&) { }
LogPrintf("CBlockPolicyEstimator::Read(): unable to read policy estimator data (non-fatal)\n"); else { // nVersionThatWrote >= 149900
filein >> nFileHistoricalFirst >> nFileHistoricalBest;
if (nFileHistoricalFirst > nFileHistoricalBest || nFileHistoricalBest > nFileBestSeenHeight) {
throw std::runtime_error("Corrupt estimates file. Historical block range for estimates is invalid");
}
std::vector<double> fileBuckets;
filein >> fileBuckets;
size_t numBuckets = fileBuckets.size();
if (numBuckets <= 1 || numBuckets > 1000)
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
std::unique_ptr<TxConfirmStats> fileFeeStats(new TxConfirmStats(buckets, bucketMap, MED_BLOCK_PERIODS, MED_DECAY, MED_SCALE));
std::unique_ptr<TxConfirmStats> fileShortStats(new TxConfirmStats(buckets, bucketMap, SHORT_BLOCK_PERIODS, SHORT_DECAY, SHORT_SCALE));
std::unique_ptr<TxConfirmStats> fileLongStats(new TxConfirmStats(buckets, bucketMap, LONG_BLOCK_PERIODS, LONG_DECAY, LONG_SCALE));
fileFeeStats->Read(filein, nVersionThatWrote, numBuckets);
fileShortStats->Read(filein, nVersionThatWrote, numBuckets);
fileLongStats->Read(filein, nVersionThatWrote, numBuckets);
// Fee estimates file parsed correctly
// Copy buckets from file and refresh our bucketmap
buckets = fileBuckets;
bucketMap.clear();
for (unsigned int i = 0; i < buckets.size(); i++) {
bucketMap[buckets[i]] = i;
}
// Destroy old TxConfirmStats and point to new ones that already reference buckets and bucketMap
delete feeStats;
delete shortStats;
delete longStats;
feeStats = fileFeeStats.release();
shortStats = fileShortStats.release();
longStats = fileLongStats.release();
nBestSeenHeight = nFileBestSeenHeight;
historicalFirst = nFileHistoricalFirst;
historicalBest = nFileHistoricalBest;
}
}
catch (const std::exception& e) {
LogPrintf("CBlockPolicyEstimator::Read(): unable to read policy estimator data (non-fatal): %s\n",e.what());
return false; return false;
} }
return true; return true;
} }
void CBlockPolicyEstimator::FlushUnconfirmed(CTxMemPool& pool) {
int64_t startclear = GetTimeMicros();
std::vector<uint256> txids;
pool.queryHashes(txids);
LOCK(cs_feeEstimator);
for (auto& txid : txids) {
removeTx(txid, false);
}
int64_t endclear = GetTimeMicros();
LogPrint(BCLog::ESTIMATEFEE, "Recorded %u unconfirmed txs from mempool in %ld micros\n",txids.size(), endclear - startclear);
}
FeeFilterRounder::FeeFilterRounder(const CFeeRate& minIncrementalFee) FeeFilterRounder::FeeFilterRounder(const CFeeRate& minIncrementalFee)
{ {
CAmount minFeeLimit = std::max(CAmount(1), minIncrementalFee.GetFeePerK() / 2); CAmount minFeeLimit = std::max(CAmount(1), minIncrementalFee.GetFeePerK() / 2);
feeset.insert(0); feeset.insert(0);
for (double bucketBoundary = minFeeLimit; bucketBoundary <= MAX_BUCKET_FEERATE; bucketBoundary *= FEE_SPACING) { for (double bucketBoundary = minFeeLimit; bucketBoundary <= MAX_FILTER_FEERATE; bucketBoundary *= FEE_FILTER_SPACING) {
feeset.insert(bucketBoundary); feeset.insert(bucketBoundary);
} }
} }

190
src/policy/fees.h

@ -42,53 +42,57 @@ class TxConfirmStats;
* within your desired 5 blocks. * within your desired 5 blocks.
* *
* Here is a brief description of the implementation: * Here is a brief description of the implementation:
* When a transaction enters the mempool, we * When a transaction enters the mempool, we track the height of the block chain
* track the height of the block chain at entry. Whenever a block comes in, * at entry. All further calculations are conducted only on this set of "seen"
* we count the number of transactions in each bucket and the total amount of feerate * transactions. Whenever a block comes in, we count the number of transactions
* paid in each bucket. Then we calculate how many blocks Y it took each * in each bucket and the total amount of feerate paid in each bucket. Then we
* transaction to be mined and we track an array of counters in each bucket * calculate how many blocks Y it took each transaction to be mined. We convert
* for how long it to took transactions to get confirmed from 1 to a max of 25 * from a number of blocks to a number of periods Y' each encompassing "scale"
* and we increment all the counters from Y up to 25. This is because for any * blocks. This is is tracked in 3 different data sets each up to a maximum
* number Z>=Y the transaction was successfully mined within Z blocks. We * number of periods. Within each data set we have an array of counters in each
* want to save a history of this information, so at any time we have a * feerate bucket and we increment all the counters from Y' up to max periods
* counter of the total number of transactions that happened in a given feerate * representing that a tx was successfullly confirmed in less than or equal to
* bucket and the total number that were confirmed in each number 1-25 blocks * that many periods. We want to save a history of this information, so at any
* or less for any bucket. We save this history by keeping an exponentially * time we have a counter of the total number of transactions that happened in a
* decaying moving average of each one of these stats. Furthermore we also * given feerate bucket and the total number that were confirmed in each of the
* keep track of the number unmined (in mempool) transactions in each bucket * periods or less for any bucket. We save this history by keeping an
* and for how many blocks they have been outstanding and use that to increase * exponentially decaying moving average of each one of these stats. This is
* the number of transactions we've seen in that feerate bucket when calculating * done for a different decay in each of the 3 data sets to keep relevant data
* an estimate for any number of confirmations below the number of blocks * from different time horizons. Furthermore we also keep track of the number
* they've been outstanding. * unmined (in mempool or left mempool without being included in a block)
* transactions in each bucket and for how many blocks they have been
* outstanding and use both of these numbers to increase the number of transactions
* we've seen in that feerate bucket when calculating an estimate for any number
* of confirmations below the number of blocks they've been outstanding.
*/ */
/** Track confirm delays up to 25 blocks, can't estimate beyond that */ /* Identifier for each of the 3 different TxConfirmStats which will track
static const unsigned int MAX_BLOCK_CONFIRMS = 25; * history over different time horizons. */
enum FeeEstimateHorizon {
/** Decay of .998 is a half-life of 346 blocks or about 2.4 days */ SHORT_HALFLIFE = 0,
static const double DEFAULT_DECAY = .998; MED_HALFLIFE = 1,
LONG_HALFLIFE = 2
/** Require greater than 95% of X feerate transactions to be confirmed within Y blocks for X to be big enough */ };
static const double MIN_SUCCESS_PCT = .95;
/** Require an avg of 1 tx in the combined feerate bucket per block to have stat significance */
static const double SUFFICIENT_FEETXS = 1;
// Minimum and Maximum values for tracking feerates /* Used to return detailed information about a feerate bucket */
// The MIN_BUCKET_FEERATE should just be set to the lowest reasonable feerate we struct EstimatorBucket
// might ever want to track. Historically this has been 1000 since it was {
// inheriting DEFAULT_MIN_RELAY_TX_FEE and changing it is disruptive as it double start = -1;
// invalidates old estimates files. So leave it at 1000 unless it becomes double end = -1;
// necessary to lower it, and then lower it substantially. double withinTarget = 0;
static constexpr double MIN_BUCKET_FEERATE = 1000; double totalConfirmed = 0;
static const double MAX_BUCKET_FEERATE = 1e7; double inMempool = 0;
static const double INF_FEERATE = MAX_MONEY; double leftMempool = 0;
};
// We have to lump transactions into buckets based on feerate, but we want to be able /* Used to return detailed information about a fee estimate calculation */
// to give accurate estimates over a large range of potential feerates struct EstimationResult
// Therefore it makes sense to exponentially space the buckets {
/** Spacing of FeeRate buckets */ EstimatorBucket pass;
static const double FEE_SPACING = 1.1; EstimatorBucket fail;
double decay;
unsigned int scale;
};
/** /**
* We want to be able to estimate feerates that are needed on tx's to be included in * We want to be able to estimate feerates that are needed on tx's to be included in
@ -97,6 +101,55 @@ static const double FEE_SPACING = 1.1;
*/ */
class CBlockPolicyEstimator class CBlockPolicyEstimator
{ {
private:
/** Track confirm delays up to 12 blocks for short horizon */
static constexpr unsigned int SHORT_BLOCK_PERIODS = 12;
static constexpr unsigned int SHORT_SCALE = 1;
/** Track confirm delays up to 48 blocks for medium horizon */
static constexpr unsigned int MED_BLOCK_PERIODS = 24;
static constexpr unsigned int MED_SCALE = 2;
/** Track confirm delays up to 1008 blocks for long horizon */
static constexpr unsigned int LONG_BLOCK_PERIODS = 42;
static constexpr unsigned int LONG_SCALE = 24;
/** Historical estimates that are older than this aren't valid */
static const unsigned int OLDEST_ESTIMATE_HISTORY = 6 * 1008;
/** Decay of .962 is a half-life of 18 blocks or about 3 hours */
static constexpr double SHORT_DECAY = .962;
/** Decay of .998 is a half-life of 144 blocks or about 1 day */
static constexpr double MED_DECAY = .9952;
/** Decay of .9995 is a half-life of 1008 blocks or about 1 week */
static constexpr double LONG_DECAY = .99931;
/** Require greater than 60% of X feerate transactions to be confirmed within Y/2 blocks*/
static constexpr double HALF_SUCCESS_PCT = .6;
/** Require greater than 85% of X feerate transactions to be confirmed within Y blocks*/
static constexpr double SUCCESS_PCT = .85;
/** Require greater than 95% of X feerate transactions to be confirmed within 2 * Y blocks*/
static constexpr double DOUBLE_SUCCESS_PCT = .95;
/** 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 fewer blocks considered*/
static constexpr double SUFFICIENT_TXS_SHORT = 0.5;
/** Minimum and Maximum values for tracking feerates
* The MIN_BUCKET_FEERATE should just be set to the lowest reasonable feerate we
* might ever want to track. Historically this has been 1000 since it was
* inheriting DEFAULT_MIN_RELAY_TX_FEE and changing it is disruptive as it
* invalidates old estimates files. So leave it at 1000 unless it becomes
* necessary to lower it, and then lower it substantially.
*/
static constexpr double MIN_BUCKET_FEERATE = 1000;
static constexpr double MAX_BUCKET_FEERATE = 1e7;
/** Spacing of FeeRate buckets
* We have to lump transactions into buckets based on feerate, but we want to be able
* to give accurate estimates over a large range of potential feerates
* Therefore it makes sense to exponentially space the buckets
*/
static constexpr double FEE_SPACING = 1.05;
public: public:
/** Create new BlockPolicyEstimator and initialize stats tracking classes with default values */ /** Create new BlockPolicyEstimator and initialize stats tracking classes with default values */
CBlockPolicyEstimator(); CBlockPolicyEstimator();
@ -110,16 +163,23 @@ public:
void processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate); void processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate);
/** Remove a transaction from the mempool tracking stats*/ /** Remove a transaction from the mempool tracking stats*/
bool removeTx(uint256 hash); bool removeTx(uint256 hash, bool inBlock);
/** Return a feerate estimate */ /** DEPRECATED. Return a feerate estimate */
CFeeRate estimateFee(int confTarget) const; CFeeRate estimateFee(int confTarget) const;
/** Estimate feerate needed to get be included in a block within /** Estimate feerate needed to get be included in a block within confTarget
* confTarget blocks. If no answer can be given at confTarget, return an * blocks. If no answer can be given at confTarget, return an estimate at
* estimate at the lowest target where one can be given. * the closest target where one can be given. 'conservative' estimates are
* valid over longer time horizons also.
*/
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool, bool conservative = true) const;
/** Return a specific fee estimate calculation with a given success
* threshold and time horizon, and optionally return detailed data about
* calculation
*/ */
CFeeRate estimateSmartFee(int confTarget, int *answerFoundAtTarget, const CTxMemPool& pool) const; CFeeRate estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult *result = nullptr) const;
/** Write estimation data to a file */ /** Write estimation data to a file */
bool Write(CAutoFile& fileout) const; bool Write(CAutoFile& fileout) const;
@ -127,9 +187,15 @@ public:
/** Read estimation data from a file */ /** Read estimation data from a file */
bool Read(CAutoFile& filein); bool Read(CAutoFile& filein);
/** Empty mempool transactions on shutdown to record failure to confirm for txs still in mempool */
void FlushUnconfirmed(CTxMemPool& pool);
private: private:
CFeeRate minTrackedFee; //!< Passed to constructor to avoid dependency on main
unsigned int nBestSeenHeight; unsigned int nBestSeenHeight;
unsigned int firstRecordedHeight;
unsigned int historicalFirst;
unsigned int historicalBest;
struct TxStatsInfo struct TxStatsInfo
{ {
unsigned int blockHeight; unsigned int blockHeight;
@ -142,19 +208,42 @@ private:
/** Classes to track historical data on transaction confirmations */ /** Classes to track historical data on transaction confirmations */
TxConfirmStats* feeStats; TxConfirmStats* feeStats;
TxConfirmStats* shortStats;
TxConfirmStats* longStats;
unsigned int trackedTxs; unsigned int trackedTxs;
unsigned int untrackedTxs; unsigned int untrackedTxs;
std::vector<double> buckets; // The upper-bound of the range for the bucket (inclusive)
std::map<double, unsigned int> bucketMap; // Map of bucket upper-bound to index into all vectors by bucket
mutable CCriticalSection cs_feeEstimator; mutable CCriticalSection cs_feeEstimator;
/** Process a transaction confirmed in a block*/ /** Process a transaction confirmed in a block*/
bool processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry); bool processBlockTx(unsigned int nBlockHeight, const CTxMemPoolEntry* entry);
/** Helper for estimateSmartFee */
double estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon) const;
/** Helper for estimateSmartFee */
double estimateConservativeFee(unsigned int doubleTarget) const;
/** Number of blocks of data recorded while fee estimates have been running */
unsigned int BlockSpan() const;
/** Number of blocks of recorded fee estimate data represented in saved data file */
unsigned int HistoricalBlockSpan() const;
/** Calculation of highest target that reasonable estimate can be provided for */
unsigned int MaxUsableEstimate() const;
}; };
class FeeFilterRounder class FeeFilterRounder
{ {
private:
static constexpr double MAX_FILTER_FEERATE = 1e7;
/** FEE_FILTER_SPACING is just used to provide some quantization of fee
* filter results. Historically it reused FEE_SPACING, but it is completely
* unrelated, and was made a separate constant so the two concepts are not
* tied together */
static constexpr double FEE_FILTER_SPACING = 1.1;
public: public:
/** Create new FeeFilterRounder */ /** Create new FeeFilterRounder */
FeeFilterRounder(const CFeeRate& minIncrementalFee); FeeFilterRounder(const CFeeRate& minIncrementalFee);
@ -166,4 +255,5 @@ private:
std::set<double> feeset; std::set<double> feeset;
FastRandomContext insecure_rand; FastRandomContext insecure_rand;
}; };
#endif /*BITCOIN_POLICYESTIMATOR_H */ #endif /*BITCOIN_POLICYESTIMATOR_H */

4
src/rpc/client.cpp

@ -108,6 +108,10 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getrawmempool", 0, "verbose" }, { "getrawmempool", 0, "verbose" },
{ "estimatefee", 0, "nblocks" }, { "estimatefee", 0, "nblocks" },
{ "estimatesmartfee", 0, "nblocks" }, { "estimatesmartfee", 0, "nblocks" },
{ "estimatesmartfee", 1, "conservative" },
{ "estimaterawfee", 0, "nblocks" },
{ "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" },

103
src/rpc/mining.cpp

@ -797,6 +797,7 @@ UniValue estimatefee(const JSONRPCRequest& request)
if (request.fHelp || request.params.size() != 1) if (request.fHelp || request.params.size() != 1)
throw std::runtime_error( throw std::runtime_error(
"estimatefee nblocks\n" "estimatefee nblocks\n"
"\nDEPRECATED. Please use estimatesmartfee for more intelligent estimates."
"\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. Uses virtual transaction size of transaction\n" "confirmation within nblocks blocks. Uses virtual transaction size of transaction\n"
"as defined in BIP 141 (witness data is discounted).\n" "as defined in BIP 141 (witness data is discounted).\n"
@ -828,16 +829,19 @@ UniValue estimatefee(const JSONRPCRequest& request)
UniValue estimatesmartfee(const JSONRPCRequest& request) UniValue estimatesmartfee(const JSONRPCRequest& request)
{ {
if (request.fHelp || request.params.size() != 1) if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error( throw std::runtime_error(
"estimatesmartfee nblocks\n" "estimatesmartfee nblocks (conservative)\n"
"\nWARNING: This interface is unstable and may disappear or change!\n"
"\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 and return the number of blocks\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" "for which the estimate is valid. 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)\n"
"2. conservative (bool, optional, default=true) Whether to return a more conservative estimate which\n"
" also satisfies a longer history. A conservative estimate potentially returns a higher\n"
" feerate and is more likely to be sufficient for the desired target, but is not as\n"
" responsive to short term drops in the prevailing fee market\n"
"\nResult:\n" "\nResult:\n"
"{\n" "{\n"
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n" " \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
@ -854,15 +858,102 @@ UniValue estimatesmartfee(const JSONRPCRequest& request)
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM)); RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM));
int nBlocks = request.params[0].get_int(); int nBlocks = request.params[0].get_int();
bool conservative = true;
if (request.params.size() > 1 && !request.params[1].isNull()) {
RPCTypeCheckArgument(request.params[1], UniValue::VBOOL);
conservative = request.params[1].get_bool();
}
UniValue result(UniValue::VOBJ); UniValue result(UniValue::VOBJ);
int answerFound; int answerFound;
CFeeRate feeRate = ::feeEstimator.estimateSmartFee(nBlocks, &answerFound, ::mempool); CFeeRate feeRate = ::feeEstimator.estimateSmartFee(nBlocks, &answerFound, ::mempool, conservative);
result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK()))); result.push_back(Pair("feerate", feeRate == CFeeRate(0) ? -1.0 : ValueFromAmount(feeRate.GetFeePerK())));
result.push_back(Pair("blocks", answerFound)); result.push_back(Pair("blocks", answerFound));
return result; return result;
} }
UniValue estimaterawfee(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() < 1|| request.params.size() > 3)
throw std::runtime_error(
"estimaterawfee nblocks (threshold horizon)\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"
" implementation of fee estimation. The parameters it can be called with\n"
" and the results it returns will change if the internal implementation changes.\n"
"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
"confirmation within nblocks blocks if possible. Uses virtual transaction size as defined\n"
"in BIP 141 (witness data is discounted).\n"
"\nArguments:\n"
"1. nblocks (numeric)\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"
" 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"
"{\n"
" \"feerate\" : x.x, (numeric) estimate fee-per-kilobyte (in BTC)\n"
" \"decay\" : x.x, (numeric) exponential decay (per block) for historical moving average of confirmation data\n"
" \"scale\" : x, (numeric) The resolution of confirmation targets at this time horizon\n"
" \"pass\" : { (json object) information about the lowest range of feerates to succeed in meeting the threshold\n"
" \"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 that were confirmed at any point\n"
" \"inmempool\" : x.x, (numeric) current number of txs in mempool in the feerate range unconfirmed for at least target blocks\n"
" \"leftmempool\" : x.x, (numeric) number of txs over history horizon in the feerate range that left mempool unconfirmed after target\n"
" }\n"
" \"fail\" : { ... } (json object) information about the highest range of feerates to fail to meet the threshold\n"
"}\n"
"\n"
"A negative feerate is returned if no answer can be given.\n"
"\nExample:\n"
+ HelpExampleCli("estimaterawfee", "6 0.9 1")
);
RPCTypeCheck(request.params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VNUM), true);
RPCTypeCheckArgument(request.params[0], UniValue::VNUM);
int nBlocks = request.params[0].get_int();
double threshold = 0.95;
if (!request.params[1].isNull())
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;
}
}
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())));
result.push_back(Pair("decay", buckets.decay));
result.push_back(Pair("scale", (int)buckets.scale));
UniValue passbucket(UniValue::VOBJ);
passbucket.push_back(Pair("startrange", round(buckets.pass.start)));
passbucket.push_back(Pair("endrange", round(buckets.pass.end)));
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));
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));
result.push_back(Pair("pass", passbucket));
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));
result.push_back(Pair("fail", failbucket));
return result;
}
static const CRPCCommand commands[] = static const CRPCCommand commands[] =
{ // category name actor (function) okSafeMode { // category name actor (function) okSafeMode
// --------------------- ------------------------ ----------------------- ---------- // --------------------- ------------------------ ----------------------- ----------
@ -876,7 +967,9 @@ static const CRPCCommand commands[] =
{ "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} }, { "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} },
{ "util", "estimatefee", &estimatefee, true, {"nblocks"} }, { "util", "estimatefee", &estimatefee, true, {"nblocks"} },
{ "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks"} }, { "util", "estimatesmartfee", &estimatesmartfee, true, {"nblocks", "conservative"} },
{ "hidden", "estimaterawfee", &estimaterawfee, true, {"nblocks", "threshold", "horizon"} },
}; };
void RegisterMiningRPCCommands(CRPCTable &t) void RegisterMiningRPCCommands(CRPCTable &t)

45
src/test/policyestimator_tests.cpp

@ -50,8 +50,8 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
int blocknum = 0; int blocknum = 0;
// Loop through 200 blocks // Loop through 200 blocks
// At a decay .998 and 4 fee transactions per block // At a decay .9952 and 4 fee transactions per block
// This makes the tx count about 1.33 per bucket, above the 1 threshold // This makes the tx count about 2.5 per bucket, well above the 0.1 threshold
while (blocknum < 200) { while (blocknum < 200) {
for (int j = 0; j < 10; j++) { // For each fee for (int j = 0; j < 10; j++) { // For each fee
for (int k = 0; k < 4; k++) { // add 4 fee txs for (int k = 0; k < 4; k++) { // add 4 fee txs
@ -75,20 +75,14 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
} }
mpool.removeForBlock(block, ++blocknum); mpool.removeForBlock(block, ++blocknum);
block.clear(); block.clear();
if (blocknum == 30) { // Check after just a few txs that combining buckets works as expected
// At this point we should need to combine 5 buckets to get enough data points if (blocknum == 3) {
// So estimateFee(1,2,3) should fail and estimateFee(4) should return somewhere around // At this point we should need to combine 3 buckets to get enough data points
// 8*baserate. estimateFee(4) %'s are 100,100,100,100,90 = average 98% // So estimateFee(1) should fail and estimateFee(2) should return somewhere around
// 9*baserate. estimateFee(2) %'s are 100,100,90 = average 97%
BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0)); BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0));
BOOST_CHECK(feeEst.estimateFee(2) == CFeeRate(0)); BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() < 9*baseRate.GetFeePerK() + deltaFee);
BOOST_CHECK(feeEst.estimateFee(3) == CFeeRate(0)); BOOST_CHECK(feeEst.estimateFee(2).GetFeePerK() > 9*baseRate.GetFeePerK() - deltaFee);
BOOST_CHECK(feeEst.estimateFee(4).GetFeePerK() < 8*baseRate.GetFeePerK() + deltaFee);
BOOST_CHECK(feeEst.estimateFee(4).GetFeePerK() > 8*baseRate.GetFeePerK() - deltaFee);
int answerFound;
BOOST_CHECK(feeEst.estimateSmartFee(1, &answerFound, mpool) == feeEst.estimateFee(4) && answerFound == 4);
BOOST_CHECK(feeEst.estimateSmartFee(3, &answerFound, mpool) == feeEst.estimateFee(4) && answerFound == 4);
BOOST_CHECK(feeEst.estimateSmartFee(4, &answerFound, mpool) == feeEst.estimateFee(4) && answerFound == 4);
BOOST_CHECK(feeEst.estimateSmartFee(8, &answerFound, mpool) == feeEst.estimateFee(8) && answerFound == 8);
} }
} }
@ -105,13 +99,14 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
BOOST_CHECK(origFeeEst[i-1] <= origFeeEst[i-2]); BOOST_CHECK(origFeeEst[i-1] <= origFeeEst[i-2]);
} }
int mult = 11-i; int mult = 11-i;
if (i > 1) { if (i % 2 == 0) { //At scale 2, test logic is only correct for even targets
BOOST_CHECK(origFeeEst[i-1] < mult*baseRate.GetFeePerK() + deltaFee); BOOST_CHECK(origFeeEst[i-1] < mult*baseRate.GetFeePerK() + deltaFee);
BOOST_CHECK(origFeeEst[i-1] > mult*baseRate.GetFeePerK() - deltaFee); BOOST_CHECK(origFeeEst[i-1] > mult*baseRate.GetFeePerK() - deltaFee);
} }
else {
BOOST_CHECK(origFeeEst[i-1] == CFeeRate(0).GetFeePerK());
} }
// Fill out rest of the original estimates
for (int i = 10; i <= 48; i++) {
origFeeEst.push_back(feeEst.estimateFee(i).GetFeePerK());
} }
// Mine 50 more blocks with no transactions happening, estimates shouldn't change // Mine 50 more blocks with no transactions happening, estimates shouldn't change
@ -140,10 +135,8 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
mpool.removeForBlock(block, ++blocknum); mpool.removeForBlock(block, ++blocknum);
} }
int answerFound;
for (int i = 1; i < 10;i++) { for (int i = 1; i < 10;i++) {
BOOST_CHECK(feeEst.estimateFee(i) == CFeeRate(0) || feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee); BOOST_CHECK(feeEst.estimateFee(i) == CFeeRate(0) || feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee);
BOOST_CHECK(feeEst.estimateSmartFee(i, &answerFound, mpool).GetFeePerK() > origFeeEst[answerFound-1] - deltaFee);
} }
// Mine all those transactions // Mine all those transactions
@ -156,16 +149,16 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
txHashes[j].pop_back(); txHashes[j].pop_back();
} }
} }
mpool.removeForBlock(block, 265); mpool.removeForBlock(block, 266);
block.clear(); block.clear();
BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0)); BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0));
for (int i = 2; i < 10;i++) { for (int i = 2; i < 10;i++) {
BOOST_CHECK(feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee); BOOST_CHECK(feeEst.estimateFee(i) == CFeeRate(0) || feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee);
} }
// Mine 200 more blocks where everything is mined every block // Mine 400 more blocks where everything is mined every block
// Estimates should be below original estimates // Estimates should be below original estimates
while (blocknum < 465) { while (blocknum < 665) {
for (int j = 0; j < 10; j++) { // For each fee multiple for (int j = 0; j < 10; j++) { // For each fee multiple
for (int k = 0; k < 4; k++) { // add 4 fee txs for (int k = 0; k < 4; k++) { // add 4 fee txs
tx.vin[0].prevout.n = 10000*blocknum+100*j+k; tx.vin[0].prevout.n = 10000*blocknum+100*j+k;
@ -181,7 +174,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
block.clear(); block.clear();
} }
BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0)); BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0));
for (int i = 2; i < 10; i++) { for (int i = 2; i < 9; i++) { // At 9, the original estimate was already at the bottom (b/c scale = 2)
BOOST_CHECK(feeEst.estimateFee(i).GetFeePerK() < origFeeEst[i-1] - deltaFee); BOOST_CHECK(feeEst.estimateFee(i).GetFeePerK() < origFeeEst[i-1] - deltaFee);
} }
@ -191,7 +184,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates)
mpool.TrimToSize(1); mpool.TrimToSize(1);
BOOST_CHECK(mpool.GetMinFee(1).GetFeePerK() > feeV[5]); BOOST_CHECK(mpool.GetMinFee(1).GetFeePerK() > feeV[5]);
for (int i = 1; i < 10; i++) { for (int i = 1; i < 10; i++) {
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= feeEst.estimateFee(i).GetFeePerK()); BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= feeEst.estimateRawFee(i, 0.85, FeeEstimateHorizon::MED_HALFLIFE).GetFeePerK());
BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= mpool.GetMinFee(1).GetFeePerK()); BOOST_CHECK(feeEst.estimateSmartFee(i, NULL, mpool).GetFeePerK() >= mpool.GetMinFee(1).GetFeePerK());
} }
} }

2
src/txmempool.cpp

@ -448,7 +448,7 @@ void CTxMemPool::removeUnchecked(txiter it, MemPoolRemovalReason reason)
mapLinks.erase(it); mapLinks.erase(it);
mapTx.erase(it); mapTx.erase(it);
nTransactionsUpdated++; nTransactionsUpdated++;
if (minerPolicyEstimator) {minerPolicyEstimator->removeTx(hash);} if (minerPolicyEstimator) {minerPolicyEstimator->removeTx(hash, false);}
} }
// Calculates descendants of entry that are not already in setDescendants, and adds to // Calculates descendants of entry that are not already in setDescendants, and adds to

4
test/functional/smartfees.py

@ -116,8 +116,8 @@ def check_estimates(node, fees_seen, max_invalid, print_estimates = True):
for i,e in enumerate(all_estimates): # estimate is for i+1 for i,e in enumerate(all_estimates): # estimate is for i+1
if e >= 0: if e >= 0:
valid_estimate = True valid_estimate = True
# estimatesmartfee should return the same result if i >= 13: # for n>=14 estimatesmartfee(n/2) should be at least as high as estimatefee(n)
assert_equal(node.estimatesmartfee(i+1)["feerate"], e) assert(node.estimatesmartfee((i+1)//2)["feerate"] > float(e) - delta)
else: else:
invalid_estimates += 1 invalid_estimates += 1

Loading…
Cancel
Save