From b75efa19ba806fbd13604a6bf67ee96a3f9eccf1 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 15 Jul 2026 19:30:13 -0400 Subject: [PATCH] txindex: skip bloom filters and legacy lookups for new databases New databases never contain legacy ('t' + txid) entries. Peek at the database before opening it, and if no legacy entries are found, skip building bloom filters (hashed entries are only read via iterators, which do not consult them) and return early from lookups instead of checking for legacy entries. --- src/dbwrapper.cpp | 24 ++++++++++++++++++++++++ src/dbwrapper.h | 5 +++++ src/index/txindex.cpp | 33 +++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index ed914273634..7406cecca92 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -153,6 +153,30 @@ static leveldb::Options GetOptions(size_t nCacheSize, bool bloom_filter) return options; } +bool CDBWrapper::HasKeyStartingWith(const fs::path& path, uint8_t prefix) +{ + if (!fs::exists(path / "CURRENT")) return false; + + CBitcoinLevelDBLogger logger; + leveldb::Options options; + options.paranoid_checks = true; + // Avoid creating or rotating LevelDB's LOG files during this probe. + options.info_log = &logger; + + leveldb::DB* raw_db; + HandleError(leveldb::DB::Open(options, fs::PathToString(path), &raw_db)); + const std::unique_ptr db{raw_db}; + + leveldb::ReadOptions iteroptions; + iteroptions.verify_checksums = true; + iteroptions.fill_cache = false; + const std::unique_ptr it{db->NewIterator(iteroptions)}; + const leveldb::Slice prefix_slice{reinterpret_cast(&prefix), sizeof(prefix)}; + it->Seek(prefix_slice); + HandleError(it->status()); + return it->Valid() && it->key().starts_with(prefix_slice); +} + struct CDBBatch::WriteBatchImpl { leveldb::WriteBatch batch; }; diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 1eb68a3bb1f..f6aaa6679f7 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -279,6 +279,11 @@ public: */ bool IsEmpty(); + //! Probe an unopened database for a key prefix. Return true if a database at + //! path exists and contains at least 1 entry beginning with prefix; missing + //! or empty databases return false, and database errors throw dbwrapper_error. + static bool HasKeyStartingWith(const fs::path& path, uint8_t prefix); + template size_t EstimateSize(const K& key_begin, const K& key_end) const { diff --git a/src/index/txindex.cpp b/src/index/txindex.cpp index 7babf5adb87..53c086d2645 100644 --- a/src/index/txindex.cpp +++ b/src/index/txindex.cpp @@ -65,13 +65,32 @@ public: /// Used to hash the txid to compute the prefix. const SipHasher13UJ m_hasher; + /// Whether the database contains any legacy ('t' + txid) entries. + const bool m_has_legacy; + CBlockLocator ReadBestBlock() const override; void WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator) override; + +private: + DB(size_t n_cache_size, bool f_memory, bool f_wipe, bool has_legacy); }; +static fs::path TxIndexDBPath() { return gArgs.GetDataDirNet() / "indexes" / "txindex"; } + TxIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe) : - BaseIndex::DB(gArgs.GetDataDirNet() / "indexes" / "txindex", n_cache_size, f_memory, f_wipe), - m_hasher{ReadOrCreateTxidHasher(*this)} + // Bloom filters are built for every key but only consulted by point reads, + // which iterators bypass: the per-tx hashed ('x') lookups seek with an + // iterator, and the 's'/'h' point reads are at most one per block against a + // tiny keyspace. Only the legacy entries' per-tx point lookups benefit, so + // enable the filters only for databases still containing them. + DB(n_cache_size, f_memory, f_wipe, + /*has_legacy=*/!f_memory && !f_wipe && CDBWrapper::HasKeyStartingWith(TxIndexDBPath(), txindex::DB_TXINDEX)) +{} + +TxIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe, bool has_legacy) : + BaseIndex::DB(TxIndexDBPath(), n_cache_size, f_memory, f_wipe, /*f_obfuscate=*/false, /*f_bloom=*/has_legacy), + m_hasher{ReadOrCreateTxidHasher(*this)}, + m_has_legacy{has_legacy} {} CBlockLocator TxIndex::DB::ReadBestBlock() const @@ -115,7 +134,13 @@ void TxIndex::DB::WriteTxs(const interfaces::BlockInfo& block) TxIndex::TxIndex(std::unique_ptr chain, size_t n_cache_size, bool f_memory, bool f_wipe) : BaseIndex(std::move(chain), "txindex", "txidx"), m_db(std::make_unique(n_cache_size, f_memory, f_wipe)) -{} +{ + if (m_db->m_has_legacy) { + LogInfo("txindex contains entries in the legacy format, which uses excessive disk space. " + "To reclaim disk space, stop the node, delete %s and restart to rebuild the index.", + fs::PathToString(TxIndexDBPath())); + } +} TxIndex::~TxIndex() = default; @@ -189,7 +214,7 @@ std::optional TxIndex::FindTx(const Txid& tx_hash) const } // Fall back to legacy if no hashed entry matched. This makes misses pay an // extra lookup, but keeps existing full-txid entries readable after upgrade. - return FindLegacyTx(tx_hash); + return m_db->m_has_legacy ? FindLegacyTx(tx_hash) : std::nullopt; } std::optional TxIndex::FindLegacyTx(const Txid& tx_hash) const