Skip to content

Commit b9551d3

Browse files
committed
Merge #10757: RPC: Introduce getblockstats to plot things
41d0476 Tests: Add data file (Anthony Towns) 4cbfb6a Tests: Test new getblockstats RPC (Jorge Timón) 35e77a0 RPC: Introduce getblockstats (Jorge Timón) cda8e36 Refactor: RPC: Separate GetBlockChecked() from getblock() (Jorge Timón) Pull request description: It returns per block statistics about several things. It should be easy to add more if people think of other things to add or remove some if I went too far (but once written, why not keep it? EDIT: answer: not to test or maintain them). The currently available options are: minfee,maxfee,totalfee,minfeerate,maxfeerate,avgfee,avgfeerate,txs,ins,outs (EDIT: see updated list in the rpc call documentation) For the x axis, one can use height or block.nTime (I guess I could add mediantime if there's interest [EDIT: nobody showed interest but I implemented mediantime nonetheless, in fact there's no distinction between x or y axis anymore, that's for the caller to judge]). To calculate fees, -txindex is required. Tree-SHA512: 2b2787a3c7dc4a11df1fce62c8a4c748f5347d7f7104205d5f0962ffec1e0370c825b49fd4d58ce8ce86bf39d8453f698bcd46206eea505f077541ca7d59b18c
2 parents 6916024 + 41d0476 commit b9551d3

File tree

5 files changed

+688
-11
lines changed

5 files changed

+688
-11
lines changed

src/rpc/blockchain.cpp

Lines changed: 301 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <consensus/validation.h>
1414
#include <validation.h>
1515
#include <core_io.h>
16+
#include <index/txindex.h>
1617
#include <policy/feerate.h>
1718
#include <policy/policy.h>
1819
#include <primitives/transaction.h>
@@ -31,6 +32,7 @@
3132

3233
#include <univalue.h>
3334

35+
#include <boost/algorithm/string.hpp>
3436
#include <boost/thread/thread.hpp> // boost::thread::interrupt
3537

3638
#include <memory>
@@ -737,6 +739,25 @@ static UniValue getblockheader(const JSONRPCRequest& request)
737739
return blockheaderToJSON(pblockindex);
738740
}
739741

742+
static CBlock GetBlockChecked(const CBlockIndex* pblockindex)
743+
{
744+
CBlock block;
745+
if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) {
746+
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
747+
}
748+
749+
if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) {
750+
// Block not found on disk. This could be because we have the block
751+
// header in our index but don't have the block (for example if a
752+
// non-whitelisted node sends us an unrequested long chain of valid
753+
// blocks, we add the headers to our index, but don't accept the
754+
// block).
755+
throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
756+
}
757+
758+
return block;
759+
}
760+
740761
static UniValue getblock(const JSONRPCRequest& request)
741762
{
742763
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
@@ -805,17 +826,7 @@ static UniValue getblock(const JSONRPCRequest& request)
805826
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
806827
}
807828

808-
CBlock block;
809-
if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0)
810-
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
811-
812-
if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus()))
813-
// Block not found on disk. This could be because we have the block
814-
// header in our index but don't have the block (for example if a
815-
// non-whitelisted node sends us an unrequested long chain of valid
816-
// blocks, we add the headers to our index, but don't accept the
817-
// block).
818-
throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
829+
const CBlock block = GetBlockChecked(pblockindex);
819830

820831
if (verbosity <= 0)
821832
{
@@ -1614,6 +1625,284 @@ static UniValue getchaintxstats(const JSONRPCRequest& request)
16141625
return ret;
16151626
}
16161627

