Files
bitcoin/src/dbwrapper.h
merge-script bd0942bbd9 Merge bitcoin/bitcoin#34887: fuzz: target CDBWrapper
b63ef20d54 test: add fuzz harness for CDBWrapper (Andrew Toth)
32169c3855 dbwrapper: accept optional testing leveldb::Env in DBParams (Andrew Toth)
8d390c93fc dbwrapper: make max_file_size a configurable DBParams field (Andrew Toth)

Pull request description:

  Inspired by https://github.com/bitcoin/bitcoin/pull/34866#issuecomment-4090291488.

  We currently don't have a dedicated harness targeting `CDBWrapper`. OSS-Fuzz has a [rudimentary harness](https://github.com/google/oss-fuzz/blob/master/projects/leveldb/fuzz_db.cc) for levelDB [which fails](https://issues.oss-fuzz.com/issues/447252244), so doesn't appear maintained.

  This PR adds a harness targeting `CDBWrapper` against an in-memory oracle to verify correctness.

  A `DeterministicEnv` wraps levelDB's `memenv` to eliminate non-determinism by capturing background compaction and running it at fuzzer-chosen points.
  The fuzzer also controls the cache_bytes and max_file_size sizes so that small values trigger memtable flushes and compaction.

ACKs for top commit:
  l0rinc:
    code review ACK b63ef20d54
  marcofleon:
    ACK b63ef20d54
  dergoegge:
    utACK b63ef20d54
  sedited:
    ACK b63ef20d54

Tree-SHA512: da1f738ec90c49830a05b8990bdaa474299b573e966e60f4febef1292d9682f2e50f0016831f26bf4677e5afdaa142dc8766d871c6bce90d35f1695d480ac8c1
2026-05-21 15:03:49 +01:00

284 lines
7.7 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 <cstddef>
#include <exception>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
namespace leveldb {
class Env;
} // namespace leveldb
static const size_t DBWRAPPER_PREALLOC_KEY_SIZE = 64;
static const size_t DBWRAPPER_PREALLOC_VALUE_SIZE = 1024;
static const 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.
size_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;
//! 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);
// 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();
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