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 15:56:12 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-14 15:56:12 -0300
commit59b9d11015f0e3a2475d0750fc534583c2ca9f77 (patch)
tree0cf369cd3e1c59eca068cc05561dd59c21bf7b49
parentc08a32d26cabf5acc9dabf298a22051596308174 (diff)
wallet: export BDB records to SQLite
-rw-r--r--docs/storage.md6
-rw-r--r--src/Makefile1
-rw-r--r--src/main_gui.cpp15
-rw-r--r--src/makefile.mingw1
-rw-r--r--src/selftest.cpp148
-rw-r--r--src/wallet_sqlite.cpp10
-rw-r--r--src/wallet_sqlite.h1
-rw-r--r--src/walletcmd.cpp97
-rw-r--r--src/walletcmd.h5
9 files changed, 281 insertions, 3 deletions
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,
104raw record write/read, close/reopen behavior, and value preservation before any 104raw record write/read, close/reopen behavior, and value preservation before any
105BDB-to-SQLite migration or runtime backend flag exists. 105BDB-to-SQLite migration or runtime backend flag exists.
106 106
107`-walletsqliteexport=FILE` is the next staging tool. It streams every raw
108`wallet.dat` record through `ScanWalletRecords()` and writes those same key and
109value bytes into the SQLite table inside one transaction. It refuses to
110overwrite an existing export and still does not change the runtime wallet
111backend.
112
107## `blkindex.dat` 113## `blkindex.dat`
108 114
109`blkindex.dat` is accessed through `CTxDB`. 115`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
189 ./bitflash-node -selftest=wallet-hd 189 ./bitflash-node -selftest=wallet-hd
190 ./bitflash-node -selftest=wallet-format 190 ./bitflash-node -selftest=wallet-format
191 ./bitflash-node -selftest=wallet-sqlite 191 ./bitflash-node -selftest=wallet-sqlite
192 ./bitflash-node -selftest=wallet-sqlite-migration
192 ./bitflash-node -selftest=wallet-crypto 193 ./bitflash-node -selftest=wallet-crypto
193 ./bitflash-node -selftest=wallet-encrypt 194 ./bitflash-node -selftest=wallet-encrypt
194 ./bitflash-node -selftest=net-message 195 ./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()
114 printf(" /gen\n"); 114 printf(" /gen\n");
115 printf(" /nogui or /daemon\n"); 115 printf(" /nogui or /daemon\n");
116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n"); 116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n");
117 printf(" db-env-reopen, wallet-sqlite, wallet-crypto, wallet-encrypt,\n"); 117 printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n");
118 printf(" wallet-portability,\n"); 118 printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n");
119 printf(" net-message,\n"); 119 printf(" net-message,\n");
120 printf(" consensus-limits, pool-stratum,\n"); 120 printf(" consensus-limits, pool-stratum,\n");
121 printf(" parse-money, socks5-proxy, or managed-tor\n"); 121 printf(" parse-money, socks5-proxy, or managed-tor\n");
@@ -181,6 +181,8 @@ static void PrintUsage()
181 printf(" (write the same audit as deterministic JSON)\n"); 181 printf(" (write the same audit as deterministic JSON)\n");
182 printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n"); 182 printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n");
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");
185 printf(" experimental SQLite store, then exit)\n");
184 printf(" /rescan (walk the chain for coins this wallet owns\n"); 186 printf(" /rescan (walk the chain for coins this wallet owns\n");
185 printf(" but never recorded, then exit)\n"); 187 printf(" but never recorded, then exit)\n");
186 printf("\n"); 188 printf("\n");
@@ -529,6 +531,15 @@ int main(int argc, char* argv[])
529 return nRet; 531 return nRet;
530 } 532 }
531 533
534 string strWalletSQLiteExport =
535 argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport");
536 if (!strWalletSQLiteExport.empty())
537 {
538 int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport);
539 DBFlush(true);
540 return nRet;
541 }
542
532 // Berkeley DB reports failure by throwing, and CDB's constructor lets it 543 // Berkeley DB reports failure by throwing, and CDB's constructor lets it
533 // through. Nothing on this path caught anything, so an unreadable 544 // through. Nothing on this path caught anything, so an unreadable
534 // blkindex.dat or wallet.dat -- one written by another platform's Berkeley 545 // 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
109 ./bitflash.exe -selftest=wallet-hd 109 ./bitflash.exe -selftest=wallet-hd
110 ./bitflash.exe -selftest=wallet-format 110 ./bitflash.exe -selftest=wallet-format
111 ./bitflash.exe -selftest=wallet-sqlite 111 ./bitflash.exe -selftest=wallet-sqlite
112 ./bitflash.exe -selftest=wallet-sqlite-migration
112 ./bitflash.exe -selftest=wallet-crypto 113 ./bitflash.exe -selftest=wallet-crypto
113 ./bitflash.exe -selftest=wallet-encrypt 114 ./bitflash.exe -selftest=wallet-encrypt
114 ./bitflash.exe -selftest=net-message 115 ./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()
1134 return nFail == 0 ? 0 : 1; 1134 return nFail == 0 ? 0 : 1;
1135} 1135}
1136 1136
1137class CWalletRecordMapVisitor : public CWalletRecordVisitor
1138{
1139public:
1140 std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords;
1141
1142 explicit CWalletRecordMapVisitor(
1143 std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn)
1144 : mapRecords(mapRecordsIn) { }
1145
1146 bool VisitWalletRecord(const CDataStream& ssKey,
1147 const CDataStream& ssValue,
1148 string& strErrorRet)
1149 {
1150 vector<unsigned char> vchKey = DataStreamBytes(ssKey);
1151 if (mapRecords.count(vchKey))
1152 {
1153 strErrorRet = "duplicate serialized wallet key";
1154 return false;
1155 }
1156 mapRecords[vchKey] = DataStreamBytes(ssValue);
1157 return true;
1158 }
1159};
1160
1161static int RunWalletSQLiteMigrationSelfTest()
1162{
1163 fflush(stdout);
1164 printf("wallet-sqlite-migration self-test\n");
1165
1166 std::string tmp;
1167 std::string cwd;
1168 if (!MakeTempDir(tmp))
1169 {
1170 printf(" FAIL could not create a temporary data directory\n");
1171 return 1;
1172 }
1173 if (!GetCurrentDir(cwd))
1174 {
1175 printf(" FAIL could not read current directory\n");
1176 RemoveTree(tmp);
1177 return 1;
1178 }
1179
1180 int nFail = 0;
1181 printf(" temp root: %s\n", tmp.c_str());
1182
1183 try
1184 {
1185#ifdef _WIN32
1186 string strExe = cwd + "\\bitflash.exe";
1187#else
1188 string strExe = cwd + "/bitflash-node";
1189#endif
1190 string strWalletDir = tmp + "/wallet";
1191 nFail += Check(MakeDirLocal(strWalletDir),
1192 "temporary wallet directory can be created") ? 0 : 1;
1193
1194 string strCreateOut = tmp + "/create.txt";
1195 vector<string> vCreateArgs;
1196 vCreateArgs.push_back("-datadir=" + strWalletDir);
1197 vCreateArgs.push_back("-nomanagedtor");
1198 vCreateArgs.push_back("-nogui");
1199 vCreateArgs.push_back("-newphrase");
1200 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
1201 nFail += Check(nCreateRet == 0 &&
1202 FileContainsText(strCreateOut, "Write these twelve words down"),
1203 "a phrase wallet can be created for SQLite export") ? 0 : 1;
1204
1205 string strSQLite = tmp + "/wallet.sqlite";
1206 string strExportOut = tmp + "/sqlite-export.txt";
1207 vector<string> vExportArgs;
1208 vExportArgs.push_back("-datadir=" + strWalletDir);
1209 vExportArgs.push_back("-nomanagedtor");
1210 vExportArgs.push_back("-nogui");
1211 vExportArgs.push_back("-walletsqliteexport=" + strSQLite);
1212 int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut);
1213 nFail += Check(nExportRet == 0 &&
1214 FileExists(strSQLite.c_str()) &&
1215 FileContainsText(strExportOut, "SQLite wallet export written"),
1216 "wallet.dat can be exported to SQLite") ? 0 : 1;
1217
1218 string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt";
1219 int nOverwriteRet =
1220 RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut);
1221 nFail += Check(nOverwriteRet != 0 &&
1222 FileContainsText(strOverwriteOut,
1223 "Refusing to overwrite existing SQLite wallet export"),
1224 "SQLite wallet export refuses to overwrite") ? 0 : 1;
1225 remove((strSQLite + "-wal").c_str());
1226 remove((strSQLite + "-shm").c_str());
1227
1228 std::map<vector<unsigned char>, vector<unsigned char> > mapBDB;
1229 string strOldDataDir = strSetDataDir;
1230 string strScanError;
1231 strSetDataDir = strWalletDir;
1232 CWalletRecordMapVisitor visitor(mapBDB);
1233 bool fScanOk = ScanWalletRecords(visitor, strScanError);
1234 DBFlush(true);
1235 strSetDataDir = strOldDataDir;
1236 nFail += Check(fScanOk && !mapBDB.empty(),
1237 "wallet.dat records can be scanned for comparison") ? 0 : 1;
1238
1239 CWalletDBSQLite db;
1240 string strError;
1241 nFail += Check(db.Open(strSQLite, strError),
1242 "exported SQLite wallet opens") ? 0 : 1;
1243 int nSQLiteRecords = 0;
1244 nFail += Check(db.CountRecords(nSQLiteRecords, strError) &&
1245 nSQLiteRecords == (int)mapBDB.size(),
1246 "SQLite export has the same record count as wallet.dat") ? 0 : 1;
1247
1248 bool fAllMatch = true;
1249 for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it =
1250 mapBDB.begin(); it != mapBDB.end(); ++it)
1251 {
1252 vector<unsigned char> vchSQLiteValue;
1253 if (!db.ReadRecord(it->first, vchSQLiteValue, strError) ||
1254 vchSQLiteValue != it->second)
1255 {
1256 fAllMatch = false;
1257 break;
1258 }
1259 }
1260 nFail += Check(fAllMatch,
1261 "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1;
1262 }
1263 catch (const std::exception& e)
1264 {
1265 printf(" FAIL exception: %s\n", e.what());
1266 nFail++;
1267 }
1268 catch (...)
1269 {
1270 printf(" FAIL unknown exception\n");
1271 nFail++;
1272 }
1273
1274 RemoveTree(tmp);
1275 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1276 nFail, nFail == 1 ? "" : "s");
1277 fflush(stdout);
1278 return nFail == 0 ? 0 : 1;
1279}
1280
1137static void ClearWalletRuntimeForTest() 1281static void ClearWalletRuntimeForTest()
1138{ 1282{
1139 CRITICAL_BLOCK(cs_mapKeys) 1283 CRITICAL_BLOCK(cs_mapKeys)
@@ -2395,6 +2539,8 @@ int RunSelfTest(const std::string& name)
2395 return RunDbEnvReopenSelfTest(); 2539 return RunDbEnvReopenSelfTest();
2396 if (name == "wallet-sqlite") 2540 if (name == "wallet-sqlite")
2397 return RunWalletSQLiteSelfTest(); 2541 return RunWalletSQLiteSelfTest();
2542 if (name == "wallet-sqlite-migration")
2543 return RunWalletSQLiteMigrationSelfTest();
2398 if (name == "wallet-crypto") 2544 if (name == "wallet-crypto")
2399 return RunWalletCryptoSelfTest(); 2545 return RunWalletCryptoSelfTest();
2400 if (name == "wallet-encrypt") 2546 if (name == "wallet-encrypt")
@@ -2415,6 +2561,6 @@ int RunSelfTest(const std::string& name)
2415 return RunManagedTorSelfTest(); 2561 return RunManagedTorSelfTest();
2416 2562
2417 printf("Unknown self-test '%s'\n", name.c_str()); 2563 printf("Unknown self-test '%s'\n", name.c_str());
2418 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"); 2564 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");
2419 return 1; 2565 return 1;
2420} 2566}
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)
97 return Exec("ROLLBACK;", strError); 97 return Exec("ROLLBACK;", strError);
98} 98}
99 99
100bool CWalletDBSQLite::Checkpoint(string& strError)
101{
102 if (!pdb)
103 {
104 strError = "SQLite wallet is not open";
105 return false;
106 }
107 return Exec("PRAGMA wal_checkpoint(TRUNCATE);", strError);
108}
109
100bool CWalletDBSQLite::Exec(const char* pszSql, string& strError) 110bool CWalletDBSQLite::Exec(const char* pszSql, string& strError)
101{ 111{
102 char* pszErr = NULL; 112 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:
18 bool BeginTransaction(string& strError); 18 bool BeginTransaction(string& strError);
19 bool CommitTransaction(string& strError); 19 bool CommitTransaction(string& strError);
20 bool RollbackTransaction(string& strError); 20 bool RollbackTransaction(string& strError);
21 bool Checkpoint(string& strError);
21 22
22 bool WriteRecord(const vector<unsigned char>& vchKey, 23 bool WriteRecord(const vector<unsigned char>& vchKey,
23 const vector<unsigned char>& vchValue, 24 const vector<unsigned char>& 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 @@
12#include "headers_core.h" 12#include "headers_core.h"
13#include "bip32.h" 13#include "bip32.h"
14#include "walletcmd.h" 14#include "walletcmd.h"
15#include "wallet_sqlite.h"
15 16
16#undef printf 17#undef printf
17 18
@@ -790,6 +791,102 @@ int CmdWalletStorageAudit(const std::string& strJsonOut)
790 return 0; 791 return 0;
791} 792}
792 793
794static vector<unsigned char> DataStreamBytesLocal(const CDataStream& ss)
795{
796 return vector<unsigned char>(ss.begin(), ss.end());
797}
798
799class CWalletSQLiteExportVisitor : public CWalletRecordVisitor
800{
801public:
802 CWalletDBSQLite& db;
803 int& nCopied;
804
805 CWalletSQLiteExportVisitor(CWalletDBSQLite& dbIn, int& nCopiedIn)
806 : db(dbIn), nCopied(nCopiedIn) { }
807
808 bool VisitWalletRecord(const CDataStream& ssKey,
809 const CDataStream& ssValue,
810 string& strErrorRet)
811 {
812 if (!db.WriteRecord(DataStreamBytesLocal(ssKey),
813 DataStreamBytesLocal(ssValue),
814 strErrorRet))
815 return false;
816 nCopied++;
817 return true;
818 }
819};
820
821int CmdWalletSQLiteExport(const std::string& strDest)
822{
823 AttachTerminal();
824
825 if (strDest.empty())
826 {
827 fprintf(stderr, "Missing SQLite wallet export path.\n");
828 return 1;
829 }
830 if (FileExists(strDest.c_str()))
831 {
832 fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n",
833 strDest.c_str());
834 return 1;
835 }
836
837 string strError;
838 CWalletDBSQLite db;
839 if (!db.Open(strDest, strError))
840 {
841 fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str());
842 return 1;
843 }
844 if (!db.BeginTransaction(strError))
845 {
846 fprintf(stderr, "Cannot begin SQLite wallet export: %s\n", strError.c_str());
847 return 1;
848 }
849
850 int nCopied = 0;
851 CWalletSQLiteExportVisitor visitor(db, nCopied);
852 if (!ScanWalletRecords(visitor, strError))
853 {
854 string strRollbackError;
855 db.RollbackTransaction(strRollbackError);
856 fprintf(stderr, "Cannot export wallet.dat to SQLite: %s\n", strError.c_str());
857 return 1;
858 }
859 if (!db.CommitTransaction(strError))
860 {
861 fprintf(stderr, "Cannot commit SQLite wallet export: %s\n", strError.c_str());
862 return 1;
863 }
864 if (!db.Checkpoint(strError))
865 {
866 fprintf(stderr, "Cannot checkpoint SQLite wallet export: %s\n", strError.c_str());
867 return 1;
868 }
869
870 int nCount = 0;
871 if (!db.CountRecords(nCount, strError))
872 {
873 fprintf(stderr, "Cannot verify SQLite wallet export count: %s\n",
874 strError.c_str());
875 return 1;
876 }
877 if (nCount != nCopied)
878 {
879 fprintf(stderr, "SQLite wallet export count mismatch: copied %d, stored %d\n",
880 nCopied, nCount);
881 return 1;
882 }
883
884 printf("SQLite wallet export written to %s\n", strDest.c_str());
885 printf(" records copied: %d\n", nCopied);
886 fflush(stdout);
887 return 0;
888}
889
793int CmdEncryptWallet(const std::string& strPassphrase) 890int CmdEncryptWallet(const std::string& strPassphrase)
794{ 891{
795 AttachTerminal(); 892 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 = "");
70// or migration-unsafe mix of storage records. 70// or migration-unsafe mix of storage records.
71int CmdWalletStorageCheck(); 71int CmdWalletStorageCheck();
72 72
73// Diagnostic/migration staging: copy every raw wallet.dat record into an
74// experimental SQLite key/value store, preserving serialized bytes. This does
75// not change which backend the runtime wallet opens.
76int CmdWalletSQLiteExport(const std::string& strDest);
77
73// Rewrite wallet.dat so private keys and the HD seed are encrypted with the 78// Rewrite wallet.dat so private keys and the HD seed are encrypted with the
74// given passphrase. Returns 0 on success and exits without starting the node. 79// given passphrase. Returns 0 on success and exits without starting the node.
75int CmdEncryptWallet(const std::string& strPassphrase); 80int CmdEncryptWallet(const std::string& strPassphrase);