Skip to content

Commit 4b7091a

Browse files
Marcin Jachymiakmarcinja
authored andcommitted
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.
1 parent df9f712 commit 4b7091a

File tree

5 files changed

+162
-12
lines changed

5 files changed

+162
-12
lines changed

src/rpc/blockchain.cpp

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,35 @@ static T CalculateTruncatedMedian(std::vector<T>& scores)
16401640
}
16411641
}
16421642

1643+
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight)
1644+
{
1645+
if (scores.empty()) {
1646+
return;
1647+
}
1648+
1649+
std::sort(scores.begin(), scores.end());
1650+
1651+
// 10th, 25th, 50th, 75th, and 90th percentile weight units.
1652+
const double weights[NUM_GETBLOCKSTATS_PERCENTILES] = {
1653+
total_weight / 10.0, total_weight / 4.0, total_weight / 2.0, (total_weight * 3.0) / 4.0, (total_weight * 9.0) / 10.0
1654+
};
1655+
1656+
int64_t next_percentile_index = 0;
1657+
int64_t cumulative_weight = 0;
1658+
for (const auto& element : scores) {
1659+
cumulative_weight += element.second;
1660+
while (next_percentile_index < NUM_GETBLOCKSTATS_PERCENTILES && cumulative_weight >= weights[next_percentile_index]) {
1661+
result[next_percentile_index] = element.first;
1662+
++next_percentile_index;
1663+
}
1664+
}
1665+
1666+
// Fill any remaining percentiles with the last value.
1667+
for (int64_t i = next_percentile_index; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
1668+
result[i] = scores.back().first;
1669+
}
1670+
}
1671+
16431672
template<typename T>
16441673
static inline bool SetHasKeys(const std::set<T>& set) {return false;}
16451674
template<typename T, typename Tk, typename... Args>
@@ -1673,13 +1702,19 @@ static UniValue getblockstats(const JSONRPCRequest& request)
16731702
" \"avgfeerate\": xxxxx, (numeric) Average feerate (in satoshis per virtual byte)\n"
16741703
" \"avgtxsize\": xxxxx, (numeric) Average transaction size\n"
16751704
" \"blockhash\": xxxxx, (string) The block hash (to check for potential reorgs)\n"
1705+
" \"feerate_percentiles\": [ (array of numeric) Feerates at the 10th, 25th, 50th, 75th, and 90th percentile weight unit (in satoshis per virtual byte)\n"
1706+
" \"10th_percentile_feerate\", (numeric) The 10th percentile feerate\n"
1707+
" \"25th_percentile_feerate\", (numeric) The 25th percentile feerate\n"
1708+
" \"50th_percentile_feerate\", (numeric) The 50th percentile feerate\n"
1709+
" \"75th_percentile_feerate\", (numeric) The 75th percentile feerate\n"
1710+
" \"90th_percentile_feerate\", (numeric) The 90th percentile feerate\n"
1711+
" ],\n"
16761712
" \"height\": xxxxx, (numeric) The height of the block\n"
16771713
" \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n"
16781714
" \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n"
16791715
" \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in satoshis per virtual byte)\n"
16801716
" \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n"
16811717
" \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n"
1682-
" \"medianfeerate\": xxxxx, (numeric) Truncated median feerate (in satoshis per virtual byte)\n"
16831718
" \"mediantime\": xxxxx, (numeric) The block median time past\n"
16841719
" \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n"
16851720
" \"minfee\": xxxxx, (numeric) Minimum fee in the block\n"
@@ -1747,13 +1782,13 @@ static UniValue getblockstats(const JSONRPCRequest& request)
17471782
const bool do_all = stats.size() == 0; // Calculate everything if nothing selected (default)
17481783
const bool do_mediantxsize = do_all || stats.count("mediantxsize") != 0;
17491784
const bool do_medianfee = do_all || stats.count("medianfee") != 0;
1750-
const bool do_medianfeerate = do_all || stats.count("medianfeerate") != 0;
1751-
const bool loop_inputs = do_all || do_medianfee || do_medianfeerate ||
1785+
const bool do_feerate_percentiles = do_all || stats.count("feerate_percentiles") != 0;
1786+
const bool loop_inputs = do_all || do_medianfee || do_feerate_percentiles ||
17521787
SetHasKeys(stats, "utxo_size_inc", "totalfee", "avgfee", "avgfeerate", "minfee", "maxfee", "minfeerate", "maxfeerate");
17531788
const bool loop_outputs = do_all || loop_inputs || stats.count("total_out");
17541789
const bool do_calculate_size = do_mediantxsize ||
17551790
SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "swtotal_size");
1756-
const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "medianfeerate", "minfeerate", "maxfeerate");
1791+
const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "feerate_percentiles", "minfeerate", "maxfeerate");
17571792
const bool do_calculate_sw = do_all || SetHasKeys(stats, "swtxs", "swtotal_size", "swtotal_weight");
17581793

17591794
CAmount maxfee = 0;
@@ -1773,7 +1808,7 @@ static UniValue getblockstats(const JSONRPCRequest& request)
17731808
int64_t total_weight = 0;
17741809
int64_t utxo_size_inc = 0;
17751810
std::vector<CAmount> fee_array;
1776-
std::vector<CAmount> feerate_array;
1811+
std::vector<std::pair<CAmount, int64_t>> feerate_array;
17771812
std::vector<int64_t> txsize_array;
17781813

17791814
for (const auto& tx : block.vtx) {
@@ -1848,26 +1883,34 @@ static UniValue getblockstats(const JSONRPCRequest& request)
18481883

18491884
// New feerate uses satoshis per virtual byte instead of per serialized byte
18501885
CAmount feerate = weight ? (txfee * WITNESS_SCALE_FACTOR) / weight : 0;
1851-
if (do_medianfeerate) {
1852-
feerate_array.push_back(feerate);
1886+
if (do_feerate_percentiles) {
1887+
feerate_array.emplace_back(std::make_pair(feerate, weight));
18531888
}
18541889
maxfeerate = std::max(maxfeerate, feerate);
18551890
minfeerate = std::min(minfeerate, feerate);
18561891
}
18571892
}
18581893

1894+
CAmount feerate_percentiles[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
1895+
CalculatePercentilesByWeight(feerate_percentiles, feerate_array, total_weight);
1896+
1897+
UniValue feerates_res(UniValue::VARR);
1898+
for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
1899+
feerates_res.push_back(feerate_percentiles[i]);
1900+
}
1901+
18591902
UniValue ret_all(UniValue::VOBJ);
18601903
ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0);
18611904
ret_all.pushKV("avgfeerate", total_weight ? (totalfee * WITNESS_SCALE_FACTOR) / total_weight : 0); // Unit: sat/vbyte
18621905
ret_all.pushKV("avgtxsize", (block.vtx.size() > 1) ? total_size / (block.vtx.size() - 1) : 0);
18631906
ret_all.pushKV("blockhash", pindex->GetBlockHash().GetHex());
1907+
ret_all.pushKV("feerate_percentiles", feerates_res);
18641908
ret_all.pushKV("height", (int64_t)pindex->nHeight);
18651909
ret_all.pushKV("ins", inputs);
18661910
ret_all.pushKV("maxfee", maxfee);
18671911
ret_all.pushKV("maxfeerate", maxfeerate);
18681912
ret_all.pushKV("maxtxsize", maxtxsize);
18691913
ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array));
1870-
ret_all.pushKV("medianfeerate", CalculateTruncatedMedian(feerate_array));
18711914
ret_all.pushKV("mediantime", pindex->GetMedianTimePast());
18721915
ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array));
18731916
ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee);

