diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1622,11 +1622,112 @@ return NullUniValue; } +UniValue getchaintxstats(const Config &config, const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() > 2) { + throw std::runtime_error( + "getchaintxstats ( nblocks blockhash )\n" + "\nCompute statistics about the total number and rate of " + "transactions in the chain.\n" + "\nArguments:\n" + "1. nblocks (numeric, optional) Size of the window in number " + "of blocks (default: one month).\n" + "2. \"blockhash\" (string, optional) The hash of the block that " + "ends the window.\n" + "\nResult:\n" + "{\n" + " \"time\": xxxxx, (numeric) The timestamp for the " + "final block in the window in UNIX format.\n" + " \"txcount\": xxxxx, (numeric) The total number of " + "transactions in the chain up to that point.\n" + " \"window_block_count\": xxxxx, (numeric) Size of the window in " + "number of blocks.\n" + " \"window_tx_count\": xxxxx, (numeric) The number of " + "transactions in the window. Only returned if " + "\"window_block_count\" is > 0.\n" + " \"window_interval\": xxxxx, (numeric) The elapsed time in " + "the window in seconds. Only returned if \"window_block_count\" is " + "> 0.\n" + " \"txrate\": x.xx, (numeric) The average rate of " + "transactions per second in the window. Only returned if " + "\"window_interval\" is > 0.\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("getchaintxstats", "") + + HelpExampleRpc("getchaintxstats", "2016")); + } + + const CBlockIndex *pindex; + + // By default: 1 month + int blockcount = + 30 * 24 * 60 * 60 / Params().GetConsensus().nPowTargetSpacing; + + bool havehash = !request.params[1].isNull(); + uint256 hash; + if (havehash) { + hash = uint256S(request.params[1].get_str()); + } + + { + LOCK(cs_main); + if (havehash) { + auto it = mapBlockIndex.find(hash); + if (it == mapBlockIndex.end()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + "Block not found"); + } + pindex = it->second; + if (!chainActive.Contains(pindex)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Block is not in main chain"); + } + } else { + pindex = chainActive.Tip(); + } + } + + assert(pindex != nullptr); + + if (request.params[0].isNull()) { + blockcount = std::max(0, std::min(blockcount, pindex->nHeight - 1)); + } else { + blockcount = request.params[0].get_int(); + + if (blockcount < 0 || + (blockcount > 0 && blockcount >= pindex->nHeight)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid block count: " + "should be between 0 and " + "the block's height - 1"); + } + } + + const CBlockIndex *pindexPast = + pindex->GetAncestor(pindex->nHeight - blockcount); + int nTimeDiff = + pindex->GetMedianTimePast() - pindexPast->GetMedianTimePast(); + int nTxDiff = pindex->nChainTx - pindexPast->nChainTx; + + UniValue ret(UniValue::VOBJ); + ret.push_back(Pair("time", (int64_t)pindex->nTime)); + ret.push_back(Pair("txcount", (int64_t)pindex->nChainTx)); + ret.push_back(Pair("window_block_count", blockcount)); + if (blockcount > 0) { + ret.push_back(Pair("window_tx_count", nTxDiff)); + ret.push_back(Pair("window_interval", nTimeDiff)); + if (nTimeDiff > 0) { + ret.push_back(Pair("txrate", ((double)nTxDiff) / nTimeDiff)); + } + } + + return ret; +} + // clang-format off static const CRPCCommand commands[] = { // category name actor (function) okSafe argNames // ------------------- ------------------------ ---------------------- ------ ---------- { "blockchain", "getblockchaininfo", getblockchaininfo, true, {} }, + { "blockchain", "getchaintxstats", &getchaintxstats, true, {"nblocks", "blockhash"} }, { "blockchain", "getbestblockhash", getbestblockhash, true, {} }, { "blockchain", "getblockcount", getblockcount, true, {} }, { "blockchain", "getblock", getblock, true, {"blockhash","verbose"} }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -77,6 +77,7 @@ {"listunspent", 2, "addresses"}, {"getblock", 1, "verbose"}, {"getblockheader", 1, "verbose"}, + {"getchaintxstats", 0, "nblocks"}, {"gettransaction", 1, "include_watchonly"}, {"getrawtransaction", 1, "verbose"}, {"createrawtransaction", 0, "inputs"}, diff --git a/test/functional/blockchain.py b/test/functional/blockchain.py --- a/test/functional/blockchain.py +++ b/test/functional/blockchain.py @@ -11,6 +11,7 @@ - getbestblockhash - getblockhash - getblockheader + - getchaintxstats - getnetworkhashps - verifychain @@ -28,8 +29,6 @@ assert_raises_jsonrpc, assert_is_hex_string, assert_is_hash_string, - start_node, - bitcoind_processes, BITCOIND_PROC_WAIT_TIMEOUT, ) @@ -42,6 +41,7 @@ self.extra_args = [['-stopatheight=207']] def run_test(self): + self._test_getchaintxstats() self._test_gettxoutsetinfo() self._test_getblockheader() self._test_getdifficulty() @@ -49,6 +49,38 @@ self._test_stopatheight() assert self.nodes[0].verifychain(4, 0) + def _test_getchaintxstats(self): + chaintxstats = self.nodes[0].getchaintxstats(1) + # 200 txs plus genesis tx + assert_equal(chaintxstats['txcount'], 201) + # tx rate should be 1 per 10 minutes, or 1/600 + # we have to round because of binary math + assert_equal(round(chaintxstats['txrate'] * 600, 10), Decimal(1)) + + b1 = self.nodes[0].getblock(self.nodes[0].getblockhash(1)) + b200 = self.nodes[0].getblock(self.nodes[0].getblockhash(200)) + time_diff = b200['mediantime'] - b1['mediantime'] + + chaintxstats = self.nodes[0].getchaintxstats() + assert_equal(chaintxstats['time'], b200['time']) + assert_equal(chaintxstats['txcount'], 201) + assert_equal(chaintxstats['window_block_count'], 199) + assert_equal(chaintxstats['window_tx_count'], 199) + assert_equal(chaintxstats['window_interval'], time_diff) + assert_equal( + round(chaintxstats['txrate'] * time_diff, 10), Decimal(199)) + + chaintxstats = self.nodes[0].getchaintxstats(blockhash=b1['hash']) + assert_equal(chaintxstats['time'], b1['time']) + assert_equal(chaintxstats['txcount'], 2) + assert_equal(chaintxstats['window_block_count'], 0) + assert('window_tx_count' not in chaintxstats) + assert('window_interval' not in chaintxstats) + assert('txrate' not in chaintxstats) + + assert_raises_jsonrpc(-8, "Invalid block count: should be between 0 and the block's height - 1", + self.nodes[0].getchaintxstats, 201) + def _test_gettxoutsetinfo(self): node = self.nodes[0] res = node.gettxoutsetinfo()