From c49b92a9a79a398b51200e69f7dde8a467b9c925 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 15:03:21 -0300 Subject: wallet: add experimental SQLite record store --- src/makefile.mingw | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/makefile.mingw') diff --git a/src/makefile.mingw b/src/makefile.mingw index 333be96..2ff4e11 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -24,7 +24,7 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ LIBS = -L$(DEPS)/lib -lsecp256k1 \ -L$(RANDOMX)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ -LC:/msys64/ucrt64/lib \ -lglfw3 -lopengl32 -lgdi32 \ -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \ @@ -37,7 +37,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ + obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -51,7 +52,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h + selftest.h walletcmd.h wallet_sqlite.h sockcount.h proxy.h tor.h compat.h font_roboto.h all: bitflash.exe @@ -107,6 +108,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=wallet-format + ./bitflash.exe -selftest=wallet-sqlite ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message -- cgit v1.2.3 From 59b9d11015f0e3a2475d0750fc534583c2ca9f77 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 15:56:12 -0300 Subject: wallet: export BDB records to SQLite --- docs/storage.md | 6 ++ src/Makefile | 1 + src/main_gui.cpp | 15 ++++- src/makefile.mingw | 1 + src/selftest.cpp | 148 +++++++++++++++++++++++++++++++++++++++++++++++++- src/wallet_sqlite.cpp | 10 ++++ src/wallet_sqlite.h | 1 + src/walletcmd.cpp | 97 +++++++++++++++++++++++++++++++++ src/walletcmd.h | 5 ++ 9 files changed, 281 insertions(+), 3 deletions(-) (limited to 'src/makefile.mingw') diff --git a/docs/storage.md b/docs/storage.md index f75f9b9..bb414b5 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -104,6 +104,12 @@ runtime wallet still opens Berkeley DB. The purpose is to prove schema creation, raw record write/read, close/reopen behavior, and value preservation before any BDB-to-SQLite migration or runtime backend flag exists. +`-walletsqliteexport=FILE` is the next staging tool. It streams every raw +`wallet.dat` record through `ScanWalletRecords()` and writes those same key and +value bytes into the SQLite table inside one transaction. It refuses to +overwrite an existing export and still does not change the runtime wallet +backend. + ## `blkindex.dat` `blkindex.dat` is accessed through `CTxDB`. diff --git a/src/Makefile b/src/Makefile index cfa40f6..b83ff58 100644 --- a/src/Makefile +++ b/src/Makefile @@ -189,6 +189,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=wallet-format ./bitflash-node -selftest=wallet-sqlite + ./bitflash-node -selftest=wallet-sqlite-migration ./bitflash-node -selftest=wallet-crypto ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 965c818..6e89270 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -114,8 +114,8 @@ static void PrintUsage() printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); - printf(" db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt,\n"); - printf(" wallet-portability,\n"); + printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n"); + printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n"); printf(" net-message,\n"); printf(" consensus-limits, pool-stratum,\n"); printf(" parse-money, socks5-proxy, or managed-tor\n"); @@ -181,6 +181,8 @@ static void PrintUsage() printf(" (write the same audit as deterministic JSON)\n"); printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); printf(" wallet.dat storage state)\n"); + printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); + printf(" experimental SQLite store, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -529,6 +531,15 @@ int main(int argc, char* argv[]) return nRet; } + string strWalletSQLiteExport = + argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport"); + if (!strWalletSQLiteExport.empty()) + { + int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/makefile.mingw b/src/makefile.mingw index 2ff4e11..14e6b32 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -109,6 +109,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=wallet-format ./bitflash.exe -selftest=wallet-sqlite + ./bitflash.exe -selftest=wallet-sqlite-migration ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message diff --git a/src/selftest.cpp b/src/selftest.cpp index f9c4a8e..7a54271 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1134,6 +1134,150 @@ static int RunWalletSQLiteSelfTest() return nFail == 0 ? 0 : 1; } +class CWalletRecordMapVisitor : public CWalletRecordVisitor +{ +public: + std::map, vector >& mapRecords; + + explicit CWalletRecordMapVisitor( + std::map, vector >& mapRecordsIn) + : mapRecords(mapRecordsIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + vector vchKey = DataStreamBytes(ssKey); + if (mapRecords.count(vchKey)) + { + strErrorRet = "duplicate serialized wallet key"; + return false; + } + mapRecords[vchKey] = DataStreamBytes(ssValue); + return true; + } +}; + +static int RunWalletSQLiteMigrationSelfTest() +{ + fflush(stdout); + printf("wallet-sqlite-migration self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd)) + { + printf(" FAIL could not read current directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + printf(" temp root: %s\n", tmp.c_str()); + + try + { +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + string strWalletDir = tmp + "/wallet"; + nFail += Check(MakeDirLocal(strWalletDir), + "temporary wallet directory can be created") ? 0 : 1; + + string strCreateOut = tmp + "/create.txt"; + vector vCreateArgs; + vCreateArgs.push_back("-datadir=" + strWalletDir); + vCreateArgs.push_back("-nomanagedtor"); + vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-newphrase"); + int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); + nFail += Check(nCreateRet == 0 && + FileContainsText(strCreateOut, "Write these twelve words down"), + "a phrase wallet can be created for SQLite export") ? 0 : 1; + + string strSQLite = tmp + "/wallet.sqlite"; + string strExportOut = tmp + "/sqlite-export.txt"; + vector vExportArgs; + vExportArgs.push_back("-datadir=" + strWalletDir); + vExportArgs.push_back("-nomanagedtor"); + vExportArgs.push_back("-nogui"); + vExportArgs.push_back("-walletsqliteexport=" + strSQLite); + int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut); + nFail += Check(nExportRet == 0 && + FileExists(strSQLite.c_str()) && + FileContainsText(strExportOut, "SQLite wallet export written"), + "wallet.dat can be exported to SQLite") ? 0 : 1; + + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; + int nOverwriteRet = + RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); + nFail += Check(nOverwriteRet != 0 && + FileContainsText(strOverwriteOut, + "Refusing to overwrite existing SQLite wallet export"), + "SQLite wallet export refuses to overwrite") ? 0 : 1; + remove((strSQLite + "-wal").c_str()); + remove((strSQLite + "-shm").c_str()); + + std::map, vector > mapBDB; + string strOldDataDir = strSetDataDir; + string strScanError; + strSetDataDir = strWalletDir; + CWalletRecordMapVisitor visitor(mapBDB); + bool fScanOk = ScanWalletRecords(visitor, strScanError); + DBFlush(true); + strSetDataDir = strOldDataDir; + nFail += Check(fScanOk && !mapBDB.empty(), + "wallet.dat records can be scanned for comparison") ? 0 : 1; + + CWalletDBSQLite db; + string strError; + nFail += Check(db.Open(strSQLite, strError), + "exported SQLite wallet opens") ? 0 : 1; + int nSQLiteRecords = 0; + nFail += Check(db.CountRecords(nSQLiteRecords, strError) && + nSQLiteRecords == (int)mapBDB.size(), + "SQLite export has the same record count as wallet.dat") ? 0 : 1; + + bool fAllMatch = true; + for (std::map, vector >::const_iterator it = + mapBDB.begin(); it != mapBDB.end(); ++it) + { + vector vchSQLiteValue; + if (!db.ReadRecord(it->first, vchSQLiteValue, strError) || + vchSQLiteValue != it->second) + { + fAllMatch = false; + break; + } + } + nFail += Check(fAllMatch, + "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + 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 void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) @@ -2395,6 +2539,8 @@ int RunSelfTest(const std::string& name) return RunDbEnvReopenSelfTest(); if (name == "wallet-sqlite") return RunWalletSQLiteSelfTest(); + if (name == "wallet-sqlite-migration") + return RunWalletSQLiteMigrationSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2415,6 +2561,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-crypto, wallet-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-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp index 4104ac6..4369af6 100644 --- a/src/wallet_sqlite.cpp +++ b/src/wallet_sqlite.cpp @@ -97,6 +97,16 @@ bool CWalletDBSQLite::RollbackTransaction(string& strError) return Exec("ROLLBACK;", strError); } +bool CWalletDBSQLite::Checkpoint(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("PRAGMA wal_checkpoint(TRUNCATE);", strError); +} + bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) { char* pszErr = NULL; diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h index 9812d3a..5ef862d 100644 --- a/src/wallet_sqlite.h +++ b/src/wallet_sqlite.h @@ -18,6 +18,7 @@ public: bool BeginTransaction(string& strError); bool CommitTransaction(string& strError); bool RollbackTransaction(string& strError); + bool Checkpoint(string& strError); bool WriteRecord(const vector& vchKey, const vector& vchValue, diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6fb09a3..20e7e33 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -12,6 +12,7 @@ #include "headers_core.h" #include "bip32.h" #include "walletcmd.h" +#include "wallet_sqlite.h" #undef printf @@ -790,6 +791,102 @@ int CmdWalletStorageAudit(const std::string& strJsonOut) return 0; } +static vector DataStreamBytesLocal(const CDataStream& ss) +{ + return vector(ss.begin(), ss.end()); +} + +class CWalletSQLiteExportVisitor : public CWalletRecordVisitor +{ +public: + CWalletDBSQLite& db; + int& nCopied; + + CWalletSQLiteExportVisitor(CWalletDBSQLite& dbIn, int& nCopiedIn) + : db(dbIn), nCopied(nCopiedIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + if (!db.WriteRecord(DataStreamBytesLocal(ssKey), + DataStreamBytesLocal(ssValue), + strErrorRet)) + return false; + nCopied++; + return true; + } +}; + +int CmdWalletSQLiteExport(const std::string& strDest) +{ + AttachTerminal(); + + if (strDest.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); + return 1; + } + if (FileExists(strDest.c_str())) + { + fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", + strDest.c_str()); + return 1; + } + + string strError; + CWalletDBSQLite db; + if (!db.Open(strDest, strError)) + { + fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + if (!db.BeginTransaction(strError)) + { + fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nCopied = 0; + CWalletSQLiteExportVisitor visitor(db, nCopied); + if (!ScanWalletRecords(visitor, strError)) + { + string strRollbackError; + db.RollbackTransaction(strRollbackError); + fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); + return 1; + } + if (!db.CommitTransaction(strError)) + { + fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + if (!db.Checkpoint(strError)) + { + fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nCount = 0; + if (!db.CountRecords(nCount, strError)) + { + fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", + strError.c_str()); + return 1; + } + if (nCount != nCopied) + { + fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", + nCopied, nCount); + return 1; + } + + printf("SQLite wallet export written to %s\n", strDest.c_str()); + printf(" records copied: %d\n", nCopied); + fflush(stdout); + return 0; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index 6077669..b1a29d2 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -70,6 +70,11 @@ int CmdWalletStorageAudit(const std::string& strJsonOut = ""); // or migration-unsafe mix of storage records. int CmdWalletStorageCheck(); +// Diagnostic/migration staging: copy every raw wallet.dat record into an +// experimental SQLite key/value store, preserving serialized bytes. This does +// not change which backend the runtime wallet opens. +int CmdWalletSQLiteExport(const std::string& strDest); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From e58c4022e0ddd137b93cdb3464753bef5df1d456 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 14:23:38 -0300 Subject: wallet: encrypt atomically under the SQLite backend EncryptWallet() had only a Berkeley DB path: it rebuilt a wallet.dat temp file and swapped it in. Under -walletbackend=sqlite that was wrong -- it would leave the live wallet.sqlite unencrypted and write a divergent wallet.dat instead. Add a SQLite persistence branch that rebuilds a fresh wallet.sqlite in one transaction (public records copied from the live store, encrypted key material serialized byte-for-byte the way the BDB path writes it), checkpoints it, and swaps it in with a .before-encrypt backup, mirroring the BDB rename dance. A fresh file rather than an in-place rewrite keeps the change atomic and leaves no plaintext key in a freed page. CmdEncryptWallet skips PurgeDbEnvironmentLogs under SQLite (no Berkeley DB environment behind the wallet) and reports the backend it used. New wallet-sqlite-encrypt self-test drives the real command path in child processes: export a wallet to SQLite, encrypt it through -walletbackend=sqlite, and prove it comes out encrypted, unlockable with the right passphrase, refusing the wrong one, with its recovery phrase intact and the original wallet.dat left untouched. --- src/db.cpp | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/makefile.mingw | 1 + src/selftest.cpp | 189 ++++++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 15 +++- 4 files changed, 410 insertions(+), 5 deletions(-) (limited to 'src/makefile.mingw') diff --git a/src/db.cpp b/src/db.cpp index 771a4ba..86f6dd7 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -1974,9 +1974,9 @@ static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo, return ScanWalletRecords(visitor, strErrorRet); } -static string WalletEncryptBackupPath() +static string WalletEncryptBackupPathFor(const char* pszBaseName) { - string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; + string strBase = GetAppDir() + "/" + pszBaseName + ".before-encrypt."; string strPath = strBase + i64tostr(GetTime()) + ".bak"; for (int i = 1; FileExists(strPath.c_str()); i++) strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; @@ -2006,6 +2006,192 @@ static void SecureClearBytes(vector& vch) vch.clear(); } +// The SQLite counterpart of CWalletPublicRecordCopyVisitor: same skip list, +// same byte-for-byte copy, but the destination is a CWalletDBSQLite record +// store rather than a Berkeley DB file. +class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor +{ +public: + CWalletDBSQLite& dbTo; + int& nCopied; + + CWalletPublicRecordCopyToSQLiteVisitor(CWalletDBSQLite& dbToIn, int& nCopiedIn) + : dbTo(dbToIn), nCopied(nCopiedIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + string strType; + try + { + CDataStream ssType = ssKey; + ssType >> strType; + } + catch (...) + { + strErrorRet = "SQLite wallet has a record whose key type cannot be read"; + return false; + } + + if (IsWalletEncryptionPrivateRecord(strType)) + return true; + + vector vchKey(ssKey.begin(), ssKey.end()); + vector vchValue(ssValue.begin(), ssValue.end()); + bool fOk = dbTo.WriteRecord(vchKey, vchValue, strErrorRet, true); + if (!vchKey.empty()) + memset(&vchKey[0], 0, vchKey.size()); + if (!vchValue.empty()) + memset(&vchValue[0], 0, vchValue.size()); + if (!fOk) + { + strErrorRet = strprintf("could not copy wallet record '%s': %s", + strType.c_str(), strErrorRet.c_str()); + return false; + } + nCopied++; + return true; + } +}; + +// Persist an encrypted wallet under the SQLite backend. Everything the caller +// computed -- the master key, the re-encrypted private keys, the fresh crypted +// key pool, the encrypted HD seed -- lands in a brand new wallet.sqlite built +// in one transaction, which is then swapped in for the plaintext original. +// +// A fresh file rather than an in-place rewrite for two reasons: it keeps the +// whole change atomic (a crash before the rename leaves the old wallet whole), +// and it guarantees no plaintext key survives in a freed page the way an +// in-place DELETE would leave behind until a VACUUM. +static bool EncryptWalletPersistSQLite( + const vector, vector > >& vCryptedKeys, + const vector, vector > >& vNewPoolKeys, + const vector > >& vNewPool, + const CWalletMasterKey& kMasterKey, + const vector& vchCryptedHDMasterNew, + const vector& vchCryptedHDChainCodeNew, + unsigned int nHDNextNew, + int& nCopiedRet, + string& strBackupRet, + string& strErrorRet) +{ + nCopiedRet = 0; + if (!pWalletSQLiteRuntime) + { + strErrorRet = "SQLite wallet backend is not active"; + return false; + } + + string strWalletPath = GetAppDir() + "/wallet.sqlite"; + string strTempPath = GetAppDir() + "/" + + strprintf("wallet.sqlite.encrypting.%lld", GetTime()); + remove(strTempPath.c_str()); + remove((strTempPath + "-wal").c_str()); + remove((strTempPath + "-shm").c_str()); + + { + CWalletDBSQLite dbTmp; + if (!dbTmp.Open(strTempPath, strErrorRet)) + { + remove(strTempPath.c_str()); + return false; + } + if (!dbTmp.BeginTransaction(strErrorRet)) + { + dbTmp.Close(); + remove(strTempPath.c_str()); + return false; + } + + // The public records come from the live SQLite store -- under this + // backend ScanWalletRecords() has no Berkeley DB behind it to read. + CWalletPublicRecordCopyToSQLiteVisitor visitor(dbTmp, nCopiedRet); + bool fOk = pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); + + // Encrypted metadata, serialized byte-for-byte the way the Berkeley DB + // path (CWalletRewriteDB) writes it, so a wallet encrypted under either + // backend is the same set of records. ckey uses INSERT-only to match + // WriteCryptedKey; the rest overwrite. + fOk = fOk && + dbTmp.WriteTypedRecord(string("walletminversion"), + (int)WALLET_FORMAT_ENCRYPTED, strErrorRet, true) && + dbTmp.WriteTypedRecord(make_pair(string("mkey"), + (unsigned int)(nWalletMasterKeyMaxID + 1)), + kMasterKey, strErrorRet, true); + for (size_t i = 0; fOk && i < vCryptedKeys.size(); i++) + fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vCryptedKeys[i].first), + vCryptedKeys[i].second, strErrorRet, false); + for (size_t i = 0; fOk && i < vNewPoolKeys.size(); i++) + fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vNewPoolKeys[i].first), + vNewPoolKeys[i].second, strErrorRet, false); + if (fOk && !vchCryptedHDMasterNew.empty()) + fOk = dbTmp.WriteTypedRecord(string("cryptedhdmaster"), + vchCryptedHDMasterNew, strErrorRet, true) && + dbTmp.WriteTypedRecord(string("cryptedhdchaincode"), + vchCryptedHDChainCodeNew, strErrorRet, true); + if (fOk) + fOk = dbTmp.WriteTypedRecord(string("hdnext"), nHDNextNew, strErrorRet, true); + for (size_t i = 0; fOk && i < vNewPool.size(); i++) + fOk = dbTmp.WriteTypedRecord(make_pair(string("pool"), vNewPool[i].first), + vNewPool[i].second, strErrorRet, true); + + if (!fOk) + { + string strIgnore; + dbTmp.RollbackTransaction(strIgnore); + dbTmp.Close(); + remove(strTempPath.c_str()); + remove((strTempPath + "-wal").c_str()); + remove((strTempPath + "-shm").c_str()); + return false; + } + + if (!dbTmp.CommitTransaction(strErrorRet)) + { + dbTmp.Close(); + remove(strTempPath.c_str()); + remove((strTempPath + "-wal").c_str()); + remove((strTempPath + "-shm").c_str()); + return false; + } + string strCheckpointError; + dbTmp.Checkpoint(strCheckpointError); + dbTmp.Close(); + } + + // The rebuilt wallet is committed and self-contained. Close the live handle + // so the file can be swapped: Windows will not rename a file that still has + // an open handle, and the node exits right after this. + WalletSQLiteRuntimeClose(); + + string strBackupPath = WalletEncryptBackupPathFor("wallet.sqlite"); + if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) + { + strErrorRet = strprintf("could not move the original SQLite wallet to %s", + strBackupPath.c_str()); + remove(strTempPath.c_str()); + return false; + } + // The moved-aside wallet owns any leftover write-ahead log or shared-memory + // sidecar; drop stale ones so they cannot shadow the new wallet. + remove((strWalletPath + "-wal").c_str()); + remove((strWalletPath + "-shm").c_str()); + + if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0) + { + rename(strBackupPath.c_str(), strWalletPath.c_str()); + strErrorRet = "could not install the encrypted SQLite wallet; the original was restored"; + remove(strTempPath.c_str()); + return false; + } + remove((strTempPath + "-wal").c_str()); + remove((strTempPath + "-shm").c_str()); + + strBackupRet = strBackupPath; + return true; +} + bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) { strBackupRet.clear(); @@ -2144,6 +2330,24 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); } + // Every ciphertext is computed; nothing has touched the store yet. Under + // the SQLite backend, persist by rebuilding wallet.sqlite in one atomic + // transaction and swapping it in. The Berkeley DB path below is unchanged. + if (WalletSQLiteRuntimeActive()) + { + int nCopiedSQLite = 0; + if (!EncryptWalletPersistSQLite(vCryptedKeys, vNewPoolKeys, vNewPool, + kMasterKey, vchCryptedHDMasterNew, + vchCryptedHDChainCodeNew, nHDNextNew, + nCopiedSQLite, strBackupRet, strErrorRet)) + return false; + printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), " + "rebuilt %d key-pool entry(s) [SQLite]\n", + nCopiedSQLite, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()), + (int)vNewPool.size()); + return true; + } + string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); string strTempPath = GetAppDir() + "/" + strTempFile; remove(strTempPath.c_str()); @@ -2211,7 +2415,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st DBFlush(true); string strWalletPath = GetAppDir() + "/wallet.dat"; - string strBackupPath = WalletEncryptBackupPath(); + string strBackupPath = WalletEncryptBackupPathFor("wallet.dat"); if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) { strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); diff --git a/src/makefile.mingw b/src/makefile.mingw index 14e6b32..1cb47fb 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -112,6 +112,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-sqlite-migration ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt + ./bitflash.exe -selftest=wallet-sqlite-encrypt ./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 098eed6..fc10947 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -2252,6 +2252,191 @@ static int RunWalletEncryptSelfTest() return nFail == 0 ? 0 : 1; } +// Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley +// DB encrypt test above, this drives the real command-line path end to end in +// child processes: build a Berkeley DB wallet, export it to SQLite, then run +// -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out +// encrypted, unlockable, and with wallet.dat left untouched. +static int RunWalletSQLiteEncryptSelfTest() +{ + fflush(stdout); + printf("wallet-sqlite-encrypt self-test\n"); + + std::string tmp; + std::string 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()); + + try + { + // Build a normal Berkeley DB wallet with a recovery phrase, a key pool, + // and a couple of public records, then release it. Everything after this + // runs the real command-line paths in child processes. + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + string strError; + const string strMnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), + "a phrase can be installed before encryption") ? 0 : 1; + TopUpKeyPool(); + + vector vchDefaultPubKey = keyUser.GetPubKey(); + CKey publicLabelKey; + publicLabelKey.MakeNewKey(); + string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); + nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && + CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), + "public wallet records can be written before encryption") ? 0 : 1; + + CPrivKey vchDefaultPrivKey; + nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), + "the default private key is readable before encryption") ? 0 : 1; + vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); + vector vchPlainHDMaster = vchHDMaster; + vector vchPlainHDChainCode = vchHDChainCode; + + // Release wallet.dat so a child process can read it for the export. + DBFlush(true); + +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + string strSQLitePath = tmp + "/wallet.sqlite"; + string strWalletDatPath = tmp + "/wallet.dat"; + string strRightPassFile = tmp + "/right-pass.txt"; + string strWrongPassFile = tmp + "/wrong-pass.txt"; + nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") && + WriteTextFile(strWrongPassFile, "wrong-passphrase\n"), + "passphrase files can be written for restarted commands") ? 0 : 1; + + // Step 1: export the Berkeley DB wallet to SQLite. + vector vExportArgs; + vExportArgs.push_back("-datadir=" + tmp); + vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath); + vExportArgs.push_back("-nogui"); + int nExportRet = RunBitflashChild(strExe, vExportArgs); + nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()), + "the wallet exports to a SQLite file") ? 0 : 1; + nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), + "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1; + + // Step 2: encrypt through the real SQLite backend command path. + string strEncryptOut = tmp + "/sqlite-encrypt-out.txt"; + vector vEncryptArgs; + vEncryptArgs.push_back("-datadir=" + tmp); + vEncryptArgs.push_back("-walletbackend=sqlite"); + vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile); + vEncryptArgs.push_back("-nogui"); + int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); + nFail += Check(nEncryptRet == 0, + "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1; + nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"), + "encryption took the SQLite persistence path") ? 0 : 1; + nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"), + "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1; + + // The encrypted SQLite wallet must no longer hold the plaintext secrets. + nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), + "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1; + nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster), + "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1; + nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode), + "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1; + + // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path + // must never touch it, so its plaintext key is still exactly there. + nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes), + "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1; + + // Step 3: a locked encrypted SQLite wallet reports that phrase coverage + // needs an unlock, exactly as the Berkeley DB path does (exit code 2). + string strLockedAudit = tmp + "/locked-recovery-audit.txt"; + vector vLockedArgs; + vLockedArgs.push_back("-datadir=" + tmp); + vLockedArgs.push_back("-walletbackend=sqlite"); + vLockedArgs.push_back("-recoveryaudit"); + vLockedArgs.push_back("-nogui"); + int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit); + nFail += Check(nLockedRet == 2 && + FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"), + "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1; + + // The wrong passphrase is rejected and reveals nothing. + string strWrongDump = tmp + "/wrong-dump.txt"; + vector vWrongArgs; + vWrongArgs.push_back("-datadir=" + tmp); + vWrongArgs.push_back("-walletbackend=sqlite"); + vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); + vWrongArgs.push_back("-dumpwallet=" + strWrongDump); + vWrongArgs.push_back("-nogui"); + int nWrongRet = RunBitflashChild(strExe, vWrongArgs); + nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), + "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1; + + // The right passphrase unlocks and can dump the original key. + string strRightDump = tmp + "/right-dump.txt"; + vector vRightArgs; + vRightArgs.push_back("-datadir=" + tmp); + vRightArgs.push_back("-walletbackend=sqlite"); + vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); + vRightArgs.push_back("-dumpwallet=" + strRightDump); + vRightArgs.push_back("-nogui"); + int nRightRet = RunBitflashChild(strExe, vRightArgs); + nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), + "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1; + nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), + "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1; + + // The recovery phrase survives the encryption. + string strAudit = tmp + "/recovery-audit.txt"; + vector vAuditArgs; + vAuditArgs.push_back("-datadir=" + tmp); + vAuditArgs.push_back("-walletbackend=sqlite"); + vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); + vAuditArgs.push_back("-recoveryaudit"); + vAuditArgs.push_back("-nogui"); + int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); + nFail += Check(nAuditRet == 0 && + FileContainsText(strAudit, "recovery phrase: present"), + "the encrypted SQLite wallet still has its recovery phrase") ? 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); @@ -2981,6 +3166,8 @@ int RunSelfTest(const std::string& name) return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") return RunWalletEncryptSelfTest(); + if (name == "wallet-sqlite-encrypt") + return RunWalletSQLiteEncryptSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") @@ -2997,6 +3184,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-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-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 4dda8dd..374b2cc 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -1365,6 +1365,11 @@ int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); + // Capture the backend now: under SQLite, EncryptWallet() closes the runtime + // store to swap the file, so WalletSQLiteRuntimeActive() reads false by the + // time we would decide whether to purge Berkeley DB logs below. + bool fSQLiteBackend = WalletSQLiteRuntimeActive(); + string strBackup; string strError; if (!EncryptWallet(strPassphrase, strBackup, strError)) @@ -1375,6 +1380,8 @@ int CmdEncryptWallet(const std::string& strPassphrase) } printf("Wallet encrypted.\n"); + if (fSQLiteBackend) + printf("Wallet backend: SQLite (wallet.sqlite).\n"); printf("The old unencrypted wallet was moved to:\n"); printf(" %s\n", strBackup.c_str()); printf("That backup still contains private keys in plain text. Move it offline or\n"); @@ -1387,7 +1394,13 @@ int CmdEncryptWallet(const std::string& strPassphrase) // them. Closing the environment and dropping the logs is the last step of // encrypting, not an optimisation -- our own backup advice is to copy the // whole directory, and following it otherwise carries the keys along. - PurgeDbEnvironmentLogs(); + // + // The SQLite backend has no Berkeley DB environment behind the wallet, and + // EncryptWallet() already closed and swapped the SQLite store; there is + // nothing to purge, and calling into the environment here would only open + // one needlessly. + if (!fSQLiteBackend) + PurgeDbEnvironmentLogs(); return 0; } -- cgit v1.2.3 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/makefile.mingw') 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 From 477d57b9a01102b6f79ad9b10b2dcfd280faa1cb Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 16:26:34 -0300 Subject: gui: offer to convert a Berkeley DB wallet to SQLite on first run A first-run dialog for an existing wallet.dat with no backend choice yet: "Convert to SQLite" exports the wallet and records the sqlite marker so the next start opens it; "Keep Berkeley DB" records that choice so the offer does not return; "Decide later" leaves no marker and asks again. wallet.dat is never touched, and the change takes effect on restart -- matching the restart-to-apply model. The conversion itself is DoConvertWalletToSQLite(), a headless-testable function the button calls: it reuses the export path (extracted as ExportActiveWalletToSQLite, shared with -walletsqliteexport, which keeps its own specific messages), refuses if already on SQLite or if a wallet.sqlite exists, and writes the marker only after a verified export. New wallet-convert self-test proves the action end to end: build a Berkeley DB wallet, convert, and confirm wallet.sqlite + the marker are written, wallet.dat is left untouched, a second convert refuses, and a flagless restart opens the converted SQLite wallet with its phrase intact. All wallet suites pass. The modal itself is thin UI over this tested action. --- src/gui.cpp | 90 ++++++++++++++++++++++++++++++++++++++++ src/makefile.mingw | 1 + src/selftest.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.cpp | 119 +++++++++++++++++++++++++++++++++++++---------------- src/walletcmd.h | 9 ++++ 5 files changed, 290 insertions(+), 37 deletions(-) (limited to 'src/makefile.mingw') diff --git a/src/gui.cpp b/src/gui.cpp index 8dcb854..89a7ab6 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1391,6 +1391,84 @@ static void DrawAboutDialog() ImGui::End(); } +// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the +// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite +// and records the backend marker (wallet.dat is left untouched, so a restart +// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB" +// just records that choice so the offer does not return; "Decide later" leaves +// no marker and asks again next start. +static bool g_showBackendWizard = false; +static std::string g_wizardStatus; +static bool g_wizardConverted = false; + +static void DrawBackendWizardDialog() +{ + if (!g_showBackendWizard) return; + ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (ImGui::Begin("Wallet Storage", &g_showBackendWizard, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + if (g_wizardConverted) + { + ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite."); + ImGui::TextWrapped("%s", g_wizardStatus.c_str()); + ImGui::Spacing(); + ImGui::TextWrapped( + "Restart Bitflash to start using the SQLite wallet. Your Berkeley " + "DB wallet.dat is kept untouched as a fallback."); + ImGui::Spacing(); + if (ImGui::Button("Close", ImVec2(120.0f, 0.0f))) + g_showBackendWizard = false; + } + else + { + ImGui::TextWrapped( + "Bitflash can store your wallet in a new SQLite format: a single " + "file that is easier to back up and does not depend on the " + "database/ directory beside it."); + ImGui::Spacing(); + ImGui::TextWrapped( + "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it " + "is -- nothing is deleted, and you can go back to it at any time. " + "The change takes effect after you restart."); + ImGui::Spacing(); + if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f))) + { + int nCopied = 0; + std::string strError; + if (DoConvertWalletToSQLite(nCopied, strError)) + { + g_wizardStatus = + strprintf("%d record(s) copied to wallet.sqlite.", nCopied); + g_wizardConverted = true; + } + else + g_wizardStatus = "Conversion failed: " + strError; + } + ImGui::SameLine(); + if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f))) + { + WriteWalletBackendMarker("bdb"); + g_showBackendWizard = false; + } + ImGui::SameLine(); + if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f))) + g_showBackendWizard = false; + + if (!g_wizardStatus.empty()) + { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f)); + ImGui::TextWrapped("%s", g_wizardStatus.c_str()); + ImGui::PopStyleColor(); + } + } + } + ImGui::End(); +} + // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- @@ -1457,6 +1535,17 @@ int RunGUI(int argc, char* argv[]) g_mineRadio = nMineMode; strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); + // First run on a Berkeley DB wallet with no recorded choice yet: offer to + // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists, + // or once a choice has been recorded in the backend marker. + if (!WalletSQLiteRuntimeActive() && + FileExists((GetAppDir() + "/wallet.dat").c_str()) && + !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) && + ReadWalletBackendMarker().empty()) + { + g_showBackendWizard = true; + } + int frame = 0; while (!glfwWindowShouldClose(window) && !fShutdown) { @@ -1480,6 +1569,7 @@ int RunGUI(int argc, char* argv[]) DrawRestorePhraseDialog(); DrawDiagnosticsDialog(); DrawAboutDialog(); + DrawBackendWizardDialog(); DrawStatusBar(); ImGui::Render(); diff --git a/src/makefile.mingw b/src/makefile.mingw index 06c9211..55f186b 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -114,6 +114,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=wallet-sqlite-encrypt ./bitflash.exe -selftest=wallet-backend-default + ./bitflash.exe -selftest=wallet-convert ./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 1960003..a9c73fe 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -2664,6 +2664,110 @@ static int RunWalletBackendDefaultSelfTest() return nFail == 0 ? 0 : 1; } +// The wizard's conversion action, headless: build a Berkeley DB wallet, run +// DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a +// usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then +// that a flagless restart opens the converted SQLite wallet. +static int RunWalletConvertSelfTest() +{ + fflush(stdout); + printf("wallet-convert 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 + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + string strError; + nFail += Check(SetHDSeedFromMnemonic( + "abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon about", strError), + "a phrase can be installed before conversion") ? 0 : 1; + TopUpKeyPool(); + + vector vchDefaultPubKey = keyUser.GetPubKey(); + CPrivKey vchDefaultPrivKey; + nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), + "the default private key is readable before conversion") ? 0 : 1; + vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), + vchDefaultPrivKey.end()); + + // The conversion action the GUI button invokes. + int nCopied = 0; + string strConvertError; + bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError); + nFail += Check(fConverted && nCopied > 0, + "DoConvertWalletToSQLite exports the wallet") ? 0 : 1; + nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) && + FileContainsText(tmp + "/wallet-backend", "sqlite"), + "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1; + nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes), + "wallet.dat is left untouched by the conversion") ? 0 : 1; + + // Idempotence guard: a second conversion refuses rather than overwrite. + int nCopied2 = 0; + string strSecondError; + nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError), + "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1; + + DBFlush(true); + + // A flagless restart now opens the converted SQLite wallet (the marker + // routes it) and finds the same recovery phrase. + string strAudit = tmp + "/converted-recovery-audit.txt"; + vector vAuditArgs; + vAuditArgs.push_back("-datadir=" + tmp); + vAuditArgs.push_back("-nomanagedtor"); + vAuditArgs.push_back("-nogui"); + vAuditArgs.push_back("-recoveryaudit"); + int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); + nFail += Check(nAuditRet == 0 && + FileContainsText(strAudit, "recovery phrase: present"), + "a flagless restart opens the converted SQLite wallet") ? 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); @@ -3398,6 +3502,8 @@ int RunSelfTest(const std::string& name) return RunWalletSQLiteEncryptSelfTest(); if (name == "wallet-backend-default") return RunWalletBackendDefaultSelfTest(); + if (name == "wallet-convert") + return RunWalletConvertSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") @@ -3414,6 +3520,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-backend-default, 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-convert, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 374b2cc..1f1fd27 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -1070,79 +1070,126 @@ public: } // namespace -int CmdWalletSQLiteExport(const std::string& strDest) +// Export the currently-loaded wallet to a SQLite file. Shared by the +// -walletsqliteexport command and the GUI conversion wizard: opens the +// destination, copies every record inside one transaction, checkpoints, and +// verifies the stored count matches. Returns false with strErrorRet set on any +// failure and leaves no partial file behind; the source wallet is only read. +bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, + std::string& strErrorRet) { - AttachTerminal(); - + nCopiedRet = 0; if (strDest.empty()) { - fprintf(stderr, "Missing SQLite wallet export path.\n"); - return 1; + strErrorRet = "no SQLite export path was given"; + return false; } if (FileExists(strDest.c_str())) { - fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", - strDest.c_str()); - return 1; + strErrorRet = strprintf("refusing to overwrite existing file: %s", strDest.c_str()); + return false; } - string strError; CWalletDBSQLite db; - if (!db.Open(strDest, strError)) + if (!db.Open(strDest, strErrorRet)) { RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.BeginTransaction(strError)) + if (!db.BeginTransaction(strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - int nCopied = 0; - CWalletSQLiteExportVisitor visitor(db, nCopied); - if (!ScanWalletRecords(visitor, strError)) + CWalletSQLiteExportVisitor visitor(db, nCopiedRet); + if (!ScanWalletRecords(visitor, strErrorRet)) { string strRollbackError; db.RollbackTransaction(strRollbackError); db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.CommitTransaction(strError)) + if (!db.CommitTransaction(strErrorRet) || !db.Checkpoint(strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - if (!db.Checkpoint(strError)) + + int nCount = 0; + if (!db.CountRecords(nCount, strErrorRet)) { db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); - return 1; + return false; } - - int nCount = 0; - if (!db.CountRecords(nCount, strError)) + if (nCount != nCopiedRet) { + strErrorRet = strprintf("export count mismatch: copied %d, stored %d", + nCopiedRet, nCount); db.Close(); RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", - strError.c_str()); + return false; + } + db.Close(); + return true; +} + +// Convert the running Berkeley DB wallet to SQLite for the GUI wizard: export +// wallet.dat to /wallet.sqlite, then record the backend marker so the +// next start opens SQLite. wallet.dat is never modified -- it stays as the +// fallback. Refuses if already on SQLite or if a wallet.sqlite already exists. +bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet) +{ + nCopiedRet = 0; + if (WalletSQLiteRuntimeActive()) + { + strErrorRet = "this wallet is already on the SQLite backend"; + return false; + } + string strDest = GetAppDir() + "/wallet.sqlite"; + if (FileExists(strDest.c_str())) + { + strErrorRet = strprintf("a SQLite wallet already exists at %s", strDest.c_str()); + return false; + } + if (!ExportActiveWalletToSQLite(strDest, nCopiedRet, strErrorRet)) + return false; + if (!WriteWalletBackendMarker("sqlite")) + { + strErrorRet = "the wallet was exported but the backend choice could not be saved"; + return false; + } + return true; +} + +int CmdWalletSQLiteExport(const std::string& strDest) +{ + AttachTerminal(); + + // Keep the command's own specific messages for the two conditions the + // shared helper folds into a generic error, so scripts and the tests that + // read this output see the same wording as before. + if (strDest.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); return 1; } - if (nCount != nCopied) + if (FileExists(strDest.c_str())) { - db.Close(); - RemoveSQLiteExportFiles(strDest); - fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", - nCopied, nCount); + fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", + strDest.c_str()); + return 1; + } + + int nCopied = 0; + string strError; + if (!ExportActiveWalletToSQLite(strDest, nCopied, strError)) + { + fprintf(stderr, "Cannot export wallet to SQLite: %s\n", strError.c_str()); return 1; } diff --git a/src/walletcmd.h b/src/walletcmd.h index d993856..38cbfdb 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -75,6 +75,15 @@ int CmdWalletStorageCheck(); // not change which backend the runtime wallet opens. int CmdWalletSQLiteExport(const std::string& strDest); +// Export the currently-loaded wallet to a SQLite file (shared by the command +// above and the GUI wizard). Returns false with an error on any failure. +bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet, + std::string& strErrorRet); + +// Convert the running Berkeley DB wallet to SQLite and record the backend +// marker so the next start opens it. wallet.dat is left untouched as fallback. +bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet); + // Diagnostic/migration staging: compare a SQLite wallet export with the current // wallet.dat raw records. Prints counts only, never record keys or values. int CmdWalletSQLiteVerify(const std::string& strPath); -- cgit v1.2.3