Files
bitcoin/src/dbwrapper.h
merge-script c90c23d388 Merge bitcoin/bitcoin#35531: txindex: hash keys and pack positions to reduce disk usage
25bed560be test: add forward-compat functional test for txindex (sedited)
703304ed8c doc: add release notes for txindex disk usage and downgrading (Andrew Toth)
8e5320a2d2 tests: cover txindex hash prefix collisions and legacy fallback (Andrew Toth)
b75efa19ba txindex: skip bloom filters and legacy lookups for new databases (Andrew Toth)
004d7c098c txindex: hash key prefixes and pack block positions (Andrew Toth)
5a255970fd refactor: move txindex db constants and legacy key to txindex_key.h (Andrew Toth)
327660134c txindex: pass the full block to DB::WriteTxs (Andrew Toth)
42771e7998 txindex: use a new block locator for downgrade safety (Andrew Toth)
4b08baed72 txindex: return optional tx and block hash from FindTx (Andrew Toth)

Pull request description:

  The current txindex uses the full 32-byte txid as keys, which takes up about 66 GB of disk space today on mainnet. Using a 5-byte key prefix instead drops the disk usage to 26 GB - cutting the size to less than half.

  Using the full 32-bytes is unnecessary since a 5-byte salted siphash will produce collisions in about 1 in 1.1 trillion. Some collisions will occur, but the penalty is just an extra disk read, deserialization and hash.
  The tx position can be appended to the key instead of used as a value, and a LevelDB iterator can seek to the prefix and then scan for the correct tx. This is an almost identical approach to `txospenderindex`.

  Also instead of storing the file position of the block, we can store only the sequence of the connected block and offset of the transaction in the block. This can be packed into a 6-byte key suffix using 3-byte representations of the sequence and offset in the block. The block file can be recovered by the CBlockIndex that is already in memory. The sequence is mapped to the block hash in the db, so we can lookup the block hash to find the CBlockIndex during reads.

  If a tx is not found with this method, we fallback to looking up the legacy entry. With this method a user with an existing db can opt to erase the `indexes/txindex` folder and reindex, or keep the current index and new entries will be appended with the smaller footprint.

  The time to index was faster on my machine with this method, 1h19m vs current 1h50m.
  Lookups are roughly the same, around 0.2ms per lookup with `getrawtransaction`.
  When testing on mainnet, I got 894,549 2-way collisions, 395 3-way collision, and 1 4-way collision that worst case could cause an extra 3 false positives when reading.

ACKs for top commit:
  l0rinc:
    diff reACK 25bed560be
  sedited:
    ACK 25bed560be
  ajtowns:
    ACK 25bed560be

Tree-SHA512: a25c79ca7e722e2f372b65f5fc11c8b194ad49f2240b4881c7e606306aabbd3604aede3f1c33606b467486affac3a3f503638f513c896935cebbc02709cb60d8
2026-08-15 15:20:47 +01:00

300 lines
8.4 KiB
C++

