Replace median fee rate with feerate percentiles

Removes medianfeerate result from getblockstats.
Adds feerate_percentiles which give the feerate of the 10th, 25th, 50th,
75th, and 90th percentile weight unit in the block.
This commit is contained in:
Marcin Jachymiak 2018-08-08 14:40:56 -04:00 committed by Marcin Jachymiak
parent df9f712746
commit 4b7091a842
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',