aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorW. J. van der Laan <laanwj@protonmail.com>2021-04-30 08:31:41 +0200
committerW. J. van der Laan <laanwj@protonmail.com>2021-04-30 17:27:19 +0200
commit2b45cf0bcdb3d2c1de46899e30885c953b57b475 (patch)
treed6accaa1e8940738517e3c441ddad72865e4c425
parent480bf01c295527bd212964efe4df3bb886db5654 (diff)
parent5f96d7d22d8e05876c6fc014e70488699950fe38 (diff)
downloadbitcoin-2b45cf0bcdb3d2c1de46899e30885c953b57b475.tar.xz
Merge bitcoin/bitcoin#19521: Coinstats Index
5f96d7d22d8e05876c6fc014e70488699950fe38 rpc: gettxoutsetinfo rejects hash_serialized_2 for specific height (Fabian Jahr) 23fe50436be641d7417152adc683192649ba206a test: Add test for coinstatsindex behavior in reorgs (Fabian Jahr) 90c966b0f3cfbd6bce5883f46d8527c6853a86a2 rpc: Allow gettxoutsetinfo and getblockstats for stale blocks (Fabian Jahr) b9362392aef2689bc106c20925859ede555d082b index, rpc: Add use_index option for gettxoutsetinfo (Fabian Jahr) bb7788b121a30489bc81a1f46dde6a9b19ae4ec1 test: Test coinstatsindex robustness across restarts (Fabian Jahr) e0938c29099635150014ffc9bb0cafa8049ec55a test: Add tests for block_info in gettxoutsetinfo (Fabian Jahr) 2501576eccb08af80471c7b7b843b189ad6758c0 rpc, index: Add verbose amounts tracking to Coinstats index (Fabian Jahr) 655d929836a71af23d2035d2e2e99ad8b8c340c3 test: add coinstatsindex getindexinfo coverage, improve current tests (Jon Atack) ca01bb8d689f93e1c7669b0ba7a4994c0206dabd rpc: Add Coinstats index to getindexinfo (Fabian Jahr) 57a026c30fef3138bb8db46e6865acb9dc2674f8 test: Add unit test for Coinstats index (Fabian Jahr) 6a4c0c09ab4d073a26c3c4a02783d5dcd88f6eef test: Add functional test for Coinstats index (Fabian Jahr) 3f166ecc125fce6ccd995687fa16572090a5d099 rpc: gettxoutsetinfo can be requested for specific blockheights (Fabian Jahr) 3c914d58ff323255b32e717d0ce28209ec0abdaa index: Coinstats index can be activated with command line flag (Fabian Jahr) dd58a4de21469d6d848ae309edc47f558628221d index: Add Coinstats index (Fabian Jahr) a8a46c4b3cfda4b95c92a36f8cebd3606377e57d refactor: Simplify ApplyStats and ApplyHash (Fabian Jahr) 9c8a265fd21a87228c18a1661df99fedc1866baf refactor: Pass hash_type to CoinsStats in stats object (Fabian Jahr) 2e2648a9021dfbb6e17dfa81472f057dacbc34e0 crypto: Make MuHash Remove method efficient (Fabian Jahr) Pull request description: This is part of the coinstats index project tracked in #18000 While the review of the new UTXO set hash algorithm (MuHash) takes longer recently #19328 was merged which added the possibility to run `gettxoutsetinfo` with a specific hash type. As the first type it added `hash_type=none` which skips the hashing of the UTXO set altogether. This alone did not make `gettxoutsetinfo` much faster but it allows the use of an index for the remaining coin statistics even before a new hashing algorithm has been added. Credit to Sjors for the idea to take this intermediate step. Features summary: - Users can start their node with the option `-coinstatsindex` which syncs the index 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 - The user can specify a height or block hash when calling `gettxoutsetinfo` to see coin statistics at a specific block height ACKs for top commit: Sjors: re-tACK 5f96d7d22d8e05876c6fc014e70488699950fe38 jonatack: Code review re-ACK 5f96d7d22d8e05876c6fc014e70488699950fe38 per `git range-diff 13d27b4 07201d3 5f96d7d` promag: Tested ACK 5f96d7d22d8e05876c6fc014e70488699950fe38. Light code review ACK 5f96d7d22d8e05876c6fc014e70488699950fe38. Tree-SHA512: cbca78bee8e9605c19da4fbcd184625fb280200718396c694a56c7daab6f44ad23ca9fb5456d09f245d8b8d9659fdc2b3f3ce5e953c1c6cf4003dbc74c0463c2
-rw-r--r--src/Makefile.am2
-rw-r--r--src/Makefile.test.include1
-rw-r--r--src/crypto/muhash.cpp2
-rw-r--r--src/index/base.h2
-rw-r--r--src/index/coinstatsindex.cpp472
-rw-r--r--src/index/coinstatsindex.h61
-rw-r--r--src/init.cpp20
-rw-r--r--src/node/coinstats.cpp104
-rw-r--r--src/node/coinstats.h28
-rw-r--r--src/rpc/blockchain.cpp153
-rw-r--r--src/rpc/client.cpp2
-rw-r--r--src/rpc/misc.cpp5
-rw-r--r--src/test/coinstatsindex_tests.cpp79
-rw-r--r--src/test/fuzz/coins_view.cpp4
-rw-r--r--src/validation.cpp4
-rw-r--r--src/validation.h1
-rwxr-xr-xtest/functional/feature_coinstatsindex.py313
-rwxr-xr-xtest/functional/rpc_misc.py17
-rw-r--r--test/functional/test_framework/blocktools.py11
-rwxr-xr-xtest/functional/test_framework/test_framework.py2
-rwxr-xr-xtest/functional/test_runner.py1
-rwxr-xr-xtest/lint/lint-circular-dependencies.sh1
22 files changed, 1177 insertions, 108 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 447015fc66..972a3e279b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -153,6 +153,7 @@ BITCOIN_CORE_H = \
i2p.h \
index/base.h \
index/blockfilterindex.h \
+ index/coinstatsindex.h \
index/disktxpos.h \
index/txindex.h \
indirectmap.h \
@@ -326,6 +327,7 @@ libbitcoin_server_a_SOURCES = \
i2p.cpp \
index/base.cpp \
index/blockfilterindex.cpp \
+ index/coinstatsindex.cpp \
index/txindex.cpp \
init.cpp \
mapport.cpp \
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index efddc5e8c4..9360aa05f1 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -80,6 +80,7 @@ BITCOIN_TESTS =\
test/bswap_tests.cpp \
test/checkqueue_tests.cpp \
test/coins_tests.cpp \
+ test/coinstatsindex_tests.cpp \
test/compilerbug_tests.cpp \
test/compress_tests.cpp \
test/crypto_tests.cpp \
diff --git a/src/crypto/muhash.cpp b/src/crypto/muhash.cpp
index e5a0d4cb9c..a2b769cd56 100644
--- a/src/crypto/muhash.cpp
+++ b/src/crypto/muhash.cpp
@@ -341,6 +341,6 @@ MuHash3072& MuHash3072::Insert(Span<const unsigned char> in) noexcept {
}
MuHash3072& MuHash3072::Remove(Span<const unsigned char> in) noexcept {
- m_numerator.Divide(ToNum3072(in));
+ m_denominator.Multiply(ToNum3072(in));
return *this;
}
diff --git a/src/index/base.h b/src/index/base.h
index ac3c429a5a..d887620524 100644
--- a/src/index/base.h
+++ b/src/index/base.h
@@ -81,6 +81,8 @@ protected:
void ChainStateFlushed(const CBlockLocator& locator) override;
+ const CBlockIndex* CurrentIndex() { return m_best_block_index.load(); };
+
/// Initialize internal state from the database and block index.
virtual bool Init();
diff --git a/src/index/coinstatsindex.cpp b/src/index/coinstatsindex.cpp
new file mode 100644
index 0000000000..c7c1f4b533
--- /dev/null
+++ b/src/index/coinstatsindex.cpp
@@ -0,0 +1,472 @@
+// Copyright (c) 2020-2021 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <chainparams.h>
+#include <coins.h>
+#include <crypto/muhash.h>
+#include <index/coinstatsindex.h>
+#include <node/blockstorage.h>
+#include <serialize.h>
+#include <txdb.h>
+#include <undo.h>
+#include <validation.h>
+
+static constexpr char DB_BLOCK_HASH = 's';
+static constexpr char DB_BLOCK_HEIGHT = 't';
+static constexpr char DB_MUHASH = 'M';
+
+namespace {
+
+struct DBVal {
+ uint256 muhash;
+ uint64_t transaction_output_count;
+ uint64_t bogo_size;
+ CAmount total_amount;
+ CAmount total_subsidy;
+ CAmount block_unspendable_amount;
+ CAmount block_prevout_spent_amount;
+ CAmount block_new_outputs_ex_coinbase_amount;
+ CAmount block_coinbase_amount;
+ CAmount unspendables_genesis_block;
+ CAmount unspendables_bip30;
+ CAmount unspendables_scripts;
+ CAmount unspendables_unclaimed_rewards;
+
+ SERIALIZE_METHODS(DBVal, obj)
+ {
+ READWRITE(obj.muhash);
+ READWRITE(obj.transaction_output_count);
+ READWRITE(obj.bogo_size);
+ READWRITE(obj.total_amount);
+ READWRITE(obj.total_subsidy);
+ READWRITE(obj.block_unspendable_amount);
+ READWRITE(obj.block_prevout_spent_amount);
+ READWRITE(obj.block_new_outputs_ex_coinbase_amount);
+ READWRITE(obj.block_coinbase_amount);
+ READWRITE(obj.unspendables_genesis_block);
+ READWRITE(obj.unspendables_bip30);
+ READWRITE(obj.unspendables_scripts);
+ READWRITE(obj.unspendables_unclaimed_rewards);
+ }
+};
+
+struct DBHeightKey {
+ int height;
+
+ explicit DBHeightKey(int height_in) : height(height_in) {}
+
+ template <typename Stream>
+ void Serialize(Stream& s) const
+ {
+ ser_writedata8(s, DB_BLOCK_HEIGHT);
+ ser_writedata32be(s, height);
+ }
+
+ template <typename Stream>
+ void Unserialize(Stream& s)
+ {
+ char prefix{static_cast<char>(ser_readdata8(s))};
+ if (prefix != DB_BLOCK_HEIGHT) {
+ throw std::ios_base::failure("Invalid format for coinstatsindex DB height key");
+ }
+ height = ser_readdata32be(s);
+ }
+};
+
+struct DBHashKey {
+ uint256 block_hash;
+
+ explicit DBHashKey(const uint256& hash_in) : block_hash(hash_in) {}
+
+ SERIALIZE_METHODS(DBHashKey, obj)
+ {
+ char prefix{DB_BLOCK_HASH};
+ READWRITE(prefix);
+ if (prefix != DB_BLOCK_HASH) {
+ throw std::ios_base::failure("Invalid format for coinstatsindex DB hash key");
+ }
+
+ READWRITE(obj.block_hash);
+ }
+};
+
+}; // namespace
+
+std::unique_ptr<CoinStatsIndex> g_coin_stats_index;
+
+CoinStatsIndex::CoinStatsIndex(size_t n_cache_size, bool f_memory, bool f_wipe)
+{
+ fs::path path{GetDataDir() / "indexes" / "coinstats"};
+ fs::create_directories(path);
+
+ m_db = std::make_unique<CoinStatsIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe);
+}
+
+bool CoinStatsIndex::WriteBlock(const CBlock& block, const CBlockIndex* pindex)
+{
+ CBlockUndo block_undo;
+ const CAmount block_subsidy{GetBlockSubsidy(pindex->nHeight, Params().GetConsensus())};
+ m_total_subsidy += block_subsidy;
+
+ // Ignore genesis block
+ if (pindex->nHeight > 0) {
+ if (!UndoReadFromDisk(block_undo, pindex)) {
+ return false;
+ }
+
+ std::pair<uint256, DBVal> read_out;
+ if (!m_db->Read(DBHeightKey(pindex->nHeight - 1), read_out)) {
+ return false;
+ }
+
+ uint256 expected_block_hash{pindex->pprev->GetBlockHash()};
+ if (read_out.first != expected_block_hash) {
+ if (!m_db->Read(DBHashKey(expected_block_hash), read_out)) {
+ return error("%s: previous block header belongs to unexpected block %s; expected %s",
+ __func__, read_out.first.ToString(), expected_block_hash.ToString());
+ }
+ }
+
+ // TODO: Deduplicate BIP30 related code
+ bool is_bip30_block{(pindex->nHeight == 91722 && pindex->GetBlockHash() == uint256S("0x00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e")) ||
+ (pindex->nHeight == 91812 && pindex->GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f"))};
+
+ // Add the new utxos created from the block
+ for (size_t i = 0; i < block.vtx.size(); ++i) {
+ const auto& tx{block.vtx.at(i)};
+
+ // Skip duplicate txid coinbase transactions (BIP30).
+ if (is_bip30_block && tx->IsCoinBase()) {
+ m_block_unspendable_amount += block_subsidy;
+ m_unspendables_bip30 += block_subsidy;
+ continue;
+ }
+
+ for (size_t j = 0; j < tx->vout.size(); ++j) {
+ const CTxOut& out{tx->vout[j]};
+ Coin coin{out, pindex->nHeight, tx->IsCoinBase()};
+ COutPoint outpoint{tx->GetHash(), static_cast<uint32_t>(j)};
+
+ // Skip unspendable coins
+ if (coin.out.scriptPubKey.IsUnspendable()) {
+ m_block_unspendable_amount += coin.out.nValue;
+ m_unspendables_scripts += coin.out.nValue;
+ continue;
+ }
+
+ m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
+
+ if (tx->IsCoinBase()) {
+ m_block_coinbase_amount += coin.out.nValue;
+ } else {
+ m_block_new_outputs_ex_coinbase_amount += coin.out.nValue;
+ }
+
+ ++m_transaction_output_count;
+ m_total_amount += coin.out.nValue;
+ m_bogo_size += GetBogoSize(coin.out.scriptPubKey);
+ }
+
+ // The coinbase tx has no undo data since no former output is spent
+ if (!tx->IsCoinBase()) {
+ const auto& tx_undo{block_undo.vtxundo.at(i - 1)};
+
+ for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) {
+ Coin coin{tx_undo.vprevout[j]};
+ COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
+
+ m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
+
+ m_block_prevout_spent_amount += coin.out.nValue;
+
+ --m_transaction_output_count;
+ m_total_amount -= coin.out.nValue;
+ m_bogo_size -= GetBogoSize(coin.out.scriptPubKey);
+ }
+ }
+ }
+ } else {
+ // genesis block
+ m_block_unspendable_amount += block_subsidy;
+ m_unspendables_genesis_block += block_subsidy;
+ }
+
+ // If spent prevouts + block subsidy are still a higher amount than
+ // new outputs + coinbase + current unspendable amount this means
+ // the miner did not claim the full block reward. Unclaimed block
+ // rewards are also unspendable.
+ const CAmount unclaimed_rewards{(m_block_prevout_spent_amount + m_total_subsidy) - (m_block_new_outputs_ex_coinbase_amount + m_block_coinbase_amount + m_block_unspendable_amount)};
+ m_block_unspendable_amount += unclaimed_rewards;
+ m_unspendables_unclaimed_rewards += unclaimed_rewards;
+
+ std::pair<uint256, DBVal> value;
+ value.first = pindex->GetBlockHash();
+ value.second.transaction_output_count = m_transaction_output_count;
+ value.second.bogo_size = m_bogo_size;
+ value.second.total_amount = m_total_amount;
+ value.second.total_subsidy = m_total_subsidy;
+ value.second.block_unspendable_amount = m_block_unspendable_amount;
+ value.second.block_prevout_spent_amount = m_block_prevout_spent_amount;
+ value.second.block_new_outputs_ex_coinbase_amount = m_block_new_outputs_ex_coinbase_amount;
+ value.second.block_coinbase_amount = m_block_coinbase_amount;
+ value.second.unspendables_genesis_block = m_unspendables_genesis_block;
+ value.second.unspendables_bip30 = m_unspendables_bip30;
+ value.second.unspendables_scripts = m_unspendables_scripts;
+ value.second.unspendables_unclaimed_rewards = m_unspendables_unclaimed_rewards;
+
+ uint256 out;
+ m_muhash.Finalize(out);
+ value.second.muhash = out;
+
+ return m_db->Write(DBHeightKey(pindex->nHeight), value) && m_db->Write(DB_MUHASH, m_muhash);
+}
+
+static bool CopyHeightIndexToHashIndex(CDBIterator& db_it, CDBBatch& batch,
+ const std::string& index_name,
+ int start_height, int stop_height)
+{
+ DBHeightKey key{start_height};
+ db_it.Seek(key);
+
+ for (int height = start_height; height <= stop_height; ++height) {
+ if (!db_it.GetKey(key) || key.height != height) {
+ return error("%s: unexpected key in %s: expected (%c, %d)",
+ __func__, index_name, DB_BLOCK_HEIGHT, height);
+ }
+
+ std::pair<uint256, DBVal> value;
+ if (!db_it.GetValue(value)) {
+ return error("%s: unable to read value in %s at key (%c, %d)",
+ __func__, index_name, DB_BLOCK_HEIGHT, height);
+ }
+
+ batch.Write(DBHashKey(value.first), std::move(value.second));
+
+ db_it.Next();
+ }
+ return true;
+}
+
+bool CoinStatsIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
+{
+ assert(current_tip->GetAncestor(new_tip->nHeight) == new_tip);
+
+ CDBBatch batch(*m_db);
+ std::unique_ptr<CDBIterator> db_it(m_db->NewIterator());
+
+ // During a reorg, we need to copy all hash digests for blocks that are
+ // getting disconnected from the height index to the hash index so we can
+ // still find them when the height index entries are overwritten.
+ if (!CopyHeightIndexToHashIndex(*db_it, batch, m_name, new_tip->nHeight, current_tip->nHeight)) {
+ return false;
+ }
+
+ if (!m_db->WriteBatch(batch)) return false;
+
+ {
+ LOCK(cs_main);
+ CBlockIndex* iter_tip{g_chainman.m_blockman.LookupBlockIndex(current_tip->GetBlockHash())};
+ const auto& consensus_params{Params().GetConsensus()};
+
+ do {
+ CBlock block;
+
+ if (!ReadBlockFromDisk(block, iter_tip, consensus_params)) {
+ return error("%s: Failed to read block %s from disk",
+ __func__, iter_tip->GetBlockHash().ToString());
+ }
+
+ ReverseBlock(block, iter_tip);
+
+ iter_tip = iter_tip->GetAncestor(iter_tip->nHeight - 1);
+ } while (new_tip != iter_tip);
+ }
+
+ return BaseIndex::Rewind(current_tip, new_tip);
+}
+
+static bool LookUpOne(const CDBWrapper& db, const CBlockIndex* block_index, DBVal& result)
+{
+ // First check if the result is stored under the height index and the value
+ // there matches the block hash. This should be the case if the block is on
+ // the active chain.
+ std::pair<uint256, DBVal> read_out;
+ if (!db.Read(DBHeightKey(block_index->nHeight), read_out)) {
+ return false;
+ }
+ if (read_out.first == block_index->GetBlockHash()) {
+ result = std::move(read_out.second);
+ return true;
+ }
+
+ // If value at the height index corresponds to an different block, the
+ // result will be stored in the hash index.
+ return db.Read(DBHashKey(block_index->GetBlockHash()), result);
+}
+
+bool CoinStatsIndex::LookUpStats(const CBlockIndex* block_index, CCoinsStats& coins_stats) const
+{
+ DBVal entry;
+ if (!LookUpOne(*m_db, block_index, entry)) {
+ return false;
+ }
+
+ coins_stats.hashSerialized = entry.muhash;
+ coins_stats.nTransactionOutputs = entry.transaction_output_count;
+ coins_stats.nBogoSize = entry.bogo_size;
+ coins_stats.nTotalAmount = entry.total_amount;
+ coins_stats.total_subsidy = entry.total_subsidy;
+ coins_stats.block_unspendable_amount = entry.block_unspendable_amount;
+ coins_stats.block_prevout_spent_amount = entry.block_prevout_spent_amount;
+ coins_stats.block_new_outputs_ex_coinbase_amount = entry.block_new_outputs_ex_coinbase_amount;
+ coins_stats.block_coinbase_amount = entry.block_coinbase_amount;
+ coins_stats.unspendables_genesis_block = entry.unspendables_genesis_block;
+ coins_stats.unspendables_bip30 = entry.unspendables_bip30;
+ coins_stats.unspendables_scripts = entry.unspendables_scripts;
+ coins_stats.unspendables_unclaimed_rewards = entry.unspendables_unclaimed_rewards;
+
+ return true;
+}
+
+bool CoinStatsIndex::Init()
+{
+ if (!m_db->Read(DB_MUHASH, m_muhash)) {
+ // Check that the cause of the read failure is that the key does not
+ // exist. Any other errors indicate database corruption or a disk
+ // failure, and starting the index would cause further corruption.
+ if (m_db->Exists(DB_MUHASH)) {
+ return error("%s: Cannot read current %s state; index may be corrupted",
+ __func__, GetName());
+ }
+ }
+
+ if (BaseIndex::Init()) {
+ const CBlockIndex* pindex{CurrentIndex()};
+
+ if (pindex) {
+ DBVal entry;
+ if (!LookUpOne(*m_db, pindex, entry)) {
+ return false;
+ }
+
+ m_transaction_output_count = entry.transaction_output_count;
+ m_bogo_size = entry.bogo_size;
+ m_total_amount = entry.total_amount;
+ m_total_subsidy = entry.total_subsidy;
+ m_block_unspendable_amount = entry.block_unspendable_amount;
+ m_block_prevout_spent_amount = entry.block_prevout_spent_amount;
+ m_block_new_outputs_ex_coinbase_amount = entry.block_new_outputs_ex_coinbase_amount;
+ m_block_coinbase_amount = entry.block_coinbase_amount;
+ m_unspendables_genesis_block = entry.unspendables_genesis_block;
+ m_unspendables_bip30 = entry.unspendables_bip30;
+ m_unspendables_scripts = entry.unspendables_scripts;
+ m_unspendables_unclaimed_rewards = entry.unspendables_unclaimed_rewards;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+// Reverse a single block as part of a reorg
+bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex)
+{
+ CBlockUndo block_undo;
+ std::pair<uint256, DBVal> read_out;
+
+ const CAmount block_subsidy{GetBlockSubsidy(pindex->nHeight, Params().GetConsensus())};
+ m_total_subsidy -= block_subsidy;
+
+ // Ignore genesis block
+ if (pindex->nHeight > 0) {
+ if (!UndoReadFromDisk(block_undo, pindex)) {
+ return false;
+ }
+
+ if (!m_db->Read(DBHeightKey(pindex->nHeight - 1), read_out)) {
+ return false;
+ }
+
+ uint256 expected_block_hash{pindex->pprev->GetBlockHash()};
+ if (read_out.first != expected_block_hash) {
+ if (!m_db->Read(DBHashKey(expected_block_hash), read_out)) {
+ return error("%s: previous block header belongs to unexpected block %s; expected %s",
+ __func__, read_out.first.ToString(), expected_block_hash.ToString());
+ }
+ }
+ }
+
+ // Remove the new UTXOs that were created from the block
+ for (size_t i = 0; i < block.vtx.size(); ++i) {
+ const auto& tx{block.vtx.at(i)};
+
+ for (size_t j = 0; j < tx->vout.size(); ++j) {
+ const CTxOut& out{tx->vout[j]};
+ COutPoint outpoint{tx->GetHash(), static_cast<uint32_t>(j)};
+ Coin coin{out, pindex->nHeight, tx->IsCoinBase()};
+
+ // Skip unspendable coins
+ if (coin.out.scriptPubKey.IsUnspendable()) {
+ m_block_unspendable_amount -= coin.out.nValue;
+ m_unspendables_scripts -= coin.out.nValue;
+ continue;
+ }
+
+ m_muhash.Remove(MakeUCharSpan(TxOutSer(outpoint, coin)));
+
+ if (tx->IsCoinBase()) {
+ m_block_coinbase_amount -= coin.out.nValue;
+ } else {
+ m_block_new_outputs_ex_coinbase_amount -= coin.out.nValue;
+ }
+
+ --m_transaction_output_count;
+ m_total_amount -= coin.out.nValue;
+ m_bogo_size -= GetBogoSize(coin.out.scriptPubKey);
+ }
+
+ // The coinbase tx has no undo data since no former output is spent
+ if (!tx->IsCoinBase()) {
+ const auto& tx_undo{block_undo.vtxundo.at(i - 1)};
+
+ for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) {
+ Coin coin{tx_undo.vprevout[j]};
+ COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n};
+
+ m_muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
+
+ m_block_prevout_spent_amount -= coin.out.nValue;
+
+ m_transaction_output_count++;
+ m_total_amount += coin.out.nValue;
+ m_bogo_size += GetBogoSize(coin.out.scriptPubKey);
+ }
+ }
+ }
+
+ const CAmount unclaimed_rewards{(m_block_new_outputs_ex_coinbase_amount + m_block_coinbase_amount + m_block_unspendable_amount) - (m_block_prevout_spent_amount + m_total_subsidy)};
+ m_block_unspendable_amount -= unclaimed_rewards;
+ m_unspendables_unclaimed_rewards -= unclaimed_rewards;
+
+ // Check that the rolled back internal values are consistent with the DB read out
+ uint256 out;
+ m_muhash.Finalize(out);
+ Assert(read_out.second.muhash == out);
+
+ Assert(m_transaction_output_count == read_out.second.transaction_output_count);
+ Assert(m_total_amount == read_out.second.total_amount);
+ Assert(m_bogo_size == read_out.second.bogo_size);
+ Assert(m_total_subsidy == read_out.second.total_subsidy);
+ Assert(m_block_unspendable_amount == read_out.second.block_unspendable_amount);
+ Assert(m_block_prevout_spent_amount == read_out.second.block_prevout_spent_amount);
+ Assert(m_block_new_outputs_ex_coinbase_amount == read_out.second.block_new_outputs_ex_coinbase_amount);
+ Assert(m_block_coinbase_amount == read_out.second.block_coinbase_amount);
+ Assert(m_unspendables_genesis_block == read_out.second.unspendables_genesis_block);
+ Assert(m_unspendables_bip30 == read_out.second.unspendables_bip30);
+ Assert(m_unspendables_scripts == read_out.second.unspendables_scripts);
+ Assert(m_unspendables_unclaimed_rewards == read_out.second.unspendables_unclaimed_rewards);
+
+ return m_db->Write(DB_MUHASH, m_muhash);
+}
diff --git a/src/index/coinstatsindex.h b/src/index/coinstatsindex.h
new file mode 100644
index 0000000000..6149f9b4b3
--- /dev/null
+++ b/src/index/coinstatsindex.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2020-2021 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_INDEX_COINSTATSINDEX_H
+#define BITCOIN_INDEX_COINSTATSINDEX_H
+
+#include <chain.h>
+#include <crypto/muhash.h>
+#include <flatfile.h>
+#include <index/base.h>
+#include <node/coinstats.h>
+
+/**
+ * CoinStatsIndex maintains statistics on the UTXO set.
+ */
+class CoinStatsIndex final : public BaseIndex
+{
+private:
+ std::string m_name;
+ std::unique_ptr<BaseIndex::DB> m_db;
+
+ MuHash3072 m_muhash;
+ uint64_t m_transaction_output_count{0};
+ uint64_t m_bogo_size{0};
+ CAmount m_total_amount{0};
+ CAmount m_total_subsidy{0};
+ CAmount m_block_unspendable_amount{0};
+ CAmount m_block_prevout_spent_amount{0};
+ CAmount m_block_new_outputs_ex_coinbase_amount{0};
+ CAmount m_block_coinbase_amount{0};
+ CAmount m_unspendables_genesis_block{0};
+ CAmount m_unspendables_bip30{0};
+ CAmount m_unspendables_scripts{0};
+ CAmount m_unspendables_unclaimed_rewards{0};
+
+ bool ReverseBlock(const CBlock& block, const CBlockIndex* pindex);
+
+protected:
+ bool Init() override;
+
+ bool WriteBlock(const CBlock& block, const CBlockIndex* pindex) override;
+
+ bool Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip) override;
+
+ BaseIndex::DB& GetDB() const override { return *m_db; }
+
+ const char* GetName() const override { return "coinstatsindex"; }
+
+public:
+ // Constructs the index, which becomes available to be queried.
+ explicit CoinStatsIndex(size_t n_cache_size, bool f_memory = false, bool f_wipe = false);
+
+ // Look up stats for a specific block using CBlockIndex
+ bool LookUpStats(const CBlockIndex* block_index, CCoinsStats& coins_stats) const;
+};
+
+/// The global UTXO set hash object.
+extern std::unique_ptr<CoinStatsIndex> g_coin_stats_index;
+
+#endif // BITCOIN_INDEX_COINSTATSINDEX_H
diff --git a/src/init.cpp b/src/init.cpp
index 3f9838f2e0..b56c25c145 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -21,6 +21,7 @@
#include <httprpc.h>
#include <httpserver.h>
#include <index/blockfilterindex.h>
+#include <index/coinstatsindex.h>
#include <index/txindex.h>
#include <init/common.h>
#include <interfaces/chain.h>
@@ -165,6 +166,9 @@ void Interrupt(NodeContext& node)
g_txindex->Interrupt();
}
ForEachBlockFilterIndex([](BlockFilterIndex& index) { index.Interrupt(); });
+ if (g_coin_stats_index) {
+ g_coin_stats_index->Interrupt();
+ }
}
void Shutdown(NodeContext& node)
@@ -237,6 +241,10 @@ void Shutdown(NodeContext& node)
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();
@@ -376,6 +384,7 @@ void SetupServerArgs(NodeContext& node)
#endif
argsman.AddArg("-blockreconstructionextratxn=<n>", strprintf("Extra transactions to keep in memory for compact block reconstructions (default: %u)", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-blocksonly", strprintf("Whether to reject transactions from network peers. Automatic broadcast and rebroadcast of any transactions from inbound peers is disabled, unless the peer has the 'forcerelay' permission. RPC transactions are not affected. (default: %u)", DEFAULT_BLOCKSONLY), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
+ argsman.AddArg("-coinstatsindex", strprintf("Maintain coinstats index used by the gettxoutset RPC (default: %u)", DEFAULT_COINSTATSINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-conf=<file>", strprintf("Specify path to read-only configuration file. Relative paths will be prefixed by datadir location. (default: %s)", BITCOIN_CONF_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-datadir=<dir>", "Specify data directory", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
@@ -391,7 +400,7 @@ void SetupServerArgs(NodeContext& node)
-GetNumCores(), MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-persistmempool", strprintf("Whether to save the mempool on shutdown and load on restart (default: %u)", DEFAULT_PERSIST_MEMPOOL), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-pid=<file>", strprintf("Specify pid file. Relative paths will be prefixed by a net-specific datadir location. (default: %s)", BITCOIN_PID_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
- argsman.AddArg("-prune=<n>", strprintf("Reduce storage requirements by enabling pruning (deleting) 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. "
+ argsman.AddArg("-prune=<n>", strprintf("Reduce storage requirements by enabling pruning (deleting) 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, -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("-reindex", "Rebuild chain state and block index from the blk*.dat files on disk", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -876,10 +885,12 @@ bool AppInitParameterInteraction(const ArgsManager& args)
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
@@ -1587,6 +1598,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
GetBlockFilterIndex(filter_type)->Start();
}
+ if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) {
+ g_coin_stats_index = std::make_unique<CoinStatsIndex>(/* 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
index f8f0fff43f..38c1d29250 100644
--- a/src/node/coinstats.cpp
+++ b/src/node/coinstats.cpp
@@ -8,6 +8,7 @@
#include <coins.h>
#include <crypto/muhash.h>
#include <hash.h>
+#include <index/coinstatsindex.h>
#include <serialize.h>
#include <uint256.h>
#include <util/system.h>
@@ -16,44 +17,22 @@
#include <map>
// Database-independent metric indicating the UTXO set size
-static uint64_t GetBogoSize(const CScript& scriptPubKey)
+uint64_t GetBogoSize(const CScript& script_pub_key)
{
return 32 /* txid */ +
4 /* vout index */ +
4 /* height + coinbase */ +
8 /* amount */ +
2 /* scriptPubKey len */ +
- scriptPubKey.size() /* scriptPubKey */;
+ script_pub_key.size() /* scriptPubKey */;
}
-static void ApplyHash(CCoinsStats& stats, CHashWriter& ss, const uint256& hash, const std::map<uint32_t, Coin>& outputs, std::map<uint32_t, Coin>::const_iterator it)
-{
- if (it == outputs.begin()) {
- ss << hash;
- ss << VARINT(it->second.nHeight * 2 + it->second.fCoinBase ? 1u : 0u);
- }
-
- ss << VARINT(it->first + 1);
- ss << it->second.out.scriptPubKey;
- ss << VARINT_MODE(it->second.out.nValue, VarIntMode::NONNEGATIVE_SIGNED);
-
- if (it == std::prev(outputs.end())) {
- ss << VARINT(0u);
- }
-}
-
-static void ApplyHash(CCoinsStats& stats, std::nullptr_t, const uint256& hash, const std::map<uint32_t, Coin>& outputs, std::map<uint32_t, Coin>::const_iterator it) {}
-
-static void ApplyHash(CCoinsStats& stats, MuHash3072& muhash, const uint256& hash, const std::map<uint32_t, Coin>& outputs, std::map<uint32_t, Coin>::const_iterator it)
-{
- COutPoint outpoint = COutPoint(hash, it->first);
- Coin coin = it->second;
-
+CDataStream TxOutSer(const COutPoint& outpoint, const Coin& coin) {
CDataStream ss(SER_DISK, PROTOCOL_VERSION);
ss << outpoint;
ss << static_cast<uint32_t>(coin.nHeight * 2 + coin.fCoinBase);
ss << coin.out;
- muhash.Insert(MakeUCharSpan(ss));
+ return ss;
}
//! Warning: be very careful when changing this! assumeutxo and UTXO snapshot
@@ -68,14 +47,40 @@ static void ApplyHash(CCoinsStats& stats, MuHash3072& muhash, const uint256& has
//! It is also possible, though very unlikely, that a change in this
//! construction could cause a previously invalid (and potentially malicious)
//! UTXO snapshot to be considered valid.
-template <typename T>
-static void ApplyStats(CCoinsStats& stats, T& hash_obj, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
+static void ApplyHash(CHashWriter& ss, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
+{
+ for (auto it = outputs.begin(); it != outputs.end(); ++it) {
+ if (it == outputs.begin()) {
+ ss << hash;
+ ss << VARINT(it->second.nHeight * 2 + it->second.fCoinBase ? 1u : 0u);
+ }
+
+ ss << VARINT(it->first + 1);
+ ss << it->second.out.scriptPubKey;
+ ss << VARINT_MODE(it->second.out.nValue, VarIntMode::NONNEGATIVE_SIGNED);
+
+ if (it == std::prev(outputs.end())) {
+ ss << VARINT(0u);
+ }
+ }
+}
+
+static void ApplyHash(std::nullptr_t, const uint256& hash, const std::map<uint32_t, Coin>& outputs) {}
+
+static void ApplyHash(MuHash3072& muhash, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
+{
+ for (auto it = outputs.begin(); it != outputs.end(); ++it) {
+ COutPoint outpoint = COutPoint(hash, it->first);
+ Coin coin = it->second;
+ muhash.Insert(MakeUCharSpan(TxOutSer(outpoint, coin)));
+ }
+}
+
+static void ApplyStats(CCoinsStats& stats, const uint256& hash, const std::map<uint32_t, Coin>& outputs)
{
assert(!outputs.empty());
stats.nTransactions++;
for (auto it = outputs.begin(); it != outputs.end(); ++it) {
- ApplyHash(stats, hash_obj, hash, outputs, it);
-
stats.nTransactionOutputs++;
stats.nTotalAmount += it->second.out.nValue;
stats.nBogoSize += GetBogoSize(it->second.out.scriptPubKey);
@@ -84,18 +89,25 @@ static void ApplyStats(CCoinsStats& stats, T& hash_obj, const uint256& hash, con
//! Calculate statistics about the unspent transaction output set
template <typename T>
-static bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats& stats, T hash_obj, const std::function<void()>& interruption_point)
+static bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats& stats, T hash_obj, const std::function<void()>& interruption_point, const CBlockIndex* pindex)
{
- stats = CCoinsStats();
std::unique_ptr<CCoinsViewCursor> pcursor(view->Cursor());
assert(pcursor);
- stats.hashBlock = pcursor->GetBestBlock();
- {
- 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;
+ if (!pindex) {
+ {
+ LOCK(cs_main);
+ assert(std::addressof(g_chainman.m_blockman) == std::addressof(blockman));
+ pindex = blockman.LookupBlockIndex(view->GetBestBlock());
+ }
+ }
+ stats.nHeight = Assert(pindex)->nHeight;
+ stats.hashBlock = pindex->GetBlockHash();
+
+ // Use CoinStatsIndex if it is requested and 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.index_requested) {
+ stats.index_used = true;
+ return g_coin_stats_index->LookUpStats(pindex, stats);
}
PrepareHash(hash_obj, stats);
@@ -108,7 +120,8 @@ static bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats&
Coin coin;
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
if (!outputs.empty() && key.hash != prevkey) {
- ApplyStats(stats, hash_obj, prevkey, outputs);
+ ApplyStats(stats, prevkey, outputs);
+ ApplyHash(hash_obj, prevkey, outputs);
outputs.clear();
}
prevkey = key.hash;
@@ -120,7 +133,8 @@ static bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats&
pcursor->Next();
}
if (!outputs.empty()) {
- ApplyStats(stats, hash_obj, prevkey, outputs);
+ ApplyStats(stats, prevkey, outputs);
+ ApplyHash(hash_obj, prevkey, outputs);
}
FinalizeHash(hash_obj, stats);
@@ -129,19 +143,19 @@ static bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats&
return true;
}
-bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats& stats, CoinStatsHashType hash_type, const std::function<void()>& interruption_point)
+bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats& stats, const std::function<void()>& interruption_point, const CBlockIndex* pindex)
{
- switch (hash_type) {
+ 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);
+ return GetUTXOStats(view, blockman, stats, muhash, interruption_point, pindex);
}
case(CoinStatsHashType::NONE): {
- return GetUTXOStats(view, blockman, stats, nullptr, interruption_point);
+ return GetUTXOStats(view, blockman, stats, nullptr, interruption_point, pindex);
}
} // no default case, so the compiler can warn about missing cases
assert(false);
diff --git a/src/node/coinstats.h b/src/node/coinstats.h
index 975651dcc4..8be256edc9 100644
--- a/src/node/coinstats.h
+++ b/src/node/coinstats.h
@@ -7,6 +7,9 @@
#define BITCOIN_NODE_COINSTATS_H
#include <amount.h>
+#include <chain.h>
+#include <coins.h>
+#include <streams.h>
#include <uint256.h>
#include <cstdint>
@@ -23,6 +26,7 @@ enum class CoinStatsHashType {
struct CCoinsStats
{
+ CoinStatsHashType m_hash_type;
int nHeight{0};
uint256 hashBlock{};
uint64_t nTransactions{0};
@@ -34,9 +38,31 @@ struct CCoinsStats
//! The number of coins contained.
uint64_t coins_count{0};
+
+ //! Signals if the coinstatsindex should be used (when available).
+ bool index_requested{true};
+ //! Signals if the coinstatsindex was used to retrieve the statistics.
+ bool index_used{false};
+
+ // Following values are only available from coinstats index
+ CAmount total_subsidy{0};
+ CAmount block_unspendable_amount{0};
+ CAmount block_prevout_spent_amount{0};
+ CAmount block_new_outputs_ex_coinbase_amount{0};
+ CAmount block_coinbase_amount{0};
+ CAmount unspendables_genesis_block{0};
+ CAmount unspendables_bip30{0};
+ CAmount unspendables_scripts{0};
+ CAmount unspendables_unclaimed_rewards{0};
+
+ 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 CoinStatsHashType hash_type, const std::function<void()>& interruption_point = {});
+bool GetUTXOStats(CCoinsView* view, BlockManager& blockman, CCoinsStats& stats, const std::function<void()>& interruption_point = {}, const CBlockIndex* pindex = nullptr);
+
+uint64_t GetBogoSize(const CScript& script_pub_key);
+
+CDataStream TxOutSer(const COutPoint& outpoint, const Coin& coin);
#endif // BITCOIN_NODE_COINSTATS_H
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index d36814716b..59f7657c0e 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -14,6 +14,7 @@
#include <core_io.h>
#include <hash.h>
#include <index/blockfilterindex.h>
+#include <index/coinstatsindex.h>
#include <node/blockstorage.h>
#include <node/coinstats.h>
#include <node/context.h>
@@ -139,6 +140,33 @@ static int ComputeNextBlockAndDepth(const CBlockIndex* tip, const CBlockIndex* b
return blockindex == tip ? 1 : -1;
}
+CBlockIndex* ParseHashOrHeight(const UniValue& param, 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 uint256 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");
+ }
+
+ return pindex;
+ }
+}
+
UniValue blockheaderToJSON(const CBlockIndex* tip, const CBlockIndex* blockindex)
{
// Serialize passed information without accessing chain state of the active chain!
@@ -1068,50 +1096,88 @@ static RPCHelpMan gettxoutsetinfo()
{
return RPCHelpMan{"gettxoutsetinfo",
"\nReturns 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, RPCArg::Default{"hash_serialized_2"}, "Which UTXO set hash should be calculated. Options: 'hash_serialized_2' (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"}},
+ {"use_index", RPCArg::Type::BOOL, RPCArg::Default{true}, "Use coinstatsindex, if available."},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::NUM, "height", "The block height (index) of the returned statistics"},
{RPCResult::Type::STR_HEX, "bestblock", "The hash of the block at which these statistics are calculated"},
- {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"},
+ {RPCResult::Type::NUM, "bogosize", "Database-independent, meaningless metric indicating the UTXO set size"},
{RPCResult::Type::STR_HEX, "hash_serialized_2", /* optional */ true, "The serialized hash (only present if 'hash_serialized_2' hash_type is chosen)"},
{RPCResult::Type::STR_HEX, "muhash", /* optional */ true, "The serialized hash (only present if 'muhash' hash_type is chosen)"},
- {RPCResult::Type::NUM, "disk_size", "The estimated size of the chainstate on disk"},
+ {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 (not available when coinstatsindex is used)"},
{RPCResult::Type::STR_AMOUNT, "total_amount", "The total amount of coins in the UTXO set"},
+ {RPCResult::Type::STR_AMOUNT, "total_unspendable_amount", "The total amount of coins permanently excluded from the UTXO set (only available if coinstatsindex is used)"},
+ {RPCResult::Type::OBJ, "block_info", "Info on amounts in the block at this block height (only available if coinstatsindex is used)",
+ {
+ {RPCResult::Type::STR_AMOUNT, "prevout_spent", ""},
+ {RPCResult::Type::STR_AMOUNT, "coinbase", ""},
+ {RPCResult::Type::STR_AMOUNT, "new_outputs_ex_coinbase", ""},
+ {RPCResult::Type::STR_AMOUNT, "unspendable", ""},
+ {RPCResult::Type::OBJ, "unspendables", "Detailed view of the unspendable categories",
+ {
+ {RPCResult::Type::STR_AMOUNT, "genesis_block", ""},
+ {RPCResult::Type::STR_AMOUNT, "bip30", "Transactions overridden by duplicates (no longer possible with BIP30)"},
+ {RPCResult::Type::STR_AMOUNT, "scripts", "Amounts sent to scripts that are unspendable (for example OP_RETURN outputs)"},
+ {RPCResult::Type::STR_AMOUNT, "unclaimed_rewards", "Fee rewards that miners did not claim in their coinbase transaction"},
+ }}
+ }},
}},
RPCExamples{
- HelpExampleCli("gettxoutsetinfo", "")
- + HelpExampleRpc("gettxoutsetinfo", "")
+ 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 JSONRPCRequest& request) -> UniValue
{
UniValue ret(UniValue::VOBJ);
- CCoinsStats stats;
+ CBlockIndex* pindex{nullptr};
+ const CoinStatsHashType hash_type{request.params[0].isNull() ? CoinStatsHashType::HASH_SERIALIZED : ParseHashType(request.params[0].get_str())};
+ CCoinsStats stats{hash_type};
+ stats.index_requested = request.params[2].isNull() || request.params[2].get_bool();
+
NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node);
CChainState& active_chainstate = chainman.ActiveChainstate();
active_chainstate.ForceFlushStateToDisk();
- const CoinStatsHashType hash_type{request.params[0].isNull() ? CoinStatsHashType::HASH_SERIALIZED : ParseHashType(request.params[0].get_str())};
-
CCoinsView* coins_view;
BlockManager* blockman;
{
LOCK(::cs_main);
coins_view = &active_chainstate.CoinsDB();
blockman = &active_chainstate.m_blockman;
+ pindex = blockman->LookupBlockIndex(coins_view->GetBestBlock());
}
- if (GetUTXOStats(coins_view, *blockman, stats, hash_type, node.rpc_interruption_point)) {
+
+ if (!request.params[1].isNull()) {
+ if (!g_coin_stats_index) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Querying specific block heights requires coinstatsindex");
+ }
+
+ if (stats.m_hash_type == CoinStatsHashType::HASH_SERIALIZED) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "hash_serialized_2 hash type cannot be queried for a specific block");
+ }
+
+ pindex = ParseHashOrHeight(request.params[1], chainman);
+ }
+
+ if (GetUTXOStats(coins_view, *blockman, stats, 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) {
@@ -1120,9 +1186,42 @@ static RPCHelpMan gettxoutsetinfo()
if (hash_type == CoinStatsHashType::MUHASH) {
ret.pushKV("muhash", stats.hashSerialized.GetHex());
}
- ret.pushKV("disk_size", stats.nDiskSize);
ret.pushKV("total_amount", ValueFromAmount(stats.nTotalAmount));
+ if (!stats.index_used) {
+ ret.pushKV("transactions", static_cast<int64_t>(stats.nTransactions));
+ ret.pushKV("disk_size", stats.nDiskSize);
+ } else {
+ ret.pushKV("total_unspendable_amount", ValueFromAmount(stats.block_unspendable_amount));
+
+ CCoinsStats prev_stats{hash_type};
+
+ if (pindex->nHeight > 0) {
+ GetUTXOStats(coins_view, WITH_LOCK(::cs_main, return std::ref(g_chainman.m_blockman)), prev_stats, node.rpc_interruption_point, pindex->pprev);
+ }
+
+ UniValue block_info(UniValue::VOBJ);
+ block_info.pushKV("prevout_spent", ValueFromAmount(stats.block_prevout_spent_amount - prev_stats.block_prevout_spent_amount));
+ block_info.pushKV("coinbase", ValueFromAmount(stats.block_coinbase_amount - prev_stats.block_coinbase_amount));
+ block_info.pushKV("new_outputs_ex_coinbase", ValueFromAmount(stats.block_new_outputs_ex_coinbase_amount - prev_stats.block_new_outputs_ex_coinbase_amount));
+ block_info.pushKV("unspendable", ValueFromAmount(stats.block_unspendable_amount - prev_stats.block_unspendable_amount));
+
+ UniValue unspendables(UniValue::VOBJ);
+ unspendables.pushKV("genesis_block", ValueFromAmount(stats.unspendables_genesis_block - prev_stats.unspendables_genesis_block));
+ unspendables.pushKV("bip30", ValueFromAmount(stats.unspendables_bip30 - prev_stats.unspendables_bip30));
+ unspendables.pushKV("scripts", ValueFromAmount(stats.unspendables_scripts - prev_stats.unspendables_scripts));
+ unspendables.pushKV("unclaimed_rewards", ValueFromAmount(stats.unspendables_unclaimed_rewards - prev_stats.unspendables_unclaimed_rewards));
+ block_info.pushKV("unspendables", unspendables);
+
+ ret.pushKV("block_info", block_info);
+ }
} 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");
}
return ret;
@@ -1910,31 +2009,7 @@ static RPCHelpMan getblockstats()
{
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 uint256 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<std::string> stats;
@@ -2492,7 +2567,7 @@ static RPCHelpMan dumptxoutset()
UniValue CreateUTXOSnapshot(NodeContext& node, CChainState& chainstate, CAutoFile& afile)
{
std::unique_ptr<CCoinsViewCursor> pcursor;
- CCoinsStats stats;
+ CCoinsStats stats{CoinStatsHashType::NONE};
CBlockIndex* tip;
{
@@ -2512,7 +2587,7 @@ UniValue CreateUTXOSnapshot(NodeContext& node, CChainState& chainstate, CAutoFil
chainstate.ForceFlushStateToDisk();
- if (!GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, stats, CoinStatsHashType::NONE, node.rpc_interruption_point)) {
+ if (!GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, stats, node.rpc_interruption_point)) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
}
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 2b593cd10b..9c8582c7a3 100644
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -127,6 +127,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "gettxout", 1, "n" },
{ "gettxout", 2, "include_mempool" },
{ "gettxoutproof", 0, "txids" },
+ { "gettxoutsetinfo", 1, "hash_or_height" },
+ { "gettxoutsetinfo", 2, "use_index"},
{ "lockunspent", 0, "unlock" },
{ "lockunspent", 1, "transactions" },
{ "send", 0, "outputs" },
diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp
index 09b32345a2..0e0da8f22b 100644
--- a/src/rpc/misc.cpp
+++ b/src/rpc/misc.cpp
@@ -5,6 +5,7 @@
#include <httpserver.h>
#include <index/blockfilterindex.h>
+#include <index/coinstatsindex.h>
#include <index/txindex.h>
#include <interfaces/chain.h>
#include <interfaces/echo.h>
@@ -729,6 +730,10 @@ static RPCHelpMan getindexinfo()
result.pushKVs(SummaryToJSON(g_txindex->GetSummary(), index_name));
}
+ if (g_coin_stats_index) {
+ result.pushKVs(SummaryToJSON(g_coin_stats_index->GetSummary(), index_name));
+ }
+
ForEachBlockFilterIndex([&result, &index_name](const BlockFilterIndex& index) {
result.pushKVs(SummaryToJSON(index.GetSummary(), index_name));
});
diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp
new file mode 100644
index 0000000000..3fc7b72077
--- /dev/null
+++ b/src/test/coinstatsindex_tests.cpp
@@ -0,0 +1,79 @@
+// 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.
+
+#include <index/coinstatsindex.h>
+#include <test/util/setup_common.h>
+#include <util/time.h>
+#include <validation.h>
+
+#include <boost/test/unit_test.hpp>
+
+#include <chrono>
+
+
+BOOST_AUTO_TEST_SUITE(coinstatsindex_tests)
+
+BOOST_FIXTURE_TEST_CASE(coinstatsindex_initial_sync, TestChain100Setup)
+{
+ CoinStatsIndex coin_stats_index{1 << 20, true};
+
+ CCoinsStats coin_stats{CoinStatsHashType::MUHASH};
+ const CBlockIndex* block_index;
+ {
+ LOCK(cs_main);
+ block_index = ChainActive().Tip();
+ }
+
+ // CoinStatsIndex should not be found before it is started.
+ BOOST_CHECK(!coin_stats_index.LookUpStats(block_index, coin_stats));
+
+ // BlockUntilSyncedToCurrentChain should return false before CoinStatsIndex
+ // is started.
+ BOOST_CHECK(!coin_stats_index.BlockUntilSyncedToCurrentChain());
+
+ coin_stats_index.Start();
+
+ // Allow the CoinStatsIndex to catch up with the block index that is syncing
+ // in a background thread.
+ const auto timeout = GetTime<std::chrono::seconds>() + 120s;
+ while (!coin_stats_index.BlockUntilSyncedToCurrentChain()) {
+ BOOST_REQUIRE(timeout > GetTime<std::chrono::milliseconds>());
+ UninterruptibleSleep(100ms);
+ }
+
+ // Check that CoinStatsIndex works for genesis block.
+ const CBlockIndex* genesis_block_index;
+ {
+ LOCK(cs_main);
+ genesis_block_index = ChainActive().Genesis();
+ }
+ BOOST_CHECK(coin_stats_index.LookUpStats(genesis_block_index, coin_stats));
+
+ // Check that CoinStatsIndex updates with new blocks.
+ coin_stats_index.LookUpStats(block_index, coin_stats);
+
+ const CScript script_pub_key{CScript() << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG};
+ std::vector<CMutableTransaction> noTxns;
+ CreateAndProcessBlock(noTxns, script_pub_key);
+
+ // Let the CoinStatsIndex to catch up again.
+ BOOST_CHECK(coin_stats_index.BlockUntilSyncedToCurrentChain());
+
+ CCoinsStats new_coin_stats{CoinStatsHashType::MUHASH};
+ const CBlockIndex* new_block_index;
+ {
+ LOCK(cs_main);
+ new_block_index = ChainActive().Tip();
+ }
+ coin_stats_index.LookUpStats(new_block_index, new_coin_stats);
+
+ BOOST_CHECK(block_index != new_block_index);
+
+ // Shutdown sequence (c.f. Shutdown() in init.cpp)
+ coin_stats_index.Stop();
+
+ // Rest of shutdown sequence and destructors happen in ~TestingSetup()
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
index b21d2eae79..21dc80cc8d 100644
--- a/src/test/fuzz/coins_view.cpp
+++ b/src/test/fuzz/coins_view.cpp
@@ -258,10 +258,10 @@ FUZZ_TARGET_INIT(coins_view, initialize_coins_view)
(void)GetTransactionSigOpCost(transaction, coins_view_cache, flags);
},
[&] {
- CCoinsStats stats;
+ CCoinsStats stats{CoinStatsHashType::HASH_SERIALIZED};
bool expected_code_path = false;
try {
- (void)GetUTXOStats(&coins_view_cache, WITH_LOCK(::cs_main, return std::ref(g_chainman.m_blockman)), stats, CoinStatsHashType::HASH_SERIALIZED);
+ (void)GetUTXOStats(&coins_view_cache, WITH_LOCK(::cs_main, return std::ref(g_chainman.m_blockman)), stats);
} catch (const std::logic_error&) {
expected_code_path = true;
}
diff --git a/src/validation.cpp b/src/validation.cpp
index 1ddafa5613..bf774a0791 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -5175,14 +5175,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
return false;
}
- CCoinsStats stats;
+ CCoinsStats stats{CoinStatsHashType::HASH_SERIALIZED};
auto breakpoint_fnc = [] { /* TODO insert breakpoint here? */ };
// As above, okay to immediately release cs_main here since no other context knows
// about the snapshot_chainstate.
CCoinsViewDB* snapshot_coinsdb = WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsDB());
- if (!GetUTXOStats(snapshot_coinsdb, WITH_LOCK(::cs_main, return std::ref(m_blockman)), stats, CoinStatsHashType::HASH_SERIALIZED, breakpoint_fnc)) {
+ if (!GetUTXOStats(snapshot_coinsdb, WITH_LOCK(::cs_main, return std::ref(m_blockman)), stats, breakpoint_fnc)) {
LogPrintf("[snapshot] failed to generate coins stats\n");
return false;
}
diff --git a/src/validation.h b/src/validation.h
index ed1ba4310c..63a8bdc096 100644
--- a/src/validation.h
+++ b/src/validation.h
@@ -79,6 +79,7 @@ static const int DEFAULT_SCRIPTCHECK_THREADS = 0;
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 */
static const bool DEFAULT_PERSIST_MEMPOOL = true;
diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py
new file mode 100755
index 0000000000..d3adde5cc5
--- /dev/null
+++ b/test/functional/feature_coinstatsindex.py
@@ -0,0 +1,313 @@
+#!/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 decimal import Decimal
+
+from test_framework.blocktools import (
+ create_block,
+ create_coinbase,
+)
+from test_framework.messages import (
+ COIN,
+ COutPoint,
+ CTransaction,
+ CTxIn,
+ CTxOut,
+ ToHex,
+)
+from test_framework.script import (
+ CScript,
+ OP_FALSE,
+ OP_RETURN,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+ assert_raises_rpc_error,
+ 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()
+ self._test_use_index_option()
+ self._test_reorg_index()
+ self._test_index_rejects_hash_serialized()
+
+ def block_sanity_check(self, block_info):
+ block_subsidy = 50
+ assert_equal(
+ block_info['prevout_spent'] + block_subsidy,
+ block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable']
+ )
+
+ 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, 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 exist on the index
+ 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)
+ # The fields 'block_info' and 'total_unspendable_amount' only exist on the index
+ del res1['block_info'], res1['total_unspendable_amount']
+ res1.pop('muhash', None)
+
+ # 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)
+ del res2['block_info'], res2['total_unspendable_amount']
+ res2.pop('muhash', None)
+ assert_equal(res0, res2)
+
+ # Fetch old stats by hash
+ res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock'])
+ del res3['block_info'], res3['total_unspendable_amount']
+ 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)
+
+ self.log.info("Test gettxoutsetinfo() with index and verbose flag")
+
+ for hash_option in index_hash_options:
+ # Genesis block is unspendable
+ res4 = index_node.gettxoutsetinfo(hash_option, 0)
+ assert_equal(res4['total_unspendable_amount'], 50)
+ assert_equal(res4['block_info'], {
+ 'unspendable': 50,
+ 'prevout_spent': 0,
+ 'new_outputs_ex_coinbase': 0,
+ 'coinbase': 0,
+ 'unspendables': {
+ 'genesis_block': 50,
+ 'bip30': 0,
+ 'scripts': 0,
+ 'unclaimed_rewards': 0
+ }
+ })
+ self.block_sanity_check(res4['block_info'])
+
+ # Test an older block height that included a normal tx
+ res5 = index_node.gettxoutsetinfo(hash_option, 102)
+ assert_equal(res5['total_unspendable_amount'], 50)
+ assert_equal(res5['block_info'], {
+ 'unspendable': 0,
+ 'prevout_spent': 50,
+ 'new_outputs_ex_coinbase': Decimal('49.99995560'),
+ 'coinbase': Decimal('50.00004440'),
+ 'unspendables': {
+ 'genesis_block': 0,
+ 'bip30': 0,
+ 'scripts': 0,
+ 'unclaimed_rewards': 0
+ }
+ })
+ self.block_sanity_check(res5['block_info'])
+
+ # Generate and send a normal tx with two outputs
+ tx1_inputs = []
+ tx1_outputs = {self.nodes[0].getnewaddress(): 21, self.nodes[0].getnewaddress(): 42}
+ raw_tx1 = self.nodes[0].createrawtransaction(tx1_inputs, tx1_outputs)
+ funded_tx1 = self.nodes[0].fundrawtransaction(raw_tx1)
+ signed_tx1 = self.nodes[0].signrawtransactionwithwallet(funded_tx1['hex'])
+ tx1_txid = self.nodes[0].sendrawtransaction(signed_tx1['hex'])
+
+ # Find the right position of the 21 BTC output
+ tx1_final = self.nodes[0].gettransaction(tx1_txid)
+ for output in tx1_final['details']:
+ if output['amount'] == Decimal('21.00000000') and output['category'] == 'receive':
+ n = output['vout']
+
+ # Generate and send another tx with an OP_RETURN output (which is unspendable)
+ tx2 = CTransaction()
+ tx2.vin.append(CTxIn(COutPoint(int(tx1_txid, 16), n), b''))
+ tx2.vout.append(CTxOut(int(20.99 * COIN), CScript([OP_RETURN] + [OP_FALSE]*30)))
+ tx2_hex = self.nodes[0].signrawtransactionwithwallet(ToHex(tx2))['hex']
+ self.nodes[0].sendrawtransaction(tx2_hex)
+
+ # Include both txs in a block
+ self.nodes[0].generate(1)
+ self.sync_all()
+
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ for hash_option in index_hash_options:
+ # Check all amounts were registered correctly
+ res6 = index_node.gettxoutsetinfo(hash_option, 108)
+ assert_equal(res6['total_unspendable_amount'], Decimal('70.98999999'))
+ assert_equal(res6['block_info'], {
+ 'unspendable': Decimal('20.98999999'),
+ 'prevout_spent': 111,
+ 'new_outputs_ex_coinbase': Decimal('89.99993620'),
+ 'coinbase': Decimal('50.01006381'),
+ 'unspendables': {
+ 'genesis_block': 0,
+ 'bip30': 0,
+ 'scripts': Decimal('20.98999999'),
+ 'unclaimed_rewards': 0
+ }
+ })
+ self.block_sanity_check(res6['block_info'])
+
+ # Create a coinbase that does not claim full subsidy and also
+ # has two outputs
+ cb = create_coinbase(109, nValue=35)
+ cb.vout.append(CTxOut(5 * COIN, CScript([OP_FALSE])))
+ cb.rehash()
+
+ # Generate a block that includes previous coinbase
+ tip = self.nodes[0].getbestblockhash()
+ block_time = self.nodes[0].getblock(tip)['time'] + 1
+ block = create_block(int(tip, 16), cb, block_time)
+ block.solve()
+ self.nodes[0].submitblock(ToHex(block))
+ self.sync_all()
+
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ for hash_option in index_hash_options:
+ res7 = index_node.gettxoutsetinfo(hash_option, 109)
+ assert_equal(res7['total_unspendable_amount'], Decimal('80.98999999'))
+ assert_equal(res7['block_info'], {
+ 'unspendable': 10,
+ 'prevout_spent': 0,
+ 'new_outputs_ex_coinbase': 0,
+ 'coinbase': 40,
+ 'unspendables': {
+ 'genesis_block': 0,
+ 'bip30': 0,
+ 'scripts': 0,
+ 'unclaimed_rewards': 10
+ }
+ })
+ self.block_sanity_check(res7['block_info'])
+
+ self.log.info("Test that the index is robust across restarts")
+
+ res8 = index_node.gettxoutsetinfo('muhash')
+ self.restart_node(1, extra_args=self.extra_args[1])
+ res9 = index_node.gettxoutsetinfo('muhash')
+ assert_equal(res8, res9)
+
+ index_node.generate(1)
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ res10 = index_node.gettxoutsetinfo('muhash')
+ assert(res8['txouts'] < res10['txouts'])
+
+ def _test_use_index_option(self):
+ self.log.info("Test use_index option for nodes running the index")
+
+ self.connect_nodes(0, 1)
+ self.nodes[0].waitforblockheight(110)
+ res = self.nodes[0].gettxoutsetinfo('muhash')
+ option_res = self.nodes[1].gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
+ del res['disk_size'], option_res['disk_size']
+ assert_equal(res, option_res)
+
+ def _test_reorg_index(self):
+ self.log.info("Test that index can handle reorgs")
+
+ # Generate two block, let the index catch up, then invalidate the blocks
+ index_node = self.nodes[1]
+ reorg_blocks = index_node.generatetoaddress(2, index_node.getnewaddress())
+ reorg_block = reorg_blocks[1]
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ res_invalid = index_node.gettxoutsetinfo('muhash')
+ index_node.invalidateblock(reorg_blocks[0])
+ assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110)
+
+ # Add two new blocks
+ block = index_node.generate(2)[1]
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False)
+
+ # Test that the result of the reorged block is not returned for its old block height
+ res2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112)
+ assert_equal(res["bestblock"], block)
+ assert_equal(res["muhash"], res2["muhash"])
+ assert(res["muhash"] != res_invalid["muhash"])
+
+ # Test that requesting reorged out block by hash is still returning correct results
+ res_invalid2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=reorg_block)
+ assert_equal(res_invalid2["muhash"], res_invalid["muhash"])
+ assert(res["muhash"] != res_invalid2["muhash"])
+
+ # Add another block, so we don't depend on reconsiderblock remembering which
+ # blocks were touched by invalidateblock
+ index_node.generate(1)
+
+ # Ensure that removing and re-adding blocks yields consistent results
+ block = index_node.getblockhash(99)
+ index_node.invalidateblock(block)
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ index_node.reconsiderblock(block)
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", index_node.gettxoutsetinfo, 'muhash'))
+ res3 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112)
+ assert_equal(res2, res3)
+
+ self.log.info("Test that a node aware of stale blocks syncs them as well")
+ node = self.nodes[0]
+ # Ensure the node is aware of a stale block prior to restart
+ node.getblock(reorg_block)
+
+ self.restart_node(0, ["-coinstatsindex"])
+ self.wait_until(lambda: not try_rpc(-32603, "Unable to read UTXO set", node.gettxoutsetinfo, 'muhash'))
+ assert_raises_rpc_error(-32603, "Unable to read UTXO set", node.gettxoutsetinfo, 'muhash', reorg_block)
+
+ def _test_index_rejects_hash_serialized(self):
+ self.log.info("Test that the rpc raises if the legacy hash is passed with the index")
+
+ msg = "hash_serialized_2 hash type cannot be queried for a specific block"
+ assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111)
+
+ for use_index in {True, False, None}:
+ assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_2', hash_or_height=111, use_index=use_index)
+
+
+if __name__ == '__main__':
+ CoinStatsIndexTest().main()
diff --git a/test/functional/rpc_misc.py b/test/functional/rpc_misc.py
index a80fa596cd..52c8fa883d 100755
--- a/test/functional/rpc_misc.py
+++ b/test/functional/rpc_misc.py
@@ -69,25 +69,22 @@ class RpcMiscTest(BitcoinTestFramework):
assert_equal(node.getindexinfo(), {})
# Restart the node with indices and wait for them to sync
- self.restart_node(0, ["-txindex", "-blockfilterindex"])
+ self.restart_node(0, ["-txindex", "-blockfilterindex", "-coinstatsindex"])
self.wait_until(lambda: all(i["synced"] for i in node.getindexinfo().values()))
# Returns a list of all running indices by default
+ values = {"synced": True, "best_block_height": 200}
assert_equal(
node.getindexinfo(),
{
- "txindex": {"synced": True, "best_block_height": 200},
- "basic block filter index": {"synced": True, "best_block_height": 200}
+ "txindex": values,
+ "basic block filter index": values,
+ "coinstatsindex": values,
}
)
-
# Specifying an index by name returns only the status of that index
- assert_equal(
- node.getindexinfo("txindex"),
- {
- "txindex": {"synced": True, "best_block_height": 200},
- }
- )
+ for i in {"txindex", "basic block filter index", "coinstatsindex"}:
+ assert_equal(node.getindexinfo(i), {i: values})
# Specifying an unknown index name returns an empty result
assert_equal(node.getindexinfo("foo"), {})
diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py
index e691b63df6..d08e025178 100644
--- a/test/functional/test_framework/blocktools.py
+++ b/test/functional/test_framework/blocktools.py
@@ -115,7 +115,7 @@ def script_BIP34_coinbase_height(height):
return CScript([CScriptNum(height)])
-def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0):
+def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0, nValue=50):
"""Create a coinbase transaction.
If pubkey is passed in, the coinbase output will be a P2PK output;
@@ -126,10 +126,11 @@ def create_coinbase(height, pubkey=None, extra_output_script=None, fees=0):
coinbase = CTransaction()
coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff))
coinbaseoutput = CTxOut()
- coinbaseoutput.nValue = 50 * COIN
- halvings = int(height / 150) # regtest
- coinbaseoutput.nValue >>= halvings
- coinbaseoutput.nValue += fees
+ coinbaseoutput.nValue = nValue * COIN
+ if nValue == 50:
+ halvings = int(height / 150) # regtest
+ coinbaseoutput.nValue >>= halvings
+ coinbaseoutput.nValue += fees
if pubkey is not None:
coinbaseoutput.scriptPubKey = CScript([pubkey, OP_CHECKSIG])
else:
diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py
index 0ff4ee0a62..a89a26caea 100755
--- a/test/functional/test_framework/test_framework.py
+++ b/test/functional/test_framework/test_framework.py
@@ -758,7 +758,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
os.rmdir(cache_path('wallets')) # Remove empty wallets dir
for entry in os.listdir(cache_path()):
- if entry not in ['chainstate', 'blocks']: # Only keep chainstate and blocks folder
+ if entry not in ['chainstate', 'blocks', 'indexes']: # Only indexes, chainstate and blocks folders
os.remove(cache_path(entry))
for i in range(self.num_nodes):
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index bd58f2cd51..00527e78f1 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -281,6 +281,7 @@ BASE_SCRIPTS = [
'rpc_scantxoutset.py',
'feature_logging.py',
'feature_anchors.py',
+ 'feature_coinstatsindex.py',
'p2p_node_network_limited.py',
'p2p_permissions.py',
'feature_blocksdir.py',
diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh
index ad2333a808..354e14f361 100755
--- a/test/lint/lint-circular-dependencies.sh
+++ b/test/lint/lint-circular-dependencies.sh
@@ -14,6 +14,7 @@ EXPECTED_CIRCULAR_DEPENDENCIES=(
"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"
"policy/fees -> txmempool -> policy/fees"
"qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel"
"qt/bitcoingui -> qt/walletframe -> qt/bitcoingui"