// Copyright (c) 2012-present 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_DBWRAPPER_H
#define BITCOIN_DBWRAPPER_H
#include <attributes.h>
#include <serialize.h>
#include <span.h>
#include <streams.h>
#include <util/byte_units.h>
#include <util/check.h>
#include <util/fs.h>
#include <util/obfuscation.h>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
namespace leveldb {
class Env;
} // namespace leveldb
inline constexpr size_t DBWRAPPER_PREALLOC_KEY_SIZE = 64;
inline constexpr size_t DBWRAPPER_PREALLOC_VALUE_SIZE = 1024;
inline constexpr size_t DBWRAPPER_MAX_FILE_SIZE{32_MiB};
//! User-controlled performance and debug options.
struct DBOptions {
//! Compact database on startup.
bool force_compact = false;
};
//! Application-specific storage settings.
struct DBParams {
//! Location in the filesystem where leveldb data will be stored.
fs::path path;
//! Configures various leveldb cache settings.
uint64_t cache_bytes;
//! If true, use leveldb's memory environment.
bool memory_only = false;
//! If true, remove all existing data.
bool wipe_data = false;
//! If true, store data obfuscated via simple XOR. If false, XOR with a
//! zero'd byte array.
bool obfuscate = false;
//! If true, build a LevelDB bloom filter to accelerate point lookups.
bool bloom_filter = true;
//! Passed-through options.
DBOptions options{};
//! If non-null, use this as the leveldb::Env instead of the default.
//! Caller retains ownership.
leveldb::Env* testing_env = nullptr;
//! Maximum LevelDB SST file size. Larger values reduce the frequency
//! of compactions but increase their duration.
size_t max_file_size = DBWRAPPER_MAX_FILE_SIZE;
};
class dbwrapper_error : public std::runtime_error
{
public:
explicit dbwrapper_error(const std::string& msg) : std::runtime_error(msg) {}
};
class CDBWrapper;
/** These should be considered an implementation detail of the specific database.
*/
namespace dbwrapper_private {
/** Work around circular dependency, as well as for testing in dbwrapper_tests.
* Database obfuscation should be considered an implementation detail of the
* specific database.
*/
const Obfuscation& GetObfuscation(const CDBWrapper&);
}; // namespace dbwrapper_private
bool DestroyDB(const std::string& path_str);
/** Batch of changes queued to be written to a CDBWrapper */
class CDBBatch
{
friend class CDBWrapper;
private:
const CDBWrapper &parent;
struct WriteBatchImpl;
const std::unique_ptr<WriteBatchImpl> m_impl_batch;
DataStream m_key_scratch{};
DataStream m_value_scratch{};
void WriteImpl(std::span<const std::byte> key, DataStream& value);
void EraseImpl(std::span<const std::byte> key);
public:
/**
* @param[in] _parent CDBWrapper that this batch is to be submitted to
*/
explicit CDBBatch(const CDBWrapper& _parent);
~CDBBatch();
void Clear();
template <typename K, typename V>
void Write(const K& key, const V& value)
{
ScopedDataStreamUsage scoped_key{m_key_scratch}, scoped_value{m_value_scratch};
m_key_scratch << key;
m_value_scratch << value;
WriteImpl(m_key_scratch, m_value_scratch);
}
template <typename K>
void Erase(const K& key)
{
ScopedDataStreamUsage scoped_key{m_key_scratch};
m_key_scratch << key;
EraseImpl(m_key_scratch);
}
size_t ApproximateSize() const;
};
class CDBIterator
{
public:
struct IteratorImpl;
private:
const CDBWrapper &parent;
const std::unique_ptr<IteratorImpl> m_impl_iter;
DataStream m_scratch{};
void SeekImpl(std::span<const std::byte> key);
std::span<const std::byte> GetKeyImpl() const;
std::span<const std::byte> GetValueImpl() const;
public:
/**
* @param[in] _parent Parent CDBWrapper instance.
* @param[in] _piter The original leveldb iterator.
*/
CDBIterator(const CDBWrapper& _parent, std::unique_ptr<IteratorImpl> _piter);
~CDBIterator();
bool Valid() const;
void SeekToFirst();
template<typename K> void Seek(const K& key) {
ScopedDataStreamUsage scoped_scratch{m_scratch};
m_scratch << key;
SeekImpl(m_scratch);
}
void Next();
template<typename K> bool GetKey(K& key) {
try {
SpanReader ssKey{GetKeyImpl()};
ssKey >> key;
} catch (const std::exception&) {
return false;
}
return true;
}
template<typename V> bool GetValue(V& value) {
try {
ScopedDataStreamUsage scoped_scratch{m_scratch};
m_scratch.write(GetValueImpl());
dbwrapper_private::GetObfuscation(parent)(m_scratch);
m_scratch >> value;
} catch (const std::exception&) {
return false;
}
return true;
}
};
struct LevelDBContext;
class CDBWrapper
{
friend const Obfuscation& dbwrapper_private::GetObfuscation(const CDBWrapper&);
private:
//! holds all leveldb-specific fields of this class
std::unique_ptr<LevelDBContext> m_db_context;
//! the name of this database
std::string m_name;
//! optional XOR-obfuscation of the database
Obfuscation m_obfuscation;
//! 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<std::string> ReadImpl(std::span<const std::byte> key) const;
bool ExistsImpl(std::span<const std::byte> key) const;
size_t EstimateSizeImpl(std::span<const std::byte> key1, std::span<const std::byte> key2) const;
auto& DBContext() const LIFETIMEBOUND { return *Assert(m_db_context); }
public:
CDBWrapper(const DBParams& params);
~CDBWrapper();
CDBWrapper(const CDBWrapper&) = delete;
CDBWrapper& operator=(const CDBWrapper&) = delete;
template <typename K, typename V>
bool Read(const K& key, V& value) const
{
DataStream ssKey{};
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
std::optional<std::string> strValue{ReadImpl(ssKey)};
if (!strValue) {
return false;
}
try {
std::span ssValue{MakeWritableByteSpan(*strValue)};
m_obfuscation(ssValue);
SpanReader{ssValue} >> value;
} catch (const std::exception&) {
return false;
}
return true;
}
template <typename K, typename V>
void Write(const K& key, const V& value, bool fSync = false)
{
CDBBatch batch(*this);
batch.Write(key, value);
WriteBatch(batch, fSync);
}
template <typename K>
bool Exists(const K& key) const
{
DataStream ssKey{};
ssKey.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey << key;
return ExistsImpl(ssKey);
}
template <typename K>
void Erase(const K& key, bool fSync = false)
{
CDBBatch batch(*this);
batch.Erase(key);
WriteBatch(batch, fSync);
}
void WriteBatch(CDBBatch& batch, bool fSync = false);
//! Perform a blocking full compaction of the underlying LevelDB.
void CompactFull();
//! Return a LevelDB property value, if available.
std::optional<std::string> GetProperty(const std::string& property) const;
// Get an estimate of LevelDB memory usage (in bytes).
size_t DynamicMemoryUsage() const;
CDBIterator* NewIterator();
/**
* Return true if the database managed by this class contains no entries.
*/
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<typename K>
size_t EstimateSize(const K& key_begin, const K& key_end) const
{
DataStream ssKey1{}, ssKey2{};
ssKey1.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey2.reserve(DBWRAPPER_PREALLOC_KEY_SIZE);
ssKey1 << key_begin;
ssKey2 << key_end;
return EstimateSizeImpl(ssKey1, ssKey2);
}
};
#endif // BITCOIN_DBWRAPPER_H