Skip to content

Commit 8b037a3

Browse files
committed
Merge #407: Replace median fee rate with feerate percentiles
61064b3 Replace median fee rate with feerate percentiles (Marcin Jachymiak)
2 parents 7927e87 + 61064b3 commit 8b037a3

File tree

6 files changed

+547
-134
lines changed

6 files changed

+547
-134
lines changed

qa/rpc-tests/data/rpc_getblockstats.json

Lines changed: 141 additions & 123 deletions
Large diffs are not rendered by default.

src/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@ BITCOIN_CORE_H = \
125125
protocol.h \
126126
random.h \
127127
reverselock.h \
128+
rpc/blockchain.h \
128129
rpc/client.h \
129130
rpc/protocol.h \
130-
rpc/server.h \
131131
rpc/register.h \
132+
rpc/server.h \
132133
scheduler.h \
133134
script/generic.hpp \
134135
script/sigcache.h \

src/rpc/blockchain.cpp

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Distributed under the MIT software license, see the accompanying
44
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

6+
#include "blockchain.h"
7+
68
#include "amount.h"
79
#include "chain.h"
810
#include "chainparams.h"
@@ -74,7 +76,7 @@ UniValue blockheaderToJSON(const CBlockIndex* blockindex)
7476
return result;
7577
}
7678

77-
UniValue blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool txDetails = false)
79+
UniValue blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool txDetails)
7880
{
7981
UniValue result(UniValue::VOBJ);
8082
result.push_back(Pair("hash", blockindex->GetBlockHash().GetHex()));
@@ -342,7 +344,7 @@ void entryToJSON(UniValue &info, const CTxMemPoolEntry &e)
342344
info.push_back(Pair("depends", depends));
343345
}
344346

345-
UniValue mempoolToJSON(bool fVerbose = false)
347+
UniValue mempoolToJSON(bool fVerbose)
346348
{
347349
if (fVerbose)
348350
{
@@ -1421,6 +1423,35 @@ static T CalculateTruncatedMedian(std::vector<T>& scores)
14211423
}
14221424
}
14231425

1426+
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight)
1427+
{
1428+
if (scores.empty()) {
1429+
return;
1430+
}
1431+
1432+
std::sort(scores.begin(), scores.end());
1433+
1434+
// 10th, 25th, 50th, 75th, and 90th percentile weight units.
1435+
const double weights[NUM_GETBLOCKSTATS_PERCENTILES] = {
1436+
total_weight / 10.0, total_weight / 4.0, total_weight / 2.0, (total_weight * 3.0) / 4.0, (total_weight * 9.0) / 10.0
1437+
};
1438+
1439+
int64_t next_percentile_index = 0;
1440+
int64_t cumulative_weight = 0;
1441+
for (const auto& element : scores) {
1442+
cumulative_weight += element.second;
1443+
while (next_percentile_index < NUM_GETBLOCKSTATS_PERCENTILES && cumulative_weight >= weights[next_percentile_index]) {
1444+
result[next_percentile_index] = element.first;
1445+
++next_percentile_index;
1446+
}
1447+
}
1448+
1449+
// Fill any remaining percentiles with the last value.
1450+
for (int64_t i = next_percentile_index; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
1451+
result[i] = scores.back().first;
1452+
}
1453+
}
1454+
14241455
template<typename T>
14251456
static inline bool SetHasKeys(const std::set<T>& set) {return false;}
14261457
template<typename T, typename Tk, typename... Args>
@@ -1454,13 +1485,19 @@ static UniValue getblockstats(const JSONRPCRequest& request)
14541485
" \"avgfeerate\": xxxxx, (numeric) Average feerate (in satoshis per virtual byte)\n"
14551486
" \"avgtxsize\": xxxxx, (numeric) Average transaction size\n"
14561487
" \"blockhash\": xxxxx, (string) The block hash (to check for potential reorgs)\n"
1488+
" \"feerate_percentiles\": [ (array of numeric) Feerates at the 10th, 25th, 50th, 75th, and 90th percentile weight unit (in satoshis per virtual byte)\n"
1489+
" \"10th_percentile_feerate\", (numeric) The 10th percentile feerate\n"
1490+
" \"25th_percentile_feerate\", (numeric) The 25th percentile feerate\n"
1491+
" \"50th_percentile_feerate\", (numeric) The 50th percentile feerate\n"
1492+
" \"75th_percentile_feerate\", (numeric) The 75th percentile feerate\n"
1493+
" \"90th_percentile_feerate\", (numeric) The 90th percentile feerate\n"
1494+
" ],\n"
14571495
" \"height\": xxxxx, (numeric) The height of the block\n"
14581496
" \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n"
14591497
" \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n"
14601498
" \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in satoshis per virtual byte)\n"
14611499
" \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n"
14621500
" \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n"
1463-
" \"medianfeerate\": xxxxx, (numeric) Truncated median feerate (in satoshis per virtual byte)\n"
14641501
" \"mediantime\": xxxxx, (numeric) The block median time past\n"
14651502
" \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n"
14661503
" \"minfee\": xxxxx, (numeric) Minimum fee in the block\n"
@@ -1529,13 +1566,13 @@ static UniValue getblockstats(const JSONRPCRequest& request)
15291566
const bool do_all = stats.size() == 0; // Calculate everything if nothing selected (default)
15301567
const bool do_mediantxsize = do_all || stats.count("mediantxsize") != 0;
15311568
const bool do_medianfee = do_all || stats.count("medianfee") != 0;
1532-
const bool do_medianfeerate = do_all || stats.count("medianfeerate") != 0;
1569+
const bool do_feerate_percentiles = do_all || stats.count("feerate_percentiles") != 0;
15331570
const bool loop_inputs = do_all || stats.count("utxo_size_inc");
1534-
const bool loop_outputs = do_all || do_medianfee || do_medianfeerate || loop_inputs ||
1571+
const bool loop_outputs = do_all || do_medianfee || do_feerate_percentiles || loop_inputs ||
15351572
SetHasKeys(stats, "totalfee", "avgfee", "avgfeerate", "minfee", "maxfee", "minfeerate", "maxfeerate", "total_out");
15361573
const bool do_calculate_size = do_mediantxsize ||
15371574
SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "swtotal_size");
1538-
const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "medianfeerate", "minfeerate", "maxfeerate");
1575+
const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "feerate_percentiles", "minfeerate", "maxfeerate");
15391576
const bool do_calculate_sw = do_all || SetHasKeys(stats, "swtxs", "swtotal_size", "swtotal_weight");
15401577

