diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index da2b51f68c3..08dfb51a65d 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -201,6 +201,17 @@ CAmountMap CTransaction::GetFee() const return fee; } +CAmount CTransaction::GetFee(const CAsset& asset) const +{ + CAmount fee = 0; + for (unsigned int i = 0; i < vout.size(); i++) { + if (vout[i].IsFee() && vout[i].nAsset.GetAsset() == asset) { + fee += vout[i].nValue.GetAmount(); + } + } + return fee; +} + CTransaction::CTransaction() : nVersion(CTransaction::CURRENT_VERSION), vin(), vout(), nLockTime(0) { } CTransaction::CTransaction(const CMutableTransaction &tx) : nVersion(tx.nVersion), vin(tx.vin), vout(tx.vout), wit(tx.wit), nLockTime(tx.nLockTime) { diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index e9619c79a81..0841dcb08e0 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -763,6 +763,7 @@ class CTransaction // Compute the fee from the explicit fee outputs. Must call HasValidFee first CAmountMap GetFee() const; + CAmount GetFee(const CAsset& asset) const; // Compute priority, given priority of inputs and (optionally) tx size double ComputePriority(double dPriorityInputs, unsigned int nTxSize=0) const; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6c3e43af2bc..e9b8ce393d6 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -25,6 +25,7 @@ #include +#include #include // boost::thread::interrupt using namespace std; @@ -527,6 +528,15 @@ UniValue getblockheader(const UniValue& params, bool fHelp) return blockheaderToJSON(pblockindex); } +static void ReadBlockCheckPruned(CBlockIndex* pblockindex, CBlock& block) +{ + if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Block not available (pruned data)"); + + if(!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); +} + UniValue getblock(const UniValue& params, bool fHelp) { if (fHelp || params.size() < 1 || params.size() > 2) @@ -582,12 +592,7 @@ UniValue getblock(const UniValue& params, bool fHelp) CBlock block; CBlockIndex* pblockindex = mapBlockIndex[hash]; - - if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) - throw JSONRPCError(RPC_INTERNAL_ERROR, "Block not available (pruned data)"); - - if(!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) - throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); + ReadBlockCheckPruned(pblockindex, block); if (!fVerbose) { @@ -1137,6 +1142,158 @@ UniValue reconsiderblock(const UniValue& params, bool fHelp) return NullUniValue; } +static void UpdateBlockStats(const CBlock& block, const CAsset& asset, std::vector& plot_values, std::map& map_stats) +{ + CAmount min = MAX_MONEY; + CAmount max = 0; + CAmount total = 0; + CFeeRate minfeerate = CFeeRate(MAX_MONEY, 1); + CFeeRate maxfeerate = CFeeRate(0); + int64_t total_weight = 0; + + for (const CTransaction& tx : block.vtx) { + if (tx.IsCoinBase()) { + continue; + } + + CAmount txfee = tx.GetFee(asset); + int64_t weight = GetTransactionWeight(tx); + total_weight += weight; + CFeeRate feerate = CFeeRate(txfee, weight); + for (const std::string& plot_value : plot_values) { + if (plot_value == "minfee") { + min = std::min(min, txfee); + } else if (plot_value == "maxfee") { + max = std::max(max, txfee); + } else if (plot_value == "totalfee") { + total += txfee; + } else if (plot_value == "minfeerate") { + minfeerate = std::min(minfeerate, feerate); + } else if (plot_value == "maxfeerate") { + maxfeerate = std::min(maxfeerate, feerate); + } + } + } + + for (const std::string& plot_value : plot_values) { + // Update map_stats + if (plot_value == "minfee") { + map_stats[plot_value].push_back((min == MAX_MONEY) ? 0 : min); + } else if (plot_value == "maxfee") { + map_stats[plot_value].push_back(max); + } else if (plot_value == "totalfee") { + map_stats[plot_value].push_back(total); + } else if (plot_value == "minfeerate") { + map_stats[plot_value].push_back((minfeerate == CFeeRate(MAX_MONEY, 1)) ? CFeeRate(0).GetFee(1) : minfeerate.GetFee(1)); + } else if (plot_value == "maxfeerate") { + map_stats[plot_value].push_back(maxfeerate.GetFee(1)); + } else if (plot_value == "avgfee") { + map_stats[plot_value].push_back((block.vtx.size() > 1) ? total / (block.vtx.size() - 1) : 0); + } else if (plot_value == "avgfeeperweightunit") { + map_stats[plot_value].push_back(CFeeRate(total, total_weight).GetFee(1)); + } else if (plot_value == "avgfeerate") { + map_stats[plot_value].push_back((block.vtx.size() > 1) ? CFeeRate(total, total_weight).GetFee(1) / (block.vtx.size() - 1) : 0); + } + } +} + +static bool IsAllowedPlotValue(const std::string& plot_value, std::vector& allowed_plot_values) +{ + for (const std::string& allowed_plot_value : allowed_plot_values) { + if (allowed_plot_value == plot_value) { + return true; + } + } + return false; +} + +UniValue getperblockstats(const UniValue& params, bool fHelp) +{ + std::string str_allowed_plot_values = "minfee,maxfee,totalfee,minfeerate,maxfeerate,avgfee,avgfeeperweightunit,avgfeerate"; + std::vector allowed_plot_values; + boost::split(allowed_plot_values, str_allowed_plot_values, boost::is_any_of(",")); + + if (fHelp || params.size() > 2) + throw std::runtime_error( + "getperblockstats ( nStart nEnd plotValues )\n" + "\nCompute per block statistics for a given window.\n" + "\nArguments:\n" + "1. \"nStart\" (numeric, required) The height of the block that starts the window.\n" + "2. \"nEnd\" (numeric, optional) The height of the block that ends the window (default: current tip).\n" + "3. \"plotValues\" (string, optional) Values to plot (comma separated), default(all): " + str_allowed_plot_values + + "4. \"asset\" (string, optional) Hex asset id or asset label for fees.\n" + "\nResult:\n" + "{\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("getperblockstats", "1000 1000 \"minfeerate,avgfeerate\"") + + HelpExampleRpc("getperblockstats", "1000 1000 \"maxfeerate,avgfeerate\"") + ); + + LOCK(cs_main); + + int start = params[0].get_int(); + if (start < 1 || start > chainActive.Height()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Start block height out of range"); + } + + int end; + if (params.size() > 1) { + end = params[1].get_int(); + } else { + end = chainActive.Height(); + } + if (end < 0 || end > chainActive.Height()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "End block height out of range"); + } + + std::string str_plot_values = str_allowed_plot_values; + if (params.size() > 2) { + str_plot_values = params[1].get_str(); + } + std::vector plot_values; + boost::split(plot_values, str_plot_values, boost::is_any_of(",")); + for (const std::string plot_value : plot_values) { + if (!IsAllowedPlotValue(plot_value, allowed_plot_values)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid plot value %s", plot_value)); + } + } + + // TODO use GetAssetFromString() to leverage gAssetsDir (ie support labels, rpc doc is lying) + CAsset asset = policyAsset; + std::string strasset = ""; + if (params.size() > 3) { + strasset = params[3].get_str(); + asset = CAsset(uint256S(strasset)); + } + + std::map map_stats; + for (const std::string& allowed_plot_value : allowed_plot_values) { + map_stats[allowed_plot_value] = UniValue(UniValue::VARR); + } + map_stats["height"] = UniValue(UniValue::VARR); + map_stats["time"] = UniValue(UniValue::VARR); + + CBlockIndex* pindex = chainActive[end]; + CBlock block; + for (int i = end; i >= start; i--) { + ReadBlockCheckPruned(pindex, block); + UpdateBlockStats(block, asset, plot_values, map_stats); + // For the X axis: + map_stats["height"].push_back(i); + map_stats["time"].push_back(block.GetBlockTime()); + pindex = pindex->pprev; + } + + UniValue ret(UniValue::VOBJ); + for (const std::string plot_value : plot_values) { + ret.push_back(Pair(plot_value, map_stats[plot_value])); + } + ret.push_back(Pair("height", map_stats["height"])); + ret.push_back(Pair("time", map_stats["time"])); + return ret; +} + static const CRPCCommand commands[] = { // category name actor (function) okSafeMode // --------------------- ------------------------ ----------------------- ---------- @@ -1156,6 +1313,7 @@ static const CRPCCommand commands[] = { "blockchain", "gettxout", &gettxout, true }, { "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true }, { "blockchain", "verifychain", &verifychain, true }, + { "blockchain", "getperblockstats", &getperblockstats, true }, /* Not shown in help */ { "hidden", "invalidateblock", &invalidateblock, true },