diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 85bd57ec93..8eebee5ff5 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1863,6 +1863,35 @@ static T CalculateTruncatedMedian(std::vector& scores) } } +void CalculatePercentilesBySize(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector>& scores, int64_t total_size) +{ + 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_size / 10.0, total_size / 4.0, total_size / 2.0, (total_size * 3.0) / 4.0, (total_size * 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 static inline bool SetHasKeys(const std::set& set) {return false;} template @@ -1896,13 +1925,19 @@ static UniValue getblockstats(const JSONRPCRequest& request) " \"avgfeerate\": xxxxx, (numeric) Average feerate (in duffs per byte)\n" " \"avgtxsize\": xxxxx, (numeric) Average transaction size\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 duffs per 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" " \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n" " \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n" " \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in duffs per byte)\n" " \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n" " \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n" - " \"medianfeerate\": xxxxx, (numeric) Truncated median feerate (in duffs per byte)\n" " \"mediantime\": xxxxx, (numeric) The block median time past\n" " \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n" " \"minfee\": xxxxx, (numeric) Minimum fee in the block\n" @@ -1972,12 +2007,12 @@ static UniValue getblockstats(const JSONRPCRequest& request) 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_medianfee = do_all || stats.count("medianfee") != 0; - const bool do_medianfeerate = do_all || stats.count("medianfeerate") != 0; - const bool loop_inputs = do_all || do_medianfee || do_medianfeerate || + const bool do_feerate_percentiles = do_all || stats.count("feerate_percentiles") != 0; + const bool loop_inputs = do_all || do_medianfee || do_feerate_percentiles || 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 do_calculate_size = do_all || do_mediantxsize || - SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "avgfeerate", "medianfeerate", "minfeerate", "maxfeerate"); + SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "avgfeerate", "feerate_percentiles", "minfeerate", "maxfeerate"); CAmount maxfee = 0; CAmount maxfeerate = 0; @@ -1992,7 +2027,7 @@ static UniValue getblockstats(const JSONRPCRequest& request) int64_t total_size = 0; int64_t utxo_size_inc = 0; std::vector fee_array; - std::vector feerate_array; + std::vector> feerate_array; std::vector txsize_array; for (const auto& tx : block.vtx) { @@ -2054,26 +2089,34 @@ static UniValue getblockstats(const JSONRPCRequest& request) totalfee += txfee; CAmount feerate = tx_size ? txfee / tx_size : 0; - if (do_medianfeerate) { - feerate_array.push_back(feerate); + if (do_feerate_percentiles) { + feerate_array.emplace_back(std::make_pair(feerate, tx_size)); } maxfeerate = std::max(maxfeerate, feerate); minfeerate = std::min(minfeerate, feerate); } } + CAmount feerate_percentiles[NUM_GETBLOCKSTATS_PERCENTILES] = { 0 }; + CalculatePercentilesBySize(feerate_percentiles, feerate_array, total_size); + + 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); ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0); ret_all.pushKV("avgfeerate", total_size ? totalfee / total_size : 0); // Unit: sat/byte 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("feerate_percentiles", feerates_res); ret_all.pushKV("height", (int64_t)pindex->nHeight); ret_all.pushKV("ins", inputs); ret_all.pushKV("maxfee", maxfee); ret_all.pushKV("maxfeerate", maxfeerate); ret_all.pushKV("maxtxsize", maxtxsize); ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array)); - ret_all.pushKV("medianfeerate", CalculateTruncatedMedian(feerate_array)); ret_all.pushKV("mediantime", pindex->GetMedianTimePast()); ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array)); ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee); diff --git a/src/rpc/blockchain.h b/src/rpc/blockchain.h index 4d3fe5534a..8703bcaf61 100644 --- a/src/rpc/blockchain.h +++ b/src/rpc/blockchain.h @@ -5,10 +5,16 @@ #ifndef BITCOIN_RPC_BLOCKCHAIN_H #define BITCOIN_RPC_BLOCKCHAIN_H +#include +#include +#include + class CBlock; class CBlockIndex; class UniValue; +static constexpr int NUM_GETBLOCKSTATS_PERCENTILES = 5; + /** * Get the difficulty of the net wrt to the given block index. * @@ -32,4 +38,7 @@ UniValue mempoolToJSON(bool fVerbose = false); /** Block header to JSON */ UniValue blockheaderToJSON(const CBlockIndex* blockindex); +/** Used by getblockstats to get feerates at different percentiles by weight */ +void CalculatePercentilesBySize(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector>& scores, int64_t total_size); + #endif diff --git a/src/test/rpc_tests.cpp b/src/test/rpc_tests.cpp index 4b1447b67f..d96863fb5e 100644 --- a/src/test/rpc_tests.cpp +++ b/src/test/rpc_tests.cpp @@ -16,6 +16,8 @@ #include +#include + UniValue CallRPC(std::string args) { std::vector vArgs; @@ -336,4 +338,82 @@ BOOST_AUTO_TEST_CASE(rpc_convert_values_generatetoaddress) } #endif // ENABLE_MINER +BOOST_AUTO_TEST_CASE(rpc_getblockstats_calculate_percentiles_by_size) +{ + int64_t total_size = 200; + std::vector> 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)); + } + + CalculatePercentilesBySize(result, feerates, total_size); + 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_size = 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 + + CalculatePercentilesBySize(result2, feerates, total_size); + + 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_size = 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 + + CalculatePercentilesBySize(result3, feerates, total_size); + + 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_size = 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)); + + CalculatePercentilesBySize(result4, feerates, total_size); + + for (int64_t i = 0; i < NUM_GETBLOCKSTATS_PERCENTILES; i++) { + BOOST_CHECK_EQUAL(result4[i], 1); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/data/rpc_getblockstats.json b/test/functional/data/rpc_getblockstats.json index f896190700..06ef5f6e99 100644 --- a/test/functional/data/rpc_getblockstats.json +++ b/test/functional/data/rpc_getblockstats.json @@ -112,13 +112,19 @@ "avgfeerate": 0, "avgtxsize": 0, "blockhash": "01654f561a1025bd97deec5b03fe31d68cb4b634a12c34d48fc24414d3c0a499", + "feerate_percentiles": [ + 0, + 0, + 0, + 0, + 0 + ], "height": 101, "ins": 0, "maxfee": 0, "maxfeerate": 0, "maxtxsize": 0, "medianfee": 0, - "medianfeerate": 0, "mediantime": 1417713355, "mediantxsize": 0, "minfee": 0, @@ -140,12 +146,18 @@ "avgtxsize": 192, "blockhash": "659ca0a80269a930b2629c47776e2a4765dbb7af642985a5fdae73c2f4361443", "height": 102, + "feerate_percentiles": [ + 1, + 1, + 1, + 1, + 1 + ], "ins": 1, "maxfee": 192, "maxfeerate": 1, "maxtxsize": 192, "medianfee": 192, - "medianfeerate": 1, "mediantime": 1417713355, "mediantxsize": 192, "minfee": 192, @@ -166,13 +178,19 @@ "avgfeerate": 106, "avgtxsize": 214, "blockhash": "1be37db8b04b80d607631fe5d77351b27eabee984140a790bd1b852a715f0e6d", + "feerate_percentiles": [ + 1, + 1, + 1, + 300, + 300 + ], "height": 103, "ins": 3, "maxfee": 67800, "maxfeerate": 300, "maxtxsize": 226, "medianfee": 226, - "medianfeerate": 1, "mediantime": 1417713356, "mediantxsize": 225, "minfee": 192, diff --git a/test/functional/rpc_getblockstats.py b/test/functional/rpc_getblockstats.py index 8ed05fcd4d..cde9ff4259 100755 --- a/test/functional/rpc_getblockstats.py +++ b/test/functional/rpc_getblockstats.py @@ -26,7 +26,7 @@ class GetblockstatsTest(BitcoinTestFramework): 'maxfee', 'maxfeerate', 'medianfee', - 'medianfeerate', + 'feerate_percentiles', 'minfee', 'minfeerate', 'totalfee',