15411578
CAmount maxfee = 0;
@@ -1555,7 +1592,7 @@ static UniValue getblockstats(const JSONRPCRequest& request)
15551592
int64_t total_weight = 0;
15561593
int64_t utxo_size_inc = 0;
15571594
std::vector<CAmount> fee_array;
1558-
std::vector<CAmount> feerate_array;
1595+
std::vector<std::pair<CAmount, int64_t>> feerate_array;
15591596
std::vector<int64_t> txsize_array;
15601597

15611598
for (const auto& tx : block.vtx) {
@@ -1642,26 +1679,34 @@ static UniValue getblockstats(const JSONRPCRequest& request)
16421679

16431680
// New feerate uses satoshis per virtual byte instead of per serialized byte
16441681
CAmount feerate = weight ? (txfee * WITNESS_SCALE_FACTOR) / weight : 0;
1645-
if (do_medianfeerate) {
1646-
feerate_array.push_back(feerate);
1682+
if (do_feerate_percentiles) {
1683+
feerate_array.emplace_back(std::make_pair(feerate, weight));
16471684
}
16481685
maxfeerate = std::max(maxfeerate, feerate);
16491686
minfeerate = std::min(minfeerate, feerate);
16501687
}
16511688
}
16521689

1690+
CAmount feerate_percentiles[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
1691+
CalculatePercentilesByWeight(feerate_percentiles, feerate_array, total_weight);
1692+
1693+
UniValue feerates_res(UniValue::VARR);
1694+
for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
1695+
feerates_res.push_back(feerate_percentiles[i]);
1696+
}
1697+
16531698
UniValue ret_all(UniValue::VOBJ);
16541699
ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0);
16551700
ret_all.pushKV("avgfeerate", total_weight ? (totalfee * WITNESS_SCALE_FACTOR) / total_weight : 0); // Unit: sat/vbyte
16561701
ret_all.pushKV("avgtxsize", (block.vtx.size() > 1) ? total_size / (block.vtx.size() - 1) : 0);
16571702
ret_all.pushKV("blockhash", pindex->GetBlockHash().GetHex());
1703+
ret_all.pushKV("feerate_percentiles", feerates_res);
16581704
ret_all.pushKV("height", (int64_t)pindex->nHeight);
16591705
ret_all.pushKV("ins", inputs);
16601706
ret_all.pushKV("maxfee", maxfee);
16611707
ret_all.pushKV("maxfeerate", maxfeerate);
16621708
ret_all.pushKV("maxtxsize", maxtxsize);
16631709
ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array));
1664-
ret_all.pushKV("medianfeerate", CalculateTruncatedMedian(feerate_array));
16651710
ret_all.pushKV("mediantime", pindex->GetMedianTimePast());
16661711
ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array));
16671712
ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee);

