upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-14 17:44:09 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-14 17:44:09 -0300
commit3e778dd6cc9045862e99503b5b9af80ec682826f (patch)
tree4a055fb61ea1c6fde9edc65c8ec7356baa8c9c07
parent307d1a79d4f9bdecd3f834f9ab4987f46d16d177 (diff)
wallet: check SQLite exports against loader types
-rw-r--r--docs/storage.md3
-rw-r--r--src/main_gui.cpp11
-rw-r--r--src/selftest.cpp54
-rw-r--r--src/walletcmd.cpp209
-rw-r--r--src/walletcmd.h4
5 files changed, 281 insertions, 0 deletions
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.
117an empty data directory, then the verifier can prove the restored BDB file still 117an empty data directory, then the verifier can prove the restored BDB file still
118matches the export byte-for-byte. Failed exports remove the incomplete SQLite 118matches the export byte-for-byte. Failed exports remove the incomplete SQLite
119file and failed restores remove the incomplete `wallet.dat`. 119file and failed restores remove the incomplete `wallet.dat`.
120`-walletsqliteloadcheck=FILE` parses a SQLite export using the same record
121types the wallet loader understands, so a migration can prove the export is not
122just byte-preserving but loader-compatible before runtime activation.
120 123
121## `blkindex.dat` 124## `blkindex.dat`
122 125
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()
187 printf(" printing counts only, then exit)\n"); 187 printf(" printing counts only, then exit)\n");
188 printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n"); 188 printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n");
189 printf(" an empty data directory, then exit)\n"); 189 printf(" an empty data directory, then exit)\n");
190 printf(" /walletsqliteloadcheck=FILE\n");
191 printf(" (parse a SQLite export like the wallet loader)\n");
190 printf(" /rescan (walk the chain for coins this wallet owns\n"); 192 printf(" /rescan (walk the chain for coins this wallet owns\n");
191 printf(" but never recorded, then exit)\n"); 193 printf(" but never recorded, then exit)\n");
192 printf("\n"); 194 printf("\n");
@@ -562,6 +564,15 @@ int main(int argc, char* argv[])
562 return nRet; 564 return nRet;
563 } 565 }
564 566
567 string strWalletSQLiteLoadCheck =
568 argval2(argc, argv, "/walletsqliteloadcheck", "-walletsqliteloadcheck");
569 if (!strWalletSQLiteLoadCheck.empty())
570 {
571 int nRet = CmdWalletSQLiteLoadCheck(strWalletSQLiteLoadCheck);
572 DBFlush(true);
573 return nRet;
574 }
575
565 // Berkeley DB reports failure by throwing, and CDB's constructor lets it 576 // Berkeley DB reports failure by throwing, and CDB's constructor lets it
566 // through. Nothing on this path caught anything, so an unreadable 577 // through. Nothing on this path caught anything, so an unreadable
567 // blkindex.dat or wallet.dat -- one written by another platform's Berkeley 578 // 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)
1195 return fOk; 1195 return fOk;
1196} 1196}
1197 1197
1198static bool CreateSQLiteWalletWithFutureMinVersion(const string& strPath)
1199{
1200 string strError;
1201 CWalletDBSQLite db;
1202 CDataStream ssKey(SER_DISK);
1203 ssKey << string("walletminversion");
1204 CDataStream ssValue(SER_DISK);
1205 ssValue << (WALLET_FORMAT_SUPPORTED + 1);
1206
1207 if (!db.Open(strPath, strError))
1208 return false;
1209 if (!db.BeginTransaction(strError))
1210 return false;
1211 if (!db.WriteRecord(DataStreamBytes(ssKey), DataStreamBytes(ssValue), strError))
1212 return false;
1213 if (!db.CommitTransaction(strError))
1214 return false;
1215 if (!db.Checkpoint(strError))
1216 return false;
1217 return true;
1218}
1219
1198static int RunWalletSQLiteMigrationSelfTest() 1220static int RunWalletSQLiteMigrationSelfTest()
1199{ 1221{
1200 fflush(stdout); 1222 fflush(stdout);
@@ -1263,6 +1285,19 @@ static int RunWalletSQLiteMigrationSelfTest()
1263 FileContainsText(strVerifyOut, "verification: ok"), 1285 FileContainsText(strVerifyOut, "verification: ok"),
1264 "SQLite wallet export verifies against wallet.dat") ? 0 : 1; 1286 "SQLite wallet export verifies against wallet.dat") ? 0 : 1;
1265 1287
1288 string strLoadCheckOut = tmp + "/sqlite-loadcheck.txt";
1289 vector<string> vLoadCheckArgs;
1290 vLoadCheckArgs.push_back("-datadir=" + strWalletDir);
1291 vLoadCheckArgs.push_back("-nomanagedtor");
1292 vLoadCheckArgs.push_back("-nogui");
1293 vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strSQLite);
1294 int nLoadCheckRet =
1295 RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strLoadCheckOut);
1296 nFail += Check(nLoadCheckRet == 0 &&
1297 FileContainsText(strLoadCheckOut,
1298 "load check: ok"),
1299 "SQLite wallet export passes the loader compatibility check") ? 0 : 1;
1300
1266 string strRestoredDir = tmp + "/restored-wallet"; 1301 string strRestoredDir = tmp + "/restored-wallet";
1267 nFail += Check(MakeDirLocal(strRestoredDir), 1302 nFail += Check(MakeDirLocal(strRestoredDir),
1268 "restored wallet directory can be created") ? 0 : 1; 1303 "restored wallet directory can be created") ? 0 : 1;
@@ -1318,6 +1353,25 @@ static int RunWalletSQLiteMigrationSelfTest()
1318 !FileExists((strBadRestoreDir + "/wallet.dat").c_str()), 1353 !FileExists((strBadRestoreDir + "/wallet.dat").c_str()),
1319 "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1; 1354 "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1;
1320 1355
1356 string strFutureSQLite = tmp + "/future-wallet.sqlite";
1357 nFail += Check(CreateSQLiteWalletWithFutureMinVersion(strFutureSQLite),
1358 "SQLite export with a future wallet format can be created") ? 0 : 1;
1359 string strFutureLoadCheckOut = tmp + "/sqlite-loadcheck-future.txt";
1360 vector<string> vFutureLoadCheckArgs;
1361 vFutureLoadCheckArgs.push_back("-datadir=" + strWalletDir);
1362 vFutureLoadCheckArgs.push_back("-nomanagedtor");
1363 vFutureLoadCheckArgs.push_back("-nogui");
1364 vFutureLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strFutureSQLite);
1365 int nFutureLoadCheckRet =
1366 RunBitflashChild(strExe, vFutureLoadCheckArgs, NULL,
1367 &strFutureLoadCheckOut);
1368 nFail += Check(nFutureLoadCheckRet == 2 &&
1369 FileContainsText(strFutureLoadCheckOut,
1370 "unsupported future wallet format") &&
1371 FileContainsText(strFutureLoadCheckOut,
1372 "load check: failed"),
1373 "SQLite wallet load check rejects future wallet formats") ? 0 : 1;
1374
1321 string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; 1375 string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt";
1322 int nOverwriteRet = 1376 int nOverwriteRet =
1323 RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); 1377 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:
903 } 903 }
904}; 904};
905 905
906class CWalletSQLiteLoadCheckVisitor : public CWalletRecordVisitor
907{
908public:
909 int nRecords;
910 int nUnknown;
911 string strFailure;
912
913 CWalletSQLiteLoadCheckVisitor() : nRecords(0), nUnknown(0) { }
914
915 bool Fail(const string& strMessage, string& strErrorRet)
916 {
917 strFailure = strMessage;
918 strErrorRet = strMessage;
919 return false;
920 }
921
922 bool VisitWalletRecord(const CDataStream& ssKeyIn,
923 const CDataStream& ssValueIn,
924 string& strErrorRet)
925 {
926 nRecords++;
927 try
928 {
929 CDataStream ssKey = ssKeyIn;
930 CDataStream ssValue = ssValueIn;
931 string strType;
932 ssKey >> strType;
933 if (ssKey.fail())
934 return Fail("malformed wallet record key", strErrorRet);
935
936 if (strType == "name")
937 {
938 string strAddress;
939 string strName;
940 ssKey >> strAddress;
941 ssValue >> strName;
942 }
943 else if (strType == "tx")
944 {
945 uint256 hash;
946 CWalletTx wtx;
947 ssKey >> hash;
948 ssValue >> wtx;
949 }
950 else if (strType == "key")
951 {
952 vector<unsigned char> vchPubKey;
953 CPrivKey vchPrivKey;
954 ssKey >> vchPubKey;
955 ssValue >> vchPrivKey;
956 }
957 else if (strType == "mkey")
958 {
959 unsigned int nID = 0;
960 CWalletMasterKey kMasterKey;
961 ssKey >> nID;
962 ssValue >> kMasterKey;
963 }
964 else if (strType == "ckey")
965 {
966 vector<unsigned char> vchPubKey;
967 vector<unsigned char> vchCryptedSecret;
968 ssKey >> vchPubKey;
969 ssValue >> vchCryptedSecret;
970 }
971 else if (strType == "defaultkey" ||
972 strType == "hdmaster" ||
973 strType == "hdchaincode" ||
974 strType == "cryptedhdmaster" ||
975 strType == "cryptedhdchaincode")
976 {
977 vector<unsigned char> vch;
978 ssValue >> vch;
979 }
980 else if (strType == "hdnext" ||
981 strType == "hdreceivenext" ||
982 strType == "hdchangenext" ||
983 strType == "hdcointype")
984 {
985 unsigned int nValue = 0;
986 ssValue >> nValue;
987 }
988 else if (strType == "hdschema")
989 {
990 int nSchema = 0;
991 ssValue >> nSchema;
992 if (!ssValue.fail() &&
993 nSchema != HD_SCHEMA_NONE &&
994 nSchema != HD_SCHEMA_LEGACY &&
995 nSchema != HD_SCHEMA_BIP44)
996 return Fail("unsupported deterministic wallet schema",
997 strErrorRet);
998 }
999 else if (strType == "walletminversion")
1000 {
1001 int nMinVersion = 0;
1002 ssValue >> nMinVersion;
1003 if (!ssValue.fail() && nMinVersion > WALLET_FORMAT_SUPPORTED)
1004 return Fail("unsupported future wallet format", strErrorRet);
1005 }
1006 else if (strType == "pool")
1007 {
1008 int64 nIndex = 0;
1009 vector<unsigned char> vchPubKey;
1010 ssKey >> nIndex;
1011 ssValue >> vchPubKey;
1012 }
1013 else if (strType == "setting")
1014 {
1015 string strKey;
1016 ssKey >> strKey;
1017 if (strKey == "nTransactionFee")
1018 {
1019 int64 nValue = 0;
1020 ssValue >> nValue;
1021 }
1022 else if (strKey == "addrIncoming")
1023 {
1024 CAddress addr;
1025 ssValue >> addr;
1026 }
1027 else if (strKey == "nMineMode")
1028 {
1029 int nValue = 0;
1030 ssValue >> nValue;
1031 }
1032 else if (strKey == "fGenerateBitcoins")
1033 {
1034 int fValue = 0;
1035 ssValue >> fValue;
1036 }
1037 else if (strKey == "strParticipantPool")
1038 {
1039 string strValue;
1040 ssValue >> strValue;
1041 }
1042 }
1043 else if (strType == "version")
1044 {
1045 int nVersion = 0;
1046 ssValue >> nVersion;
1047 }
1048 else
1049 {
1050 nUnknown++;
1051 }
1052
1053 if (ssKey.fail())
1054 return Fail("malformed wallet record key", strErrorRet);
1055 if (ssValue.fail())
1056 return Fail("malformed wallet record value", strErrorRet);
1057 }
1058 catch (const std::exception& e)
1059 {
1060 return Fail(strprintf("wallet record parse exception: %s", e.what()),
1061 strErrorRet);
1062 }
1063 catch (...)
1064 {
1065 return Fail("wallet record parse exception", strErrorRet);
1066 }
1067 return true;
1068 }
1069};
1070
906} // namespace 1071} // namespace
907 1072
908int CmdWalletSQLiteExport(const std::string& strDest) 1073int CmdWalletSQLiteExport(const std::string& strDest)
@@ -1152,6 +1317,50 @@ int CmdWalletSQLiteRestore(const std::string& strPath)
1152 return 0; 1317 return 0;
1153} 1318}
1154 1319
1320int CmdWalletSQLiteLoadCheck(const std::string& strPath)
1321{
1322 AttachTerminal();
1323
1324 if (strPath.empty())
1325 {
1326 fprintf(stderr, "Missing SQLite wallet export path.\n");
1327 return 1;
1328 }
1329 if (!FileExists(strPath.c_str()))
1330 {
1331 fprintf(stderr, "SQLite wallet export does not exist: %s\n",
1332 strPath.c_str());
1333 return 1;
1334 }
1335
1336 string strError;
1337 CWalletDBSQLite sqlite;
1338 if (!sqlite.Open(strPath, strError))
1339 {
1340 fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str());
1341 return 1;
1342 }
1343
1344 CWalletSQLiteLoadCheckVisitor visitor;
1345 if (!sqlite.ScanRecords(visitor, strError))
1346 {
1347 printf("SQLite wallet load check\n");
1348 printf(" records checked: %d\n", visitor.nRecords);
1349 printf(" unknown records: %d\n", visitor.nUnknown);
1350 printf(" load check: failed\n");
1351 printf(" failure: %s\n", strError.c_str());
1352 fflush(stdout);
1353 return 2;
1354 }
1355
1356 printf("SQLite wallet load check\n");
1357 printf(" records checked: %d\n", visitor.nRecords);
1358 printf(" unknown records: %d\n", visitor.nUnknown);
1359 printf(" load check: ok\n");
1360 fflush(stdout);
1361 return 0;
1362}
1363
1155int CmdEncryptWallet(const std::string& strPassphrase) 1364int CmdEncryptWallet(const std::string& strPassphrase)
1156{ 1365{
1157 AttachTerminal(); 1366 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);
83// empty data directory. Refuses to overwrite an existing wallet.dat. 83// empty data directory. Refuses to overwrite an existing wallet.dat.
84int CmdWalletSQLiteRestore(const std::string& strPath); 84int CmdWalletSQLiteRestore(const std::string& strPath);
85 85
86// Diagnostic/migration staging: parse a SQLite wallet export as the runtime
87// wallet loader would. Prints counts only, never record keys or values.
88int CmdWalletSQLiteLoadCheck(const std::string& strPath);
89
86// Rewrite wallet.dat so private keys and the HD seed are encrypted with the 90// Rewrite wallet.dat so private keys and the HD seed are encrypted with the
87// given passphrase. Returns 0 on success and exits without starting the node. 91// given passphrase. Returns 0 on success and exits without starting the node.
88int CmdEncryptWallet(const std::string& strPassphrase); 92int CmdEncryptWallet(const std::string& strPassphrase);