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') 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 b3088010726c9ab1ceeaa84357e91a8f81763fe2 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 08:24:17 -0300 Subject: wallet: initialize storage audit counts explicitly --- src/walletcmd.cpp | 49 ++++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 265febe..6fc8c38 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -476,33 +476,28 @@ int CmdRecoveryAudit() 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)); - } + unsigned int nTotal = 0; + unsigned int nMalformed = 0; + unsigned int nUnknown = 0; + unsigned int nVersion = 0; + unsigned int nNames = 0; + unsigned int nTransactions = 0; + unsigned int nPlainKeys = 0; + unsigned int nEncryptedKeys = 0; + unsigned int nMasterKeys = 0; + unsigned int nDefaultKey = 0; + unsigned int nPlainHDMaster = 0; + unsigned int nPlainHDChainCode = 0; + unsigned int nCryptedHDMaster = 0; + unsigned int nCryptedHDChainCode = 0; + unsigned int nWalletMinVersion = 0; + unsigned int nPool = 0; + unsigned int nSettings = 0; + unsigned int nHDNext = 0; + unsigned int nHDSchema = 0; + unsigned int nHDCoinType = 0; + unsigned int nHDReceiveNext = 0; + unsigned int nHDChangeNext = 0; }; static std::string HDSeedStorageState(unsigned int nMaster, unsigned int nChain) -- cgit v1.2.3 From 33af98c042340f6431aa93b4b6f893d85f40f237 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 14 Aug 2026 08:48:26 -0300 Subject: wallet: drop bdb log check from storage sanity --- src/walletcmd.cpp | 51 --------------------------------------------------- 1 file changed, 51 deletions(-) (limited to 'src') diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 6fc8c38..ce0da02 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -13,10 +13,6 @@ #include "bip32.h" #include "walletcmd.h" -#ifndef _WIN32 -#include -#endif - #undef printf // How far ahead to look for used addresses when restoring. @@ -657,51 +653,6 @@ 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) { @@ -742,8 +693,6 @@ static void BuildWalletStorageSanityFailures(const WalletStorageAuditCounts& cou 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"); } } -- cgit v1.2.3