src/rpc/blockchain.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2017 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_RPC_BLOCKCHAIN_H
6+
#define BITCOIN_RPC_BLOCKCHAIN_H
7+
8+
#include <vector>
9+
#include <stdint.h>
10+
#include <amount.h>
11+
12+
class CBlock;
13+
class CBlockIndex;
14+
class UniValue;
15+
16+
static constexpr int NUM_GETBLOCKSTATS_PERCENTILES = 5;
17+
18+
/**
19+
* Get the difficulty of the net wrt to the given block index, or the chain tip if
20+
* not provided.
21+
*
22+
* @return A floating point number that is a multiple of the main net minimum
23+
* difficulty (4295032833 hashes).
24+
*/
25+
double GetDifficulty(const CBlockIndex* blockindex = nullptr);
26+
27+
/** Callback for when block tip changed. */
28+
void RPCNotifyBlockChange(bool ibd, const CBlockIndex *);
29+
30+
/** Block description to JSON */
31+
UniValue blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool txDetails = false);
32+
33+
/** Mempool information to JSON */
34+
UniValue mempoolInfoToJSON();
35+
36+
/** Mempool to JSON */
37+
UniValue mempoolToJSON(bool fVerbose = false);
38+
39+
/** Block header to JSON */
40+
UniValue blockheaderToJSON(const CBlockIndex* blockindex);
41+
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+
45+
#endif
46+

src/test/rpc_tests.cpp

Lines changed: 81 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;
@@ -355,4 +357,83 @@ BOOST_AUTO_TEST_CASE(rpc_convert_values_generatetoaddress)
355357
BOOST_CHECK_EQUAL(result[2].get_int(), 9);
356358
}
357359
*/
360+
361+
BOOST_AUTO_TEST_CASE(rpc_getblockstats_calculate_percentiles_by_weight)
362+
{
363+
int64_t total_weight = 200;
364+
std::vector<std::pair<CAmount, int64_t>> feerates;
365+
CAmount result[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
366+
367+
for (int64_t i = 0; i < 100; i++) {
368+
feerates.emplace_back(std::make_pair(1 ,1));
369+
}
370+
371+
for (int64_t i = 0; i < 100; i++) {
372+
feerates.emplace_back(std::make_pair(2 ,1));
373+
}
374+
375+
CalculatePercentilesByWeight(result, feerates, total_weight);
376+
BOOST_CHECK_EQUAL(result[0], 1);
377+
BOOST_CHECK_EQUAL(result[1], 1);
378+
BOOST_CHECK_EQUAL(result[2], 1);
379+
BOOST_CHECK_EQUAL(result[3], 2);
380+
BOOST_CHECK_EQUAL(result[4], 2);
381+
382+
// Test with more pairs, and two pairs overlapping 2 percentiles.
383+
total_weight = 100;
384+
CAmount result2[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
385+
feerates.clear();
386+
387+
feerates.emplace_back(std::make_pair(1, 9));
388+
feerates.emplace_back(std::make_pair(2 , 16)); //10th + 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(result2, feerates, total_weight);
394+
395+
BOOST_CHECK_EQUAL(result2[0], 2);
396+
BOOST_CHECK_EQUAL(result2[1], 2);
397+
BOOST_CHECK_EQUAL(result2[2], 4);
398+
BOOST_CHECK_EQUAL(result2[3], 4);
399+
BOOST_CHECK_EQUAL(result2[4], 9);
400+
401+
// Same test as above, but one of the percentile-overlapping pairs is split in 2.
402+
total_weight = 100;
403+
CAmount result3[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
404+
feerates.clear();
405+
406+
feerates.emplace_back(std::make_pair(1, 9));
407+
feerates.emplace_back(std::make_pair(2 , 11)); // 10th percentile
408+
feerates.emplace_back(std::make_pair(2 , 5)); // 25th percentile
409+
feerates.emplace_back(std::make_pair(4 ,50)); //50th + 75th percentile
410+
feerates.emplace_back(std::make_pair(5 ,10));
411+
feerates.emplace_back(std::make_pair(9 ,15)); // 90th percentile
412+
413+
CalculatePercentilesByWeight(result3, feerates, total_weight);
414+
415+
BOOST_CHECK_EQUAL(result3[0], 2);
416+
BOOST_CHECK_EQUAL(result3[1], 2);
417+
BOOST_CHECK_EQUAL(result3[2], 4);
418+
BOOST_CHECK_EQUAL(result3[3], 4);
419+
BOOST_CHECK_EQUAL(result3[4], 9);
420+
421+
// Test with one transaction spanning all percentiles.
422+
total_weight = 104;
423+
CAmount result4[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 };
424+
feerates.clear();
425+
426+
feerates.emplace_back(std::make_pair(1, 100));
427+
feerates.emplace_back(std::make_pair(2, 1));
428+
feerates.emplace_back(std::make_pair(3, 1));
429+
feerates.emplace_back(std::make_pair(3, 1));
430+
feerates.emplace_back(std::make_pair(999999, 1));
431+
432+
CalculatePercentilesByWeight(result4, feerates, total_weight);
433+
434+
for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) {
435+
BOOST_CHECK_EQUAL(result4[i], 1);
436+
}
437+
}
438+
358439
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)