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