|
|
@@ -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"} },
|
|
|
|