From 6f9e07186d06f772c93832f0dcbd4f9432b44579 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 17:19:37 -0300 Subject: tests: cover portable wallet copies --- src/main_gui.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 3f52e91..50c4f34 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-crypto, wallet-encrypt,\n"); - printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n"); + printf(" wallet-portability, net-message, consensus-limits, pool-stratum,\n"); + printf(" parse-money, socks5-proxy, or managed-tor\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); -- cgit v1.2.3 From 8e62e362e2fbe6fd6c5d66ddb354ec544f8cf0db Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 17:36:09 -0300 Subject: wallet: add storage audit command --- src/main_gui.cpp | 9 +++ src/selftest.cpp | 32 ++++++++++ src/walletcmd.cpp | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 4 ++ 4 files changed, 223 insertions(+) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 50c4f34..532bc3b 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -172,6 +172,8 @@ static void PrintUsage() printf(" /recoveryaudit (show how much spendable balance is\n"); printf(" covered by the recovery phrase;\n"); printf(" exits 2 when wallet.dat is still needed)\n"); + printf(" /walletstorageaudit (count wallet.dat record types without\n"); + printf(" printing wallet values, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -704,6 +706,13 @@ int main(int argc, char* argv[]) return nRet; } + if (arg(argc,argv,"/walletstorageaudit") || arg(argc,argv,"-walletstorageaudit")) + { + int nRet = CmdWalletStorageAudit(); + DBFlush(true); + return nRet; + } + if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) { int nRet = CmdNewPhrase(); diff --git a/src/selftest.cpp b/src/selftest.cpp index 8a448f0..b112238 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1314,6 +1314,21 @@ static int RunWalletPortabilitySelfTest() FileContainsText(strOriginalAudit, "SLIP-0044 BITFLASH"), "the original wallet reports its recovery phrase") ? 0 : 1; + string strOriginalStorageAudit = tmp + "/original-storage-audit.txt"; + vector vOriginalStorageAuditArgs; + vOriginalStorageAuditArgs.push_back("-datadir=" + strOriginal); + vOriginalStorageAuditArgs.push_back("-nomanagedtor"); + vOriginalStorageAuditArgs.push_back("-nogui"); + vOriginalStorageAuditArgs.push_back("-walletstorageaudit"); + int nOriginalStorageAuditRet = + RunBitflashChild(strExe, vOriginalStorageAuditArgs, NULL, &strOriginalStorageAudit); + nFail += Check(nOriginalStorageAuditRet == 0 && + FileContainsText(strOriginalStorageAudit, "Wallet storage audit") && + FileContainsText(strOriginalStorageAudit, "plain HD seed: complete") && + FileContainsText(strOriginalStorageAudit, "encrypted HD seed: none") && + FileContainsText(strOriginalStorageAudit, "malformed records: 0"), + "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; + string strPortableWallet = tmp + "/portable-wallet.dat"; string strBackupOut = tmp + "/backupwallet.txt"; vector vBackupArgs; @@ -1368,6 +1383,23 @@ static int RunWalletPortabilitySelfTest() nFail += Check(nEncryptRet == 0 && !DirectoryHasFileWithPrefix(strOriginal + "/database", "log."), "the encryptwallet command purges Berkeley DB environment logs") ? 0 : 1; + + string strEncryptedStorageAudit = tmp + "/encrypted-storage-audit.txt"; + vector vEncryptedStorageAuditArgs; + vEncryptedStorageAuditArgs.push_back("-datadir=" + strOriginal); + vEncryptedStorageAuditArgs.push_back("-nomanagedtor"); + vEncryptedStorageAuditArgs.push_back("-nogui"); + vEncryptedStorageAuditArgs.push_back("-walletstorageaudit"); + int nEncryptedStorageAuditRet = + RunBitflashChild(strExe, vEncryptedStorageAuditArgs, NULL, &strEncryptedStorageAudit); + nFail += Check(nEncryptedStorageAuditRet == 0 && + FileContainsText(strEncryptedStorageAudit, "plain private keys: 0") && + FileContainsText(strEncryptedStorageAudit, "encrypted private keys:") && + FileContainsText(strEncryptedStorageAudit, "plain HD seed: none") && + FileContainsText(strEncryptedStorageAudit, "encrypted HD seed: complete") && + FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") && + FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), + "the storage audit recognizes an encrypted wallet") ? 0 : 1; } catch (const std::exception& e) { diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index eccb9b3..896d593 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -470,6 +470,184 @@ int CmdRecoveryAudit() return 0; } +struct WalletStorageAuditCounts +{ + unsigned int nTotal; + unsigned int nMalformed; + unsigned int nUnknown; + unsigned int nVersion; + unsigned int nNames; + unsigned int nTransactions; + unsigned int nPlainKeys; + unsigned int nEncryptedKeys; + unsigned int nMasterKeys; + unsigned int nDefaultKey; + unsigned int nPlainHDMaster; + unsigned int nPlainHDChainCode; + unsigned int nCryptedHDMaster; + unsigned int nCryptedHDChainCode; + unsigned int nWalletMinVersion; + unsigned int nPool; + unsigned int nSettings; + unsigned int nHDNext; + unsigned int nHDSchema; + unsigned int nHDCoinType; + unsigned int nHDReceiveNext; + unsigned int nHDChangeNext; + + WalletStorageAuditCounts() + { + memset(this, 0, sizeof(*this)); + } +}; + +static std::string HDSeedStorageState(unsigned int nMaster, unsigned int nChain) +{ + if (nMaster == 0 && nChain == 0) + return "none"; + if (nMaster > 0 && nChain > 0) + return "complete"; + return "incomplete"; +} + +static void CountWalletStorageType(const std::string& strType, + WalletStorageAuditCounts& c) +{ + if (strType == "version") + c.nVersion++; + else if (strType == "name") + c.nNames++; + else if (strType == "tx") + c.nTransactions++; + else if (strType == "key") + c.nPlainKeys++; + else if (strType == "ckey") + c.nEncryptedKeys++; + else if (strType == "mkey") + c.nMasterKeys++; + else if (strType == "defaultkey") + c.nDefaultKey++; + else if (strType == "hdmaster") + c.nPlainHDMaster++; + else if (strType == "hdchaincode") + c.nPlainHDChainCode++; + else if (strType == "cryptedhdmaster") + c.nCryptedHDMaster++; + else if (strType == "cryptedhdchaincode") + c.nCryptedHDChainCode++; + else if (strType == "walletminversion") + c.nWalletMinVersion++; + else if (strType == "pool") + c.nPool++; + else if (strType == "setting") + c.nSettings++; + else if (strType == "hdnext") + c.nHDNext++; + else if (strType == "hdschema") + c.nHDSchema++; + else if (strType == "hdcointype") + c.nHDCoinType++; + else if (strType == "hdreceivenext") + c.nHDReceiveNext++; + else if (strType == "hdchangenext") + c.nHDChangeNext++; + else + c.nUnknown++; +} + +static bool ReadWalletStorageCounts(WalletStorageAuditCounts& counts, + std::string& strError) +{ + class CWalletAuditDB : public CWalletDB + { + public: + CWalletAuditDB() : CWalletDB("r") { } + using CDB::GetCursor; + using CDB::ReadAtCursor; + }; + + CWalletAuditDB walletdb; + Dbc* pcursor = walletdb.GetCursor(); + if (!pcursor) + { + strError = "cannot open wallet.dat cursor"; + return false; + } + + for (;;) + { + CDataStream ssKey(SER_DISK); + CDataStream ssValue(SER_DISK); + int ret = walletdb.ReadAtCursor(pcursor, ssKey, ssValue); + if (ret == DB_NOTFOUND) + break; + if (ret != 0) + { + pcursor->close(); + strError = strprintf("Berkeley DB cursor read failed (%d)", ret); + return false; + } + + counts.nTotal++; + try + { + std::string strType; + ssKey >> strType; + CountWalletStorageType(strType, counts); + } + catch (...) + { + counts.nMalformed++; + } + } + + pcursor->close(); + return true; +} + +int CmdWalletStorageAudit() +{ + AttachTerminal(); + + WalletStorageAuditCounts counts; + std::string strError; + if (!ReadWalletStorageCounts(counts, strError)) + { + fprintf(stderr, "Cannot audit wallet storage: %s\n", strError.c_str()); + return 1; + } + + printf("Wallet storage audit\n"); + printf(" records total: %u\n", counts.nTotal); + printf(" malformed records: %u\n", counts.nMalformed); + printf(" unknown records: %u\n", counts.nUnknown); + printf(" plain private keys: %u\n", counts.nPlainKeys); + printf(" encrypted private keys: %u\n", counts.nEncryptedKeys); + printf(" encryption master keys: %u\n", counts.nMasterKeys); + printf(" default public key: %s\n", counts.nDefaultKey ? "present" : "none"); + printf(" plain HD seed: %s\n", + HDSeedStorageState(counts.nPlainHDMaster, + counts.nPlainHDChainCode).c_str()); + printf(" encrypted HD seed: %s\n", + HDSeedStorageState(counts.nCryptedHDMaster, + counts.nCryptedHDChainCode).c_str()); + printf(" wallet minimum version: %s\n", + counts.nWalletMinVersion ? "present" : "none"); + printf(" keypool entries: %u\n", counts.nPool); + printf(" wallet transactions: %u\n", counts.nTransactions); + printf(" address book labels: %u\n", counts.nNames); + printf(" settings: %u\n", counts.nSettings); + printf(" HD metadata records: %u\n", + counts.nHDNext + counts.nHDSchema + counts.nHDCoinType + + counts.nHDReceiveNext + counts.nHDChangeNext); + printf(" database version records: %u\n", counts.nVersion); + fflush(stdout); + + if (counts.nMalformed > 0) + return 2; + return 0; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index 007b8b5..4a703db 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -62,6 +62,10 @@ int CmdShowDerived(int nCount); // by the installed recovery phrase or still depends on wallet.dat-only keys. int CmdRecoveryAudit(); +// Diagnostic: count wallet.dat record types without printing keys, +// addresses, transaction ids, labels, or other wallet values. +int CmdWalletStorageAudit(); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From 805c5757b801d80ea575c6a8b58e2855254f9046 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 17:40:20 -0300 Subject: wallet: export storage audit json --- src/main_gui.cpp | 10 ++++++-- src/selftest.cpp | 38 +++++++++++++++++++++++++++++ src/walletcmd.cpp | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- src/walletcmd.h | 2 +- 4 files changed, 117 insertions(+), 4 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 532bc3b..395cc0f 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -174,6 +174,8 @@ static void PrintUsage() printf(" exits 2 when wallet.dat is still needed)\n"); printf(" /walletstorageaudit (count wallet.dat record types without\n"); printf(" printing wallet values, then exit)\n"); + printf(" /walletstorageauditjson=FILE\n"); + printf(" (write the same audit as deterministic JSON)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -706,9 +708,13 @@ int main(int argc, char* argv[]) return nRet; } - if (arg(argc,argv,"/walletstorageaudit") || arg(argc,argv,"-walletstorageaudit")) + string strWalletStorageAuditJson = + argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); + if (arg(argc,argv,"/walletstorageaudit") || + arg(argc,argv,"-walletstorageaudit") || + !strWalletStorageAuditJson.empty()) { - int nRet = CmdWalletStorageAudit(); + int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); DBFlush(true); return nRet; } diff --git a/src/selftest.cpp b/src/selftest.cpp index b112238..73c2935 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1329,6 +1329,24 @@ static int RunWalletPortabilitySelfTest() FileContainsText(strOriginalStorageAudit, "malformed records: 0"), "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; + string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; + vector vOriginalStorageAuditJsonArgs; + vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); + vOriginalStorageAuditJsonArgs.push_back("-nomanagedtor"); + vOriginalStorageAuditJsonArgs.push_back("-nogui"); + vOriginalStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + + strOriginalStorageAuditJson); + int nOriginalStorageAuditJsonRet = + RunBitflashChild(strExe, vOriginalStorageAuditJsonArgs); + nFail += Check(nOriginalStorageAuditJsonRet == 0 && + FileContainsText(strOriginalStorageAuditJson, + "\"format\": \"bitflash-wallet-storage-audit-v1\"") && + FileContainsText(strOriginalStorageAuditJson, + "\"plain_hd_seed\": \"complete\"") && + FileContainsText(strOriginalStorageAuditJson, + "\"encrypted_hd_seed\": \"none\""), + "the storage audit can write plaintext-wallet JSON") ? 0 : 1; + string strPortableWallet = tmp + "/portable-wallet.dat"; string strBackupOut = tmp + "/backupwallet.txt"; vector vBackupArgs; @@ -1400,6 +1418,26 @@ static int RunWalletPortabilitySelfTest() FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") && FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), "the storage audit recognizes an encrypted wallet") ? 0 : 1; + + string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; + vector vEncryptedStorageAuditJsonArgs; + vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); + vEncryptedStorageAuditJsonArgs.push_back("-nomanagedtor"); + vEncryptedStorageAuditJsonArgs.push_back("-nogui"); + vEncryptedStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + + strEncryptedStorageAuditJson); + int nEncryptedStorageAuditJsonRet = + RunBitflashChild(strExe, vEncryptedStorageAuditJsonArgs); + nFail += Check(nEncryptedStorageAuditJsonRet == 0 && + FileContainsText(strEncryptedStorageAuditJson, + "\"plain_private_keys\": 0") && + FileContainsText(strEncryptedStorageAuditJson, + "\"plain_hd_seed\": \"none\"") && + FileContainsText(strEncryptedStorageAuditJson, + "\"encrypted_hd_seed\": \"complete\"") && + FileContainsText(strEncryptedStorageAuditJson, + "\"wallet_minimum_version\": \"present\""), + "the storage audit can write encrypted-wallet JSON") ? 0 : 1; } catch (const std::exception& e) { diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 896d593..6f8258d 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -605,7 +605,60 @@ static bool ReadWalletStorageCounts(WalletStorageAuditCounts& counts, return true; } -int CmdWalletStorageAudit() +static std::string WalletStorageAuditJson(const WalletStorageAuditCounts& counts) +{ + std::string strJson; + strJson += "{\n"; + strJson += " \"format\": \"bitflash-wallet-storage-audit-v1\",\n"; + strJson += strprintf(" \"records_total\": %u,\n", counts.nTotal); + strJson += strprintf(" \"malformed_records\": %u,\n", counts.nMalformed); + strJson += strprintf(" \"unknown_records\": %u,\n", counts.nUnknown); + strJson += strprintf(" \"plain_private_keys\": %u,\n", counts.nPlainKeys); + strJson += strprintf(" \"encrypted_private_keys\": %u,\n", counts.nEncryptedKeys); + strJson += strprintf(" \"encryption_master_keys\": %u,\n", counts.nMasterKeys); + strJson += strprintf(" \"default_public_key\": \"%s\",\n", + counts.nDefaultKey ? "present" : "none"); + strJson += strprintf(" \"plain_hd_seed\": \"%s\",\n", + HDSeedStorageState(counts.nPlainHDMaster, + counts.nPlainHDChainCode).c_str()); + strJson += strprintf(" \"encrypted_hd_seed\": \"%s\",\n", + HDSeedStorageState(counts.nCryptedHDMaster, + counts.nCryptedHDChainCode).c_str()); + strJson += strprintf(" \"wallet_minimum_version\": \"%s\",\n", + counts.nWalletMinVersion ? "present" : "none"); + strJson += strprintf(" \"keypool_entries\": %u,\n", counts.nPool); + strJson += strprintf(" \"wallet_transactions\": %u,\n", counts.nTransactions); + strJson += strprintf(" \"address_book_labels\": %u,\n", counts.nNames); + strJson += strprintf(" \"settings\": %u,\n", counts.nSettings); + strJson += strprintf(" \"hd_metadata_records\": %u,\n", + counts.nHDNext + counts.nHDSchema + counts.nHDCoinType + + counts.nHDReceiveNext + counts.nHDChangeNext); + strJson += strprintf(" \"database_version_records\": %u\n", counts.nVersion); + strJson += "}\n"; + return strJson; +} + +static bool WriteAuditTextFile(const std::string& strPath, + const std::string& strText, + std::string& strError) +{ + FILE* pf = fopen(strPath.c_str(), "wb"); + if (!pf) + { + strError = "cannot open output file"; + return false; + } + size_t nWritten = fwrite(strText.data(), 1, strText.size(), pf); + bool fCloseOk = fclose(pf) == 0; + if (nWritten != strText.size() || !fCloseOk) + { + strError = "could not write the complete output file"; + return false; + } + return true; +} + +int CmdWalletStorageAudit(const std::string& strJsonOut) { AttachTerminal(); @@ -617,6 +670,22 @@ int CmdWalletStorageAudit() return 1; } + if (!strJsonOut.empty()) + { + std::string strWriteError; + if (!WriteAuditTextFile(strJsonOut, + WalletStorageAuditJson(counts), + strWriteError)) + { + fprintf(stderr, "Cannot write wallet storage audit JSON: %s\n", + strWriteError.c_str()); + return 1; + } + printf("Wallet storage audit written to %s\n", strJsonOut.c_str()); + fflush(stdout); + return counts.nMalformed > 0 ? 2 : 0; + } + printf("Wallet storage audit\n"); printf(" records total: %u\n", counts.nTotal); printf(" malformed records: %u\n", counts.nMalformed); diff --git a/src/walletcmd.h b/src/walletcmd.h index 4a703db..c6342a6 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -64,7 +64,7 @@ int CmdRecoveryAudit(); // Diagnostic: count wallet.dat record types without printing keys, // addresses, transaction ids, labels, or other wallet values. -int CmdWalletStorageAudit(); +int CmdWalletStorageAudit(const std::string& strJsonOut = ""); // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. -- cgit v1.2.3 From 540ed06974ff6b05364d4922895288744f076f6b Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 18:11:30 -0300 Subject: wallet: add storage sanity check --- src/main_gui.cpp | 9 ++++ src/selftest.cpp | 27 +++++++++++ src/walletcmd.cpp | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 4 ++ 4 files changed, 177 insertions(+) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 395cc0f..8763211 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -176,6 +176,8 @@ static void PrintUsage() printf(" printing wallet values, then exit)\n"); printf(" /walletstorageauditjson=FILE\n"); printf(" (write the same audit as deterministic JSON)\n"); + printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); + printf(" wallet.dat storage state)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -719,6 +721,13 @@ int main(int argc, char* argv[]) return nRet; } + if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) + { + int nRet = CmdWalletStorageCheck(); + DBFlush(true); + return nRet; + } + if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) { int nRet = CmdNewPhrase(); diff --git a/src/selftest.cpp b/src/selftest.cpp index 73c2935..5e5bab6 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1329,6 +1329,19 @@ static int RunWalletPortabilitySelfTest() FileContainsText(strOriginalStorageAudit, "malformed records: 0"), "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; + string strOriginalStorageCheck = tmp + "/original-storage-check.txt"; + vector vOriginalStorageCheckArgs; + vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal); + vOriginalStorageCheckArgs.push_back("-nomanagedtor"); + vOriginalStorageCheckArgs.push_back("-nogui"); + vOriginalStorageCheckArgs.push_back("-walletstoragecheck"); + int nOriginalStorageCheckRet = + RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck); + nFail += Check(nOriginalStorageCheckRet == 0 && + FileContainsText(strOriginalStorageCheck, "storage sanity: ok") && + FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"), + "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1; + string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; vector vOriginalStorageAuditJsonArgs; vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); @@ -1419,6 +1432,20 @@ static int RunWalletPortabilitySelfTest() FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), "the storage audit recognizes an encrypted wallet") ? 0 : 1; + string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt"; + vector vEncryptedStorageCheckArgs; + vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal); + vEncryptedStorageCheckArgs.push_back("-nomanagedtor"); + vEncryptedStorageCheckArgs.push_back("-nogui"); + vEncryptedStorageCheckArgs.push_back("-walletstoragecheck"); + int nEncryptedStorageCheckRet = + RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck); + nFail += Check(nEncryptedStorageCheckRet == 0 && + FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") && + FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") && + FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"), + "the storage sanity check accepts an encrypted wallet") ? 0 : 1; + string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; vector vEncryptedStorageAuditJsonArgs; vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6f8258d..265febe 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -13,6 +13,10 @@ #include "bip32.h" #include "walletcmd.h" +#ifndef _WIN32 +#include +#endif + #undef printf // How far ahead to look for used addresses when restoring. @@ -658,6 +662,139 @@ static bool WriteAuditTextFile(const std::string& strPath, return true; } +static bool DirectoryHasFileWithPrefixLocal(const std::string& strDir, + const std::string& strPrefix) +{ +#ifdef _WIN32 + std::string pattern = strDir + "/*"; + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); + if (hFind == INVALID_HANDLE_VALUE) + return false; + bool fFound = false; + do + { + if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + { + std::string name = findData.cFileName; + if (name.compare(0, strPrefix.size(), strPrefix) == 0) + { + fFound = true; + break; + } + } + } + while (FindNextFileA(hFind, &findData)); + FindClose(hFind); + return fFound; +#else + DIR* dir = opendir(strDir.c_str()); + if (!dir) + return false; + bool fFound = false; + struct dirent* ent; + while ((ent = readdir(dir)) != NULL) + { + std::string name = ent->d_name; + if (name.compare(0, strPrefix.size(), strPrefix) == 0) + { + fFound = true; + break; + } + } + closedir(dir); + return fFound; +#endif +} + +static void AddWalletStorageFailure(std::vector& vFailures, + const std::string& strFailure) +{ + vFailures.push_back(strFailure); +} + +static void BuildWalletStorageSanityFailures(const WalletStorageAuditCounts& counts, + std::vector& vFailures) +{ + bool fPlainHD = counts.nPlainHDMaster > 0 || counts.nPlainHDChainCode > 0; + bool fCryptedHD = counts.nCryptedHDMaster > 0 || counts.nCryptedHDChainCode > 0; + bool fEncryptedRecords = counts.nEncryptedKeys > 0 || + counts.nMasterKeys > 0 || + fCryptedHD; + + if (counts.nMalformed > 0) + AddWalletStorageFailure(vFailures, "wallet.dat contains malformed records"); + if (counts.nUnknown > 0) + AddWalletStorageFailure(vFailures, "wallet.dat contains unknown records"); + if ((counts.nPlainHDMaster > 0) != (counts.nPlainHDChainCode > 0)) + AddWalletStorageFailure(vFailures, "plain HD seed is incomplete"); + if ((counts.nCryptedHDMaster > 0) != (counts.nCryptedHDChainCode > 0)) + AddWalletStorageFailure(vFailures, "encrypted HD seed is incomplete"); + if (fPlainHD && fCryptedHD) + AddWalletStorageFailure(vFailures, "wallet.dat contains both plain and encrypted HD seed records"); + if (counts.nDefaultKey > 1) + AddWalletStorageFailure(vFailures, "wallet.dat contains more than one default public key record"); + if (counts.nWalletMinVersion > 1) + AddWalletStorageFailure(vFailures, "wallet.dat contains more than one minimum-version record"); + + if (fEncryptedRecords) + { + if (counts.nPlainKeys > 0) + AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain private key records"); + if (fPlainHD) + AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain HD seed records"); + if (counts.nMasterKeys == 0) + AddWalletStorageFailure(vFailures, "encrypted wallet has no encryption master key record"); + if (counts.nWalletMinVersion == 0) + AddWalletStorageFailure(vFailures, "encrypted wallet has no minimum-version record"); + if (DirectoryHasFileWithPrefixLocal(GetAppDir() + "/database", "log.")) + AddWalletStorageFailure(vFailures, "Berkeley DB log files are still present for an encrypted wallet"); + } +} + +int CmdWalletStorageCheck() +{ + AttachTerminal(); + + WalletStorageAuditCounts counts; + std::string strError; + if (!ReadWalletStorageCounts(counts, strError)) + { + fprintf(stderr, "Cannot check wallet storage: %s\n", strError.c_str()); + return 1; + } + + std::vector vFailures; + BuildWalletStorageSanityFailures(counts, vFailures); + + printf("Wallet storage sanity check\n"); + printf(" records total: %u\n", counts.nTotal); + printf(" encrypted records: %s\n", + (counts.nEncryptedKeys || counts.nMasterKeys || + counts.nCryptedHDMaster || counts.nCryptedHDChainCode) ? "yes" : "no"); + printf(" plain private keys: %u\n", counts.nPlainKeys); + printf(" encrypted private keys: %u\n", counts.nEncryptedKeys); + printf(" plain HD seed: %s\n", + HDSeedStorageState(counts.nPlainHDMaster, + counts.nPlainHDChainCode).c_str()); + printf(" encrypted HD seed: %s\n", + HDSeedStorageState(counts.nCryptedHDMaster, + counts.nCryptedHDChainCode).c_str()); + if (vFailures.empty()) + { + printf(" storage sanity: ok\n"); + fflush(stdout); + return 0; + } + + printf(" storage sanity: failed\n"); + for (std::vector::const_iterator it = vFailures.begin(); + it != vFailures.end(); ++it) + printf(" failure: %s\n", it->c_str()); + fflush(stdout); + return 2; +} + int CmdWalletStorageAudit(const std::string& strJsonOut) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index c6342a6..6077669 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -66,6 +66,10 @@ int CmdRecoveryAudit(); // addresses, transaction ids, labels, or other wallet values. int CmdWalletStorageAudit(const std::string& strJsonOut = ""); +// Diagnostic: fail closed when wallet.dat contains an internally inconsistent +// or migration-unsafe mix of storage records. +int CmdWalletStorageCheck(); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From 83b9c4feddeac14bbc5edf63b44d3a2013384ecc Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 11:51:14 -0300 Subject: tests: prove wallet storage sanity failures --- src/main_gui.cpp | 50 +++++++++------ src/selftest.cpp | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++++- src/selftest.h | 1 + src/walletcmd.cpp | 5 ++ 4 files changed, 222 insertions(+), 21 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 8763211..feda428 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -113,8 +113,9 @@ static void PrintUsage() printf(" /debug\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); - printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); - printf(" wallet-portability, net-message, consensus-limits, pool-stratum,\n"); + printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); + printf(" wallet-crypto, wallet-encrypt, wallet-portability, net-message,\n"); + printf(" consensus-limits, pool-stratum,\n"); printf(" parse-money, socks5-proxy, or managed-tor\n"); printf("\n"); printf("Mining mode:\n"); @@ -495,10 +496,37 @@ int main(int argc, char* argv[]) ParseStartupArguments(argc, argv); + // Hidden self-test helper. It must run before LoadWallet(): several + // storage-sanity scenarios deliberately make wallet.dat unsafe to load. + string strSelfTestMutateWallet = + argval2(argc, argv, "/selftestmutatewallet", "-selftestmutatewallet"); + if (!strSelfTestMutateWallet.empty()) + return RunSelfTestMutateWallet(strSelfTestMutateWallet); + string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); if (!strSelfTest.empty()) return RunSelfTest(strSelfTest); + // Storage diagnostics must run before LoadWallet(). Their job is to + // 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()) + { + int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); + DBFlush(true); + return nRet; + } + + if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) + { + int nRet = CmdWalletStorageCheck(); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley @@ -710,24 +738,6 @@ int main(int argc, char* argv[]) return nRet; } - string strWalletStorageAuditJson = - argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson"); - if (arg(argc,argv,"/walletstorageaudit") || - arg(argc,argv,"-walletstorageaudit") || - !strWalletStorageAuditJson.empty()) - { - int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson); - DBFlush(true); - return nRet; - } - - if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) - { - int nRet = CmdWalletStorageCheck(); - DBFlush(true); - return nRet; - } - if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) { int nRet = CmdNewPhrase(); diff --git a/src/selftest.cpp b/src/selftest.cpp index 81c4439..81aa480 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -736,6 +736,189 @@ static int RunWalletFormatSelfTest() return nFail == 0 ? 0 : 1; } +static bool FileContainsText(const string& strPath, const string& strNeedle); +static bool MakeDirLocal(const string& strPath); +static int RunBitflashChild(const string& strExe, + const vector& vArgs, + const string* pStdinFile, + const string* pOutputFile); + +class CWalletStorageSanityRawDB : public CWalletDB +{ +public: + CWalletStorageSanityRawDB(const char* pszMode="r+") + : CWalletDB(pszMode) { } + + bool WriteUnknownRecord() + { + return Write(string("storage-sanity-unknown"), 1); + } + + bool WriteMalformedKeyRecord() + { + if (!pdb) + return false; + unsigned char chKey = 0xff; + unsigned char chValue = 0x01; + Dbt datKey(&chKey, 1); + Dbt datValue(&chValue, 1); + return pdb->put(GetTxn(), &datKey, &datValue, 0) == 0; + } + + bool MakePlainHDSeedIncomplete() + { + return Erase(string("hdchaincode")); + } + + bool AddEncryptedMarkerToPlainWallet() + { + CWalletMasterKey kMasterKey; + kMasterKey.vchCryptedKey.resize(48); + for (size_t i = 0; i < kMasterKey.vchCryptedKey.size(); i++) + kMasterKey.vchCryptedKey[i] = (unsigned char)(0x80 + (i & 0x3f)); + for (size_t i = 0; i < kMasterKey.vchSalt.size(); i++) + kMasterKey.vchSalt[i] = (unsigned char)(0x40 + i); + return WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) && + WriteMasterKey(1, kMasterKey); + } +}; + +int RunSelfTestMutateWallet(const std::string& name) +{ + bool fOk = false; + try + { + CWalletStorageSanityRawDB walletdb; + if (name == "unknown-record") + fOk = walletdb.WriteUnknownRecord(); + else if (name == "malformed-key") + fOk = walletdb.WriteMalformedKeyRecord(); + else if (name == "incomplete-plain-hd") + fOk = walletdb.MakePlainHDSeedIncomplete(); + else if (name == "encrypted-marker-with-plain-keys") + fOk = walletdb.AddEncryptedMarkerToPlainWallet(); + else + { + fprintf(stderr, "Unknown wallet storage mutation '%s'\n", name.c_str()); + return 1; + } + } + catch (const std::exception& e) + { + fprintf(stderr, "Cannot mutate wallet storage: %s\n", e.what()); + return 1; + } + catch (...) + { + fprintf(stderr, "Cannot mutate wallet storage: unknown exception\n"); + return 1; + } + + DBFlush(true); + return fOk ? 0 : 1; +} + +static int RunWalletStorageSanitySelfTest() +{ + fflush(stdout); + printf("wallet-storage-sanity self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd)) + { + printf(" FAIL could not read current directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + printf(" temp root: %s\n", tmp.c_str()); + + try + { +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + + struct StorageScenario + { + const char* pszName; + const char* pszExpectedFailure; + }; + + StorageScenario scenarios[] = { + { "unknown-record", "wallet.dat contains unknown records" }, + { "malformed-key", "wallet.dat contains malformed records" }, + { "incomplete-plain-hd", "plain HD seed is incomplete" }, + { "encrypted-marker-with-plain-keys", + "encrypted wallet still contains plain private key records" }, + }; + + for (size_t i = 0; i < sizeof(scenarios)/sizeof(scenarios[0]); i++) + { + string strScenarioDir = tmp + "/" + scenarios[i].pszName; + nFail += Check(MakeDirLocal(strScenarioDir), + "scenario datadir can be created") ? 0 : 1; + + string strCreateOut = tmp + "/" + scenarios[i].pszName + "-create.txt"; + vector vCreateArgs; + vCreateArgs.push_back("-datadir=" + strScenarioDir); + vCreateArgs.push_back("-nomanagedtor"); + vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-newphrase"); + int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); + nFail += Check(nCreateRet == 0 && + FileContainsText(strCreateOut, "Write these twelve words down"), + "scenario phrase wallet can be created") ? 0 : 1; + + string strMutateOut = tmp + "/" + scenarios[i].pszName + "-mutate.txt"; + vector vMutateArgs; + vMutateArgs.push_back("-datadir=" + strScenarioDir); + vMutateArgs.push_back("-nomanagedtor"); + vMutateArgs.push_back("-nogui"); + vMutateArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); + int nMutateRet = RunBitflashChild(strExe, vMutateArgs, NULL, &strMutateOut); + nFail += Check(nMutateRet == 0, scenarios[i].pszName) ? 0 : 1; + + string strCheckOut = tmp + "/" + scenarios[i].pszName + "-check.txt"; + vector vCheckArgs; + vCheckArgs.push_back("-datadir=" + strScenarioDir); + vCheckArgs.push_back("-nomanagedtor"); + vCheckArgs.push_back("-nogui"); + vCheckArgs.push_back("-walletstoragecheck"); + int nCheckRet = RunBitflashChild(strExe, vCheckArgs, NULL, &strCheckOut); + nFail += Check(nCheckRet == 2 && + FileContainsText(strCheckOut, "storage sanity: failed") && + FileContainsText(strCheckOut, scenarios[i].pszExpectedFailure), + scenarios[i].pszExpectedFailure) ? 0 : 1; + } + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) @@ -1991,6 +2174,8 @@ int RunSelfTest(const std::string& name) return RunWalletHDSelfTest(); if (name == "wallet-format") return RunWalletFormatSelfTest(); + if (name == "wallet-storage-sanity") + return RunWalletStorageSanitySelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2011,6 +2196,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-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, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/selftest.h b/src/selftest.h index d3699c6..0ffe232 100644 --- a/src/selftest.h +++ b/src/selftest.h @@ -7,5 +7,6 @@ #include int RunSelfTest(const std::string& name); +int RunSelfTestMutateWallet(const std::string& name); #endif diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 71f1ec3..6fb09a3 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -573,6 +573,11 @@ static bool ReadWalletStorageCounts(WalletStorageAuditCounts& counts, CDataStream ssKey = ssKeyIn; std::string strType; ssKey >> strType; + if (ssKey.fail()) + { + counts.nMalformed++; + return true; + } CountWalletStorageType(strType, counts); } catch (...) -- 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/main_gui.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 c49b92a9a79a398b51200e69f7dde8a467b9c925 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 15:03:21 -0300 Subject: wallet: add experimental SQLite record store --- .github/workflows/ci.yml | 1 + Makefile | 8 +- docs/storage.md | 19 ++++ src/Makefile | 10 +- src/main_gui.cpp | 3 +- src/makefile.mingw | 8 +- src/selftest.cpp | 108 +++++++++++++++++++- src/wallet_sqlite.cpp | 254 +++++++++++++++++++++++++++++++++++++++++++++++ src/wallet_sqlite.h | 40 ++++++++ 9 files changed, 439 insertions(+), 12 deletions(-) create mode 100644 src/wallet_sqlite.cpp create mode 100644 src/wallet_sqlite.h (limited to 'src/main_gui.cpp') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4045f4..e22b5a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: mingw-w64-ucrt-x86_64-glfw mingw-w64-ucrt-x86_64-openssl mingw-w64-ucrt-x86_64-db + mingw-w64-ucrt-x86_64-sqlite3 mingw-w64-ucrt-x86_64-libsodium mingw-w64-ucrt-x86_64-nlohmann-json mingw-w64-ucrt-x86_64-boost diff --git a/Makefile b/Makefile index 60d1227..fab2507 100644 --- a/Makefile +++ b/Makefile @@ -43,10 +43,11 @@ deps-apt: @if command -v apt-get >/dev/null 2>&1; then \ $(SUDO) apt-get install -y build-essential cmake git pkg-config autoconf \ libtool libssl-dev libdb5.3++-dev libsodium-dev nlohmann-json3-dev \ - libboost-system-dev libglfw3-dev libgl-dev python3-pil clang; \ + libboost-system-dev libglfw3-dev libgl-dev libsqlite3-dev \ + python3-pil clang; \ else \ echo "Not an apt system — install: g++ cmake git autoconf libtool"; \ - echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl python3-pil"; \ + echo " libssl libdb++ libsodium nlohmann-json boost glfw3 opengl sqlite3 python3-pil"; \ fi deps-secp256k1: @@ -113,7 +114,7 @@ img = ico.convert('RGBA'); \ img.save('/tmp/bitflash.AppDir/bitflash.png'); \ import shutil; shutil.copy('/tmp/bitflash.AppDir/bitflash.png', \ '/tmp/bitflash.AppDir/usr/share/icons/hicolor/256x256/apps/bitflash.png')" - @for lib in libsodium.so.23 libdb_cxx-5.3.so libglfw.so.3; do \ + @for lib in libsodium.so.23 libdb_cxx-5.3.so libsqlite3.so.0 libglfw.so.3; do \ path=$$(ldconfig -p | grep " $$lib " | awk '{print $$NF}' | head -1); \ [ -z "$$path" ] && path=$$(find /usr/lib /lib -name "$$lib" 2>/dev/null | head -1); \ [ -n "$$path" ] && cp -L "$$path" /tmp/bitflash.AppDir/usr/lib/ && echo " bundled $$lib" || echo " missing $$lib"; \ @@ -131,6 +132,7 @@ deps-windows: mingw-w64-ucrt-x86_64-glfw \ mingw-w64-ucrt-x86_64-openssl \ mingw-w64-ucrt-x86_64-db \ + mingw-w64-ucrt-x86_64-sqlite3 \ mingw-w64-ucrt-x86_64-libsodium \ mingw-w64-ucrt-x86_64-nlohmann-json \ mingw-w64-ucrt-x86_64-boost \ diff --git a/docs/storage.md b/docs/storage.md index c1cda45..f75f9b9 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -85,6 +85,25 @@ A future wallet backend must keep these properties: - recovery phrase encrypted and locked, requiring unlock before coverage can be audited. +## Experimental SQLite Wallet Store + +The SQLite wallet path starts as a byte-preserving record store, not as a new +wallet format: + +```sql +CREATE TABLE wallet_records ( + key BLOB PRIMARY KEY NOT NULL, + value BLOB NOT NULL +); +PRAGMA user_version=1; +``` + +Keys and values are the same serialized `CDataStream` bytes used by `wallet.dat`. +The first implementation is exercised only by `-selftest=wallet-sqlite`; the +runtime wallet still opens Berkeley DB. The purpose is to prove schema creation, +raw record write/read, close/reopen behavior, and value preservation before any +BDB-to-SQLite migration or runtime backend flag exists. + ## `blkindex.dat` `blkindex.dat` is accessed through `CTxDB`. diff --git a/src/Makefile b/src/Makefile index 19ad33a..cfa40f6 100644 --- a/src/Makefile +++ b/src/Makefile @@ -40,7 +40,7 @@ FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ -lglfw -lGL # Every header a .cpp here includes has to be listed, not just the interesting @@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h + selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -61,7 +61,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ + obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -116,7 +117,7 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ -L$(RANDOMX_DIR)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ obj-node/main_gui.o obj-node/headless.o @@ -187,6 +188,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-keypool ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=wallet-format + ./bitflash-node -selftest=wallet-sqlite ./bitflash-node -selftest=wallet-crypto ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 705b4ef..965c818 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(" db-env-reopen, wallet-crypto, wallet-encrypt, wallet-portability,\n"); + printf(" db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt,\n"); + printf(" wallet-portability,\n"); printf(" net-message,\n"); printf(" consensus-limits, pool-stratum,\n"); printf(" parse-money, socks5-proxy, or managed-tor\n"); diff --git a/src/makefile.mingw b/src/makefile.mingw index 333be96..2ff4e11 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -24,7 +24,7 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ LIBS = -L$(DEPS)/lib -lsecp256k1 \ -L$(RANDOMX)/build -lrandomx \ - -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ + -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \ -LC:/msys64/ucrt64/lib \ -lglfw3 -lopengl32 -lgdi32 \ -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \ @@ -37,7 +37,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \ + obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -51,7 +52,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h + selftest.h walletcmd.h wallet_sqlite.h sockcount.h proxy.h tor.h compat.h font_roboto.h all: bitflash.exe @@ -107,6 +108,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-keypool ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=wallet-format + ./bitflash.exe -selftest=wallet-sqlite ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message diff --git a/src/selftest.cpp b/src/selftest.cpp index 31c168f..f9c4a8e 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -13,6 +13,7 @@ #include "sockcount.h" #include "tor.h" #include "walletcmd.h" +#include "wallet_sqlite.h" extern int RunPoolStratumSelfTest(); @@ -62,6 +63,11 @@ static std::string HexStrLocal(const std::vector& v) return out; } +static vector DataStreamBytes(const CDataStream& ss) +{ + return vector(ss.begin(), ss.end()); +} + static bool MakeTempDir(std::string& out) { #ifdef _WIN32 @@ -1030,6 +1036,104 @@ static int RunDbEnvReopenSelfTest() return nFail == 0 ? 0 : 1; } +static int RunWalletSQLiteSelfTest() +{ + fflush(stdout); + printf("wallet-sqlite self-test\n"); + + std::string tmp; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + + int nFail = 0; + string strPath = tmp + "/wallet.sqlite"; + string strError; + + try + { + CDataStream ssKeySetting(SER_DISK); + ssKeySetting << make_pair(string("setting"), string("sqlite-roundtrip")); + CDataStream ssValueSetting(SER_DISK); + ssValueSetting << (int64)424242; + + vector vchPubKey; + vchPubKey.push_back(0x04); + for (int i = 0; i < 64; i++) + vchPubKey.push_back((unsigned char)i); + CDataStream ssKeyDefault(SER_DISK); + ssKeyDefault << string("defaultkey"); + CDataStream ssValueDefault(SER_DISK); + ssValueDefault << vchPubKey; + + { + CWalletDBSQLite db; + nFail += Check(db.Open(strPath, strError), + "SQLite wallet opens and creates schema") ? 0 : 1; + nFail += Check(db.BeginTransaction(strError), + "SQLite wallet begins a write transaction") ? 0 : 1; + nFail += Check(db.WriteRecord(DataStreamBytes(ssKeySetting), + DataStreamBytes(ssValueSetting), + strError), + "SQLite wallet writes a serialized setting record") ? 0 : 1; + nFail += Check(db.WriteRecord(DataStreamBytes(ssKeyDefault), + DataStreamBytes(ssValueDefault), + strError), + "SQLite wallet writes a serialized defaultkey record") ? 0 : 1; + nFail += Check(db.CommitTransaction(strError), + "SQLite wallet commits a write transaction") ? 0 : 1; + int nCount = 0; + nFail += Check(db.CountRecords(nCount, strError) && nCount == 2, + "SQLite wallet counts raw records") ? 0 : 1; + } + + { + CWalletDBSQLite db; + nFail += Check(db.Open(strPath, strError), + "SQLite wallet reopens") ? 0 : 1; + vector vchRead; + nFail += Check(db.ReadRecord(DataStreamBytes(ssKeySetting), + vchRead, + strError), + "SQLite wallet reads a serialized setting record") ? 0 : 1; + int64 nReadValue = 0; + CDataStream ssRead(vchRead, SER_DISK); + ssRead >> nReadValue; + nFail += Check(!ssRead.fail() && nReadValue == 424242, + "SQLite wallet preserves serialized values byte-for-byte") ? 0 : 1; + + vchRead.clear(); + nFail += Check(db.ReadRecord(DataStreamBytes(ssKeyDefault), + vchRead, + strError), + "SQLite wallet reads a serialized defaultkey record") ? 0 : 1; + vector vchReadPubKey; + CDataStream ssReadDefault(vchRead, SER_DISK); + ssReadDefault >> vchReadPubKey; + nFail += Check(!ssReadDefault.fail() && vchReadPubKey == vchPubKey, + "SQLite wallet preserves public key blobs") ? 0 : 1; + } + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) @@ -2289,6 +2393,8 @@ int RunSelfTest(const std::string& name) return RunWalletStorageSanitySelfTest(); if (name == "db-env-reopen") return RunDbEnvReopenSelfTest(); + if (name == "wallet-sqlite") + return RunWalletSQLiteSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2309,6 +2415,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-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-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp new file mode 100644 index 0000000..4104ac6 --- /dev/null +++ b/src/wallet_sqlite.cpp @@ -0,0 +1,254 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#include "headers.h" +#include "wallet_sqlite.h" + +static void SetSQLiteError(sqlite3* pdb, const string& strContext, + string& strError) +{ + if (pdb) + strError = strContext + ": " + sqlite3_errmsg(pdb); + else + strError = strContext; +} + +CWalletDBSQLite::CWalletDBSQLite() : pdb(NULL) +{ +} + +CWalletDBSQLite::~CWalletDBSQLite() +{ + Close(); +} + +bool CWalletDBSQLite::Open(const string& strPath, string& strError) +{ + Close(); + + int ret = sqlite3_open_v2(strPath.c_str(), &pdb, + SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, + NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot open SQLite wallet", strError); + Close(); + return false; + } + + sqlite3_busy_timeout(pdb, 5000); + + if (!Exec("PRAGMA journal_mode=WAL;", strError)) + { + Close(); + return false; + } + if (!Exec("PRAGMA synchronous=FULL;", strError)) + { + Close(); + return false; + } + if (!InitSchema(strError)) + { + Close(); + return false; + } + return true; +} + +void CWalletDBSQLite::Close() +{ + if (pdb) + { + sqlite3_close(pdb); + pdb = NULL; + } +} + +bool CWalletDBSQLite::BeginTransaction(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("BEGIN IMMEDIATE;", strError); +} + +bool CWalletDBSQLite::CommitTransaction(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("COMMIT;", strError); +} + +bool CWalletDBSQLite::RollbackTransaction(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("ROLLBACK;", strError); +} + +bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) +{ + char* pszErr = NULL; + int ret = sqlite3_exec(pdb, pszSql, NULL, NULL, &pszErr); + if (ret != SQLITE_OK) + { + if (pszErr) + { + strError = pszErr; + sqlite3_free(pszErr); + } + else + SetSQLiteError(pdb, "SQLite exec failed", strError); + return false; + } + return true; +} + +bool CWalletDBSQLite::InitSchema(string& strError) +{ + return Exec("CREATE TABLE IF NOT EXISTS wallet_records (" + "key BLOB PRIMARY KEY NOT NULL," + "value BLOB NOT NULL" + ");", + strError) && + Exec("PRAGMA user_version=1;", strError); +} + +bool CWalletDBSQLite::WriteRecord(const vector& vchKey, + const vector& vchValue, + string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + if (vchKey.empty()) + { + strError = "SQLite wallet record key is empty"; + return false; + } + + sqlite3_stmt* stmt = NULL; + const char* pszSql = + "INSERT OR REPLACE INTO wallet_records(key, value) VALUES(?, ?);"; + int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot prepare SQLite wallet write", strError); + return false; + } + + ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), + SQLITE_TRANSIENT); + if (ret == SQLITE_OK) + ret = sqlite3_bind_blob(stmt, 2, + vchValue.empty() ? NULL : &vchValue[0], + (int)vchValue.size(), + SQLITE_TRANSIENT); + if (ret == SQLITE_OK) + ret = sqlite3_step(stmt); + + bool fOk = (ret == SQLITE_DONE); + if (!fOk) + SetSQLiteError(pdb, "cannot write SQLite wallet record", strError); + sqlite3_finalize(stmt); + return fOk; +} + +bool CWalletDBSQLite::ReadRecord(const vector& vchKey, + vector& vchValueRet, + string& strError) +{ + vchValueRet.clear(); + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + if (vchKey.empty()) + { + strError = "SQLite wallet record key is empty"; + return false; + } + + sqlite3_stmt* stmt = NULL; + const char* pszSql = "SELECT value FROM wallet_records WHERE key = ?;"; + int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot prepare SQLite wallet read", strError); + return false; + } + + ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(), + SQLITE_TRANSIENT); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot bind SQLite wallet key", strError); + sqlite3_finalize(stmt); + return false; + } + + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) + { + const unsigned char* pch = + (const unsigned char*)sqlite3_column_blob(stmt, 0); + int nBytes = sqlite3_column_bytes(stmt, 0); + if (nBytes > 0 && pch) + vchValueRet.assign(pch, pch + nBytes); + sqlite3_finalize(stmt); + return true; + } + + if (ret == SQLITE_DONE) + strError = "SQLite wallet record not found"; + else + SetSQLiteError(pdb, "cannot read SQLite wallet record", strError); + sqlite3_finalize(stmt); + return false; +} + +bool CWalletDBSQLite::CountRecords(int& nRecordsRet, string& strError) +{ + nRecordsRet = 0; + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + + sqlite3_stmt* stmt = NULL; + int ret = sqlite3_prepare_v2(pdb, + "SELECT COUNT(*) FROM wallet_records;", + -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + SetSQLiteError(pdb, "cannot prepare SQLite wallet count", strError); + return false; + } + + ret = sqlite3_step(stmt); + if (ret != SQLITE_ROW) + { + SetSQLiteError(pdb, "cannot count SQLite wallet records", strError); + sqlite3_finalize(stmt); + return false; + } + + nRecordsRet = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + return true; +} diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h new file mode 100644 index 0000000..9812d3a --- /dev/null +++ b/src/wallet_sqlite.h @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#ifndef BITFLASH_WALLET_SQLITE_H +#define BITFLASH_WALLET_SQLITE_H + +#include + +class CWalletDBSQLite +{ +public: + CWalletDBSQLite(); + ~CWalletDBSQLite(); + + bool Open(const string& strPath, string& strError); + void Close(); + + bool BeginTransaction(string& strError); + bool CommitTransaction(string& strError); + bool RollbackTransaction(string& strError); + + bool WriteRecord(const vector& vchKey, + const vector& vchValue, + string& strError); + bool ReadRecord(const vector& vchKey, + vector& vchValueRet, + string& strError); + bool CountRecords(int& nRecordsRet, string& strError); + +private: + sqlite3* pdb; + + bool Exec(const char* pszSql, string& strError); + bool InitSchema(string& strError); + + CWalletDBSQLite(const CWalletDBSQLite&); + void operator=(const CWalletDBSQLite&); +}; + +#endif -- cgit v1.2.3 From 59b9d11015f0e3a2475d0750fc534583c2ca9f77 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 15:56:12 -0300 Subject: wallet: export BDB records to SQLite --- docs/storage.md | 6 ++ src/Makefile | 1 + src/main_gui.cpp | 15 ++++- src/makefile.mingw | 1 + src/selftest.cpp | 148 +++++++++++++++++++++++++++++++++++++++++++++++++- src/wallet_sqlite.cpp | 10 ++++ src/wallet_sqlite.h | 1 + src/walletcmd.cpp | 97 +++++++++++++++++++++++++++++++++ src/walletcmd.h | 5 ++ 9 files changed, 281 insertions(+), 3 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/docs/storage.md b/docs/storage.md index f75f9b9..bb414b5 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -104,6 +104,12 @@ runtime wallet still opens Berkeley DB. The purpose is to prove schema creation, raw record write/read, close/reopen behavior, and value preservation before any BDB-to-SQLite migration or runtime backend flag exists. +`-walletsqliteexport=FILE` is the next staging tool. It streams every raw +`wallet.dat` record through `ScanWalletRecords()` and writes those same key and +value bytes into the SQLite table inside one transaction. It refuses to +overwrite an existing export and still does not change the runtime wallet +backend. + ## `blkindex.dat` `blkindex.dat` is accessed through `CTxDB`. diff --git a/src/Makefile b/src/Makefile index cfa40f6..b83ff58 100644 --- a/src/Makefile +++ b/src/Makefile @@ -189,6 +189,7 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=wallet-hd ./bitflash-node -selftest=wallet-format ./bitflash-node -selftest=wallet-sqlite + ./bitflash-node -selftest=wallet-sqlite-migration ./bitflash-node -selftest=wallet-crypto ./bitflash-node -selftest=wallet-encrypt ./bitflash-node -selftest=net-message diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 965c818..6e89270 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -114,8 +114,8 @@ static void PrintUsage() printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); - printf(" db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt,\n"); - printf(" wallet-portability,\n"); + printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n"); + printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n"); printf(" net-message,\n"); printf(" consensus-limits, pool-stratum,\n"); printf(" parse-money, socks5-proxy, or managed-tor\n"); @@ -181,6 +181,8 @@ static void PrintUsage() printf(" (write the same audit as deterministic JSON)\n"); printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); printf(" wallet.dat storage state)\n"); + printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); + printf(" experimental SQLite store, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -529,6 +531,15 @@ int main(int argc, char* argv[]) return nRet; } + string strWalletSQLiteExport = + argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport"); + if (!strWalletSQLiteExport.empty()) + { + int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/makefile.mingw b/src/makefile.mingw index 2ff4e11..14e6b32 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -109,6 +109,7 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=wallet-hd ./bitflash.exe -selftest=wallet-format ./bitflash.exe -selftest=wallet-sqlite + ./bitflash.exe -selftest=wallet-sqlite-migration ./bitflash.exe -selftest=wallet-crypto ./bitflash.exe -selftest=wallet-encrypt ./bitflash.exe -selftest=net-message diff --git a/src/selftest.cpp b/src/selftest.cpp index f9c4a8e..7a54271 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1134,6 +1134,150 @@ static int RunWalletSQLiteSelfTest() return nFail == 0 ? 0 : 1; } +class CWalletRecordMapVisitor : public CWalletRecordVisitor +{ +public: + std::map, vector >& mapRecords; + + explicit CWalletRecordMapVisitor( + std::map, vector >& mapRecordsIn) + : mapRecords(mapRecordsIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + vector vchKey = DataStreamBytes(ssKey); + if (mapRecords.count(vchKey)) + { + strErrorRet = "duplicate serialized wallet key"; + return false; + } + mapRecords[vchKey] = DataStreamBytes(ssValue); + return true; + } +}; + +static int RunWalletSQLiteMigrationSelfTest() +{ + fflush(stdout); + printf("wallet-sqlite-migration self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd)) + { + printf(" FAIL could not read current directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + printf(" temp root: %s\n", tmp.c_str()); + + try + { +#ifdef _WIN32 + string strExe = cwd + "\\bitflash.exe"; +#else + string strExe = cwd + "/bitflash-node"; +#endif + string strWalletDir = tmp + "/wallet"; + nFail += Check(MakeDirLocal(strWalletDir), + "temporary wallet directory can be created") ? 0 : 1; + + string strCreateOut = tmp + "/create.txt"; + vector vCreateArgs; + vCreateArgs.push_back("-datadir=" + strWalletDir); + vCreateArgs.push_back("-nomanagedtor"); + vCreateArgs.push_back("-nogui"); + vCreateArgs.push_back("-newphrase"); + int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); + nFail += Check(nCreateRet == 0 && + FileContainsText(strCreateOut, "Write these twelve words down"), + "a phrase wallet can be created for SQLite export") ? 0 : 1; + + string strSQLite = tmp + "/wallet.sqlite"; + string strExportOut = tmp + "/sqlite-export.txt"; + vector vExportArgs; + vExportArgs.push_back("-datadir=" + strWalletDir); + vExportArgs.push_back("-nomanagedtor"); + vExportArgs.push_back("-nogui"); + vExportArgs.push_back("-walletsqliteexport=" + strSQLite); + int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut); + nFail += Check(nExportRet == 0 && + FileExists(strSQLite.c_str()) && + FileContainsText(strExportOut, "SQLite wallet export written"), + "wallet.dat can be exported to SQLite") ? 0 : 1; + + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; + int nOverwriteRet = + RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); + nFail += Check(nOverwriteRet != 0 && + FileContainsText(strOverwriteOut, + "Refusing to overwrite existing SQLite wallet export"), + "SQLite wallet export refuses to overwrite") ? 0 : 1; + remove((strSQLite + "-wal").c_str()); + remove((strSQLite + "-shm").c_str()); + + std::map, vector > mapBDB; + string strOldDataDir = strSetDataDir; + string strScanError; + strSetDataDir = strWalletDir; + CWalletRecordMapVisitor visitor(mapBDB); + bool fScanOk = ScanWalletRecords(visitor, strScanError); + DBFlush(true); + strSetDataDir = strOldDataDir; + nFail += Check(fScanOk && !mapBDB.empty(), + "wallet.dat records can be scanned for comparison") ? 0 : 1; + + CWalletDBSQLite db; + string strError; + nFail += Check(db.Open(strSQLite, strError), + "exported SQLite wallet opens") ? 0 : 1; + int nSQLiteRecords = 0; + nFail += Check(db.CountRecords(nSQLiteRecords, strError) && + nSQLiteRecords == (int)mapBDB.size(), + "SQLite export has the same record count as wallet.dat") ? 0 : 1; + + bool fAllMatch = true; + for (std::map, vector >::const_iterator it = + mapBDB.begin(); it != mapBDB.end(); ++it) + { + vector vchSQLiteValue; + if (!db.ReadRecord(it->first, vchSQLiteValue, strError) || + vchSQLiteValue != it->second) + { + fAllMatch = false; + break; + } + } + nFail += Check(fAllMatch, + "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + static void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) @@ -2395,6 +2539,8 @@ int RunSelfTest(const std::string& name) return RunDbEnvReopenSelfTest(); if (name == "wallet-sqlite") return RunWalletSQLiteSelfTest(); + if (name == "wallet-sqlite-migration") + return RunWalletSQLiteMigrationSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") @@ -2415,6 +2561,6 @@ int RunSelfTest(const std::string& name) return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp index 4104ac6..4369af6 100644 --- a/src/wallet_sqlite.cpp +++ b/src/wallet_sqlite.cpp @@ -97,6 +97,16 @@ bool CWalletDBSQLite::RollbackTransaction(string& strError) return Exec("ROLLBACK;", strError); } +bool CWalletDBSQLite::Checkpoint(string& strError) +{ + if (!pdb) + { + strError = "SQLite wallet is not open"; + return false; + } + return Exec("PRAGMA wal_checkpoint(TRUNCATE);", strError); +} + bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) { char* pszErr = NULL; diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h index 9812d3a..5ef862d 100644 --- a/src/wallet_sqlite.h +++ b/src/wallet_sqlite.h @@ -18,6 +18,7 @@ public: bool BeginTransaction(string& strError); bool CommitTransaction(string& strError); bool RollbackTransaction(string& strError); + bool Checkpoint(string& strError); bool WriteRecord(const vector& vchKey, const vector& vchValue, diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6fb09a3..20e7e33 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -12,6 +12,7 @@ #include "headers_core.h" #include "bip32.h" #include "walletcmd.h" +#include "wallet_sqlite.h" #undef printf @@ -790,6 +791,102 @@ int CmdWalletStorageAudit(const std::string& strJsonOut) return 0; } +static vector DataStreamBytesLocal(const CDataStream& ss) +{ + return vector(ss.begin(), ss.end()); +} + +class CWalletSQLiteExportVisitor : public CWalletRecordVisitor +{ +public: + CWalletDBSQLite& db; + int& nCopied; + + CWalletSQLiteExportVisitor(CWalletDBSQLite& dbIn, int& nCopiedIn) + : db(dbIn), nCopied(nCopiedIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + if (!db.WriteRecord(DataStreamBytesLocal(ssKey), + DataStreamBytesLocal(ssValue), + strErrorRet)) + return false; + nCopied++; + return true; + } +}; + +int CmdWalletSQLiteExport(const std::string& strDest) +{ + AttachTerminal(); + + if (strDest.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); + return 1; + } + if (FileExists(strDest.c_str())) + { + fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n", + strDest.c_str()); + return 1; + } + + string strError; + CWalletDBSQLite db; + if (!db.Open(strDest, strError)) + { + fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + if (!db.BeginTransaction(strError)) + { + fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nCopied = 0; + CWalletSQLiteExportVisitor visitor(db, nCopied); + if (!ScanWalletRecords(visitor, strError)) + { + string strRollbackError; + db.RollbackTransaction(strRollbackError); + fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str()); + return 1; + } + if (!db.CommitTransaction(strError)) + { + fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + if (!db.Checkpoint(strError)) + { + fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nCount = 0; + if (!db.CountRecords(nCount, strError)) + { + fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n", + strError.c_str()); + return 1; + } + if (nCount != nCopied) + { + fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n", + nCopied, nCount); + return 1; + } + + printf("SQLite wallet export written to %s\n", strDest.c_str()); + printf(" records copied: %d\n", nCopied); + fflush(stdout); + return 0; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index 6077669..b1a29d2 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -70,6 +70,11 @@ int CmdWalletStorageAudit(const std::string& strJsonOut = ""); // or migration-unsafe mix of storage records. int CmdWalletStorageCheck(); +// Diagnostic/migration staging: copy every raw wallet.dat record into an +// experimental SQLite key/value store, preserving serialized bytes. This does +// not change which backend the runtime wallet opens. +int CmdWalletSQLiteExport(const std::string& strDest); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From 99c6b5f2971da51cca1c739b5e982ae2022acc85 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 16:34:43 -0300 Subject: wallet: verify SQLite wallet exports --- docs/storage.md | 2 + src/main_gui.cpp | 11 ++++++ src/selftest.cpp | 41 +++++++++++++++++++++ src/walletcmd.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 4 ++ 5 files changed, 165 insertions(+) (limited to 'src/main_gui.cpp') diff --git a/docs/storage.md b/docs/storage.md index 2d748fa..0a441bc 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -111,6 +111,8 @@ overwrite an existing export and still does not change the runtime wallet backend. `CWalletDBSQLite::ScanRecords()` can stream those exported records back through the same visitor shape, so tests can compare BDB and SQLite record streams without introducing a SQLite runtime wallet loader yet. +`-walletsqliteverify=FILE` compares the current `wallet.dat` stream with a +SQLite export and prints only record counts and mismatch counts. ## `blkindex.dat` diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 6e89270..caacf13 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -183,6 +183,8 @@ static void PrintUsage() printf(" wallet.dat storage state)\n"); printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); printf(" experimental SQLite store, then exit)\n"); + printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n"); + printf(" printing counts only, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -540,6 +542,15 @@ int main(int argc, char* argv[]) return nRet; } + string strWalletSQLiteVerify = + argval2(argc, argv, "/walletsqliteverify", "-walletsqliteverify"); + if (!strWalletSQLiteVerify.empty()) + { + int nRet = CmdWalletSQLiteVerify(strWalletSQLiteVerify); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/selftest.cpp b/src/selftest.cpp index 408dd41..4ba90c3 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1134,6 +1134,8 @@ static int RunWalletSQLiteSelfTest() return nFail == 0 ? 0 : 1; } +namespace { + class CWalletRecordMapVisitor : public CWalletRecordVisitor { public: @@ -1158,6 +1160,8 @@ public: } }; +} // namespace + static int RunWalletSQLiteMigrationSelfTest() { fflush(stdout); @@ -1215,6 +1219,17 @@ static int RunWalletSQLiteMigrationSelfTest() FileContainsText(strExportOut, "SQLite wallet export written"), "wallet.dat can be exported to SQLite") ? 0 : 1; + string strVerifyOut = tmp + "/sqlite-verify.txt"; + vector vVerifyArgs; + vVerifyArgs.push_back("-datadir=" + strWalletDir); + vVerifyArgs.push_back("-nomanagedtor"); + vVerifyArgs.push_back("-nogui"); + vVerifyArgs.push_back("-walletsqliteverify=" + strSQLite); + int nVerifyRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyOut); + nFail += Check(nVerifyRet == 0 && + FileContainsText(strVerifyOut, "verification: ok"), + "SQLite wallet export verifies against wallet.dat") ? 0 : 1; + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; int nOverwriteRet = RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); @@ -1265,6 +1280,32 @@ static int RunWalletSQLiteMigrationSelfTest() nFail += Check(db.ScanRecords(sqliteVisitor, strError) && mapSQLite == mapBDB, "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1; + + db.Close(); + CWalletDBSQLite dbCorrupt; + vector vchBadValue; + vchBadValue.push_back(0xba); + vchBadValue.push_back(0xad); + nFail += Check(!mapBDB.empty() && + dbCorrupt.Open(strSQLite, strError) && + dbCorrupt.BeginTransaction(strError) && + dbCorrupt.WriteRecord(mapBDB.begin()->first, + vchBadValue, + strError) && + dbCorrupt.CommitTransaction(strError) && + dbCorrupt.Checkpoint(strError), + "SQLite export can be deliberately corrupted for verifier test") ? 0 : 1; + dbCorrupt.Close(); + + string strVerifyBadOut = tmp + "/sqlite-verify-bad.txt"; + int nVerifyBadRet = + RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyBadOut); + nFail += Check(nVerifyBadRet == 2 && + FileContainsText(strVerifyBadOut, + "record value mismatches: 1") && + FileContainsText(strVerifyBadOut, + "verification: failed"), + "SQLite wallet verifier detects a mismatched record value") ? 0 : 1; } catch (const std::exception& e) { diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 20e7e33..36f6783 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -796,6 +796,8 @@ static vector DataStreamBytesLocal(const CDataStream& ss) return vector(ss.begin(), ss.end()); } +namespace { + class CWalletSQLiteExportVisitor : public CWalletRecordVisitor { public: @@ -818,6 +820,32 @@ public: } }; +class CWalletRecordMapVisitor : public CWalletRecordVisitor +{ +public: + std::map, vector >& mapRecords; + + explicit CWalletRecordMapVisitor( + std::map, vector >& mapRecordsIn) + : mapRecords(mapRecordsIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + vector vchKey = DataStreamBytesLocal(ssKey); + if (mapRecords.count(vchKey)) + { + strErrorRet = "duplicate serialized wallet key"; + return false; + } + mapRecords[vchKey] = DataStreamBytesLocal(ssValue); + return true; + } +}; + +} // namespace + int CmdWalletSQLiteExport(const std::string& strDest) { AttachTerminal(); @@ -887,6 +915,85 @@ int CmdWalletSQLiteExport(const std::string& strDest) return 0; } +int CmdWalletSQLiteVerify(const std::string& strPath) +{ + AttachTerminal(); + + if (strPath.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); + return 1; + } + if (!FileExists(strPath.c_str())) + { + fprintf(stderr, "SQLite wallet export does not exist: %s\n", + strPath.c_str()); + return 1; + } + + std::map, vector > mapBDB; + std::map, vector > mapSQLite; + string strError; + + CWalletRecordMapVisitor bdbVisitor(mapBDB); + if (!ScanWalletRecords(bdbVisitor, strError)) + { + fprintf(stderr, "Cannot scan wallet.dat records: %s\n", strError.c_str()); + return 1; + } + + CWalletDBSQLite db; + if (!db.Open(strPath, strError)) + { + fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + CWalletRecordMapVisitor sqliteVisitor(mapSQLite); + if (!db.ScanRecords(sqliteVisitor, strError)) + { + fprintf(stderr, "Cannot scan SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nMissing = 0; + int nMismatched = 0; + for (std::map, vector >::const_iterator it = + mapBDB.begin(); it != mapBDB.end(); ++it) + { + std::map, vector >::const_iterator sit = + mapSQLite.find(it->first); + if (sit == mapSQLite.end()) + nMissing++; + else if (sit->second != it->second) + nMismatched++; + } + + int nExtra = 0; + for (std::map, vector >::const_iterator it = + mapSQLite.begin(); it != mapSQLite.end(); ++it) + { + if (!mapBDB.count(it->first)) + nExtra++; + } + + printf("SQLite wallet export verification\n"); + printf(" wallet.dat records: %u\n", (unsigned int)mapBDB.size()); + printf(" SQLite records: %u\n", (unsigned int)mapSQLite.size()); + printf(" missing records: %d\n", nMissing); + printf(" extra records: %d\n", nExtra); + printf(" record value mismatches: %d\n", nMismatched); + if (nMissing == 0 && nExtra == 0 && nMismatched == 0) + { + printf(" verification: ok\n"); + fflush(stdout); + return 0; + } + + printf(" verification: failed\n"); + fflush(stdout); + return 2; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index b1a29d2..784c74c 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -75,6 +75,10 @@ int CmdWalletStorageCheck(); // not change which backend the runtime wallet opens. int CmdWalletSQLiteExport(const std::string& strDest); +// Diagnostic/migration staging: compare a SQLite wallet export with the current +// wallet.dat raw records. Prints counts only, never record keys or values. +int CmdWalletSQLiteVerify(const std::string& strPath); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From df129cc672b9b894ff2558d3e9361ddab082433d Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 17:02:35 -0300 Subject: wallet: restore BDB wallets from SQLite exports --- docs/storage.md | 3 ++ src/main_gui.cpp | 11 +++++ src/selftest.cpp | 35 +++++++++++++++ src/walletcmd.cpp | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 4 ++ 5 files changed, 179 insertions(+) (limited to 'src/main_gui.cpp') diff --git a/docs/storage.md b/docs/storage.md index 0a441bc..4947575 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -113,6 +113,9 @@ through the same visitor shape, so tests can compare BDB and SQLite record streams without introducing a SQLite runtime wallet loader yet. `-walletsqliteverify=FILE` compares the current `wallet.dat` stream with a SQLite export and prints only record counts and mismatch counts. +`-walletsqliterestore=FILE` rebuilds `wallet.dat` from a SQLite export only in +an empty data directory, then the verifier can prove the restored BDB file still +matches the export byte-for-byte. ## `blkindex.dat` diff --git a/src/main_gui.cpp b/src/main_gui.cpp index caacf13..cf96822 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -185,6 +185,8 @@ static void PrintUsage() printf(" experimental SQLite store, then exit)\n"); printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n"); printf(" printing counts only, then exit)\n"); + printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n"); + printf(" an empty data directory, then exit)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -551,6 +553,15 @@ int main(int argc, char* argv[]) return nRet; } + string strWalletSQLiteRestore = + argval2(argc, argv, "/walletsqliterestore", "-walletsqliterestore"); + if (!strWalletSQLiteRestore.empty()) + { + int nRet = CmdWalletSQLiteRestore(strWalletSQLiteRestore); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/selftest.cpp b/src/selftest.cpp index 4ba90c3..7d3a96e 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1230,6 +1230,41 @@ static int RunWalletSQLiteMigrationSelfTest() FileContainsText(strVerifyOut, "verification: ok"), "SQLite wallet export verifies against wallet.dat") ? 0 : 1; + string strRestoredDir = tmp + "/restored-wallet"; + nFail += Check(MakeDirLocal(strRestoredDir), + "restored wallet directory can be created") ? 0 : 1; + string strRestoreOut = tmp + "/sqlite-restore.txt"; + vector vRestoreArgs; + vRestoreArgs.push_back("-datadir=" + strRestoredDir); + vRestoreArgs.push_back("-nomanagedtor"); + vRestoreArgs.push_back("-nogui"); + vRestoreArgs.push_back("-walletsqliterestore=" + strSQLite); + int nRestoreRet = RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOut); + nFail += Check(nRestoreRet == 0 && + FileContainsText(strRestoreOut, "records restored:"), + "SQLite wallet export restores into a fresh wallet.dat") ? 0 : 1; + + string strVerifyRestoredOut = tmp + "/sqlite-verify-restored.txt"; + vector vVerifyRestoredArgs; + vVerifyRestoredArgs.push_back("-datadir=" + strRestoredDir); + vVerifyRestoredArgs.push_back("-nomanagedtor"); + vVerifyRestoredArgs.push_back("-nogui"); + vVerifyRestoredArgs.push_back("-walletsqliteverify=" + strSQLite); + int nVerifyRestoredRet = + RunBitflashChild(strExe, vVerifyRestoredArgs, NULL, &strVerifyRestoredOut); + nFail += Check(nVerifyRestoredRet == 0 && + FileContainsText(strVerifyRestoredOut, + "verification: ok"), + "restored wallet.dat verifies against the SQLite export") ? 0 : 1; + + string strRestoreOverwriteOut = tmp + "/sqlite-restore-overwrite.txt"; + int nRestoreOverwriteRet = + RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOverwriteOut); + nFail += Check(nRestoreOverwriteRet == 1 && + FileContainsText(strRestoreOverwriteOut, + "Refusing to overwrite existing wallet.dat"), + "SQLite wallet restore refuses to overwrite wallet.dat") ? 0 : 1; + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; int nOverwriteRet = RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 36f6783..d1bad98 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -844,6 +844,52 @@ public: } }; +class CWalletRawRestoreDB : public CWalletDB +{ +public: + CWalletRawRestoreDB(const char* pszMode="cr+", bool fTxn=true) + : CWalletDB(pszMode, fTxn) { } + + bool WriteRaw(CDataStream ssKey, CDataStream ssValue) + { + if (!pdb || ssKey.empty()) + return false; + + Dbt datKey((void*)&ssKey[0], ssKey.size()); + Dbt datValue(ssValue.empty() ? NULL : (void*)&ssValue[0], + ssValue.size()); + int ret = pdb->put(GetTxn(), &datKey, &datValue, 0); + + memset(datKey.get_data(), 0, datKey.get_size()); + if (datValue.get_data()) + memset(datValue.get_data(), 0, datValue.get_size()); + return ret == 0; + } +}; + +class CWalletSQLiteRestoreVisitor : public CWalletRecordVisitor +{ +public: + CWalletRawRestoreDB& db; + int& nCopied; + + CWalletSQLiteRestoreVisitor(CWalletRawRestoreDB& dbIn, int& nCopiedIn) + : db(dbIn), nCopied(nCopiedIn) { } + + bool VisitWalletRecord(const CDataStream& ssKey, + const CDataStream& ssValue, + string& strErrorRet) + { + if (!db.WriteRaw(ssKey, ssValue)) + { + strErrorRet = "could not write raw wallet record"; + return false; + } + nCopied++; + return true; + } +}; + } // namespace int CmdWalletSQLiteExport(const std::string& strDest) @@ -994,6 +1040,86 @@ int CmdWalletSQLiteVerify(const std::string& strPath) return 2; } +int CmdWalletSQLiteRestore(const std::string& strPath) +{ + AttachTerminal(); + + if (strPath.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); + return 1; + } + if (!FileExists(strPath.c_str())) + { + fprintf(stderr, "SQLite wallet export does not exist: %s\n", + strPath.c_str()); + return 1; + } + + string strWalletPath = GetAppDir() + "/wallet.dat"; + if (FileExists(strWalletPath.c_str())) + { + fprintf(stderr, "Refusing to overwrite existing wallet.dat: %s\n", + strWalletPath.c_str()); + return 1; + } + + string strError; + CWalletDBSQLite sqlite; + if (!sqlite.Open(strPath, strError)) + { + fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + int nSQLiteRecords = 0; + if (!sqlite.CountRecords(nSQLiteRecords, strError)) + { + fprintf(stderr, "Cannot count SQLite wallet export records: %s\n", + strError.c_str()); + return 1; + } + if (nSQLiteRecords <= 0) + { + fprintf(stderr, "SQLite wallet export contains no records.\n"); + return 2; + } + + CWalletRawRestoreDB bdb("cr+", true); + if (!bdb.TxnBegin()) + { + fprintf(stderr, "Cannot begin Berkeley DB restore transaction.\n"); + return 1; + } + + int nCopied = 0; + CWalletSQLiteRestoreVisitor visitor(bdb, nCopied); + if (!sqlite.ScanRecords(visitor, strError)) + { + bdb.TxnAbort(); + fprintf(stderr, "Cannot restore SQLite wallet export: %s\n", + strError.c_str()); + return 1; + } + if (nCopied != nSQLiteRecords) + { + bdb.TxnAbort(); + fprintf(stderr, "SQLite wallet restore copied %d records, expected %d.\n", + nCopied, nSQLiteRecords); + return 2; + } + if (!bdb.TxnCommit()) + { + fprintf(stderr, "Cannot commit Berkeley DB restore transaction.\n"); + return 1; + } + + printf("SQLite wallet export restored to wallet.dat\n"); + printf(" records restored: %d\n", nCopied); + fflush(stdout); + return 0; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index 784c74c..50dc69e 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -79,6 +79,10 @@ int CmdWalletSQLiteExport(const std::string& strDest); // wallet.dat raw records. Prints counts only, never record keys or values. int CmdWalletSQLiteVerify(const std::string& strPath); +// Diagnostic/migration staging: rebuild wallet.dat from a SQLite export in an +// empty data directory. Refuses to overwrite an existing wallet.dat. +int CmdWalletSQLiteRestore(const std::string& strPath); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From 3e778dd6cc9045862e99503b5b9af80ec682826f Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 17:44:09 -0300 Subject: wallet: check SQLite exports against loader types --- docs/storage.md | 3 + src/main_gui.cpp | 11 +++ src/selftest.cpp | 54 ++++++++++++++ src/walletcmd.cpp | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/walletcmd.h | 4 ++ 5 files changed, 281 insertions(+) (limited to 'src/main_gui.cpp') diff --git a/docs/storage.md b/docs/storage.md index aca3f6e..07c7170 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -117,6 +117,9 @@ SQLite export and prints only record counts and mismatch counts. an empty data directory, then the verifier can prove the restored BDB file still matches the export byte-for-byte. Failed exports remove the incomplete SQLite 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. ## `blkindex.dat` diff --git a/src/main_gui.cpp b/src/main_gui.cpp index cf96822..c9aac7d 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -187,6 +187,8 @@ static void PrintUsage() printf(" printing counts only, then exit)\n"); printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n"); printf(" an empty data directory, then exit)\n"); + printf(" /walletsqliteloadcheck=FILE\n"); + printf(" (parse a SQLite export like the wallet loader)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -562,6 +564,15 @@ int main(int argc, char* argv[]) return nRet; } + string strWalletSQLiteLoadCheck = + argval2(argc, argv, "/walletsqliteloadcheck", "-walletsqliteloadcheck"); + if (!strWalletSQLiteLoadCheck.empty()) + { + int nRet = CmdWalletSQLiteLoadCheck(strWalletSQLiteLoadCheck); + DBFlush(true); + return nRet; + } + // Berkeley DB reports failure by throwing, and CDB's constructor lets it // through. Nothing on this path caught anything, so an unreadable // blkindex.dat or wallet.dat -- one written by another platform's Berkeley diff --git a/src/selftest.cpp b/src/selftest.cpp index 343d9c5..feb4076 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1195,6 +1195,28 @@ static bool CreateSQLiteWalletWithEmptyKey(const string& strPath) return fOk; } +static bool CreateSQLiteWalletWithFutureMinVersion(const string& strPath) +{ + string strError; + CWalletDBSQLite db; + CDataStream ssKey(SER_DISK); + ssKey << string("walletminversion"); + CDataStream ssValue(SER_DISK); + ssValue << (WALLET_FORMAT_SUPPORTED + 1); + + if (!db.Open(strPath, strError)) + return false; + if (!db.BeginTransaction(strError)) + return false; + if (!db.WriteRecord(DataStreamBytes(ssKey), DataStreamBytes(ssValue), strError)) + return false; + if (!db.CommitTransaction(strError)) + return false; + if (!db.Checkpoint(strError)) + return false; + return true; +} + static int RunWalletSQLiteMigrationSelfTest() { fflush(stdout); @@ -1263,6 +1285,19 @@ static int RunWalletSQLiteMigrationSelfTest() FileContainsText(strVerifyOut, "verification: ok"), "SQLite wallet export verifies against wallet.dat") ? 0 : 1; + string strLoadCheckOut = tmp + "/sqlite-loadcheck.txt"; + vector vLoadCheckArgs; + vLoadCheckArgs.push_back("-datadir=" + strWalletDir); + vLoadCheckArgs.push_back("-nomanagedtor"); + vLoadCheckArgs.push_back("-nogui"); + vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strSQLite); + int nLoadCheckRet = + RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strLoadCheckOut); + nFail += Check(nLoadCheckRet == 0 && + FileContainsText(strLoadCheckOut, + "load check: ok"), + "SQLite wallet export passes the loader compatibility check") ? 0 : 1; + string strRestoredDir = tmp + "/restored-wallet"; nFail += Check(MakeDirLocal(strRestoredDir), "restored wallet directory can be created") ? 0 : 1; @@ -1318,6 +1353,25 @@ static int RunWalletSQLiteMigrationSelfTest() !FileExists((strBadRestoreDir + "/wallet.dat").c_str()), "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1; + string strFutureSQLite = tmp + "/future-wallet.sqlite"; + nFail += Check(CreateSQLiteWalletWithFutureMinVersion(strFutureSQLite), + "SQLite export with a future wallet format can be created") ? 0 : 1; + string strFutureLoadCheckOut = tmp + "/sqlite-loadcheck-future.txt"; + vector vFutureLoadCheckArgs; + vFutureLoadCheckArgs.push_back("-datadir=" + strWalletDir); + vFutureLoadCheckArgs.push_back("-nomanagedtor"); + vFutureLoadCheckArgs.push_back("-nogui"); + vFutureLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strFutureSQLite); + int nFutureLoadCheckRet = + RunBitflashChild(strExe, vFutureLoadCheckArgs, NULL, + &strFutureLoadCheckOut); + nFail += Check(nFutureLoadCheckRet == 2 && + FileContainsText(strFutureLoadCheckOut, + "unsupported future wallet format") && + FileContainsText(strFutureLoadCheckOut, + "load check: failed"), + "SQLite wallet load check rejects future wallet formats") ? 0 : 1; + string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; int nOverwriteRet = RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 9e4a981..4dda8dd 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -903,6 +903,171 @@ public: } }; +class CWalletSQLiteLoadCheckVisitor : public CWalletRecordVisitor +{ +public: + int nRecords; + int nUnknown; + string strFailure; + + CWalletSQLiteLoadCheckVisitor() : nRecords(0), nUnknown(0) { } + + bool Fail(const string& strMessage, string& strErrorRet) + { + strFailure = strMessage; + strErrorRet = strMessage; + return false; + } + + bool VisitWalletRecord(const CDataStream& ssKeyIn, + const CDataStream& ssValueIn, + string& strErrorRet) + { + nRecords++; + try + { + CDataStream ssKey = ssKeyIn; + CDataStream ssValue = ssValueIn; + string strType; + ssKey >> strType; + if (ssKey.fail()) + return Fail("malformed wallet record key", strErrorRet); + + if (strType == "name") + { + string strAddress; + string strName; + ssKey >> strAddress; + ssValue >> strName; + } + else if (strType == "tx") + { + uint256 hash; + CWalletTx wtx; + ssKey >> hash; + ssValue >> wtx; + } + else if (strType == "key") + { + vector vchPubKey; + CPrivKey vchPrivKey; + ssKey >> vchPubKey; + ssValue >> vchPrivKey; + } + else if (strType == "mkey") + { + unsigned int nID = 0; + CWalletMasterKey kMasterKey; + ssKey >> nID; + ssValue >> kMasterKey; + } + else if (strType == "ckey") + { + vector vchPubKey; + vector vchCryptedSecret; + ssKey >> vchPubKey; + ssValue >> vchCryptedSecret; + } + else if (strType == "defaultkey" || + strType == "hdmaster" || + strType == "hdchaincode" || + strType == "cryptedhdmaster" || + strType == "cryptedhdchaincode") + { + vector vch; + ssValue >> vch; + } + else if (strType == "hdnext" || + strType == "hdreceivenext" || + strType == "hdchangenext" || + strType == "hdcointype") + { + unsigned int nValue = 0; + ssValue >> nValue; + } + else if (strType == "hdschema") + { + int nSchema = 0; + ssValue >> nSchema; + if (!ssValue.fail() && + nSchema != HD_SCHEMA_NONE && + nSchema != HD_SCHEMA_LEGACY && + nSchema != HD_SCHEMA_BIP44) + return Fail("unsupported deterministic wallet schema", + strErrorRet); + } + else if (strType == "walletminversion") + { + int nMinVersion = 0; + ssValue >> nMinVersion; + if (!ssValue.fail() && nMinVersion > WALLET_FORMAT_SUPPORTED) + return Fail("unsupported future wallet format", strErrorRet); + } + else if (strType == "pool") + { + int64 nIndex = 0; + vector vchPubKey; + ssKey >> nIndex; + ssValue >> vchPubKey; + } + else if (strType == "setting") + { + string strKey; + ssKey >> strKey; + if (strKey == "nTransactionFee") + { + int64 nValue = 0; + ssValue >> nValue; + } + else if (strKey == "addrIncoming") + { + CAddress addr; + ssValue >> addr; + } + else if (strKey == "nMineMode") + { + int nValue = 0; + ssValue >> nValue; + } + else if (strKey == "fGenerateBitcoins") + { + int fValue = 0; + ssValue >> fValue; + } + else if (strKey == "strParticipantPool") + { + string strValue; + ssValue >> strValue; + } + } + else if (strType == "version") + { + int nVersion = 0; + ssValue >> nVersion; + } + else + { + nUnknown++; + } + + if (ssKey.fail()) + return Fail("malformed wallet record key", strErrorRet); + if (ssValue.fail()) + return Fail("malformed wallet record value", strErrorRet); + } + catch (const std::exception& e) + { + return Fail(strprintf("wallet record parse exception: %s", e.what()), + strErrorRet); + } + catch (...) + { + return Fail("wallet record parse exception", strErrorRet); + } + return true; + } +}; + } // namespace int CmdWalletSQLiteExport(const std::string& strDest) @@ -1152,6 +1317,50 @@ int CmdWalletSQLiteRestore(const std::string& strPath) return 0; } +int CmdWalletSQLiteLoadCheck(const std::string& strPath) +{ + AttachTerminal(); + + if (strPath.empty()) + { + fprintf(stderr, "Missing SQLite wallet export path.\n"); + return 1; + } + if (!FileExists(strPath.c_str())) + { + fprintf(stderr, "SQLite wallet export does not exist: %s\n", + strPath.c_str()); + return 1; + } + + string strError; + CWalletDBSQLite sqlite; + if (!sqlite.Open(strPath, strError)) + { + fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); + return 1; + } + + CWalletSQLiteLoadCheckVisitor visitor; + if (!sqlite.ScanRecords(visitor, strError)) + { + printf("SQLite wallet load check\n"); + printf(" records checked: %d\n", visitor.nRecords); + printf(" unknown records: %d\n", visitor.nUnknown); + printf(" load check: failed\n"); + printf(" failure: %s\n", strError.c_str()); + fflush(stdout); + return 2; + } + + printf("SQLite wallet load check\n"); + printf(" records checked: %d\n", visitor.nRecords); + printf(" unknown records: %d\n", visitor.nUnknown); + printf(" load check: ok\n"); + fflush(stdout); + return 0; +} + int CmdEncryptWallet(const std::string& strPassphrase) { AttachTerminal(); diff --git a/src/walletcmd.h b/src/walletcmd.h index 50dc69e..d993856 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -83,6 +83,10 @@ int CmdWalletSQLiteVerify(const std::string& strPath); // empty data directory. Refuses to overwrite an existing wallet.dat. int CmdWalletSQLiteRestore(const std::string& strPath); +// Diagnostic/migration staging: parse a SQLite wallet export as the runtime +// wallet loader would. Prints counts only, never record keys or values. +int CmdWalletSQLiteLoadCheck(const std::string& strPath); + // Rewrite wallet.dat so private keys and the HD seed are encrypted with the // given passphrase. Returns 0 on success and exits without starting the node. int CmdEncryptWallet(const std::string& strPassphrase); -- cgit v1.2.3 From 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/main_gui.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 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/main_gui.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/main_gui.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 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/main_gui.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/main_gui.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 From d4c49085a10c0f7c3f5a1dcc72184938dca38fea Mon Sep 17 00:00:00 2001 From: bitflash Date: Wed, 19 Aug 2026 09:48:07 -0300 Subject: gui: harden the restart relaunch (ultra edge cases) Follow-up to the Restart Now button. Addresses the edge cases the review flagged, and drops the fragile batch layer entirely: - Windows no longer writes a .bat and shells through cmd (which re-parsed the arguments and could leave a stray file on failure). It builds the command line once, quoted correctly (only backslashes before a quote are doubled, so ordinary paths are not mangled), and CreateProcess launches the new instance directly. - The delay moved into the new instance: a -restartwait flag makes it wait ~2.5s before it opens the network port and the block index, so the old process is gone first. No shell delay hack. Verified: -restartwait adds the wait (0.16s -> 2.67s). - Linux re-execs /proc/self/exe (absolute) and only falls back to argv[0], which may be relative or a bare PATH name that execv cannot resolve. - This instance now exits only if the relaunch actually started; on failure it keeps running and tells the user to restart manually, so a failed relaunch never leaves them with no node. GUI/process wiring only; wallet code unchanged, suites still green. --- src/gui.cpp | 91 +++++++++++++++++++++++++++++++++++++------------------- src/main_gui.cpp | 8 +++++ 2 files changed, 69 insertions(+), 30 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 672f0ff..296d20c 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1410,63 +1410,94 @@ static bool g_wizardConverted = false; static int g_restartArgc = 0; static char** g_restartArgv = NULL; +#ifdef _WIN32 +// Quote one argument so CommandLineToArgvW parses it back to exactly this +// string: only backslashes that immediately precede a double quote (including +// the closing one) are doubled, so ordinary Windows paths are not mangled. +static std::string QuoteArgWin(const std::string& a) +{ + if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos) + return a; + std::string r = "\""; + size_t bs = 0; + for (size_t i = 0; i < a.size(); i++) + { + if (a[i] == '\\') { bs++; continue; } + if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; } + r.append(bs, '\\'); bs = 0; r += a[i]; + } + r.append(bs * 2, '\\'); + r += "\""; + return r; +} +#endif + // Relaunch Bitflash with the same arguments and datadir, then ask this instance -// to exit. A short delay lets the old process release the network port and the -// block-index files before the new one opens them, so mining resumes cleanly on -// the SQLite wallet instead of both instances fighting over the same files. +// to exit -- but only once the relaunch has actually started, so a failure never +// leaves the user with no node running. The new instance waits (-restartwait) +// before it opens the network port and the block index, so this one is gone +// first and they never fight over the same files. static void RestartApplication() { + bool fLaunched = false; #ifdef _WIN32 char exePath[MAX_PATH]; if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0) return; - std::string strArgs; + std::string strCmd = QuoteArgWin(exePath); bool fHasDataDir = false; for (int i = 1; i < g_restartArgc; i++) { std::string a = g_restartArgv[i] ? g_restartArgv[i] : ""; if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0) fHasDataDir = true; - strArgs += " \"" + a + "\""; + strCmd += " " + QuoteArgWin(a); } if (!fHasDataDir) - strArgs += " \"-datadir=" + GetAppDir() + "\""; - - std::string strBat = GetAppDir() + "\\bitflash-restart.bat"; - FILE* pf = fopen(strBat.c_str(), "wb"); - if (pf) + strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir()); + strCmd += " -restartwait"; + + std::vector cmdBuf(strCmd.begin(), strCmd.end()); + cmdBuf.push_back('\0'); + STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); + PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); + if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { - // ping is the reliable no-console delay on Windows; timeout needs a tty. - fprintf(pf, - "@echo off\r\n" - "ping 127.0.0.1 -n 3 >nul\r\n" - "start \"\" \"%s\"%s\r\n" - "del \"%%~f0\"\r\n", - exePath, strArgs.c_str()); - fclose(pf); - std::string strCmd = "cmd /c \"" + strBat + "\""; - STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); - PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); - if (CreateProcessA(NULL, (char*)strCmd.c_str(), NULL, NULL, FALSE, - CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) - { - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + fLaunched = true; } #else pid_t pid = fork(); if (pid == 0) { + // Child: wait for the parent to release the port and files, then re-exec. + // /proc/self/exe is the reliable absolute path; argv[0] may be relative + // or a bare name that execv() cannot resolve. sleep(2); + char exePath[4096]; + ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (len > 0) + { + exePath[len] = '\0'; + execv(exePath, g_restartArgv); + } if (g_restartArgv) execv(g_restartArgv[0], g_restartArgv); _exit(127); } + fLaunched = (pid > 0); #endif - fShutdown = true; - if (glfwGetCurrentContext()) - glfwSetWindowShouldClose(glfwGetCurrentContext(), true); + + if (fLaunched) + { + fShutdown = true; + if (glfwGetCurrentContext()) + glfwSetWindowShouldClose(glfwGetCurrentContext(), true); + } + else + g_wizardStatus = "Could not relaunch automatically -- please restart " + "Bitflash yourself to switch to the SQLite wallet."; } static void DrawBackendWizardDialog() diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 6cd8b75..55ca8c6 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -509,6 +509,14 @@ int main(int argc, char* argv[]) ParseStartupArguments(argc, argv); +#ifdef _WIN32 + // Set by the GUI "Restart Now" button: give the previous instance a moment + // to release the network port and the block-index files before this one + // opens them, so a relaunch does not race the process it is replacing. + if (arg(argc, argv, "/restartwait") || arg(argc, argv, "-restartwait")) + Sleep(2500); +#endif + // Hidden self-test helper. It must run before LoadWallet(): several // storage-sanity scenarios deliberately make wallet.dat unsafe to load. string strSelfTestMutateWallet = -- cgit v1.2.3 From 4c24a86a8aa01b02874e5bdb7856beed6e4cf6b6 Mon Sep 17 00:00:00 2001 From: bitflash Date: Wed, 19 Aug 2026 11:20:55 -0300 Subject: wallet/docs: SQLite is the default backend, not experimental Drop the "experimental" label from the wallet code and docs now that SQLite is the default backend for new wallets, is coin-proven, and carries encrypt, backup, atomic seed writes, and the storage diagnostics. - main_gui.cpp: -help text, the unknown-backend error (SQLite is no longer "experimental" and Berkeley DB is no longer "the default"), and the startup banner ("Wallet backend: SQLite"). - walletcmd.h: the export command comment. - docs/storage.md: rewrite the SQLite section from staging-only ("runtime still opens Berkeley DB", "no runtime backend flag exists yet") to the shipped state -- default backend, conversion wizard, per-backend ops -- and reframe the migration order as shipped in v1.2.20. --- docs/storage.md | 67 +++++++++++++++++++++++++++----------------------------- src/main_gui.cpp | 16 +++++++------- src/walletcmd.h | 6 ++--- 3 files changed, 43 insertions(+), 46 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/docs/storage.md b/docs/storage.md index 08c4a01..b52a955 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -85,10 +85,11 @@ A future wallet backend must keep these properties: - recovery phrase encrypted and locked, requiring unlock before coverage can be audited. -## Experimental SQLite Wallet Store +## SQLite Wallet Store -The SQLite wallet path starts as a byte-preserving record store, not as a new -wallet format: +Since v1.2.20 SQLite is the default wallet backend. It is a byte-preserving +record store, not a new wallet format -- the same serialized `CDataStream` bytes +`wallet.dat` uses: ```sql CREATE TABLE wallet_records ( @@ -98,32 +99,26 @@ CREATE TABLE wallet_records ( PRAGMA user_version=1; ``` -Keys and values are the same serialized `CDataStream` bytes used by `wallet.dat`. -The first implementation is exercised only by `-selftest=wallet-sqlite`; the -runtime wallet still opens Berkeley DB. The purpose is to prove schema creation, -raw record write/read, close/reopen behavior, and value preservation before any -BDB-to-SQLite migration or runtime backend flag exists. - -`-walletsqliteexport=FILE` is the next staging tool. It streams every raw -`wallet.dat` record through `ScanWalletRecords()` and writes those same key and -value bytes into the SQLite table inside one transaction. It refuses to -overwrite an existing export and still does not change the runtime wallet -backend. `CWalletDBSQLite::ScanRecords()` can stream those exported records back -through the same visitor shape, so tests can compare BDB and SQLite record -streams without introducing a SQLite runtime wallet loader yet. -`-walletsqliteverify=FILE` compares the current `wallet.dat` stream with a -SQLite export and prints only record counts and mismatch counts. -`-walletsqliterestore=FILE` rebuilds `wallet.dat` from a SQLite export only in -an empty data directory, then the verifier can prove the restored BDB file still -matches the export byte-for-byte. Failed exports remove the incomplete SQLite -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. +A fresh data directory starts on `wallet.sqlite`. An existing `wallet.dat` keeps +running on Berkeley DB until it is converted: the GUI offers a one-click +conversion on first run, and `-walletbackend=sqlite` / `-walletbackend=bdb` +(recorded in a plain-text `wallet-backend` marker file in the datadir) select a +backend explicitly. Conversion never modifies `wallet.dat`; it stays as a +fallback, and the change takes effect on restart. + +Every wallet operation runs on either backend: read, write, erase, key-pool and +HD-seed writes (the two halves of the seed bracketed in one SQLite transaction), +`-backupwallet` (checkpoint then copy the self-contained file), `-encryptwallet` +(an atomic rebuild-and-swap in one transaction), and the `-walletstorageaudit` / +`-walletstoragecheck` diagnostics, which read the active backend's file. + +The migration tooling remains available for scripted use. `-walletsqliteexport=FILE` +streams every raw `wallet.dat` record through `ScanWalletRecords()` into a SQLite +file in one transaction, refusing to overwrite. `-walletsqliteverify=FILE` +compares the two streams and prints only counts. `-walletsqliteloadcheck=FILE` +parses an export with the same record types the loader uses. +`-walletsqliterestore=FILE` rebuilds a `wallet.dat` from an export in an empty +data directory. Failed exports and restores remove their incomplete output. ## `blkindex.dat` @@ -150,12 +145,14 @@ conversion path. The old IP address database (`addr.dat`) has been removed. Peer discovery is handled by Nostr, `.btf`, rendezvous relays, and direct onion peers. -## Recommended Migration Order +## Migration Order (shipped in v1.2.20) -1. Keep adding storage self-tests around current BDB behavior. -2. Add an experimental wallet backend behind an explicit flag. -3. Implement BDB-to-new-wallet migration as a one-way copy with a backup. -4. Make the new wallet backend default only after cross-version restore, - encryption, backup, and recovery-audit tests pass. +1. Storage self-tests were added around Berkeley DB behavior first. +2. The SQLite backend landed behind an explicit flag, then behind a + `wallet-backend` marker and a GUI conversion wizard. +3. BDB-to-SQLite migration is a one-way copy that leaves `wallet.dat` as a backup. +4. SQLite became the default for new wallets only after cross-version restore, + encryption, backup, and recovery-audit tests passed -- and a live coin-proof + of receive, spend, restart, and encrypt on real mainnet funds. 5. Treat `blkindex.dat` separately; it is safer to rebuild than to migrate under pressure. diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 55ca8c6..943799f 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -182,7 +182,7 @@ static void PrintUsage() printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); printf(" wallet.dat storage state)\n"); printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); - printf(" experimental SQLite store, then exit)\n"); + printf(" SQLite store, then exit)\n"); printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n"); printf(" printing counts only, then exit)\n"); printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n"); @@ -680,17 +680,17 @@ int main(int argc, char* argv[]) 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."); + "Use -walletbackend=sqlite for the SQLite backend, or " + "-walletbackend=bdb for the Berkeley DB wallet.dat."); return 1; } 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 + // Explicit opt-in via the flag. 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"; @@ -724,7 +724,7 @@ int main(int argc, char* argv[]) } fprintf(stderr, - "Wallet backend: SQLite (experimental)\n" + "Wallet backend: SQLite\n" " %s\n" "Your Berkeley DB wallet.dat is left untouched. Restart without\n" "-walletbackend=sqlite to go back to it.\n", diff --git a/src/walletcmd.h b/src/walletcmd.h index 38cbfdb..7013d44 100644 --- a/src/walletcmd.h +++ b/src/walletcmd.h @@ -70,9 +70,9 @@ int CmdWalletStorageAudit(const std::string& strJsonOut = ""); // or migration-unsafe mix of storage records. int CmdWalletStorageCheck(); -// Diagnostic/migration staging: copy every raw wallet.dat record into an -// experimental SQLite key/value store, preserving serialized bytes. This does -// not change which backend the runtime wallet opens. +// Copy every raw wallet.dat record into a SQLite key/value store, preserving +// serialized bytes. A migration/scripting tool; it does not change which backend +// the runtime wallet opens. int CmdWalletSQLiteExport(const std::string& strDest); // Export the currently-loaded wallet to a SQLite file (shared by the command -- cgit v1.2.3 From 1017644bc8d4bf183086ffb816c5e3f18068568e Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Tue, 1 Sep 2026 17:16:04 +0000 Subject: wallet: refuse to mint a wallet a headless node never asked for v1.2.20 makes SQLite the default backend for a datadir that holds no wallet. On a desktop that is a first run. On a headless node it is usually a fault -- the datadir is not mounted, or wallet.dat was caught mid-rename by a backup -- and the new default turns that fault into a silent loss of custody rather than a loud failure. The damage is not the empty wallet, it is the marker. Creating the wallet also writes /wallet-backend = sqlite, and every later start reads that marker first: putting the real wallet.dat back no longer does anything, because the node stays on the empty SQLite wallet it made. 1.2.19 had the same fail-open on a missing wallet, but it created a wallet.dat, and restoring the backup over it recovered the funds. That runbook stops working here. So refuse when the startup is headless and no wallet of either kind is present, and say which flag makes it deliberate: /createwallet to start a new one, or /walletbackend=bdb for the Berkeley DB path. A node that meant to create a wallet says so; a node that lost its datadir gets an error instead of a fresh identity and a re-paid ledger. Upstream keeps the old behaviour; this is a fork divergence, and the three findings that would gate enabling the SQLite backend at all -- the non-atomic backend marker, WriteHDMaster's connection-wide transaction, and the encrypt-swap crash window -- remain open against upstream. --- src/main_gui.cpp | 29 +++++++++++++++++++++++++++++ src/selftest.cpp | 26 +++++++++++++++++++++----- 2 files changed, 50 insertions(+), 5 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 2743429..25cde1c 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -196,6 +196,9 @@ static void PrintUsage() 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(" /createwallet (headless only: permit creating a new wallet in a\n"); + printf(" datadir that has none -- otherwise refused, since\n"); + printf(" a missing wallet is usually a mount/backup fault)\n"); printf(" /rescan (walk the chain for coins this wallet owns\n"); printf(" but never recorded, then exit)\n"); printf("\n"); @@ -835,6 +838,32 @@ int main(int argc, char* argv[]) else { fUseSQLite = true; fCreateSQLite = true; } } + // A headless node whose datadir holds no wallet of either kind is + // far more often an operations fault -- datadir not mounted, or + // wallet.dat caught mid-rename by a backup -- than a first run. + // Creating one here is not the harmless default it looks like: + // success also writes the backend marker, and from then on + // restoring the real wallet.dat no longer takes effect, because the + // marker keeps the node on the empty wallet it just made. Under + // 1.2.19 the same fault created a wallet.dat that a restore + // overwrote, so operators could recover by putting the file back. + // Refuse instead, and name the flag that makes it deliberate. + if (fCreateSQLite && fHeadlessStartup && + !arg(argc, argv, "/createwallet") && !arg(argc, argv, "-createwallet")) + { + AttachTerminal(); + fprintf(stderr, + "No wallet found in %s\n" + "Refusing to create one. On a headless node an absent wallet is\n" + "usually a mount or backup fault, and creating one would record a\n" + "backend marker that makes restoring the real wallet a no-op.\n" + "Pass -createwallet to start a new one deliberately, or\n" + "-walletbackend=bdb to run on a Berkeley DB wallet.dat.\n", + GetAppDir().c_str()); + fflush(stderr); + return 1; + } + if (fUseSQLite) { AttachTerminal(); diff --git a/src/selftest.cpp b/src/selftest.cpp index 2ada8f3..c62d70f 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -2580,7 +2580,11 @@ static int RunWalletBackendDefaultSelfTest() try { - // 1. Fresh datadir, no flag -> new wallet.sqlite, marker "sqlite", no wallet.dat. + // 1. Fresh datadir, no flag -> this fork refuses, where upstream creates. + // An empty datadir on a headless node is far more often a mount or + // backup fault than a first run, and creating a wallet also pins the + // backend marker, after which restoring the real wallet is a no-op. + // Nothing may be left behind: no wallet of either kind, no marker. string d1 = tmp + "/fresh"; nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; vector a1; @@ -2588,19 +2592,31 @@ static int RunWalletBackendDefaultSelfTest() a1.push_back("-nomanagedtor"); a1.push_back("-nogui"); a1.push_back("-newaddress"); - int r1 = RunBitflashChild(strExe, a1); + int r0 = RunBitflashChild(strExe, a1); + nFail += Check(r0 != 0 && + !FileExists((d1 + "/wallet.sqlite").c_str()) && + !FileExists((d1 + "/wallet.dat").c_str()) && + !FileExists((d1 + "/wallet-backend").c_str()), + "a flagless headless start refuses to create a wallet") ? 0 : 1; + + // 1a. -createwallet says it was meant: new wallet.sqlite, marker + // "sqlite", no wallet.dat. + vector a1c = a1; + a1c.push_back("-createwallet"); + int r1 = RunBitflashChild(strExe, a1c); 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; + "-createwallet starts a fresh install on SQLite and records the marker") ? 0 : 1; - // 1b. Second run reuses it, still SQLite, still no wallet.dat. + // 1b. Second run reuses it without the flag -- the refusal only guards + // creation, so an existing wallet still starts flagless. 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; + "the recorded SQLite choice survives a flagless restart") ? 0 : 1; // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. string d2 = tmp + "/existing-bdb"; -- cgit v1.2.3 From ae1c6a7b49d9b4f8cf7218c0baec84f348eb5ef6 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 05:26:51 +0000 Subject: log: cap debug.log in-process, and ship a logrotate policy debug.log grew without bound (production: 1.95 GiB, 51-129 MB/day). Nothing shrank it once the process was up -- there is no ShrinkDebugFile in this tree, and upstream's only ran at startup. OutputDebugStringF now rolls the file to debug.log.old when it passes -maxdebugfilesize (MB, default 1024, 0 disables). The write path already opens and closes the file per line under one lock, so the rename-and-reopen is a few lines inside that lock and no fd can be stranded mid-roll. contrib/logrotate/bitflash-debug.logrotate is the longer-term policy (size 1G, rotate 4, compress, missingok, notifempty, copytruncate); copytruncate keeps the writer's append handle valid across rotation. Closes #34 --- src/main_gui.cpp | 15 +++++++++++++++ src/util.cpp | 5 +++++ src/util.h | 29 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 211e1e2..05f5ecf 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -111,6 +111,7 @@ static void PrintUsage() printf(" /help, -help, --help, /?\n"); printf(" /datadir=PATH\n"); printf(" /debug\n"); + printf(" /maxdebugfilesize=N (roll debug.log at N MB, default 1024; 0 = never)\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); @@ -191,6 +192,20 @@ static void ParseStartupArguments(int argc, char* argv[]) if (arg(argc,argv,"/debug") || arg(argc,argv,"-debug")) fDebug = true; + // /maxdebugfilesize=N -- roll debug.log to debug.log.old once it passes + // N MB, checked on each write. Default is 1024 (see util.cpp); 0 turns + // the check off and leaves rotation entirely to logrotate. + string strMaxDebugSize = argval2(argc, argv, "/maxdebugfilesize", "-maxdebugfilesize"); + if (!strMaxDebugSize.empty()) + { + int64 nMB = atoi64(strMaxDebugSize.c_str()); + if (nMB < 0) + fprintf(stderr, "Ignoring /maxdebugfilesize=%s: not a size in MB\n", + strMaxDebugSize.c_str()); + else + nMaxDebugFileSize = nMB * 1024 * 1024; + } + if (arg(argc,argv,"/gen") || arg(argc,argv,"-gen")) { fGenerateBitcoins = 1; diff --git a/src/util.cpp b/src/util.cpp index 0cd0133..52cda84 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -8,6 +8,11 @@ bool fDebug = false; +// In-process cap for debug.log, in bytes. Default matches the 1G trigger of +// the shipped logrotate policy so a long-running node stays bounded even +// without it; -maxdebugfilesize=N (MB) tunes it, 0 disables. +int64 nMaxDebugFileSize = 1024 * 1024 * (int64)1024; + // --------------------------------------------------------------------------- // Categorized, burst-collapsing logging -- see the LogPrint doc comment in // util.h for the problem this solves. diff --git a/src/util.h b/src/util.h index d72518b..f2ea23f 100644 --- a/src/util.h +++ b/src/util.h @@ -87,6 +87,11 @@ inline T& REF(const T& val) extern bool fDebug; +// debug.log is rolled to debug.log.old once it grows past this many bytes +// (set from -maxdebugfilesize, in MB; 0 disables the check). See the roll +// site in OutputDebugStringF() below. +extern int64 nMaxDebugFileSize; + // --------------------------------------------------------------------------- // Categorized, burst-collapsing logging // @@ -391,6 +396,30 @@ inline int OutputDebugStringF(const char* pszFormat, ...) strDebugFile = "debug.log"; } FILE* fileout = fopen(strDebugFile.c_str(), "a"); + if (fileout && nMaxDebugFileSize > 0) + { + // Roll an overgrown debug.log before adding to it. Nothing in the + // tree shrinks this file once the process is running (there is no + // ShrinkDebugFile, and even Bitcoin's only ran at startup), so a + // node that stays up for months grows the log without bound -- + // production hit ~2 GiB. The file is opened and closed per line, + // so a rename is safe here: the next line simply reopens the + // path. One generation is kept as debug.log.old; logrotate + // (contrib/logrotate/) handles anything longer-term. + int nSize = GetFilesize(fileout); + if (nSize >= 0 && (int64)nSize > nMaxDebugFileSize) + { + fclose(fileout); + string strOldFile = strDebugFile + ".old"; + remove(strOldFile.c_str()); // rename() won't clobber on Windows + rename(strDebugFile.c_str(), strOldFile.c_str()); + fileout = fopen(strDebugFile.c_str(), "a"); + if (fileout) + fprintf(fileout, + "debug.log grew past %lld MB; previous contents moved to debug.log.old (-maxdebugfilesize)\n", + (long long)(nMaxDebugFileSize / 1024 / 1024)); + } + } if (fileout) { va_list arg_ptr; -- cgit v1.2.3 From 2661de141e46dadfb2903bb12e72dc87c5a64375 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 05:31:22 +0000 Subject: log: demote the four lines that were 90% of debug.log behind /debugrelay A 13-hour production capture was ~71% "sending: tx", ~16% "Nostr: discovered .btf peer", ~5% "Relaying wtx", and ~43% of raw lines were CBlock/CTransaction dumps -- all already under -debug, which operators run permanently, so -debug was useless as a filter. The category mechanism gains a verbose tier: "relay" and "nostrdiscovery" are accepted only with the new /debugrelay flag (which implies /debug); everything else keeps its current gate. Demoted, with message text unchanged for anyone grepping it: net.h "sending: tx (N bytes)" (BeginMessage/EndMessage/ AbortMessage all pick the category from the command, so no orphaned "(N bytes)" fragments leak into a plain -debug log) main.cpp "Relaying wtx", "received block:" + CBlock::print, and the BitcoinMiner block dump nostr.cpp "Nostr: discovered .btf peer" -> "nostrdiscovery" Pool/worker/payout, [payout], BLOCK ORPHANED/ACCEPTED and share flow lines are untouched. Because LogPrint dedup keys on category, demoted lines also keep their burst collapse under /debugrelay. Closes #35 --- src/main.cpp | 6 +++--- src/main_gui.cpp | 12 ++++++++++++ src/net.h | 22 ++++++++++++++++++---- src/nostr.cpp | 2 +- src/util.cpp | 13 +++++++++++++ src/util.h | 4 ++++ 6 files changed, 51 insertions(+), 8 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..80f5f31 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1712,7 +1712,7 @@ void CWalletTx::RelayWalletTransaction(CTxDB& txdb) uint256 hash = GetHash(); if (!txdb.ContainsTx(hash)) { - if (LogAcceptsCategory("net")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); + if (LogAcceptsCategory("relay")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); } } @@ -3263,7 +3263,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) vRecv >> *pblock; //// debug print - if (LogAcceptsCategory("net")) { printf("received block:\n"); pblock->print(); } + if (LogAcceptsCategory("relay")) { printf("received block:\n"); pblock->print(); } CInv inv(MSG_BLOCK, pblock->GetHash()); pfrom->AddInventoryKnown(inv); @@ -4555,7 +4555,7 @@ bool BitcoinMiner(int nThreadId) //// debug print if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); - if (LogAcceptsCategory("net")) pblock->print(); + if (LogAcceptsCategory("relay")) pblock->print(); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); CRITICAL_BLOCK(cs_main) diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 05f5ecf..163d78e 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -111,6 +111,7 @@ static void PrintUsage() printf(" /help, -help, --help, /?\n"); printf(" /datadir=PATH\n"); printf(" /debug\n"); + printf(" /debugrelay (/debug plus per-message relay chatter and block dumps)\n"); printf(" /maxdebugfilesize=N (roll debug.log at N MB, default 1024; 0 = never)\n"); printf(" /gen\n"); printf(" /nogui or /daemon\n"); @@ -192,6 +193,17 @@ static void ParseStartupArguments(int argc, char* argv[]) if (arg(argc,argv,"/debug") || arg(argc,argv,"-debug")) fDebug = true; + // /debugrelay -- everything /debug shows, plus the per-message firehose + // ("relay" and "nostrdiscovery" categories: sending: tx, Relaying wtx, + // raw CBlock dumps, per-peer discovery lines). That traffic was ~90% of + // a 13-hour production debug.log (issue #35), so plain /debug leaves it + // off; the message text itself is unchanged for anyone grepping it. + if (arg(argc,argv,"/debugrelay") || arg(argc,argv,"-debugrelay")) + { + fDebug = true; + fDebugRelay = true; + } + // /maxdebugfilesize=N -- roll debug.log to debug.log.old once it passes // N MB, checked on each write. Default is 1024 (see util.cpp); 0 turns // the check off and leaves rotation entirely to logrotate. diff --git a/src/net.h b/src/net.h index 9f3c7e4..1aee973 100644 --- a/src/net.h +++ b/src/net.h @@ -802,6 +802,18 @@ public: + // "sending: tx (N bytes)" alone was ~71% of a 13-hour production + // debug.log (issue #35), so tx sends are demoted to the /debugrelay-gated + // "relay" category; every other command's send trace stays on "net". + // The line is emitted in pieces across BeginMessage/EndMessage/ + // AbortMessage, so all three must agree on the category or orphaned + // "(N bytes)" fragments would leak into a plain -debug log. + // Valid only mid-message (nPushPos != -1). + const char* SendLogCategory() const + { + return strcmp(GetMessageCommand(), "tx") == 0 ? "relay" : "net"; + } + void BeginMessage(const char* pszCommand) { EnterCriticalSection(&cs_vSend); @@ -809,17 +821,19 @@ public: AbortMessage(); nPushPos = vSend.size(); vSend << CMessageHeader(pszCommand, 0); - if (LogAcceptsCategory("net")) printf("sending: %-12s ", pszCommand); + if (LogAcceptsCategory(SendLogCategory())) printf("sending: %-12s ", pszCommand); } void AbortMessage() { if (nPushPos == -1) return; + // Capture before nPushPos is cleared; GetMessageCommand() needs it. + const char* pszCategory = SendLogCategory(); vSend.resize(nPushPos); nPushPos = -1; LeaveCriticalSection(&cs_vSend); - if (LogAcceptsCategory("net")) printf("(aborted)\n"); + if (LogAcceptsCategory(pszCategory)) printf("(aborted)\n"); } void EndMessage() @@ -839,10 +853,10 @@ public: unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader); memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize)); - if (LogAcceptsCategory("net")) printf("(%d bytes) ", nSize); + if (LogAcceptsCategory(SendLogCategory())) printf("(%d bytes) ", nSize); //for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++) // printf("%02x ", vSend[i] & 0xff); - if (LogAcceptsCategory("net")) printf("\n"); + if (LogAcceptsCategory(SendLogCategory())) printf("\n"); nPushPos = -1; LeaveCriticalSection(&cs_vSend); diff --git a/src/nostr.cpp b/src/nostr.cpp index e2d839c..9bbf74e 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -1440,7 +1440,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) else if (createdAt > it->second) it->second = createdAt; // refresh } if (fNew && fDebug) - LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); + LogPrint("nostrdiscovery", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); if (++nPeers > 500) break; } else if (type == "EOSE") diff --git a/src/util.cpp b/src/util.cpp index 52cda84..a5a4e80 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -7,6 +7,7 @@ bool fDebug = false; +bool fDebugRelay = false; // In-process cap for debug.log, in bytes. Default matches the 1G trigger of // the shipped logrotate policy so a long-running node stays bounded even @@ -31,10 +32,22 @@ static bool LogCategoryDefaultOn(const char* category) strcmp(category, "payout") == 0; } +// Categories so noisy they drown out everything else even under -debug: +// per-message send traces, wallet relay rebroadcasts, raw CBlock dumps and +// per-address discovery lines made up ~90% of a 13-hour production capture +// (issue #35). They need /debugrelay, not merely /debug. +static bool LogCategoryVerboseOnly(const char* category) +{ + return strcmp(category, "relay") == 0 || + strcmp(category, "nostrdiscovery") == 0; +} + bool LogAcceptsCategory(const char* category) { if (LogCategoryDefaultOn(category)) return true; + if (LogCategoryVerboseOnly(category)) + return fDebugRelay; return fDebug; } diff --git a/src/util.h b/src/util.h index f2ea23f..c47b8a7 100644 --- a/src/util.h +++ b/src/util.h @@ -87,6 +87,10 @@ inline T& REF(const T& val) extern bool fDebug; +// /debugrelay: the per-message firehose ("relay" and "nostrdiscovery" +// categories) that even -debug keeps off. See LogAcceptsCategory(). +extern bool fDebugRelay; + // debug.log is rolled to debug.log.old once it grows past this many bytes // (set from -maxdebugfilesize, in MB; 0 disables the check). See the roll // site in OutputDebugStringF() below. -- cgit v1.2.3 From e3cc976ae3f211727e3fae702776aacca46efb0c Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 16:42:39 +0000 Subject: log: reject a -maxdebugfilesize that is not a plain count of MB atoi64() is strtoll() with the endptr discarded, so it yields 0 for anything it cannot read. -maxdebugfilesize=1G and -maxdebugfilesize=1024MB both parsed as 0, which the roll reads as "never roll" -- indistinguishable from requesting that deliberately, and silent. An operator who wrote a unit suffix got no rotation and no warning. Only a negative value said anything. The multiplication was unchecked as well: a large enough N made nMB * 1024 * 1024 overflow to a negative cap, which disables the roll too. Parse with strtoll() and its endptr, and reject trailing characters, ERANGE, negatives, and any value that would overflow the byte cap -- each reported with the reason and the cap being kept. 0 still means never roll and is still accepted without comment. --- src/main_gui.cpp | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) (limited to 'src/main_gui.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 163d78e..0e64985 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -210,10 +210,33 @@ static void ParseStartupArguments(int argc, char* argv[]) string strMaxDebugSize = argval2(argc, argv, "/maxdebugfilesize", "-maxdebugfilesize"); if (!strMaxDebugSize.empty()) { - int64 nMB = atoi64(strMaxDebugSize.c_str()); - if (nMB < 0) - fprintf(stderr, "Ignoring /maxdebugfilesize=%s: not a size in MB\n", - strMaxDebugSize.c_str()); + // Parsed with atoi64() before, which is strtoll() with the endptr + // thrown away: it returns 0 for anything it cannot read, so + // /maxdebugfilesize=1G and =1024MB both came out as 0 -- "never + // roll", identical to asking for that on purpose, with nothing said + // to the operator. The old code also multiplied without a range + // check, so a big enough N wrapped to a negative cap, which the roll + // reads as "off" too. Every rejection is now reported and leaves the + // default in place; the value is a plain count of MB, no suffix. + const char* psz = strMaxDebugSize.c_str(); + char* pszEnd = NULL; + errno = 0; + int64 nMB = strtoll(psz, &pszEnd, 10); + // INT64_MAX, without depending on which limits header this tree pulls in. + const int64 nMaxMB = (int64)((~(uint64)0) >> 1) / (1024 * 1024); + const char* pszWhy = NULL; + if (pszEnd == psz) + pszWhy = "not a number"; + else if (*pszEnd != '\0') + pszWhy = "trailing characters -- the value is a count of MB, with no unit suffix"; + else if (errno == ERANGE || nMB < 0) + pszWhy = "not a size in MB"; + else if (nMB > nMaxMB) + pszWhy = "larger than a file size can be"; + if (pszWhy) + fprintf(stderr, "Ignoring /maxdebugfilesize=%s: %s (keeping %lld MB)\n", + strMaxDebugSize.c_str(), pszWhy, + (long long)(nMaxDebugFileSize / 1024 / 1024)); else nMaxDebugFileSize = nMB * 1024 * 1024; } -- cgit v1.2.3