src/rpc/blockchain.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55
#ifndef BITCOIN_RPC_BLOCKCHAIN_H
66
#define BITCOIN_RPC_BLOCKCHAIN_H
77

8+
#include <vector>
9+
#include <stdint.h>
10+
#include <amount.h>
11+
812
class CBlock;
913
class CBlockIndex;
1014
class UniValue;
1115

16+
static constexpr int NUM_GETBLOCKSTATS_PERCENTILES = 5;
17+
1218
/**
1319
* Get the difficulty of the net wrt to the given block index, or the chain tip if
1420
* not provided.
@@ -33,4 +39,7 @@ UniValue mempoolToJSON(bool fVerbose = false);
3339
/** Block header to JSON */
3440
UniValue blockheaderToJSON(const CBlockIndex* blockindex);
3541

42+
/** Used by getblockstats to get feerates at different percentiles by weight */
43+
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight);
44+
3645
#endif

src/test/rpc_tests.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#include <univalue.h>
1818

19+
#include <rpc/blockchain.h>
20+
1921
UniValue CallRPC(std::string args)
2022
{
2123
std::vector<std::string> vArgs;
@@ -336,4 +338,82 @@ BOOST_AUTO_TEST_CASE(rpc_convert_values_generatetoaddress)
336338
BOOST_CHECK_EQUAL(result[2].get_int(), 9);
337339
}
338340

