RPC: Introduce getblockstats to plot things #10757

Open
wants to merge 4 commits into
from
View
@@ -20,20 +20,22 @@ CFeeRate::CFeeRate(const CAmount& nFeePaid, size_t nBytes_)
nSatoshisPerK = 0;
}
-CAmount CFeeRate::GetFee(size_t nBytes_) const
+CAmount CFeeRate::GetTruncatedFee(size_t bytes) const
{
- assert(nBytes_ <= uint64_t(std::numeric_limits<int64_t>::max()));
- int64_t nSize = int64_t(nBytes_);
+ assert(bytes <= uint64_t(std::numeric_limits<int64_t>::max()));
+ return nSatoshisPerK * int64_t(bytes) / 1000;
+}
- CAmount nFee = nSatoshisPerK * nSize / 1000;
+CAmount CFeeRate::GetFee(size_t bytes) const
+{
+ CAmount nFee = GetTruncatedFee(bytes);
- if (nFee == 0 && nSize != 0) {
+ if (nFee == 0 && bytes != 0) {
if (nSatoshisPerK > 0)
nFee = CAmount(1);
if (nSatoshisPerK < 0)
nFee = CAmount(-1);
}
-
return nFee;
}
View
@@ -30,7 +30,12 @@ class CFeeRate
/**
* Return the fee in satoshis for the given size in bytes.
*/
- CAmount GetFee(size_t nBytes) const;
+ CAmount GetTruncatedFee(size_t bytes) const;
+ /**
+ * Return the fee in satoshis for the given size in bytes. If the
+ * result is zero, return 1 if the fee was positive or -1 if was negative.
+ */
+ CAmount GetFee(size_t bytes) const;
/**
* Return the fee in satoshis for a size of 1000 bytes
*/
View
@@ -29,6 +29,7 @@
#include <univalue.h>
+#include <boost/algorithm/string.hpp>
#include <boost/thread/thread.hpp> // boost::thread::interrupt
#include <mutex>
@@ -685,6 +686,22 @@ UniValue getblockheader(const JSONRPCRequest& request)
return blockheaderToJSON(pblockindex);
}
+static void ReadBlockCheckPruned(CBlock& block, const CBlockIndex* pblockindex)
+{
+ if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) {
+ throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
+ }
+
+ if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) {
+ // Block not found on disk. This could be because we have the block
+ // header in our index but don't have the block (for example if a
+ // non-whitelisted node sends us an unrequested long chain of valid
+ // blocks, we add the headers to our index, but don't accept the
+ // block).
+ throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
+ }
+}
+
UniValue getblock(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
@@ -753,17 +770,7 @@ UniValue getblock(const JSONRPCRequest& request)
CBlock block;
CBlockIndex* pblockindex = mapBlockIndex[hash];
-
- if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0)
- throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
-
- if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus()))
- // Block not found on disk. This could be because we have the block
- // header in our index but don't have the block (for example if a
- // non-whitelisted node sends us an unrequested long chain of valid
- // blocks, we add the headers to our index, but don't accept the
- // block).
- throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
+ ReadBlockCheckPruned(block, pblockindex);
if (verbosity <= 0)
{
@@ -1532,6 +1539,293 @@ UniValue getchaintxstats(const JSONRPCRequest& request)
return ret;
}
+static void RpcGetTx(const uint256& hash, CTransactionRef& tx_out)
+{
+ uint256 hashBlock;
+ if (!GetTransaction(hash, tx_out, Params().GetConsensus(), hashBlock, true)) {
+ throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string(fTxIndex ? "No such mempool or blockchain transaction"
+ : "No such mempool transaction. Use -txindex to enable blockchain transaction queries"));
+ }
+}
+
+template<typename T>
+static T CalculateTruncatedMedian(std::vector<T>& scores)
+{
+ size_t size = scores.size();
+ if (size == 0) {
+ return 0;
+ } if (size == 1) {
+ return scores[0];
+ }
+
+ std::sort(scores.begin(), scores.end());
+ if (size % 2 == 0) {
+ return (scores[size / 2 - 1] + scores[size / 2]) / 2;
+ } else {
+ return scores[size / 2];
+ }
+}
+
+// outpoint (needed for the utxo index) + nHeight + fCoinBase
+static const size_t PER_UTXO_OVERHEAD = sizeof(COutPoint) + sizeof(uint32_t) + sizeof(bool);
+
+static void UpdateBlockStats(const CBlockIndex* pindex, std::set<std::string>& stats, std::map<std::string, UniValue>& map_stats)
+{
+ int64_t inputs = 0;
+ int64_t outputs = 0;
+ int64_t total_size = 0;
+ int64_t total_weight = 0;
+ int64_t utxo_size_inc = 0;
+ CAmount total_out = 0;
+ CAmount totalfee = 0;
+ CAmount minfee = MAX_MONEY;
+ CAmount maxfee = 0;
+ CAmount minfeerate = MAX_MONEY;
+ CAmount maxfeerate = 0;
+ CAmount minfeerate_old = MAX_MONEY;
+ CAmount maxfeerate_old = 0;
+ std::vector<CAmount> fee_array;
+ std::vector<CAmount> feerate_old_array;
+ std::vector<CAmount> feerate_array;
+
+ CBlock block;
+ ReadBlockCheckPruned(block, pindex);
+
+ for (const auto& tx : block.vtx) {
+ outputs += tx->vout.size();
+ CAmount tx_total_out = 0;
+ for (const CTxOut& out : tx->vout) {
+ utxo_size_inc += GetSerializeSize(out, SER_NETWORK, PROTOCOL_VERSION) + PER_UTXO_OVERHEAD;
+ tx_total_out += out.nValue;
+ }
+
+ if (tx->IsCoinBase()) {
+ continue;
+ }
+ total_out += tx_total_out;
+
+ inputs += tx->vin.size(); // Don't count coinbase's fake input
+ int64_t tx_size = tx->GetTotalSize();
+ total_size += tx_size;
+ int64_t weight = GetTransactionWeight(*tx);
+ total_weight += weight;
+
+ CAmount tx_total_in = 0;
+ for (const CTxIn& in : tx->vin) {
+ CTransactionRef tx_in;
+ RpcGetTx(in.prevout.hash, tx_in);
+ CTxOut prevoutput = tx_in->vout[in.prevout.n];
+
+ tx_total_in += prevoutput.nValue;
+ utxo_size_inc -= GetSerializeSize(prevoutput, SER_NETWORK, PROTOCOL_VERSION) + PER_UTXO_OVERHEAD;
+ }
+ CAmount txfee = tx_total_in - tx_total_out;
+ assert(MoneyRange(txfee));
+ fee_array.push_back(txfee);
+ totalfee += txfee;
+ minfee = std::min(minfee, txfee);
+ maxfee = std::max(maxfee, txfee);
+
+ CAmount feerate_old = CFeeRate(txfee, tx_size).GetTruncatedFee(1);
+ feerate_old_array.push_back(feerate_old);
+ // New feerate uses satoshis per weighted byte instead of per byte
+ CAmount feerate = CFeeRate(txfee, weight).GetTruncatedFee(WITNESS_SCALE_FACTOR);
+ feerate_array.push_back(feerate);
+
+ minfeerate = std::min(minfeerate, feerate);
+ maxfeerate = std::max(maxfeerate, feerate);
+ minfeerate_old = std::min(minfeerate_old, feerate_old);
+ maxfeerate_old = std::max(maxfeerate_old, feerate_old);
+ }
+
+ for (const std::string& stat : stats) {
+ // Update map_stats
+ if (stat == "height") {
+ map_stats[stat].push_back((int64_t)pindex->nHeight);
+ } else if (stat == "time") {
+ map_stats[stat].push_back(pindex->GetBlockTime());
+ } else if (stat == "mediantime") {
+ map_stats[stat].push_back(pindex->GetMedianTimePast());
+ } else if (stat == "subsidy") {
+ map_stats[stat].push_back(GetBlockSubsidy(pindex->nHeight, Params().GetConsensus()));
+ } else if (stat == "totalfee") {
+ map_stats[stat].push_back(totalfee);
+ } else if (stat == "reward") {
+ map_stats[stat].push_back(totalfee + GetBlockSubsidy(pindex->nHeight, Params().GetConsensus()));
+ } else if (stat == "txs") {
+ map_stats[stat].push_back((int64_t)block.vtx.size());
+ } else if (stat == "ins") {
+ map_stats[stat].push_back(inputs);
+ } else if (stat == "outs") {
+ map_stats[stat].push_back(outputs);
+ } else if (stat == "utxo_increase") {
+ map_stats[stat].push_back(outputs - inputs);
+ } else if (stat == "utxo_size_inc") {
+ map_stats[stat].push_back(utxo_size_inc);
+ } else if (stat == "total_size") {
+ map_stats[stat].push_back(total_size);
+ } else if (stat == "total_weight") {
+ map_stats[stat].push_back(total_weight);
+ } else if (stat == "total_out") {
+ map_stats[stat].push_back(total_out);
+ } else if (stat == "minfee") {
+ map_stats[stat].push_back((minfee == MAX_MONEY) ? 0 : minfee);
+ } else if (stat == "maxfee") {
+ map_stats[stat].push_back(maxfee);
+ } else if (stat == "medianfee") {
+ map_stats[stat].push_back(CalculateTruncatedMedian(fee_array));
+ } else if (stat == "avgfee") {
+ map_stats[stat].push_back((block.vtx.size() > 1) ? totalfee / (block.vtx.size() - 1) : 0);
+ } else if (stat == "minfeerate") {
+ map_stats[stat].push_back((minfeerate == MAX_MONEY) ? 0 : minfeerate);
+ } else if (stat == "maxfeerate") {
+ map_stats[stat].push_back(maxfeerate);
+ } else if (stat == "medianfeerate") {
+ map_stats[stat].push_back(CalculateTruncatedMedian(feerate_array));
+ } else if (stat == "avgfeerate") {
+ map_stats[stat].push_back(CFeeRate(totalfee, total_weight).GetTruncatedFee(WITNESS_SCALE_FACTOR));
+ } else if (stat == "minfeerate_old") {
+ map_stats[stat].push_back((minfeerate_old == MAX_MONEY) ? 0 : minfeerate_old);
+ } else if (stat == "maxfeerate_old") {
+ map_stats[stat].push_back(maxfeerate_old);
+ } else if (stat == "medianfeerate_old") {
+ map_stats[stat].push_back(CalculateTruncatedMedian(feerate_old_array));
+ } else if (stat == "avgfeerate_old") {
+ map_stats[stat].push_back(CFeeRate(totalfee, total_size).GetTruncatedFee(1));
+ }
+ }
+}
+
+UniValue getblockstats(const JSONRPCRequest& request)
+{
+ std::set<std::string> valid_stats = {
+ "height",
+ "time",
+ "mediantime",
+ "txs",
+ "ins",
+ "outs",
+ "subsidy",
+ "totalfee",
+ "reward",
+ "utxo_increase",
+ "utxo_size_inc",
+ "total_size",
+ "total_weight",
+ "total_out",
+ "minfee",
+ "maxfee",
+ "medianfee",
+ "avgfee",
+ "minfeerate",
+ "maxfeerate",
+ "medianfeerate",
+ "avgfeerate",
+ "minfeerate_old",
+ "maxfeerate_old",
+ "medianfeerate_old",
+ "avgfeerate_old",
+ };
+ if (request.fHelp || request.params.size() < 1 || request.params.size() > 4)
+ throw std::runtime_error(
+ "getblockstats ( nStart nEnd stats )\n"
+ "\nCompute per block statistics for a given window. All amounts are in satoshis.\n"
+ "\nNegative values for start or end count back from the current tip.\n"
+ "\nIt won't work in some cases with pruning or without -txindex.\n"
+ "\nArguments:\n"
+ "1. \"start\" (numeric, required) The height of the block that starts the window.\n"
+ "2. \"end\" (numeric, optional) The height of the block that ends the window (default: current tip).\n"
+ "3. \"stats\" (string, optional) Values to plot (comma separated), default(all): " + boost::join(valid_stats, ",") +
+ "\nResult: (all values are in reverse order height-wise)\n"
+ "{ (json object)\n"
+ " \"height\": [], (array) The height of the blocks, ie: [end, end-1, ..., start+1, start].\n"
+ " \"time\": [], (array) The block time.\n"
+ " \"mediantime\": [], (array) The block median time past.\n"
+ " \"txs\": [], (array) The number of transactions (excluding coinbase).\n"
+ " \"ins\": [], (array) The number of inputs (excluding coinbase).\n"
+ " \"outs\": [], (array) The number of outputs (including coinbase).\n"
+ " \"subsidy\": [], (array) The block subsidy.\n"
+ " \"totalfee\": [], (array) The fee total.\n"
+ " \"reward\": [], (array) The subsidy plus the fee total.\n"
+ " \"utxo_increase\": [], (array) The increase/decrease in the number of unspent outputs.\n"
+ " \"utxo_size_inc\": [], (array) The increase/decrease in size for the utxo index (not discounting op_return and similar).\n"
+ " \"total_size\": [], (array) Total size of all non-coinbase transactions.\n"
+ " \"total_weight\": [], (array) Total weight of all non-coinbase transactions divided by segwit scale factor (4).\n"
+ " \"total_out\": [], (array) Total amount in all outputs (excluding coinbase and thus reward).\n"
+ " \"minfee\": [], (array) Minimum fee in the block.\n"
+ " \"maxfee\": [], (array) Maximum fee in the block.\n"
+ " \"medianfee\": [], (array) Truncated median fee in the block.\n"
+ " \"avgfee\": [], (array) Average fee in the block.\n"
+ " \"minfeerate\": [], (array) Minimum feerate (in satoshis per weigthed byte).\n"
+ " \"maxfeerate\": [], (array) Maximum feerate (in satoshis per weigthed byte).\n"
+ " \"medianfeerate\": [], (array) Truncated median feerate (in satoshis per weigthed byte).\n"
+ " \"avgfeerate\": [], (array) Average feerate (in satoshis per weigthed byte).\n"
+ " \"minfeerate_old\": [], (array) Minimum feerate (in satoshis per byte [excluding segwits]).\n"
+ " \"maxfeerate_old\": [], (array) Maximum feerate (in satoshis per byte [excluding segwits]).\n"
+ " \"medianfeerate_old\": [], (array) Truncated median feerate (in satoshis per byte [excluding segwits]).\n"
+ " \"avgfeerate_old\": [], (array) Average feerate (in satoshis per byte [excluding segwits]).\n"
+ "}\n"
+ "\nExamples:\n"
+ + HelpExampleCli("getblockstats", "1000 1000 \"minfeerate,avgfeerate\"")
+ + HelpExampleRpc("getblockstats", "1000 1000 \"maxfeerate,avgfeerate\"")
+ );
+
+ LOCK(cs_main);
+
+ int start = request.params[0].get_int();
+ int current_tip = chainActive.Height();
+ if (start < 0) {
+ start = current_tip + start;
+ }
+ if (start < 0 || start > current_tip) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Start block height %d after current tip %d", start, current_tip));
+ }
+
+ int end;
+ if (request.params.size() > 1) {
+ end = request.params[1].get_int();
+ if (end < 0) {
+ end = current_tip + end;
+ }
+ } else {
+ end = current_tip;
+ }
+ if (end < 0 || end > current_tip) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("End block height %d after current tip %d", end, current_tip));
+ }
+ if (start > end) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Start block height %d higher than end %d", start, end));
+ }
+
+ std::set<std::string> stats;
+ if (request.params.size() > 2) {
+ boost::split(stats, request.params[2].get_str(), boost::is_any_of(","));
+
+ for (const std::string& stat : stats) {
+ if (valid_stats.count(stat) == 0) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid selected statistic %s", stat));
+ }
+ }
+ } else {
+ stats = valid_stats;
+ }
+
+ std::map<std::string, UniValue> map_stats;
+ for (const std::string& stat : stats) {
+ map_stats[stat] = UniValue(UniValue::VARR);
+ }
+
+ for (int i = end; i >= start; i--) {
+ UpdateBlockStats(chainActive[i], stats, map_stats);
+ }
+
+ UniValue ret(UniValue::VOBJ);
+ for (const std::string stat : stats) {
+ ret.push_back(Pair(stat, map_stats[stat]));
+ }
+ return ret;
+}
+
static const CRPCCommand commands[] =
{ // category name actor (function) okSafe argNames
// --------------------- ------------------------ ----------------------- ------ ----------
@@ -1555,6 +1849,7 @@ static const CRPCCommand commands[] =
{ "blockchain", "verifychain", &verifychain, true, {"checklevel","nblocks"} },
{ "blockchain", "preciousblock", &preciousblock, true, {"blockhash"} },
+ { "blockchain", "getblockstats", &getblockstats, true, {"start", "end", "stats"} },
/* Not shown in help */
{ "hidden", "invalidateblock", &invalidateblock, true, {"blockhash"} },
Oops, something went wrong.