diff --git a/contrib/debian/examples/bitcoin.conf b/contrib/debian/examples/bitcoin.conf --- a/contrib/debian/examples/bitcoin.conf +++ b/contrib/debian/examples/bitcoin.conf @@ -127,8 +127,11 @@ # be validated sooner. #paytxfee=0.00 +# Maintain coinstats index used by the gettxoutsetinfo RPC (default: 0). +#coinstatsindex=1 + # Enable pruning to reduce storage requirements by deleting old blocks. -# This mode is incompatible with -txindex and -rescan. +# This mode is incompatible with -txindex, -coinstatsindex and -rescan. # 0 = default (no pruning). # 1 = allows manual pruning via RPC. # >=550 = target to stay under in MiB. diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -5,3 +5,5 @@ 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. diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -175,6 +176,9 @@ g_txindex->Interrupt(); } ForEachBlockFilterIndex([](BlockFilterIndex &index) { index.Interrupt(); }); + if (g_coin_stats_index) { + g_coin_stats_index->Interrupt(); + } } void Shutdown(NodeContext &node) { @@ -269,6 +273,10 @@ g_txindex->Stop(); g_txindex.reset(); } + if (g_coin_stats_index) { + g_coin_stats_index->Stop(); + g_coin_stats_index.reset(); + } ForEachBlockFilterIndex([](BlockFilterIndex &index) { index.Stop(); }); DestroyAllBlockFilterIndexes(); @@ -467,6 +475,11 @@ " not affected. (default: %u)", DEFAULT_BLOCKSONLY), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-coinstatsindex", + strprintf("Maintain coinstats index used by the " + "gettxoutsetinfo RPC (default: %u)", + DEFAULT_COINSTATSINDEX), + ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg( "-conf=", strprintf("Specify path to read-only configuration file. Relative " @@ -567,11 +580,12 @@ "of old blocks. This allows the pruneblockchain RPC to be " "called to delete specific blocks, and enables automatic " "pruning of old blocks if a target size in MiB is provided. " - "This mode is incompatible with -txindex and -rescan. " - "Warning: Reverting this setting requires re-downloading the " - "entire blockchain. (default: 0 = disable pruning blocks, 1 " - "= allow manual pruning via RPC, >=%u = automatically prune " - "block files to stay under the specified target size in MiB)", + "This mode is incompatible with -txindex, -coinstatsindex " + "and -rescan. Warning: Reverting this setting requires " + "re-downloading the entire blockchain. (default: 0 = disable " + "pruning blocks, 1 = allow manual pruning via RPC, >=%u = " + "automatically prune block files to stay under the specified " + "target size in MiB)", MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg( @@ -1846,11 +1860,15 @@ nLocalServices = ServiceFlags(nLocalServices | NODE_COMPACT_FILTERS); } - // if using block pruning, then disallow txindex + // if using block pruning, then disallow txindex and coinstatsindex if (args.GetArg("-prune", 0)) { if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) { return InitError(_("Prune mode is incompatible with -txindex.")); } + if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) { + return InitError( + _("Prune mode is incompatible with -coinstatsindex.")); + } } // -bind and -whitebind can't be set when not listening @@ -2855,6 +2873,11 @@ GetBlockFilterIndex(filter_type)->Start(); } + if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) { + g_coin_stats_index = std::make_unique( + /* cache size */ 0, false, fReindex); + g_coin_stats_index->Start(); + } // Step 9: load wallet for (const auto &client : node.chain_clients) { if (!client->load()) { diff --git a/src/node/coinstats.cpp b/src/node/coinstats.cpp --- a/src/node/coinstats.cpp +++ b/src/node/coinstats.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -91,15 +92,24 @@ const std::function &interruption_point) { std::unique_ptr pcursor(view->Cursor()); assert(pcursor); - stats.hashBlock = pcursor->GetBestBlock(); + + const CBlockIndex *pindex; { LOCK(cs_main); assert(std::addressof(g_chainman.m_blockman) == std::addressof(blockman)); - const CBlockIndex *block = blockman.LookupBlockIndex(stats.hashBlock); - stats.nHeight = Assert(block)->nHeight; + pindex = blockman.LookupBlockIndex(stats.hashBlock); + stats.nHeight = Assert(pindex)->nHeight; + } + + // 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) { + return g_coin_stats_index->LookUpStats(pindex, stats); } PrepareHash(hash_obj, stats); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1321,6 +1322,19 @@ ret.pushKV("disk_size", stats.nDiskSize); ret.pushKV("total_amount", stats.nTotalAmount); } else { + if (g_coin_stats_index) { + const IndexSummary summary{ + g_coin_stats_index->GetSummary()}; + + if (!summary.synced) { + throw JSONRPCError( + RPC_INTERNAL_ERROR, + strprintf("Unable to read UTXO set because " + "coinstatsindex is still syncing. " + "Current height: %d", + summary.best_block_height)); + } + } throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set"); } diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -89,6 +89,7 @@ static const int64_t DEFAULT_MAX_TIP_AGE = 24 * 60 * 60; static const bool DEFAULT_CHECKPOINTS_ENABLED = true; static const bool DEFAULT_TXINDEX = false; +static constexpr bool DEFAULT_COINSTATSINDEX{false}; static const char *const DEFAULT_BLOCKFILTERINDEX = "0"; /** Default for -persistmempool */ diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py new file mode 100755 --- /dev/null +++ b/test/functional/feature_coinstatsindex.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 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 coinstatsindex across nodes. + +Test that the values returned by gettxoutsetinfo are consistent +between a node running the coinstatsindex and a node without +the index. +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, try_rpc + + +class CoinStatsIndexTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + self.supports_cli = False + self.extra_args = [ + [], + ["-coinstatsindex"] + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self._test_coin_stats_index() + + def _test_coin_stats_index(self): + node = self.nodes[0] + index_node = self.nodes[1] + # Both none and muhash options allow the usage of the index + index_hash_options = ['none', 'muhash'] + + # Generate a normal transaction and mine it + node.generate(101) + address = self.nodes[0].get_deterministic_priv_key().address + node.sendtoaddress( + address=address, + amount=10_000_000, + subtractfeefromamount=True) + node.generate(1) + + self.sync_blocks(timeout=120) + + self.log.info( + "Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option") + self.wait_until(lambda: not try_rpc(-32603, + "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. + del res0['disk_size'], res0['transactions'] + + self.wait_until(lambda: not try_rpc(-32603, + "Unable to read UTXO set", + index_node.gettxoutsetinfo, + 'muhash')) + for hash_option in index_hash_options: + 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) + + +if __name__ == '__main__': + CoinStatsIndexTest().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -794,8 +794,8 @@ # Remove empty wallets dir os.rmdir(cache_path('wallets')) for entry in os.listdir(cache_path()): - # Only keep chainstate and blocks folder - if entry not in ['chainstate', 'blocks']: + # Only keep indexes, chainstate and blocks folders + if entry not in ['chainstate', 'blocks', 'indexes']: os.remove(cache_path(entry)) for i in range(self.num_nodes): diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh --- a/test/lint/lint-circular-dependencies.sh +++ b/test/lint/lint-circular-dependencies.sh @@ -17,6 +17,7 @@ "node/blockstorage -> validation -> node/blockstorage" "index/blockfilterindex -> node/blockstorage -> validation -> index/blockfilterindex" "index/base -> validation -> index/blockfilterindex -> index/base" + "index/coinstatsindex -> node/coinstats -> index/coinstatsindex" "qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel" "qt/bitcoingui -> qt/walletframe -> qt/bitcoingui" "qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel"