diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -6,4 +6,9 @@ This release includes the following features and fixes: - Users can start their node with the option `-coinstatsindex` which syncs an - index of coin statistics in the background. + index of coin statistics in the background. After the index is synced the user + can use `gettxoutsetinfo` with hash_type=none or hash_type=muhash and will get + the response instantly out of the index +- Users can specify a height or block hash when calling `gettxoutsetinfo` to + see coin statistics at a specific block height when they use the `-coinstatsindex` + option. diff --git a/src/node/coinstats.h b/src/node/coinstats.h --- a/src/node/coinstats.h +++ b/src/node/coinstats.h @@ -39,12 +39,15 @@ //! The number of coins contained. uint64_t coins_count{0}; + bool from_index{false}; + CCoinsStats(CoinStatsHashType hash_type) : m_hash_type(hash_type) {} }; //! Calculate statistics about the unspent transaction output set bool GetUTXOStats(CCoinsView *view, BlockManager &blockman, CCoinsStats &stats, - const std::function &interruption_point = {}); + const std::function &interruption_point = {}, + const CBlockIndex *pindex = nullptr); uint64_t GetBogoSize(const CScript &script_pub_key); diff --git a/src/node/coinstats.cpp b/src/node/coinstats.cpp --- a/src/node/coinstats.cpp +++ b/src/node/coinstats.cpp @@ -89,26 +89,26 @@ template static bool GetUTXOStats(CCoinsView *view, BlockManager &blockman, CCoinsStats &stats, T hash_obj, - const std::function &interruption_point) { + const std::function &interruption_point, + const CBlockIndex *pindex) { std::unique_ptr pcursor(view->Cursor()); assert(pcursor); - stats.hashBlock = pcursor->GetBestBlock(); - const CBlockIndex *pindex; - { + if (!pindex) { LOCK(cs_main); assert(std::addressof(g_chainman.m_blockman) == std::addressof(blockman)); - - pindex = blockman.LookupBlockIndex(stats.hashBlock); - stats.nHeight = Assert(pindex)->nHeight; + pindex = blockman.LookupBlockIndex(view->GetBestBlock()); } + stats.nHeight = Assert(pindex)->nHeight; + stats.hashBlock = pindex->GetBlockHash(); // Use CoinStatsIndex if it is available and a hash_type of Muhash or None // was requested if ((stats.m_hash_type == CoinStatsHashType::MUHASH || stats.m_hash_type == CoinStatsHashType::NONE) && g_coin_stats_index) { + stats.from_index = true; return g_coin_stats_index->LookUpStats(pindex, stats); } @@ -146,20 +146,22 @@ } bool GetUTXOStats(CCoinsView *view, BlockManager &blockman, CCoinsStats &stats, - const std::function &interruption_point) { + const std::function &interruption_point, + const CBlockIndex *pindex) { switch (stats.m_hash_type) { case (CoinStatsHashType::HASH_SERIALIZED): { CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION); - return GetUTXOStats(view, blockman, stats, ss, interruption_point); + return GetUTXOStats(view, blockman, stats, ss, interruption_point, + pindex); } case (CoinStatsHashType::MUHASH): { MuHash3072 muhash; return GetUTXOStats(view, blockman, stats, muhash, - interruption_point); + interruption_point, pindex); } case (CoinStatsHashType::NONE): { return GetUTXOStats(view, blockman, stats, nullptr, - interruption_point); + interruption_point, pindex); } } // no default case, so the compiler can warn about missing cases assert(false); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -115,6 +115,42 @@ return blockindex == tip ? 1 : -1; } +static CBlockIndex *ParseHashOrHeight(const UniValue ¶m, + ChainstateManager &chainman) { + LOCK(::cs_main); + CChain &active_chain = chainman.ActiveChain(); + + if (param.isNum()) { + const int height{param.get_int()}; + if (height < 0) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("Target block height %d is negative", height)); + } + const int current_tip{active_chain.Height()}; + if (height > current_tip) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + strprintf("Target block height %d after current tip %d", height, + current_tip)); + } + + return active_chain[height]; + } else { + const BlockHash hash{ParseHashV(param, "hash_or_height")}; + CBlockIndex *pindex = chainman.m_blockman.LookupBlockIndex(hash); + + if (!pindex) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + if (!active_chain.Contains(pindex)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Block is not in chain %s", + Params().NetworkIDString())); + } + return pindex; + } +} UniValue blockheaderToJSON(const CBlockIndex *tip, const CBlockIndex *blockindex) { // Serialize passed information without accessing chain state of the active @@ -1248,11 +1284,19 @@ return RPCHelpMan{ "gettxoutsetinfo", "Returns statistics about the unspent transaction output set.\n" - "Note this call may take some time.\n", + "Note this call may take some time if you are not using " + "coinstatsindex.\n", { {"hash_type", RPCArg::Type::STR, /* default */ "hash_serialized", "Which UTXO set hash should be calculated. Options: " "'hash_serialized' (the legacy algorithm), 'muhash', 'none'."}, + {"hash_or_height", + RPCArg::Type::NUM, + RPCArg::Optional::OMITTED, + "The block hash or height of the target height (only available " + "with coinstatsindex).", + "", + {"", "string or numeric"}}, }, RPCResult{RPCResult::Type::OBJ, "", @@ -1262,12 +1306,11 @@ "The current block height (index)"}, {RPCResult::Type::STR_HEX, "bestblock", "The hash of the block at the tip of the chain"}, - {RPCResult::Type::NUM, "transactions", - "The number of transactions with unspent outputs"}, {RPCResult::Type::NUM, "txouts", "The number of unspent transaction outputs"}, {RPCResult::Type::NUM, "bogosize", - "A meaningless metric for UTXO set size"}, + "Database-independent, meaningless metric indicating " + "the UTXO set size"}, {RPCResult::Type::STR_HEX, "hash_serialized", /* optional */ true, "The serialized hash (only present if 'hash_serialized' " @@ -1275,17 +1318,34 @@ {RPCResult::Type::STR_HEX, "muhash", /* optional */ true, "The serialized hash (only present if 'muhash' " "hash_type is chosen)"}, + {RPCResult::Type::NUM, "transactions", + "The number of transactions with unspent outputs (not " + "available when coinstatsindex is used)"}, {RPCResult::Type::NUM, "disk_size", - "The estimated size of the chainstate on disk"}, + "The estimated size of the chainstate on disk (not " + "available when coinstatsindex is used)"}, {RPCResult::Type::STR_AMOUNT, "total_amount", "The total amount"}, }}, - RPCExamples{HelpExampleCli("gettxoutsetinfo", "") + - HelpExampleRpc("gettxoutsetinfo", "")}, + RPCExamples{ + HelpExampleCli("gettxoutsetinfo", "") + + HelpExampleCli("gettxoutsetinfo", R"("none")") + + HelpExampleCli("gettxoutsetinfo", R"("none" 1000)") + + HelpExampleCli( + "gettxoutsetinfo", + R"("none" '"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09"')") + + HelpExampleRpc("gettxoutsetinfo", "") + + HelpExampleRpc("gettxoutsetinfo", R"("none")") + + HelpExampleRpc("gettxoutsetinfo", R"("none", 1000)") + + HelpExampleRpc( + "gettxoutsetinfo", + R"("none", "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09")")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { UniValue ret(UniValue::VOBJ); + CBlockIndex *pindex{nullptr}; + const CoinStatsHashType hash_type{ request.params[0].isNull() ? CoinStatsHashType::HASH_SERIALIZED @@ -1305,11 +1365,20 @@ blockman = &active_chainstate.m_blockman; } + if (!request.params[1].isNull()) { + if (!g_coin_stats_index) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Querying specific block heights " + "requires coinstatsindex"); + } + + pindex = ParseHashOrHeight(request.params[1], chainman); + } + if (GetUTXOStats(coins_view, *blockman, stats, - node.rpc_interruption_point)) { + node.rpc_interruption_point, pindex)) { ret.pushKV("height", int64_t(stats.nHeight)); ret.pushKV("bestblock", stats.hashBlock.GetHex()); - ret.pushKV("transactions", int64_t(stats.nTransactions)); ret.pushKV("txouts", int64_t(stats.nTransactionOutputs)); ret.pushKV("bogosize", int64_t(stats.nBogoSize)); if (hash_type == CoinStatsHashType::HASH_SERIALIZED) { @@ -1319,7 +1388,11 @@ if (hash_type == CoinStatsHashType::MUHASH) { ret.pushKV("muhash", stats.hashSerialized.GetHex()); } - ret.pushKV("disk_size", stats.nDiskSize); + if (!stats.from_index) { + ret.pushKV("transactions", + static_cast(stats.nTransactions)); + ret.pushKV("disk_size", stats.nDiskSize); + } ret.pushKV("total_amount", stats.nTotalAmount); } else { if (g_coin_stats_index) { @@ -2405,41 +2478,7 @@ const JSONRPCRequest &request) -> UniValue { ChainstateManager &chainman = EnsureAnyChainman(request.context); LOCK(cs_main); - CChain &active_chain = chainman.ActiveChain(); - - CBlockIndex *pindex; - if (request.params[0].isNum()) { - const int height = request.params[0].get_int(); - const int current_tip = active_chain.Height(); - if (height < 0) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - strprintf("Target block height %d is negative", - height)); - } - if (height > current_tip) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - strprintf("Target block height %d after current tip %d", - height, current_tip)); - } - - pindex = active_chain[height]; - } else { - const BlockHash hash( - ParseHashV(request.params[0], "hash_or_height")); - pindex = chainman.m_blockman.LookupBlockIndex(hash); - if (!pindex) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, - "Block not found"); - } - if (!active_chain.Contains(pindex)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Block is not in chain %s", - Params().NetworkIDString())); - } - } - + CBlockIndex *pindex{ParseHashOrHeight(request.params[0], chainman)}; CHECK_NONFATAL(pindex != nullptr); std::set stats; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -111,6 +111,7 @@ {"gettxout", 1, "n"}, {"gettxout", 2, "include_mempool"}, {"gettxoutproof", 0, "txids"}, + {"gettxoutsetinfo", 1, "hash_or_height"}, {"lockunspent", 0, "unlock"}, {"lockunspent", 1, "transactions"}, {"send", 0, "outputs"}, diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py --- a/test/functional/feature_coinstatsindex.py +++ b/test/functional/feature_coinstatsindex.py @@ -10,7 +10,7 @@ """ from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, try_rpc +from test_framework.util import assert_equal, assert_raises_rpc_error, try_rpc class CoinStatsIndexTest(BitcoinTestFramework): @@ -52,8 +52,7 @@ "Unable to read UTXO set", node.gettxoutsetinfo)) res0 = node.gettxoutsetinfo('none') - # The fields 'disk_size' and 'transactions' do not work on the index, so - # don't check them. + # The fields 'disk_size' and 'transactions' do not exist on the index del res0['disk_size'], res0['transactions'] self.wait_until(lambda: not try_rpc(-32603, @@ -64,14 +63,35 @@ res1 = index_node.gettxoutsetinfo(hash_option) res1.pop('muhash', None) - # The fields 'disk_size' and 'transactions' do not work on the index - # so don't check them (they will be removed from the index in the - # next commit). - del res1['disk_size'], res1['transactions'] - # Everything left should be the same assert_equal(res1, res0) + self.log.info( + "Test that gettxoutsetinfo() can get fetch data on specific " + "heights with index") + + # Generate a new tip + node.generate(5) + + self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", + index_node.gettxoutsetinfo, + 'muhash')) + for hash_option in index_hash_options: + # Fetch old stats by height + res2 = index_node.gettxoutsetinfo(hash_option, 102) + res2.pop('muhash', None) + assert_equal(res0, res2) + + # Fetch old stats by hash + res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock']) + res3.pop('muhash', None) + assert_equal(res0, res3) + + # It does not work without coinstatsindex + assert_raises_rpc_error( + -8, "Querying specific block heights requires coinstatsindex", + node.gettxoutsetinfo, hash_option, 102) + if __name__ == '__main__': CoinStatsIndexTest().main()