1628+
template<typename T>
1629+
static T CalculateTruncatedMedian(std::vector<T>& scores)
1630+
{
1631+
size_t size = scores.size();
1632+
if (size == 0) {
1633+
return 0;
1634+
}
1635+
1636+
std::sort(scores.begin(), scores.end());
1637+
if (size % 2 == 0) {
1638+
return (scores[size / 2 - 1] + scores[size / 2]) / 2;
1639+
} else {
1640+
return scores[size / 2];
1641+
}
1642+
}
1643+
1644+
template<typename T>
1645+
static inline bool SetHasKeys(const std::set<T>& set) {return false;}
1646+
template<typename T, typename Tk, typename... Args>
1647+
static inline bool SetHasKeys(const std::set<T>& set, const Tk& key, const Args&... args)
1648+
{
1649+
return (set.count(key) != 0) || SetHasKeys(set, args...);
1650+
}
1651+
1652+
// outpoint (needed for the utxo index) + nHeight + fCoinBase
1653+
static constexpr size_t PER_UTXO_OVERHEAD = sizeof(COutPoint) + sizeof(uint32_t) + sizeof(bool);
1654+
1655+
static UniValue getblockstats(const JSONRPCRequest& request)
1656+
{
1657+
if (request.fHelp || request.params.size() < 1 || request.params.size() > 4) {
1658+
throw std::runtime_error(
1659+
"getblockstats hash_or_height ( stats )\n"
1660+
"\nCompute per block statistics for a given window. All amounts are in satoshis.\n"
1661+
"It won't work for some heights with pruning.\n"
1662+
"It won't work without -txindex for utxo_size_inc, *fee or *feerate stats.\n"
1663+
"\nArguments:\n"
1664+
"1. \"hash_or_height\" (string or numeric, required) The block hash or height of the target block\n"
1665+
"2. \"stats\" (array, optional) Values to plot, by default all values (see result below)\n"
1666+
" [\n"
1667+
" \"height\", (string, optional) Selected statistic\n"
1668+
" \"time\", (string, optional) Selected statistic\n"
1669+
" ,...\n"
1670+
" ]\n"
1671+
"\nResult:\n"
1672+
"{ (json object)\n"
1673+
" \"avgfee\": xxxxx, (numeric) Average fee in the block\n"
1674+
" \"avgfeerate\": xxxxx, (numeric) Average feerate (in satoshis per virtual byte)\n"
1675+
" \"avgtxsize\": xxxxx, (numeric) Average transaction size\n"
1676+
" \"blockhash\": xxxxx, (string) The block hash (to check for potential reorgs)\n"
1677+
" \"height\": xxxxx, (numeric) The height of the block\n"
1678+
" \"ins\": xxxxx, (numeric) The number of inputs (excluding coinbase)\n"
1679+
" \"maxfee\": xxxxx, (numeric) Maximum fee in the block\n"
1680+
" \"maxfeerate\": xxxxx, (numeric) Maximum feerate (in satoshis per virtual byte)\n"
1681+
" \"maxtxsize\": xxxxx, (numeric) Maximum transaction size\n"
1682+
" \"medianfee\": xxxxx, (numeric) Truncated median fee in the block\n"
1683+
" \"medianfeerate\": xxxxx, (numeric) Truncated median feerate (in satoshis per virtual byte)\n"
1684+
" \"mediantime\": xxxxx, (numeric) The block median time past\n"
1685+
" \"mediantxsize\": xxxxx, (numeric) Truncated median transaction size\n"
1686+
" \"minfee\": xxxxx, (numeric) Minimum fee in the block\n"
1687+
" \"minfeerate\": xxxxx, (numeric) Minimum feerate (in satoshis per virtual byte)\n"
1688+
" \"mintxsize\": xxxxx, (numeric) Minimum transaction size\n"
1689+
" \"outs\": xxxxx, (numeric) The number of outputs\n"
1690+
" \"subsidy\": xxxxx, (numeric) The block subsidy\n"
1691+
" \"swtotal_size\": xxxxx, (numeric) Total size of all segwit transactions\n"
1692+
" \"swtotal_weight\": xxxxx, (numeric) Total weight of all segwit transactions divided by segwit scale factor (4)\n"
1693+
" \"swtxs\": xxxxx, (numeric) The number of segwit transactions\n"
1694+
" \"time\": xxxxx, (numeric) The block time\n"
1695+
" \"total_out\": xxxxx, (numeric) Total amount in all outputs (excluding coinbase and thus reward [ie subsidy + totalfee])\n"
1696+
" \"total_size\": xxxxx, (numeric) Total size of all non-coinbase transactions\n"
1697+
" \"total_weight\": xxxxx, (numeric) Total weight of all non-coinbase transactions divided by segwit scale factor (4)\n"
1698+
" \"totalfee\": xxxxx, (numeric) The fee total\n"
1699+
" \"txs\": xxxxx, (numeric) The number of transactions (excluding coinbase)\n"
1700+
" \"utxo_increase\": xxxxx, (numeric) The increase/decrease in the number of unspent outputs\n"
1701+
" \"utxo_size_inc\": xxxxx, (numeric) The increase/decrease in size for the utxo index (not discounting op_return and similar)\n"
1702+
"}\n"
1703+
"\nExamples:\n"
1704+
+ HelpExampleCli("getblockstats", "1000 '[\"minfeerate\",\"avgfeerate\"]'")
1705+
+ HelpExampleRpc("getblockstats", "1000 '[\"minfeerate\",\"avgfeerate\"]'")
1706+
);
1707+
}
1708+
1709+
LOCK(cs_main);
1710+
1711+
CBlockIndex* pindex;
1712+
if (request.params[0].isNum()) {
1713+
const int height = request.params[0].get_int();
1714+
const int current_tip = chainActive.Height();
1715+
if (height < 0) {
1716+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %d is negative", height));
1717+
}
1718+
if (height > current_tip) {
1719+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Target block height %d after current tip %d", height, current_tip));
1720+
}
1721+
1722+
pindex = chainActive[height];
1723+
} else {
1724+
const std::string strHash = request.params[0].get_str();
1725+
const uint256 hash(uint256S(strHash));
1726+
pindex = LookupBlockIndex(hash);
1727+
if (!pindex) {
1728+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
1729+
}
1730+
if (!chainActive.Contains(pindex)) {
1731+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Block is not in chain %s", Params().NetworkIDString()));
1732+
}
1733+
}
1734+
1735+
assert(pindex != nullptr);
1736+
1737+
std::set<std::string> stats;
1738+
if (!request.params[1].isNull()) {
1739+
const UniValue stats_univalue = request.params[1].get_array();
1740+
for (unsigned int i = 0; i < stats_univalue.size(); i++) {
1741+
const std::string stat = stats_univalue[i].get_str();
1742+
stats.insert(stat);
1743+
}
1744+
}
1745+
1746+
const CBlock block = GetBlockChecked(pindex);
1747+
1748+
const bool do_all = stats.size() == 0; // Calculate everything if nothing selected (default)
1749+
const bool do_mediantxsize = do_all || stats.count("mediantxsize") != 0;
1750+
const bool do_medianfee = do_all || stats.count("medianfee") != 0;
1751+
const bool do_medianfeerate = do_all || stats.count("medianfeerate") != 0;
1752+
const bool loop_inputs = do_all || do_medianfee || do_medianfeerate ||
1753+
SetHasKeys(stats, "utxo_size_inc", "totalfee", "avgfee", "avgfeerate", "minfee", "maxfee", "minfeerate", "maxfeerate");
1754+
const bool loop_outputs = do_all || loop_inputs || stats.count("total_out");
1755+
const bool do_calculate_size = do_mediantxsize ||
1756+
SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize", "maxtxsize", "swtotal_size");
1757+
const bool do_calculate_weight = do_all || SetHasKeys(stats, "total_weight", "avgfeerate", "swtotal_weight", "avgfeerate", "medianfeerate", "minfeerate", "maxfeerate");
1758+
const bool do_calculate_sw = do_all || SetHasKeys(stats, "swtxs", "swtotal_size", "swtotal_weight");
1759+
1760+
CAmount maxfee = 0;
1761+
CAmount maxfeerate = 0;
1762+
CAmount minfee = MAX_MONEY;
1763+
CAmount minfeerate = MAX_MONEY;
1764+
CAmount total_out = 0;
1765+
CAmount totalfee = 0;
1766+
int64_t inputs = 0;
1767+
int64_t maxtxsize = 0;
1768+
int64_t mintxsize = MAX_BLOCK_SERIALIZED_SIZE;
1769+
int64_t outputs = 0;
1770+
int64_t swtotal_size = 0;
1771+
int64_t swtotal_weight = 0;
1772+
int64_t swtxs = 0;
1773+
int64_t total_size = 0;
1774+
int64_t total_weight = 0;
1775+
int64_t utxo_size_inc = 0;
1776+
std::vector<CAmount> fee_array;
1777+
std::vector<CAmount> feerate_array;
1778+
std::vector<int64_t> txsize_array;
1779+
1780+
for (const auto& tx : block.vtx) {
1781+
outputs += tx->vout.size();
1782+
1783+
CAmount tx_total_out = 0;
1784+
if (loop_outputs) {
1785+
for (const CTxOut& out : tx->vout) {
1786+
tx_total_out += out.nValue;
1787+
utxo_size_inc += GetSerializeSize(out, SER_NETWORK, PROTOCOL_VERSION) + PER_UTXO_OVERHEAD;
1788+
}
1789+
}
1790+
1791+
if (tx->IsCoinBase()) {
1792+
continue;
1793+
}
1794+
1795+
inputs += tx->vin.size(); // Don't count coinbase's fake input
1796+
total_out += tx_total_out; // Don't count coinbase reward
1797+
1798+
int64_t tx_size = 0;
1799+
if (do_calculate_size) {
1800+
1801+
tx_size = tx->GetTotalSize();
1802+
if (do_mediantxsize) {
1803+
txsize_array.push_back(tx_size);
1804+
}
1805+
maxtxsize = std::max(maxtxsize, tx_size);
1806+
mintxsize = std::min(mintxsize, tx_size);
1807+
total_size += tx_size;
1808+
}
1809+
1810+
int64_t weight = 0;
1811+
if (do_calculate_weight) {
1812+
weight = GetTransactionWeight(*tx);
1813+
total_weight += weight;
1814+
}
1815+
1816+
if (do_calculate_sw && tx->HasWitness()) {
1817+
++swtxs;
1818+
swtotal_size += tx_size;
1819+
swtotal_weight += weight;
1820+
}
1821+
1822+
if (loop_inputs) {
1823+
1824+
if (!g_txindex) {
1825+
throw JSONRPCError(RPC_INVALID_PARAMETER, "One or more of the selected stats requires -txindex enabled");
1826+
}
1827+
CAmount tx_total_in = 0;
1828+
for (const CTxIn& in : tx->vin) {
1829+
CTransactionRef tx_in;
1830+
uint256 hashBlock;
1831+
if (!GetTransaction(in.prevout.hash, tx_in, Params().GetConsensus(), hashBlock, false)) {
1832+
throw JSONRPCError(RPC_INTERNAL_ERROR, std::string("Unexpected internal error (tx index seems corrupt)"));
1833+
}
1834+
1835+
CTxOut prevoutput = tx_in->vout[in.prevout.n];
1836+
1837+
tx_total_in += prevoutput.nValue;
1838+
utxo_size_inc -= GetSerializeSize(prevoutput, SER_NETWORK, PROTOCOL_VERSION) + PER_UTXO_OVERHEAD;
1839+
}
1840+
1841+
CAmount txfee = tx_total_in - tx_total_out;
1842+
assert(MoneyRange(txfee));
1843+
if (do_medianfee) {
1844+
fee_array.push_back(txfee);
1845+
}
1846+
maxfee = std::max(maxfee, txfee);
1847+
minfee = std::min(minfee, txfee);
1848+
totalfee += txfee;
1849+
1850+
// New feerate uses satoshis per virtual byte instead of per serialized byte
1851+
CAmount feerate = weight ? (txfee * WITNESS_SCALE_FACTOR) / weight : 0;
1852+
if (do_medianfeerate) {
1853+
feerate_array.push_back(feerate);
1854+
}
1855+
maxfeerate = std::max(maxfeerate, feerate);
1856+
minfeerate = std::min(minfeerate, feerate);
1857+
}
1858+
}
1859+
1860+
UniValue ret_all(UniValue::VOBJ);
1861+
ret_all.pushKV("avgfee", (block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0);
1862+
ret_all.pushKV("avgfeerate", total_weight ? (totalfee * WITNESS_SCALE_FACTOR) / total_weight : 0); // Unit: sat/vbyte
1863+
ret_all.pushKV("avgtxsize", (block.vtx.size() > 1) ? total_size / (block.vtx.size() - 1) : 0);
1864+
ret_all.pushKV("blockhash", pindex->GetBlockHash().GetHex());
1865+
ret_all.pushKV("height", (int64_t)pindex->nHeight);
1866+
ret_all.pushKV("ins", inputs);
1867+
ret_all.pushKV("maxfee", maxfee);
1868+
ret_all.pushKV("maxfeerate", maxfeerate);
1869+
ret_all.pushKV("maxtxsize", maxtxsize);
1870+
ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array));
1871+
ret_all.pushKV("medianfeerate", CalculateTruncatedMedian(feerate_array));
1872+
ret_all.pushKV("mediantime", pindex->GetMedianTimePast());
1873+
ret_all.pushKV("mediantxsize", CalculateTruncatedMedian(txsize_array));
1874+
ret_all.pushKV("minfee", (minfee == MAX_MONEY) ? 0 : minfee);
1875+
ret_all.pushKV("minfeerate", (minfeerate == MAX_MONEY) ? 0 : minfeerate);
1876+
ret_all.pushKV("mintxsize", mintxsize == MAX_BLOCK_SERIALIZED_SIZE ? 0 : mintxsize);
1877+
ret_all.pushKV("outs", outputs);
1878+
ret_all.pushKV("subsidy", GetBlockSubsidy(pindex->nHeight, Params().GetConsensus()));
1879+
ret_all.pushKV("swtotal_size", swtotal_size);
1880+
ret_all.pushKV("swtotal_weight", swtotal_weight);
1881+
ret_all.pushKV("swtxs", swtxs);
1882+
ret_all.pushKV("time", pindex->GetBlockTime());
1883+
ret_all.pushKV("total_out", total_out);
1884+
ret_all.pushKV("total_size", total_size);
1885+
ret_all.pushKV("total_weight", total_weight);
1886+
ret_all.pushKV("totalfee", totalfee);
1887+
ret_all.pushKV("txs", (int64_t)block.vtx.size());
1888+
ret_all.pushKV("utxo_increase", outputs - inputs);
1889+
ret_all.pushKV("utxo_size_inc", utxo_size_inc);
1890+
1891+
if (do_all) {
1892+
return ret_all;
1893+
}
1894+
1895+
UniValue ret(UniValue::VOBJ);
1896+
for (const std::string& stat : stats) {
1897+
const UniValue& value = ret_all[stat];
1898+
if (value.isNull()) {
1899+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid selected statistic %s", stat));
1900+
}
1901+
ret.pushKV(stat, value);
1902+
}
1903+
return ret;
1904+
}
1905+
16171906
static UniValue savemempool(const JSONRPCRequest& request)
16181907
{
16191908
if (request.fHelp || request.params.size() != 0) {
@@ -1642,6 +1931,7 @@ static const CRPCCommand commands[] =
16421931
// --------------------- ------------------------ ----------------------- ----------
16431932
{ "blockchain", "getblockchaininfo", &getblockchaininfo, {} },
16441933
{ "blockchain", "getchaintxstats", &getchaintxstats, {"nblocks", "blockhash"} },
1934+
{ "blockchain", "getblockstats", &getblockstats, {"hash_or_height", "stats"} },
16451935
{ "blockchain", "getbestblockhash", &getbestblockhash, {} },
16461936
{ "blockchain", "getblockcount", &getblockcount, {} },
16471937
{ "blockchain", "getblock", &getblock, {"blockhash","verbosity|verbose"} },

src/rpc/client.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
123123
{ "importmulti", 1, "options" },
124124
{ "verifychain", 0, "checklevel" },
125125
{ "verifychain", 1, "nblocks" },
126+
{ "getblockstats", 0, "hash_or_height" },
127+
{ "getblockstats", 1, "stats" },
126128
{ "pruneblockchain", 0, "height" },
127129
{ "keypoolrefill", 0, "newsize" },
128130
{ "getrawmempool", 0, "verbose" },

0 commit comments

Comments
 (0)