341+
BOOST_AUTO_TEST_CASE(rpc_getblockstats_calculate_percentiles_by_weight)
342+
{
343+
int64_t total_weight = 200;
344+
std::vector<std::pair<CAmount, int64_t>> feerates;
345+
CAmount result[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
346+
347+
for (int64_t i = 0; i < 100; i++) {
348+
feerates.emplace_back(std::make_pair(1 ,1));
349+
}
350+
351+
for (int64_t i = 0; i < 100; i++) {
352+
feerates.emplace_back(std::make_pair(2 ,1));
353+
}
354+
355+
CalculatePercentilesByWeight(result, feerates, total_weight);
356+
BOOST_CHECK_EQUAL(result[0], 1);
357+
BOOST_CHECK_EQUAL(result[1], 1);
358+
BOOST_CHECK_EQUAL(result[2], 1);
359+
BOOST_CHECK_EQUAL(result[3], 2);
360+
BOOST_CHECK_EQUAL(result[4], 2);
361+
362+
// Test with more pairs, and two pairs overlapping 2 percentiles.
363+
total_weight = 100;
364+
CAmount result2[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
365+
feerates.clear();
366+
367+
feerates.emplace_back(std::make_pair(1, 9));
368+
feerates.emplace_back(std::make_pair(2 , 16)); //10th + 25th percentile
369+
feerates.emplace_back(std::make_pair(4 ,50)); //50th + 75th percentile
370+
feerates.emplace_back(std::make_pair(5 ,10));
371+
feerates.emplace_back(std::make_pair(9 ,15)); // 90th percentile
372+
373+
CalculatePercentilesByWeight(result2, feerates, total_weight);
374+
375+
BOOST_CHECK_EQUAL(result2[0], 2);
376+
BOOST_CHECK_EQUAL(result2[1], 2);
377+
BOOST_CHECK_EQUAL(result2[2], 4);
378+
BOOST_CHECK_EQUAL(result2[3], 4);
379+
BOOST_CHECK_EQUAL(result2[4], 9);
380+
381+
// Same test as above, but one of the percentile-overlapping pairs is split in 2.
382+
total_weight = 100;
383+
CAmount result3[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
384+
feerates.clear();
385+
386+
feerates.emplace_back(std::make_pair(1, 9));
387+
feerates.emplace_back(std::make_pair(2 , 11)); // 10th percentile
388+
feerates.emplace_back(std::make_pair(2 , 5)); // 25th percentile
389+
feerates.emplace_back(std::make_pair(4 ,50)); //50th + 75th percentile
390+
feerates.emplace_back(std::make_pair(5 ,10));
391+
feerates.emplace_back(std::make_pair(9 ,15)); // 90th percentile
392+
393+
CalculatePercentilesByWeight(result3, feerates, total_weight);
394+
395+
BOOST_CHECK_EQUAL(result3[0], 2);
396+
BOOST_CHECK_EQUAL(result3[1], 2);
397+
BOOST_CHECK_EQUAL(result3[2], 4);
398+
BOOST_CHECK_EQUAL(result3[3], 4);
399+
BOOST_CHECK_EQUAL(result3[4], 9);
400+
401+
// Test with one transaction spanning all percentiles.
402+
total_weight = 104;
403+
CAmount result4[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
404+
feerates.clear();
405+
406+
feerates.emplace_back(std::make_pair(1, 100));
407+
feerates.emplace_back(std::make_pair(2, 1));
408+
feerates.emplace_back(std::make_pair(3, 1));
409+
feerates.emplace_back(std::make_pair(3, 1));
410+
feerates.emplace_back(std::make_pair(999999, 1));
411+
412+
CalculatePercentilesByWeight(result4, feerates, total_weight);
413+
414+
for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
415+
BOOST_CHECK_EQUAL(result4[i], 1);
416+
}
417+
}
418+
339419
BOOST_AUTO_TEST_SUITE_END()

test/functional/data/rpc_getblockstats.json

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,19 @@
112112
"avgfeerate": 0,
113113
"avgtxsize": 0,
114114
"blockhash": "1d7fe80f19d28b8e712af0399ac84006db753441f3033111b3a8d610afab364f",
115+
"feerate_percentiles": [
116+
0,
117+
0,
118+
0,
119+
0,
120+
0
121+
],
115122
"height": 101,
116123
"ins": 0,
117124
"maxfee": 0,
118125
"maxfeerate": 0,
119126
"maxtxsize": 0,
120127
"medianfee": 0,
121-
"medianfeerate": 0,
122128
"mediantime": 1525107242,
123129
"mediantxsize": 0,
124130
"minfee": 0,
@@ -144,12 +150,18 @@
144150
"avgtxsize": 187,
145151
"blockhash": "4e21a43675d7a41cb6b944e068c5bcd0a677baf658d9ebe021ae2d2f99397ccc",
146152
"height": 102,
153+
"feerate_percentiles": [
154+
20,
155+
20,
156+
20,
157+
20,
158+
20
159+
],
147160
"ins": 1,
148161
"maxfee": 3760,
149162
"maxfeerate": 20,
150163
"maxtxsize": 187,
151164
"medianfee": 3760,
152-
"medianfeerate": 20,
153165
"mediantime": 1525107242,
154166
"mediantxsize": 187,
155167
"minfee": 3760,
@@ -174,13 +186,19 @@
174186
"avgfeerate": 109,
175187
"avgtxsize": 228,
176188
"blockhash": "22d9b8b9c2a37c81515f3fc84f7241f6c07dbcea85ef16b00bcc33ae400a030f",
189+
"feerate_percentiles": [
190+
20,
191+
20,
192+
20,
193+
300,
194+
300
195+
],
177196
"height": 103,
178197
"ins": 3,
179198
"maxfee": 49800,
180199
"maxfeerate": 300,
181200
"maxtxsize": 248,
182201
"medianfee": 3760,
183-
"medianfeerate": 20,
184202
"mediantime": 1525107243,
185203
"mediantxsize": 248,
186204
"minfee": 3320,

test/functional/rpc_getblockstats.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class GetblockstatsTest(BitcoinTestFramework):
2727
'maxfee',
2828
'maxfeerate',
2929
'medianfee',
30-
'medianfeerate',
30+
'feerate_percentiles',
3131
'minfee',
3232
'minfeerate',
3333
'totalfee',

0 commit comments

Comments
 (0)