diff options
| author | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-13 18:11:30 -0300 |
|---|---|---|
| committer | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-13 18:11:30 -0300 |
| commit | 540ed06974ff6b05364d4922895288744f076f6b (patch) | |
| tree | 6d7ba6802278f148fd98ffc3a482a47991dc08a7 /src | |
| parent | 805c5757b801d80ea575c6a8b58e2855254f9046 (diff) | |
wallet: add storage sanity check
Diffstat (limited to 'src')
| -rw-r--r-- | src/main_gui.cpp | 9 | ||||
| -rw-r--r-- | src/selftest.cpp | 27 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 137 | ||||
| -rw-r--r-- | src/walletcmd.h | 4 |
4 files changed, 177 insertions, 0 deletions
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() | |||
| 176 | printf(" printing wallet values, then exit)\n"); | 176 | printf(" printing wallet values, then exit)\n"); |
| 177 | printf(" /walletstorageauditjson=FILE\n"); | 177 | printf(" /walletstorageauditjson=FILE\n"); |
| 178 | printf(" (write the same audit as deterministic JSON)\n"); | 178 | printf(" (write the same audit as deterministic JSON)\n"); |
| 179 | printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); | ||
| 180 | printf(" wallet.dat storage state)\n"); | ||
| 179 | printf(" /rescan (walk the chain for coins this wallet owns\n"); | 181 | printf(" /rescan (walk the chain for coins this wallet owns\n"); |
| 180 | printf(" but never recorded, then exit)\n"); | 182 | printf(" but never recorded, then exit)\n"); |
| 181 | printf("\n"); | 183 | printf("\n"); |
| @@ -719,6 +721,13 @@ int main(int argc, char* argv[]) | |||
| 719 | return nRet; | 721 | return nRet; |
| 720 | } | 722 | } |
| 721 | 723 | ||
| 724 | if (arg(argc,argv,"/walletstoragecheck") || arg(argc,argv,"-walletstoragecheck")) | ||
| 725 | { | ||
| 726 | int nRet = CmdWalletStorageCheck(); | ||
| 727 | DBFlush(true); | ||
| 728 | return nRet; | ||
| 729 | } | ||
| 730 | |||
| 722 | if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) | 731 | if (arg(argc,argv,"/newphrase") || arg(argc,argv,"-newphrase")) |
| 723 | { | 732 | { |
| 724 | int nRet = CmdNewPhrase(); | 733 | 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() | |||
| 1329 | FileContainsText(strOriginalStorageAudit, "malformed records: 0"), | 1329 | FileContainsText(strOriginalStorageAudit, "malformed records: 0"), |
| 1330 | "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; | 1330 | "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; |
| 1331 | 1331 | ||
| 1332 | string strOriginalStorageCheck = tmp + "/original-storage-check.txt"; | ||
| 1333 | vector<string> vOriginalStorageCheckArgs; | ||
| 1334 | vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 1335 | vOriginalStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 1336 | vOriginalStorageCheckArgs.push_back("-nogui"); | ||
| 1337 | vOriginalStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 1338 | int nOriginalStorageCheckRet = | ||
| 1339 | RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck); | ||
| 1340 | nFail += Check(nOriginalStorageCheckRet == 0 && | ||
| 1341 | FileContainsText(strOriginalStorageCheck, "storage sanity: ok") && | ||
| 1342 | FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"), | ||
| 1343 | "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1; | ||
| 1344 | |||
| 1332 | string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; | 1345 | string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; |
| 1333 | vector<string> vOriginalStorageAuditJsonArgs; | 1346 | vector<string> vOriginalStorageAuditJsonArgs; |
| 1334 | vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | 1347 | vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); |
| @@ -1419,6 +1432,20 @@ static int RunWalletPortabilitySelfTest() | |||
| 1419 | FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), | 1432 | FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), |
| 1420 | "the storage audit recognizes an encrypted wallet") ? 0 : 1; | 1433 | "the storage audit recognizes an encrypted wallet") ? 0 : 1; |
| 1421 | 1434 | ||
| 1435 | string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt"; | ||
| 1436 | vector<string> vEncryptedStorageCheckArgs; | ||
| 1437 | vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal); | ||
| 1438 | vEncryptedStorageCheckArgs.push_back("-nomanagedtor"); | ||
| 1439 | vEncryptedStorageCheckArgs.push_back("-nogui"); | ||
| 1440 | vEncryptedStorageCheckArgs.push_back("-walletstoragecheck"); | ||
| 1441 | int nEncryptedStorageCheckRet = | ||
| 1442 | RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck); | ||
| 1443 | nFail += Check(nEncryptedStorageCheckRet == 0 && | ||
| 1444 | FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") && | ||
| 1445 | FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") && | ||
| 1446 | FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"), | ||
| 1447 | "the storage sanity check accepts an encrypted wallet") ? 0 : 1; | ||
| 1448 | |||
| 1422 | string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; | 1449 | string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; |
| 1423 | vector<string> vEncryptedStorageAuditJsonArgs; | 1450 | vector<string> vEncryptedStorageAuditJsonArgs; |
| 1424 | vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); | 1451 | 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 @@ | |||
| 13 | #include "bip32.h" | 13 | #include "bip32.h" |
| 14 | #include "walletcmd.h" | 14 | #include "walletcmd.h" |
| 15 | 15 | ||
| 16 | #ifndef _WIN32 | ||
| 17 | #include <dirent.h> | ||
| 18 | #endif | ||
| 19 | |||
| 16 | #undef printf | 20 | #undef printf |
| 17 | 21 | ||
| 18 | // How far ahead to look for used addresses when restoring. | 22 | // How far ahead to look for used addresses when restoring. |
| @@ -658,6 +662,139 @@ static bool WriteAuditTextFile(const std::string& strPath, | |||
| 658 | return true; | 662 | return true; |
| 659 | } | 663 | } |
| 660 | 664 | ||
| 665 | static bool DirectoryHasFileWithPrefixLocal(const std::string& strDir, | ||
| 666 | const std::string& strPrefix) | ||
| 667 | { | ||
| 668 | #ifdef _WIN32 | ||
| 669 | std::string pattern = strDir + "/*"; | ||
| 670 | WIN32_FIND_DATAA findData; | ||
| 671 | HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); | ||
| 672 | if (hFind == INVALID_HANDLE_VALUE) | ||
| 673 | return false; | ||
| 674 | bool fFound = false; | ||
| 675 | do | ||
| 676 | { | ||
| 677 | if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) | ||
| 678 | { | ||
| 679 | std::string name = findData.cFileName; | ||
| 680 | if (name.compare(0, strPrefix.size(), strPrefix) == 0) | ||
| 681 | { | ||
| 682 | fFound = true; | ||
| 683 | break; | ||
| 684 | } | ||
| 685 | } | ||
| 686 | } | ||
| 687 | while (FindNextFileA(hFind, &findData)); | ||
| 688 | FindClose(hFind); | ||
| 689 | return fFound; | ||
| 690 | #else | ||
| 691 | DIR* dir = opendir(strDir.c_str()); | ||
| 692 | if (!dir) | ||
| 693 | return false; | ||
| 694 | bool fFound = false; | ||
| 695 | struct dirent* ent; | ||
| 696 | while ((ent = readdir(dir)) != NULL) | ||
| 697 | { | ||
| 698 | std::string name = ent->d_name; | ||
| 699 | if (name.compare(0, strPrefix.size(), strPrefix) == 0) | ||
| 700 | { | ||
| 701 | fFound = true; | ||
| 702 | break; | ||
| 703 | } | ||
| 704 | } | ||
| 705 | closedir(dir); | ||
| 706 | return fFound; | ||
| 707 | #endif | ||
| 708 | } | ||
| 709 | |||
| 710 | static void AddWalletStorageFailure(std::vector<std::string>& vFailures, | ||
| 711 | const std::string& strFailure) | ||
| 712 | { | ||
| 713 | vFailures.push_back(strFailure); | ||
| 714 | } | ||
| 715 | |||
| 716 | static void BuildWalletStorageSanityFailures(const WalletStorageAuditCounts& counts, | ||
| 717 | std::vector<std::string>& vFailures) | ||
| 718 | { | ||
| 719 | bool fPlainHD = counts.nPlainHDMaster > 0 || counts.nPlainHDChainCode > 0; | ||
| 720 | bool fCryptedHD = counts.nCryptedHDMaster > 0 || counts.nCryptedHDChainCode > 0; | ||
| 721 | bool fEncryptedRecords = counts.nEncryptedKeys > 0 || | ||
| 722 | counts.nMasterKeys > 0 || | ||
| 723 | fCryptedHD; | ||
| 724 | |||
| 725 | if (counts.nMalformed > 0) | ||
| 726 | AddWalletStorageFailure(vFailures, "wallet.dat contains malformed records"); | ||
| 727 | if (counts.nUnknown > 0) | ||
| 728 | AddWalletStorageFailure(vFailures, "wallet.dat contains unknown records"); | ||
| 729 | if ((counts.nPlainHDMaster > 0) != (counts.nPlainHDChainCode > 0)) | ||
| 730 | AddWalletStorageFailure(vFailures, "plain HD seed is incomplete"); | ||
| 731 | if ((counts.nCryptedHDMaster > 0) != (counts.nCryptedHDChainCode > 0)) | ||
| 732 | AddWalletStorageFailure(vFailures, "encrypted HD seed is incomplete"); | ||
| 733 | if (fPlainHD && fCryptedHD) | ||
| 734 | AddWalletStorageFailure(vFailures, "wallet.dat contains both plain and encrypted HD seed records"); | ||
| 735 | if (counts.nDefaultKey > 1) | ||
| 736 | AddWalletStorageFailure(vFailures, "wallet.dat contains more than one default public key record"); | ||
| 737 | if (counts.nWalletMinVersion > 1) | ||
| 738 | AddWalletStorageFailure(vFailures, "wallet.dat contains more than one minimum-version record"); | ||
| 739 | |||
| 740 | if (fEncryptedRecords) | ||
| 741 | { | ||
| 742 | if (counts.nPlainKeys > 0) | ||
| 743 | AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain private key records"); | ||
| 744 | if (fPlainHD) | ||
| 745 | AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain HD seed records"); | ||
| 746 | if (counts.nMasterKeys == 0) | ||
| 747 | AddWalletStorageFailure(vFailures, "encrypted wallet has no encryption master key record"); | ||
| 748 | if (counts.nWalletMinVersion == 0) | ||
| 749 | AddWalletStorageFailure(vFailures, "encrypted wallet has no minimum-version record"); | ||
| 750 | if (DirectoryHasFileWithPrefixLocal(GetAppDir() + "/database", "log.")) | ||
| 751 | AddWalletStorageFailure(vFailures, "Berkeley DB log files are still present for an encrypted wallet"); | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | int CmdWalletStorageCheck() | ||
| 756 | { | ||
| 757 | AttachTerminal(); | ||
| 758 | |||
| 759 | WalletStorageAuditCounts counts; | ||
| 760 | std::string strError; | ||
| 761 | if (!ReadWalletStorageCounts(counts, strError)) | ||
| 762 | { | ||
| 763 | fprintf(stderr, "Cannot check wallet storage: %s\n", strError.c_str()); | ||
| 764 | return 1; | ||
| 765 | } | ||
| 766 | |||
| 767 | std::vector<std::string> vFailures; | ||
| 768 | BuildWalletStorageSanityFailures(counts, vFailures); | ||
| 769 | |||
| 770 | printf("Wallet storage sanity check\n"); | ||
| 771 | printf(" records total: %u\n", counts.nTotal); | ||
| 772 | printf(" encrypted records: %s\n", | ||
| 773 | (counts.nEncryptedKeys || counts.nMasterKeys || | ||
| 774 | counts.nCryptedHDMaster || counts.nCryptedHDChainCode) ? "yes" : "no"); | ||
| 775 | printf(" plain private keys: %u\n", counts.nPlainKeys); | ||
| 776 | printf(" encrypted private keys: %u\n", counts.nEncryptedKeys); | ||
| 777 | printf(" plain HD seed: %s\n", | ||
| 778 | HDSeedStorageState(counts.nPlainHDMaster, | ||
| 779 | counts.nPlainHDChainCode).c_str()); | ||
| 780 | printf(" encrypted HD seed: %s\n", | ||
| 781 | HDSeedStorageState(counts.nCryptedHDMaster, | ||
| 782 | counts.nCryptedHDChainCode).c_str()); | ||
| 783 | if (vFailures.empty()) | ||
| 784 | { | ||
| 785 | printf(" storage sanity: ok\n"); | ||
| 786 | fflush(stdout); | ||
| 787 | return 0; | ||
| 788 | } | ||
| 789 | |||
| 790 | printf(" storage sanity: failed\n"); | ||
| 791 | for (std::vector<std::string>::const_iterator it = vFailures.begin(); | ||
| 792 | it != vFailures.end(); ++it) | ||
| 793 | printf(" failure: %s\n", it->c_str()); | ||
| 794 | fflush(stdout); | ||
| 795 | return 2; | ||
| 796 | } | ||
| 797 | |||
| 661 | int CmdWalletStorageAudit(const std::string& strJsonOut) | 798 | int CmdWalletStorageAudit(const std::string& strJsonOut) |
| 662 | { | 799 | { |
| 663 | AttachTerminal(); | 800 | 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(); | |||
| 66 | // addresses, transaction ids, labels, or other wallet values. | 66 | // addresses, transaction ids, labels, or other wallet values. |
| 67 | int CmdWalletStorageAudit(const std::string& strJsonOut = ""); | 67 | int CmdWalletStorageAudit(const std::string& strJsonOut = ""); |
| 68 | 68 | ||
| 69 | // Diagnostic: fail closed when wallet.dat contains an internally inconsistent | ||
| 70 | // or migration-unsafe mix of storage records. | ||
| 71 | int CmdWalletStorageCheck(); | ||
| 72 | |||
| 69 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the | 73 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the |
| 70 | // given passphrase. Returns 0 on success and exits without starting the node. | 74 | // given passphrase. Returns 0 on success and exits without starting the node. |
| 71 | int CmdEncryptWallet(const std::string& strPassphrase); | 75 | int CmdEncryptWallet(const std::string& strPassphrase); |