3566eed9e1
Move CWalletDB off the Berkeley CDB base class and onto the typed batch
seam introduced by walletdb-batch.h / walletdb-{factory,sqlite}.{h,cpp}.
Build seam
----------
* CWalletDB now derives from CWalletBatchTyped. The typed Read/Write/
Erase/Exists templates come from the seam; their bodies (WriteTx,
WriteKey, WriteMasterKey, ReadPool, WriteSetting, ...) are unchanged
because only the base class swapped — the call signatures resolve to
the same templates.
* CWalletBatchTyped takes ownership of the WalletDatabase so the
underlying connection outlives any batch issued by it (SQLiteBatch
holds a reference, not a value). The two-phase Open() pattern lets
CWalletDB hand the freshly opened database to the base class after
MakeWalletDatabase() returns.
* MakeWalletDatabase (walletdb-factory.cpp) routes -walletdb=sqlite to
SQLiteDatabase, returning nullptr with a clear error for the
unfinished Berkeley branch. The CWalletDB constructor surfaces that
error string on failure.
Cursor sites (the only Berkeley-specific call sites)
---------------------------------------------------
Three sites used GetCursor()/ReadAtCursor() directly:
* LoadWallet — full scan, now uses StartCursor()/NextRecord()
* ListAccountCreditDebit — used DB_SET_RANGE + DB_NEXT loop; replaced
with full keyspace scan + filter-in-loop (SQLite cursor does not
support keyed range seeks). Behaviour matches Berkeley: terminates
when strType changes or, in single-account mode, when
acentry.strAccount differs.
* ZapWalletTx — moved to BerkeleyZapWalletTx (see below) because it
operates on raw Berkeley Db/Dbc/Dbt now that CWalletDB is on the
seam.
The 3 unused public methods on the old CWalletDB (GetAtCursor /
GetTxnCursor / GetAtActiveTxn) had no callers outside walletdb.{h,cpp}
(verified by grep) and were removed.
Berkeley-only escape hatches
----------------------------
Recover(CDBEnv&,...) and ZapWalletTx(...) became BerkeleyRecoverWallet
and BerkeleyZapWalletTx in a new walletdb-recover.{h,cpp} pair. They
operate directly on DbEnv/Db/Dbc/Dbt because CDB's members are
protected (free functions cannot use the wrapper). The recovery logic
duplicates a BDB-only ReadKeyValue variant locally to avoid pulling
the typed batch seam into a Berkeley-only file.
Init.cpp uses these via:
* -salvagewallet -> BerkeleyRecoverWallet(bitdb, ..., fOnlyKeys=true)
* -zapwallettxes -> BerkeleyZapWalletTx(...)
* bitdb.Verify -> BerkeleyRecoverWallet as the recover callback
Wallet migration hook
---------------------
After the Berkeley verify/salvage/zap steps and before CWalletDB is
opened for the live wallet, init.cpp now calls:
if (ResolveWalletDbKind() == SQLite &&
!IsSQLiteFile(walletPath))
MaybeMigrateBerkeleyWalletToSQLite(walletPath, err)
The migration code (walletmigrate.{h,cpp}) is unchanged — it opens a
private Berkeley environment over the wallet directory, copies every
record verbatim (raw key/value bytes) into a fresh SQLite file,
verifies the row count, then atomically renames the BDB original to
"<name>.bdb.bak" and the SQLite file into place. On any failure the
BDB original is left exactly as it was. Errors surface through
InitError so the daemon refuses to start with a corrupt wallet rather
than silently falling back to Berkeley.
No working Berkeley fallback
----------------------------
MakeWalletDatabase returns nullptr for the Berkeley branch, so
-walletdb=bdb no longer opens a working wallet through the seam. This
is intentional for this release — the migration hook handles existing
BDB wallets at first startup, after which the on-disk file is SQLite
and the BDB code path becomes pure recovery glue.
Header fallout
--------------
walletdb.h no longer pulls in db.h (which would drag <db_cxx.h> into
every TU that includes the wallet API). Forward decls added for
CWalletTx, CBlockLocator, CWallet, CPubKey, CScript, CMasterKey,
uint160, uint256. nWalletDBUpdated is now extern-declared in
walletdb.h and defined in db.cpp (was previously declared in db.h).
Validation
----------
Build: GREEN with USE_TOR_EMBEDDED=ON USE_I2P_EMBEDDED=ON. 6 binaries:
trianglesd, triangles-cli, test_triangles, test_chaindb_runtime,
test_chaindb_equivalence, test_snapshotnet.
Tests: 107/107 + 10/10 + 5/5 = byte-identical to the 5d9da84
baseline. wallet_tests and accounting_tests inside test_triangles now
exercise the SQLite path for the first time — their pass is the
de-facto wallet-migration validation at the test-suite level.
169 lines
6.0 KiB
C++
169 lines
6.0 KiB
C++
// Copyright (c) 2026 The Triangles developers.
|
|
// Distributed under the MIT/X11 software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
//
|
|
// Typed, backend-agnostic wallet batch — the bridge between CWalletDB's typed
|
|
// record calls and the raw byte-level WalletBatch interface (walletdb-base.h).
|
|
//
|
|
// It reproduces the exact serialization behavior of the old Berkeley CDB
|
|
// (CDataStream with SER_DISK / CLIENT_VERSION), so the bytes written are
|
|
// identical regardless of backend and CWalletDB's call sites need only change
|
|
// their base class — the Read/Write/Erase/Exists template calls are unchanged.
|
|
//
|
|
// CWalletDB is intended to derive from CWalletBatchTyped (replacing `: public
|
|
// CDB`). The Berkeley cursor methods CWalletDB used directly (GetAtCursor,
|
|
// ReadAtCursor with DB_NEXT/DB_SET_RANGE) map onto StartCursor()/NextRecord()
|
|
// here, which iterate the whole keyspace; range-seek call sites filter in the
|
|
// loop, as the SQLite cursor does not support keyed range seeks.
|
|
|
|
#ifndef TRIANGLES_WALLETDB_BATCH_H
|
|
#define TRIANGLES_WALLETDB_BATCH_H
|
|
|
|
#include "walletdb-base.h"
|
|
#include "serialize.h" // CDataStream, SER_DISK
|
|
#include "version.h" // CLIENT_VERSION
|
|
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
class CWalletBatchTyped
|
|
{
|
|
public:
|
|
// Default-constructed handle is unusable until Open() runs. Subclasses
|
|
// (CWalletDB) call Open() once they have opened a WalletDatabase.
|
|
CWalletBatchTyped() = default;
|
|
virtual ~CWalletBatchTyped() { Close(); }
|
|
|
|
// Open a fresh batch against the given database. Closes any previously
|
|
// open batch+database. Returns false (and leaves the handle null) if the
|
|
// database fails to produce a batch.
|
|
bool Open(std::unique_ptr<WalletDatabase> db)
|
|
{
|
|
Close();
|
|
if (!db)
|
|
return false;
|
|
m_database = std::move(db);
|
|
m_batch = m_database->MakeBatch(/*flush_on_close=*/true);
|
|
if (!m_batch) {
|
|
m_database.reset();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Close()
|
|
{
|
|
m_batch.reset();
|
|
m_database.reset();
|
|
}
|
|
bool IsNull() const { return m_batch == nullptr; }
|
|
|
|
// ── Transactions ─────────────────────────────────────────────────────────
|
|
bool TxnBegin() { return m_batch && m_batch->TxnBegin(); }
|
|
bool TxnCommit() { return m_batch && m_batch->TxnCommit(); }
|
|
bool TxnAbort() { return m_batch && m_batch->TxnAbort(); }
|
|
|
|
protected:
|
|
std::unique_ptr<WalletDatabase> m_database;
|
|
std::unique_ptr<WalletBatch> m_batch;
|
|
|
|
// ── Typed accessors (serialize key/value, dispatch to the raw batch) ──────
|
|
template <typename K, typename T>
|
|
bool Read(const K& key, T& value)
|
|
{
|
|
if (!m_batch) return false;
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
ssKey.reserve(1000);
|
|
ssKey << key;
|
|
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
|
|
|
ValueBytes vValue;
|
|
if (!m_batch->ReadKey(vKey, vValue))
|
|
return false;
|
|
try {
|
|
CDataStream ssValue(reinterpret_cast<const char*>(vValue.data()),
|
|
reinterpret_cast<const char*>(vValue.data()) + vValue.size(),
|
|
SER_DISK, CLIENT_VERSION);
|
|
ssValue >> value;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename K, typename T>
|
|
bool Write(const K& key, const T& value, bool fOverwrite = true)
|
|
{
|
|
if (!m_batch) return false;
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
ssKey.reserve(1000);
|
|
ssKey << key;
|
|
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
|
|
|
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
|
ssValue.reserve(10000);
|
|
ssValue << value;
|
|
ValueBytes vValue(ssValue.begin(), ssValue.end());
|
|
|
|
return m_batch->WriteKey(vKey, vValue, fOverwrite);
|
|
}
|
|
|
|
template <typename K>
|
|
bool Erase(const K& key)
|
|
{
|
|
if (!m_batch) return false;
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
ssKey.reserve(1000);
|
|
ssKey << key;
|
|
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
|
return m_batch->EraseKey(vKey);
|
|
}
|
|
|
|
template <typename K>
|
|
bool Exists(const K& key)
|
|
{
|
|
if (!m_batch) return false;
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
ssKey.reserve(1000);
|
|
ssKey << key;
|
|
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
|
return m_batch->HasKey(vKey);
|
|
}
|
|
|
|
// ── Cursor ────────────────────────────────────────────────────────────────
|
|
// Replaces CDB::GetCursor()/ReadAtCursor(). Open a cursor, then call
|
|
// NextRecord() repeatedly: returns true and fills the streams while records
|
|
// remain, false at end-of-data, and sets fError on failure.
|
|
std::unique_ptr<WalletCursor> StartCursor()
|
|
{
|
|
if (!m_batch) return nullptr;
|
|
return m_batch->GetNewCursor();
|
|
}
|
|
|
|
bool NextRecord(WalletCursor& cursor, CDataStream& ssKey, CDataStream& ssValue, bool& fError)
|
|
{
|
|
fError = false;
|
|
KeyBytes vKey;
|
|
ValueBytes vValue;
|
|
switch (cursor.Next(vKey, vValue)) {
|
|
case WalletCursorStatus::MORE:
|
|
ssKey.SetType(SER_DISK);
|
|
ssKey.clear();
|
|
ssKey.write(reinterpret_cast<const char*>(vKey.data()), vKey.size());
|
|
ssValue.SetType(SER_DISK);
|
|
ssValue.clear();
|
|
ssValue.write(reinterpret_cast<const char*>(vValue.data()), vValue.size());
|
|
return true;
|
|
case WalletCursorStatus::DONE:
|
|
return false;
|
|
case WalletCursorStatus::FAIL:
|
|
default:
|
|
fError = true;
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
#endif // TRIANGLES_WALLETDB_BATCH_H
|