diff --git a/src/coins.cpp b/src/coins.cpp index fc33c521617c..8011bba3e788 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -43,6 +43,10 @@ CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) : CCoinsViewBacked(baseIn), m_deterministic(deterministic), cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource) { + // Favor cache density once the UTXO set no longer fits in memory. + // A higher load factor reduces bucket array overhead and allows caching more coins + // for a fixed -dbcache at the cost of slightly more work per lookup. + cacheCoins.max_load_factor(2.0f); m_sentinel.second.SelfRef(m_sentinel); } @@ -124,16 +128,26 @@ void CCoinsViewCache::EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coi void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool check_for_overwrite) { bool fCoinbase = tx.IsCoinBase(); const Txid& txid = tx.GetHash(); + // Reuse the outpoint to avoid copying the txid twice per output. + COutPoint outpoint{txid, 0}; for (size_t i = 0; i < tx.vout.size(); ++i) { - bool overwrite = check_for_overwrite ? cache.HaveCoin(COutPoint(txid, i)) : fCoinbase; + // Skip provably unspendable outputs early to avoid unnecessary cache work. + // AddCoin() will also check this, but doing it here avoids the overwrite + // lookup and Coin construction on common OP_RETURN outputs. + if (tx.vout[i].scriptPubKey.IsUnspendable()) continue; + outpoint.n = static_cast(i); + bool overwrite = check_for_overwrite ? cache.HaveCoin(outpoint) : fCoinbase; // Coinbase transactions can always be overwritten, in order to correctly // deal with the pre-BIP30 occurrences of duplicate coinbase transactions. - cache.AddCoin(COutPoint(txid, i), Coin(tx.vout[i], nHeight, fCoinbase), overwrite); + cache.AddCoin(outpoint, Coin(tx.vout[i], nHeight, fCoinbase), overwrite); } } bool CCoinsViewCache::SpendCoin(const COutPoint &outpoint, Coin* moveout) { - CCoinsMap::iterator it = FetchCoin(outpoint); + // SpendCoin is frequently called after an AccessCoin/HaveCoin on the same outpoint. + // Avoid the heavier try_emplace() path when the coin is already in the cache. + CCoinsMap::iterator it = cacheCoins.find(outpoint); + if (it == cacheCoins.end()) it = FetchCoin(outpoint); if (it == cacheCoins.end()) return false; cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); TRACEPOINT(utxocache, spent, diff --git a/src/coins.h b/src/coins.h index 4b39c0bacd28..03aec9239886 100644 --- a/src/coins.h +++ b/src/coins.h @@ -362,7 +362,9 @@ class CCoinsViewCache : public CCoinsViewBacked * declared as "const". */ mutable uint256 hashBlock; - mutable CCoinsMapMemoryResource m_cache_coins_memory_resource{}; + // Use larger pool chunks to reduce malloc/free churn when the cache grows large during + // reindex/IBD, while keeping the same total memory usage. + mutable CCoinsMapMemoryResource m_cache_coins_memory_resource{1 << 20}; /* The starting sentinel of the flagged entry circular doubly linked list. */ mutable CoinsCachePair m_sentinel; mutable CCoinsMap cacheCoins; diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 4efed70fd411..34701ecac68d 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -147,33 +147,57 @@ int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& i if (tx.IsCoinBase()) return nSigOps; - if (flags & SCRIPT_VERIFY_P2SH) { - nSigOps += GetP2SHSigOpCount(tx, inputs) * WITNESS_SCALE_FACTOR; - } - - for (unsigned int i = 0; i < tx.vin.size(); i++) - { + const bool fP2SH{static_cast(flags & SCRIPT_VERIFY_P2SH)}; + for (unsigned int i = 0; i < tx.vin.size(); i++) { const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout); assert(!coin.IsSpent()); - const CTxOut &prevout = coin.out; + const CTxOut& prevout{coin.out}; + if (fP2SH && prevout.scriptPubKey.IsPayToScriptHash()) { + nSigOps += prevout.scriptPubKey.GetSigOpCount(tx.vin[i].scriptSig) * WITNESS_SCALE_FACTOR; + } nSigOps += CountWitnessSigOps(tx.vin[i].scriptSig, prevout.scriptPubKey, tx.vin[i].scriptWitness, flags); } return nSigOps; } -bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee) +bool Consensus::CheckTxInputs(const CTransaction& tx, + TxValidationState& state, + const CCoinsViewCache& inputs, + int nSpendHeight, + CAmount& txfee, + std::vector* prev_heights, + script_verify_flags flags, + int64_t* tx_sigops_cost) { - // are the actual inputs available? - if (!inputs.HaveInputs(tx)) { - return state.Invalid(TxValidationResult::TX_MISSING_INPUTS, "bad-txns-inputs-missingorspent", - strprintf("%s: inputs missing/spent", __func__)); + if (prev_heights) assert(prev_heights->size() == tx.vin.size()); + + int64_t sigops_cost{0}; + if (tx_sigops_cost) { + // Compute sigops alongside input value checks to avoid re-walking the + // UTXO set (a major IBD bottleneck). + sigops_cost = GetLegacySigOpCount(tx) * WITNESS_SCALE_FACTOR; } CAmount nValueIn = 0; for (unsigned int i = 0; i < tx.vin.size(); ++i) { const COutPoint &prevout = tx.vin[i].prevout; const Coin& coin = inputs.AccessCoin(prevout); - assert(!coin.IsSpent()); + // AccessCoin() returns an empty coin for missing inputs, so checking + // IsSpent() avoids a redundant HaveInputs() pass. + if (coin.IsSpent()) { + return state.Invalid(TxValidationResult::TX_MISSING_INPUTS, "bad-txns-inputs-missingorspent", + strprintf("%s: inputs missing/spent", __func__)); + } + if (prev_heights) { + (*prev_heights)[i] = coin.nHeight; + } + if (tx_sigops_cost) { + const CTxOut& prev_tx_out{coin.out}; + if (flags & SCRIPT_VERIFY_P2SH && prev_tx_out.scriptPubKey.IsPayToScriptHash()) { + sigops_cost += prev_tx_out.scriptPubKey.GetSigOpCount(tx.vin[i].scriptSig) * WITNESS_SCALE_FACTOR; + } + sigops_cost += CountWitnessSigOps(tx.vin[i].scriptSig, prev_tx_out.scriptPubKey, tx.vin[i].scriptWitness, flags); + } // If prev is coinbase, check that it's matured if (coin.IsCoinBase() && nSpendHeight - coin.nHeight < COINBASE_MATURITY) { @@ -210,5 +234,6 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, } txfee = txfee_aux; + if (tx_sigops_cost) *tx_sigops_cost = sigops_cost; return true; } diff --git a/src/consensus/tx_verify.h b/src/consensus/tx_verify.h index ed44d435c1b9..49c4ce8cc4b4 100644 --- a/src/consensus/tx_verify.h +++ b/src/consensus/tx_verify.h @@ -23,9 +23,16 @@ namespace Consensus { * Check whether all inputs of this transaction are valid (no double spends and amounts) * This does not modify the UTXO set. This does not check scripts and sigs. * @param[out] txfee Set to the transaction fee if successful. + * @param[out] prev_heights If provided, it must be sized to tx.vin.size() and + * will be filled with the confirmation heights of the + * spent outputs (for use in BIP68 sequence locks). + * @param[out] tx_sigops_cost If provided, it will be filled with the sigops + * cost for this transaction (for use in block-level + * MAX_BLOCK_SIGOPS_COST checks). + * @param[in] flags Script verification flags used when calculating sigops cost. * Preconditions: tx.IsCoinBase() is false. */ -[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee); +[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee, std::vector* prev_heights = nullptr, script_verify_flags flags = script_verify_flags{}, int64_t* tx_sigops_cost = nullptr); } // namespace Consensus /** Auxiliary functions for transaction validation (ideally should not be exposed) */ diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index eb222078b5eb..bccd5d459fee 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -139,9 +139,18 @@ static void SetMaxOpenFiles(leveldb::Options *options) { static leveldb::Options GetOptions(size_t nCacheSize) { leveldb::Options options; - options.block_cache = leveldb::NewLRUCache(nCacheSize / 2); - options.write_buffer_size = nCacheSize / 4; // up to two write buffers may be held in memory simultaneously - options.filter_policy = leveldb::NewBloomFilterPolicy(10); + options.max_file_size = std::max(options.max_file_size, DBWRAPPER_MAX_FILE_SIZE); + // During reindex/IBD, the chainstate DB is write-heavy and compaction can become a major IO + // bottleneck once the UTXO working set no longer fits in memory. + // + // LevelDB uses write_buffer_size as the memtable size, which also affects the size of + // level-0 files produced by memtable flushes. Keep it bounded by the target table file size + // to avoid producing oversized level-0 files that can increase overlap and compaction work. + const size_t max_write_buffer{options.max_file_size}; + options.write_buffer_size = std::min(nCacheSize / 3, max_write_buffer); // up to two write buffers may be held in memory simultaneously + options.block_cache = leveldb::NewLRUCache(nCacheSize - options.write_buffer_size); + options.block_restart_interval = 4; + options.filter_policy = leveldb::NewBloomFilterPolicy(16); options.compression = leveldb::kNoCompression; options.info_log = new CBitcoinLevelDBLogger(); if (leveldb::kMajorVersion > 1 || (leveldb::kMajorVersion == 1 && leveldb::kMinorVersion >= 16)) { @@ -149,7 +158,6 @@ static leveldb::Options GetOptions(size_t nCacheSize) // on corruption in later versions. options.paranoid_checks = true; } - options.max_file_size = std::max(options.max_file_size, DBWRAPPER_MAX_FILE_SIZE); SetMaxOpenFiles(&options); return options; } @@ -302,33 +310,24 @@ size_t CDBWrapper::DynamicMemoryUsage() const return parsed.value(); } -std::optional CDBWrapper::ReadImpl(std::span key) const +bool CDBWrapper::ReadImpl(std::span key, std::string& value) const { leveldb::Slice slKey(CharCast(key.data()), key.size()); - std::string strValue; - leveldb::Status status = DBContext().pdb->Get(DBContext().readoptions, slKey, &strValue); + leveldb::Status status = DBContext().pdb->Get(DBContext().readoptions, slKey, &value); if (!status.ok()) { if (status.IsNotFound()) - return std::nullopt; + return false; LogError("LevelDB read failure: %s", status.ToString()); HandleError(status); } - return strValue; + return true; } bool CDBWrapper::ExistsImpl(std::span key) const { - leveldb::Slice slKey(CharCast(key.data()), key.size()); - - std::string strValue; - leveldb::Status status = DBContext().pdb->Get(DBContext().readoptions, slKey, &strValue); - if (!status.ok()) { - if (status.IsNotFound()) - return false; - LogError("LevelDB read failure: %s", status.ToString()); - HandleError(status); - } - return true; + std::string& value{ScratchValueString()}; + value.clear(); + return ReadImpl(key, value); } size_t CDBWrapper::EstimateSizeImpl(std::span key1, std::span key2) const diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 2eee6c1c023c..b87cf9d7cf9d 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -143,7 +143,8 @@ class CDBIterator void SeekToFirst(); template void Seek(const K& key) { - DataStream ssKey{}; + static thread_local DataStream ssKey{}; + ssKey.clear(); ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE); ssKey << key; SeekImpl(ssKey); @@ -153,7 +154,7 @@ class CDBIterator template bool GetKey(K& key) { try { - DataStream ssKey{GetKeyImpl()}; + SpanReader ssKey{GetKeyImpl()}; ssKey >> key; } catch (const std::exception&) { return false; @@ -163,8 +164,12 @@ class CDBIterator template bool GetValue(V& value) { try { - DataStream ssValue{GetValueImpl()}; - dbwrapper_private::GetObfuscation(parent)(ssValue); + static thread_local DataStream ssValue{}; + const auto sp_value{GetValueImpl()}; + ssValue.clear(); + ssValue.resize(sp_value.size()); + std::memcpy(ssValue.data(), sp_value.data(), sp_value.size()); + dbwrapper_private::GetObfuscation(parent)(std::span{ssValue.data(), ssValue.size()}); ssValue >> value; } catch (const std::exception&) { return false; @@ -191,11 +196,29 @@ class CDBWrapper //! obfuscation key storage key, null-prefixed to avoid collisions inline static const std::string OBFUSCATION_KEY{"\000obfuscate_key", 14}; // explicit size to avoid truncation at leading \0 - std::optional ReadImpl(std::span key) const; + bool ReadImpl(std::span key, std::string& value) const; bool ExistsImpl(std::span key) const; size_t EstimateSizeImpl(std::span key1, std::span key2) const; auto& DBContext() const LIFETIMEBOUND { return *Assert(m_db_context); } + static DataStream& ScratchKeyStream() noexcept + { + static thread_local DataStream ssKey{}; + return ssKey; + } + + static DataStream& ScratchKeyStream2() noexcept + { + static thread_local DataStream ssKey{}; + return ssKey; + } + + static std::string& ScratchValueString() noexcept + { + static thread_local std::string value; + return value; + } + public: CDBWrapper(const DBParams& params); ~CDBWrapper(); @@ -206,17 +229,19 @@ class CDBWrapper template bool Read(const K& key, V& value) const { - DataStream ssKey{}; + DataStream& ssKey{ScratchKeyStream()}; + ssKey.clear(); ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE); ssKey << key; - std::optional strValue{ReadImpl(ssKey)}; - if (!strValue) { + std::string& strValue{ScratchValueString()}; + strValue.clear(); + if (!ReadImpl(ssKey, strValue)) { return false; } try { - std::span ssValue{MakeWritableByteSpan(*strValue)}; - m_obfuscation(ssValue); - SpanReader{ssValue} >> value; + m_obfuscation(MakeWritableByteSpan(strValue)); + SpanReader ssValue{MakeByteSpan(strValue)}; + ssValue >> value; } catch (const std::exception&) { return false; } @@ -234,7 +259,8 @@ class CDBWrapper template bool Exists(const K& key) const { - DataStream ssKey{}; + DataStream& ssKey{ScratchKeyStream()}; + ssKey.clear(); ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE); ssKey << key; return ExistsImpl(ssKey); @@ -263,7 +289,10 @@ class CDBWrapper template size_t EstimateSize(const K& key_begin, const K& key_end) const { - DataStream ssKey1{}, ssKey2{}; + DataStream& ssKey1{ScratchKeyStream()}; + DataStream& ssKey2{ScratchKeyStream2()}; + ssKey1.clear(); + ssKey2.clear(); ssKey1.reserve(DBWRAPPER_PREALLOC_KEY_SIZE); ssKey2.reserve(DBWRAPPER_PREALLOC_KEY_SIZE); ssKey1 << key_begin; diff --git a/src/init.cpp b/src/init.cpp index e2cdb9c64772..d7682c5cf4d0 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1333,6 +1333,8 @@ static ChainstateLoadResult InitAndLoadChainstate( BlockManager::Options blockman_opts{ .chainparams = chainman_opts.chainparams, + .drop_os_cache = (do_reindex || do_reindex_chainstate) || + (args.IsArgSet("-connect") && args.GetArgs("-connect").size() == 1 && args.GetArgs("-connect")[0] == "0"), .blocks_dir = args.GetBlocksDirPath(), .notifications = chainman_opts.notifications, .block_tree_db_params = DBParams{ diff --git a/src/kernel/blockmanager_opts.h b/src/kernel/blockmanager_opts.h index 3d8af68b8087..44fac25a02d4 100644 --- a/src/kernel/blockmanager_opts.h +++ b/src/kernel/blockmanager_opts.h @@ -26,6 +26,8 @@ struct BlockManagerOpts { bool use_xor{DEFAULT_XOR_BLOCKSDIR}; uint64_t prune_target{0}; bool fast_prune{false}; + /** Hint to the OS that block/undo file pages can be dropped from cache after use. */ + bool drop_os_cache{false}; const fs::path blocks_dir; Notifications& notifications; DBParams block_tree_db_params; diff --git a/src/kernel/caches.h b/src/kernel/caches.h index aa3214e5df74..28d7aba82313 100644 --- a/src/kernel/caches.h +++ b/src/kernel/caches.h @@ -10,14 +10,14 @@ #include //! Suggested default amount of cache reserved for the kernel (bytes) -static constexpr size_t DEFAULT_KERNEL_CACHE{450_MiB}; +static constexpr size_t DEFAULT_KERNEL_CACHE{550_MiB}; //! Default LevelDB write batch size static constexpr size_t DEFAULT_DB_CACHE_BATCH{32_MiB}; //! Max memory allocated to block tree DB specific cache (bytes) -static constexpr size_t MAX_BLOCK_DB_CACHE{2_MiB}; +static constexpr size_t MAX_BLOCK_DB_CACHE{8_MiB}; //! Max memory allocated to coin DB specific cache (bytes) -static constexpr size_t MAX_COINS_DB_CACHE{8_MiB}; +static constexpr size_t MAX_COINS_DB_CACHE{512_MiB}; namespace kernel { struct CacheSizes { @@ -29,7 +29,10 @@ struct CacheSizes { { block_tree_db = std::min(total_cache / 8, MAX_BLOCK_DB_CACHE); total_cache -= block_tree_db; - coins_db = std::min(total_cache / 2, MAX_COINS_DB_CACHE); + // Prefer reserving most of the cache for the in-memory UTXO set, while still allowing + // the chainstate LevelDB cache (block cache + write buffers) to scale with -dbcache + // for IO-heavy startup/import/reindex scenarios. + coins_db = std::min(total_cache / 6, MAX_COINS_DB_CACHE); total_cache -= coins_db; coins = total_cache; // the rest goes to the coins cache } diff --git a/src/leveldb/include/leveldb/env.h b/src/leveldb/include/leveldb/env.h index 96c21b3966c4..bcd2c8137672 100644 --- a/src/leveldb/include/leveldb/env.h +++ b/src/leveldb/include/leveldb/env.h @@ -232,6 +232,11 @@ class LEVELDB_EXPORT RandomAccessFile { virtual ~RandomAccessFile(); + // Return true if this implementation may write into the caller-provided + // scratch buffer passed to Read(). Implementations backed by a stable mapping + // (e.g. mmap) can ignore scratch and return pointers into the mapping. + virtual bool RequiresScratch() const { return true; } + // Read up to "n" bytes from the file starting at "offset". // "scratch[0..n-1]" may be written by this routine. Sets "*result" // to the data that was read (including if fewer than "n" bytes were diff --git a/src/leveldb/table/format.cc b/src/leveldb/table/format.cc index a3d67de2e41d..4d88038a637a 100644 --- a/src/leveldb/table/format.cc +++ b/src/leveldb/table/format.cc @@ -70,7 +70,10 @@ Status ReadBlock(RandomAccessFile* file, const ReadOptions& options, // Read the block contents as well as the type/crc footer. // See table_builder.cc for the code that built this structure. size_t n = static_cast(handle.size()); - char* buf = new char[n + kBlockTrailerSize]; + char* buf = nullptr; + if (file->RequiresScratch()) { + buf = new char[n + kBlockTrailerSize]; + } Slice contents; Status s = file->Read(handle.offset(), n + kBlockTrailerSize, &contents, buf); if (!s.ok()) { diff --git a/src/leveldb/util/bloom.cc b/src/leveldb/util/bloom.cc index 87547a7e62cd..bef57848822c 100644 --- a/src/leveldb/util/bloom.cc +++ b/src/leveldb/util/bloom.cc @@ -35,6 +35,7 @@ class BloomFilterPolicy : public FilterPolicy { size_t bytes = (bits + 7) / 8; bits = bytes * 8; + const uint32_t bits32 = static_cast(bits); const size_t init_size = dst->size(); dst->resize(init_size + bytes, 0); @@ -46,7 +47,7 @@ class BloomFilterPolicy : public FilterPolicy { uint32_t h = BloomHash(keys[i]); const uint32_t delta = (h >> 17) | (h << 15); // Rotate right 17 bits for (size_t j = 0; j < k_; j++) { - const uint32_t bitpos = h % bits; + const uint32_t bitpos = h % bits32; array[bitpos / 8] |= (1 << (bitpos % 8)); h += delta; } @@ -58,7 +59,7 @@ class BloomFilterPolicy : public FilterPolicy { if (len < 2) return false; const char* array = bloom_filter.data(); - const size_t bits = (len - 1) * 8; + const uint32_t bits = static_cast((len - 1) * 8); // Use the encoded k so that we can read filters generated by // bloom filters created using different parameters. diff --git a/src/leveldb/util/env_posix.cc b/src/leveldb/util/env_posix.cc index 8a33534ed5dc..a352d6c9be9c 100644 --- a/src/leveldb/util/env_posix.cc +++ b/src/leveldb/util/env_posix.cc @@ -42,8 +42,10 @@ namespace { // Set by EnvPosixTestHelper::SetReadOnlyMMapLimit() and MaxOpenFiles(). int g_open_read_only_file_limit = -1; -// Up to 4096 mmap regions for 64-bit binaries; none for 32-bit. -constexpr const int kDefaultMmapLimit = (sizeof(void*) >= 8) ? 4096 : 0; +// Default to no read-only mmaps. For large, cache-unfriendly workloads (like +// chainstate lookups once the working set no longer fits in memory) this avoids +// page-fault overhead and can reduce memory pressure. +constexpr const int kDefaultMmapLimit = 0; // Can be set using EnvPosixTestHelper::SetReadOnlyMMapLimit(). int g_mmap_limit = kDefaultMmapLimit; @@ -159,6 +161,13 @@ class PosixRandomAccessFile final : public RandomAccessFile { if (!has_permanent_fd_) { assert(fd_ == -1); ::close(fd); // The file will be opened on every read. + } else { +#ifdef POSIX_FADV_RANDOM + // Best-effort: SSTable access is typically random. Hint to the kernel that + // readahead is unlikely to help for point lookups once the working set no + // longer fits in memory. + posix_fadvise(fd_, 0, 0, POSIX_FADV_RANDOM); +#endif } } @@ -232,6 +241,8 @@ class PosixMmapReadableFile final : public RandomAccessFile { mmap_limiter_->Release(); } + bool RequiresScratch() const override { return false; } + Status Read(uint64_t offset, size_t n, Slice* result, char* scratch) const override { if (offset + n > length_) { diff --git a/src/net_processing.cpp b/src/net_processing.cpp index e5b4bc7772df..5e57a431fddb 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -2061,6 +2061,12 @@ void PeerManagerImpl::BlockConnected( if (role.historical) { return; } + // During initial block download, transaction announcements are discarded and we + // do not request transactions. TxDownloadManager has no relevant state to + // maintain on new blocks. + if (m_chainman.IsInitialBlockDownload()) { + return; + } LOCK(m_tx_download_mutex); m_txdownloadman.BlockConnected(pblock); } diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 218ee222ef97..3b5bc9b7b48a 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -45,6 +45,9 @@ #include #include #include +#ifndef WIN32 +#include +#endif #include #include #include @@ -255,14 +258,17 @@ CBlockIndex* BlockManager::AddToBlockIndex(const CBlockHeader& block, CBlockInde return pindexNew; } -void BlockManager::PruneOneBlockFile(const int fileNumber) +void BlockManager::PruneBlockFiles(const std::set& file_numbers) { AssertLockHeld(cs_main); + if (file_numbers.empty()) return; LOCK(cs_LastBlockFile); for (auto& entry : m_block_index) { CBlockIndex* pindex = &entry.second; - if (pindex->nFile == fileNumber) { + if (!file_numbers.contains(pindex->nFile)) continue; + + { pindex->nStatus &= ~BLOCK_HAVE_DATA; pindex->nStatus &= ~BLOCK_HAVE_UNDO; pindex->nFile = 0; @@ -285,8 +291,15 @@ void BlockManager::PruneOneBlockFile(const int fileNumber) } } - m_blockfile_info.at(fileNumber) = CBlockFileInfo{}; - m_dirty_fileinfo.insert(fileNumber); + for (const int fileNumber : file_numbers) { + m_blockfile_info.at(fileNumber) = CBlockFileInfo{}; + m_dirty_fileinfo.insert(fileNumber); + } +} + +void BlockManager::PruneOneBlockFile(const int fileNumber) +{ + PruneBlockFiles({fileNumber}); } void BlockManager::FindFilesToPruneManual( @@ -310,10 +323,10 @@ void BlockManager::FindFilesToPruneManual( continue; } - PruneOneBlockFile(fileNumber); setFilesToPrune.insert(fileNumber); count++; } + PruneBlockFiles(setFilesToPrune); LogInfo("[%s] Prune (Manual): prune_height=%d removed %d blk/rev pairs", chain.GetRole(), last_block_can_prune, count); } @@ -385,12 +398,12 @@ void BlockManager::FindFilesToPrune( continue; } - PruneOneBlockFile(fileNumber); // Queue up the files for removal setFilesToPrune.insert(fileNumber); nCurrentUsage -= nBytesToPrune; count++; } + PruneBlockFiles(setFilesToPrune); } LogDebug(BCLog::PRUNE, "[%s] target=%dMiB actual=%dMiB diff=%dMiB min_height=%d max_prune_height=%d removed %d blk/rev pairs\n", @@ -970,6 +983,7 @@ bool BlockManager::WriteBlockUndo(const CBlockUndo& blockundo, BlockValidationSt LogError("FindUndoPos failed for %s while writing block undo", pos.ToString()); return false; } + [[maybe_unused]] const uint32_t undo_pos_start{pos.nPos}; // Open history file to append AutoFile file{OpenUndoFile(pos)}; @@ -986,13 +1000,27 @@ bool BlockManager::WriteBlockUndo(const CBlockUndo& blockundo, BlockValidationSt { // Calculate checksum HashWriter hasher{}; - hasher << block.pprev->GetBlockHash() << blockundo; - // Write undo data & checksum - fileout << blockundo << hasher.GetHash(); + hasher << block.pprev->GetBlockHash(); + + // Hash the exact bytes written to disk, to avoid serializing + // the undo data twice (once for hashing and once for writing). + TeeWriter hashing_out{fileout, hasher}; + hashing_out << blockundo; + // Write checksum + fileout << hasher.GetHash(); } // BufferedWriter will flush pending data to file when fileout goes out of scope. } + if (m_opts.drop_os_cache) { +#ifdef POSIX_FADV_DONTNEED + if (const int fd{file.GetFd()}; fd != -1) { + // Best-effort: avoid polluting the OS cache during bulk validation/reindex. + posix_fadvise(fd, undo_pos_start, blockundo_size + UNDO_DATA_DISK_OVERHEAD, POSIX_FADV_DONTNEED); + } +#endif + } + // Make sure that the file is closed before we call `FlushUndoFile`. if (file.fclose() != 0) { LogError("Failed to close block undo file %s: %s", pos.ToString(), SysErrorString(errno)); @@ -1029,15 +1057,66 @@ bool BlockManager::ReadBlock(CBlock& block, const FlatFilePos& pos, const std::o { block.SetNull(); - // Open history file to read - const auto block_data{ReadRawBlock(pos)}; - if (!block_data) { + if (pos.nPos < STORAGE_HEADER_BYTES) { + // If nPos is less than STORAGE_HEADER_BYTES, we can't read the header that precedes the block data + // This would cause an unsigned integer underflow when trying to position the file cursor + // This can happen after pruning or default constructed positions. + LogError("Failed for %s while reading block storage header", pos.ToString()); return false; } + // Avoid allocating a new buffer for every block read. This makes the hot + // ReadBlock path much cheaper during reindex/IBD without impacting semantics. + static thread_local std::vector raw_block; + + AutoFile filein{OpenBlockFile({pos.nFile, pos.nPos - STORAGE_HEADER_BYTES}, /*fReadOnly=*/true)}; + if (filein.IsNull()) { + LogError("OpenBlockFile failed for %s while reading block", pos.ToString()); + return false; + } + + MessageStartChars blk_start; + unsigned int blk_size; try { - // Read block - SpanReader{*block_data} >> TX_WITH_WITNESS(block); + filein >> blk_start >> blk_size; + } catch (const std::exception& e) { + LogError("Read from block file failed: %s for %s while reading block header", e.what(), pos.ToString()); + return false; + } + + if (blk_start != GetParams().MessageStart()) { + LogError("Block magic mismatch for %s: %s versus expected %s while reading block", + pos.ToString(), HexStr(blk_start), HexStr(GetParams().MessageStart())); + return false; + } + + if (blk_size > MAX_SIZE) { + LogError("Block data is larger than maximum deserialization size for %s: %s versus %s while reading block", + pos.ToString(), blk_size, MAX_SIZE); + return false; + } + + if (raw_block.size() < blk_size) raw_block.resize(blk_size); + const auto block_span{std::span{raw_block}.first(blk_size)}; + + try { + filein.read(block_span); + } catch (const std::exception& e) { + LogError("Read from block file failed: %s for %s while reading block data", e.what(), pos.ToString()); + return false; + } + + if (m_opts.drop_os_cache) { +#ifdef POSIX_FADV_DONTNEED + if (const int fd{filein.GetFd()}; fd != -1) { + // Best-effort: avoid polluting the OS cache during bulk validation/reindex. + posix_fadvise(fd, pos.nPos - STORAGE_HEADER_BYTES, STORAGE_HEADER_BYTES + blk_size, POSIX_FADV_DONTNEED); + } +#endif + } + + try { + SpanReader{block_span} >> TX_WITH_WITNESS(block); } catch (const std::exception& e) { LogError("Deserialize or I/O error - %s at %s while reading block", e.what(), pos.ToString()); return false; @@ -1116,6 +1195,15 @@ BlockManager::ReadRawBlockResult BlockManager::ReadRawBlock(const FlatFilePos& p std::vector data(blk_size); // Zeroing of memory is intentional here filein.read(data); + + if (m_opts.drop_os_cache && !block_part) { +#ifdef POSIX_FADV_DONTNEED + if (const int fd{filein.GetFd()}; fd != -1) { + // Best-effort: avoid polluting the OS cache during bulk validation/reindex. + posix_fadvise(fd, pos.nPos - STORAGE_HEADER_BYTES, STORAGE_HEADER_BYTES + blk_size, POSIX_FADV_DONTNEED); + } +#endif + } return data; } catch (const std::exception& e) { LogError("Read from block file failed: %s for %s while reading raw block", e.what(), pos.ToString()); @@ -1265,13 +1353,18 @@ void ImportBlocks(ChainstateManager& chainman, std::span import_ // parent hash -> child disk position, multiple children can have the same parent. std::multimap blocks_with_unknown_parent; + int last_reported_percent{-1}; for (int nFile{0}; nFile < total_files; ++nFile) { FlatFilePos pos(nFile, 0); AutoFile file{chainman.m_blockman.OpenBlockFile(pos, /*fReadOnly=*/true)}; if (file.IsNull()) { break; // This error is logged in OpenBlockFile } - LogInfo("Reindexing block file blk%05u.dat (%d%% complete)...", (unsigned int)nFile, nFile * 100 / total_files); + const int progress_percent{nFile * 100 / total_files}; + if (progress_percent != last_reported_percent || nFile == total_files - 1) { + LogInfo("Reindexing block file blk%05u.dat (%d%% complete)...", (unsigned int)nFile, progress_percent); + last_reported_percent = progress_percent; + } chainman.LoadExternalBlockFile(file, &pos, &blocks_with_unknown_parent); if (chainman.m_interrupt) { LogInfo("Interrupt requested. Exit reindexing."); diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 8c8a6d2c743f..f391a27d2aa8 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -372,6 +372,8 @@ class BlockManager //! Mark one block file as pruned (modify associated database entries) void PruneOneBlockFile(int fileNumber) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + //! Mark multiple block files as pruned, scanning the block index once. + void PruneBlockFiles(const std::set& file_numbers) EXCLUSIVE_LOCKS_REQUIRED(cs_main); CBlockIndex* LookupBlockIndex(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); const CBlockIndex* LookupBlockIndex(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_main); diff --git a/src/node/caches.cpp b/src/node/caches.cpp index ecff3c628369..91d472038d37 100644 --- a/src/node/caches.cpp +++ b/src/node/caches.cpp @@ -49,7 +49,9 @@ CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes) index_sizes.filter_index = max_cache / n_indexes; total_cache -= index_sizes.filter_index * n_indexes; } - return {index_sizes, kernel::CacheSizes{total_cache}}; + kernel::CacheSizes kernel_sizes{total_cache}; + + return {index_sizes, kernel_sizes}; } void LogOversizedDbCache(const ArgsManager& args) noexcept diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index 894dfdce8744..71f4ec495329 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -78,22 +78,121 @@ bool CTransaction::ComputeHasWitness() const }); } -Txid CTransaction::ComputeHash() const +namespace { +/** A writer stream (for serialization) that computes two 256-bit hashes in parallel. */ +class DualHashWriter { - return Txid::FromUint256((HashWriter{} << TX_NO_WITNESS(*this)).GetHash()); -} + CSHA256 m_ctx_base; + CSHA256 m_ctx_witness; + +public: + void write(std::span src) + { + const auto* p{UCharCast(src.data())}; + m_ctx_base.Write(p, src.size()); + m_ctx_witness.Write(p, src.size()); + } + + CSHA256& WitnessCtx() { return m_ctx_witness; } + + template + DualHashWriter& operator<<(const T& obj) + { + ::Serialize(*this, obj); + return *this; + } + + uint256 GetBaseHash() + { + uint256 result; + m_ctx_base.Finalize(result.begin()); + m_ctx_base.Reset().Write(result.begin(), CSHA256::OUTPUT_SIZE).Finalize(result.begin()); + return result; + } + + uint256 GetWitnessHash() + { + uint256 result; + m_ctx_witness.Finalize(result.begin()); + m_ctx_witness.Reset().Write(result.begin(), CSHA256::OUTPUT_SIZE).Finalize(result.begin()); + return result; + } +}; + +/** A writer stream that appends only to a provided CSHA256 context. */ +class HashWriterRef +{ + CSHA256& m_ctx; -Wtxid CTransaction::ComputeWitnessHash() const +public: + explicit HashWriterRef(CSHA256& ctx) : m_ctx{ctx} {} + + void write(std::span src) + { + const auto* p{UCharCast(src.data())}; + m_ctx.Write(p, src.size()); + } + + template + HashWriterRef& operator<<(const T& obj) + { + ::Serialize(*this, obj); + return *this; + } +}; +} // namespace + +CTransaction::Hashes CTransaction::ComputeHashes() const { - if (!HasWitness()) { - return Wtxid::FromUint256(hash.ToUint256()); + if (!m_has_witness) { + const Txid txid{Txid::FromUint256((HashWriter{} << TX_NO_WITNESS(*this)).GetHash())}; + return {txid, Wtxid::FromUint256(txid.ToUint256())}; + } + + DualHashWriter common; + HashWriterRef witness{common.WitnessCtx()}; + + // Common prefix. + common << version; + + // Segwit "extended" serialization prefix is part of the witness hash only. + unsigned char flags{1}; + std::vector vinDummy; + witness << vinDummy; + witness << flags; + + // Common body. + common << vin; + common << vout; + + // Witness data is part of the witness hash only. + for (const auto& in : vin) { + witness << in.scriptWitness.stack; } - return Wtxid::FromUint256((HashWriter{} << TX_WITH_WITNESS(*this)).GetHash()); + // Common suffix. + common << nLockTime; + + const Txid txid{Txid::FromUint256(common.GetBaseHash())}; + const Wtxid wtxid{Wtxid::FromUint256(common.GetWitnessHash())}; + return {txid, wtxid}; } -CTransaction::CTransaction(const CMutableTransaction& tx) : vin(tx.vin), vout(tx.vout), version{tx.version}, nLockTime{tx.nLockTime}, m_has_witness{ComputeHasWitness()}, hash{ComputeHash()}, m_witness_hash{ComputeWitnessHash()} {} -CTransaction::CTransaction(CMutableTransaction&& tx) : vin(std::move(tx.vin)), vout(std::move(tx.vout)), version{tx.version}, nLockTime{tx.nLockTime}, m_has_witness{ComputeHasWitness()}, hash{ComputeHash()}, m_witness_hash{ComputeWitnessHash()} {} +CTransaction::CTransaction(const CMutableTransaction& tx) : + vin(tx.vin), + vout(tx.vout), + version{tx.version}, + nLockTime{tx.nLockTime}, + m_has_witness{ComputeHasWitness()}, + m_hashes{ComputeHashes()} {} + +CTransaction::CTransaction(CMutableTransaction&& tx) : + vin(std::move(tx.vin)), + vout(std::move(tx.vout)), + version{tx.version}, + nLockTime{tx.nLockTime}, + m_has_witness{ComputeHasWitness()}, + m_hashes{ComputeHashes()} {} CAmount CTransaction::GetValueOut() const { diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 34bb9571c153..5a1a7592ab8c 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -296,11 +296,13 @@ class CTransaction private: /** Memory only. */ const bool m_has_witness; - const Txid hash; - const Wtxid m_witness_hash; + struct Hashes { + Txid txid; + Wtxid wtxid; + }; + const Hashes m_hashes; - Txid ComputeHash() const; - Wtxid ComputeWitnessHash() const; + Hashes ComputeHashes() const; bool ComputeHasWitness() const; @@ -325,8 +327,8 @@ class CTransaction return vin.empty() && vout.empty(); } - const Txid& GetHash() const LIFETIMEBOUND { return hash; } - const Wtxid& GetWitnessHash() const LIFETIMEBOUND { return m_witness_hash; }; + const Txid& GetHash() const LIFETIMEBOUND { return m_hashes.txid; } + const Wtxid& GetWitnessHash() const LIFETIMEBOUND { return m_hashes.wtxid; }; // Return sum of txouts. CAmount GetValueOut() const; diff --git a/src/streams.h b/src/streams.h index f70adcf74a71..b26f05ce4ee1 100644 --- a/src/streams.h +++ b/src/streams.h @@ -425,6 +425,16 @@ class AutoFile */ bool IsNull() const { return m_file == nullptr; } + /** Return the file descriptor for the wrapped FILE* where available, or -1 otherwise. */ + int GetFd() const + { +#ifndef WIN32 + return m_file ? ::fileno(m_file) : -1; +#else + return -1; +#endif + } + /** Continue with a different XOR key */ void SetObfuscation(const Obfuscation& obfuscation) { m_obfuscation = obfuscation; } @@ -559,6 +569,22 @@ class BufferedFile while (m_read_pos < file_pos) AdvanceStream(file_pos - m_read_pos); } + //! Move to file_pos without reading intervening bytes. This discards rewind history before file_pos. + void FastSkipNoRewind(const uint64_t file_pos) + { + assert(file_pos >= m_read_pos); + if (file_pos > nReadLimit) { + throw std::ios_base::failure("Attempt to position past buffer limit"); + } + if (file_pos <= nSrcPos) { + m_read_pos = file_pos; + return; + } + m_src.seek(file_pos, SEEK_SET); + m_read_pos = file_pos; + nSrcPos = file_pos; + } + //! return the current reading position uint64_t GetPos() const { return m_read_pos; @@ -703,4 +729,28 @@ class BufferedWriter } }; +/** Writer stream (for serialization) that forwards all written bytes to two underlying writers. */ +template +class TeeWriter +{ + A& m_a; + B& m_b; + +public: + TeeWriter(A& a LIFETIMEBOUND, B& b LIFETIMEBOUND) : m_a{a}, m_b{b} {} + + void write(std::span src) + { + m_a.write(src); + m_b.write(src); + } + + template + TeeWriter& operator<<(const T& obj) + { + ::Serialize(*this, obj); + return *this; + } +}; + #endif // BITCOIN_STREAMS_H diff --git a/src/test/caches_tests.cpp b/src/test/caches_tests.cpp index f444f1be2397..ad1e264a2207 100644 --- a/src/test/caches_tests.cpp +++ b/src/test/caches_tests.cpp @@ -13,10 +13,10 @@ BOOST_AUTO_TEST_SUITE(caches_tests) BOOST_AUTO_TEST_CASE(oversized_dbcache_warning) { - // memory restricted setup - cap is DEFAULT_DB_CACHE (450 MiB) + // memory restricted setup - cap is DEFAULT_DB_CACHE (550 MiB) BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/4_MiB, /*total_ram=*/1024_MiB)); // Under cap - BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/512_MiB, /*total_ram=*/1024_MiB)); // At cap - BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/1500_MiB, /*total_ram=*/1024_MiB)); // Over cap + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/550_MiB, /*total_ram=*/1024_MiB)); // At cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/600_MiB, /*total_ram=*/1024_MiB)); // Over cap // 2 GiB RAM - cap is 75% BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/1500_MiB, /*total_ram=*/2048_MiB)); // Under cap diff --git a/src/test/validation_flush_tests.cpp b/src/test/validation_flush_tests.cpp index 66c284b97914..dc182059fa6c 100644 --- a/src/test/validation_flush_tests.cpp +++ b/src/test/validation_flush_tests.cpp @@ -10,6 +10,17 @@ #include +namespace { +void AddLargeTestCoin(FastRandomContext& rng, CCoinsViewCache& view) +{ + CScript script; + script.resize(8 << 10); + for (auto& byte : script) byte = OP_TRUE; + const COutPoint outpoint{Txid::FromUint256(rng.rand256()), rng.rand32()}; + view.AddCoin(outpoint, Coin{CTxOut{1, std::move(script)}, /*nHeight=*/1, /*fCoinBase=*/false}, /*possible_overwrite=*/false); +} +} // namespace + BOOST_FIXTURE_TEST_SUITE(validation_flush_tests, TestingSetup) //! Verify that Chainstate::GetCoinsCacheSizeState() switches from OK→LARGE→CRITICAL @@ -22,12 +33,14 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate) LOCK(::cs_main); CCoinsViewCache& view{chainstate.CoinsTip()}; - // Sanity: an empty cache should be ≲ 1 chunk (~ 256 KiB). - BOOST_CHECK_LT(view.DynamicMemoryUsage() / (256 * 1024.0), 1.1); + // Sanity: an empty cache should remain close to one pool chunk. + // With 1 MiB pool chunks this is ~4 x 256 KiB. + BOOST_CHECK_LT(view.DynamicMemoryUsage() / (256 * 1024.0), 4.5); constexpr size_t MAX_COINS_BYTES{8_MiB}; constexpr size_t MAX_MEMPOOL_BYTES{4_MiB}; - constexpr size_t MAX_ATTEMPTS{50'000}; + constexpr size_t MAX_ATTEMPTS_TO_LARGE{50'000}; + constexpr size_t MAX_ATTEMPTS_TO_CRITICAL{20'000}; // Run the same growth-path twice: first with 0 head-room, then with extra head-room for (size_t max_mempool_size_bytes : {size_t{0}, MAX_MEMPOOL_BYTES}) { @@ -36,16 +49,19 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate) // OK → LARGE auto state{chainstate.GetCoinsCacheSizeState(MAX_COINS_BYTES, max_mempool_size_bytes)}; - for (size_t i{0}; i < MAX_ATTEMPTS && int64_t(view.DynamicMemoryUsage()) <= large_cap; ++i) { + for (size_t i{0}; i < MAX_ATTEMPTS_TO_LARGE && int64_t(view.DynamicMemoryUsage()) <= large_cap; ++i) { BOOST_CHECK_EQUAL(state, CoinsCacheSizeState::OK); AddTestCoin(m_rng, view); state = chainstate.GetCoinsCacheSizeState(MAX_COINS_BYTES, max_mempool_size_bytes); } // LARGE → CRITICAL - for (size_t i{0}; i < MAX_ATTEMPTS && int64_t(view.DynamicMemoryUsage()) <= full_cap; ++i) { + // + // With an explicit overshoot allowance before CRITICAL, use larger + // synthetic coins so this transition remains fast in tests. + for (size_t i{0}; i < MAX_ATTEMPTS_TO_CRITICAL && state != CoinsCacheSizeState::CRITICAL; ++i) { BOOST_CHECK_EQUAL(state, CoinsCacheSizeState::LARGE); - AddTestCoin(m_rng, view); + AddLargeTestCoin(m_rng, view); state = chainstate.GetCoinsCacheSizeState(MAX_COINS_BYTES, max_mempool_size_bytes); } BOOST_CHECK_EQUAL(state, CoinsCacheSizeState::CRITICAL); diff --git a/src/util/hasher.h b/src/util/hasher.h index 02c77033918b..a2f8c435d389 100644 --- a/src/util/hasher.h +++ b/src/util/hasher.h @@ -62,11 +62,12 @@ class SaltedOutpointHasher SaltedOutpointHasher(bool deterministic = false); /** - * Having the hash noexcept allows libstdc++'s unordered_map to recalculate - * the hash during rehash, so it does not have to cache the value. This - * reduces node's memory by sizeof(size_t). The required recalculation has - * a slight performance penalty (around 1.6%), but this is compensated by - * memory savings of about 9% which allow for a larger dbcache setting. + * Marking the hash functor noexcept allows libstdc++ to recalculate the + * hash during rehash rather than caching it in unordered_map nodes. + * + * This saves sizeof(size_t) per node, which is significant for large UTXO + * caches and can reduce coins DB lookups when the UTXO set does not fit in + * memory. * * @see https://gcc.gnu.org/onlinedocs/gcc-13.2.0/libstdc++/manual/manual/unordered_associative.html */ diff --git a/src/validation.cpp b/src/validation.cpp index 1285edc261b5..4703711263a3 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2505,15 +2505,16 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, CBlockUndo blockundo; - // Precomputed transaction data pointers must not be invalidated - // until after `control` has run the script checks (potentially - // in multiple threads). Preallocate the vector size so a new allocation - // doesn't invalidate pointers into the vector, and keep txsdata in scope - // for as long as `control`. + // Precomputed transaction data pointers must not be invalidated until after + // `control` has run the script checks (potentially in multiple threads). + // Only allocate the per-tx data when script checks are enabled to avoid + // wasting time initializing it during IBD when script verification is + // skipped (assumevalid). std::optional> control; if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && fScriptChecks) control.emplace(queue); - std::vector txsdata(block.vtx.size()); + std::vector txsdata; + if (fScriptChecks) txsdata.resize(block.vtx.size()); std::vector prevheights; CAmount nFees = 0; @@ -2527,11 +2528,13 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, nInputs += tx.vin.size(); + int64_t tx_sigops_cost{0}; if (!tx.IsCoinBase()) { + prevheights.resize(tx.vin.size()); CAmount txfee = 0; TxValidationState tx_state; - if (!Consensus::CheckTxInputs(tx, tx_state, view, pindex->nHeight, txfee)) { + if (!Consensus::CheckTxInputs(tx, tx_state, view, pindex->nHeight, txfee, &prevheights, flags, &tx_sigops_cost)) { // Any transaction validation failure in ConnectBlock is a block consensus failure state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(), @@ -2548,23 +2551,17 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state, // Check that transaction is BIP68 final // BIP68 lock checks (as opposed to nLockTime checks) must // be in ConnectBlock because they require the UTXO set - prevheights.resize(tx.vin.size()); - for (size_t j = 0; j < tx.vin.size(); j++) { - prevheights[j] = view.AccessCoin(tx.vin[j].prevout).nHeight; - } - if (!SequenceLocks(tx, nLockTimeFlags, prevheights, *pindex)) { state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-txns-nonfinal", "contains a non-BIP68-final transaction " + tx.GetHash().ToString()); break; } + } else { + // Coinbase: sigops are limited to legacy sigops. + tx_sigops_cost = GetLegacySigOpCount(tx) * WITNESS_SCALE_FACTOR; } - // GetTransactionSigOpCost counts 3 types of sigops: - // * legacy (always) - // * p2sh (when P2SH enabled in flags and excludes coinbase) - // * witness (when witness enabled in flags and excludes coinbase) - nSigOpsCost += GetTransactionSigOpCost(tx, view, flags); + nSigOpsCost += tx_sigops_cost; if (nSigOpsCost > MAX_BLOCK_SIGOPS_COST) { state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-blk-sigops", "too many sigops"); break; @@ -2689,7 +2686,18 @@ CoinsCacheSizeState Chainstate::GetCoinsCacheSizeState( int64_t nTotalSpace = max_coins_cache_size_bytes + std::max(int64_t(max_mempool_size_bytes) - nMempoolUsage, 0); - if (cacheSize > nTotalSpace) { + // Allow a small amount of overshoot above the configured cache limit. + // + // The coins cache allocates memory in chunks (e.g. unordered_map bucket growth), + // so it can briefly exceed the target size by a small amount. Treating these + // tiny overshoots as CRITICAL can wipe the entire cache and cause long IO-bound + // periods while it warms up again. + // `cacheCoins` is an `unordered_map` and can grow in sudden steps when it + // rehashes (bucket array growth). Allow a small fixed overshoot so we don't + // fall into the CRITICAL->wipe path due to a modest rehash. + static constexpr int64_t COINS_CACHE_CRITICAL_OVERSHOOT{64 << 20}; // 64 MiB + + if (cacheSize > nTotalSpace + COINS_CACHE_CRITICAL_OVERSHOOT) { LogInfo("Cache size (%s) exceeds total space (%s)\n", cacheSize, nTotalSpace); return CoinsCacheSizeState::CRITICAL; } else if (cacheSize > LargeCoinsCacheThreshold(nTotalSpace)) { @@ -2765,9 +2773,17 @@ bool Chainstate::FlushStateToDisk( bool fCacheCritical = mode == FlushStateMode::IF_NEEDED && cache_state >= CoinsCacheSizeState::CRITICAL; // It's been a while since we wrote the block index and chain state to disk. Do this frequently, so we don't need to redownload or reindex after a crash. bool fPeriodicWrite = mode == FlushStateMode::PERIODIC && nNow >= m_next_write; - const auto empty_cache{(mode == FlushStateMode::FORCE_FLUSH) || fCacheLarge || fCacheCritical}; + // Only empty the coins cache when forced or when over the configured limit. In IBD + // and reindex-chainstate, wiping a large cache can cause extended IO-bound periods + // due to cold UTXO lookups. + const auto empty_cache{(mode == FlushStateMode::FORCE_FLUSH) || fCacheCritical}; // Combine all conditions that result in a write to disk. bool should_write = (mode == FlushStateMode::FORCE_SYNC) || empty_cache || fPeriodicWrite || fFlushForPrune; + // The coins database write is the most expensive part of a flush during IBD. + // Avoid writing coins in prune-only flushes during IBD; the chainstate will + // still be flushed on shutdown and on periodic/cache-pressure triggers. + const bool should_write_coins{(mode == FlushStateMode::FORCE_SYNC) || empty_cache || fPeriodicWrite || + (!m_chainman.IsInitialBlockDownload() && fFlushForPrune)}; // Write blocks, block index and best chain related state to disk. if (should_write) { LogDebug(BCLog::COINDB, "Writing chainstate to disk: flush mode=%s, prune=%d, large=%d, critical=%d, periodic=%d", @@ -2801,7 +2817,7 @@ bool Chainstate::FlushStateToDisk( m_blockman.UnlinkPrunedFiles(setFilesToPrune); } - if (!CoinsTip().GetBestBlock().IsNull()) { + if (should_write_coins && !CoinsTip().GetBestBlock().IsNull()) { if (coins_mem_usage >= WARN_FLUSH_COINS_SIZE) LogWarning("Flushing large (%d GiB) UTXO set to disk, it may take several minutes", coins_mem_usage >> 30); LOG_TIME_MILLIS_WITH_CATEGORY(strprintf("write coins cache to disk (%d coins, %.2fKiB)", coins_count, coins_mem_usage >> 10), BCLog::BENCH); @@ -2826,7 +2842,7 @@ bool Chainstate::FlushStateToDisk( } } - if (should_write || m_next_write == NodeClock::time_point::max()) { + if (should_write_coins || m_next_write == NodeClock::time_point::max()) { constexpr auto range{DATABASE_WRITE_INTERVAL_MAX - DATABASE_WRITE_INTERVAL_MIN}; m_next_write = FastRandomContext().rand_uniform_delay(NodeClock::now() + DATABASE_WRITE_INTERVAL_MIN, range); } @@ -5043,10 +5059,7 @@ void ChainstateManager::LoadExternalBlockFile( CBlockHeader header; blkdat >> header; const uint256 hash{header.GetHash()}; - // Skip the rest of this block (this may read from disk into memory); position to the marker before the - // next block, but it's still possible to rewind to the start of the current block (without a disk read). - nRewind = nBlockPos + nSize; - blkdat.SkipTo(nRewind); + const uint64_t nBlockEndPos{nBlockPos + nSize}; std::shared_ptr pblock{}; // needs to remain available after the cs_main lock is released to avoid duplicate reads from disk @@ -5059,12 +5072,17 @@ void ChainstateManager::LoadExternalBlockFile( if (dbp && blocks_with_unknown_parent) { blocks_with_unknown_parent->emplace(header.hashPrevBlock, *dbp); } + nRewind = nBlockEndPos; + blkdat.FastSkipNoRewind(nRewind); continue; } // process in case the block isn't known yet const CBlockIndex* pindex = m_blockman.LookupBlockIndex(hash); if (!pindex || (pindex->nStatus & BLOCK_HAVE_DATA) == 0) { + // Skip to the block end first so we can rewind and deserialize without another disk read. + nRewind = nBlockEndPos; + blkdat.SkipTo(nRewind); // This block can be processed immediately; rewind to its start, read and deserialize it. blkdat.SetPos(nBlockPos); pblock = std::make_shared(); @@ -5079,7 +5097,13 @@ void ChainstateManager::LoadExternalBlockFile( break; } } else if (hash != params.GetConsensus().hashGenesisBlock && pindex->nHeight % 1000 == 0) { + // For known blocks, seek over payload bytes without reading and deobfuscating them. + nRewind = nBlockEndPos; + blkdat.FastSkipNoRewind(nRewind); LogDebug(BCLog::REINDEX, "Block Import: already had block %s at height %d\n", hash.ToString(), pindex->nHeight); + } else { + nRewind = nBlockEndPos; + blkdat.FastSkipNoRewind(nRewind); } } @@ -5158,7 +5182,12 @@ void ChainstateManager::LoadExternalBlockFile( } catch (const std::runtime_error& e) { GetNotifications().fatalError(strprintf(_("System error while loading external block file: %s"), e.what())); } - LogInfo("Loaded %i blocks from external file in %dms", nLoaded, Ticks(SteadyClock::now() - start)); + const auto elapsed_ms{Ticks(SteadyClock::now() - start)}; + if (nLoaded > 0 || elapsed_ms >= 1000) { + LogInfo("Loaded %i blocks from external file in %dms", nLoaded, elapsed_ms); + } else { + LogDebug(BCLog::REINDEX, "Loaded %i blocks from external file in %dms", nLoaded, elapsed_ms); + } } bool ChainstateManager::ShouldCheckBlockIndex() const