Merge #13918: rpc: Replace median fee rate with feerate percentiles in getblockstats

4b7091a842 Replace median fee rate with feerate percentiles (Marcin Jachymiak)

Pull request description:

  Currently,  the `medianfeerate` statistic is calculated from the feerate of the middle transaction of a list of transactions sorted by feerate.

  This PR instead uses the value of the 50th percentile weight unit in the block, and also calculates the feerate at the 10th, 25th, 75th, and 90th percentiles.  This more accurately corresponds with what is generally meant by median feerate.

Tree-SHA512: 59255e243df90d7afbe69839408c58c9723884b8ab82c66dc24a769e89c6d539db1905374a3f025ff28272fb25a0b90e92d8101103e39a6d9c0d60423a596714
This commit is contained in:
MarcoFalke 2018-08-13 07:18:00 -04:00
commit a9c56b6634
No known key found for this signature in database
GPG key ID: D2EA4850E7528B25
5 changed files with 162 additions and 12 deletions

View file

@ -1640,6 +1640,35 @@ static T CalculateTruncatedMedian(std::vector<T>& scores)
} }
} }
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight)
{
if (scores.empty()) {
return;
}
std::sort(scores.begin(), scores.end());
// 10th, 25th, 50th, 75th, and 90th percentile weight units.
const double weights[NUM_GETBLOCKSTATS_PERCENTILES] = {
total_weight / 10.0, total_weight / 4.0, total_weight / 2.0, (total_weight * 3.0) / 4.0, (total_weight * 9.0) / 10.0
};
int64_t next_percentile_index = 0;
int64_t cumulative_weight = 0;
for (const auto& element : scores) {
cumulative_weight += element.second;
while (next_percentile_index < NUM_GETBLOCKSTATS_PERCENTILES && cumulative_weight >= weights[next_percentile_index]) {
result[next_percentile_index] = element.first;
++next_percentile_index;
}
}
// Fill any remaining percentiles with the last value.
for (int64_t i = next_percentile_index; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
result[i] = scores.back().first;
}
}
template<typename T> template<typename T>
static inline bool SetHasKeys(const std::set<T>& set) {return false;} static inline bool SetHasKeys(const std::set<T>& set) {return false;}
template<typename T, typename Tk, typename... Args> template<typename T, typename Tk, typename... Args>
@ -1673,13 +1702,19 @@ static UniValue getblockstats(const JSONRPCRequest& request)
" \"avgfeerate\": xxxxx, (numeric) Average feerate (in satoshis per virtual byte)\n" " \"avgfeerate\": xxxxx, (numeric) Average feerate (in satoshis per virtual byte)\n"
" \"avgtxsize\": xxxxx, (numeric) Average transaction size\n" " \"avgtxsize\": xxxxx, (numeric) Average transaction size\n"
" \"blockhash\": xxxxx, (string) The block hash (to check for potential reorgs)\n" " \"blockhash\": xxxxx, (string) The block hash (to check for potential reorgs)\n"
" \"feerate_percentiles\": [ (array of numeric) Feerates at the 10th, 25th, 50th, 75th, and 90th percentile weight unit (in satoshis per virtual byte)\n"
" \"10th_percentile_feerate\", (numeric) The 10th percentile feerate\n"
" \"25th_percentile_feerate\", (numeric) The 25th percentile feerate\n"
" \"50th_percentile_feerate\", (numeric) The 50th percentile feerate\n"
" \"75th_percentile_feerate\", (numeric) The 75th percentile feerate\n"
" \"90th_percentile_feerate\", (numeric) The 90th percentile feerate\n"
" ],\n"
" \"height\": xxxxx, (numeric) The height of the block\n" " \"height\": xxxxx, (numeric) The height of the block\n"
" \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n" " \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n"
" \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n" " \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n"
" \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in satoshis per virtual byte)\n" " \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in satoshis per virtual byte)\n"
" \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n" " \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n"
" \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n" " \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n"
" \"medianfeerate\": xxxxx, (numeric) Truncated median feerate (in satoshis per virtual byte)\n"
" \"mediantime\": xxxxx, (numeric) The block median time past\n" " \"mediantime\": xxxxx, (numeric) The block median time past\n"
" \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n" " \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n"
" \"minfee\": xxxxx, (numeric) Minimum fee in the block\n" " \"minfee\": xxxxx, (numeric) Minimum fee in the block\n"
@ -1747,13 +1782,13 @@ static UniValue getblockstats(const JSONRPCRequest& request)
const bool do_all = stats.size() == 0; // Calculate everything if nothing selected (default) const bool do_all = stats.size() == 0; // Calculate everything if nothing selected (default)
const bool do_mediantxsize = do_all || stats.count("mediantxsize") != 0; const bool do_mediantxsize = do_all || stats.count("mediantxsize") != 0;
const bool do_medianfee = do_all || stats.count("medianfee") != 0; const bool do_medianfee = do_all || stats.count("medianfee") != 0;
const bool do_medianfeerate = do_all || stats.count("medianfeerate") != 0; const bool do_feerate_percentiles = do_all || stats.count("feerate_percentiles") != 0;
const bool loop_inputs = do_all || do_medianfee || do_medianfeerate || const bool loop_inputs = do_all || do_medianfee || do_feerate_percentiles ||
SetHasKeys(stats, "utxo_size_inc", "totalfee", "avgfee", "avgfeerate", "minfee", "maxfee", "minfeerate", "maxfeerate"); SetHasKeys(stats, "utxo_size_inc", "totalfee", "avgfee", "avgfeerate", "minfee", "maxfee", "minfeerate", "maxfeerate");
const bool loop_outputs = do_all || loop_inputs || stats.count("total_out"); const bool loop_outputs = do_all || loop_inputs || stats.count("total_out");
const bool do_calculate_size = do_mediantxsize || const bool do_calculate_size = do_mediantxsize ||
SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "swtotal_size"); SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "swtotal_size");
const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "medianfeerate", "minfeerate", "maxfeerate"); const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "feerate_percentiles", "minfeerate", "maxfeerate");
const bool do_calculate_sw = do_all || SetHasKeys(stats, "swtxs", "swtotal_size", "swtotal_weight"); const bool do_calculate_sw = do_all || SetHasKeys(stats, "swtxs", "swtotal_size", "swtotal_weight");
CAmount maxfee = 0; CAmount maxfee = 0;
@ -1773,7 +1808,7 @@ static UniValue getblockstats(const JSONRPCRequest& request)
int64_t total_weight = 0; int64_t total_weight = 0;
int64_t utxo_size_inc = 0; int64_t utxo_size_inc = 0;
std::vector<CAmount> fee_array; std::vector<CAmount> fee_array;
std::vector<CAmount> feerate_array; std::vector<std::pair<CAmount, int64_t>> feerate_array;
std::vector<int64_t> txsize_array; std::vector<int64_t> txsize_array;
for (const auto& tx : block.vtx) { for (const auto& tx : block.vtx) {
@ -1848,26 +1883,34 @@ static UniValue getblockstats(const JSONRPCRequest& request)
// New feerate uses satoshis per virtual byte instead of per serialized byte // New feerate uses satoshis per virtual byte instead of per serialized byte
CAmount feerate = weight ? (txfee * WITNESS_SCALE_FACTOR) / weight : 0; CAmount feerate = weight ? (txfee * WITNESS_SCALE_FACTOR) / weight : 0;
if (do_medianfeerate) { if (do_feerate_percentiles) {
feerate_array.push_back(feerate); feerate_array.emplace_back(std::make_pair(feerate, weight));
} }
maxfeerate = std::max(maxfeerate, feerate); maxfeerate = std::max(maxfeerate, feerate);
minfeerate = std::min(minfeerate, feerate); minfeerate = std::min(minfeerate, feerate);
} }
} }
CAmount feerate_percentiles[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
CalculatePercentilesByWeight(feerate_percentiles, feerate_array, total_weight);
UniValue feerates_res(UniValue::VARR);
for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
feerates_res.push_back(feerate_percentiles[i]);
}
UniValue ret_all(UniValue::VOBJ); UniValue ret_all(UniValue::VOBJ);
ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0); ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0);
ret_all.pushKV("avgfeerate", total_weight ? (totalfee * WITNESS_SCALE_FACTOR) / total_weight : 0); // Unit: sat/vbyte ret_all.pushKV("avgfeerate", total_weight ? (totalfee * WITNESS_SCALE_FACTOR) / total_weight : 0); // Unit: sat/vbyte
ret_all.pushKV("avgtxsize", (block.vtx.size() > 1) ? total_size / (block.vtx.size() - 1) : 0); ret_all.pushKV("avgtxsize", (block.vtx.size() > 1) ? total_size / (block.vtx.size() - 1) : 0);
ret_all.pushKV("blockhash", pindex->GetBlockHash().GetHex()); ret_all.pushKV("blockhash", pindex->GetBlockHash().GetHex());
ret_all.pushKV("feerate_percentiles", feerates_res);
ret_all.pushKV("height", (int64_t)pindex->nHeight); ret_all.pushKV("height", (int64_t)pindex->nHeight);
ret_all.pushKV("ins", inputs); ret_all.pushKV("ins", inputs);
ret_all.pushKV("maxfee", maxfee); ret_all.pushKV("maxfee", maxfee);
ret_all.pushKV("maxfeerate", maxfeerate); ret_all.pushKV("maxfeerate", maxfeerate);
ret_all.pushKV("maxtxsize", maxtxsize); ret_all.pushKV("maxtxsize", maxtxsize);
ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array)); ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array));
ret_all.pushKV("medianfeerate", CalculateTruncatedMedian(feerate_array));
ret_all.pushKV("mediantime", pindex->GetMedianTimePast()); ret_all.pushKV("mediantime", pindex->GetMedianTimePast());
ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array)); ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array));
ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee); ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee);

