From fc389a44eb7bf05414fe784bbe4c59ccd0c9b347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Tim=C3=B3n?= Date: Sat, 8 Jul 2017 02:15:36 +0200 Subject: [PATCH 1/4] Avoid special case for truncated zeros with new CFeeRate::GetTruncatedFee --- src/policy/feerate.cpp | 14 ++++++++------ src/policy/feerate.h | 7 ++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/policy/feerate.cpp b/src/policy/feerate.cpp index a089c02284f..c8b4f54e194 100644 --- a/src/policy/feerate.cpp +++ b/src/policy/feerate.cpp @@ -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::max())); - int64_t nSize = int64_t(nBytes_); + assert(bytes <= uint64_t(std::numeric_limits::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; } diff --git a/src/policy/feerate.h b/src/policy/feerate.h index e82268b095a..b7cda91206b 100644 --- a/src/policy/feerate.h +++ b/src/policy/feerate.h @@ -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 */ From 95128f84ec8002050ca3a8894a6feea0b4aca637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Tim=C3=B3n?= Date: Sun, 4 Jun 2017 17:14:00 +0200 Subject: [PATCH 2/4] RPC: Separate ReadBlockCheckPruned() from getblock() --- src/rpc/blockchain.cpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c17ca2fa3a1..bd256481eed 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -685,6 +685,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 +769,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) { From 7158154065dc1c6bc91be1425da90d8e90377716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Tim=C3=B3n?= Date: Sun, 4 Jun 2017 00:25:55 +0200 Subject: [PATCH 3/4] RPC: Introduce getblockstats --- src/rpc/blockchain.cpp | 289 +++++++++++++++++++++++++++++++++++++++++++++++++ src/rpc/client.cpp | 2 + 2 files changed, 291 insertions(+) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index bd256481eed..fd4f0fdb775 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -29,6 +29,7 @@ #include +#include #include // boost::thread::interrupt #include @@ -1538,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 +static T CalculateTruncatedMedian(std::vector& 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& stats, std::map& 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 fee_array; + std::vector feerate_old_array; + std::vector 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 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 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 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 // --------------------- ------------------------ ----------------------- ------ ---------- @@ -1561,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"} }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index a3ea5390eec..00f1860f0f6 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -104,6 +104,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "importmulti", 1, "options" }, { "verifychain", 0, "checklevel" }, { "verifychain", 1, "nblocks" }, + { "getblockstats", 0, "start" }, + { "getblockstats", 1, "end" }, { "pruneblockchain", 0, "height" }, { "keypoolrefill", 0, "newsize" }, { "getrawmempool", 0, "verbose" }, From 90c72644627ad58ed11d9027d7f5a25e0190d818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Tim=C3=B3n?= Date: Wed, 21 Jun 2017 03:08:05 +0200 Subject: [PATCH 4/4] QA: Test new getblockstats RPC --- test/functional/getblockstats.py | 199 +++++++++++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 200 insertions(+) create mode 100755 test/functional/getblockstats.py diff --git a/test/functional/getblockstats.py b/test/functional/getblockstats.py new file mode 100755 index 00000000000..fa46e05a77f --- /dev/null +++ b/test/functional/getblockstats.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# Test getblockstats rpc call +# +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_jsonrpc, + connect_nodes_bi, +) + +class GetblockstatsTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.is_network_split = False + self.num_nodes = 2 + self.extra_args = [ + ['-debug', '-txindex', '-whitelist=127.0.0.1'], + ['-debug', '-txindex', '-whitelist=127.0.0.1', '-paytxfee=0.003'], + ] + + def setup_network(self): + self.nodes = self.start_nodes(self.num_nodes, self.options.tmpdir, self.extra_args) + connect_nodes_bi(self.nodes, 0 , 1) + self.sync_all() + + def assert_contains(self, data, values, check_cointains=True): + for val in values: + if (check_cointains): + assert(val in data) + else: + assert(val not in data) + + def run_test(self): + node = self.nodes[0] + node.generate(101) + + node.sendtoaddress(address=self.nodes[1].getnewaddress(), amount=10, subtractfeefromamount=True) + node.generate(1) + self.sync_all() + + node.sendtoaddress(address=node.getnewaddress(), amount=10, subtractfeefromamount=True) + node.sendtoaddress(address=node.getnewaddress(), amount=10, subtractfeefromamount=True) + self.nodes[1].sendtoaddress(address=node.getnewaddress(), amount=1, subtractfeefromamount=True) + self.sync_all() + node.generate(1) + + start_height = 101 + max_stat_pos = 2 + stats = node.getblockstats(start=start_height, end=start_height + max_stat_pos) + + all_values = [ + "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", + ] + self.assert_contains(stats, all_values) + # Make sure all valid statistics are included + self.assert_contains(all_values, stats.keys()) + + # The order of the data is inverted for every value + assert_equal(stats['height'][max_stat_pos], start_height) + assert_equal(stats['height'][0], start_height + max_stat_pos) + + assert_equal(stats['txs'][max_stat_pos], 1) + assert_equal(stats['ins'][max_stat_pos], 0) + assert_equal(stats['outs'][max_stat_pos], 2) + assert_equal(stats['totalfee'][max_stat_pos], 0) + assert_equal(stats['utxo_increase'][max_stat_pos], 2) + assert_equal(stats['utxo_size_inc'][max_stat_pos], 173) + assert_equal(stats['total_size'][max_stat_pos], 0) + assert_equal(stats['total_weight'][max_stat_pos], 0) + assert_equal(stats['total_out'][max_stat_pos], 0) + assert_equal(stats['minfee'][max_stat_pos], 0) + assert_equal(stats['maxfee'][max_stat_pos], 0) + assert_equal(stats['medianfee'][max_stat_pos], 0) + assert_equal(stats['avgfee'][max_stat_pos], 0) + assert_equal(stats['minfeerate'][max_stat_pos], 0) + assert_equal(stats['maxfeerate'][max_stat_pos], 0) + assert_equal(stats['medianfeerate'][max_stat_pos], 0) + assert_equal(stats['avgfeerate'][max_stat_pos], 0) + assert_equal(stats['minfeerate_old'][max_stat_pos], 0) + assert_equal(stats['maxfeerate_old'][max_stat_pos], 0) + assert_equal(stats['medianfeerate_old'][max_stat_pos], 0) + assert_equal(stats['avgfeerate_old'][max_stat_pos], 0) + + assert_equal(stats['txs'][1], 2) + assert_equal(stats['ins'][1], 1) + assert_equal(stats['outs'][1], 4) + assert_equal(stats['totalfee'][1], 3840) + assert_equal(stats['utxo_increase'][1], 3) + assert_equal(stats['utxo_size_inc'][1], 238) + # assert_equal(stats['total_size'][1], 191) + # assert_equal(stats['total_weight'][1], 768) + assert_equal(stats['total_out'][1], 4999996160) + assert_equal(stats['minfee'][1], 3840) + assert_equal(stats['maxfee'][1], 3840) + assert_equal(stats['medianfee'][1], 3840) + assert_equal(stats['avgfee'][1], 3840) + assert_equal(stats['minfeerate'][1], 20) + assert_equal(stats['maxfeerate'][1], 20) + assert_equal(stats['medianfeerate'][1], 20) + assert_equal(stats['avgfeerate'][1], 20) + assert_equal(stats['minfeerate_old'][1], 20) + assert_equal(stats['maxfeerate_old'][1], 20) + assert_equal(stats['medianfeerate_old'][1], 20) + assert_equal(stats['avgfeerate_old'][1], 20) + + assert_equal(stats['txs'][0], 4) + assert_equal(stats['ins'][0], 3) + assert_equal(stats['outs'][0], 8) + assert_equal(stats['totalfee'][0], 76160) + assert_equal(stats['utxo_increase'][0], 5) + assert_equal(stats['utxo_size_inc'][0], 388) + # assert_equal(stats['total_size'][0], 643) + # assert_equal(stats['total_weight'][0], 2572) + assert_equal(stats['total_out'][0], 9999920000) + assert_equal(stats['minfee'][0], 3840) + assert_equal(stats['maxfee'][0], 67800) + assert_equal(stats['medianfee'][0], 4520) + assert_equal(stats['avgfee'][0], 25386) + assert_equal(stats['minfeerate'][0], 20) + # assert_equal(stats['maxfeerate'][0], 300) + assert_equal(stats['medianfeerate'][0], 20) + assert_equal(stats['avgfeerate'][0], 118) + assert_equal(stats['minfeerate_old'][0], 20) + # assert_equal(stats['maxfeerate_old'][0], 301) + assert_equal(stats['medianfeerate_old'][0], 20) + assert_equal(stats['avgfeerate_old'][0], 118) + + # Testing reward is redundant, can be calculated in the plotter client + subsidy = 5000000000 + assert_equal(stats['reward'][0], subsidy + stats['totalfee'][0]) + assert_equal(stats['reward'][1], subsidy + stats['totalfee'][1]) + assert_equal(stats['reward'][max_stat_pos], subsidy + stats['totalfee'][max_stat_pos]) + + # Test invalid parameters raise the proper json exceptions + tip = start_height + max_stat_pos + assert_raises_jsonrpc(-8, 'Start block height %d after current tip %d' % (tip+1, tip), node.getblockstats, start=tip+1) + assert_raises_jsonrpc(-8, 'Start block height %d after current tip %d' % (-1, tip), node.getblockstats, start=-tip-1) + assert_raises_jsonrpc(-8, 'Start block height %d higher than end %d' % (tip-1, tip-2), node.getblockstats, start=-1, end=-2) + assert_raises_jsonrpc(-8, 'End block height %d after current tip %d' % (tip+1, tip), node.getblockstats, start=1, end=tip+1) + assert_raises_jsonrpc(-8, 'Start block height 2 higher than end 1', node.getblockstats, start=2, end=1) + assert_raises_jsonrpc(-8, 'Start block height %d higher than end %d' % (tip, tip-1), node.getblockstats, start=tip, end=tip-1) + + # Make sure not valid stats aren't allowed + inv_sel_stat = 'asdfghjkl' + inv_stats = [ + 'minfee,%s' % inv_sel_stat, + '%s,minfee' % inv_sel_stat, + 'minfee,%s,maxfee' % inv_sel_stat, + ] + for inv_stat in inv_stats: + assert_raises_jsonrpc(-8, 'Invalid selected statistic %s' % inv_sel_stat, node.getblockstats, start=1, end=2, stats=inv_stat) + # Make sure we aren't always returning inv_sel_stat as the culprit stat + assert_raises_jsonrpc(-8, 'Invalid selected statistic aaa%s' % inv_sel_stat, node.getblockstats, start=1, end=2, stats='minfee,aaa%s' % inv_sel_stat) + + # Make sure only the selected statistics are included + stats = node.getblockstats(start=1, end=2, stats='minfee,maxfee') + some_values = [ + 'minfee', + 'maxfee', + ] + self.assert_contains(stats, some_values) + # Make sure valid stats that haven't been selected don't appear + other_values = [x for x in all_values if x not in some_values] + self.assert_contains(stats, other_values, False) + +if __name__ == '__main__': + GetblockstatsTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 54f625514bd..4ff7a0d4ed4 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -114,6 +114,7 @@ 'p2p-leaktests.py', 'wallet-encryption.py', 'uptime.py', + 'getblockstats.py', ] EXTENDED_SCRIPTS = [