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/selftest.cpp | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) (limited to 'src/selftest.cpp') 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; } -- 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/selftest.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/selftest.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