From 3f94ec0649f935d00de49ad7b55026317dc64a27 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 16:14:19 -0300 Subject: wallet: make SQLite the default backend for new wallets A fresh install with no wallet.dat now starts on the SQLite backend and creates wallet.sqlite, instead of a Berkeley DB wallet.dat. The choice is recorded in a plain-text /wallet-backend marker so it survives a restart without a command-line flag. Backend resolution when no -walletbackend is given: an existing wallet.sqlite (or a "sqlite" marker) stays on SQLite; an existing wallet.dat stays on Berkeley DB -- nothing is converted behind the user; a fresh datadir starts on SQLite. An explicit -walletbackend=bdb or a "bdb" marker forces Berkeley DB and overrides the default. Adds CreateNewSQLiteWallet() (WalletSQLiteRuntimeOpen gains fMustExist so the create path can make a new file) which mints the first default key straight into the SQLite store via the same FinishLoadedWallet() call the Berkeley DB create path uses, then cleans up on failure. Marker read/write helpers are plain text, hand-editable. New wallet-backend-default self-test proves the resolution in child processes (fresh->SQLite+marker, existing wallet.dat->BDB, "bdb" marker->BDB, explicit flag->wallet.dat). The three wallet.dat-specific suites now ask for -walletbackend=bdb explicitly, since a flagless fresh datadir is SQLite now. All wallet suites pass. --- src/db.cpp | 91 +++++++++++++++++++++++++++++++++++- src/db.h | 5 +- src/main_gui.cpp | 80 ++++++++++++++++++++++++++++--- src/makefile.mingw | 1 + src/selftest.cpp | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 301 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/db.cpp b/src/db.cpp index aafc0f9..949865f 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -101,10 +101,13 @@ bool WalletSQLiteRuntimeActive() return pWalletSQLiteRuntime != NULL; } -bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet) +bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist) { strErrorRet.clear(); - if (!FileExists(strPath.c_str())) + // Callers that load an existing wallet want the clear "does not exist" + // message rather than silently creating a blank one; the create path passes + // fMustExist=false so Open() (SQLITE_OPEN_CREATE) can make a fresh file. + if (fMustExist && !FileExists(strPath.c_str())) { strErrorRet = strprintf("SQLite wallet does not exist: %s", strPath.c_str()); @@ -230,6 +233,47 @@ bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet) return pWalletSQLiteRuntime->Checkpoint(strErrorRet); } +// The persistent record of which backend this datadir uses, so the choice +// survives a restart without a command-line flag. A plain text file holding +// "sqlite" or "bdb" -- visible and hand-editable on purpose, no hidden state. +static string WalletBackendMarkerPath() +{ + return GetAppDir() + "/wallet-backend"; +} + +string ReadWalletBackendMarker() +{ + FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "rb"); + if (!pf) + return ""; + char buf[16] = {0}; + size_t n = fread(buf, 1, sizeof(buf) - 1, pf); + fclose(pf); + string s(buf, n); + while (!s.empty() && isspace((unsigned char)s[s.size() - 1])) + s.erase(s.size() - 1); + while (!s.empty() && isspace((unsigned char)s[0])) + s.erase(0, 1); + if (s == "sqlite" || s == "bdb") + return s; + return ""; +} + +bool WriteWalletBackendMarker(const string& strBackend) +{ + if (strBackend != "sqlite" && strBackend != "bdb") + return false; + FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "wb"); + if (!pf) + return false; + bool fOk = fwrite(strBackend.c_str(), 1, strBackend.size(), pf) == strBackend.size(); + if (fputc('\n', pf) == EOF) + fOk = false; + if (fclose(pf) != 0) + fOk = false; + return fOk; +} + CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) { @@ -1609,6 +1653,49 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath) return true; } +// Create a brand-new wallet directly in the SQLite backend. For a fresh install +// where SQLite is the default there is no wallet.dat to export from, so open a +// new wallet.sqlite (Open() creates the file and its schema) and let +// FinishLoadedWallet mint the first default key straight into it -- the same +// call the Berkeley DB create path uses, so a new wallet is identical either +// way. Refuses to touch an existing file, and cleans up on failure so a broken +// half-initialised wallet.sqlite is never left for the next start to load. +bool CreateNewSQLiteWallet(const string& strPath) +{ + strWalletLoadError.clear(); + + if (FileExists(strPath.c_str())) + { + strWalletLoadError = strprintf("SQLite wallet already exists: %s", + strPath.c_str()); + printf("CreateNewSQLiteWallet: %s\n", strWalletLoadError.c_str()); + return false; + } + + string strError; + if (!WalletSQLiteRuntimeOpen(strPath, strError, false)) + { + strWalletLoadError = strError; + printf("CreateNewSQLiteWallet: cannot create SQLite wallet: %s\n", + strError.c_str()); + return false; + } + + vector vchNoDefaultKey; + if (!FinishLoadedWallet(vchNoDefaultKey, true)) + { + WalletSQLiteRuntimeClose(); + remove(strPath.c_str()); + remove((strPath + "-wal").c_str()); + remove((strPath + "-shm").c_str()); + if (strWalletLoadError.empty()) + strWalletLoadError = "could not initialise the new SQLite wallet"; + 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 5bc94b1..c50065b 100644 --- a/src/db.h +++ b/src/db.h @@ -357,7 +357,7 @@ private: // peer discovery is entirely Nostr/.btf-based now. bool WalletSQLiteRuntimeActive(); -bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet); +bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist=true); void WalletSQLiteRuntimeClose(); bool WalletSQLiteRuntimeReadRecord(const vector& vchKey, vector& vchValueRet); @@ -370,6 +370,9 @@ bool WalletSQLiteRuntimeCommitTxn(); void WalletSQLiteRuntimeRollbackTxn(); bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet); +string ReadWalletBackendMarker(); +bool WriteWalletBackendMarker(const string& strBackend); +bool CreateNewSQLiteWallet(const string& strPath); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index af6053f..6cd8b75 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -743,14 +743,80 @@ int main(int argc, char* argv[]) DBFlush(true); return 0; } - else if (!LoadWallet()) + else if (strWalletBackend == "bdb") { - // LoadWallet() explains itself into strWalletLoadError when it - // knows why -- an unsupported wallet format, for one, which is the - // case this whole path exists to make visible. - FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", - strWalletLoadError); - return 1; + // Explicit -walletbackend=bdb overrides the marker and the default: + // force Berkeley DB (LoadWallet creates wallet.dat if none exists). + if (!LoadWallet()) + { + FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", + strWalletLoadError); + return 1; + } + } + else + { + // No explicit -walletbackend on the command line. Resolve it from + // the persistent marker, then defaults: an existing wallet.sqlite + // stays on SQLite, an existing wallet.dat stays on Berkeley DB (the + // GUI offers to convert), and a fresh install starts on SQLite -- + // the new default. wallet.dat is never touched by any of this. + string strSQLitePath = GetAppDir() + "/wallet.sqlite"; + bool fSQLiteExists = FileExists(strSQLitePath.c_str()); + bool fWalletDatExists = FileExists((GetAppDir() + "/wallet.dat").c_str()); + string strMarker = ReadWalletBackendMarker(); + + bool fUseSQLite = false; + bool fCreateSQLite = false; + if (strMarker == "sqlite") + { + if (fSQLiteExists) fUseSQLite = true; + else if (fWalletDatExists) fUseSQLite = false; // marker stale; real wallet is BDB + else { fUseSQLite = true; fCreateSQLite = true; } + } + else if (strMarker == "bdb") + { + fUseSQLite = false; + } + else // no marker recorded yet + { + if (fSQLiteExists && !fWalletDatExists) fUseSQLite = true; + else if (fWalletDatExists) fUseSQLite = false; + else { fUseSQLite = true; fCreateSQLite = true; } + } + + if (fUseSQLite) + { + AttachTerminal(); + bool fOk = fCreateSQLite + ? CreateNewSQLiteWallet(strSQLitePath) + : LoadWalletFromSQLiteRuntime(strSQLitePath); + if (!fOk) + { + FatalStartupError(fHeadlessStartup, + fCreateSQLite ? "Cannot create the SQLite wallet." + : "Cannot open the SQLite wallet.", + strWalletLoadError); + return 1; + } + // Record the choice so it is stable from here on. + if (ReadWalletBackendMarker() != "sqlite") + WriteWalletBackendMarker("sqlite"); + fprintf(stderr, "Wallet backend: SQLite\n %s\n%s", + strSQLitePath.c_str(), + fCreateSQLite ? "A new wallet was created here.\n" : ""); + fflush(stderr); + // Fall through and run the node; CWalletDB routes to this file. + } + else if (!LoadWallet()) + { + // LoadWallet() explains itself into strWalletLoadError when it + // knows why -- an unsupported wallet format, for one, which is + // the case this whole path exists to make visible. + FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", + strWalletLoadError); + return 1; + } } // After the block index, so there is a chain to compare the wallet diff --git a/src/makefile.mingw b/src/makefile.mingw index 1cb47fb..06c9211 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -113,6 +113,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=wallet-sqlite-encrypt + ./bitflash.exe -selftest=wallet-backend-default ./bitflash.exe -selftest=net-message ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum diff --git a/src/selftest.cpp b/src/selftest.cpp index 90a30d2..1960003 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -911,6 +911,7 @@ static int RunWalletStorageSanitySelfTest() vCreateArgs.push_back("-datadir=" + strScenarioDir); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && @@ -1474,6 +1475,7 @@ static int RunWalletSQLiteMigrationSelfTest() vCreateArgs.push_back("-datadir=" + strWalletDir); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && @@ -2534,6 +2536,134 @@ static int RunWalletSQLiteEncryptSelfTest() return nFail == 0 ? 0 : 1; } +// Backend resolution when no -walletbackend flag is given: a fresh install +// starts on SQLite (the new default), an existing wallet.dat stays on Berkeley +// DB, the persistent marker overrides the default, and an explicit flag +// overrides everything. Each scenario runs a real child process. +static int RunWalletBackendDefaultSelfTest() +{ + fflush(stdout); + printf("wallet-backend-default self-test\n"); + + std::string tmp, cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + + try + { + // 1. Fresh datadir, no flag -> new wallet.sqlite, marker "sqlite", no wallet.dat. + string d1 = tmp + "/fresh"; + nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; + vector a1; + a1.push_back("-datadir=" + d1); + a1.push_back("-nomanagedtor"); + a1.push_back("-nogui"); + a1.push_back("-newaddress"); + int r1 = RunBitflashChild(strExe, a1); + nFail += Check(r1 == 0 && + FileExists((d1 + "/wallet.sqlite").c_str()) && + !FileExists((d1 + "/wallet.dat").c_str()) && + FileContainsText(d1 + "/wallet-backend", "sqlite"), + "a fresh install starts on SQLite (new default) and records the marker") ? 0 : 1; + + // 1b. Second run reuses it, still SQLite, still no wallet.dat. + int r1b = RunBitflashChild(strExe, a1); + nFail += Check(r1b == 0 && + FileExists((d1 + "/wallet.sqlite").c_str()) && + !FileExists((d1 + "/wallet.dat").c_str()), + "the recorded SQLite choice survives a restart") ? 0 : 1; + + // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. + string d2 = tmp + "/existing-bdb"; + nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1; + vector a2c; + a2c.push_back("-datadir=" + d2); + a2c.push_back("-nomanagedtor"); + a2c.push_back("-nogui"); + a2c.push_back("-walletbackend=bdb"); + a2c.push_back("-newphrase"); + RunBitflashChild(strExe, a2c); + vector a2; + a2.push_back("-datadir=" + d2); + a2.push_back("-nomanagedtor"); + a2.push_back("-nogui"); + a2.push_back("-newaddress"); + int r2 = RunBitflashChild(strExe, a2); + nFail += Check(r2 == 0 && + FileExists((d2 + "/wallet.dat").c_str()) && + !FileExists((d2 + "/wallet.sqlite").c_str()), + "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1; + + // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir. + string d3 = tmp + "/marker-bdb"; + nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1; + nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"), + "a bdb marker can be written") ? 0 : 1; + vector a3; + a3.push_back("-datadir=" + d3); + a3.push_back("-nomanagedtor"); + a3.push_back("-nogui"); + a3.push_back("-newaddress"); + int r3 = RunBitflashChild(strExe, a3); + nFail += Check(r3 == 0 && + FileExists((d3 + "/wallet.dat").c_str()) && + !FileExists((d3 + "/wallet.sqlite").c_str()), + "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1; + + // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat. + string d4 = tmp + "/explicit-bdb"; + nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1; + vector a4; + a4.push_back("-datadir=" + d4); + a4.push_back("-nomanagedtor"); + a4.push_back("-nogui"); + a4.push_back("-walletbackend=bdb"); + a4.push_back("-newaddress"); + int r4 = RunBitflashChild(strExe, a4); + nFail += Check(r4 == 0 && + FileExists((d4 + "/wallet.dat").c_str()) && + !FileExists((d4 + "/wallet.sqlite").c_str()), + "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static int RunWalletPortabilitySelfTest() { fflush(stdout); @@ -2576,6 +2706,7 @@ static int RunWalletPortabilitySelfTest() vCreateArgs.push_back("-datadir=" + strOriginal); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && @@ -3265,6 +3396,8 @@ int RunSelfTest(const std::string& name) return RunWalletEncryptSelfTest(); if (name == "wallet-sqlite-encrypt") return RunWalletSQLiteEncryptSelfTest(); + if (name == "wallet-backend-default") + return RunWalletBackendDefaultSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") @@ -3281,6 +3414,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-sqlite-encrypt, wallet-backend-default, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } -- cgit v1.2.3