diff options
| author | Bitflash <admin@bitflash.network> | 2026-08-14 16:53:01 -0300 |
|---|---|---|
| committer | Bitflash <admin@bitflash.network> | 2026-08-14 16:53:01 -0300 |
| commit | 35f6c752f9f879ca94478985978e4a9c862ede3e (patch) | |
| tree | a32b59dde39832f1fc7a21a29681b77c95de40cb | |
| parent | 0c58896a045246c3518d5d379f217172defdfd91 (diff) | |
| parent | 99c6b5f2971da51cca1c739b5e982ae2022acc85 (diff) | |
Merge pull request 'wallet: verify SQLite wallet exports' (#50) from wallet-sqlite-verify into main
| -rw-r--r-- | docs/storage.md | 2 | ||||
| -rw-r--r-- | src/main_gui.cpp | 11 | ||||
| -rw-r--r-- | src/selftest.cpp | 41 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 107 | ||||
| -rw-r--r-- | src/walletcmd.h | 4 |
5 files changed, 165 insertions, 0 deletions
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 | |||
| 111 | backend. `CWalletDBSQLite::ScanRecords()` can stream those exported records back | 111 | backend. `CWalletDBSQLite::ScanRecords()` can stream those exported records back |
| 112 | through the same visitor shape, so tests can compare BDB and SQLite record | 112 | through the same visitor shape, so tests can compare BDB and SQLite record |
| 113 | streams without introducing a SQLite runtime wallet loader yet. | 113 | streams without introducing a SQLite runtime wallet loader yet. |
| 114 | `-walletsqliteverify=FILE` compares the current `wallet.dat` stream with a | ||
| 115 | SQLite export and prints only record counts and mismatch counts. | ||
| 114 | 116 | ||
| 115 | ## `blkindex.dat` | 117 | ## `blkindex.dat` |
| 116 | 118 | ||
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() | |||
| 183 | printf(" wallet.dat storage state)\n"); | 183 | printf(" wallet.dat storage state)\n"); |
| 184 | printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); | 184 | printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n"); |
| 185 | printf(" experimental SQLite store, then exit)\n"); | 185 | printf(" experimental SQLite store, then exit)\n"); |
| 186 | printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n"); | ||
| 187 | printf(" printing counts only, then exit)\n"); | ||
| 186 | printf(" /rescan (walk the chain for coins this wallet owns\n"); | 188 | printf(" /rescan (walk the chain for coins this wallet owns\n"); |
| 187 | printf(" but never recorded, then exit)\n"); | 189 | printf(" but never recorded, then exit)\n"); |
| 188 | printf("\n"); | 190 | printf("\n"); |
| @@ -540,6 +542,15 @@ int main(int argc, char* argv[]) | |||
| 540 | return nRet; | 542 | return nRet; |
| 541 | } | 543 | } |
| 542 | 544 | ||
| 545 | string strWalletSQLiteVerify = | ||
| 546 | argval2(argc, argv, "/walletsqliteverify", "-walletsqliteverify"); | ||
| 547 | if (!strWalletSQLiteVerify.empty()) | ||
| 548 | { | ||
| 549 | int nRet = CmdWalletSQLiteVerify(strWalletSQLiteVerify); | ||
| 550 | DBFlush(true); | ||
| 551 | return nRet; | ||
| 552 | } | ||
| 553 | |||
| 543 | // Berkeley DB reports failure by throwing, and CDB's constructor lets it | 554 | // Berkeley DB reports failure by throwing, and CDB's constructor lets it |
| 544 | // through. Nothing on this path caught anything, so an unreadable | 555 | // through. Nothing on this path caught anything, so an unreadable |
| 545 | // blkindex.dat or wallet.dat -- one written by another platform's Berkeley | 556 | // 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() | |||
| 1134 | return nFail == 0 ? 0 : 1; | 1134 | return nFail == 0 ? 0 : 1; |
| 1135 | } | 1135 | } |
| 1136 | 1136 | ||
| 1137 | namespace { | ||
| 1138 | |||
| 1137 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | 1139 | class CWalletRecordMapVisitor : public CWalletRecordVisitor |
| 1138 | { | 1140 | { |
| 1139 | public: | 1141 | public: |
| @@ -1158,6 +1160,8 @@ public: | |||
| 1158 | } | 1160 | } |
| 1159 | }; | 1161 | }; |
| 1160 | 1162 | ||
| 1163 | } // namespace | ||
| 1164 | |||
| 1161 | static int RunWalletSQLiteMigrationSelfTest() | 1165 | static int RunWalletSQLiteMigrationSelfTest() |
| 1162 | { | 1166 | { |
| 1163 | fflush(stdout); | 1167 | fflush(stdout); |
| @@ -1215,6 +1219,17 @@ static int RunWalletSQLiteMigrationSelfTest() | |||
| 1215 | FileContainsText(strExportOut, "SQLite wallet export written"), | 1219 | FileContainsText(strExportOut, "SQLite wallet export written"), |
| 1216 | "wallet.dat can be exported to SQLite") ? 0 : 1; | 1220 | "wallet.dat can be exported to SQLite") ? 0 : 1; |
| 1217 | 1221 | ||
| 1222 | string strVerifyOut = tmp + "/sqlite-verify.txt"; | ||
| 1223 | vector<string> vVerifyArgs; | ||
| 1224 | vVerifyArgs.push_back("-datadir=" + strWalletDir); | ||
| 1225 | vVerifyArgs.push_back("-nomanagedtor"); | ||
| 1226 | vVerifyArgs.push_back("-nogui"); | ||
| 1227 | vVerifyArgs.push_back("-walletsqliteverify=" + strSQLite); | ||
| 1228 | int nVerifyRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyOut); | ||
| 1229 | nFail += Check(nVerifyRet == 0 && | ||
| 1230 | FileContainsText(strVerifyOut, "verification: ok"), | ||
| 1231 | "SQLite wallet export verifies against wallet.dat") ? 0 : 1; | ||
| 1232 | |||
| 1218 | string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; | 1233 | string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; |
| 1219 | int nOverwriteRet = | 1234 | int nOverwriteRet = |
| 1220 | RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); | 1235 | RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); |
| @@ -1265,6 +1280,32 @@ static int RunWalletSQLiteMigrationSelfTest() | |||
| 1265 | nFail += Check(db.ScanRecords(sqliteVisitor, strError) && | 1280 | nFail += Check(db.ScanRecords(sqliteVisitor, strError) && |
| 1266 | mapSQLite == mapBDB, | 1281 | mapSQLite == mapBDB, |
| 1267 | "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1; | 1282 | "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1; |
| 1283 | |||
| 1284 | db.Close(); | ||
| 1285 | CWalletDBSQLite dbCorrupt; | ||
| 1286 | vector<unsigned char> vchBadValue; | ||
| 1287 | vchBadValue.push_back(0xba); | ||
| 1288 | vchBadValue.push_back(0xad); | ||
| 1289 | nFail += Check(!mapBDB.empty() && | ||
| 1290 | dbCorrupt.Open(strSQLite, strError) && | ||
| 1291 | dbCorrupt.BeginTransaction(strError) && | ||
| 1292 | dbCorrupt.WriteRecord(mapBDB.begin()->first, | ||
| 1293 | vchBadValue, | ||
| 1294 | strError) && | ||
| 1295 | dbCorrupt.CommitTransaction(strError) && | ||
| 1296 | dbCorrupt.Checkpoint(strError), | ||
| 1297 | "SQLite export can be deliberately corrupted for verifier test") ? 0 : 1; | ||
| 1298 | dbCorrupt.Close(); | ||
| 1299 | |||
| 1300 | string strVerifyBadOut = tmp + "/sqlite-verify-bad.txt"; | ||
| 1301 | int nVerifyBadRet = | ||
| 1302 | RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyBadOut); | ||
| 1303 | nFail += Check(nVerifyBadRet == 2 && | ||
| 1304 | FileContainsText(strVerifyBadOut, | ||
| 1305 | "record value mismatches: 1") && | ||
| 1306 | FileContainsText(strVerifyBadOut, | ||
| 1307 | "verification: failed"), | ||
| 1308 | "SQLite wallet verifier detects a mismatched record value") ? 0 : 1; | ||
| 1268 | } | 1309 | } |
| 1269 | catch (const std::exception& e) | 1310 | catch (const std::exception& e) |
| 1270 | { | 1311 | { |
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<unsigned char> DataStreamBytesLocal(const CDataStream& ss) | |||
| 796 | return vector<unsigned char>(ss.begin(), ss.end()); | 796 | return vector<unsigned char>(ss.begin(), ss.end()); |
| 797 | } | 797 | } |
| 798 | 798 | ||
| 799 | namespace { | ||
| 800 | |||
| 799 | class CWalletSQLiteExportVisitor : public CWalletRecordVisitor | 801 | class CWalletSQLiteExportVisitor : public CWalletRecordVisitor |
| 800 | { | 802 | { |
| 801 | public: | 803 | public: |
| @@ -818,6 +820,32 @@ public: | |||
| 818 | } | 820 | } |
| 819 | }; | 821 | }; |
| 820 | 822 | ||
| 823 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | ||
| 824 | { | ||
| 825 | public: | ||
| 826 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords; | ||
| 827 | |||
| 828 | explicit CWalletRecordMapVisitor( | ||
| 829 | std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn) | ||
| 830 | : mapRecords(mapRecordsIn) { } | ||
| 831 | |||
| 832 | bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 833 | const CDataStream& ssValue, | ||
| 834 | string& strErrorRet) | ||
| 835 | { | ||
| 836 | vector<unsigned char> vchKey = DataStreamBytesLocal(ssKey); | ||
| 837 | if (mapRecords.count(vchKey)) | ||
| 838 | { | ||
| 839 | strErrorRet = "duplicate serialized wallet key"; | ||
| 840 | return false; | ||
| 841 | } | ||
| 842 | mapRecords[vchKey] = DataStreamBytesLocal(ssValue); | ||
| 843 | return true; | ||
| 844 | } | ||
| 845 | }; | ||
| 846 | |||
| 847 | } // namespace | ||
| 848 | |||
| 821 | int CmdWalletSQLiteExport(const std::string& strDest) | 849 | int CmdWalletSQLiteExport(const std::string& strDest) |
| 822 | { | 850 | { |
| 823 | AttachTerminal(); | 851 | AttachTerminal(); |
| @@ -887,6 +915,85 @@ int CmdWalletSQLiteExport(const std::string& strDest) | |||
| 887 | return 0; | 915 | return 0; |
| 888 | } | 916 | } |
| 889 | 917 | ||
| 918 | int CmdWalletSQLiteVerify(const std::string& strPath) | ||
| 919 | { | ||
| 920 | AttachTerminal(); | ||
| 921 | |||
| 922 | if (strPath.empty()) | ||
| 923 | { | ||
| 924 | fprintf(stderr, "Missing SQLite wallet export path.\n"); | ||
| 925 | return 1; | ||
| 926 | } | ||
| 927 | if (!FileExists(strPath.c_str())) | ||
| 928 | { | ||
| 929 | fprintf(stderr, "SQLite wallet export does not exist: %s\n", | ||
| 930 | strPath.c_str()); | ||
| 931 | return 1; | ||
| 932 | } | ||
| 933 | |||
| 934 | std::map<vector<unsigned char>, vector<unsigned char> > mapBDB; | ||
| 935 | std::map<vector<unsigned char>, vector<unsigned char> > mapSQLite; | ||
| 936 | string strError; | ||
| 937 | |||
| 938 | CWalletRecordMapVisitor bdbVisitor(mapBDB); | ||
| 939 | if (!ScanWalletRecords(bdbVisitor, strError)) | ||
| 940 | { | ||
| 941 | fprintf(stderr, "Cannot scan wallet.dat records: %s\n", strError.c_str()); | ||
| 942 | return 1; | ||
| 943 | } | ||
| 944 | |||
| 945 | CWalletDBSQLite db; | ||
| 946 | if (!db.Open(strPath, strError)) | ||
| 947 | { | ||
| 948 | fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str()); | ||
| 949 | return 1; | ||
| 950 | } | ||
| 951 | CWalletRecordMapVisitor sqliteVisitor(mapSQLite); | ||
| 952 | if (!db.ScanRecords(sqliteVisitor, strError)) | ||
| 953 | { | ||
| 954 | fprintf(stderr, "Cannot scan SQLite wallet export: %s\n", strError.c_str()); | ||
| 955 | return 1; | ||
| 956 | } | ||
| 957 | |||
| 958 | int nMissing = 0; | ||
| 959 | int nMismatched = 0; | ||
| 960 | for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it = | ||
| 961 | mapBDB.begin(); it != mapBDB.end(); ++it) | ||
| 962 | { | ||
| 963 | std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator sit = | ||
| 964 | mapSQLite.find(it->first); | ||
| 965 | if (sit == mapSQLite.end()) | ||
| 966 | nMissing++; | ||
| 967 | else if (sit->second != it->second) | ||
| 968 | nMismatched++; | ||
| 969 | } | ||
| 970 | |||
| 971 | int nExtra = 0; | ||
| 972 | for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it = | ||
| 973 | mapSQLite.begin(); it != mapSQLite.end(); ++it) | ||
| 974 | { | ||
| 975 | if (!mapBDB.count(it->first)) | ||
| 976 | nExtra++; | ||
| 977 | } | ||
| 978 | |||
| 979 | printf("SQLite wallet export verification\n"); | ||
| 980 | printf(" wallet.dat records: %u\n", (unsigned int)mapBDB.size()); | ||
| 981 | printf(" SQLite records: %u\n", (unsigned int)mapSQLite.size()); | ||
| 982 | printf(" missing records: %d\n", nMissing); | ||
| 983 | printf(" extra records: %d\n", nExtra); | ||
| 984 | printf(" record value mismatches: %d\n", nMismatched); | ||
| 985 | if (nMissing == 0 && nExtra == 0 && nMismatched == 0) | ||
| 986 | { | ||
| 987 | printf(" verification: ok\n"); | ||
| 988 | fflush(stdout); | ||
| 989 | return 0; | ||
| 990 | } | ||
| 991 | |||
| 992 | printf(" verification: failed\n"); | ||
| 993 | fflush(stdout); | ||
| 994 | return 2; | ||
| 995 | } | ||
| 996 | |||
| 890 | int CmdEncryptWallet(const std::string& strPassphrase) | 997 | int CmdEncryptWallet(const std::string& strPassphrase) |
| 891 | { | 998 | { |
| 892 | AttachTerminal(); | 999 | 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(); | |||
| 75 | // not change which backend the runtime wallet opens. | 75 | // not change which backend the runtime wallet opens. |
| 76 | int CmdWalletSQLiteExport(const std::string& strDest); | 76 | int CmdWalletSQLiteExport(const std::string& strDest); |
| 77 | 77 | ||
| 78 | // Diagnostic/migration staging: compare a SQLite wallet export with the current | ||
| 79 | // wallet.dat raw records. Prints counts only, never record keys or values. | ||
| 80 | int CmdWalletSQLiteVerify(const std::string& strPath); | ||
| 81 | |||
| 78 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the | 82 | // Rewrite wallet.dat so private keys and the HD seed are encrypted with the |
| 79 | // given passphrase. Returns 0 on success and exits without starting the node. | 83 | // given passphrase. Returns 0 on success and exits without starting the node. |
| 80 | int CmdEncryptWallet(const std::string& strPassphrase); | 84 | int CmdEncryptWallet(const std::string& strPassphrase); |