From 95bd9cbcb1d17f4bc6223e6bfe3a92bfb8c3067d Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 09:08:29 -0300 Subject: wallet: add read-only record scan boundary --- src/db.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) (limited to 'src/db.cpp') diff --git a/src/db.cpp b/src/db.cpp index 9f62964..4fa72af 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -932,6 +932,45 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) return true; } +bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) +{ + class CWalletScanDB : public CWalletDB + { + public: + CWalletScanDB() : CWalletDB("r") { } + using CDB::GetCursor; + using CDB::ReadAtCursor; + }; + + strErrorRet.clear(); + CWalletScanDB dbWallet; + Dbc* pcursor = dbWallet.GetCursor(); + if (!pcursor) + { + strErrorRet = "could not open a wallet cursor"; + return false; + } + CAutoCursor cursorGuard(pcursor); + + loop + { + CDataStream ssKey(SER_DISK); + CDataStream ssValue(SER_DISK); + int ret = dbWallet.ReadAtCursor(pcursor, ssKey, ssValue); + if (ret == DB_NOTFOUND) + break; + if (ret != 0) + { + strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret); + return false; + } + if (!visitor.VisitWalletRecord(ssKey, ssValue, strErrorRet)) + return false; + } + + return true; +} + bool LoadWallet() { vector vchDefaultKey; -- cgit v1.2.3 From 5352185c7198a183c5906898e16bc4a6bb4e8654 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 09:55:53 -0300 Subject: wallet: route rewrite copies through record scan --- docs/storage.md | 4 ++- src/db.cpp | 105 +++++++++++++++++++++++++++----------------------------- 2 files changed, 54 insertions(+), 55 deletions(-) (limited to 'src/db.cpp') diff --git a/docs/storage.md b/docs/storage.md index e533589..e4a66d2 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -43,7 +43,9 @@ level schema migration is intentionally introduced. `wallet.dat` is accessed through `CWalletDB`. Read-only tooling should go through `ScanWalletRecords()` instead of opening a Berkeley DB cursor directly. That keeps migration checks, audits, and future -backends behind one record-stream boundary. +backends behind one record-stream boundary. Wallet rewrite code should also use +that boundary when it needs to copy existing records into a newly written +wallet file. | Key shape | Value | Secret? | Meaning | |---|---|---:|---| diff --git a/src/db.cpp b/src/db.cpp index 4fa72af..b90219e 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -1354,70 +1354,68 @@ public: { return Write(make_pair(string("pool"), nIndex), vchPubKey); } +}; + +static bool IsWalletEncryptionPrivateRecord(const string& strType) +{ + return strType == "key" || + strType == "pool" || + strType == "hdmaster" || + strType == "hdchaincode" || + strType == "mkey" || + strType == "ckey" || + strType == "cryptedhdmaster" || + strType == "cryptedhdchaincode" || + strType == "walletminversion"; +} - bool CopyPublicRecordsTo(CWalletRewriteDB& dbTo, int& nCopiedRet, string& strErrorRet) +class CWalletPublicRecordCopyVisitor : public CWalletRecordVisitor +{ +public: + CWalletRewriteDB& dbTo; + int& nCopied; + + CWalletPublicRecordCopyVisitor(CWalletRewriteDB& dbToIn, int& nCopiedIn) + : dbTo(dbToIn), nCopied(nCopiedIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) { - nCopiedRet = 0; - Dbc* pcursor = GetCursor(); - if (!pcursor) + string strType; + try { - strErrorRet = "could not open a wallet cursor"; - return false; + CDataStream ssType = ssKey; + ssType >> strType; } - - unsigned int fFlags = DB_NEXT; - loop + catch (...) { - CDataStream ssKey; - CDataStream ssValue; - int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags); - if (ret == DB_NOTFOUND) - break; - if (ret != 0) - { - pcursor->close(); - strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret); - return false; - } - - string strType; - try - { - CDataStream ssType = ssKey; - ssType >> strType; - } - catch (...) - { - pcursor->close(); - strErrorRet = "wallet.dat has a record whose key type cannot be read"; - return false; - } + strErrorRet = "wallet.dat has a record whose key type cannot be read"; + return false; + } - if (strType == "key" || - strType == "pool" || - strType == "hdmaster" || - strType == "hdchaincode" || - strType == "mkey" || - strType == "ckey" || - strType == "cryptedhdmaster" || - strType == "cryptedhdchaincode" || - strType == "walletminversion") - continue; + if (IsWalletEncryptionPrivateRecord(strType)) + return true; - if (!dbTo.WriteRaw(ssKey, ssValue)) - { - pcursor->close(); - strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); - return false; - } - nCopiedRet++; + if (!dbTo.WriteRaw(ssKey, ssValue)) + { + strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); + return false; } - - pcursor->close(); + nCopied++; return true; } }; +static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo, + int& nCopiedRet, + string& strErrorRet) +{ + nCopiedRet = 0; + CWalletPublicRecordCopyVisitor visitor(dbTo, nCopiedRet); + return ScanWalletRecords(visitor, strErrorRet); +} + static string WalletEncryptBackupPath() { string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; @@ -1594,9 +1592,8 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st int nCopied = 0; { - CWalletRewriteDB dbSource("wallet.dat", "r"); CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+"); - if (!dbSource.CopyPublicRecordsTo(dbTarget, nCopied, strErrorRet)) + if (!CopyPublicWalletRecordsTo(dbTarget, nCopied, strErrorRet)) { remove(strTempPath.c_str()); return false; -- cgit v1.2.3 From dd9db5ba5407b9c30b4f41fc1153db39f0770154 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 11:08:34 -0300 Subject: wallet: install backups via temporary file --- docs/storage.md | 3 ++- src/db.cpp | 40 ++++++++++++++++++++++++++++++++++------ src/selftest.cpp | 7 +++++++ 3 files changed, 43 insertions(+), 7 deletions(-) (limited to 'src/db.cpp') diff --git a/docs/storage.md b/docs/storage.md index e4a66d2..c1cda45 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -77,7 +77,8 @@ A future wallet backend must keep these properties: 3. A locked encrypted wallet may expose public keys and balances, but not private keys or decrypted HD seed material. 4. `-backupwallet` must produce a wallet file that opens in a different datadir - without copying `database/`. + without copying `database/`, and must not leave a half-written final backup + path if the copy or install step fails. 5. `-recoveryaudit` must distinguish: - no recovery phrase installed; - recovery phrase present; diff --git a/src/db.cpp b/src/db.cpp index b90219e..0ff58e7 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -1030,6 +1030,13 @@ bool BackupWallet(const string& strDest) if (!FileExists(strSrc.c_str())) return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); + string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime()); + for (int i = 1; FileExists(strTemp.c_str()); i++) + strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i); + string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime()); + for (int i = 1; FileExists(strOld.c_str()); i++) + strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i); + CRITICAL_BLOCK(cs_db) { // Committing first, so the copy is not missing the newest records. @@ -1044,11 +1051,11 @@ bool BackupWallet(const string& strDest) FILE* pfIn = fopen(strSrc.c_str(), "rb"); if (!pfIn) return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); - FILE* pfOut = fopen(strDest.c_str(), "wb"); + FILE* pfOut = fopen(strTemp.c_str(), "wb"); if (!pfOut) { fclose(pfIn); - return error("BackupWallet() : cannot write %s\n", strDest.c_str()); + return error("BackupWallet() : cannot write %s\n", strTemp.c_str()); } char buf[65536]; @@ -1065,18 +1072,39 @@ bool BackupWallet(const string& strDest) if (!fOk) { - remove(strDest.c_str()); // half a wallet is worse than none - return error("BackupWallet() : copy to %s failed\n", strDest.c_str()); + remove(strTemp.c_str()); // half a wallet is worse than none + return error("BackupWallet() : copy to %s failed\n", strTemp.c_str()); } // Without this the copy is welded to this node's database/ dir. - int ret = dbenv.lsn_reset(strDest.c_str(), 0); + int ret = dbenv.lsn_reset(strTemp.c_str(), 0); if (ret != 0) printf("BackupWallet() : warning -- lsn_reset returned %d; the copy " "may only open beside this node's database/ directory\n", ret); + + bool fHadOldDest = FileExists(strDest.c_str()); + if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0) + { + remove(strTemp.c_str()); + return error("BackupWallet() : could not move old %s aside\n", strDest.c_str()); + } + if (rename(strTemp.c_str(), strDest.c_str()) != 0) + { + remove(strTemp.c_str()); + if (fHadOldDest) + rename(strOld.c_str(), strDest.c_str()); + return error("BackupWallet() : could not install %s\n", strDest.c_str()); + } + if (fHadOldDest) + remove(strOld.c_str()); } catch (const std::exception& e) - { return error("BackupWallet() : %s\n", e.what()); } + { + remove(strTemp.c_str()); + if (FileExists(strOld.c_str()) && !FileExists(strDest.c_str())) + rename(strOld.c_str(), strDest.c_str()); + return error("BackupWallet() : %s\n", e.what()); + } } // Recorded here rather than by whoever called, so that a backup taken with diff --git a/src/selftest.cpp b/src/selftest.cpp index 638aa66..81c4439 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1391,6 +1391,13 @@ static int RunWalletPortabilitySelfTest() nFail += Check(nBackupRet == 0 && FileExists(strPortableWallet.c_str()), "/backupwallet writes a portable wallet.dat") ? 0 : 1; + string strBackupOut2 = tmp + "/backupwallet-overwrite.txt"; + int nBackupRet2 = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut2); + nFail += Check(nBackupRet2 == 0 && FileExists(strPortableWallet.c_str()) && + !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.tmp.") && + !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.old."), + "/backupwallet replaces an existing backup without leaving temp files") ? 0 : 1; + nFail += Check(CopyFileLocal(strOriginal + "/wallet.dat", strRawCopy + "/wallet.dat"), "a raw wallet.dat can be copied without database logs") ? 0 : 1; -- cgit v1.2.3 From 82c3e10da01260650ddd2944e3f895ccf2c07ffc Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 12:59:03 -0300 Subject: db: recreate Berkeley DB environment after shutdown --- src/db.cpp | 93 +++++++++++++++++++++++++++++++++++++------------------- src/db.h | 4 +-- src/main_gui.cpp | 3 +- src/selftest.cpp | 66 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 36 deletions(-) (limited to 'src/db.cpp') diff --git a/src/db.cpp b/src/db.cpp index 0ff58e7..8b82fe5 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -52,9 +52,34 @@ static bool fDbEnvInit = false; // Why LoadWallet() refused, in words meant for the person who has to act on it. // Empty unless the reason is one we can name. string strWalletLoadError; -DbEnv dbenv(0u); +static DbEnv* pdbenv = NULL; +static FILE* pdbErrFile = NULL; static map mapFileUseCount; +DbEnv& GetDbEnv() +{ + if (!pdbenv) + pdbenv = new DbEnv(0u); + return *pdbenv; +} + +static void CloseDbEnv() +{ + if (pdbenv) + { + try { pdbenv->close(0); } + catch (...) { } + delete pdbenv; + pdbenv = NULL; + } + if (pdbErrFile) + { + fclose(pdbErrFile); + pdbErrFile = NULL; + } + fDbEnvInit = false; +} + class CDBInit { public: @@ -64,11 +89,7 @@ public: ~CDBInit() { if (fDbEnvInit) - { - try { dbenv.close(0); } - catch (...) { } - fDbEnvInit = false; - } + CloseDbEnv(); } } instance_of_cdbinit; @@ -104,24 +125,30 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) printf("dbenv.open strAppDir=%s\n", strAppDir.c_str()); - dbenv.set_lg_dir(strLogDir.c_str()); - dbenv.set_lg_max(10000000); - dbenv.set_lk_max_locks(10000); - dbenv.set_lk_max_objects(10000); - dbenv.set_errfile(fopen("db.log", "a")); /// debug - ///dbenv.log_set_config(DB_LOG_AUTO_REMOVE, 1); /// causes corruption - ret = dbenv.open(strAppDir.c_str(), - DB_CREATE | - DB_INIT_LOCK | - DB_INIT_LOG | - DB_INIT_MPOOL | - DB_INIT_TXN | - DB_THREAD | - DB_PRIVATE | - DB_RECOVER, - 0); + DbEnv& env = GetDbEnv(); + env.set_lg_dir(strLogDir.c_str()); + env.set_lg_max(10000000); + env.set_lk_max_locks(10000); + env.set_lk_max_objects(10000); + pdbErrFile = fopen("db.log", "a"); /// debug + if (pdbErrFile) + env.set_errfile(pdbErrFile); + ///env.log_set_config(DB_LOG_AUTO_REMOVE, 1); /// causes corruption + ret = env.open(strAppDir.c_str(), + DB_CREATE | + DB_INIT_LOCK | + DB_INIT_LOG | + DB_INIT_MPOOL | + DB_INIT_TXN | + DB_THREAD | + DB_PRIVATE | + DB_RECOVER, + 0); if (ret > 0) + { + CloseDbEnv(); throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret)); + } fDbEnvInit = true; // A Berkeley DB file records, in every page header, a log sequence @@ -151,7 +178,7 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) continue; try { - if (dbenv.lsn_reset(pszName, 0) == 0) + if (env.lsn_reset(pszName, 0) == 0) printf("CDB() : adopted %s from another environment " "(log sequence numbers reset)\n", pszName); } @@ -164,7 +191,7 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) ++mapFileUseCount[strFile]; } - pdb = new Db(&dbenv, 0); + pdb = new Db(&GetDbEnv(), 0); ret = pdb->open(NULL, // Txn pointer pszFile, // Filename @@ -199,7 +226,8 @@ void CDB::Close() try { pdb->close(0); } catch (...) { } delete pdb; pdb = NULL; - try { dbenv.txn_checkpoint(0, 0, 0); } catch (...) { } + if (fDbEnvInit) + try { GetDbEnv().txn_checkpoint(0, 0, 0); } catch (...) { } CRITICAL_BLOCK(cs_db) --mapFileUseCount[strFile]; @@ -279,7 +307,8 @@ void DBFlush(bool fShutdown) // takes the process down before the wallet has been put down cleanly, // which is the difference between a portable wallet.dat and one welded // to this directory -- the lsn_reset just below is what frees it. - try { dbenv.txn_checkpoint(0, 0, 0); } + DbEnv& env = GetDbEnv(); + try { env.txn_checkpoint(0, 0, 0); } catch (const std::exception& e) { printf("DBFlush() : checkpoint failed: %s\n", e.what()); } @@ -290,7 +319,7 @@ void DBFlush(bool fShutdown) int nRefCount = (*mi).second; if (nRefCount == 0) { - try { dbenv.lsn_reset(strFile.c_str(), 0); } + try { env.lsn_reset(strFile.c_str(), 0); } catch (const std::exception& e) { printf("DBFlush() : lsn_reset(%s) failed: %s\n", strFile.c_str(), e.what()); } mapFileUseCount.erase(mi++); @@ -310,9 +339,8 @@ void DBFlush(bool fShutdown) // Found while making wallet encryption clean up after itself: // calling DBFlush(true) a second time crashed here every time. if (mapFileUseCount.empty()) - try { dbenv.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } - try { dbenv.close(0); } catch (...) { } - fDbEnvInit = false; + try { env.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } + CloseDbEnv(); } } } @@ -1042,7 +1070,8 @@ bool BackupWallet(const string& strDest) // Committing first, so the copy is not missing the newest records. // Both calls can throw, and a failed backup must not take the node // down with it. - try { dbenv.txn_checkpoint(0, 0, 0); } + DbEnv& env = GetDbEnv(); + try { env.txn_checkpoint(0, 0, 0); } catch (const std::exception& e) { return error("BackupWallet() : checkpoint failed: %s\n", e.what()); } @@ -1077,7 +1106,7 @@ bool BackupWallet(const string& strDest) } // Without this the copy is welded to this node's database/ dir. - int ret = dbenv.lsn_reset(strTemp.c_str(), 0); + int ret = env.lsn_reset(strTemp.c_str(), 0); if (ret != 0) printf("BackupWallet() : warning -- lsn_reset returned %d; the copy " "may only open beside this node's database/ directory\n", ret); diff --git a/src/db.h b/src/db.h index 2816957..53bda59 100644 --- a/src/db.h +++ b/src/db.h @@ -48,7 +48,7 @@ public: }; -extern DbEnv dbenv; +extern DbEnv& GetDbEnv(); extern void DBFlush(bool fShutdown); // Close the environment and delete its logs. Only the encryption command calls // this: the logs hold the plaintext records the old wallet wrote. @@ -237,7 +237,7 @@ public: if (!pdb) return false; DbTxn* ptxn = NULL; - int ret = dbenv.txn_begin(GetTxn(), &ptxn, 0); + int ret = GetDbEnv().txn_begin(GetTxn(), &ptxn, 0); if (!ptxn || ret != 0) return false; vTxn.push_back(ptxn); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index feda428..705b4ef 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -114,7 +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(" wallet-crypto, wallet-encrypt, wallet-portability, net-message,\n"); + printf(" db-env-reopen, 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"); printf("\n"); diff --git a/src/selftest.cpp b/src/selftest.cpp index 54249b1..31c168f 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -968,6 +968,68 @@ static int RunWalletStorageSanitySelfTest() return nFail == 0 ? 0 : 1; } +static int RunDbEnvReopenSelfTest() +{ + fflush(stdout); + printf("db-env-reopen self-test\n"); + + std::string tmp; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + + int nFail = 0; + std::string strOldDataDir = strSetDataDir; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + try + { + for (int i = 0; i < 5; i++) + { + int64 nValue = 1000 + i; + { + CWalletDB walletdb("cr+"); + nFail += Check(walletdb.WriteSetting("db-env-reopen-cycle", nValue), + "wallet setting can be written before shutdown") ? 0 : 1; + } + + DBFlush(true); + + int64 nRead = 0; + { + CWalletDB walletdb("r"); + nFail += Check(walletdb.ReadSetting("db-env-reopen-cycle", nRead), + "wallet setting can be read after env reopen") ? 0 : 1; + } + nFail += Check(nRead == nValue, + "reopened environment reads the last committed value") ? 0 : 1; + + DBFlush(true); + } + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + strSetDataDir = strOldDataDir; + 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) @@ -2225,6 +2287,8 @@ int RunSelfTest(const std::string& name) return RunWalletFormatSelfTest(); if (name == "wallet-storage-sanity") return RunWalletStorageSanitySelfTest(); + if (name == "db-env-reopen") + return RunDbEnvReopenSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2245,6 +2309,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, 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-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } -- cgit v1.2.3 From f9ee957bd6d3f28dfee166a82f389aad666a0119 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 18:17:23 -0300 Subject: wallet: stage SQLite runtime loading --- docs/storage.md | 4 + src/db.cpp | 361 +++++++++++++++++++++++++++++++++++++++++++++++--- src/db.h | 1 + src/main_gui.cpp | 22 +++ src/selftest.cpp | 29 ++++ src/wallet_sqlite.cpp | 19 +++ src/wallet_sqlite.h | 1 + 7 files changed, 420 insertions(+), 17 deletions(-) (limited to 'src/db.cpp') diff --git a/docs/storage.md b/docs/storage.md index 07c7170..08c4a01 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -120,6 +120,10 @@ file and failed restores remove the incomplete `wallet.dat`. `-walletsqliteloadcheck=FILE` parses a SQLite export using the same record types the wallet loader understands, so a migration can prove the export is not just byte-preserving but loader-compatible before runtime activation. +`-walletsqlite=FILE` is the first runtime staging flag: it loads a SQLite export +through the startup wallet path, initializes the in-memory wallet state, reports +success, and exits before network, mining, GUI, or wallet mutation begins. It is +deliberately read-only until the write path is routed to SQLite as well. ## `blkindex.dat` diff --git a/src/db.cpp b/src/db.cpp index 8b82fe5..581e5a7 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -3,6 +3,7 @@ // file license.txt or http://www.opensource.org/licenses/mit-license.php. #include "headers.h" +#include "wallet_sqlite.h" @@ -650,6 +651,289 @@ bool CReviewDB::WriteReviews(uint256 hash, const vector& vReviews) // CWalletDB // +class CSQLiteWalletRuntimeLoadVisitor : public CWalletRecordVisitor +{ +public: + bool fHaveStoredMineMode; + bool fHaveStoredHDCoinType; + string strLastType; + + CSQLiteWalletRuntimeLoadVisitor() : + fHaveStoredMineMode(false), + fHaveStoredHDCoinType(false) + { + } + + bool VisitWalletRecord(const CDataStream& ssKeyIn, + const CDataStream& ssValueIn, + string& strErrorRet) + { + CDataStream ssKey = ssKeyIn; + CDataStream ssValue = ssValueIn; + string strType; + ssKey >> strType; + strLastType = strType; + + if (strType == "name") + { + string strAddress; + ssKey >> strAddress; + ssValue >> mapAddressBook[strAddress]; + } + else if (strType == "tx") + { + uint256 hash; + ssKey >> hash; + CWalletTx& wtx = mapWallet[hash]; + ssValue >> wtx; + + if (wtx.GetHash() != hash) + printf("Error in SQLite wallet export, hash mismatch\n"); + } + else if (strType == "key") + { + vector vchPubKey; + ssKey >> vchPubKey; + CPrivKey vchPrivKey; + ssValue >> vchPrivKey; + + mapKeys[vchPubKey] = vchPrivKey; + mapPubKeys[Hash160(vchPubKey)] = vchPubKey; + } + else if (strType == "mkey") + { + unsigned int nID = 0; + ssKey >> nID; + CWalletMasterKey kMasterKey; + ssValue >> kMasterKey; + mapMasterKeys[nID] = kMasterKey; + nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID); + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "ckey") + { + vector vchPubKey; + ssKey >> vchPubKey; + vector vchCryptedSecret; + ssValue >> vchCryptedSecret; + if (!AddCryptedKey(vchPubKey, vchCryptedSecret)) + { + strErrorRet = "SQLite wallet export has an unreadable encrypted key record"; + return false; + } + } + else if (strType == "defaultkey") + { + ssValue >> vchDefaultKey; + } + else if (strType == "hdmaster") + { + ssValue >> vchHDMaster; + } + else if (strType == "hdchaincode") + { + ssValue >> vchHDChainCode; + } + else if (strType == "hdnext") + { + ssValue >> nHDNext; + } + else if (strType == "hdschema") + { + ssValue >> nHDKeySchema; + } + else if (strType == "hdcointype") + { + ssValue >> nHDCoinType; + fHaveStoredHDCoinType = true; + } + else if (strType == "hdreceivenext") + { + ssValue >> nHDReceiveNext; + } + else if (strType == "hdchangenext") + { + ssValue >> nHDChangeNext; + } + else if (strType == "cryptedhdmaster") + { + ssValue >> vchCryptedHDMaster; + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "cryptedhdchaincode") + { + ssValue >> vchCryptedHDChainCode; + fWalletEncrypted = true; + fWalletLocked = true; + } + else if (strType == "walletminversion") + { + int nMinVersion = 0; + ssValue >> nMinVersion; + if (nMinVersion > WALLET_FORMAT_SUPPORTED) + { + strWalletLoadError = strprintf( + "This SQLite wallet export was written by a newer version of " + "Bitflash (wallet format %d; this build understands %d). " + "Upgrade Bitflash before opening it.", + nMinVersion, WALLET_FORMAT_SUPPORTED); + strErrorRet = strWalletLoadError; + return false; + } + } + else if (strType == "pool") + { + int64 nIndex; + ssKey >> nIndex; + vector vchPubKey; + ssValue >> vchPubKey; + mapKeyPool[nIndex] = vchPubKey; + } + else if (strType == "setting") + { + string strKey; + ssKey >> strKey; + if (strKey == "nTransactionFee") ssValue >> nTransactionFee; + if (strKey == "addrIncoming") ssValue >> addrIncoming; + + if (!fMineModeFromCommandLine) + { + if (strKey == "nMineMode") { ssValue >> nMineMode; fHaveStoredMineMode = true; } + if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins; + if (strKey == "strParticipantPool") ssValue >> strParticipantPool; + } + } + + if (ssKey.fail()) + { + strErrorRet = strprintf("malformed SQLite wallet record key while handling '%s'", + strLastType.c_str()); + return false; + } + if (ssValue.fail()) + { + strErrorRet = strprintf("malformed SQLite wallet record value while handling '%s'", + strLastType.c_str()); + return false; + } + return true; + } + + vector vchDefaultKey; +}; + +static bool FinishLoadedWallet(const vector& vchDefaultKey, + bool fMayCreateDefaultKey) +{ + if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) + { + keyUser.SetPubKey(vchDefaultKey); + if (mapKeys.count(vchDefaultKey)) + keyUser.SetPrivKey(mapKeys[vchDefaultKey]); + } + else if (IsWalletEncrypted()) + { + printf("LoadWallet: encrypted wallet has no usable default public key. " + "The file was left untouched.\n"); + return false; + } + else + { + if (!fMayCreateDefaultKey) + { + strWalletLoadError = + "SQLite wallet export has no usable default public key"; + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; + } + + keyUser.MakeNewKey(); + if (!AddKey(keyUser)) + return false; + if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address")) + return false; + CWalletDB().WriteDefaultKey(keyUser.GetPubKey()); + } + + return true; +} + +static bool ReconcileLoadedWalletMetadata(bool fHaveStoredMineMode, + bool fHaveStoredHDCoinType) +{ + // An encrypted wallet has a seed; it just has not been decrypted yet. + // + // The reset in the else branch was written when "no seed in memory after + // loading" could only mean "this wallet has no seed". With encryption that + // is no longer true, and taking the else branch here wiped the schema and + // the derivation counters that had just been read correctly out of the + // file. Unlocking later restores the seed but nothing restores those, so a + // BIP44 wallet came back deriving legacy addresses -- different addresses + // from the ones its own recovery phrase produces, while the audit reported + // the phrase as covering nothing. + if (HaveHDSeed() || !vchCryptedHDMaster.empty()) + { + if (nHDKeySchema == HD_SCHEMA_NONE) + { + nHDKeySchema = HD_SCHEMA_LEGACY; + printf("LoadWallet: deterministic seed has no schema metadata; " + "treating it as legacy m/index'\n"); + } + else if (nHDKeySchema != HD_SCHEMA_LEGACY && + nHDKeySchema != HD_SCHEMA_BIP44) + { + printf("LoadWallet: deterministic wallet schema %d (%s) is not " + "supported by this build\n", + nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str()); + return false; + } + if (!fHaveStoredHDCoinType) + { + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; + printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; " + "using Bitflash coin type %u\n", nHDCoinType); + } + } + else + { + nHDKeySchema = HD_SCHEMA_NONE; + nHDNext = 0; + nHDReceiveNext = 0; + nHDChangeNext = 0; + nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH; + } + + // fGenerateBitcoins and nMineMode only mean anything together, and a + // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already + // wrote fGenerateBitcoins when mining was toggled, years before this fork + // had a mode at all. Restoring that lone flag put a node into the one state + // the miner cannot act on -- generate on, mode relay -- so every miner + // thread started and returned at the relay guard, and the machine sat there + // looking like it was mining while hashing nothing. That happened to a + // 32-core node on the first 1.2.11 start. + // + // So the flag is only believed when the mode it belongs to was stored with + // it, and the pair is made coherent either way. + if (!fMineModeFromCommandLine) + { + if (!fHaveStoredMineMode) + { + if (fGenerateBitcoins) + printf("LoadWallet: ignoring a stored fGenerateBitcoins with no mining mode " + "beside it -- it predates this setting and cannot be read on its own\n"); + fGenerateBitcoins = 0; + } + else + { + fGenerateBitcoins = (nMineMode != MINE_RELAY) ? 1 : 0; + } + } + + return true; +} + bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { // Whether wallet.dat actually carried a mining mode, as opposed to just the @@ -1005,31 +1289,74 @@ bool LoadWallet() if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) return false; - if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) + return FinishLoadedWallet(vchDefaultKey, true); +} + +bool LoadWalletFromSQLite(const string& strPath) +{ + strWalletLoadError.clear(); + + if (!FileExists(strPath.c_str())) { - // Set keyUser - keyUser.SetPubKey(vchDefaultKey); - if (mapKeys.count(vchDefaultKey)) - keyUser.SetPrivKey(mapKeys[vchDefaultKey]); + strWalletLoadError = strprintf("SQLite wallet export does not exist: %s", + strPath.c_str()); + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; } - else if (IsWalletEncrypted()) + + CWalletDBSQLite sqlite; + string strError; + if (!sqlite.OpenReadOnly(strPath, strError)) { - printf("LoadWallet: encrypted wallet has no usable default public key. " - "The file was left untouched.\n"); + strWalletLoadError = strError; + printf("LoadWallet: cannot open SQLite wallet export: %s\n", + strError.c_str()); return false; } - else + + CSQLiteWalletRuntimeLoadVisitor visitor; + try { - // Create new keyUser and set as default key - keyUser.MakeNewKey(); - if (!AddKey(keyUser)) - return false; - if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address")) - return false; - CWalletDB().WriteDefaultKey(keyUser.GetPubKey()); + CRITICAL_BLOCK(cs_mapKeys) + CRITICAL_BLOCK(cs_mapWallet) + { + if (!sqlite.ScanRecords(visitor, strError)) + { + if (strWalletLoadError.empty()) + strWalletLoadError = strError; + printf("LoadWallet: SQLite wallet export 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) + { + strWalletLoadError = strprintf( + "SQLite wallet export 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 (...) + { + strWalletLoadError = strprintf( + "SQLite wallet export could not be read while handling a '%s' record", + visitor.strLastType.c_str()); + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; } - return true; + if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode, + visitor.fHaveStoredHDCoinType)) + { + if (strWalletLoadError.empty()) + strWalletLoadError = "SQLite wallet metadata is not supported by this build"; + return false; + } + + return FinishLoadedWallet(visitor.vchDefaultKey, false); } diff --git a/src/db.h b/src/db.h index 53bda59..59ff24a 100644 --- a/src/db.h +++ b/src/db.h @@ -536,6 +536,7 @@ public: bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet); bool LoadWallet(); +bool LoadWalletFromSQLite(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 c9aac7d..ee875c7 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -189,6 +189,7 @@ static void PrintUsage() printf(" an empty data directory, then exit)\n"); 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(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -617,6 +618,27 @@ int main(int argc, char* argv[]) printf("Loading wallet...\n"); try { + string strWalletSQLite = + argval2(argc, argv, "/walletsqlite", "-walletsqlite"); + if (!strWalletSQLite.empty()) + { + AttachTerminal(); + if (!LoadWalletFromSQLite(strWalletSQLite)) + { + FatalStartupError(fHeadlessStartup, + "Cannot open SQLite wallet export.", + strWalletLoadError); + return 1; + } + + fprintf(stderr, "SQLite wallet loaded: read-only staging backend\n"); + fprintf(stderr, "SQLite runtime writes are not enabled yet; exiting before " + "network, mining, GUI, or wallet mutation starts.\n"); + fflush(stderr); + DBFlush(true); + return 0; + } + if (!LoadWallet()) { // LoadWallet() explains itself into strWalletLoadError when it diff --git a/src/selftest.cpp b/src/selftest.cpp index feb4076..14bf45a 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1298,6 +1298,19 @@ static int RunWalletSQLiteMigrationSelfTest() "load check: ok"), "SQLite wallet export passes the loader compatibility check") ? 0 : 1; + string strRuntimeOut = tmp + "/sqlite-runtime.txt"; + vector vRuntimeArgs; + vRuntimeArgs.push_back("-datadir=" + strWalletDir); + vRuntimeArgs.push_back("-nomanagedtor"); + vRuntimeArgs.push_back("-nogui"); + vRuntimeArgs.push_back("-walletsqlite=" + strSQLite); + int nRuntimeRet = + RunBitflashChild(strExe, vRuntimeArgs, NULL, &strRuntimeOut); + nFail += Check(nRuntimeRet == 0 && + FileContainsText(strRuntimeOut, + "SQLite wallet loaded: read-only staging backend"), + "SQLite wallet export loads through the runtime startup path") ? 0 : 1; + string strRestoredDir = tmp + "/restored-wallet"; nFail += Check(MakeDirLocal(strRestoredDir), "restored wallet directory can be created") ? 0 : 1; @@ -1372,6 +1385,22 @@ static int RunWalletSQLiteMigrationSelfTest() "load check: failed"), "SQLite wallet load check rejects future wallet formats") ? 0 : 1; + string strFutureRuntimeOut = tmp + "/sqlite-runtime-future.txt"; + vector vFutureRuntimeArgs; + vFutureRuntimeArgs.push_back("-datadir=" + strWalletDir); + vFutureRuntimeArgs.push_back("-nomanagedtor"); + vFutureRuntimeArgs.push_back("-nogui"); + vFutureRuntimeArgs.push_back("-walletsqlite=" + strFutureSQLite); + int nFutureRuntimeRet = + RunBitflashChild(strExe, vFutureRuntimeArgs, NULL, + &strFutureRuntimeOut); + nFail += Check(nFutureRuntimeRet == 1 && + FileContainsText(strFutureRuntimeOut, + "Cannot open SQLite wallet export") && + FileContainsText(strFutureRuntimeOut, + "newer version of Bitflash"), + "SQLite runtime startup rejects future wallet formats") ? 0 : 1; + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; int nOverwriteRet = RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp index 8489cac..ca81573 100644 --- a/src/wallet_sqlite.cpp +++ b/src/wallet_sqlite.cpp @@ -58,6 +58,25 @@ bool CWalletDBSQLite::Open(const string& strPath, string& strError) return true; } +bool CWalletDBSQLite::OpenReadOnly(const string& strPath, string& strError) +{ + Close(); + + int ret = sqlite3_open_v2(strPath.c_str(), &pdb, + SQLITE_OPEN_READONLY | + SQLITE_OPEN_FULLMUTEX, + NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot open SQLite wallet read-only", strError); + Close(); + return false; + } + + sqlite3_busy_timeout(pdb, 5000); + return true; +} + void CWalletDBSQLite::Close() { if (pdb) diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h index 385a340..5e6467b 100644 --- a/src/wallet_sqlite.h +++ b/src/wallet_sqlite.h @@ -15,6 +15,7 @@ public: ~CWalletDBSQLite(); bool Open(const string& strPath, string& strError); + bool OpenReadOnly(const string& strPath, string& strError); void Close(); bool BeginTransaction(string& strError); -- cgit v1.2.3 From d082210c5d080c0a91412b94a109d8e73422eb31 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Mon, 17 Aug 2026 11:32:45 -0300 Subject: wallet: route CWalletDB through SQLite runtime --- src/db.cpp | 67 +++++++++++++++++++++++++ src/db.h | 145 ++++++++++++++++++++++++++++++++++++++++++++----------- src/selftest.cpp | 76 +++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 29 deletions(-) (limited to 'src/db.cpp') diff --git a/src/db.cpp b/src/db.cpp index 581e5a7..7cfb7c5 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -56,6 +56,7 @@ string strWalletLoadError; static DbEnv* pdbenv = NULL; static FILE* pdbErrFile = NULL; static map mapFileUseCount; +static CWalletDBSQLite* pWalletSQLiteRuntime = NULL; DbEnv& GetDbEnv() { @@ -95,6 +96,67 @@ public: } instance_of_cdbinit; +bool WalletSQLiteRuntimeActive() +{ + return pWalletSQLiteRuntime != NULL; +} + +bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet) +{ + strErrorRet.clear(); + if (!FileExists(strPath.c_str())) + { + strErrorRet = strprintf("SQLite wallet does not exist: %s", + strPath.c_str()); + return false; + } + + WalletSQLiteRuntimeClose(); + std::auto_ptr db(new CWalletDBSQLite()); + if (!db->Open(strPath, strErrorRet)) + return false; + + pWalletSQLiteRuntime = db.release(); + return true; +} + +void WalletSQLiteRuntimeClose() +{ + if (pWalletSQLiteRuntime) + { + delete pWalletSQLiteRuntime; + pWalletSQLiteRuntime = NULL; + } +} + +bool WalletSQLiteRuntimeReadRecord(const vector& vchKey, + vector& vchValueRet) +{ + if (!pWalletSQLiteRuntime) + return false; + string strError; + return pWalletSQLiteRuntime->ReadRecord(vchKey, vchValueRet, strError); +} + +bool WalletSQLiteRuntimeWriteRecord(const vector& vchKey, + const vector& vchValue, + bool fOverwrite) +{ + if (!pWalletSQLiteRuntime) + return false; + string strError; + return pWalletSQLiteRuntime->WriteRecord(vchKey, vchValue, strError, + fOverwrite); +} + +bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey) +{ + if (!pWalletSQLiteRuntime) + return false; + string strError; + return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); +} + CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) { @@ -651,6 +713,11 @@ bool CReviewDB::WriteReviews(uint256 hash, const vector& vReviews) // CWalletDB // +CWalletDB::CWalletDB(const char* pszMode, bool fTxn) : + CDB(WalletSQLiteRuntimeActive() ? NULL : "wallet.dat", pszMode, fTxn) +{ +} + class CSQLiteWalletRuntimeLoadVisitor : public CWalletRecordVisitor { public: diff --git a/src/db.h b/src/db.h index 59ff24a..8fa4ea8 100644 --- a/src/db.h +++ b/src/db.h @@ -356,6 +356,16 @@ private: // Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- // peer discovery is entirely Nostr/.btf-based now. +bool WalletSQLiteRuntimeActive(); +bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet); +void WalletSQLiteRuntimeClose(); +bool WalletSQLiteRuntimeReadRecord(const vector& vchKey, + vector& vchValueRet); +bool WalletSQLiteRuntimeWriteRecord(const vector& vchKey, + const vector& vchValue, + bool fOverwrite); +bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey); + @@ -363,64 +373,141 @@ private: class CWalletDB : public CDB { public: - CWalletDB(const char* pszMode="r+", bool fTxn=false) : CDB("wallet.dat", pszMode, fTxn) { } + CWalletDB(const char* pszMode="r+", bool fTxn=false); private: CWalletDB(const CWalletDB&); void operator=(const CWalletDB&); + + template + bool ReadWalletRecord(const K& key, T& value) + { + if (!WalletSQLiteRuntimeActive()) + return Read(key, value); + + CDataStream ssKey(SER_DISK); + ssKey.reserve(1000); + ssKey << key; + vector vchKey(ssKey.begin(), ssKey.end()); + vector vchValue; + + bool fOk = WalletSQLiteRuntimeReadRecord(vchKey, vchValue); + if (!vchKey.empty()) + memset(&vchKey[0], 0, vchKey.size()); + if (!ssKey.empty()) + memset(&ssKey[0], 0, ssKey.size()); + if (!fOk) + return false; + + CDataStream ssValue(vchValue, SER_DISK); + ssValue >> value; + bool fReadOk = !ssValue.fail(); + if (!vchValue.empty()) + memset(&vchValue[0], 0, vchValue.size()); + if (!ssValue.empty()) + memset(&ssValue[0], 0, ssValue.size()); + return fReadOk; + } + + template + bool WriteWalletRecord(const K& key, const T& value, bool fOverwrite=true) + { + if (!WalletSQLiteRuntimeActive()) + return Write(key, value, fOverwrite); + + CDataStream ssKey(SER_DISK); + ssKey.reserve(1000); + ssKey << key; + CDataStream ssValue(SER_DISK); + ssValue.reserve(10000); + ssValue << value; + + vector vchKey(ssKey.begin(), ssKey.end()); + vector vchValue(ssValue.begin(), ssValue.end()); + bool fOk = WalletSQLiteRuntimeWriteRecord(vchKey, vchValue, fOverwrite); + + if (!vchKey.empty()) + memset(&vchKey[0], 0, vchKey.size()); + if (!vchValue.empty()) + memset(&vchValue[0], 0, vchValue.size()); + if (!ssKey.empty()) + memset(&ssKey[0], 0, ssKey.size()); + if (!ssValue.empty()) + memset(&ssValue[0], 0, ssValue.size()); + return fOk; + } + + template + bool EraseWalletRecord(const K& key) + { + if (!WalletSQLiteRuntimeActive()) + return Erase(key); + + CDataStream ssKey(SER_DISK); + ssKey.reserve(1000); + ssKey << key; + vector vchKey(ssKey.begin(), ssKey.end()); + bool fOk = WalletSQLiteRuntimeEraseRecord(vchKey); + + if (!vchKey.empty()) + memset(&vchKey[0], 0, vchKey.size()); + if (!ssKey.empty()) + memset(&ssKey[0], 0, ssKey.size()); + return fOk; + } public: bool ReadName(const string& strAddress, string& strName) { strName = ""; - return Read(make_pair(string("name"), strAddress), strName); + return ReadWalletRecord(make_pair(string("name"), strAddress), strName); } bool WriteName(const string& strAddress, const string& strName) { mapAddressBook[strAddress] = strName; - return Write(make_pair(string("name"), strAddress), strName); + return WriteWalletRecord(make_pair(string("name"), strAddress), strName); } bool EraseName(const string& strAddress) { mapAddressBook.erase(strAddress); - return Erase(make_pair(string("name"), strAddress)); + return EraseWalletRecord(make_pair(string("name"), strAddress)); } bool ReadTx(uint256 hash, CWalletTx& wtx) { - return Read(make_pair(string("tx"), hash), wtx); + return ReadWalletRecord(make_pair(string("tx"), hash), wtx); } bool WriteTx(uint256 hash, const CWalletTx& wtx) { - return Write(make_pair(string("tx"), hash), wtx); + return WriteWalletRecord(make_pair(string("tx"), hash), wtx); } bool EraseTx(uint256 hash) { - return Erase(make_pair(string("tx"), hash)); + return EraseWalletRecord(make_pair(string("tx"), hash)); } bool ReadKey(const vector& vchPubKey, CPrivKey& vchPrivKey) { vchPrivKey.clear(); - return Read(make_pair(string("key"), vchPubKey), vchPrivKey); + return ReadWalletRecord(make_pair(string("key"), vchPubKey), vchPrivKey); } bool WriteKey(const vector& vchPubKey, const CPrivKey& vchPrivKey) { - return Write(make_pair(string("key"), vchPubKey), vchPrivKey, false); + return WriteWalletRecord(make_pair(string("key"), vchPubKey), vchPrivKey, false); } bool ReadDefaultKey(vector& vchPubKey) { vchPubKey.clear(); - return Read(string("defaultkey"), vchPubKey); + return ReadWalletRecord(string("defaultkey"), vchPubKey); } bool WriteDefaultKey(const vector& vchPubKey) { - return Write(string("defaultkey"), vchPubKey); + return WriteWalletRecord(string("defaultkey"), vchPubKey); } // --- Deterministic (BIP32) seed --------------------------------------- @@ -438,62 +525,62 @@ public: { vchMasterRet.clear(); vchChainCodeRet.clear(); - return Read(string("hdmaster"), vchMasterRet) - && Read(string("hdchaincode"), vchChainCodeRet); + return ReadWalletRecord(string("hdmaster"), vchMasterRet) + && ReadWalletRecord(string("hdchaincode"), vchChainCodeRet); } bool WriteHDMaster(const vector& vchMaster, const vector& vchChainCode) { - return Write(string("hdmaster"), vchMaster) - && Write(string("hdchaincode"), vchChainCode); + return WriteWalletRecord(string("hdmaster"), vchMaster) + && WriteWalletRecord(string("hdchaincode"), vchChainCode); } bool WriteWalletMinVersion(int nVersion) { - return Write(string("walletminversion"), nVersion); + return WriteWalletRecord(string("walletminversion"), nVersion); } bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) { - return Write(make_pair(string("mkey"), nID), kMasterKey, true); + return WriteWalletRecord(make_pair(string("mkey"), nID), kMasterKey, true); } bool WriteCryptedKey(const vector& vchPubKey, const vector& vchCryptedSecret) { - return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); + return WriteWalletRecord(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); } bool WriteCryptedHDMaster(const vector& vchCryptedMaster, const vector& vchCryptedChainCode) { - return Write(string("cryptedhdmaster"), vchCryptedMaster) - && Write(string("cryptedhdchaincode"), vchCryptedChainCode); + return WriteWalletRecord(string("cryptedhdmaster"), vchCryptedMaster) + && WriteWalletRecord(string("cryptedhdchaincode"), vchCryptedChainCode); } bool WriteHDNext(unsigned int nNext) { - return Write(string("hdnext"), nNext); + return WriteWalletRecord(string("hdnext"), nNext); } bool WriteHDSchema(int nSchema) { - return Write(string("hdschema"), nSchema); + return WriteWalletRecord(string("hdschema"), nSchema); } bool WriteHDCoinType(unsigned int nCoinType) { - return Write(string("hdcointype"), nCoinType); + return WriteWalletRecord(string("hdcointype"), nCoinType); } bool WriteHDReceiveNext(unsigned int nNext) { - return Write(string("hdreceivenext"), nNext); + return WriteWalletRecord(string("hdreceivenext"), nNext); } bool WriteHDChangeNext(unsigned int nNext) { - return Write(string("hdchangenext"), nNext); + return WriteWalletRecord(string("hdchangenext"), nNext); } // Keys generated ahead of time and not yet handed out. The private key of @@ -501,24 +588,24 @@ public: // record only says that this one is still unspoken for. bool WritePool(int64 nIndex, const vector& vchPubKey) { - return Write(make_pair(string("pool"), nIndex), vchPubKey); + return WriteWalletRecord(make_pair(string("pool"), nIndex), vchPubKey); } bool ErasePool(int64 nIndex) { - return Erase(make_pair(string("pool"), nIndex)); + return EraseWalletRecord(make_pair(string("pool"), nIndex)); } template bool ReadSetting(const string& strKey, T& value) { - return Read(make_pair(string("setting"), strKey), value); + return ReadWalletRecord(make_pair(string("setting"), strKey), value); } template bool WriteSetting(const string& strKey, const T& value) { - return Write(make_pair(string("setting"), strKey), value); + return WriteWalletRecord(make_pair(string("setting"), strKey), value); } bool LoadWallet(vector& vchDefaultKeyRet); diff --git a/src/selftest.cpp b/src/selftest.cpp index a1efaa0..9fa0938 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1051,6 +1051,8 @@ static int RunWalletSQLiteSelfTest() int nFail = 0; string strPath = tmp + "/wallet.sqlite"; string strError; + string strOldRuntimeDataDir; + bool fRestoreRuntimeDataDir = false; try { @@ -1166,14 +1168,88 @@ static int RunWalletSQLiteSelfTest() vchTypedPubKey == vchPubKey, "SQLite wallet reads a typed defaultkey record") ? 0 : 1; } + + { + string strRuntimePath = tmp + "/runtime-wallet.sqlite"; + string strRuntimeDir = tmp + "/runtime-datadir"; + nFail += Check(MakeDirLocal(strRuntimeDir), + "SQLite runtime datadir can be created") ? 0 : 1; + + { + CWalletDBSQLite db; + nFail += Check(db.Open(strRuntimePath, strError), + "SQLite runtime wallet file can be created") ? 0 : 1; + } + + string strOldDataDir = strSetDataDir; + strSetDataDir = strRuntimeDir; + strOldRuntimeDataDir = strOldDataDir; + fRestoreRuntimeDataDir = true; + bool fRuntimeOpened = WalletSQLiteRuntimeOpen(strRuntimePath, strError); + nFail += Check(fRuntimeOpened, + "SQLite runtime wallet can be activated") ? 0 : 1; + + if (fRuntimeOpened) + { + CKey key; + key.MakeNewKey(); + vector vchRuntimePubKey = key.GetPubKey(); + CPrivKey vchRuntimePrivKey = key.GetPrivKey(); + string strRuntimeAddress = PubKeyToAddress(vchRuntimePubKey); + + nFail += Check(CWalletDB().WriteSetting("runtime-sqlite-setting", + (int64)909090) && + CWalletDB().WriteDefaultKey(vchRuntimePubKey) && + CWalletDB().WriteKey(vchRuntimePubKey, + vchRuntimePrivKey) && + CWalletDB().WriteName(strRuntimeAddress, + "SQLite Runtime Address"), + "CWalletDB writes route to active SQLite runtime") ? 0 : 1; + + int64 nRuntimeSetting = 0; + vector vchRuntimeDefaultKey; + CPrivKey vchRuntimeReadPrivKey; + string strRuntimeName; + nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", + nRuntimeSetting) && + nRuntimeSetting == 909090 && + CWalletDB("r").ReadDefaultKey(vchRuntimeDefaultKey) && + vchRuntimeDefaultKey == vchRuntimePubKey && + CWalletDB("r").ReadKey(vchRuntimePubKey, + vchRuntimeReadPrivKey) && + vchRuntimeReadPrivKey == vchRuntimePrivKey && + CWalletDB("r").ReadName(strRuntimeAddress, + strRuntimeName) && + strRuntimeName == "SQLite Runtime Address", + "CWalletDB reads route to active SQLite runtime") ? 0 : 1; + + nFail += Check(CWalletDB().EraseName(strRuntimeAddress) && + !CWalletDB("r").ReadName(strRuntimeAddress, + strRuntimeName), + "CWalletDB erases route to active SQLite runtime") ? 0 : 1; + + nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), + "active SQLite runtime does not create wallet.dat") ? 0 : 1; + } + + WalletSQLiteRuntimeClose(); + strSetDataDir = strOldDataDir; + fRestoreRuntimeDataDir = false; + } } catch (const std::exception& e) { + WalletSQLiteRuntimeClose(); + if (fRestoreRuntimeDataDir) + strSetDataDir = strOldRuntimeDataDir; printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { + WalletSQLiteRuntimeClose(); + if (fRestoreRuntimeDataDir) + strSetDataDir = strOldRuntimeDataDir; printf(" FAIL unknown exception\n"); nFail++; } -- cgit v1.2.3 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/db.cpp') 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 From 00c7aaed74e57da18598faa6751b7720598de3bd Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 10:16:59 -0300 Subject: wallet: clearer refusal for an empty or misnamed SQLite backend Review follow-up on the SQLite activation. Two guards, both fail-closed; neither is a correctness fix -- FinishLoadedWallet already refuses a missing default key -- but they make the refusal say what happened: - an empty or non-wallet wallet.sqlite is refused with "no default key record; it may be empty or not a wallet export", instead of the generic "no usable default public key" that reads as corruption; - a misspelled -walletbackend value (e.g. sqlit) is refused instead of silently falling through to Berkeley DB; -walletbackend=bdb is accepted as the explicit default. --- src/db.cpp | 16 ++++++++++++++++ src/main_gui.cpp | 15 +++++++++++++++ 2 files changed, 31 insertions(+) (limited to 'src/db.cpp') diff --git a/src/db.cpp b/src/db.cpp index c4ea81e..771a4ba 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -1496,6 +1496,22 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath) return false; } + // Refuse an empty or non-wallet SQLite file up front, with a message that + // names the likely cause. FinishLoadedWallet below already rejects a + // missing default key (it is the fMayCreateDefaultKey=false path), but its + // wording -- "no usable default public key" -- reads as corruption when the + // real story is usually "this file was never an exported wallet". A blank + // wallet.sqlite left by a typo or a half-run export should say so. + if (visitor.vchDefaultKey.empty()) + { + WalletSQLiteRuntimeClose(); + strWalletLoadError = + "SQLite wallet has no default key record; it may be empty or not a " + "wallet export. Create it with -walletsqliteexport."; + printf("LoadWallet: %s\n", strWalletLoadError.c_str()); + return false; + } + if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode, visitor.fHaveStoredHDCoinType)) { diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 4d77533..4be4995 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -625,6 +625,21 @@ int main(int argc, char* argv[]) string strWalletSQLite = argval2(argc, argv, "/walletsqlite", "-walletsqlite"); + // A misspelled backend must not fall through to Berkeley DB silently -- + // someone who typed -walletbackend=sqlit meant to run on SQLite, and + // opening wallet.dat instead of telling them is the wrong surprise. + if (!strWalletBackend.empty() && + strWalletBackend != "sqlite" && strWalletBackend != "bdb") + { + AttachTerminal(); + FatalStartupError(fHeadlessStartup, + strprintf("Unknown wallet backend '%s'.", strWalletBackend.c_str()), + "Use -walletbackend=sqlite for the experimental SQLite backend, " + "or omit -walletbackend (or -walletbackend=bdb) to use the " + "default Berkeley DB wallet.dat."); + return 1; + } + if (strWalletBackend == "sqlite") { // Opt-in, experimental. The node runs on /wallet.sqlite and -- 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/db.cpp') 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 440d7f393d74f309d29e1d10ba1f11c41c869226 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 14:33:40 -0300 Subject: wallet: bracket WriteHDMaster in a SQLite runtime transaction The SQLite runtime wrote each record as its own autocommit, so a group that must land together -- hdmaster and hdchaincode, the two halves of an HD seed -- could be left half-written by a failure or crash between them, the incomplete-seed state -walletstoragecheck exists to catch. Berkeley DB was no worse (it is not transactional here either) but no better. Add WalletSQLiteRuntimeBeginTxn/CommitTxn/RollbackTxn -- no-ops that succeed when the SQLite backend is not active, so Berkeley DB keeps its existing per-write behaviour -- and wrap WriteHDMaster in them so the seed lands all-or-nothing under SQLite. The one BDB-atomic wallet rewrite, the encrypt path, is already matched by its own transaction in the SQLite encrypt branch; every other wallet multi-write is ordering-based on both backends by design. wallet-sqlite self-test now proves the bracket: a rolled-back runtime write leaves no record, a committed one persists, and WriteHDMaster lands both halves of the seed. --- src/db.cpp | 34 ++++++++++++++++++++++++++++++++++ src/db.h | 20 ++++++++++++++++++-- src/selftest.cpp | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) (limited to 'src/db.cpp') diff --git a/src/db.cpp b/src/db.cpp index 86f6dd7..c16edb4 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -164,6 +164,40 @@ bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey) return pWalletSQLiteRuntime->EraseRecord(vchKey, strError); } +// Transaction bracket for the SQLite runtime. When the SQLite backend is not +// active these are no-ops that succeed, so a caller can wrap a group of related +// writes and Berkeley DB keeps its existing per-write behaviour unchanged while +// SQLite gets all-or-nothing. +bool WalletSQLiteRuntimeBeginTxn() +{ + if (!pWalletSQLiteRuntime) + return true; + string strError; + if (pWalletSQLiteRuntime->BeginTransaction(strError)) + return true; + printf("WalletSQLiteRuntimeBeginTxn() : %s\n", strError.c_str()); + return false; +} + +bool WalletSQLiteRuntimeCommitTxn() +{ + if (!pWalletSQLiteRuntime) + return true; + string strError; + if (pWalletSQLiteRuntime->CommitTransaction(strError)) + return true; + printf("WalletSQLiteRuntimeCommitTxn() : %s\n", strError.c_str()); + return false; +} + +void WalletSQLiteRuntimeRollbackTxn() +{ + if (!pWalletSQLiteRuntime) + return; + string strError; + pWalletSQLiteRuntime->RollbackTransaction(strError); +} + CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) { diff --git a/src/db.h b/src/db.h index fee5a82..1a272b9 100644 --- a/src/db.h +++ b/src/db.h @@ -365,6 +365,9 @@ bool WalletSQLiteRuntimeWriteRecord(const vector& vchKey, const vector& vchValue, bool fOverwrite); bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey); +bool WalletSQLiteRuntimeBeginTxn(); +bool WalletSQLiteRuntimeCommitTxn(); +void WalletSQLiteRuntimeRollbackTxn(); @@ -531,8 +534,21 @@ public: bool WriteHDMaster(const vector& vchMaster, const vector& vchChainCode) { - return WriteWalletRecord(string("hdmaster"), vchMaster) - && WriteWalletRecord(string("hdchaincode"), vchChainCode); + // hdmaster and hdchaincode are halves of one seed: a wallet with one but + // not the other is an incomplete HD wallet (-walletstoragecheck flags + // exactly that). Under the SQLite runtime write them in one transaction + // so a failure or crash between the two cannot leave that half-state. + // Berkeley DB keeps its existing per-write behaviour -- the bracket is a + // no-op there. + if (!WalletSQLiteRuntimeBeginTxn()) + return false; + if (!WriteWalletRecord(string("hdmaster"), vchMaster) || + !WriteWalletRecord(string("hdchaincode"), vchChainCode)) + { + WalletSQLiteRuntimeRollbackTxn(); + return false; + } + return WalletSQLiteRuntimeCommitTxn(); } bool WriteWalletMinVersion(int nVersion) diff --git a/src/selftest.cpp b/src/selftest.cpp index fc10947..7ad5a53 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1231,6 +1231,43 @@ static int RunWalletSQLiteSelfTest() nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), "active SQLite runtime does not create wallet.dat") ? 0 : 1; + // Transaction bracket: a rolled-back group leaves nothing, a + // committed group persists. This is the all-or-nothing that + // WriteHDMaster relies on so a half-written seed cannot survive + // a failure between its two records. + bool fTxnBegan = WalletSQLiteRuntimeBeginTxn(); + bool fTxnWrote = fTxnBegan && + CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111); + if (fTxnBegan) + WalletSQLiteRuntimeRollbackTxn(); + nFail += Check(fTxnBegan && fTxnWrote, + "a SQLite runtime transaction opens and rolls back") ? 0 : 1; + int64 nRolledBack = 0; + nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback", + nRolledBack), + "a rolled-back SQLite runtime write leaves no record") ? 0 : 1; + + bool fCommitOk = WalletSQLiteRuntimeBeginTxn() && + CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) && + WalletSQLiteRuntimeCommitTxn(); + int64 nCommitted = 0; + nFail += Check(fCommitOk && + CWalletDB("r").ReadSetting("runtime-txn-commit", + nCommitted) && + nCommitted == 222, + "a committed SQLite runtime write persists") ? 0 : 1; + + // WriteHDMaster brackets its two records in that transaction; + // both halves of the seed must land. + vector vchTxnMaster(32, 0x5a); + vector vchTxnChainCode(32, 0xa5); + vector vchReadMaster, vchReadChainCode; + nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) && + CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) && + vchReadMaster == vchTxnMaster && + vchReadChainCode == vchTxnChainCode, + "WriteHDMaster commits both halves of the seed under SQLite") ? 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 -- cgit v1.2.3 From 84a60275569dca5d1fb12db38ce184caf7487ed6 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 14:44:11 -0300 Subject: wallet: one-shot storage commands honour -walletbackend=sqlite -walletstorageaudit, -walletstoragecheck and -backupwallet all read wallet.dat regardless of the selected backend. Under -walletbackend=sqlite that is wrong, and for backup it is dangerous: it silently backs up the stale plaintext wallet.dat instead of the live wallet.sqlite. - ScanWalletRecords() routes to the active SQLite store instead of a Berkeley DB cursor (it returned an error under the runtime before). This is the single choke point audit and check scan through. - WalletSQLiteRuntimeOpenReadOnly() opens wallet.sqlite for scanning without loading or validating records, so the pre-load diagnostics can inspect a wallet the loader would refuse. main_gui opens it for audit/check when the SQLite backend is selected (and rejects an unknown backend value there too). - BackupWallet() copies wallet.sqlite (checkpoint, then the temp/old/rename dance; a checkpointed SQLite file is self-contained, no lsn_reset). wallet-sqlite-encrypt self-test proves routing on a datadir where the SQLite wallet is encrypted and wallet.dat beside it is still plaintext: audit with the flag reports the encrypted seed, without it reports the plaintext one; the SQLite backup carries no plaintext key and loadchecks as a standalone wallet. --- src/db.cpp | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/db.h | 2 + src/main_gui.cpp | 50 ++++++++++++++++++++++--- src/selftest.cpp | 60 +++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 6 deletions(-) (limited to 'src/db.cpp') diff --git a/src/db.cpp b/src/db.cpp index c16edb4..aafc0f9 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -198,6 +198,38 @@ void WalletSQLiteRuntimeRollbackTxn() pWalletSQLiteRuntime->RollbackTransaction(strError); } +// Open the SQLite wallet read-only as the active runtime. Used by the one-shot +// storage diagnostics, which run before the normal wallet load and must be able +// to inspect a wallet that the loader would refuse -- so this opens the file for +// scanning without loading or validating its records. +bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet) +{ + strErrorRet.clear(); + if (!FileExists(strPath.c_str())) + { + strErrorRet = strprintf("SQLite wallet does not exist: %s", strPath.c_str()); + return false; + } + + WalletSQLiteRuntimeClose(); + std::auto_ptr db(new CWalletDBSQLite()); + if (!db->OpenReadOnly(strPath, strErrorRet)) + return false; + + pWalletSQLiteRuntime = db.release(); + return true; +} + +bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet) +{ + if (!pWalletSQLiteRuntime) + { + strErrorRet = "SQLite wallet backend is not active"; + return false; + } + return pWalletSQLiteRuntime->Checkpoint(strErrorRet); +} + CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) { @@ -1354,6 +1386,16 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet) { + // Under the SQLite backend there is no Berkeley DB cursor to walk; scan the + // active SQLite store instead. This is the single choke point every "read + // every wallet record" caller goes through (storage audit and check among + // them), so routing it here makes those honour -walletbackend=sqlite. + if (WalletSQLiteRuntimeActive()) + { + strErrorRet.clear(); + return pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet); + } + class CWalletScanDB : public CWalletDB { public: @@ -1584,11 +1626,81 @@ bool LoadWalletFromSQLiteRuntime(const string& strPath) // this file was written are not spendable from it. Backing up once is not // enough, and that is a property of the wallet format rather than of this // function. +// Back up the active SQLite wallet. The store is checkpointed so the WAL is +// folded into the main file, then the single self-contained wallet.sqlite is +// copied through a temp file and renamed into place (same temp/old/rename dance +// as the Berkeley DB path, for Windows' non-overwriting rename). No lsn_reset: +// a checkpointed SQLite file needs nothing beside it to open. +static bool BackupWalletSQLite(const string& strDest) +{ + string strSrc = GetAppDir() + "/wallet.sqlite"; + if (!FileExists(strSrc.c_str())) + return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); + + string strCheckpointError; + if (!WalletSQLiteRuntimeCheckpoint(strCheckpointError)) + return error("BackupWallet() : checkpoint failed: %s\n", strCheckpointError.c_str()); + + string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime()); + for (int i = 1; FileExists(strTemp.c_str()); i++) + strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i); + string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime()); + for (int i = 1; FileExists(strOld.c_str()); i++) + strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i); + + FILE* pfIn = fopen(strSrc.c_str(), "rb"); + if (!pfIn) + return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); + FILE* pfOut = fopen(strTemp.c_str(), "wb"); + if (!pfOut) + { + fclose(pfIn); + return error("BackupWallet() : cannot write %s\n", strTemp.c_str()); + } + + char buf[65536]; + size_t n; + bool fOk = true; + while ((n = fread(buf, 1, sizeof(buf), pfIn)) > 0) + if (fwrite(buf, 1, n, pfOut) != n) { fOk = false; break; } + if (ferror(pfIn)) + fOk = false; + fclose(pfIn); + if (fclose(pfOut) != 0) + fOk = false; + if (!fOk) + { + remove(strTemp.c_str()); // half a wallet is worse than none + return error("BackupWallet() : copy to %s failed\n", strTemp.c_str()); + } + + bool fHadOldDest = FileExists(strDest.c_str()); + if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0) + { + remove(strTemp.c_str()); + return error("BackupWallet() : could not move old %s aside\n", strDest.c_str()); + } + if (rename(strTemp.c_str(), strDest.c_str()) != 0) + { + remove(strTemp.c_str()); + if (fHadOldDest) + rename(strOld.c_str(), strDest.c_str()); + return error("BackupWallet() : could not install %s\n", strDest.c_str()); + } + if (fHadOldDest) + remove(strOld.c_str()); + return true; +} + bool BackupWallet(const string& strDest) { if (strDest.empty()) return error("BackupWallet() : no destination given\n"); + // Under the SQLite backend the live wallet is wallet.sqlite, not wallet.dat. + if (WalletSQLiteRuntimeActive()) + return BackupWalletSQLite(strDest); + string strSrc = GetAppDir() + "/wallet.dat"; if (!FileExists(strSrc.c_str())) return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); diff --git a/src/db.h b/src/db.h index 1a272b9..5bc94b1 100644 --- a/src/db.h +++ b/src/db.h @@ -368,6 +368,8 @@ bool WalletSQLiteRuntimeEraseRecord(const vector& vchKey); bool WalletSQLiteRuntimeBeginTxn(); bool WalletSQLiteRuntimeCommitTxn(); void WalletSQLiteRuntimeRollbackTxn(); +bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); +bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet); diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 4be4995..af6053f 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -524,19 +524,57 @@ int main(int argc, char* argv[]) // explain wallet.dat states that the normal wallet loader may refuse. string strWalletStorageAuditJson = argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); - if (arg(argc,argv,"/walletstorageaudit") || - arg(argc,argv,"-walletstorageaudit") || - !strWalletStorageAuditJson.empty()) + bool fDiagWantsAudit = arg(argc,argv,"/walletstorageaudit") || + arg(argc,argv,"-walletstorageaudit") || + !strWalletStorageAuditJson.empty(); + bool fDiagWantsCheck = arg(argc,argv,"/walletstoragecheck") || + arg(argc,argv,"-walletstoragecheck"); + + // These diagnostics normally read wallet.dat. Under -walletbackend=sqlite + // the live wallet is wallet.sqlite, so open it read-only as the active + // store and let the same scan walk the right file. This runs before the + // normal backend resolution further down, so validate the value here too. + string strDiagBackend = argval2(argc, argv, "/walletbackend", "-walletbackend"); + if ((fDiagWantsAudit || fDiagWantsCheck) && !strDiagBackend.empty() && + strDiagBackend != "sqlite" && strDiagBackend != "bdb") + { + AttachTerminal(); + fprintf(stderr, "Unknown wallet backend '%s'. Use -walletbackend=sqlite " + "or -walletbackend=bdb.\n", strDiagBackend.c_str()); + return 1; + } + bool fDiagSQLite = (strDiagBackend == "sqlite") && + (fDiagWantsAudit || fDiagWantsCheck); + if (fDiagSQLite) + { + AttachTerminal(); + string strSQLitePath = GetAppDir() + "/wallet.sqlite"; + string strOpenErr; + if (!WalletSQLiteRuntimeOpenReadOnly(strSQLitePath, strOpenErr)) + { + fprintf(stderr, "Cannot open the SQLite wallet for diagnostics: %s\n", + strOpenErr.c_str()); + return 1; + } + } + + if (fDiagWantsAudit) { int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); - DBFlush(true); + if (fDiagSQLite) + WalletSQLiteRuntimeClose(); + else + DBFlush(true); return nRet; } - if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) + if (fDiagWantsCheck) { int nRet = CmdWalletStorageCheck(); - DBFlush(true); + if (fDiagSQLite) + WalletSQLiteRuntimeClose(); + else + DBFlush(true); return nRet; } diff --git a/src/selftest.cpp b/src/selftest.cpp index 7ad5a53..90a30d2 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -2453,6 +2453,66 @@ static int RunWalletSQLiteEncryptSelfTest() nFail += Check(nAuditRet == 0 && FileContainsText(strAudit, "recovery phrase: present"), "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; + + // One-shot storage commands honour -walletbackend. The SQLite wallet is + // encrypted; the wallet.dat beside it is still plaintext. Same datadir, + // opposite answers -> the flag routes the diagnostic to the right file. + string strSqliteAuditJson = tmp + "/sqlite-audit.json"; + vector vSqliteAuditArgs; + vSqliteAuditArgs.push_back("-datadir=" + tmp); + vSqliteAuditArgs.push_back("-walletbackend=sqlite"); + vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson); + vSqliteAuditArgs.push_back("-nogui"); + int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs); + nFail += Check(nSqliteAuditRet == 0 && + FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") && + FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""), + "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1; + + string strBdbAuditJson = tmp + "/bdb-audit.json"; + vector vBdbAuditArgs; + vBdbAuditArgs.push_back("-datadir=" + tmp); + vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson); + vBdbAuditArgs.push_back("-nogui"); + int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs); + nFail += Check(nBdbAuditRet == 0 && + FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") && + FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""), + "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1; + + string strSqliteCheckOut = tmp + "/sqlite-check.txt"; + vector vSqliteCheckArgs; + vSqliteCheckArgs.push_back("-datadir=" + tmp); + vSqliteCheckArgs.push_back("-walletbackend=sqlite"); + vSqliteCheckArgs.push_back("-walletstoragecheck"); + vSqliteCheckArgs.push_back("-nogui"); + int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut); + nFail += Check(nSqliteCheckRet == 0, + "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1; + + // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat. + // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds + // none of the plaintext key that wallet.dat still contains. + string strBackupDest = tmp + "/backup.sqlite"; + vector vBackupArgs; + vBackupArgs.push_back("-datadir=" + tmp); + vBackupArgs.push_back("-walletbackend=sqlite"); + vBackupArgs.push_back("-backupwallet=" + strBackupDest); + vBackupArgs.push_back("-nogui"); + int nBackupRet = RunBitflashChild(strExe, vBackupArgs); + nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()), + "-backupwallet under SQLite writes a backup file") ? 0 : 1; + nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes), + "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1; + + string strBackupLoadCheck = tmp + "/backup-loadcheck.txt"; + vector vLoadCheckArgs; + vLoadCheckArgs.push_back("-datadir=" + tmp); + vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest); + vLoadCheckArgs.push_back("-nogui"); + int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck); + nFail += Check(nBackupLoadRet == 0, + "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1; } catch (const std::exception& e) { -- 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/db.cpp') 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