Files
triangles_v5/src/walletdb-batch.h
Triangles Dev 3566eed9e1 wallet: rebase CWalletDB onto CWalletBatchTyped (SQLite default)
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.
2026-06-30 01:01:16 -07:00

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