diff --git a/doc/files.md b/doc/files.md index 0bf115e1c11..c8d8aab3a7a 100644 --- a/doc/files.md +++ b/doc/files.md @@ -57,7 +57,7 @@ Subdirectory | File(s) | Description `indexes/txindex/` | LevelDB database | Transaction index; *optional*, used if `-txindex=1` `indexes/blockfilter/basic/db/` | LevelDB database | Blockfilter index LevelDB database for the basic filtertype; *optional*, used if `-blockfilterindex=basic` `indexes/blockfilter/basic/` | `fltrNNNNN.dat`[\[2\]](#note2) | Blockfilter index filters for the basic filtertype; *optional*, used if `-blockfilterindex=basic` -`indexes/coinstats/db/` | LevelDB database | Coinstats index; *optional*, used if `-coinstatsindex=1` +`indexes/coinstatsindex/db/` | LevelDB database | Coinstats index; *optional*, used if `-coinstatsindex=1` `wallets/` | | [Contains wallets](#multi-wallet-environment); can be specified by `-walletdir` option; if `wallets/` subdirectory does not exist, wallets reside in the [data directory](#data-directory-location) `./` | `anchors.dat` | Anchor IP address database, created on shutdown and deleted at startup. Anchors are last known outgoing block-relay-only peers that are tried to re-connect to on startup `./` | `banlist.json` | Stores the addresses/subnets of banned nodes. diff --git a/src/index/coinstatsindex.cpp b/src/index/coinstatsindex.cpp index 48d4c749619..bb4be90bf40 100644 --- a/src/index/coinstatsindex.cpp +++ b/src/index/coinstatsindex.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include @@ -27,35 +28,42 @@ static constexpr uint8_t DB_MUHASH{'M'}; namespace { struct DBVal { - uint256 muhash; - uint64_t transaction_output_count; - uint64_t bogo_size; - CAmount total_amount; - CAmount total_subsidy; - CAmount total_unspendable_amount; - CAmount total_prevout_spent_amount; - CAmount total_new_outputs_ex_coinbase_amount; - CAmount total_coinbase_amount; - CAmount total_unspendables_genesis_block; - CAmount total_unspendables_bip30; - CAmount total_unspendables_scripts; - CAmount total_unspendables_unclaimed_rewards; + uint256 muhash{uint256::ZERO}; + uint64_t transaction_output_count{0}; + uint64_t bogo_size{0}; + CAmount total_amount{0}; + CAmount total_subsidy{0}; + arith_uint256 total_prevout_spent_amount{0}; + arith_uint256 total_new_outputs_ex_coinbase_amount{0}; + arith_uint256 total_coinbase_amount{0}; + CAmount total_unspendables_genesis_block{0}; + CAmount total_unspendables_bip30{0}; + CAmount total_unspendables_scripts{0}; + CAmount total_unspendables_unclaimed_rewards{0}; SERIALIZE_METHODS(DBVal, obj) { + uint256 prevout_spent, new_outputs, coinbase; + SER_WRITE(obj, prevout_spent = ArithToUint256(obj.total_prevout_spent_amount)); + SER_WRITE(obj, new_outputs = ArithToUint256(obj.total_new_outputs_ex_coinbase_amount)); + SER_WRITE(obj, coinbase = ArithToUint256(obj.total_coinbase_amount)); + READWRITE(obj.muhash); READWRITE(obj.transaction_output_count); READWRITE(obj.bogo_size); READWRITE(obj.total_amount); READWRITE(obj.total_subsidy); - READWRITE(obj.total_unspendable_amount); - READWRITE(obj.total_prevout_spent_amount); - READWRITE(obj.total_new_outputs_ex_coinbase_amount); - READWRITE(obj.total_coinbase_amount); + READWRITE(prevout_spent); + READWRITE(new_outputs); + READWRITE(coinbase); READWRITE(obj.total_unspendables_genesis_block); READWRITE(obj.total_unspendables_bip30); READWRITE(obj.total_unspendables_scripts); READWRITE(obj.total_unspendables_unclaimed_rewards); + + SER_READ(obj, obj.total_prevout_spent_amount = UintToArith256(prevout_spent)); + SER_READ(obj, obj.total_new_outputs_ex_coinbase_amount = UintToArith256(new_outputs)); + SER_READ(obj, obj.total_coinbase_amount = UintToArith256(coinbase)); } }; @@ -106,7 +114,17 @@ std::unique_ptr g_coin_stats_index; CoinStatsIndex::CoinStatsIndex(std::unique_ptr chain, size_t n_cache_size, bool f_memory, bool f_wipe) : BaseIndex(std::move(chain), "coinstatsindex") { - fs::path path{gArgs.GetDataDirNet() / "indexes" / "coinstats"}; + // An earlier version of the index used "indexes/coinstats" but it contained + // a bug and is superseded by a fixed version at "indexes/coinstatsindex". + // The original index is kept around until the next release in case users + // decide to downgrade their node. + auto old_path = gArgs.GetDataDirNet() / "indexes" / "coinstats"; + if (fs::exists(old_path)) { + // TODO: Change this to deleting the old index with v31. + LogWarning("Old version of coinstatsindex found at %s. This folder can be safely deleted unless you " \ + "plan to downgrade your node to version 29 or lower.", fs::PathToString(old_path)); + } + fs::path path{gArgs.GetDataDirNet() / "indexes" / "coinstatsindex"}; fs::create_directories(path); m_db = std::make_unique(path / "db", n_cache_size, f_memory, f_wipe); @@ -144,7 +162,6 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) // Skip duplicate txid coinbase transactions (BIP30). if (is_coinbase && IsBIP30Unspendable(block.hash, block.height)) { - m_total_unspendable_amount += block_subsidy; m_total_unspendables_bip30 += block_subsidy; continue; } @@ -156,7 +173,6 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) // Skip unspendable coins if (coin.out.scriptPubKey.IsUnspendable()) { - m_total_unspendable_amount += coin.out.nValue; m_total_unspendables_scripts += coin.out.nValue; continue; } @@ -179,7 +195,7 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) const auto& tx_undo{Assert(block.undo_data)->vtxundo.at(i - 1)}; for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) { - const Coin coin{tx_undo.vprevout[j]}; + const Coin& coin{tx_undo.vprevout[j]}; const COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n}; RemoveCoinHash(m_muhash, outpoint, coin); @@ -194,7 +210,6 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) } } else { // genesis block - m_total_unspendable_amount += block_subsidy; m_total_unspendables_genesis_block += block_subsidy; } @@ -202,9 +217,10 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) // 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_total_prevout_spent_amount + m_total_subsidy) - (m_total_new_outputs_ex_coinbase_amount + m_total_coinbase_amount + m_total_unspendable_amount)}; - m_total_unspendable_amount += unclaimed_rewards; - m_total_unspendables_unclaimed_rewards += unclaimed_rewards; + const CAmount temp_total_unspendable_amount{m_total_unspendables_genesis_block + m_total_unspendables_bip30 + m_total_unspendables_scripts + m_total_unspendables_unclaimed_rewards}; + const arith_uint256 unclaimed_rewards{(m_total_prevout_spent_amount + m_total_subsidy) - (m_total_new_outputs_ex_coinbase_amount + m_total_coinbase_amount + temp_total_unspendable_amount)}; + assert(unclaimed_rewards <= arith_uint256(std::numeric_limits::max())); + m_total_unspendables_unclaimed_rewards += static_cast(unclaimed_rewards.GetLow64()); std::pair value; value.first = block.hash; @@ -212,7 +228,6 @@ bool CoinStatsIndex::CustomAppend(const interfaces::BlockInfo& block) value.second.bogo_size = m_bogo_size; value.second.total_amount = m_total_amount; value.second.total_subsidy = m_total_subsidy; - value.second.total_unspendable_amount = m_total_unspendable_amount; value.second.total_prevout_spent_amount = m_total_prevout_spent_amount; value.second.total_new_outputs_ex_coinbase_amount = m_total_new_outputs_ex_coinbase_amount; value.second.total_coinbase_amount = m_total_coinbase_amount; @@ -307,7 +322,6 @@ std::optional CoinStatsIndex::LookUpStats(const CBlockIndex& block_ stats.nBogoSize = entry.bogo_size; stats.total_amount = entry.total_amount; stats.total_subsidy = entry.total_subsidy; - stats.total_unspendable_amount = entry.total_unspendable_amount; stats.total_prevout_spent_amount = entry.total_prevout_spent_amount; stats.total_new_outputs_ex_coinbase_amount = entry.total_new_outputs_ex_coinbase_amount; stats.total_coinbase_amount = entry.total_coinbase_amount; @@ -352,7 +366,6 @@ bool CoinStatsIndex::CustomInit(const std::optional& block m_bogo_size = entry.bogo_size; m_total_amount = entry.total_amount; m_total_subsidy = entry.total_subsidy; - m_total_unspendable_amount = entry.total_unspendable_amount; m_total_prevout_spent_amount = entry.total_prevout_spent_amount; m_total_new_outputs_ex_coinbase_amount = entry.total_new_outputs_ex_coinbase_amount; m_total_coinbase_amount = entry.total_coinbase_amount; @@ -387,9 +400,6 @@ bool CoinStatsIndex::RevertBlock(const interfaces::BlockInfo& block) { std::pair read_out; - const CAmount block_subsidy{GetBlockSubsidy(block.height, Params().GetConsensus())}; - m_total_subsidy -= block_subsidy; - // Ignore genesis block if (block.height > 0) { if (!m_db->Read(DBHeightKey(block.height - 1), read_out)) { @@ -409,7 +419,8 @@ bool CoinStatsIndex::RevertBlock(const interfaces::BlockInfo& block) } } - // Remove the new UTXOs that were created from the block + // Roll back muhash by removing the new UTXOs that were created by the + // block and reapplying the old UTXOs that were spent by the block assert(block.data); assert(block.undo_data); for (size_t i = 0; i < block.data->vtx.size(); ++i) { @@ -421,24 +432,9 @@ bool CoinStatsIndex::RevertBlock(const interfaces::BlockInfo& block) const COutPoint outpoint{tx->GetHash(), j}; const Coin coin{out, block.height, is_coinbase}; - // Skip unspendable coins - if (coin.out.scriptPubKey.IsUnspendable()) { - m_total_unspendable_amount -= coin.out.nValue; - m_total_unspendables_scripts -= coin.out.nValue; - continue; + if (!coin.out.scriptPubKey.IsUnspendable()) { + RemoveCoinHash(m_muhash, outpoint, coin); } - - RemoveCoinHash(m_muhash, outpoint, coin); - - if (tx->IsCoinBase()) { - m_total_coinbase_amount -= coin.out.nValue; - } else { - m_total_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 @@ -446,40 +442,30 @@ bool CoinStatsIndex::RevertBlock(const interfaces::BlockInfo& block) const auto& tx_undo{block.undo_data->vtxundo.at(i - 1)}; for (size_t j = 0; j < tx_undo.vprevout.size(); ++j) { - const Coin coin{tx_undo.vprevout[j]}; + const Coin& coin{tx_undo.vprevout[j]}; const COutPoint outpoint{tx->vin[j].prevout.hash, tx->vin[j].prevout.n}; ApplyCoinHash(m_muhash, outpoint, coin); - - m_total_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_total_new_outputs_ex_coinbase_amount + m_total_coinbase_amount + m_total_unspendable_amount) - (m_total_prevout_spent_amount + m_total_subsidy)}; - m_total_unspendable_amount -= unclaimed_rewards; - m_total_unspendables_unclaimed_rewards -= unclaimed_rewards; - - // Check that the rolled back internal values are consistent with the DB read out + // Check that the rolled back muhash is 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_total_unspendable_amount == read_out.second.total_unspendable_amount); - Assert(m_total_prevout_spent_amount == read_out.second.total_prevout_spent_amount); - Assert(m_total_new_outputs_ex_coinbase_amount == read_out.second.total_new_outputs_ex_coinbase_amount); - Assert(m_total_coinbase_amount == read_out.second.total_coinbase_amount); - Assert(m_total_unspendables_genesis_block == read_out.second.total_unspendables_genesis_block); - Assert(m_total_unspendables_bip30 == read_out.second.total_unspendables_bip30); - Assert(m_total_unspendables_scripts == read_out.second.total_unspendables_scripts); - Assert(m_total_unspendables_unclaimed_rewards == read_out.second.total_unspendables_unclaimed_rewards); + // Apply the other values from the DB to the member variables + m_transaction_output_count = read_out.second.transaction_output_count; + m_total_amount = read_out.second.total_amount; + m_bogo_size = read_out.second.bogo_size; + m_total_subsidy = read_out.second.total_subsidy; + m_total_prevout_spent_amount = read_out.second.total_prevout_spent_amount; + m_total_new_outputs_ex_coinbase_amount = read_out.second.total_new_outputs_ex_coinbase_amount; + m_total_coinbase_amount = read_out.second.total_coinbase_amount; + m_total_unspendables_genesis_block = read_out.second.total_unspendables_genesis_block; + m_total_unspendables_bip30 = read_out.second.total_unspendables_bip30; + m_total_unspendables_scripts = read_out.second.total_unspendables_scripts; + m_total_unspendables_unclaimed_rewards = read_out.second.total_unspendables_unclaimed_rewards; return true; } diff --git a/src/index/coinstatsindex.h b/src/index/coinstatsindex.h index f2e95b449e4..5dcbc186415 100644 --- a/src/index/coinstatsindex.h +++ b/src/index/coinstatsindex.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_INDEX_COINSTATSINDEX_H #define BITCOIN_INDEX_COINSTATSINDEX_H +#include #include #include @@ -29,10 +30,9 @@ private: uint64_t m_bogo_size{0}; CAmount m_total_amount{0}; CAmount m_total_subsidy{0}; - CAmount m_total_unspendable_amount{0}; - CAmount m_total_prevout_spent_amount{0}; - CAmount m_total_new_outputs_ex_coinbase_amount{0}; - CAmount m_total_coinbase_amount{0}; + arith_uint256 m_total_prevout_spent_amount{0}; + arith_uint256 m_total_new_outputs_ex_coinbase_amount{0}; + arith_uint256 m_total_coinbase_amount{0}; CAmount m_total_unspendables_genesis_block{0}; CAmount m_total_unspendables_bip30{0}; CAmount m_total_unspendables_scripts{0}; diff --git a/src/kernel/coinstats.h b/src/kernel/coinstats.h index c0c363a8428..8a782ed5af4 100644 --- a/src/kernel/coinstats.h +++ b/src/kernel/coinstats.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_KERNEL_COINSTATS_H #define BITCOIN_KERNEL_COINSTATS_H +#include #include #include #include @@ -50,14 +51,6 @@ struct CCoinsStats { //! Total cumulative amount of block subsidies up to and including this block CAmount total_subsidy{0}; - //! Total cumulative amount of unspendable coins up to and including this block - CAmount total_unspendable_amount{0}; - //! Total cumulative amount of prevouts spent up to and including this block - CAmount total_prevout_spent_amount{0}; - //! Total cumulative amount of outputs created up to and including this block - CAmount total_new_outputs_ex_coinbase_amount{0}; - //! Total cumulative amount of coinbase outputs up to and including this block - CAmount total_coinbase_amount{0}; //! The unspendable coinbase amount from the genesis block CAmount total_unspendables_genesis_block{0}; //! The two unspendable coinbase outputs total amount caused by BIP30 @@ -67,6 +60,15 @@ struct CCoinsStats { //! Total cumulative amount of coins lost due to unclaimed miner rewards up to and including this block CAmount total_unspendables_unclaimed_rewards{0}; + // Despite containing amounts the following values use a uint256 type to prevent overflowing + + //! Total cumulative amount of prevouts spent up to and including this block + arith_uint256 total_prevout_spent_amount{0}; + //! Total cumulative amount of outputs created up to and including this block + arith_uint256 total_new_outputs_ex_coinbase_amount{0}; + //! Total cumulative amount of coinbase outputs up to and including this block + arith_uint256 total_coinbase_amount{0}; + CCoinsStats() = default; CCoinsStats(int block_height, const uint256& block_hash); }; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index cfc0379f683..bd5deedf6ee 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1101,8 +1101,6 @@ static RPCHelpMan gettxoutsetinfo() ret.pushKV("transactions", static_cast(stats.nTransactions)); ret.pushKV("disk_size", stats.nDiskSize); } else { - ret.pushKV("total_unspendable_amount", ValueFromAmount(stats.total_unspendable_amount)); - CCoinsStats prev_stats{}; if (pindex->nHeight > 0) { const std::optional maybe_prev_stats = GetUTXOStats(coins_view, *blockman, hash_type, node.rpc_interruption_point, pindex->pprev, index_requested); @@ -1112,11 +1110,29 @@ static RPCHelpMan gettxoutsetinfo() prev_stats = maybe_prev_stats.value(); } + CAmount block_total_unspendable_amount = stats.total_unspendables_genesis_block + + stats.total_unspendables_bip30 + + stats.total_unspendables_scripts + + stats.total_unspendables_unclaimed_rewards; + CAmount prev_block_total_unspendable_amount = prev_stats.total_unspendables_genesis_block + + prev_stats.total_unspendables_bip30 + + prev_stats.total_unspendables_scripts + + prev_stats.total_unspendables_unclaimed_rewards; + + ret.pushKV("total_unspendable_amount", ValueFromAmount(block_total_unspendable_amount)); + UniValue block_info(UniValue::VOBJ); - block_info.pushKV("prevout_spent", ValueFromAmount(stats.total_prevout_spent_amount - prev_stats.total_prevout_spent_amount)); - block_info.pushKV("coinbase", ValueFromAmount(stats.total_coinbase_amount - prev_stats.total_coinbase_amount)); - block_info.pushKV("new_outputs_ex_coinbase", ValueFromAmount(stats.total_new_outputs_ex_coinbase_amount - prev_stats.total_new_outputs_ex_coinbase_amount)); - block_info.pushKV("unspendable", ValueFromAmount(stats.total_unspendable_amount - prev_stats.total_unspendable_amount)); + // These per-block values should fit uint64 under normal circumstances + arith_uint256 diff_prevout = stats.total_prevout_spent_amount - prev_stats.total_prevout_spent_amount; + arith_uint256 diff_coinbase = stats.total_coinbase_amount - prev_stats.total_coinbase_amount; + arith_uint256 diff_outputs = stats.total_new_outputs_ex_coinbase_amount - prev_stats.total_new_outputs_ex_coinbase_amount; + CAmount prevout_amount = static_cast(diff_prevout.GetLow64()); + CAmount coinbase_amount = static_cast(diff_coinbase.GetLow64()); + CAmount outputs_amount = static_cast(diff_outputs.GetLow64()); + block_info.pushKV("prevout_spent", ValueFromAmount(prevout_amount)); + block_info.pushKV("coinbase", ValueFromAmount(coinbase_amount)); + block_info.pushKV("new_outputs_ex_coinbase", ValueFromAmount(outputs_amount)); + block_info.pushKV("unspendable", ValueFromAmount(block_total_unspendable_amount - prev_block_total_unspendable_amount)); UniValue unspendables(UniValue::VOBJ); unspendables.pushKV("genesis_block", ValueFromAmount(stats.total_unspendables_genesis_block - prev_stats.total_unspendables_genesis_block)); diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py index a7b7e0c6760..b9d41a9713c 100755 --- a/test/functional/feature_init.py +++ b/test/functional/feature_init.py @@ -128,7 +128,7 @@ class InitTest(BitcoinTestFramework): 'startup_args': ['-txindex=1'], }, # Removing these files does not result in a startup error: - # 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstats/db/*.*', + # 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstatsindex/db/*.*', # 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK' ] @@ -154,7 +154,7 @@ class InitTest(BitcoinTestFramework): 'startup_args': ['-blockfilterindex=1'], }, { - 'filepath_glob': 'indexes/coinstats/db/*.*', + 'filepath_glob': 'indexes/coinstatsindex/db/*.*', 'error_message': 'LevelDB error: Corruption', 'startup_args': ['-coinstatsindex=1'], },