From 2a2b9d74f8fb6b5d86d750e6e5d18c942738f95c Mon Sep 17 00:00:00 2001 From: bitflash Date: Mon, 17 Aug 2026 17:45:01 -0300 Subject: wallet: run the node on a SQLite backend (opt-in) Adds -walletbackend=sqlite: an opt-in flag that runs the node on /wallet.sqlite instead of wallet.dat. It requires an export to exist first (no silent migration) and prints a step-by-step guide when one does not; wallet.dat is never opened or touched, so a plain restart goes back to Berkeley DB. - LoadWalletFromSQLiteRuntime opens the export read-write, keeps the handle open as the active backend, loads records into memory through the same visitor/reconcile/finish path as the read-only staging load, and never fabricates a default key. - Shutdown checkpoints and closes the SQLite backend; a kill before that loses nothing because WAL + synchronous=FULL already made every committed write durable. - Self-test proves runtime writes persist across a close and reopen. BDB stays the default: nothing activates the SQLite runtime unless the flag is given. One-shot audit/recovery commands still use Berkeley DB. --- src/db.cpp | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/db.h | 1 + src/main_gui.cpp | 66 +++++++++++++++++++++++++++++++++++++--- src/selftest.cpp | 25 ++++++++++++++++ 4 files changed, 179 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/db.cpp b/src/db.cpp index 7cfb7c5..c4ea81e 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -124,6 +124,13 @@ void WalletSQLiteRuntimeClose() { if (pWalletSQLiteRuntime) { + // Fold the write-ahead log into the main file on the way out. WAL + + // synchronous=FULL already makes every committed write durable across a + // kill, so this is housekeeping, not the thing that protects the data: + // if it fails, or the node is killed before it runs, the wallet is + // still whole. Best effort, never throws. + string strCheckpointError; + pWalletSQLiteRuntime->Checkpoint(strCheckpointError); delete pWalletSQLiteRuntime; pWalletSQLiteRuntime = NULL; } @@ -1426,6 +1433,90 @@ bool LoadWalletFromSQLite(const string& strPath) return FinishLoadedWallet(visitor.vchDefaultKey, false); } +// Open a SQLite wallet read-write and keep it open as the active runtime +// backend, then load its records into memory the same way LoadWalletFromSQLite +// does. Unlike the read-only staging path, this leaves pWalletSQLiteRuntime set +// so every later CWalletDB read/write/erase routes to this file for the life of +// the process. The Berkeley DB wallet.dat is never opened. +bool LoadWalletFromSQLiteRuntime(const string& strPath) +{ + strWalletLoadError.clear(); + + if (!FileExists(strPath.c_str())) + { + strWalletLoadError = strprintf("SQLite wallet does not exist: %s", + strPath.c_str()); + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; + } + + string strError; + if (!WalletSQLiteRuntimeOpen(strPath, strError)) + { + strWalletLoadError = strError; + printf("LoadWallet: cannot open SQLite wallet backend: %s\n", + strError.c_str()); + return false; + } + + CSQLiteWalletRuntimeLoadVisitor visitor; + try + { + CRITICAL_BLOCK(cs_mapKeys) + CRITICAL_BLOCK(cs_mapWallet) + { + if (!pWalletSQLiteRuntime->ScanRecords(visitor, strError)) + { + WalletSQLiteRuntimeClose(); + if (strWalletLoadError.empty()) + strWalletLoadError = strError; + printf("LoadWallet: SQLite wallet backend could not be read. " + "Failed while handling a '%s' record: %s\n", + visitor.strLastType.c_str(), strError.c_str()); + return false; + } + } + } + catch (const std::exception& e) + { + WalletSQLiteRuntimeClose(); + strWalletLoadError = strprintf( + "SQLite wallet backend could not be read while handling a '%s' record: %s", + visitor.strLastType.c_str(), e.what()); + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; + } + catch (...) + { + WalletSQLiteRuntimeClose(); + strWalletLoadError = strprintf( + "SQLite wallet backend could not be read while handling a '%s' record", + visitor.strLastType.c_str()); + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; + } + + if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode, + visitor.fHaveStoredHDCoinType)) + { + WalletSQLiteRuntimeClose(); + if (strWalletLoadError.empty()) + strWalletLoadError = "SQLite wallet metadata is not supported by this build"; + return false; + } + + // Never fabricate a default key: an export always carries one, and a file + // that does not is refused with the wallet left open to no writes rather + // than quietly minted into a new identity. + if (!FinishLoadedWallet(visitor.vchDefaultKey, false)) + { + WalletSQLiteRuntimeClose(); + return false; + } + + return true; +} + // Write a copy of wallet.dat that can be opened anywhere. // diff --git a/src/db.h b/src/db.h index 8fa4ea8..fee5a82 100644 --- a/src/db.h +++ b/src/db.h @@ -624,6 +624,7 @@ bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet); bool LoadWallet(); bool LoadWalletFromSQLite(const string& strPath); +bool LoadWalletFromSQLiteRuntime(const string& strPath); // Writes a wallet.dat that opens on its own, anywhere -- see the definition in // db.cpp for why a plain file copy does not. Still a point-in-time snapshot: diff --git a/src/main_gui.cpp b/src/main_gui.cpp index ee875c7..4d77533 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -190,6 +190,8 @@ static void PrintUsage() printf(" /walletsqliteloadcheck=FILE\n"); printf(" (parse a SQLite export like the wallet loader)\n"); printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n"); + printf(" /walletbackend=sqlite (opt-in: run the node on /wallet.sqlite\n"); + printf(" instead of wallet.dat; export one first)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -618,9 +620,59 @@ int main(int argc, char* argv[]) printf("Loading wallet...\n"); try { + string strWalletBackend = + argval2(argc, argv, "/walletbackend", "-walletbackend"); string strWalletSQLite = argval2(argc, argv, "/walletsqlite", "-walletsqlite"); - if (!strWalletSQLite.empty()) + + if (strWalletBackend == "sqlite") + { + // Opt-in, experimental. The node runs on /wallet.sqlite and + // never opens wallet.dat, but the user has to have exported one + // first -- there is no silent migration, and wallet.dat is left + // exactly where it is so a plain restart goes back to it. + AttachTerminal(); + string strSQLitePath = GetAppDir() + "/wallet.sqlite"; + if (!FileExists(strSQLitePath.c_str())) + { + string strGuide = strprintf( + "No SQLite wallet was found at\n" + " %s\n\n" + "The SQLite wallet backend is opt-in and needs an exported\n" + "wallet first. Your Berkeley DB wallet.dat is not touched by\n" + "any of this.\n\n" + " Step 1 Export your existing wallet to SQLite:\n" + " bitflash -walletsqliteexport=\"%s\"\n" + " Step 2 Start again with the SQLite backend:\n" + " bitflash -walletbackend=sqlite\n\n" + "To go back to Berkeley DB at any time, just start without\n" + "-walletbackend=sqlite. wallet.dat is still your wallet.", + strSQLitePath.c_str(), strSQLitePath.c_str()); + FatalStartupError(fHeadlessStartup, + "The SQLite wallet backend needs an exported wallet first.", + strGuide); + return 1; + } + + if (!LoadWalletFromSQLiteRuntime(strSQLitePath)) + { + FatalStartupError(fHeadlessStartup, + "Cannot open the SQLite wallet backend.", + strWalletLoadError); + return 1; + } + + fprintf(stderr, + "Wallet backend: SQLite (experimental)\n" + " %s\n" + "Your Berkeley DB wallet.dat is left untouched. Restart without\n" + "-walletbackend=sqlite to go back to it.\n", + strSQLitePath.c_str()); + fflush(stderr); + // Fall through and run the node normally: every CWalletDB read, + // write, and erase now routes to this SQLite file. + } + else if (!strWalletSQLite.empty()) { AttachTerminal(); if (!LoadWalletFromSQLite(strWalletSQLite)) @@ -638,8 +690,7 @@ int main(int argc, char* argv[]) DBFlush(true); return 0; } - - if (!LoadWallet()) + else if (!LoadWallet()) { // LoadWallet() explains itself into strWalletLoadError when it // knows why -- an unsupported wallet format, for one, which is the @@ -650,7 +701,9 @@ int main(int argc, char* argv[]) } // After the block index, so there is a chain to compare the wallet - // against, and before anything reports a balance. + // against, and before anything reports a balance. Works on either + // backend: RescanSpentFlags writes corrected flags through CWalletDB, + // which routes to SQLite when that backend is active. RescanSpentFlags(); } catch (const std::exception& e) @@ -894,6 +947,10 @@ int main(int argc, char* argv[]) // called here, which is why a wallet.dat copied elsewhere would not // open -- issue #40. Not optional. DBFlush(true); + // Checkpoint and close the SQLite wallet backend if one is active. A + // no-op on the Berkeley DB path. Safe to kill before this runs: WAL + + // synchronous=FULL already made every committed write durable. + WalletSQLiteRuntimeClose(); #ifdef _WIN32 // wallet.dat is safely flushed and closed now; let a pending console // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish. @@ -910,6 +967,7 @@ int main(int argc, char* argv[]) StopNode(); BtfStopManagedTor(); DBFlush(true); + WalletSQLiteRuntimeClose(); return ret; #endif } diff --git a/src/selftest.cpp b/src/selftest.cpp index 9fa0938..098eed6 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1230,6 +1230,31 @@ static int RunWalletSQLiteSelfTest() nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), "active SQLite runtime does not create wallet.dat") ? 0 : 1; + + // Durability: close the backend (checkpoints the WAL), reopen + // the same file, and confirm what CWalletDB wrote is still + // there. This is the write-survives-a-restart property the real + // -walletbackend=sqlite node depends on. + WalletSQLiteRuntimeClose(); + bool fReopened = + WalletSQLiteRuntimeOpen(strRuntimePath, strError); + nFail += Check(fReopened, + "SQLite runtime wallet reopens after close") ? 0 : 1; + if (fReopened) + { + int64 nPersisted = 0; + CPrivKey vchPersistedPrivKey; + vector vchPersistedDefaultKey; + nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", + nPersisted) && + nPersisted == 909090 && + CWalletDB("r").ReadDefaultKey(vchPersistedDefaultKey) && + vchPersistedDefaultKey == vchRuntimePubKey && + CWalletDB("r").ReadKey(vchRuntimePubKey, + vchPersistedPrivKey) && + vchPersistedPrivKey == vchRuntimePrivKey, + "SQLite runtime writes persist across a close and reopen") ? 0 : 1; + } } WalletSQLiteRuntimeClose(); -- cgit v1.2.3