View file

@ -5,10 +5,16 @@
#ifndef BITCOIN_RPC_BLOCKCHAIN_H #ifndef BITCOIN_RPC_BLOCKCHAIN_H
#define BITCOIN_RPC_BLOCKCHAIN_H #define BITCOIN_RPC_BLOCKCHAIN_H
#include <vector>
#include <stdint.h>
#include <amount.h>
class CBlock; class CBlock;
class CBlockIndex; class CBlockIndex;
class UniValue; class UniValue;
static constexpr int NUM_GETBLOCKSTATS_PERCENTILES = 5;
/** /**
* Get the difficulty of the net wrt to the given block index, or the chain tip if * Get the difficulty of the net wrt to the given block index, or the chain tip if
* not provided. * not provided.
@ -33,4 +39,7 @@ UniValue mempoolToJSON(bool fVerbose = false);
/** Block header to JSON */ /** Block header to JSON */
UniValue blockheaderToJSON(const CBlockIndex* blockindex); UniValue blockheaderToJSON(const CBlockIndex* blockindex);
/** Used by getblockstats to get feerates at different percentiles by weight */
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight);
#endif #endif

View file

@ -16,6 +16,8 @@
#include <univalue.h> #include <univalue.h>
#include <rpc/blockchain.h>
UniValue CallRPC(std::string args) UniValue CallRPC(std::string args)
{ {
std::vector<std::string> vArgs; std::vector<std::string> vArgs;
@ -336,4 +338,82 @@ BOOST_AUTO_TEST_CASE(rpc_convert_values_generatetoaddress)
BOOST_CHECK_EQUAL(result[2].get_int(), 9); BOOST_CHECK_EQUAL(result[2].get_int(), 9);
} }
BOOST_AUTO_TEST_CASE(rpc_getblockstats_calculate_percentiles_by_weight)
{
int64_t total_weight = 200;
std::vector<std::pair<CAmount, int64_t>> feerates;
CAmount result[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
for (int64_t i = 0; i < 100; i++) {
feerates.emplace_back(std::make_pair(1 ,1));
}
for (int64_t i = 0; i < 100; i++) {
feerates.emplace_back(std::make_pair(2 ,1));
}
CalculatePercentilesByWeight(result, feerates, total_weight);
BOOST_CHECK_EQUAL(result[0], 1);
BOOST_CHECK_EQUAL(result[1], 1);
BOOST_CHECK_EQUAL(result[2], 1);
BOOST_CHECK_EQUAL(result[3], 2);
BOOST_CHECK_EQUAL(result[4], 2);
// Test with more pairs, and two pairs overlapping 2 percentiles.
total_weight = 100;
CAmount result2[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
feerates.clear();
feerates.emplace_back(std::make_pair(1, 9));
feerates.emplace_back(std::make_pair(2 , 16)); //10th + 25th percentile
feerates.emplace_back(std::make_pair(4 ,50)); //50th + 75th percentile
feerates.emplace_back(std::make_pair(5 ,10));
feerates.emplace_back(std::make_pair(9 ,15)); // 90th percentile
CalculatePercentilesByWeight(result2, feerates, total_weight);
BOOST_CHECK_EQUAL(result2[0], 2);
BOOST_CHECK_EQUAL(result2[1], 2);
BOOST_CHECK_EQUAL(result2[2], 4);
BOOST_CHECK_EQUAL(result2[3], 4);
BOOST_CHECK_EQUAL(result2[4], 9);
// Same test as above, but one of the percentile-overlapping pairs is split in 2.
total_weight = 100;
CAmount result3[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
feerates.clear();
feerates.emplace_back(std::make_pair(1, 9));
feerates.emplace_back(std::make_pair(2 , 11)); // 10th percentile
feerates.emplace_back(std::make_pair(2 , 5)); // 25th percentile
feerates.emplace_back(std::make_pair(4 ,50)); //50th + 75th percentile
feerates.emplace_back(std::make_pair(5 ,10));
feerates.emplace_back(std::make_pair(9 ,15)); // 90th percentile
CalculatePercentilesByWeight(result3, feerates, total_weight);
BOOST_CHECK_EQUAL(result3[0], 2);
BOOST_CHECK_EQUAL(result3[1], 2);
BOOST_CHECK_EQUAL(result3[2], 4);
BOOST_CHECK_EQUAL(result3[3], 4);
BOOST_CHECK_EQUAL(result3[4], 9);
// Test with one transaction spanning all percentiles.
total_weight = 104;
CAmount result4[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
feerates.clear();
feerates.emplace_back(std::make_pair(1, 100));
feerates.emplace_back(std::make_pair(2, 1));
feerates.emplace_back(std::make_pair(3, 1));
feerates.emplace_back(std::make_pair(3, 1));
feerates.emplace_back(std::make_pair(999999, 1));
CalculatePercentilesByWeight(result4, feerates, total_weight);
for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
BOOST_CHECK_EQUAL(result4[i], 1);
}
}
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()

View file

@ -112,13 +112,19 @@
"avgfeerate": 0, "avgfeerate": 0,
"avgtxsize": 0, "avgtxsize": 0,
"blockhash": "1d7fe80f19d28b8e712af0399ac84006db753441f3033111b3a8d610afab364f", "blockhash": "1d7fe80f19d28b8e712af0399ac84006db753441f3033111b3a8d610afab364f",
"feerate_percentiles": [
0,
0,
0,
0,
0
],
"height": 101, "height": 101,
"ins": 0, "ins": 0,
"maxfee": 0, "maxfee": 0,
"maxfeerate": 0, "maxfeerate": 0,
"maxtxsize": 0, "maxtxsize": 0,
"medianfee": 0, "medianfee": 0,
"medianfeerate": 0,
"mediantime": 1525107242, "mediantime": 1525107242,
"mediantxsize": 0, "mediantxsize": 0,
"minfee": 0, "minfee": 0,
@ -144,12 +150,18 @@
"avgtxsize": 187, "avgtxsize": 187,
"blockhash": "4e21a43675d7a41cb6b944e068c5bcd0a677baf658d9ebe021ae2d2f99397ccc", "blockhash": "4e21a43675d7a41cb6b944e068c5bcd0a677baf658d9ebe021ae2d2f99397ccc",
"height": 102, "height": 102,
"feerate_percentiles": [
20,
20,
20,
20,
20
],
"ins": 1, "ins": 1,
"maxfee": 3760, "maxfee": 3760,
"maxfeerate": 20, "maxfeerate": 20,
"maxtxsize": 187, "maxtxsize": 187,
"medianfee": 3760, "medianfee": 3760,
"medianfeerate": 20,
"mediantime": 1525107242, "mediantime": 1525107242,
"mediantxsize": 187, "mediantxsize": 187,
"minfee": 3760, "minfee": 3760,
@ -174,13 +186,19 @@
"avgfeerate": 109, "avgfeerate": 109,
"avgtxsize": 228, "avgtxsize": 228,
"blockhash": "22d9b8b9c2a37c81515f3fc84f7241f6c07dbcea85ef16b00bcc33ae400a030f", "blockhash": "22d9b8b9c2a37c81515f3fc84f7241f6c07dbcea85ef16b00bcc33ae400a030f",
"feerate_percentiles": [
20,
20,
20,
300,
300
],
"height": 103, "height": 103,
"ins": 3, "ins": 3,
"maxfee": 49800, "maxfee": 49800,
"maxfeerate": 300, "maxfeerate": 300,
"maxtxsize": 248, "maxtxsize": 248,
"medianfee": 3760, "medianfee": 3760,
"medianfeerate": 20,
"mediantime": 1525107243, "mediantime": 1525107243,
"mediantxsize": 248, "mediantxsize": 248,
"minfee": 3320, "minfee": 3320,

View file

@ -27,7 +27,7 @@ class GetblockstatsTest(BitcoinTestFramework):
'maxfee', 'maxfee',
'maxfeerate', 'maxfeerate',
'medianfee', 'medianfee',
'medianfeerate', 'feerate_percentiles',
'minfee', 'minfee',
'minfeerate', 'minfeerate',
'totalfee', 'totalfee',