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') 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