upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-03 22:33:20 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-03 22:33:20 +0000
commitb810d86752ddb2674118f737582c924bb9eb74c1 (patch)
tree1ceadf421609a2e58da81103165a2fb7085387de /src
parentdb44eac5d347c229539946e8d61f1ff0d1f68001 (diff)
parent5ef59833756da6f47274cb990eb874fb0ed99019 (diff)
merge: upstream 1.2.20 into deploy/all-2026-08-24
# Conflicts: # Makefile
Diffstat (limited to 'src')
-rw-r--r--src/Makefile11
-rw-r--r--src/db.cpp1251
-rw-r--r--src/db.h183
-rw-r--r--src/gui.cpp239
-rw-r--r--src/headers_core.h5
-rw-r--r--src/main.cpp3
-rw-r--r--src/main.h2
-rw-r--r--src/main_gui.cpp335
-rw-r--r--src/makefile.mingw12
-rw-r--r--src/net.cpp29
-rw-r--r--src/net.h17
-rw-r--r--src/selftest.cpp1926
-rw-r--r--src/selftest.h1
-rw-r--r--src/wallet_sqlite.cpp374
-rw-r--r--src/wallet_sqlite.h120
-rw-r--r--src/walletcmd.cpp975
-rw-r--r--src/walletcmd.h34
17 files changed, 5343 insertions, 174 deletions
diff --git a/src/Makefile b/src/Makefile
index 2c6d304..b931007 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -42,7 +42,7 @@ FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined
42 42
43LIBS = -L$(SECP_LIB) -lsecp256k1 \ 43LIBS = -L$(SECP_LIB) -lsecp256k1 \
44 -L$(RANDOMX_DIR)/build -lrandomx \ 44 -L$(RANDOMX_DIR)/build -lrandomx \
45 -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ 45 -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \
46 -lglfw -lGL 46 -lglfw -lGL
47 47
48# Every header a .cpp here includes has to be listed, not just the interesting 48# Every header a .cpp here includes has to be listed, not just the interesting
@@ -55,7 +55,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
55HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 55HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
56 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 56 key.h bignum.h script.h db.h base58.h net.h nostr.h \
57 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 57 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
58 selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h 58 selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h
59 59
60IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 60IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
61 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 61 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
@@ -63,7 +63,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
63NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 63NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
64 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 64 obj/sha.o obj/nostr.o obj/randomx_pow.o \
65 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 65 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
66 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o 66 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \
67 obj/proxy.o obj/tor.o
67 68
68GUI_OBJS = obj/gui.o obj/main_gui.o 69GUI_OBJS = obj/gui.o obj/main_gui.o
69 70
@@ -132,7 +133,7 @@ LDHARDFLAGS = -Wl,-z,noexecstack
132 133
133NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ 134NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \
134 -L$(RANDOMX_DIR)/build -lrandomx \ 135 -L$(RANDOMX_DIR)/build -lrandomx \
135 -lsodium -lssl -lcrypto -ldb_cxx -lpthread 136 -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread
136 137
137NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ 138NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \
138 obj-node/main_gui.o obj-node/headless.o 139 obj-node/main_gui.o obj-node/headless.o
@@ -203,6 +204,8 @@ tests: test_bip32 bitflash-node
203 ./bitflash-node -selftest=wallet-keypool 204 ./bitflash-node -selftest=wallet-keypool
204 ./bitflash-node -selftest=wallet-hd 205 ./bitflash-node -selftest=wallet-hd
205 ./bitflash-node -selftest=wallet-format 206 ./bitflash-node -selftest=wallet-format
207 ./bitflash-node -selftest=wallet-sqlite
208 ./bitflash-node -selftest=wallet-sqlite-migration
206 ./bitflash-node -selftest=wallet-crypto 209 ./bitflash-node -selftest=wallet-crypto
207 ./bitflash-node -selftest=wallet-encrypt 210 ./bitflash-node -selftest=wallet-encrypt
208 ./bitflash-node -selftest=net-message 211 ./bitflash-node -selftest=net-message
diff --git a/src/db.cpp b/src/db.cpp
index 405a3db..bf1958d 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -3,6 +3,7 @@
3// file license.txt or http://www.opensource.org/licenses/mit-license.php. 3// file license.txt or http://www.opensource.org/licenses/mit-license.php.
4 4
5#include "headers.h" 5#include "headers.h"
6#include "wallet_sqlite.h"
6 7
7 8
8 9
@@ -52,8 +53,34 @@ static bool fDbEnvInit = false;
52// Why LoadWallet() refused, in words meant for the person who has to act on it. 53// Why LoadWallet() refused, in words meant for the person who has to act on it.
53// Empty unless the reason is one we can name. 54// Empty unless the reason is one we can name.
54string strWalletLoadError; 55string strWalletLoadError;
55DbEnv dbenv(0u); 56static DbEnv* pdbenv = NULL;
57static FILE* pdbErrFile = NULL;
56static map<string, int> mapFileUseCount; 58static map<string, int> mapFileUseCount;
59static CWalletDBSQLite* pWalletSQLiteRuntime = NULL;
60
61DbEnv& GetDbEnv()
62{
63 if (!pdbenv)
64 pdbenv = new DbEnv(0u);
65 return *pdbenv;
66}
67
68static void CloseDbEnv()
69{
70 if (pdbenv)
71 {
72 try { pdbenv->close(0); }
73 catch (...) { }
74 delete pdbenv;
75 pdbenv = NULL;
76 }
77 if (pdbErrFile)
78 {
79 fclose(pdbErrFile);
80 pdbErrFile = NULL;
81 }
82 fDbEnvInit = false;
83}
57 84
58class CDBInit 85class CDBInit
59{ 86{
@@ -64,15 +91,189 @@ public:
64 ~CDBInit() 91 ~CDBInit()
65 { 92 {
66 if (fDbEnvInit) 93 if (fDbEnvInit)
67 { 94 CloseDbEnv();
68 try { dbenv.close(0); }
69 catch (...) { }
70 fDbEnvInit = false;
71 }
72 } 95 }
73} 96}
74instance_of_cdbinit; 97instance_of_cdbinit;
75 98
99bool WalletSQLiteRuntimeActive()
100{
101 return pWalletSQLiteRuntime != NULL;
102}
103
104bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist)
105{
106 strErrorRet.clear();
107 // Callers that load an existing wallet want the clear "does not exist"
108 // message rather than silently creating a blank one; the create path passes
109 // fMustExist=false so Open() (SQLITE_OPEN_CREATE) can make a fresh file.
110 if (fMustExist && !FileExists(strPath.c_str()))
111 {
112 strErrorRet = strprintf("SQLite wallet does not exist: %s",
113 strPath.c_str());
114 return false;
115 }
116
117 WalletSQLiteRuntimeClose();
118 std::auto_ptr<CWalletDBSQLite> db(new CWalletDBSQLite());
119 if (!db->Open(strPath, strErrorRet))
120 return false;
121
122 pWalletSQLiteRuntime = db.release();
123 return true;
124}
125
126void WalletSQLiteRuntimeClose()
127{
128 if (pWalletSQLiteRuntime)
129 {
130 // Fold the write-ahead log into the main file on the way out. WAL +
131 // synchronous=FULL already makes every committed write durable across a
132 // kill, so this is housekeeping, not the thing that protects the data:
133 // if it fails, or the node is killed before it runs, the wallet is
134 // still whole. Best effort, never throws.
135 string strCheckpointError;
136 pWalletSQLiteRuntime->Checkpoint(strCheckpointError);
137 delete pWalletSQLiteRuntime;
138 pWalletSQLiteRuntime = NULL;
139 }
140}
141
142bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey,
143 vector<unsigned char>& vchValueRet)
144{
145 if (!pWalletSQLiteRuntime)
146 return false;
147 string strError;
148 return pWalletSQLiteRuntime->ReadRecord(vchKey, vchValueRet, strError);
149}
150
151bool WalletSQLiteRuntimeWriteRecord(const vector<unsigned char>& vchKey,
152 const vector<unsigned char>& vchValue,
153 bool fOverwrite)
154{
155 if (!pWalletSQLiteRuntime)
156 return false;
157 string strError;
158 return pWalletSQLiteRuntime->WriteRecord(vchKey, vchValue, strError,
159 fOverwrite);
160}
161
162bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey)
163{
164 if (!pWalletSQLiteRuntime)
165 return false;
166 string strError;
167 return pWalletSQLiteRuntime->EraseRecord(vchKey, strError);
168}
169
170// Transaction bracket for the SQLite runtime. When the SQLite backend is not
171// active these are no-ops that succeed, so a caller can wrap a group of related
172// writes and Berkeley DB keeps its existing per-write behaviour unchanged while
173// SQLite gets all-or-nothing.
174bool WalletSQLiteRuntimeBeginTxn()
175{
176 if (!pWalletSQLiteRuntime)
177 return true;
178 string strError;
179 if (pWalletSQLiteRuntime->BeginTransaction(strError))
180 return true;
181 printf("WalletSQLiteRuntimeBeginTxn() : %s\n", strError.c_str());
182 return false;
183}
184
185bool WalletSQLiteRuntimeCommitTxn()
186{
187 if (!pWalletSQLiteRuntime)
188 return true;
189 string strError;
190 if (pWalletSQLiteRuntime->CommitTransaction(strError))
191 return true;
192 printf("WalletSQLiteRuntimeCommitTxn() : %s\n", strError.c_str());
193 return false;
194}
195
196void WalletSQLiteRuntimeRollbackTxn()
197{
198 if (!pWalletSQLiteRuntime)
199 return;
200 string strError;
201 pWalletSQLiteRuntime->RollbackTransaction(strError);
202}
203
204// Open the SQLite wallet read-only as the active runtime. Used by the one-shot
205// storage diagnostics, which run before the normal wallet load and must be able
206// to inspect a wallet that the loader would refuse -- so this opens the file for
207// scanning without loading or validating its records.
208bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet)
209{
210 strErrorRet.clear();
211 if (!FileExists(strPath.c_str()))
212 {
213 strErrorRet = strprintf("SQLite wallet does not exist: %s", strPath.c_str());
214 return false;
215 }
216
217 WalletSQLiteRuntimeClose();
218 std::auto_ptr<CWalletDBSQLite> db(new CWalletDBSQLite());
219 if (!db->OpenReadOnly(strPath, strErrorRet))
220 return false;
221
222 pWalletSQLiteRuntime = db.release();
223 return true;
224}
225
226bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet)
227{
228 if (!pWalletSQLiteRuntime)
229 {
230 strErrorRet = "SQLite wallet backend is not active";
231 return false;
232 }
233 return pWalletSQLiteRuntime->Checkpoint(strErrorRet);
234}
235
236// The persistent record of which backend this datadir uses, so the choice
237// survives a restart without a command-line flag. A plain text file holding
238// "sqlite" or "bdb" -- visible and hand-editable on purpose, no hidden state.
239static string WalletBackendMarkerPath()
240{
241 return GetAppDir() + "/wallet-backend";
242}
243
244string ReadWalletBackendMarker()
245{
246 FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "rb");
247 if (!pf)
248 return "";
249 char buf[16] = {0};
250 size_t n = fread(buf, 1, sizeof(buf) - 1, pf);
251 fclose(pf);
252 string s(buf, n);
253 while (!s.empty() && isspace((unsigned char)s[s.size() - 1]))
254 s.erase(s.size() - 1);
255 while (!s.empty() && isspace((unsigned char)s[0]))
256 s.erase(0, 1);
257 if (s == "sqlite" || s == "bdb")
258 return s;
259 return "";
260}
261
262bool WriteWalletBackendMarker(const string& strBackend)
263{
264 if (strBackend != "sqlite" && strBackend != "bdb")
265 return false;
266 FILE* pf = fopen(WalletBackendMarkerPath().c_str(), "wb");
267 if (!pf)
268 return false;
269 bool fOk = fwrite(strBackend.c_str(), 1, strBackend.size(), pf) == strBackend.size();
270 if (fputc('\n', pf) == EOF)
271 fOk = false;
272 if (fclose(pf) != 0)
273 fOk = false;
274 return fOk;
275}
276
76 277
77CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) 278CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
78{ 279{
@@ -104,24 +305,30 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
104 305
105 printf("dbenv.open strAppDir=%s\n", strAppDir.c_str()); 306 printf("dbenv.open strAppDir=%s\n", strAppDir.c_str());
106 307
107 dbenv.set_lg_dir(strLogDir.c_str()); 308 DbEnv& env = GetDbEnv();
108 dbenv.set_lg_max(10000000); 309 env.set_lg_dir(strLogDir.c_str());
109 dbenv.set_lk_max_locks(10000); 310 env.set_lg_max(10000000);
110 dbenv.set_lk_max_objects(10000); 311 env.set_lk_max_locks(10000);
111 dbenv.set_errfile(fopen("db.log", "a")); /// debug 312 env.set_lk_max_objects(10000);
112 ///dbenv.log_set_config(DB_LOG_AUTO_REMOVE, 1); /// causes corruption 313 pdbErrFile = fopen("db.log", "a"); /// debug
113 ret = dbenv.open(strAppDir.c_str(), 314 if (pdbErrFile)
114 DB_CREATE | 315 env.set_errfile(pdbErrFile);
115 DB_INIT_LOCK | 316 ///env.log_set_config(DB_LOG_AUTO_REMOVE, 1); /// causes corruption
116 DB_INIT_LOG | 317 ret = env.open(strAppDir.c_str(),
117 DB_INIT_MPOOL | 318 DB_CREATE |
118 DB_INIT_TXN | 319 DB_INIT_LOCK |
119 DB_THREAD | 320 DB_INIT_LOG |
120 DB_PRIVATE | 321 DB_INIT_MPOOL |
121 DB_RECOVER, 322 DB_INIT_TXN |
122 0); 323 DB_THREAD |
324 DB_PRIVATE |
325 DB_RECOVER,
326 0);
123 if (ret > 0) 327 if (ret > 0)
328 {
329 CloseDbEnv();
124 throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret)); 330 throw runtime_error(strprintf("CDB() : error %d opening database environment\n", ret));
331 }
125 fDbEnvInit = true; 332 fDbEnvInit = true;
126 333
127 // A Berkeley DB file records, in every page header, a log sequence 334 // A Berkeley DB file records, in every page header, a log sequence
@@ -151,7 +358,7 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
151 continue; 358 continue;
152 try 359 try
153 { 360 {
154 if (dbenv.lsn_reset(pszName, 0) == 0) 361 if (env.lsn_reset(pszName, 0) == 0)
155 printf("CDB() : adopted %s from another environment " 362 printf("CDB() : adopted %s from another environment "
156 "(log sequence numbers reset)\n", pszName); 363 "(log sequence numbers reset)\n", pszName);
157 } 364 }
@@ -164,7 +371,7 @@ CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL)
164 ++mapFileUseCount[strFile]; 371 ++mapFileUseCount[strFile];
165 } 372 }
166 373
167 pdb = new Db(&dbenv, 0); 374 pdb = new Db(&GetDbEnv(), 0);
168 375
169 ret = pdb->open(NULL, // Txn pointer 376 ret = pdb->open(NULL, // Txn pointer
170 pszFile, // Filename 377 pszFile, // Filename
@@ -199,7 +406,8 @@ void CDB::Close()
199 try { pdb->close(0); } catch (...) { } 406 try { pdb->close(0); } catch (...) { }
200 delete pdb; 407 delete pdb;
201 pdb = NULL; 408 pdb = NULL;
202 try { dbenv.txn_checkpoint(0, 0, 0); } catch (...) { } 409 if (fDbEnvInit)
410 try { GetDbEnv().txn_checkpoint(0, 0, 0); } catch (...) { }
203 411
204 CRITICAL_BLOCK(cs_db) 412 CRITICAL_BLOCK(cs_db)
205 --mapFileUseCount[strFile]; 413 --mapFileUseCount[strFile];
@@ -279,7 +487,8 @@ void DBFlush(bool fShutdown)
279 // takes the process down before the wallet has been put down cleanly, 487 // takes the process down before the wallet has been put down cleanly,
280 // which is the difference between a portable wallet.dat and one welded 488 // which is the difference between a portable wallet.dat and one welded
281 // to this directory -- the lsn_reset just below is what frees it. 489 // to this directory -- the lsn_reset just below is what frees it.
282 try { dbenv.txn_checkpoint(0, 0, 0); } 490 DbEnv& env = GetDbEnv();
491 try { env.txn_checkpoint(0, 0, 0); }
283 catch (const std::exception& e) 492 catch (const std::exception& e)
284 { printf("DBFlush() : checkpoint failed: %s\n", e.what()); } 493 { printf("DBFlush() : checkpoint failed: %s\n", e.what()); }
285 494
@@ -290,7 +499,7 @@ void DBFlush(bool fShutdown)
290 int nRefCount = (*mi).second; 499 int nRefCount = (*mi).second;
291 if (nRefCount == 0) 500 if (nRefCount == 0)
292 { 501 {
293 try { dbenv.lsn_reset(strFile.c_str(), 0); } 502 try { env.lsn_reset(strFile.c_str(), 0); }
294 catch (const std::exception& e) 503 catch (const std::exception& e)
295 { printf("DBFlush() : lsn_reset(%s) failed: %s\n", strFile.c_str(), e.what()); } 504 { printf("DBFlush() : lsn_reset(%s) failed: %s\n", strFile.c_str(), e.what()); }
296 mapFileUseCount.erase(mi++); 505 mapFileUseCount.erase(mi++);
@@ -310,9 +519,8 @@ void DBFlush(bool fShutdown)
310 // Found while making wallet encryption clean up after itself: 519 // Found while making wallet encryption clean up after itself:
311 // calling DBFlush(true) a second time crashed here every time. 520 // calling DBFlush(true) a second time crashed here every time.
312 if (mapFileUseCount.empty()) 521 if (mapFileUseCount.empty())
313 try { dbenv.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { } 522 try { env.log_archive(NULL, DB_ARCH_REMOVE); } catch (...) { }
314 try { dbenv.close(0); } catch (...) { } 523 CloseDbEnv();
315 fDbEnvInit = false;
316 } 524 }
317 } 525 }
318} 526}
@@ -614,6 +822,294 @@ bool CTxDB::LoadBlockIndex()
614// CWalletDB 822// CWalletDB
615// 823//
616 824
825CWalletDB::CWalletDB(const char* pszMode, bool fTxn) :
826 CDB(WalletSQLiteRuntimeActive() ? NULL : "wallet.dat", pszMode, fTxn)
827{
828}
829
830class CSQLiteWalletRuntimeLoadVisitor : public CWalletRecordVisitor
831{
832public:
833 bool fHaveStoredMineMode;
834 bool fHaveStoredHDCoinType;
835 string strLastType;
836
837 CSQLiteWalletRuntimeLoadVisitor() :
838 fHaveStoredMineMode(false),
839 fHaveStoredHDCoinType(false)
840 {
841 }
842
843 bool VisitWalletRecord(const CDataStream& ssKeyIn,
844 const CDataStream& ssValueIn,
845 string& strErrorRet)
846 {
847 CDataStream ssKey = ssKeyIn;
848 CDataStream ssValue = ssValueIn;
849 string strType;
850 ssKey >> strType;
851 strLastType = strType;
852
853 if (strType == "name")
854 {
855 string strAddress;
856 ssKey >> strAddress;
857 ssValue >> mapAddressBook[strAddress];
858 }
859 else if (strType == "tx")
860 {
861 uint256 hash;
862 ssKey >> hash;
863 CWalletTx& wtx = mapWallet[hash];
864 ssValue >> wtx;
865
866 if (wtx.GetHash() != hash)
867 printf("Error in SQLite wallet export, hash mismatch\n");
868 }
869 else if (strType == "key")
870 {
871 vector<unsigned char> vchPubKey;
872 ssKey >> vchPubKey;
873 CPrivKey vchPrivKey;
874 ssValue >> vchPrivKey;
875
876 mapKeys[vchPubKey] = vchPrivKey;
877 mapPubKeys[Hash160(vchPubKey)] = vchPubKey;
878 }
879 else if (strType == "mkey")
880 {
881 unsigned int nID = 0;
882 ssKey >> nID;
883 CWalletMasterKey kMasterKey;
884 ssValue >> kMasterKey;
885 mapMasterKeys[nID] = kMasterKey;
886 nWalletMasterKeyMaxID = max(nWalletMasterKeyMaxID, nID);
887 fWalletEncrypted = true;
888 fWalletLocked = true;
889 }
890 else if (strType == "ckey")
891 {
892 vector<unsigned char> vchPubKey;
893 ssKey >> vchPubKey;
894 vector<unsigned char> vchCryptedSecret;
895 ssValue >> vchCryptedSecret;
896 if (!AddCryptedKey(vchPubKey, vchCryptedSecret))
897 {
898 strErrorRet = "SQLite wallet export has an unreadable encrypted key record";
899 return false;
900 }
901 }
902 else if (strType == "defaultkey")
903 {
904 ssValue >> vchDefaultKey;
905 }
906 else if (strType == "hdmaster")
907 {
908 ssValue >> vchHDMaster;
909 }
910 else if (strType == "hdchaincode")
911 {
912 ssValue >> vchHDChainCode;
913 }
914 else if (strType == "hdnext")
915 {
916 ssValue >> nHDNext;
917 }
918 else if (strType == "hdschema")
919 {
920 ssValue >> nHDKeySchema;
921 }
922 else if (strType == "hdcointype")
923 {
924 ssValue >> nHDCoinType;
925 fHaveStoredHDCoinType = true;
926 }
927 else if (strType == "hdreceivenext")
928 {
929 ssValue >> nHDReceiveNext;
930 }
931 else if (strType == "hdchangenext")
932 {
933 ssValue >> nHDChangeNext;
934 }
935 else if (strType == "cryptedhdmaster")
936 {
937 ssValue >> vchCryptedHDMaster;
938 fWalletEncrypted = true;
939 fWalletLocked = true;
940 }
941 else if (strType == "cryptedhdchaincode")
942 {
943 ssValue >> vchCryptedHDChainCode;
944 fWalletEncrypted = true;
945 fWalletLocked = true;
946 }
947 else if (strType == "walletminversion")
948 {
949 int nMinVersion = 0;
950 ssValue >> nMinVersion;
951 if (nMinVersion > WALLET_FORMAT_SUPPORTED)
952 {
953 strWalletLoadError = strprintf(
954 "This SQLite wallet export was written by a newer version of "
955 "Bitflash (wallet format %d; this build understands %d). "
956 "Upgrade Bitflash before opening it.",
957 nMinVersion, WALLET_FORMAT_SUPPORTED);
958 strErrorRet = strWalletLoadError;
959 return false;
960 }
961 }
962 else if (strType == "pool")
963 {
964 int64 nIndex;
965 ssKey >> nIndex;
966 vector<unsigned char> vchPubKey;
967 ssValue >> vchPubKey;
968 mapKeyPool[nIndex] = vchPubKey;
969 }
970 else if (strType == "setting")
971 {
972 string strKey;
973 ssKey >> strKey;
974 if (strKey == "nTransactionFee") ssValue >> nTransactionFee;
975 if (strKey == "addrIncoming") ssValue >> addrIncoming;
976
977 if (!fMineModeFromCommandLine)
978 {
979 if (strKey == "nMineMode") { ssValue >> nMineMode; fHaveStoredMineMode = true; }
980 if (strKey == "fGenerateBitcoins") ssValue >> fGenerateBitcoins;
981 if (strKey == "strParticipantPool") ssValue >> strParticipantPool;
982 }
983 }
984
985 if (ssKey.fail())
986 {
987 strErrorRet = strprintf("malformed SQLite wallet record key while handling '%s'",
988 strLastType.c_str());
989 return false;
990 }
991 if (ssValue.fail())
992 {
993 strErrorRet = strprintf("malformed SQLite wallet record value while handling '%s'",
994 strLastType.c_str());
995 return false;
996 }
997 return true;
998 }
999
1000 vector<unsigned char> vchDefaultKey;
1001};
1002
1003static bool FinishLoadedWallet(const vector<unsigned char>& vchDefaultKey,
1004 bool fMayCreateDefaultKey)
1005{
1006 if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey)))
1007 {
1008 keyUser.SetPubKey(vchDefaultKey);
1009 if (mapKeys.count(vchDefaultKey))
1010 keyUser.SetPrivKey(mapKeys[vchDefaultKey]);
1011 }
1012 else if (IsWalletEncrypted())
1013 {
1014 printf("LoadWallet: encrypted wallet has no usable default public key. "
1015 "The file was left untouched.\n");
1016 return false;
1017 }
1018 else
1019 {
1020 if (!fMayCreateDefaultKey)
1021 {
1022 strWalletLoadError =
1023 "SQLite wallet export has no usable default public key";
1024 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1025 return false;
1026 }
1027
1028 keyUser.MakeNewKey();
1029 if (!AddKey(keyUser))
1030 return false;
1031 if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address"))
1032 return false;
1033 CWalletDB().WriteDefaultKey(keyUser.GetPubKey());
1034 }
1035
1036 return true;
1037}
1038
1039static bool ReconcileLoadedWalletMetadata(bool fHaveStoredMineMode,
1040 bool fHaveStoredHDCoinType)
1041{
1042 // An encrypted wallet has a seed; it just has not been decrypted yet.
1043 //
1044 // The reset in the else branch was written when "no seed in memory after
1045 // loading" could only mean "this wallet has no seed". With encryption that
1046 // is no longer true, and taking the else branch here wiped the schema and
1047 // the derivation counters that had just been read correctly out of the
1048 // file. Unlocking later restores the seed but nothing restores those, so a
1049 // BIP44 wallet came back deriving legacy addresses -- different addresses
1050 // from the ones its own recovery phrase produces, while the audit reported
1051 // the phrase as covering nothing.
1052 if (HaveHDSeed() || !vchCryptedHDMaster.empty())
1053 {
1054 if (nHDKeySchema == HD_SCHEMA_NONE)
1055 {
1056 nHDKeySchema = HD_SCHEMA_LEGACY;
1057 printf("LoadWallet: deterministic seed has no schema metadata; "
1058 "treating it as legacy m/index'\n");
1059 }
1060 else if (nHDKeySchema != HD_SCHEMA_LEGACY &&
1061 nHDKeySchema != HD_SCHEMA_BIP44)
1062 {
1063 printf("LoadWallet: deterministic wallet schema %d (%s) is not "
1064 "supported by this build\n",
1065 nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str());
1066 return false;
1067 }
1068 if (!fHaveStoredHDCoinType)
1069 {
1070 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
1071 printf("LoadWallet: deterministic seed has no BIP44 coin type metadata; "
1072 "using Bitflash coin type %u\n", nHDCoinType);
1073 }
1074 }
1075 else
1076 {
1077 nHDKeySchema = HD_SCHEMA_NONE;
1078 nHDNext = 0;
1079 nHDReceiveNext = 0;
1080 nHDChangeNext = 0;
1081 nHDCoinType = HD_BIP44_COIN_TYPE_BITFLASH;
1082 }
1083
1084 // fGenerateBitcoins and nMineMode only mean anything together, and a
1085 // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already
1086 // wrote fGenerateBitcoins when mining was toggled, years before this fork
1087 // had a mode at all. Restoring that lone flag put a node into the one state
1088 // the miner cannot act on -- generate on, mode relay -- so every miner
1089 // thread started and returned at the relay guard, and the machine sat there
1090 // looking like it was mining while hashing nothing. That happened to a
1091 // 32-core node on the first 1.2.11 start.
1092 //
1093 // So the flag is only believed when the mode it belongs to was stored with
1094 // it, and the pair is made coherent either way.
1095 if (!fMineModeFromCommandLine)
1096 {
1097 if (!fHaveStoredMineMode)
1098 {
1099 if (fGenerateBitcoins)
1100 printf("LoadWallet: ignoring a stored fGenerateBitcoins with no mining mode "
1101 "beside it -- it predates this setting and cannot be read on its own\n");
1102 fGenerateBitcoins = 0;
1103 }
1104 else
1105 {
1106 fGenerateBitcoins = (nMineMode != MINE_RELAY) ? 1 : 0;
1107 }
1108 }
1109
1110 return true;
1111}
1112
617bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) 1113bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
618{ 1114{
619 // Whether wallet.dat actually carried a mining mode, as opposed to just the 1115 // Whether wallet.dat actually carried a mining mode, as opposed to just the
@@ -924,34 +1420,269 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet)
924 return true; 1420 return true;
925} 1421}
926 1422
1423bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet)
1424{
1425 // Under the SQLite backend there is no Berkeley DB cursor to walk; scan the
1426 // active SQLite store instead. This is the single choke point every "read
1427 // every wallet record" caller goes through (storage audit and check among
1428 // them), so routing it here makes those honour -walletbackend=sqlite.
1429 if (WalletSQLiteRuntimeActive())
1430 {
1431 strErrorRet.clear();
1432 return pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet);
1433 }
1434
1435 class CWalletScanDB : public CWalletDB
1436 {
1437 public:
1438 CWalletScanDB() : CWalletDB("r") { }
1439 using CDB::GetCursor;
1440 using CDB::ReadAtCursor;
1441 };
1442
1443 strErrorRet.clear();
1444 CWalletScanDB dbWallet;
1445 Dbc* pcursor = dbWallet.GetCursor();
1446 if (!pcursor)
1447 {
1448 strErrorRet = "could not open a wallet cursor";
1449 return false;
1450 }
1451 CAutoCursor cursorGuard(pcursor);
1452
1453 loop
1454 {
1455 CDataStream ssKey(SER_DISK);
1456 CDataStream ssValue(SER_DISK);
1457 int ret = dbWallet.ReadAtCursor(pcursor, ssKey, ssValue);
1458 if (ret == DB_NOTFOUND)
1459 break;
1460 if (ret != 0)
1461 {
1462 strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret);
1463 return false;
1464 }
1465 if (!visitor.VisitWalletRecord(ssKey, ssValue, strErrorRet))
1466 return false;
1467 }
1468
1469 return true;
1470}
1471
927bool LoadWallet() 1472bool LoadWallet()
928{ 1473{
929 vector<unsigned char> vchDefaultKey; 1474 vector<unsigned char> vchDefaultKey;
930 if (!CWalletDB("cr").LoadWallet(vchDefaultKey)) 1475 if (!CWalletDB("cr").LoadWallet(vchDefaultKey))
931 return false; 1476 return false;
932 1477
933 if (!vchDefaultKey.empty() && mapPubKeys.count(Hash160(vchDefaultKey))) 1478 return FinishLoadedWallet(vchDefaultKey, true);
1479}
1480
1481bool LoadWalletFromSQLite(const string& strPath)
1482{
1483 strWalletLoadError.clear();
1484
1485 if (!FileExists(strPath.c_str()))
934 { 1486 {
935 // Set keyUser 1487 strWalletLoadError = strprintf("SQLite wallet export does not exist: %s",
936 keyUser.SetPubKey(vchDefaultKey); 1488 strPath.c_str());
937 if (mapKeys.count(vchDefaultKey)) 1489 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
938 keyUser.SetPrivKey(mapKeys[vchDefaultKey]); 1490 return false;
939 } 1491 }
940 else if (IsWalletEncrypted()) 1492
1493 CWalletDBSQLite sqlite;
1494 string strError;
1495 if (!sqlite.OpenReadOnly(strPath, strError))
941 { 1496 {
942 printf("LoadWallet: encrypted wallet has no usable default public key. " 1497 strWalletLoadError = strError;
943 "The file was left untouched.\n"); 1498 printf("LoadWallet: cannot open SQLite wallet export: %s\n",
1499 strError.c_str());
944 return false; 1500 return false;
945 } 1501 }
946 else 1502
1503 CSQLiteWalletRuntimeLoadVisitor visitor;
1504 try
947 { 1505 {
948 // Create new keyUser and set as default key 1506 CRITICAL_BLOCK(cs_mapKeys)
949 keyUser.MakeNewKey(); 1507 CRITICAL_BLOCK(cs_mapWallet)
950 if (!AddKey(keyUser)) 1508 {
951 return false; 1509 if (!sqlite.ScanRecords(visitor, strError))
952 if (!SetAddressBookName(PubKeyToAddress(keyUser.GetPubKey()), "Your Address")) 1510 {
953 return false; 1511 if (strWalletLoadError.empty())
954 CWalletDB().WriteDefaultKey(keyUser.GetPubKey()); 1512 strWalletLoadError = strError;
1513 printf("LoadWallet: SQLite wallet export could not be read. "
1514 "Failed while handling a '%s' record: %s\n",
1515 visitor.strLastType.c_str(), strError.c_str());
1516 return false;
1517 }
1518 }
1519 }
1520 catch (const std::exception& e)
1521 {
1522 strWalletLoadError = strprintf(
1523 "SQLite wallet export could not be read while handling a '%s' record: %s",
1524 visitor.strLastType.c_str(), e.what());
1525 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1526 return false;
1527 }
1528 catch (...)
1529 {
1530 strWalletLoadError = strprintf(
1531 "SQLite wallet export could not be read while handling a '%s' record",
1532 visitor.strLastType.c_str());
1533 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1534 return false;
1535 }
1536
1537 if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode,
1538 visitor.fHaveStoredHDCoinType))
1539 {
1540 if (strWalletLoadError.empty())
1541 strWalletLoadError = "SQLite wallet metadata is not supported by this build";
1542 return false;
1543 }
1544
1545 return FinishLoadedWallet(visitor.vchDefaultKey, false);
1546}
1547
1548// Open a SQLite wallet read-write and keep it open as the active runtime
1549// backend, then load its records into memory the same way LoadWalletFromSQLite
1550// does. Unlike the read-only staging path, this leaves pWalletSQLiteRuntime set
1551// so every later CWalletDB read/write/erase routes to this file for the life of
1552// the process. The Berkeley DB wallet.dat is never opened.
1553bool LoadWalletFromSQLiteRuntime(const string& strPath)
1554{
1555 strWalletLoadError.clear();
1556
1557 if (!FileExists(strPath.c_str()))
1558 {
1559 strWalletLoadError = strprintf("SQLite wallet does not exist: %s",
1560 strPath.c_str());
1561 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1562 return false;
1563 }
1564
1565 string strError;
1566 if (!WalletSQLiteRuntimeOpen(strPath, strError))
1567 {
1568 strWalletLoadError = strError;
1569 printf("LoadWallet: cannot open SQLite wallet backend: %s\n",
1570 strError.c_str());
1571 return false;
1572 }
1573
1574 CSQLiteWalletRuntimeLoadVisitor visitor;
1575 try
1576 {
1577 CRITICAL_BLOCK(cs_mapKeys)
1578 CRITICAL_BLOCK(cs_mapWallet)
1579 {
1580 if (!pWalletSQLiteRuntime->ScanRecords(visitor, strError))
1581 {
1582 WalletSQLiteRuntimeClose();
1583 if (strWalletLoadError.empty())
1584 strWalletLoadError = strError;
1585 printf("LoadWallet: SQLite wallet backend could not be read. "
1586 "Failed while handling a '%s' record: %s\n",
1587 visitor.strLastType.c_str(), strError.c_str());
1588 return false;
1589 }
1590 }
1591 }
1592 catch (const std::exception& e)
1593 {
1594 WalletSQLiteRuntimeClose();
1595 strWalletLoadError = strprintf(
1596 "SQLite wallet backend could not be read while handling a '%s' record: %s",
1597 visitor.strLastType.c_str(), e.what());
1598 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1599 return false;
1600 }
1601 catch (...)
1602 {
1603 WalletSQLiteRuntimeClose();
1604 strWalletLoadError = strprintf(
1605 "SQLite wallet backend could not be read while handling a '%s' record",
1606 visitor.strLastType.c_str());
1607 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1608 return false;
1609 }
1610
1611 // Refuse an empty or non-wallet SQLite file up front, with a message that
1612 // names the likely cause. FinishLoadedWallet below already rejects a
1613 // missing default key (it is the fMayCreateDefaultKey=false path), but its
1614 // wording -- "no usable default public key" -- reads as corruption when the
1615 // real story is usually "this file was never an exported wallet". A blank
1616 // wallet.sqlite left by a typo or a half-run export should say so.
1617 if (visitor.vchDefaultKey.empty())
1618 {
1619 WalletSQLiteRuntimeClose();
1620 strWalletLoadError =
1621 "SQLite wallet has no default key record; it may be empty or not a "
1622 "wallet export. Create it with -walletsqliteexport.";
1623 printf("LoadWallet: %s\n", strWalletLoadError.c_str());
1624 return false;
1625 }
1626
1627 if (!ReconcileLoadedWalletMetadata(visitor.fHaveStoredMineMode,
1628 visitor.fHaveStoredHDCoinType))
1629 {
1630 WalletSQLiteRuntimeClose();
1631 if (strWalletLoadError.empty())
1632 strWalletLoadError = "SQLite wallet metadata is not supported by this build";
1633 return false;
1634 }
1635
1636 // Never fabricate a default key: an export always carries one, and a file
1637 // that does not is refused with the wallet left open to no writes rather
1638 // than quietly minted into a new identity.
1639 if (!FinishLoadedWallet(visitor.vchDefaultKey, false))
1640 {
1641 WalletSQLiteRuntimeClose();
1642 return false;
1643 }
1644
1645 return true;
1646}
1647
1648// Create a brand-new wallet directly in the SQLite backend. For a fresh install
1649// where SQLite is the default there is no wallet.dat to export from, so open a
1650// new wallet.sqlite (Open() creates the file and its schema) and let
1651// FinishLoadedWallet mint the first default key straight into it -- the same
1652// call the Berkeley DB create path uses, so a new wallet is identical either
1653// way. Refuses to touch an existing file, and cleans up on failure so a broken
1654// half-initialised wallet.sqlite is never left for the next start to load.
1655bool CreateNewSQLiteWallet(const string& strPath)
1656{
1657 strWalletLoadError.clear();
1658
1659 if (FileExists(strPath.c_str()))
1660 {
1661 strWalletLoadError = strprintf("SQLite wallet already exists: %s",
1662 strPath.c_str());
1663 printf("CreateNewSQLiteWallet: %s\n", strWalletLoadError.c_str());
1664 return false;
1665 }
1666
1667 string strError;
1668 if (!WalletSQLiteRuntimeOpen(strPath, strError, false))
1669 {
1670 strWalletLoadError = strError;
1671 printf("CreateNewSQLiteWallet: cannot create SQLite wallet: %s\n",
1672 strError.c_str());
1673 return false;
1674 }
1675
1676 vector<unsigned char> vchNoDefaultKey;
1677 if (!FinishLoadedWallet(vchNoDefaultKey, true))
1678 {
1679 WalletSQLiteRuntimeClose();
1680 remove(strPath.c_str());
1681 remove((strPath + "-wal").c_str());
1682 remove((strPath + "-shm").c_str());
1683 if (strWalletLoadError.empty())
1684 strWalletLoadError = "could not initialise the new SQLite wallet";
1685 return false;
955 } 1686 }
956 1687
957 return true; 1688 return true;
@@ -974,21 +1705,99 @@ bool LoadWallet()
974// this file was written are not spendable from it. Backing up once is not 1705// this file was written are not spendable from it. Backing up once is not
975// enough, and that is a property of the wallet format rather than of this 1706// enough, and that is a property of the wallet format rather than of this
976// function. 1707// function.
1708// Back up the active SQLite wallet. The store is checkpointed so the WAL is
1709// folded into the main file, then the single self-contained wallet.sqlite is
1710// copied through a temp file and renamed into place (same temp/old/rename dance
1711// as the Berkeley DB path, for Windows' non-overwriting rename). No lsn_reset:
1712// a checkpointed SQLite file needs nothing beside it to open.
1713static bool BackupWalletSQLite(const string& strDest)
1714{
1715 string strSrc = GetAppDir() + "/wallet.sqlite";
1716 if (!FileExists(strSrc.c_str()))
1717 return error("BackupWallet() : %s does not exist\n", strSrc.c_str());
1718
1719 string strCheckpointError;
1720 if (!WalletSQLiteRuntimeCheckpoint(strCheckpointError))
1721 return error("BackupWallet() : checkpoint failed: %s\n", strCheckpointError.c_str());
1722
1723 string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime());
1724 for (int i = 1; FileExists(strTemp.c_str()); i++)
1725 strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i);
1726 string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime());
1727 for (int i = 1; FileExists(strOld.c_str()); i++)
1728 strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i);
1729
1730 FILE* pfIn = fopen(strSrc.c_str(), "rb");
1731 if (!pfIn)
1732 return error("BackupWallet() : cannot read %s\n", strSrc.c_str());
1733 FILE* pfOut = fopen(strTemp.c_str(), "wb");
1734 if (!pfOut)
1735 {
1736 fclose(pfIn);
1737 return error("BackupWallet() : cannot write %s\n", strTemp.c_str());
1738 }
1739
1740 char buf[65536];
1741 size_t n;
1742 bool fOk = true;
1743 while ((n = fread(buf, 1, sizeof(buf), pfIn)) > 0)
1744 if (fwrite(buf, 1, n, pfOut) != n) { fOk = false; break; }
1745 if (ferror(pfIn))
1746 fOk = false;
1747 fclose(pfIn);
1748 if (fclose(pfOut) != 0)
1749 fOk = false;
1750 if (!fOk)
1751 {
1752 remove(strTemp.c_str()); // half a wallet is worse than none
1753 return error("BackupWallet() : copy to %s failed\n", strTemp.c_str());
1754 }
1755
1756 bool fHadOldDest = FileExists(strDest.c_str());
1757 if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0)
1758 {
1759 remove(strTemp.c_str());
1760 return error("BackupWallet() : could not move old %s aside\n", strDest.c_str());
1761 }
1762 if (rename(strTemp.c_str(), strDest.c_str()) != 0)
1763 {
1764 remove(strTemp.c_str());
1765 if (fHadOldDest)
1766 rename(strOld.c_str(), strDest.c_str());
1767 return error("BackupWallet() : could not install %s\n", strDest.c_str());
1768 }
1769 if (fHadOldDest)
1770 remove(strOld.c_str());
1771 return true;
1772}
1773
977bool BackupWallet(const string& strDest) 1774bool BackupWallet(const string& strDest)
978{ 1775{
979 if (strDest.empty()) 1776 if (strDest.empty())
980 return error("BackupWallet() : no destination given\n"); 1777 return error("BackupWallet() : no destination given\n");
981 1778
1779 // Under the SQLite backend the live wallet is wallet.sqlite, not wallet.dat.
1780 if (WalletSQLiteRuntimeActive())
1781 return BackupWalletSQLite(strDest);
1782
982 string strSrc = GetAppDir() + "/wallet.dat"; 1783 string strSrc = GetAppDir() + "/wallet.dat";
983 if (!FileExists(strSrc.c_str())) 1784 if (!FileExists(strSrc.c_str()))
984 return error("BackupWallet() : %s does not exist\n", strSrc.c_str()); 1785 return error("BackupWallet() : %s does not exist\n", strSrc.c_str());
985 1786
1787 string strTemp = strprintf("%s.tmp.%lld", strDest.c_str(), GetTime());
1788 for (int i = 1; FileExists(strTemp.c_str()); i++)
1789 strTemp = strprintf("%s.tmp.%lld.%d", strDest.c_str(), GetTime(), i);
1790 string strOld = strprintf("%s.old.%lld", strDest.c_str(), GetTime());
1791 for (int i = 1; FileExists(strOld.c_str()); i++)
1792 strOld = strprintf("%s.old.%lld.%d", strDest.c_str(), GetTime(), i);
1793
986 CRITICAL_BLOCK(cs_db) 1794 CRITICAL_BLOCK(cs_db)
987 { 1795 {
988 // Committing first, so the copy is not missing the newest records. 1796 // Committing first, so the copy is not missing the newest records.
989 // Both calls can throw, and a failed backup must not take the node 1797 // Both calls can throw, and a failed backup must not take the node
990 // down with it. 1798 // down with it.
991 try { dbenv.txn_checkpoint(0, 0, 0); } 1799 DbEnv& env = GetDbEnv();
1800 try { env.txn_checkpoint(0, 0, 0); }
992 catch (const std::exception& e) 1801 catch (const std::exception& e)
993 { return error("BackupWallet() : checkpoint failed: %s\n", e.what()); } 1802 { return error("BackupWallet() : checkpoint failed: %s\n", e.what()); }
994 1803
@@ -997,11 +1806,11 @@ bool BackupWallet(const string& strDest)
997 FILE* pfIn = fopen(strSrc.c_str(), "rb"); 1806 FILE* pfIn = fopen(strSrc.c_str(), "rb");
998 if (!pfIn) 1807 if (!pfIn)
999 return error("BackupWallet() : cannot read %s\n", strSrc.c_str()); 1808 return error("BackupWallet() : cannot read %s\n", strSrc.c_str());
1000 FILE* pfOut = fopen(strDest.c_str(), "wb"); 1809 FILE* pfOut = fopen(strTemp.c_str(), "wb");
1001 if (!pfOut) 1810 if (!pfOut)
1002 { 1811 {
1003 fclose(pfIn); 1812 fclose(pfIn);
1004 return error("BackupWallet() : cannot write %s\n", strDest.c_str()); 1813 return error("BackupWallet() : cannot write %s\n", strTemp.c_str());
1005 } 1814 }
1006 1815
1007 char buf[65536]; 1816 char buf[65536];
@@ -1018,18 +1827,39 @@ bool BackupWallet(const string& strDest)
1018 1827
1019 if (!fOk) 1828 if (!fOk)
1020 { 1829 {
1021 remove(strDest.c_str()); // half a wallet is worse than none 1830 remove(strTemp.c_str()); // half a wallet is worse than none
1022 return error("BackupWallet() : copy to %s failed\n", strDest.c_str()); 1831 return error("BackupWallet() : copy to %s failed\n", strTemp.c_str());
1023 } 1832 }
1024 1833
1025 // Without this the copy is welded to this node's database/ dir. 1834 // Without this the copy is welded to this node's database/ dir.
1026 int ret = dbenv.lsn_reset(strDest.c_str(), 0); 1835 int ret = env.lsn_reset(strTemp.c_str(), 0);
1027 if (ret != 0) 1836 if (ret != 0)
1028 printf("BackupWallet() : warning -- lsn_reset returned %d; the copy " 1837 printf("BackupWallet() : warning -- lsn_reset returned %d; the copy "
1029 "may only open beside this node's database/ directory\n", ret); 1838 "may only open beside this node's database/ directory\n", ret);
1839
1840 bool fHadOldDest = FileExists(strDest.c_str());
1841 if (fHadOldDest && rename(strDest.c_str(), strOld.c_str()) != 0)
1842 {
1843 remove(strTemp.c_str());
1844 return error("BackupWallet() : could not move old %s aside\n", strDest.c_str());
1845 }
1846 if (rename(strTemp.c_str(), strDest.c_str()) != 0)
1847 {
1848 remove(strTemp.c_str());
1849 if (fHadOldDest)
1850 rename(strOld.c_str(), strDest.c_str());
1851 return error("BackupWallet() : could not install %s\n", strDest.c_str());
1852 }
1853 if (fHadOldDest)
1854 remove(strOld.c_str());
1030 } 1855 }
1031 catch (const std::exception& e) 1856 catch (const std::exception& e)
1032 { return error("BackupWallet() : %s\n", e.what()); } 1857 {
1858 remove(strTemp.c_str());
1859 if (FileExists(strOld.c_str()) && !FileExists(strDest.c_str()))
1860 rename(strOld.c_str(), strDest.c_str());
1861 return error("BackupWallet() : %s\n", e.what());
1862 }
1033 } 1863 }
1034 1864
1035 // Recorded here rather than by whoever called, so that a backup taken with 1865 // Recorded here rather than by whoever called, so that a backup taken with
@@ -1307,73 +2137,71 @@ public:
1307 { 2137 {
1308 return Write(make_pair(string("pool"), nIndex), vchPubKey); 2138 return Write(make_pair(string("pool"), nIndex), vchPubKey);
1309 } 2139 }
2140};
2141
2142static bool IsWalletEncryptionPrivateRecord(const string& strType)
2143{
2144 return strType == "key" ||
2145 strType == "pool" ||
2146 strType == "hdmaster" ||
2147 strType == "hdchaincode" ||
2148 strType == "mkey" ||
2149 strType == "ckey" ||
2150 strType == "cryptedhdmaster" ||
2151 strType == "cryptedhdchaincode" ||
2152 strType == "walletminversion";
2153}
2154
2155class CWalletPublicRecordCopyVisitor : public CWalletRecordVisitor
2156{
2157public:
2158 CWalletRewriteDB& dbTo;
2159 int& nCopied;
1310 2160
1311 bool CopyPublicRecordsTo(CWalletRewriteDB& dbTo, int& nCopiedRet, string& strErrorRet) 2161 CWalletPublicRecordCopyVisitor(CWalletRewriteDB& dbToIn, int& nCopiedIn)
2162 : dbTo(dbToIn), nCopied(nCopiedIn) { }
2163
2164 bool VisitWalletRecord(const CDataStream& ssKey,
2165 const CDataStream& ssValue,
2166 string& strErrorRet)
1312 { 2167 {
1313 nCopiedRet = 0; 2168 string strType;
1314 Dbc* pcursor = GetCursor(); 2169 try
1315 if (!pcursor)
1316 { 2170 {
1317 strErrorRet = "could not open a wallet cursor"; 2171 CDataStream ssType = ssKey;
1318 return false; 2172 ssType >> strType;
1319 } 2173 }
1320 2174 catch (...)
1321 unsigned int fFlags = DB_NEXT;
1322 loop
1323 { 2175 {
1324 CDataStream ssKey; 2176 strErrorRet = "wallet.dat has a record whose key type cannot be read";
1325 CDataStream ssValue; 2177 return false;
1326 int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags); 2178 }
1327 if (ret == DB_NOTFOUND)
1328 break;
1329 if (ret != 0)
1330 {
1331 pcursor->close();
1332 strErrorRet = strprintf("could not read wallet record: Berkeley DB error %d", ret);
1333 return false;
1334 }
1335
1336 string strType;
1337 try
1338 {
1339 CDataStream ssType = ssKey;
1340 ssType >> strType;
1341 }
1342 catch (...)
1343 {
1344 pcursor->close();
1345 strErrorRet = "wallet.dat has a record whose key type cannot be read";
1346 return false;
1347 }
1348 2179
1349 if (strType == "key" || 2180 if (IsWalletEncryptionPrivateRecord(strType))
1350 strType == "pool" || 2181 return true;
1351 strType == "hdmaster" ||
1352 strType == "hdchaincode" ||
1353 strType == "mkey" ||
1354 strType == "ckey" ||
1355 strType == "cryptedhdmaster" ||
1356 strType == "cryptedhdchaincode" ||
1357 strType == "walletminversion")
1358 continue;
1359 2182
1360 if (!dbTo.WriteRaw(ssKey, ssValue)) 2183 if (!dbTo.WriteRaw(ssKey, ssValue))
1361 { 2184 {
1362 pcursor->close(); 2185 strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str());
1363 strErrorRet = strprintf("could not copy wallet record '%s'", strType.c_str()); 2186 return false;
1364 return false;
1365 }
1366 nCopiedRet++;
1367 } 2187 }
1368 2188 nCopied++;
1369 pcursor->close();
1370 return true; 2189 return true;
1371 } 2190 }
1372}; 2191};
1373 2192
1374static string WalletEncryptBackupPath() 2193static bool CopyPublicWalletRecordsTo(CWalletRewriteDB& dbTo,
2194 int& nCopiedRet,
2195 string& strErrorRet)
1375{ 2196{
1376 string strBase = GetAppDir() + "/wallet.dat.before-encrypt."; 2197 nCopiedRet = 0;
2198 CWalletPublicRecordCopyVisitor visitor(dbTo, nCopiedRet);
2199 return ScanWalletRecords(visitor, strErrorRet);
2200}
2201
2202static string WalletEncryptBackupPathFor(const char* pszBaseName)
2203{
2204 string strBase = GetAppDir() + "/" + pszBaseName + ".before-encrypt.";
1377 string strPath = strBase + i64tostr(GetTime()) + ".bak"; 2205 string strPath = strBase + i64tostr(GetTime()) + ".bak";
1378 for (int i = 1; FileExists(strPath.c_str()); i++) 2206 for (int i = 1; FileExists(strPath.c_str()); i++)
1379 strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak"; 2207 strPath = strBase + i64tostr(GetTime()) + "." + itostr(i) + ".bak";
@@ -1403,6 +2231,192 @@ static void SecureClearBytes(vector<unsigned char>& vch)
1403 vch.clear(); 2231 vch.clear();
1404} 2232}
1405 2233
2234// The SQLite counterpart of CWalletPublicRecordCopyVisitor: same skip list,
2235// same byte-for-byte copy, but the destination is a CWalletDBSQLite record
2236// store rather than a Berkeley DB file.
2237class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor
2238{
2239public:
2240 CWalletDBSQLite& dbTo;
2241 int& nCopied;
2242
2243 CWalletPublicRecordCopyToSQLiteVisitor(CWalletDBSQLite& dbToIn, int& nCopiedIn)
2244 : dbTo(dbToIn), nCopied(nCopiedIn) { }
2245
2246 bool VisitWalletRecord(const CDataStream& ssKey,
2247 const CDataStream& ssValue,
2248 string& strErrorRet)
2249 {
2250 string strType;
2251 try
2252 {
2253 CDataStream ssType = ssKey;
2254 ssType >> strType;
2255 }
2256 catch (...)
2257 {
2258 strErrorRet = "SQLite wallet has a record whose key type cannot be read";
2259 return false;
2260 }
2261
2262 if (IsWalletEncryptionPrivateRecord(strType))
2263 return true;
2264
2265 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
2266 vector<unsigned char> vchValue(ssValue.begin(), ssValue.end());
2267 bool fOk = dbTo.WriteRecord(vchKey, vchValue, strErrorRet, true);
2268 if (!vchKey.empty())
2269 memset(&vchKey[0], 0, vchKey.size());
2270 if (!vchValue.empty())
2271 memset(&vchValue[0], 0, vchValue.size());
2272 if (!fOk)
2273 {
2274 strErrorRet = strprintf("could not copy wallet record '%s': %s",
2275 strType.c_str(), strErrorRet.c_str());
2276 return false;
2277 }
2278 nCopied++;
2279 return true;
2280 }
2281};
2282
2283// Persist an encrypted wallet under the SQLite backend. Everything the caller
2284// computed -- the master key, the re-encrypted private keys, the fresh crypted
2285// key pool, the encrypted HD seed -- lands in a brand new wallet.sqlite built
2286// in one transaction, which is then swapped in for the plaintext original.
2287//
2288// A fresh file rather than an in-place rewrite for two reasons: it keeps the
2289// whole change atomic (a crash before the rename leaves the old wallet whole),
2290// and it guarantees no plaintext key survives in a freed page the way an
2291// in-place DELETE would leave behind until a VACUUM.
2292static bool EncryptWalletPersistSQLite(
2293 const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vCryptedKeys,
2294 const vector<pair<vector<unsigned char>, vector<unsigned char> > >& vNewPoolKeys,
2295 const vector<pair<int64, vector<unsigned char> > >& vNewPool,
2296 const CWalletMasterKey& kMasterKey,
2297 const vector<unsigned char>& vchCryptedHDMasterNew,
2298 const vector<unsigned char>& vchCryptedHDChainCodeNew,
2299 unsigned int nHDNextNew,
2300 int& nCopiedRet,
2301 string& strBackupRet,
2302 string& strErrorRet)
2303{
2304 nCopiedRet = 0;
2305 if (!pWalletSQLiteRuntime)
2306 {
2307 strErrorRet = "SQLite wallet backend is not active";
2308 return false;
2309 }
2310
2311 string strWalletPath = GetAppDir() + "/wallet.sqlite";
2312 string strTempPath = GetAppDir() + "/" +
2313 strprintf("wallet.sqlite.encrypting.%lld", GetTime());
2314 remove(strTempPath.c_str());
2315 remove((strTempPath + "-wal").c_str());
2316 remove((strTempPath + "-shm").c_str());
2317
2318 {
2319 CWalletDBSQLite dbTmp;
2320 if (!dbTmp.Open(strTempPath, strErrorRet))
2321 {
2322 remove(strTempPath.c_str());
2323 return false;
2324 }
2325 if (!dbTmp.BeginTransaction(strErrorRet))
2326 {
2327 dbTmp.Close();
2328 remove(strTempPath.c_str());
2329 return false;
2330 }
2331
2332 // The public records come from the live SQLite store -- under this
2333 // backend ScanWalletRecords() has no Berkeley DB behind it to read.
2334 CWalletPublicRecordCopyToSQLiteVisitor visitor(dbTmp, nCopiedRet);
2335 bool fOk = pWalletSQLiteRuntime->ScanRecords(visitor, strErrorRet);
2336
2337 // Encrypted metadata, serialized byte-for-byte the way the Berkeley DB
2338 // path (CWalletRewriteDB) writes it, so a wallet encrypted under either
2339 // backend is the same set of records. ckey uses INSERT-only to match
2340 // WriteCryptedKey; the rest overwrite.
2341 fOk = fOk &&
2342 dbTmp.WriteTypedRecord(string("walletminversion"),
2343 (int)WALLET_FORMAT_ENCRYPTED, strErrorRet, true) &&
2344 dbTmp.WriteTypedRecord(make_pair(string("mkey"),
2345 (unsigned int)(nWalletMasterKeyMaxID + 1)),
2346 kMasterKey, strErrorRet, true);
2347 for (size_t i = 0; fOk && i < vCryptedKeys.size(); i++)
2348 fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vCryptedKeys[i].first),
2349 vCryptedKeys[i].second, strErrorRet, false);
2350 for (size_t i = 0; fOk && i < vNewPoolKeys.size(); i++)
2351 fOk = dbTmp.WriteTypedRecord(make_pair(string("ckey"), vNewPoolKeys[i].first),
2352 vNewPoolKeys[i].second, strErrorRet, false);
2353 if (fOk && !vchCryptedHDMasterNew.empty())
2354 fOk = dbTmp.WriteTypedRecord(string("cryptedhdmaster"),
2355 vchCryptedHDMasterNew, strErrorRet, true) &&
2356 dbTmp.WriteTypedRecord(string("cryptedhdchaincode"),
2357 vchCryptedHDChainCodeNew, strErrorRet, true);
2358 if (fOk)
2359 fOk = dbTmp.WriteTypedRecord(string("hdnext"), nHDNextNew, strErrorRet, true);
2360 for (size_t i = 0; fOk && i < vNewPool.size(); i++)
2361 fOk = dbTmp.WriteTypedRecord(make_pair(string("pool"), vNewPool[i].first),
2362 vNewPool[i].second, strErrorRet, true);
2363
2364 if (!fOk)
2365 {
2366 string strIgnore;
2367 dbTmp.RollbackTransaction(strIgnore);
2368 dbTmp.Close();
2369 remove(strTempPath.c_str());
2370 remove((strTempPath + "-wal").c_str());
2371 remove((strTempPath + "-shm").c_str());
2372 return false;
2373 }
2374
2375 if (!dbTmp.CommitTransaction(strErrorRet))
2376 {
2377 dbTmp.Close();
2378 remove(strTempPath.c_str());
2379 remove((strTempPath + "-wal").c_str());
2380 remove((strTempPath + "-shm").c_str());
2381 return false;
2382 }
2383 string strCheckpointError;
2384 dbTmp.Checkpoint(strCheckpointError);
2385 dbTmp.Close();
2386 }
2387
2388 // The rebuilt wallet is committed and self-contained. Close the live handle
2389 // so the file can be swapped: Windows will not rename a file that still has
2390 // an open handle, and the node exits right after this.
2391 WalletSQLiteRuntimeClose();
2392
2393 string strBackupPath = WalletEncryptBackupPathFor("wallet.sqlite");
2394 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0)
2395 {
2396 strErrorRet = strprintf("could not move the original SQLite wallet to %s",
2397 strBackupPath.c_str());
2398 remove(strTempPath.c_str());
2399 return false;
2400 }
2401 // The moved-aside wallet owns any leftover write-ahead log or shared-memory
2402 // sidecar; drop stale ones so they cannot shadow the new wallet.
2403 remove((strWalletPath + "-wal").c_str());
2404 remove((strWalletPath + "-shm").c_str());
2405
2406 if (rename(strTempPath.c_str(), strWalletPath.c_str()) != 0)
2407 {
2408 rename(strBackupPath.c_str(), strWalletPath.c_str());
2409 strErrorRet = "could not install the encrypted SQLite wallet; the original was restored";
2410 remove(strTempPath.c_str());
2411 return false;
2412 }
2413 remove((strTempPath + "-wal").c_str());
2414 remove((strTempPath + "-shm").c_str());
2415
2416 strBackupRet = strBackupPath;
2417 return true;
2418}
2419
1406bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) 2420bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet)
1407{ 2421{
1408 strBackupRet.clear(); 2422 strBackupRet.clear();
@@ -1541,15 +2555,32 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1541 vNewPool.push_back(make_pair((int64)i + 1, vchPubKey)); 2555 vNewPool.push_back(make_pair((int64)i + 1, vchPubKey));
1542 } 2556 }
1543 2557
2558 // Every ciphertext is computed; nothing has touched the store yet. Under
2559 // the SQLite backend, persist by rebuilding wallet.sqlite in one atomic
2560 // transaction and swapping it in. The Berkeley DB path below is unchanged.
2561 if (WalletSQLiteRuntimeActive())
2562 {
2563 int nCopiedSQLite = 0;
2564 if (!EncryptWalletPersistSQLite(vCryptedKeys, vNewPoolKeys, vNewPool,
2565 kMasterKey, vchCryptedHDMasterNew,
2566 vchCryptedHDChainCodeNew, nHDNextNew,
2567 nCopiedSQLite, strBackupRet, strErrorRet))
2568 return false;
2569 printf("EncryptWallet() : copied %d public record(s), encrypted %u key(s), "
2570 "rebuilt %d key-pool entry(s) [SQLite]\n",
2571 nCopiedSQLite, (unsigned int)(vCryptedKeys.size() + vNewPoolKeys.size()),
2572 (int)vNewPool.size());
2573 return true;
2574 }
2575
1544 string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime()); 2576 string strTempFile = strprintf("wallet.encrypting.%lld.dat", GetTime());
1545 string strTempPath = GetAppDir() + "/" + strTempFile; 2577 string strTempPath = GetAppDir() + "/" + strTempFile;
1546 remove(strTempPath.c_str()); 2578 remove(strTempPath.c_str());
1547 2579
1548 int nCopied = 0; 2580 int nCopied = 0;
1549 { 2581 {
1550 CWalletRewriteDB dbSource("wallet.dat", "r");
1551 CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+"); 2582 CWalletRewriteDB dbTarget(strTempFile.c_str(), "c+");
1552 if (!dbSource.CopyPublicRecordsTo(dbTarget, nCopied, strErrorRet)) 2583 if (!CopyPublicWalletRecordsTo(dbTarget, nCopied, strErrorRet))
1553 { 2584 {
1554 remove(strTempPath.c_str()); 2585 remove(strTempPath.c_str());
1555 return false; 2586 return false;
@@ -1609,7 +2640,7 @@ bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& st
1609 DBFlush(true); 2640 DBFlush(true);
1610 2641
1611 string strWalletPath = GetAppDir() + "/wallet.dat"; 2642 string strWalletPath = GetAppDir() + "/wallet.dat";
1612 string strBackupPath = WalletEncryptBackupPath(); 2643 string strBackupPath = WalletEncryptBackupPathFor("wallet.dat");
1613 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0) 2644 if (rename(strWalletPath.c_str(), strBackupPath.c_str()) != 0)
1614 { 2645 {
1615 strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str()); 2646 strErrorRet = strprintf("could not move the original wallet to %s", strBackupPath.c_str());
diff --git a/src/db.h b/src/db.h
index 37a252f..859e3fb 100644
--- a/src/db.h
+++ b/src/db.h
@@ -46,7 +46,7 @@ public:
46}; 46};
47 47
48 48
49extern DbEnv dbenv; 49extern DbEnv& GetDbEnv();
50extern void DBFlush(bool fShutdown); 50extern void DBFlush(bool fShutdown);
51// Close the environment and delete its logs. Only the encryption command calls 51// Close the environment and delete its logs. Only the encryption command calls
52// this: the logs hold the plaintext records the old wallet wrote. 52// this: the logs hold the plaintext records the old wallet wrote.
@@ -235,7 +235,7 @@ public:
235 if (!pdb) 235 if (!pdb)
236 return false; 236 return false;
237 DbTxn* ptxn = NULL; 237 DbTxn* ptxn = NULL;
238 int ret = dbenv.txn_begin(GetTxn(), &ptxn, 0); 238 int ret = GetDbEnv().txn_begin(GetTxn(), &ptxn, 0);
239 if (!ptxn || ret != 0) 239 if (!ptxn || ret != 0)
240 return false; 240 return false;
241 vTxn.push_back(ptxn); 241 vTxn.push_back(ptxn);
@@ -333,6 +333,24 @@ private:
333// Legacy IP-based peer address database (CAddrDB / addr.dat) removed -- 333// Legacy IP-based peer address database (CAddrDB / addr.dat) removed --
334// peer discovery is entirely Nostr/.btf-based now. 334// peer discovery is entirely Nostr/.btf-based now.
335 335
336bool WalletSQLiteRuntimeActive();
337bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist=true);
338void WalletSQLiteRuntimeClose();
339bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey,
340 vector<unsigned char>& vchValueRet);
341bool WalletSQLiteRuntimeWriteRecord(const vector<unsigned char>& vchKey,
342 const vector<unsigned char>& vchValue,
343 bool fOverwrite);
344bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey);
345bool WalletSQLiteRuntimeBeginTxn();
346bool WalletSQLiteRuntimeCommitTxn();
347void WalletSQLiteRuntimeRollbackTxn();
348bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet);
349bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet);
350string ReadWalletBackendMarker();
351bool WriteWalletBackendMarker(const string& strBackend);
352bool CreateNewSQLiteWallet(const string& strPath);
353
336 354
337 355
338 356
@@ -340,64 +358,141 @@ private:
340class CWalletDB : public CDB 358class CWalletDB : public CDB
341{ 359{
342public: 360public:
343 CWalletDB(const char* pszMode="r+", bool fTxn=false) : CDB("wallet.dat", pszMode, fTxn) { } 361 CWalletDB(const char* pszMode="r+", bool fTxn=false);
344private: 362private:
345 CWalletDB(const CWalletDB&); 363 CWalletDB(const CWalletDB&);
346 void operator=(const CWalletDB&); 364 void operator=(const CWalletDB&);
365
366 template<typename K, typename T>
367 bool ReadWalletRecord(const K& key, T& value)
368 {
369 if (!WalletSQLiteRuntimeActive())
370 return Read(key, value);
371
372 CDataStream ssKey(SER_DISK);
373 ssKey.reserve(1000);
374 ssKey << key;
375 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
376 vector<unsigned char> vchValue;
377
378 bool fOk = WalletSQLiteRuntimeReadRecord(vchKey, vchValue);
379 if (!vchKey.empty())
380 memset(&vchKey[0], 0, vchKey.size());
381 if (!ssKey.empty())
382 memset(&ssKey[0], 0, ssKey.size());
383 if (!fOk)
384 return false;
385
386 CDataStream ssValue(vchValue, SER_DISK);
387 ssValue >> value;
388 bool fReadOk = !ssValue.fail();
389 if (!vchValue.empty())
390 memset(&vchValue[0], 0, vchValue.size());
391 if (!ssValue.empty())
392 memset(&ssValue[0], 0, ssValue.size());
393 return fReadOk;
394 }
395
396 template<typename K, typename T>
397 bool WriteWalletRecord(const K& key, const T& value, bool fOverwrite=true)
398 {
399 if (!WalletSQLiteRuntimeActive())
400 return Write(key, value, fOverwrite);
401
402 CDataStream ssKey(SER_DISK);
403 ssKey.reserve(1000);
404 ssKey << key;
405 CDataStream ssValue(SER_DISK);
406 ssValue.reserve(10000);
407 ssValue << value;
408
409 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
410 vector<unsigned char> vchValue(ssValue.begin(), ssValue.end());
411 bool fOk = WalletSQLiteRuntimeWriteRecord(vchKey, vchValue, fOverwrite);
412
413 if (!vchKey.empty())
414 memset(&vchKey[0], 0, vchKey.size());
415 if (!vchValue.empty())
416 memset(&vchValue[0], 0, vchValue.size());
417 if (!ssKey.empty())
418 memset(&ssKey[0], 0, ssKey.size());
419 if (!ssValue.empty())
420 memset(&ssValue[0], 0, ssValue.size());
421 return fOk;
422 }
423
424 template<typename K>
425 bool EraseWalletRecord(const K& key)
426 {
427 if (!WalletSQLiteRuntimeActive())
428 return Erase(key);
429
430 CDataStream ssKey(SER_DISK);
431 ssKey.reserve(1000);
432 ssKey << key;
433 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
434 bool fOk = WalletSQLiteRuntimeEraseRecord(vchKey);
435
436 if (!vchKey.empty())
437 memset(&vchKey[0], 0, vchKey.size());
438 if (!ssKey.empty())
439 memset(&ssKey[0], 0, ssKey.size());
440 return fOk;
441 }
347public: 442public:
348 bool ReadName(const string& strAddress, string& strName) 443 bool ReadName(const string& strAddress, string& strName)
349 { 444 {
350 strName = ""; 445 strName = "";
351 return Read(make_pair(string("name"), strAddress), strName); 446 return ReadWalletRecord(make_pair(string("name"), strAddress), strName);
352 } 447 }
353 448
354 bool WriteName(const string& strAddress, const string& strName) 449 bool WriteName(const string& strAddress, const string& strName)
355 { 450 {
356 mapAddressBook[strAddress] = strName; 451 mapAddressBook[strAddress] = strName;
357 return Write(make_pair(string("name"), strAddress), strName); 452 return WriteWalletRecord(make_pair(string("name"), strAddress), strName);
358 } 453 }
359 454
360 bool EraseName(const string& strAddress) 455 bool EraseName(const string& strAddress)
361 { 456 {
362 mapAddressBook.erase(strAddress); 457 mapAddressBook.erase(strAddress);
363 return Erase(make_pair(string("name"), strAddress)); 458 return EraseWalletRecord(make_pair(string("name"), strAddress));
364 } 459 }
365 460
366 bool ReadTx(uint256 hash, CWalletTx& wtx) 461 bool ReadTx(uint256 hash, CWalletTx& wtx)
367 { 462 {
368 return Read(make_pair(string("tx"), hash), wtx); 463 return ReadWalletRecord(make_pair(string("tx"), hash), wtx);
369 } 464 }
370 465
371 bool WriteTx(uint256 hash, const CWalletTx& wtx) 466 bool WriteTx(uint256 hash, const CWalletTx& wtx)
372 { 467 {
373 return Write(make_pair(string("tx"), hash), wtx); 468 return WriteWalletRecord(make_pair(string("tx"), hash), wtx);
374 } 469 }
375 470
376 bool EraseTx(uint256 hash) 471 bool EraseTx(uint256 hash)
377 { 472 {
378 return Erase(make_pair(string("tx"), hash)); 473 return EraseWalletRecord(make_pair(string("tx"), hash));
379 } 474 }
380 475
381 bool ReadKey(const vector<unsigned char>& vchPubKey, CPrivKey& vchPrivKey) 476 bool ReadKey(const vector<unsigned char>& vchPubKey, CPrivKey& vchPrivKey)
382 { 477 {
383 vchPrivKey.clear(); 478 vchPrivKey.clear();
384 return Read(make_pair(string("key"), vchPubKey), vchPrivKey); 479 return ReadWalletRecord(make_pair(string("key"), vchPubKey), vchPrivKey);
385 } 480 }
386 481
387 bool WriteKey(const vector<unsigned char>& vchPubKey, const CPrivKey& vchPrivKey) 482 bool WriteKey(const vector<unsigned char>& vchPubKey, const CPrivKey& vchPrivKey)
388 { 483 {
389 return Write(make_pair(string("key"), vchPubKey), vchPrivKey, false); 484 return WriteWalletRecord(make_pair(string("key"), vchPubKey), vchPrivKey, false);
390 } 485 }
391 486
392 bool ReadDefaultKey(vector<unsigned char>& vchPubKey) 487 bool ReadDefaultKey(vector<unsigned char>& vchPubKey)
393 { 488 {
394 vchPubKey.clear(); 489 vchPubKey.clear();
395 return Read(string("defaultkey"), vchPubKey); 490 return ReadWalletRecord(string("defaultkey"), vchPubKey);
396 } 491 }
397 492
398 bool WriteDefaultKey(const vector<unsigned char>& vchPubKey) 493 bool WriteDefaultKey(const vector<unsigned char>& vchPubKey)
399 { 494 {
400 return Write(string("defaultkey"), vchPubKey); 495 return WriteWalletRecord(string("defaultkey"), vchPubKey);
401 } 496 }
402 497
403 // --- Deterministic (BIP32) seed --------------------------------------- 498 // --- Deterministic (BIP32) seed ---------------------------------------
@@ -415,62 +510,75 @@ public:
415 { 510 {
416 vchMasterRet.clear(); 511 vchMasterRet.clear();
417 vchChainCodeRet.clear(); 512 vchChainCodeRet.clear();
418 return Read(string("hdmaster"), vchMasterRet) 513 return ReadWalletRecord(string("hdmaster"), vchMasterRet)
419 && Read(string("hdchaincode"), vchChainCodeRet); 514 && ReadWalletRecord(string("hdchaincode"), vchChainCodeRet);
420 } 515 }
421 516
422 bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode) 517 bool WriteHDMaster(const vector<unsigned char>& vchMaster, const vector<unsigned char>& vchChainCode)
423 { 518 {
424 return Write(string("hdmaster"), vchMaster) 519 // hdmaster and hdchaincode are halves of one seed: a wallet with one but
425 && Write(string("hdchaincode"), vchChainCode); 520 // not the other is an incomplete HD wallet (-walletstoragecheck flags
521 // exactly that). Under the SQLite runtime write them in one transaction
522 // so a failure or crash between the two cannot leave that half-state.
523 // Berkeley DB keeps its existing per-write behaviour -- the bracket is a
524 // no-op there.
525 if (!WalletSQLiteRuntimeBeginTxn())
526 return false;
527 if (!WriteWalletRecord(string("hdmaster"), vchMaster) ||
528 !WriteWalletRecord(string("hdchaincode"), vchChainCode))
529 {
530 WalletSQLiteRuntimeRollbackTxn();
531 return false;
532 }
533 return WalletSQLiteRuntimeCommitTxn();
426 } 534 }
427 535
428 bool WriteWalletMinVersion(int nVersion) 536 bool WriteWalletMinVersion(int nVersion)
429 { 537 {
430 return Write(string("walletminversion"), nVersion); 538 return WriteWalletRecord(string("walletminversion"), nVersion);
431 } 539 }
432 540
433 bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey) 541 bool WriteMasterKey(unsigned int nID, const CWalletMasterKey& kMasterKey)
434 { 542 {
435 return Write(make_pair(string("mkey"), nID), kMasterKey, true); 543 return WriteWalletRecord(make_pair(string("mkey"), nID), kMasterKey, true);
436 } 544 }
437 545
438 bool WriteCryptedKey(const vector<unsigned char>& vchPubKey, 546 bool WriteCryptedKey(const vector<unsigned char>& vchPubKey,
439 const vector<unsigned char>& vchCryptedSecret) 547 const vector<unsigned char>& vchCryptedSecret)
440 { 548 {
441 return Write(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false); 549 return WriteWalletRecord(make_pair(string("ckey"), vchPubKey), vchCryptedSecret, false);
442 } 550 }
443 551
444 bool WriteCryptedHDMaster(const vector<unsigned char>& vchCryptedMaster, 552 bool WriteCryptedHDMaster(const vector<unsigned char>& vchCryptedMaster,
445 const vector<unsigned char>& vchCryptedChainCode) 553 const vector<unsigned char>& vchCryptedChainCode)
446 { 554 {
447 return Write(string("cryptedhdmaster"), vchCryptedMaster) 555 return WriteWalletRecord(string("cryptedhdmaster"), vchCryptedMaster)
448 && Write(string("cryptedhdchaincode"), vchCryptedChainCode); 556 && WriteWalletRecord(string("cryptedhdchaincode"), vchCryptedChainCode);
449 } 557 }
450 558
451 bool WriteHDNext(unsigned int nNext) 559 bool WriteHDNext(unsigned int nNext)
452 { 560 {
453 return Write(string("hdnext"), nNext); 561 return WriteWalletRecord(string("hdnext"), nNext);
454 } 562 }
455 563
456 bool WriteHDSchema(int nSchema) 564 bool WriteHDSchema(int nSchema)
457 { 565 {
458 return Write(string("hdschema"), nSchema); 566 return WriteWalletRecord(string("hdschema"), nSchema);
459 } 567 }
460 568
461 bool WriteHDCoinType(unsigned int nCoinType) 569 bool WriteHDCoinType(unsigned int nCoinType)
462 { 570 {
463 return Write(string("hdcointype"), nCoinType); 571 return WriteWalletRecord(string("hdcointype"), nCoinType);
464 } 572 }
465 573
466 bool WriteHDReceiveNext(unsigned int nNext) 574 bool WriteHDReceiveNext(unsigned int nNext)
467 { 575 {
468 return Write(string("hdreceivenext"), nNext); 576 return WriteWalletRecord(string("hdreceivenext"), nNext);
469 } 577 }
470 578
471 bool WriteHDChangeNext(unsigned int nNext) 579 bool WriteHDChangeNext(unsigned int nNext)
472 { 580 {
473 return Write(string("hdchangenext"), nNext); 581 return WriteWalletRecord(string("hdchangenext"), nNext);
474 } 582 }
475 583
476 // Keys generated ahead of time and not yet handed out. The private key of 584 // Keys generated ahead of time and not yet handed out. The private key of
@@ -478,30 +586,43 @@ public:
478 // record only says that this one is still unspoken for. 586 // record only says that this one is still unspoken for.
479 bool WritePool(int64 nIndex, const vector<unsigned char>& vchPubKey) 587 bool WritePool(int64 nIndex, const vector<unsigned char>& vchPubKey)
480 { 588 {
481 return Write(make_pair(string("pool"), nIndex), vchPubKey); 589 return WriteWalletRecord(make_pair(string("pool"), nIndex), vchPubKey);
482 } 590 }
483 591
484 bool ErasePool(int64 nIndex) 592 bool ErasePool(int64 nIndex)
485 { 593 {
486 return Erase(make_pair(string("pool"), nIndex)); 594 return EraseWalletRecord(make_pair(string("pool"), nIndex));
487 } 595 }
488 596
489 template<typename T> 597 template<typename T>
490 bool ReadSetting(const string& strKey, T& value) 598 bool ReadSetting(const string& strKey, T& value)
491 { 599 {
492 return Read(make_pair(string("setting"), strKey), value); 600 return ReadWalletRecord(make_pair(string("setting"), strKey), value);
493 } 601 }
494 602
495 template<typename T> 603 template<typename T>
496 bool WriteSetting(const string& strKey, const T& value) 604 bool WriteSetting(const string& strKey, const T& value)
497 { 605 {
498 return Write(make_pair(string("setting"), strKey), value); 606 return WriteWalletRecord(make_pair(string("setting"), strKey), value);
499 } 607 }
500 608
501 bool LoadWallet(vector<unsigned char>& vchDefaultKeyRet); 609 bool LoadWallet(vector<unsigned char>& vchDefaultKeyRet);
502}; 610};
503 611
612class CWalletRecordVisitor
613{
614public:
615 virtual ~CWalletRecordVisitor() { }
616 virtual bool VisitWalletRecord(const CDataStream& ssKey,
617 const CDataStream& ssValue,
618 string& strErrorRet) = 0;
619};
620
621bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet);
622
504bool LoadWallet(); 623bool LoadWallet();
624bool LoadWalletFromSQLite(const string& strPath);
625bool LoadWalletFromSQLiteRuntime(const string& strPath);
505 626
506// Writes a wallet.dat that opens on its own, anywhere -- see the definition in 627// Writes a wallet.dat that opens on its own, anywhere -- see the definition in
507// db.cpp for why a plain file copy does not. Still a point-in-time snapshot: 628// db.cpp for why a plain file copy does not. Still a point-in-time snapshot:
diff --git a/src/gui.cpp b/src/gui.cpp
index 9b3802b..ff6059a 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -11,6 +11,11 @@
11#include <ctime> 11#include <ctime>
12#include <cstdio> 12#include <cstdio>
13#include <sstream> 13#include <sstream>
14#ifdef _WIN32
15#include <shellapi.h> // ShellExecuteA, to open the website in the browser
16#else
17#include <unistd.h>
18#endif
14#include <iomanip> 19#include <iomanip>
15#include <atomic> 20#include <atomic>
16#include <mutex> 21#include <mutex>
@@ -173,7 +178,11 @@ static bool PathIsInsideDataDir(const std::string& strPath)
173 178
174static void SetDefaultBackupPath() 179static void SetDefaultBackupPath()
175{ 180{
176 std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; 181 // Match the extension to the live backend: BackupWallet copies wallet.sqlite
182 // under the SQLite backend and wallet.dat under Berkeley DB, so a ".dat"
183 // default name would put SQLite contents in a misleadingly-named file.
184 const char* pszExt = WalletSQLiteRuntimeActive() ? ".sqlite" : ".dat";
185 std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + pszExt;
177 strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); 186 strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1);
178 g_backupPath[sizeof(g_backupPath)-1] = '\0'; 187 g_backupPath[sizeof(g_backupPath)-1] = '\0';
179} 188}
@@ -1280,11 +1289,11 @@ static void DrawWalletSafetyDialog()
1280 } 1289 }
1281 ImGui::Text("Phrase-backed spendable balance: %s BTF", 1290 ImGui::Text("Phrase-backed spendable balance: %s BTF",
1282 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); 1291 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str());
1283 ImGui::Text("Wallet.dat-only spendable balance: %s BTF", 1292 ImGui::Text("Not phrase-backed spendable balance: %s BTF",
1284 FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); 1293 FmtMoney(g_recoveryAudit.nLegacyCredit).c_str());
1285 ImGui::Text("Phrase-backed immature mining rewards: %s BTF", 1294 ImGui::Text("Phrase-backed immature mining rewards: %s BTF",
1286 FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); 1295 FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str());
1287 ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", 1296 ImGui::Text("Not phrase-backed immature mining rewards: %s BTF",
1288 FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); 1297 FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str());
1289 if (!g_recoveryAudit.fDeriveComplete) 1298 if (!g_recoveryAudit.fDeriveComplete)
1290 { 1299 {
@@ -1298,8 +1307,8 @@ static void DrawWalletSafetyDialog()
1298 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); 1307 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f));
1299 ImGui::TextWrapped( 1308 ImGui::TextWrapped(
1300 "Some coins are on keys the phrase does not reproduce. " 1309 "Some coins are on keys the phrase does not reproduce. "
1301 "Keep wallet.dat backups until that balance has been moved to a " 1310 "Keep a file backup of this wallet until that balance has been "
1302 "phrase-backed address."); 1311 "moved to a phrase-backed address.");
1303 ImGui::PopStyleColor(); 1312 ImGui::PopStyleColor();
1304 } 1313 }
1305 else if (HaveHDSeed() && 1314 else if (HaveHDSeed() &&
@@ -1395,16 +1404,38 @@ static void DrawDiagnosticsDialog()
1395 ImGui::End(); 1404 ImGui::End();
1396} 1405}
1397 1406
1407static void OpenURL(const char* pszUrl)
1408{
1409#ifdef _WIN32
1410 ShellExecuteA(NULL, "open", pszUrl, NULL, NULL, SW_SHOWNORMAL);
1411#else
1412 std::string cmd = std::string("xdg-open '") + pszUrl + "' >/dev/null 2>&1 &";
1413 int r = system(cmd.c_str());
1414 (void)r;
1415#endif
1416}
1417
1398static void DrawAboutDialog() 1418static void DrawAboutDialog()
1399{ 1419{
1400 if (!g_showAbout) return; 1420 if (!g_showAbout) return;
1401 ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); 1421 ImGui::SetNextWindowSize(ImVec2(380.0f, 205.0f), ImGuiCond_Always);
1402 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), 1422 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1403 ImGuiCond_Always, ImVec2(0.5f, 0.5f)); 1423 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1404 if (ImGui::Begin("About Bitflash", &g_showAbout, 1424 if (ImGui::Begin("About Bitflash", &g_showAbout,
1405 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) 1425 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1406 { 1426 {
1407 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); 1427 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
1428 "Bitflash BTF v" BITFLASH_VERSION_STRING);
1429 ImGui::Spacing();
1430 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, 1.0f));
1431 ImGui::TextUnformatted("https://bitflash.network");
1432 ImGui::PopStyleColor();
1433 if (ImGui::IsItemHovered())
1434 {
1435 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
1436 if (ImGui::IsMouseClicked(0))
1437 OpenURL("https://bitflash.network");
1438 }
1408 ImGui::Spacing(); 1439 ImGui::Spacing();
1409 ImGui::TextWrapped( 1440 ImGui::TextWrapped(
1410 "CPU-only cryptocurrency. RandomX proof of work. " 1441 "CPU-only cryptocurrency. RandomX proof of work. "
@@ -1417,6 +1448,184 @@ static void DrawAboutDialog()
1417 ImGui::End(); 1448 ImGui::End();
1418} 1449}
1419 1450
1451// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the
1452// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite
1453// and records the backend marker (wallet.dat is left untouched, so a restart
1454// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB"
1455// just records that choice so the offer does not return; "Decide later" leaves
1456// no marker and asks again next start.
1457static bool g_showBackendWizard = false;
1458static std::string g_wizardStatus;
1459static bool g_wizardConverted = false;
1460static int g_restartArgc = 0;
1461static char** g_restartArgv = NULL;
1462
1463#ifdef _WIN32
1464// Quote one argument so CommandLineToArgvW parses it back to exactly this
1465// string: only backslashes that immediately precede a double quote (including
1466// the closing one) are doubled, so ordinary Windows paths are not mangled.
1467static std::string QuoteArgWin(const std::string& a)
1468{
1469 if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos)
1470 return a;
1471 std::string r = "\"";
1472 size_t bs = 0;
1473 for (size_t i = 0; i < a.size(); i++)
1474 {
1475 if (a[i] == '\\') { bs++; continue; }
1476 if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; }
1477 r.append(bs, '\\'); bs = 0; r += a[i];
1478 }
1479 r.append(bs * 2, '\\');
1480 r += "\"";
1481 return r;
1482}
1483#endif
1484
1485// Relaunch Bitflash with the same arguments and datadir, then ask this instance
1486// to exit -- but only once the relaunch has actually started, so a failure never
1487// leaves the user with no node running. The new instance waits (-restartwait)
1488// before it opens the network port and the block index, so this one is gone
1489// first and they never fight over the same files.
1490static void RestartApplication()
1491{
1492 bool fLaunched = false;
1493#ifdef _WIN32
1494 char exePath[MAX_PATH];
1495 if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0)
1496 return;
1497 std::string strCmd = QuoteArgWin(exePath);
1498 bool fHasDataDir = false;
1499 for (int i = 1; i < g_restartArgc; i++)
1500 {
1501 std::string a = g_restartArgv[i] ? g_restartArgv[i] : "";
1502 if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0)
1503 fHasDataDir = true;
1504 strCmd += " " + QuoteArgWin(a);
1505 }
1506 if (!fHasDataDir)
1507 strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir());
1508 strCmd += " -restartwait";
1509
1510 std::vector<char> cmdBuf(strCmd.begin(), strCmd.end());
1511 cmdBuf.push_back('\0');
1512 STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si);
1513 PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi));
1514 if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
1515 {
1516 CloseHandle(pi.hProcess);
1517 CloseHandle(pi.hThread);
1518 fLaunched = true;
1519 }
1520#else
1521 pid_t pid = fork();
1522 if (pid == 0)
1523 {
1524 // Child: wait for the parent to release the port and files, then re-exec.
1525 // /proc/self/exe is the reliable absolute path; argv[0] may be relative
1526 // or a bare name that execv() cannot resolve.
1527 sleep(2);
1528 char exePath[4096];
1529 ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
1530 if (len > 0)
1531 {
1532 exePath[len] = '\0';
1533 execv(exePath, g_restartArgv);
1534 }
1535 if (g_restartArgv)
1536 execv(g_restartArgv[0], g_restartArgv);
1537 _exit(127);
1538 }
1539 fLaunched = (pid > 0);
1540#endif
1541
1542 if (fLaunched)
1543 {
1544 fShutdown = true;
1545 if (glfwGetCurrentContext())
1546 glfwSetWindowShouldClose(glfwGetCurrentContext(), true);
1547 }
1548 else
1549 g_wizardStatus = "Could not relaunch automatically -- please restart "
1550 "Bitflash yourself to switch to the SQLite wallet.";
1551}
1552
1553static void DrawBackendWizardDialog()
1554{
1555 if (!g_showBackendWizard) return;
1556 ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always);
1557 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1558 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1559 if (ImGui::Begin("Wallet Storage", &g_showBackendWizard,
1560 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1561 {
1562 if (g_wizardConverted)
1563 {
1564 ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite.");
1565 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1566 ImGui::Spacing();
1567 ImGui::TextWrapped(
1568 "The switch takes effect when Bitflash restarts. Until then it is "
1569 "still running on the old Berkeley DB wallet. Your wallet.dat is "
1570 "kept untouched as a fallback.");
1571 ImGui::Spacing();
1572 if (ImGui::Button("Restart Now", ImVec2(140.0f, 0.0f)))
1573 RestartApplication();
1574 ImGui::SameLine();
1575 if (ImGui::Button("Later", ImVec2(120.0f, 0.0f)))
1576 g_showBackendWizard = false;
1577 ImGui::Spacing();
1578 ImGui::TextDisabled(
1579 "If you keep mining before restarting, new coins go to the old "
1580 "wallet until you switch.");
1581 }
1582 else
1583 {
1584 ImGui::TextWrapped(
1585 "Bitflash can store your wallet in a new SQLite format: a single "
1586 "file that is easier to back up and does not depend on the "
1587 "database/ directory beside it.");
1588 ImGui::Spacing();
1589 ImGui::TextWrapped(
1590 "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it "
1591 "is -- nothing is deleted, and you can go back to it at any time. "
1592 "The change takes effect after you restart.");
1593 ImGui::Spacing();
1594 if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f)))
1595 {
1596 int nCopied = 0;
1597 std::string strError;
1598 if (DoConvertWalletToSQLite(nCopied, strError))
1599 {
1600 g_wizardStatus =
1601 strprintf("%d record(s) copied to wallet.sqlite.", nCopied);
1602 g_wizardConverted = true;
1603 }
1604 else
1605 g_wizardStatus = "Conversion failed: " + strError;
1606 }
1607 ImGui::SameLine();
1608 if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f)))
1609 {
1610 WriteWalletBackendMarker("bdb");
1611 g_showBackendWizard = false;
1612 }
1613 ImGui::SameLine();
1614 if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f)))
1615 g_showBackendWizard = false;
1616
1617 if (!g_wizardStatus.empty())
1618 {
1619 ImGui::Spacing();
1620 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f));
1621 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1622 ImGui::PopStyleColor();
1623 }
1624 }
1625 }
1626 ImGui::End();
1627}
1628
1420// --------------------------------------------------------------------------- 1629// ---------------------------------------------------------------------------
1421// Entry point 1630// Entry point
1422// --------------------------------------------------------------------------- 1631// ---------------------------------------------------------------------------
@@ -1483,6 +1692,21 @@ int RunGUI(int argc, char* argv[])
1483 g_mineRadio = nMineMode; 1692 g_mineRadio = nMineMode;
1484 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); 1693 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1);
1485 1694
1695 // Kept for the wizard's "Restart Now": relaunch with the same command line.
1696 g_restartArgc = argc;
1697 g_restartArgv = argv;
1698
1699 // First run on a Berkeley DB wallet with no recorded choice yet: offer to
1700 // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists,
1701 // or once a choice has been recorded in the backend marker.
1702 if (!WalletSQLiteRuntimeActive() &&
1703 FileExists((GetAppDir() + "/wallet.dat").c_str()) &&
1704 !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) &&
1705 ReadWalletBackendMarker().empty())
1706 {
1707 g_showBackendWizard = true;
1708 }
1709
1486 int frame = 0; 1710 int frame = 0;
1487 while (!glfwWindowShouldClose(window) && !fShutdown) 1711 while (!glfwWindowShouldClose(window) && !fShutdown)
1488 { 1712 {
@@ -1506,6 +1730,7 @@ int RunGUI(int argc, char* argv[])
1506 DrawRestorePhraseDialog(); 1730 DrawRestorePhraseDialog();
1507 DrawDiagnosticsDialog(); 1731 DrawDiagnosticsDialog();
1508 DrawAboutDialog(); 1732 DrawAboutDialog();
1733 DrawBackendWizardDialog();
1509 DrawStatusBar(); 1734 DrawStatusBar();
1510 1735
1511 ImGui::Render(); 1736 ImGui::Render();
diff --git a/src/headers_core.h b/src/headers_core.h
index 6a60b52..2824126 100644
--- a/src/headers_core.h
+++ b/src/headers_core.h
@@ -1,6 +1,11 @@
1// Core node headers -- no GUI framework dependency. 1// Core node headers -- no GUI framework dependency.
2// Included by gui.cpp and all non-GUI source files. 2// Included by gui.cpp and all non-GUI source files.
3 3
4// The one place the human-readable release version lives -- bump it at release.
5// It is what the About dialog shows. Protocol and transaction versions elsewhere
6// (nVersion fields) are unrelated and must not be changed for a release.
7#define BITFLASH_VERSION_STRING "1.2.20"
8
4#ifdef _MSC_VER 9#ifdef _MSC_VER
5#pragma warning(disable:4786) 10#pragma warning(disable:4786)
6#endif 11#endif
diff --git a/src/main.cpp b/src/main.cpp
index 018627d..7398064 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5184,6 +5184,9 @@ WalletRecoveryAudit GetWalletRecoveryAudit()
5184 CRITICAL_BLOCK(cs_keyPool) 5184 CRITICAL_BLOCK(cs_keyPool)
5185 { 5185 {
5186 audit.fHaveSeed = HaveHDSeed(); 5186 audit.fHaveSeed = HaveHDSeed();
5187 audit.fSeedEncryptedLocked = !audit.fHaveSeed &&
5188 (!vchCryptedHDMaster.empty() ||
5189 !vchCryptedHDChainCode.empty());
5187 audit.nSchema = nHDKeySchema; 5190 audit.nSchema = nHDKeySchema;
5188 audit.nDerivedKnown = nHDNext; 5191 audit.nDerivedKnown = nHDNext;
5189 audit.nReceiveNext = nHDReceiveNext; 5192 audit.nReceiveNext = nHDReceiveNext;
diff --git a/src/main.h b/src/main.h
index 30e25ef..e05b467 100644
--- a/src/main.h
+++ b/src/main.h
@@ -335,6 +335,7 @@ int64 GetBalance();
335struct WalletRecoveryAudit 335struct WalletRecoveryAudit
336{ 336{
337 bool fHaveSeed; 337 bool fHaveSeed;
338 bool fSeedEncryptedLocked;
338 bool fDeriveComplete; 339 bool fDeriveComplete;
339 int nSchema; 340 int nSchema;
340 unsigned int nDerivedKnown; 341 unsigned int nDerivedKnown;
@@ -354,6 +355,7 @@ struct WalletRecoveryAudit
354 WalletRecoveryAudit() 355 WalletRecoveryAudit()
355 { 356 {
356 fHaveSeed = false; 357 fHaveSeed = false;
358 fSeedEncryptedLocked = false;
357 fDeriveComplete = true; 359 fDeriveComplete = true;
358 nSchema = HD_SCHEMA_NONE; 360 nSchema = HD_SCHEMA_NONE;
359 nDerivedKnown = 0; 361 nDerivedKnown = 0;
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 0e64985..dd001c8 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -115,8 +115,12 @@ static void PrintUsage()
115 printf(" /maxdebugfilesize=N (roll debug.log at N MB, default 1024; 0 = never)\n"); 115 printf(" /maxdebugfilesize=N (roll debug.log at N MB, default 1024; 0 = never)\n");
116 printf(" /gen\n"); 116 printf(" /gen\n");
117 printf(" /nogui or /daemon\n"); 117 printf(" /nogui or /daemon\n");
118 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); 118 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n");
119 printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n"); 119 printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n");
120 printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n");
121 printf(" net-message,\n");
122 printf(" consensus-limits, pool-stratum,\n");
123 printf(" parse-money, socks5-proxy, or managed-tor\n");
120 printf("\n"); 124 printf("\n");
121 printf("Mining mode:\n"); 125 printf("Mining mode:\n");
122 printf(" /operator\n"); 126 printf(" /operator\n");
@@ -177,6 +181,26 @@ static void PrintUsage()
177 printf(" /recoveryaudit (show how much spendable balance is\n"); 181 printf(" /recoveryaudit (show how much spendable balance is\n");
178 printf(" covered by the recovery phrase;\n"); 182 printf(" covered by the recovery phrase;\n");
179 printf(" exits 2 when wallet.dat is still needed)\n"); 183 printf(" exits 2 when wallet.dat is still needed)\n");
184 printf(" /walletstorageaudit (count wallet.dat record types without\n");
185 printf(" printing wallet values, then exit)\n");
186 printf(" /walletstorageauditjson=FILE\n");
187 printf(" (write the same audit as deterministic JSON)\n");
188 printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n");
189 printf(" wallet.dat storage state)\n");
190 printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n");
191 printf(" SQLite store, then exit)\n");
192 printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n");
193 printf(" printing counts only, then exit)\n");
194 printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n");
195 printf(" an empty data directory, then exit)\n");
196 printf(" /walletsqliteloadcheck=FILE\n");
197 printf(" (parse a SQLite export like the wallet loader)\n");
198 printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n");
199 printf(" /walletbackend=sqlite (opt-in: run the node on <datadir>/wallet.sqlite\n");
200 printf(" instead of wallet.dat; export one first)\n");
201 printf(" /createwallet (headless only: permit creating a new wallet in a\n");
202 printf(" datadir that has none -- otherwise refused, since\n");
203 printf(" a missing wallet is usually a mount/backup fault)\n");
180 printf(" /rescan (walk the chain for coins this wallet owns\n"); 204 printf(" /rescan (walk the chain for coins this wallet owns\n");
181 printf(" but never recorded, then exit)\n"); 205 printf(" but never recorded, then exit)\n");
182 printf("\n"); 206 printf("\n");
@@ -580,10 +604,119 @@ int main(int argc, char* argv[])
580 604
581 ParseStartupArguments(argc, argv); 605 ParseStartupArguments(argc, argv);
582 606
607#ifdef _WIN32
608 // Set by the GUI "Restart Now" button: give the previous instance a moment
609 // to release the network port and the block-index files before this one
610 // opens them, so a relaunch does not race the process it is replacing.
611 if (arg(argc, argv, "/restartwait") || arg(argc, argv, "-restartwait"))
612 Sleep(2500);
613#endif
614
615 // Hidden self-test helper. It must run before LoadWallet(): several
616 // storage-sanity scenarios deliberately make wallet.dat unsafe to load.
617 string strSelfTestMutateWallet =
618 argval2(argc, argv, "/selftestmutatewallet", "-selftestmutatewallet");
619 if (!strSelfTestMutateWallet.empty())
620 return RunSelfTestMutateWallet(strSelfTestMutateWallet);
621
583 string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); 622 string strSelfTest = argval2(argc, argv, "/selftest", "-selftest");
584 if (!strSelfTest.empty()) 623 if (!strSelfTest.empty())
585 return RunSelfTest(strSelfTest); 624 return RunSelfTest(strSelfTest);
586 625
626 // Storage diagnostics must run before LoadWallet(). Their job is to
627 // explain wallet.dat states that the normal wallet loader may refuse.
628 string strWalletStorageAuditJson =
629 argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson");
630 bool fDiagWantsAudit = arg(argc,argv,"/walletstorageaudit") ||
631 arg(argc,argv,"-walletstorageaudit") ||
632 !strWalletStorageAuditJson.empty();
633 bool fDiagWantsCheck = arg(argc,argv,"/walletstoragecheck") ||
634 arg(argc,argv,"-walletstoragecheck");
635
636 // These diagnostics normally read wallet.dat. Under -walletbackend=sqlite
637 // the live wallet is wallet.sqlite, so open it read-only as the active
638 // store and let the same scan walk the right file. This runs before the
639 // normal backend resolution further down, so validate the value here too.
640 string strDiagBackend = argval2(argc, argv, "/walletbackend", "-walletbackend");
641 if ((fDiagWantsAudit || fDiagWantsCheck) && !strDiagBackend.empty() &&
642 strDiagBackend != "sqlite" && strDiagBackend != "bdb")
643 {
644 AttachTerminal();
645 fprintf(stderr, "Unknown wallet backend '%s'. Use -walletbackend=sqlite "
646 "or -walletbackend=bdb.\n", strDiagBackend.c_str());
647 return 1;
648 }
649 bool fDiagSQLite = (strDiagBackend == "sqlite") &&
650 (fDiagWantsAudit || fDiagWantsCheck);
651 if (fDiagSQLite)
652 {
653 AttachTerminal();
654 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
655 string strOpenErr;
656 if (!WalletSQLiteRuntimeOpenReadOnly(strSQLitePath, strOpenErr))
657 {
658 fprintf(stderr, "Cannot open the SQLite wallet for diagnostics: %s\n",
659 strOpenErr.c_str());
660 return 1;
661 }
662 }
663
664 if (fDiagWantsAudit)
665 {
666 int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson);
667 if (fDiagSQLite)
668 WalletSQLiteRuntimeClose();
669 else
670 DBFlush(true);
671 return nRet;
672 }
673
674 if (fDiagWantsCheck)
675 {
676 int nRet = CmdWalletStorageCheck();
677 if (fDiagSQLite)
678 WalletSQLiteRuntimeClose();
679 else
680 DBFlush(true);
681 return nRet;
682 }
683
684 string strWalletSQLiteExport =
685 argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport");
686 if (!strWalletSQLiteExport.empty())
687 {
688 int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport);
689 DBFlush(true);
690 return nRet;
691 }
692
693 string strWalletSQLiteVerify =
694 argval2(argc, argv, "/walletsqliteverify", "-walletsqliteverify");
695 if (!strWalletSQLiteVerify.empty())
696 {
697 int nRet = CmdWalletSQLiteVerify(strWalletSQLiteVerify);
698 DBFlush(true);
699 return nRet;
700 }
701
702 string strWalletSQLiteRestore =
703 argval2(argc, argv, "/walletsqliterestore", "-walletsqliterestore");
704 if (!strWalletSQLiteRestore.empty())
705 {
706 int nRet = CmdWalletSQLiteRestore(strWalletSQLiteRestore);
707 DBFlush(true);
708 return nRet;
709 }
710
711 string strWalletSQLiteLoadCheck =
712 argval2(argc, argv, "/walletsqliteloadcheck", "-walletsqliteloadcheck");
713 if (!strWalletSQLiteLoadCheck.empty())
714 {
715 int nRet = CmdWalletSQLiteLoadCheck(strWalletSQLiteLoadCheck);
716 DBFlush(true);
717 return nRet;
718 }
719
587 // Berkeley DB reports failure by throwing, and CDB's constructor lets it 720 // Berkeley DB reports failure by throwing, and CDB's constructor lets it
588 // through. Nothing on this path caught anything, so an unreadable 721 // through. Nothing on this path caught anything, so an unreadable
589 // blkindex.dat or wallet.dat -- one written by another platform's Berkeley 722 // blkindex.dat or wallet.dat -- one written by another platform's Berkeley
@@ -628,18 +761,197 @@ int main(int argc, char* argv[])
628 printf("Loading wallet...\n"); 761 printf("Loading wallet...\n");
629 try 762 try
630 { 763 {
631 if (!LoadWallet()) 764 string strWalletBackend =
765 argval2(argc, argv, "/walletbackend", "-walletbackend");
766 string strWalletSQLite =
767 argval2(argc, argv, "/walletsqlite", "-walletsqlite");
768
769 // A misspelled backend must not fall through to Berkeley DB silently --
770 // someone who typed -walletbackend=sqlit meant to run on SQLite, and
771 // opening wallet.dat instead of telling them is the wrong surprise.
772 if (!strWalletBackend.empty() &&
773 strWalletBackend != "sqlite" && strWalletBackend != "bdb")
632 { 774 {
633 // LoadWallet() explains itself into strWalletLoadError when it 775 AttachTerminal();
634 // knows why -- an unsupported wallet format, for one, which is the 776 FatalStartupError(fHeadlessStartup,
635 // case this whole path exists to make visible. 777 strprintf("Unknown wallet backend '%s'.", strWalletBackend.c_str()),
636 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", 778 "Use -walletbackend=sqlite for the SQLite backend, or "
637 strWalletLoadError); 779 "-walletbackend=bdb for the Berkeley DB wallet.dat.");
638 return 1; 780 return 1;
639 } 781 }
640 782
783 if (strWalletBackend == "sqlite")
784 {
785 // Explicit opt-in via the flag. The node runs on
786 // <datadir>/wallet.sqlite and never opens wallet.dat, but the user
787 // has to have exported one first -- there is no silent migration,
788 // and wallet.dat is left
789 // exactly where it is so a plain restart goes back to it.
790 AttachTerminal();
791 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
792 if (!FileExists(strSQLitePath.c_str()))
793 {
794 string strGuide = strprintf(
795 "No SQLite wallet was found at\n"
796 " %s\n\n"
797 "The SQLite wallet backend is opt-in and needs an exported\n"
798 "wallet first. Your Berkeley DB wallet.dat is not touched by\n"
799 "any of this.\n\n"
800 " Step 1 Export your existing wallet to SQLite:\n"
801 " bitflash -walletsqliteexport=\"%s\"\n"
802 " Step 2 Start again with the SQLite backend:\n"
803 " bitflash -walletbackend=sqlite\n\n"
804 "To go back to Berkeley DB at any time, just start without\n"
805 "-walletbackend=sqlite. wallet.dat is still your wallet.",
806 strSQLitePath.c_str(), strSQLitePath.c_str());
807 FatalStartupError(fHeadlessStartup,
808 "The SQLite wallet backend needs an exported wallet first.",
809 strGuide);
810 return 1;
811 }
812
813 if (!LoadWalletFromSQLiteRuntime(strSQLitePath))
814 {
815 FatalStartupError(fHeadlessStartup,
816 "Cannot open the SQLite wallet backend.",
817 strWalletLoadError);
818 return 1;
819 }
820
821 fprintf(stderr,
822 "Wallet backend: SQLite\n"
823 " %s\n"
824 "Your Berkeley DB wallet.dat is left untouched. Restart without\n"
825 "-walletbackend=sqlite to go back to it.\n",
826 strSQLitePath.c_str());
827 fflush(stderr);
828 // Fall through and run the node normally: every CWalletDB read,
829 // write, and erase now routes to this SQLite file.
830 }
831 else if (!strWalletSQLite.empty())
832 {
833 AttachTerminal();
834 if (!LoadWalletFromSQLite(strWalletSQLite))
835 {
836 FatalStartupError(fHeadlessStartup,
837 "Cannot open SQLite wallet export.",
838 strWalletLoadError);
839 return 1;
840 }
841
842 fprintf(stderr, "SQLite wallet loaded: read-only staging backend\n");
843 fprintf(stderr, "SQLite runtime writes are not enabled yet; exiting before "
844 "network, mining, GUI, or wallet mutation starts.\n");
845 fflush(stderr);
846 DBFlush(true);
847 return 0;
848 }
849 else if (strWalletBackend == "bdb")
850 {
851 // Explicit -walletbackend=bdb overrides the marker and the default:
852 // force Berkeley DB (LoadWallet creates wallet.dat if none exists).
853 if (!LoadWallet())
854 {
855 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.",
856 strWalletLoadError);
857 return 1;
858 }
859 }
860 else
861 {
862 // No explicit -walletbackend on the command line. Resolve it from
863 // the persistent marker, then defaults: an existing wallet.sqlite
864 // stays on SQLite, an existing wallet.dat stays on Berkeley DB (the
865 // GUI offers to convert), and a fresh install starts on SQLite --
866 // the new default. wallet.dat is never touched by any of this.
867 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
868 bool fSQLiteExists = FileExists(strSQLitePath.c_str());
869 bool fWalletDatExists = FileExists((GetAppDir() + "/wallet.dat").c_str());
870 string strMarker = ReadWalletBackendMarker();
871
872 bool fUseSQLite = false;
873 bool fCreateSQLite = false;
874 if (strMarker == "sqlite")
875 {
876 if (fSQLiteExists) fUseSQLite = true;
877 else if (fWalletDatExists) fUseSQLite = false; // marker stale; real wallet is BDB
878 else { fUseSQLite = true; fCreateSQLite = true; }
879 }
880 else if (strMarker == "bdb")
881 {
882 fUseSQLite = false;
883 }
884 else // no marker recorded yet
885 {
886 if (fSQLiteExists && !fWalletDatExists) fUseSQLite = true;
887 else if (fWalletDatExists) fUseSQLite = false;
888 else { fUseSQLite = true; fCreateSQLite = true; }
889 }
890
891 // A headless node whose datadir holds no wallet of either kind is
892 // far more often an operations fault -- datadir not mounted, or
893 // wallet.dat caught mid-rename by a backup -- than a first run.
894 // Creating one here is not the harmless default it looks like:
895 // success also writes the backend marker, and from then on
896 // restoring the real wallet.dat no longer takes effect, because the
897 // marker keeps the node on the empty wallet it just made. Under
898 // 1.2.19 the same fault created a wallet.dat that a restore
899 // overwrote, so operators could recover by putting the file back.
900 // Refuse instead, and name the flag that makes it deliberate.
901 if (fCreateSQLite && fHeadlessStartup &&
902 !arg(argc, argv, "/createwallet") && !arg(argc, argv, "-createwallet"))
903 {
904 AttachTerminal();
905 fprintf(stderr,
906 "No wallet found in %s\n"
907 "Refusing to create one. On a headless node an absent wallet is\n"
908 "usually a mount or backup fault, and creating one would record a\n"
909 "backend marker that makes restoring the real wallet a no-op.\n"
910 "Pass -createwallet to start a new one deliberately, or\n"
911 "-walletbackend=bdb to run on a Berkeley DB wallet.dat.\n",
912 GetAppDir().c_str());
913 fflush(stderr);
914 return 1;
915 }
916
917 if (fUseSQLite)
918 {
919 AttachTerminal();
920 bool fOk = fCreateSQLite
921 ? CreateNewSQLiteWallet(strSQLitePath)
922 : LoadWalletFromSQLiteRuntime(strSQLitePath);
923 if (!fOk)
924 {
925 FatalStartupError(fHeadlessStartup,
926 fCreateSQLite ? "Cannot create the SQLite wallet."
927 : "Cannot open the SQLite wallet.",
928 strWalletLoadError);
929 return 1;
930 }
931 // Record the choice so it is stable from here on.
932 if (ReadWalletBackendMarker() != "sqlite")
933 WriteWalletBackendMarker("sqlite");
934 fprintf(stderr, "Wallet backend: SQLite\n %s\n%s",
935 strSQLitePath.c_str(),
936 fCreateSQLite ? "A new wallet was created here.\n" : "");
937 fflush(stderr);
938 // Fall through and run the node; CWalletDB routes to this file.
939 }
940 else if (!LoadWallet())
941 {
942 // LoadWallet() explains itself into strWalletLoadError when it
943 // knows why -- an unsupported wallet format, for one, which is
944 // the case this whole path exists to make visible.
945 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.",
946 strWalletLoadError);
947 return 1;
948 }
949 }
950
641 // After the block index, so there is a chain to compare the wallet 951 // After the block index, so there is a chain to compare the wallet
642 // against, and before anything reports a balance. 952 // against, and before anything reports a balance. Works on either
953 // backend: RescanSpentFlags writes corrected flags through CWalletDB,
954 // which routes to SQLite when that backend is active.
643 RescanSpentFlags(); 955 RescanSpentFlags();
644 } 956 }
645 catch (const std::exception& e) 957 catch (const std::exception& e)
@@ -908,6 +1220,10 @@ int main(int argc, char* argv[])
908 // called here, which is why a wallet.dat copied elsewhere would not 1220 // called here, which is why a wallet.dat copied elsewhere would not
909 // open -- issue #40. Not optional. 1221 // open -- issue #40. Not optional.
910 DBFlush(true); 1222 DBFlush(true);
1223 // Checkpoint and close the SQLite wallet backend if one is active. A
1224 // no-op on the Berkeley DB path. Safe to kill before this runs: WAL +
1225 // synchronous=FULL already made every committed write durable.
1226 WalletSQLiteRuntimeClose();
911#ifdef _WIN32 1227#ifdef _WIN32
912 // wallet.dat is safely flushed and closed now; let a pending console 1228 // wallet.dat is safely flushed and closed now; let a pending console
913 // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish. 1229 // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish.
@@ -924,6 +1240,7 @@ int main(int argc, char* argv[])
924 StopNode(); 1240 StopNode();
925 BtfStopManagedTor(); 1241 BtfStopManagedTor();
926 DBFlush(true); 1242 DBFlush(true);
1243 WalletSQLiteRuntimeClose();
927 return ret; 1244 return ret;
928#endif 1245#endif
929} 1246}
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 0ba1d76..740bba6 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -24,7 +24,7 @@ CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \
24 24
25LIBS = -L$(DEPS)/lib -lsecp256k1 \ 25LIBS = -L$(DEPS)/lib -lsecp256k1 \
26 -L$(RANDOMX)/build -lrandomx \ 26 -L$(RANDOMX)/build -lrandomx \
27 -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ 27 -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \
28 -LC:/msys64/ucrt64/lib \ 28 -LC:/msys64/ucrt64/lib \
29 -lglfw3 -lopengl32 -lgdi32 \ 29 -lglfw3 -lopengl32 -lgdi32 \
30 -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \ 30 -lkernel32 -luser32 -lcomdlg32 -lwinspool -lwinmm \
@@ -37,7 +37,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
38 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 38 obj/sha.o obj/nostr.o obj/randomx_pow.o \
39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o 40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \
41 obj/proxy.o obj/tor.o
41 42
42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 43GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
43 44
@@ -51,7 +52,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 52HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
52 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 53 key.h bignum.h script.h db.h base58.h net.h nostr.h \
53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 54 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
54 selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h 55 selftest.h walletcmd.h wallet_sqlite.h sockcount.h proxy.h tor.h compat.h font_roboto.h
55 56
56all: bitflash.exe 57all: bitflash.exe
57 58
@@ -107,8 +108,13 @@ tests: test_bip32.exe bitflash.exe
107 ./bitflash.exe -selftest=wallet-keypool 108 ./bitflash.exe -selftest=wallet-keypool
108 ./bitflash.exe -selftest=wallet-hd 109 ./bitflash.exe -selftest=wallet-hd
109 ./bitflash.exe -selftest=wallet-format 110 ./bitflash.exe -selftest=wallet-format
111 ./bitflash.exe -selftest=wallet-sqlite
112 ./bitflash.exe -selftest=wallet-sqlite-migration
110 ./bitflash.exe -selftest=wallet-crypto 113 ./bitflash.exe -selftest=wallet-crypto
111 ./bitflash.exe -selftest=wallet-encrypt 114 ./bitflash.exe -selftest=wallet-encrypt
115 ./bitflash.exe -selftest=wallet-sqlite-encrypt
116 ./bitflash.exe -selftest=wallet-backend-default
117 ./bitflash.exe -selftest=wallet-convert
112 ./bitflash.exe -selftest=net-message 118 ./bitflash.exe -selftest=net-message
113 ./bitflash.exe -selftest=consensus-limits 119 ./bitflash.exe -selftest=consensus-limits
114 ./bitflash.exe -selftest=pool-stratum 120 ./bitflash.exe -selftest=pool-stratum
diff --git a/src/net.cpp b/src/net.cpp
index 16038c8..bb05259 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -1560,12 +1560,41 @@ void ThreadSocketHandler2(void* parg)
1560 unsigned int nNodes = 0; 1560 unsigned int nNodes = 0;
1561 CRITICAL_BLOCK(cs_vNodes) 1561 CRITICAL_BLOCK(cs_vNodes)
1562 nNodes = (unsigned int)vNodes.size(); 1562 nNodes = (unsigned int)vNodes.size();
1563 // Also count how many connections this one address already
1564 // holds. A single IP that keeps opening sockets -- a broken
1565 // node or a deliberate connection-exhaustion flood -- must not
1566 // be able to take the whole table, which is what starved the
1567 // relays.
1568 //
1569 // Loopback is exempt. Onion peers reach a managed-Tor node
1570 // through its local hidden-service listener, so every one of
1571 // them accepts as 127.0.0.1; capping loopback would cap all
1572 // inbound onion peers as a single group, which is the opposite
1573 // of where the network is headed. The global MAX_CONNECTIONS
1574 // still bounds the total, and the direct-IP flood this defends
1575 // against never arrives over loopback. (Per-onion-identity
1576 // limiting, once a peer's identity is known past accept(), is
1577 // the right tool for onion and is left for later.)
1578 bool fLoopback = (addr.GetByte(3) == 127);
1579 unsigned int nFromThisIP = 0;
1580 if (!fLoopback)
1581 CRITICAL_BLOCK(cs_vNodes)
1582 for (vector<CNode*>::iterator it = vNodes.begin(); it != vNodes.end(); ++it)
1583 if ((*it)->addr.ip == addr.ip)
1584 nFromThisIP++;
1585
1563 if (nNodes >= MAX_CONNECTIONS) 1586 if (nNodes >= MAX_CONNECTIONS)
1564 { 1587 {
1565 LogPrint("net", "refusing connection from %s, already at %u\n", 1588 LogPrint("net", "refusing connection from %s, already at %u\n",
1566 addr.ToString().c_str(), nNodes); 1589 addr.ToString().c_str(), nNodes);
1567 BtfCloseSocket(hSocket); 1590 BtfCloseSocket(hSocket);
1568 } 1591 }
1592 else if (!fLoopback && nFromThisIP >= MAX_CONNECTIONS_PER_IP)
1593 {
1594 LogPrint("net", "refusing connection from %s, already %u from that address\n",
1595 addr.ToString().c_str(), nFromThisIP);
1596 BtfCloseSocket(hSocket);
1597 }
1569 else 1598 else
1570 { 1599 {
1571 LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); 1600 LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str());
diff --git a/src/net.h b/src/net.h
index fc48d0b..752b1fe 100644
--- a/src/net.h
+++ b/src/net.h
@@ -124,6 +124,23 @@ static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE;
124// 125 is Bitcoin's number and remains a conservative public-node default. 124// 125 is Bitcoin's number and remains a conservative public-node default.
125static const unsigned int MAX_CONNECTIONS = 125; 125static const unsigned int MAX_CONNECTIONS = 125;
126 126
127// Cap inbound connections from any single address. Without it one source can
128// hold every slot up to MAX_CONNECTIONS -- exactly what happened to the
129// rendezvous relays, where a single IP parked hundreds of connections and
130// starved real peers. A legitimate peer, even a relay talking to a relay,
131// needs only a handful; 32 leaves generous room while denying any one address
132// more than a quarter of the table. The firewall connlimit on the relays does
133// the same thing a layer lower; this makes every build carry the defense.
134//
135// Loopback (127.0.0.0/8) is exempt in the accept path. A managed-Tor node
136// receives every inbound onion peer over its local hidden-service listener, so
137// they all appear as 127.0.0.1 -- capping loopback would cap onion peers as a
138// group, which is exactly wrong for a network moving toward onion-only. The
139// global MAX_CONNECTIONS still bounds the total; a per-onion-identity limit,
140// applied once identity is known after accept(), is the right future tool for
141// onion floods.
142static const unsigned int MAX_CONNECTIONS_PER_IP = 32;
143
127// A node marked for disconnect is normally held until its buffers drain, so 144// A node marked for disconnect is normally held until its buffers drain, so
128// a last message still goes out. But vSend cannot drain through a socket 145// a last message still goes out. But vSend cannot drain through a socket
129// whose far end is gone, and that made such a node immortal: it stayed in 146// whose far end is gone, and that made such a node immortal: it stayed in
diff --git a/src/selftest.cpp b/src/selftest.cpp
index d11fdd8..65f215b 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -10,9 +10,13 @@
10#include "bip32.h" 10#include "bip32.h"
11#include "proxy.h" 11#include "proxy.h"
12#include "selftest.h" 12#include "selftest.h"
13#ifndef _WIN32
14#include <sys/wait.h> // WIFEXITED/WEXITSTATUS for the child-process helper
15#endif
13#include "sockcount.h" 16#include "sockcount.h"
14#include "tor.h" 17#include "tor.h"
15#include "walletcmd.h" 18#include "walletcmd.h"
19#include "wallet_sqlite.h"
16 20
17extern int RunPoolStratumSelfTest(); 21extern int RunPoolStratumSelfTest();
18extern int RunPayoutLedgerSelfTest(); 22extern int RunPayoutLedgerSelfTest();
@@ -35,6 +39,7 @@ extern int RunPayoutLedgerSelfTest();
35#undef printf 39#undef printf
36 40
37#include <mutex> 41#include <mutex>
42#include <cerrno>
38#include <stdexcept> 43#include <stdexcept>
39#include <thread> 44#include <thread>
40 45
@@ -62,6 +67,11 @@ static std::string HexStrLocal(const std::vector<unsigned char>& v)
62 return out; 67 return out;
63} 68}
64 69
70static vector<unsigned char> DataStreamBytes(const CDataStream& ss)
71{
72 return vector<unsigned char>(ss.begin(), ss.end());
73}
74
65static bool MakeTempDir(std::string& out) 75static bool MakeTempDir(std::string& out)
66{ 76{
67#ifdef _WIN32 77#ifdef _WIN32
@@ -736,6 +746,1017 @@ static int RunWalletFormatSelfTest()
736 return nFail == 0 ? 0 : 1; 746 return nFail == 0 ? 0 : 1;
737} 747}
738 748
749static bool FileContainsText(const string& strPath, const string& strNeedle);
750static bool MakeDirLocal(const string& strPath);
751static int RunBitflashChild(const string& strExe,
752 const vector<string>& vArgs,
753 const string* pStdinFile,
754 const string* pOutputFile);
755
756class CWalletStorageSanityRawDB : public CWalletDB
757{
758public:
759 CWalletStorageSanityRawDB(const char* pszMode="r+")
760 : CWalletDB(pszMode) { }
761
762 bool WriteUnknownRecord()
763 {
764 return Write(string("storage-sanity-unknown"), 1);
765 }
766
767 bool WriteMalformedKeyRecord()
768 {
769 if (!pdb)
770 return false;
771 unsigned char chKey = 0xff;
772 unsigned char chValue = 0x01;
773 Dbt datKey(&chKey, 1);
774 Dbt datValue(&chValue, 1);
775 return pdb->put(GetTxn(), &datKey, &datValue, 0) == 0;
776 }
777
778 bool MakePlainHDSeedIncomplete()
779 {
780 return Erase(string("hdchaincode"));
781 }
782
783 bool AddEncryptedMarkerToPlainWallet()
784 {
785 CWalletMasterKey kMasterKey;
786 kMasterKey.vchCryptedKey.resize(48);
787 for (size_t i = 0; i < kMasterKey.vchCryptedKey.size(); i++)
788 kMasterKey.vchCryptedKey[i] = (unsigned char)(0x80 + (i & 0x3f));
789 for (size_t i = 0; i < kMasterKey.vchSalt.size(); i++)
790 kMasterKey.vchSalt[i] = (unsigned char)(0x40 + i);
791 return WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) &&
792 WriteMasterKey(1, kMasterKey);
793 }
794};
795
796static const char* SELFTEST_MUTATE_WALLET_MARKER =
797 ".bitflash-selftest-wallet-mutate-ok";
798static const char* SELFTEST_MUTATE_WALLET_MARKER_TEXT =
799 "bitflash destructive wallet storage self-test\n";
800
801static string SelfTestMutationMarkerPath()
802{
803 return GetAppDir() + "/" + SELFTEST_MUTATE_WALLET_MARKER;
804}
805
806static bool WriteSelfTestMutationMarker(const string& strDir)
807{
808 string strPath = strDir + "/" + SELFTEST_MUTATE_WALLET_MARKER;
809 FILE* pf = fopen(strPath.c_str(), "wb");
810 if (!pf)
811 return false;
812 bool fOk = fwrite(SELFTEST_MUTATE_WALLET_MARKER_TEXT, 1,
813 strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT), pf) ==
814 strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT);
815 if (fclose(pf) != 0)
816 fOk = false;
817 return fOk;
818}
819
820int RunSelfTestMutateWallet(const std::string& name)
821{
822 if (!FileContainsText(SelfTestMutationMarkerPath(),
823 SELFTEST_MUTATE_WALLET_MARKER_TEXT))
824 {
825 fprintf(stderr, "Refusing destructive wallet storage mutation outside "
826 "a marked self-test datadir.\n");
827 return 1;
828 }
829
830 bool fOk = false;
831 try
832 {
833 CWalletStorageSanityRawDB walletdb;
834 if (name == "unknown-record")
835 fOk = walletdb.WriteUnknownRecord();
836 else if (name == "malformed-key")
837 fOk = walletdb.WriteMalformedKeyRecord();
838 else if (name == "incomplete-plain-hd")
839 fOk = walletdb.MakePlainHDSeedIncomplete();
840 else if (name == "encrypted-marker-with-plain-keys")
841 fOk = walletdb.AddEncryptedMarkerToPlainWallet();
842 else
843 {
844 fprintf(stderr, "Unknown wallet storage mutation '%s'\n", name.c_str());
845 return 1;
846 }
847 }
848 catch (const std::exception& e)
849 {
850 fprintf(stderr, "Cannot mutate wallet storage: %s\n", e.what());
851 return 1;
852 }
853 catch (...)
854 {
855 fprintf(stderr, "Cannot mutate wallet storage: unknown exception\n");
856 return 1;
857 }
858
859 DBFlush(true);
860 return fOk ? 0 : 1;
861}
862
863static int RunWalletStorageSanitySelfTest()
864{
865 fflush(stdout);
866 printf("wallet-storage-sanity self-test\n");
867
868 std::string tmp;
869 std::string cwd;
870 if (!MakeTempDir(tmp))
871 {
872 printf(" FAIL could not create a temporary data directory\n");
873 return 1;
874 }
875 if (!GetCurrentDir(cwd))
876 {
877 printf(" FAIL could not read current directory\n");
878 RemoveTree(tmp);
879 return 1;
880 }
881
882 int nFail = 0;
883 printf(" temp root: %s\n", tmp.c_str());
884
885 try
886 {
887#ifdef _WIN32
888 string strExe = cwd + "\\bitflash.exe";
889#else
890 string strExe = cwd + "/bitflash-node";
891#endif
892
893 struct StorageScenario
894 {
895 const char* pszName;
896 const char* pszExpectedFailure;
897 };
898
899 StorageScenario scenarios[] = {
900 { "unknown-record", "wallet.dat contains unknown records" },
901 { "malformed-key", "wallet.dat contains malformed records" },
902 { "incomplete-plain-hd", "plain HD seed is incomplete" },
903 { "encrypted-marker-with-plain-keys",
904 "encrypted wallet still contains plain private key records" },
905 };
906
907 for (size_t i = 0; i < sizeof(scenarios)/sizeof(scenarios[0]); i++)
908 {
909 string strScenarioDir = tmp + "/" + scenarios[i].pszName;
910 nFail += Check(MakeDirLocal(strScenarioDir),
911 "scenario datadir can be created") ? 0 : 1;
912
913 string strCreateOut = tmp + "/" + scenarios[i].pszName + "-create.txt";
914 vector<string> vCreateArgs;
915 vCreateArgs.push_back("-datadir=" + strScenarioDir);
916 vCreateArgs.push_back("-nomanagedtor");
917 vCreateArgs.push_back("-nogui");
918 vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat
919 vCreateArgs.push_back("-newphrase");
920 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
921 nFail += Check(nCreateRet == 0 &&
922 FileContainsText(strCreateOut, "Write these twelve words down"),
923 "scenario phrase wallet can be created") ? 0 : 1;
924
925 if (i == 0)
926 {
927 string strBlockedOut = tmp + "/" + scenarios[i].pszName + "-blocked-mutate.txt";
928 vector<string> vBlockedArgs;
929 vBlockedArgs.push_back("-datadir=" + strScenarioDir);
930 vBlockedArgs.push_back("-nomanagedtor");
931 vBlockedArgs.push_back("-nogui");
932 vBlockedArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName));
933 int nBlockedRet = RunBitflashChild(strExe, vBlockedArgs, NULL, &strBlockedOut);
934 nFail += Check(nBlockedRet != 0 &&
935 FileContainsText(strBlockedOut, "Refusing destructive wallet storage mutation"),
936 "wallet mutation helper is blocked without marker") ? 0 : 1;
937 }
938
939 nFail += Check(WriteSelfTestMutationMarker(strScenarioDir),
940 "scenario mutation marker can be written") ? 0 : 1;
941
942 string strMutateOut = tmp + "/" + scenarios[i].pszName + "-mutate.txt";
943 vector<string> vMutateArgs;
944 vMutateArgs.push_back("-datadir=" + strScenarioDir);
945 vMutateArgs.push_back("-nomanagedtor");
946 vMutateArgs.push_back("-nogui");
947 vMutateArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName));
948 int nMutateRet = RunBitflashChild(strExe, vMutateArgs, NULL, &strMutateOut);
949 nFail += Check(nMutateRet == 0, scenarios[i].pszName) ? 0 : 1;
950
951 string strCheckOut = tmp + "/" + scenarios[i].pszName + "-check.txt";
952 vector<string> vCheckArgs;
953 vCheckArgs.push_back("-datadir=" + strScenarioDir);
954 vCheckArgs.push_back("-nomanagedtor");
955 vCheckArgs.push_back("-nogui");
956 vCheckArgs.push_back("-walletstoragecheck");
957 int nCheckRet = RunBitflashChild(strExe, vCheckArgs, NULL, &strCheckOut);
958 nFail += Check(nCheckRet == 2 &&
959 FileContainsText(strCheckOut, "storage sanity: failed") &&
960 FileContainsText(strCheckOut, scenarios[i].pszExpectedFailure),
961 scenarios[i].pszExpectedFailure) ? 0 : 1;
962 }
963 }
964 catch (const std::exception& e)
965 {
966 printf(" FAIL exception: %s\n", e.what());
967 nFail++;
968 }
969 catch (...)
970 {
971 printf(" FAIL unknown exception\n");
972 nFail++;
973 }
974
975 RemoveTree(tmp);
976 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
977 nFail, nFail == 1 ? "" : "s");
978 fflush(stdout);
979 return nFail == 0 ? 0 : 1;
980}
981
982static int RunDbEnvReopenSelfTest()
983{
984 fflush(stdout);
985 printf("db-env-reopen self-test\n");
986
987 std::string tmp;
988 if (!MakeTempDir(tmp))
989 {
990 printf(" FAIL could not create a temporary data directory\n");
991 return 1;
992 }
993
994 int nFail = 0;
995 std::string strOldDataDir = strSetDataDir;
996 strSetDataDir = tmp;
997 printf(" temp datadir: %s\n", tmp.c_str());
998
999 try
1000 {
1001 for (int i = 0; i < 5; i++)
1002 {
1003 int64 nValue = 1000 + i;
1004 {
1005 CWalletDB walletdb("cr+");
1006 nFail += Check(walletdb.WriteSetting("db-env-reopen-cycle", nValue),
1007 "wallet setting can be written before shutdown") ? 0 : 1;
1008 }
1009
1010 DBFlush(true);
1011
1012 int64 nRead = 0;
1013 {
1014 CWalletDB walletdb("r");
1015 nFail += Check(walletdb.ReadSetting("db-env-reopen-cycle", nRead),
1016 "wallet setting can be read after env reopen") ? 0 : 1;
1017 }
1018 nFail += Check(nRead == nValue,
1019 "reopened environment reads the last committed value") ? 0 : 1;
1020
1021 DBFlush(true);
1022 }
1023 }
1024 catch (const std::exception& e)
1025 {
1026 printf(" FAIL exception: %s\n", e.what());
1027 nFail++;
1028 }
1029 catch (...)
1030 {
1031 printf(" FAIL unknown exception\n");
1032 nFail++;
1033 }
1034
1035 DBFlush(true);
1036 strSetDataDir = strOldDataDir;
1037 RemoveTree(tmp);
1038 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1039 nFail, nFail == 1 ? "" : "s");
1040 fflush(stdout);
1041 return nFail == 0 ? 0 : 1;
1042}
1043
1044static int RunWalletSQLiteSelfTest()
1045{
1046 fflush(stdout);
1047 printf("wallet-sqlite self-test\n");
1048
1049 std::string tmp;
1050 if (!MakeTempDir(tmp))
1051 {
1052 printf(" FAIL could not create a temporary data directory\n");
1053 return 1;
1054 }
1055
1056 int nFail = 0;
1057 string strPath = tmp + "/wallet.sqlite";
1058 string strError;
1059 string strOldRuntimeDataDir;
1060 bool fRestoreRuntimeDataDir = false;
1061
1062 try
1063 {
1064 CDataStream ssKeySetting(SER_DISK);
1065 ssKeySetting << make_pair(string("setting"), string("sqlite-roundtrip"));
1066 CDataStream ssValueSetting(SER_DISK);
1067 ssValueSetting << (int64)424242;
1068
1069 vector<unsigned char> vchPubKey;
1070 vchPubKey.push_back(0x04);
1071 for (int i = 0; i < 64; i++)
1072 vchPubKey.push_back((unsigned char)i);
1073 CDataStream ssKeyDefault(SER_DISK);
1074 ssKeyDefault << string("defaultkey");
1075 CDataStream ssValueDefault(SER_DISK);
1076 ssValueDefault << vchPubKey;
1077
1078 {
1079 CWalletDBSQLite db;
1080 nFail += Check(db.Open(strPath, strError),
1081 "SQLite wallet opens and creates schema") ? 0 : 1;
1082 nFail += Check(db.BeginTransaction(strError),
1083 "SQLite wallet begins a write transaction") ? 0 : 1;
1084 nFail += Check(db.WriteRecord(DataStreamBytes(ssKeySetting),
1085 DataStreamBytes(ssValueSetting),
1086 strError),
1087 "SQLite wallet writes a serialized setting record") ? 0 : 1;
1088 nFail += Check(db.WriteRecord(DataStreamBytes(ssKeyDefault),
1089 DataStreamBytes(ssValueDefault),
1090 strError),
1091 "SQLite wallet writes a serialized defaultkey record") ? 0 : 1;
1092 nFail += Check(db.CommitTransaction(strError),
1093 "SQLite wallet commits a write transaction") ? 0 : 1;
1094 int nCount = 0;
1095 nFail += Check(db.CountRecords(nCount, strError) && nCount == 2,
1096 "SQLite wallet counts raw records") ? 0 : 1;
1097 }
1098
1099 {
1100 CWalletDBSQLite db;
1101 nFail += Check(db.Open(strPath, strError),
1102 "SQLite wallet reopens") ? 0 : 1;
1103 vector<unsigned char> vchRead;
1104 nFail += Check(db.ReadRecord(DataStreamBytes(ssKeySetting),
1105 vchRead,
1106 strError),
1107 "SQLite wallet reads a serialized setting record") ? 0 : 1;
1108 int64 nReadValue = 0;
1109 CDataStream ssRead(vchRead, SER_DISK);
1110 ssRead >> nReadValue;
1111 nFail += Check(!ssRead.fail() && nReadValue == 424242,
1112 "SQLite wallet preserves serialized values byte-for-byte") ? 0 : 1;
1113
1114 vchRead.clear();
1115 nFail += Check(db.ReadRecord(DataStreamBytes(ssKeyDefault),
1116 vchRead,
1117 strError),
1118 "SQLite wallet reads a serialized defaultkey record") ? 0 : 1;
1119 vector<unsigned char> vchReadPubKey;
1120 CDataStream ssReadDefault(vchRead, SER_DISK);
1121 ssReadDefault >> vchReadPubKey;
1122 nFail += Check(!ssReadDefault.fail() && vchReadPubKey == vchPubKey,
1123 "SQLite wallet preserves public key blobs") ? 0 : 1;
1124 }
1125
1126 {
1127 CWalletDBSQLite db;
1128 nFail += Check(db.Open(strPath, strError),
1129 "SQLite wallet reopens for typed records") ? 0 : 1;
1130
1131 nFail += Check(db.WriteTypedRecord(make_pair(string("setting"),
1132 string("sqlite-typed")),
1133 (int64)777, strError),
1134 "SQLite wallet writes a typed setting record") ? 0 : 1;
1135
1136 int64 nTypedRead = 0;
1137 nFail += Check(db.ReadTypedRecord(make_pair(string("setting"),
1138 string("sqlite-typed")),
1139 nTypedRead, strError) &&
1140 nTypedRead == 777,
1141 "SQLite wallet reads a typed setting record") ? 0 : 1;
1142
1143 nFail += Check(!db.WriteTypedRecord(make_pair(string("setting"),
1144 string("sqlite-typed")),
1145 (int64)888, strError, false),
1146 "SQLite wallet refuses typed overwrite when asked") ? 0 : 1;
1147
1148 nTypedRead = 0;
1149 nFail += Check(db.ReadTypedRecord(make_pair(string("setting"),
1150 string("sqlite-typed")),
1151 nTypedRead, strError) &&
1152 nTypedRead == 777,
1153 "SQLite wallet preserves the old typed value after overwrite refusal") ? 0 : 1;
1154
1155 nFail += Check(db.EraseTypedRecord(make_pair(string("setting"),
1156 string("sqlite-typed")),
1157 strError),
1158 "SQLite wallet erases a typed record") ? 0 : 1;
1159
1160 nTypedRead = 0;
1161 nFail += Check(!db.ReadTypedRecord(make_pair(string("setting"),
1162 string("sqlite-typed")),
1163 nTypedRead, strError),
1164 "SQLite wallet typed erase removes the record") ? 0 : 1;
1165
1166 nFail += Check(db.WriteTypedRecord(string("defaultkey"), vchPubKey,
1167 strError),
1168 "SQLite wallet writes a typed defaultkey record") ? 0 : 1;
1169
1170 vector<unsigned char> vchTypedPubKey;
1171 nFail += Check(db.ReadTypedRecord(string("defaultkey"), vchTypedPubKey,
1172 strError) &&
1173 vchTypedPubKey == vchPubKey,
1174 "SQLite wallet reads a typed defaultkey record") ? 0 : 1;
1175 }
1176
1177 {
1178 string strRuntimePath = tmp + "/runtime-wallet.sqlite";
1179 string strRuntimeDir = tmp + "/runtime-datadir";
1180 nFail += Check(MakeDirLocal(strRuntimeDir),
1181 "SQLite runtime datadir can be created") ? 0 : 1;
1182
1183 {
1184 CWalletDBSQLite db;
1185 nFail += Check(db.Open(strRuntimePath, strError),
1186 "SQLite runtime wallet file can be created") ? 0 : 1;
1187 }
1188
1189 string strOldDataDir = strSetDataDir;
1190 strSetDataDir = strRuntimeDir;
1191 strOldRuntimeDataDir = strOldDataDir;
1192 fRestoreRuntimeDataDir = true;
1193 bool fRuntimeOpened = WalletSQLiteRuntimeOpen(strRuntimePath, strError);
1194 nFail += Check(fRuntimeOpened,
1195 "SQLite runtime wallet can be activated") ? 0 : 1;
1196
1197 if (fRuntimeOpened)
1198 {
1199 CKey key;
1200 key.MakeNewKey();
1201 vector<unsigned char> vchRuntimePubKey = key.GetPubKey();
1202 CPrivKey vchRuntimePrivKey = key.GetPrivKey();
1203 string strRuntimeAddress = PubKeyToAddress(vchRuntimePubKey);
1204
1205 nFail += Check(CWalletDB().WriteSetting("runtime-sqlite-setting",
1206 (int64)909090) &&
1207 CWalletDB().WriteDefaultKey(vchRuntimePubKey) &&
1208 CWalletDB().WriteKey(vchRuntimePubKey,
1209 vchRuntimePrivKey) &&
1210 CWalletDB().WriteName(strRuntimeAddress,
1211 "SQLite Runtime Address"),
1212 "CWalletDB writes route to active SQLite runtime") ? 0 : 1;
1213
1214 int64 nRuntimeSetting = 0;
1215 vector<unsigned char> vchRuntimeDefaultKey;
1216 CPrivKey vchRuntimeReadPrivKey;
1217 string strRuntimeName;
1218 nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting",
1219 nRuntimeSetting) &&
1220 nRuntimeSetting == 909090 &&
1221 CWalletDB("r").ReadDefaultKey(vchRuntimeDefaultKey) &&
1222 vchRuntimeDefaultKey == vchRuntimePubKey &&
1223 CWalletDB("r").ReadKey(vchRuntimePubKey,
1224 vchRuntimeReadPrivKey) &&
1225 vchRuntimeReadPrivKey == vchRuntimePrivKey &&
1226 CWalletDB("r").ReadName(strRuntimeAddress,
1227 strRuntimeName) &&
1228 strRuntimeName == "SQLite Runtime Address",
1229 "CWalletDB reads route to active SQLite runtime") ? 0 : 1;
1230
1231 nFail += Check(CWalletDB().EraseName(strRuntimeAddress) &&
1232 !CWalletDB("r").ReadName(strRuntimeAddress,
1233 strRuntimeName),
1234 "CWalletDB erases route to active SQLite runtime") ? 0 : 1;
1235
1236 nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()),
1237 "active SQLite runtime does not create wallet.dat") ? 0 : 1;
1238
1239 // Transaction bracket: a rolled-back group leaves nothing, a
1240 // committed group persists. This is the all-or-nothing that
1241 // WriteHDMaster relies on so a half-written seed cannot survive
1242 // a failure between its two records.
1243 bool fTxnBegan = WalletSQLiteRuntimeBeginTxn();
1244 bool fTxnWrote = fTxnBegan &&
1245 CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111);
1246 if (fTxnBegan)
1247 WalletSQLiteRuntimeRollbackTxn();
1248 nFail += Check(fTxnBegan && fTxnWrote,
1249 "a SQLite runtime transaction opens and rolls back") ? 0 : 1;
1250 int64 nRolledBack = 0;
1251 nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback",
1252 nRolledBack),
1253 "a rolled-back SQLite runtime write leaves no record") ? 0 : 1;
1254
1255 bool fCommitOk = WalletSQLiteRuntimeBeginTxn() &&
1256 CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) &&
1257 WalletSQLiteRuntimeCommitTxn();
1258 int64 nCommitted = 0;
1259 nFail += Check(fCommitOk &&
1260 CWalletDB("r").ReadSetting("runtime-txn-commit",
1261 nCommitted) &&
1262 nCommitted == 222,
1263 "a committed SQLite runtime write persists") ? 0 : 1;
1264
1265 // WriteHDMaster brackets its two records in that transaction;
1266 // both halves of the seed must land.
1267 vector<unsigned char> vchTxnMaster(32, 0x5a);
1268 vector<unsigned char> vchTxnChainCode(32, 0xa5);
1269 vector<unsigned char> vchReadMaster, vchReadChainCode;
1270 nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) &&
1271 CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) &&
1272 vchReadMaster == vchTxnMaster &&
1273 vchReadChainCode == vchTxnChainCode,
1274 "WriteHDMaster commits both halves of the seed under SQLite") ? 0 : 1;
1275
1276 // Durability: close the backend (checkpoints the WAL), reopen
1277 // the same file, and confirm what CWalletDB wrote is still
1278 // there. This is the write-survives-a-restart property the real
1279 // -walletbackend=sqlite node depends on.
1280 WalletSQLiteRuntimeClose();
1281 bool fReopened =
1282 WalletSQLiteRuntimeOpen(strRuntimePath, strError);
1283 nFail += Check(fReopened,
1284 "SQLite runtime wallet reopens after close") ? 0 : 1;
1285 if (fReopened)
1286 {
1287 int64 nPersisted = 0;
1288 CPrivKey vchPersistedPrivKey;
1289 vector<unsigned char> vchPersistedDefaultKey;
1290 nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting",
1291 nPersisted) &&
1292 nPersisted == 909090 &&
1293 CWalletDB("r").ReadDefaultKey(vchPersistedDefaultKey) &&
1294 vchPersistedDefaultKey == vchRuntimePubKey &&
1295 CWalletDB("r").ReadKey(vchRuntimePubKey,
1296 vchPersistedPrivKey) &&
1297 vchPersistedPrivKey == vchRuntimePrivKey,
1298 "SQLite runtime writes persist across a close and reopen") ? 0 : 1;
1299 }
1300 }
1301
1302 WalletSQLiteRuntimeClose();
1303 strSetDataDir = strOldDataDir;
1304 fRestoreRuntimeDataDir = false;
1305 }
1306 }
1307 catch (const std::exception& e)
1308 {
1309 WalletSQLiteRuntimeClose();
1310 if (fRestoreRuntimeDataDir)
1311 strSetDataDir = strOldRuntimeDataDir;
1312 printf(" FAIL exception: %s\n", e.what());
1313 nFail++;
1314 }
1315 catch (...)
1316 {
1317 WalletSQLiteRuntimeClose();
1318 if (fRestoreRuntimeDataDir)
1319 strSetDataDir = strOldRuntimeDataDir;
1320 printf(" FAIL unknown exception\n");
1321 nFail++;
1322 }
1323
1324 RemoveTree(tmp);
1325 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1326 nFail, nFail == 1 ? "" : "s");
1327 fflush(stdout);
1328 return nFail == 0 ? 0 : 1;
1329}
1330
1331namespace {
1332
1333class CWalletRecordMapVisitor : public CWalletRecordVisitor
1334{
1335public:
1336 std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords;
1337
1338 explicit CWalletRecordMapVisitor(
1339 std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn)
1340 : mapRecords(mapRecordsIn) { }
1341
1342 bool VisitWalletRecord(const CDataStream& ssKey,
1343 const CDataStream& ssValue,
1344 string& strErrorRet)
1345 {
1346 vector<unsigned char> vchKey = DataStreamBytes(ssKey);
1347 if (mapRecords.count(vchKey))
1348 {
1349 strErrorRet = "duplicate serialized wallet key";
1350 return false;
1351 }
1352 mapRecords[vchKey] = DataStreamBytes(ssValue);
1353 return true;
1354 }
1355};
1356
1357} // namespace
1358
1359static bool CreateSQLiteWalletWithEmptyKey(const string& strPath)
1360{
1361 sqlite3* pdb = NULL;
1362 int ret = sqlite3_open_v2(strPath.c_str(), &pdb,
1363 SQLITE_OPEN_READWRITE |
1364 SQLITE_OPEN_CREATE |
1365 SQLITE_OPEN_FULLMUTEX,
1366 NULL);
1367 if (ret != SQLITE_OK || !pdb)
1368 {
1369 if (pdb)
1370 sqlite3_close(pdb);
1371 return false;
1372 }
1373
1374 char* pszErr = NULL;
1375 bool fOk =
1376 sqlite3_exec(pdb,
1377 "CREATE TABLE wallet_records ("
1378 "key BLOB PRIMARY KEY NOT NULL,"
1379 "value BLOB NOT NULL"
1380 ");",
1381 NULL, NULL, &pszErr) == SQLITE_OK &&
1382 sqlite3_exec(pdb,
1383 "INSERT INTO wallet_records(key, value) "
1384 "VALUES(x'', x'01');",
1385 NULL, NULL, &pszErr) == SQLITE_OK;
1386 if (pszErr)
1387 sqlite3_free(pszErr);
1388 sqlite3_close(pdb);
1389 return fOk;
1390}
1391
1392static bool CreateSQLiteWalletWithFutureMinVersion(const string& strPath)
1393{
1394 string strError;
1395 CWalletDBSQLite db;
1396 CDataStream ssKey(SER_DISK);
1397 ssKey << string("walletminversion");
1398 CDataStream ssValue(SER_DISK);
1399 ssValue << (WALLET_FORMAT_SUPPORTED + 1);
1400
1401 if (!db.Open(strPath, strError))
1402 return false;
1403 if (!db.BeginTransaction(strError))
1404 return false;
1405 if (!db.WriteRecord(DataStreamBytes(ssKey), DataStreamBytes(ssValue), strError))
1406 return false;
1407 if (!db.CommitTransaction(strError))
1408 return false;
1409 if (!db.Checkpoint(strError))
1410 return false;
1411 return true;
1412}
1413
1414static bool CreateTypedSQLiteWallet(const string& strPath)
1415{
1416 string strError;
1417 CWalletDBSQLite db;
1418 CKey key;
1419 key.MakeNewKey();
1420
1421 vector<unsigned char> vchPubKey = key.GetPubKey();
1422 CPrivKey vchPrivKey = key.GetPrivKey();
1423 string strAddress = PubKeyToAddress(vchPubKey);
1424
1425 if (!db.Open(strPath, strError))
1426 return false;
1427 if (!db.BeginTransaction(strError))
1428 return false;
1429 if (!db.WriteTypedRecord(make_pair(string("key"), vchPubKey),
1430 vchPrivKey, strError, false))
1431 return false;
1432 if (!db.WriteTypedRecord(string("defaultkey"), vchPubKey, strError))
1433 return false;
1434 if (!db.WriteTypedRecord(make_pair(string("name"), strAddress),
1435 string("Typed SQLite Address"), strError))
1436 return false;
1437 if (!db.CommitTransaction(strError))
1438 return false;
1439 if (!db.Checkpoint(strError))
1440 return false;
1441 return true;
1442}
1443
1444static int RunWalletSQLiteMigrationSelfTest()
1445{
1446 fflush(stdout);
1447 printf("wallet-sqlite-migration self-test\n");
1448
1449 std::string tmp;
1450 std::string cwd;
1451 if (!MakeTempDir(tmp))
1452 {
1453 printf(" FAIL could not create a temporary data directory\n");
1454 return 1;
1455 }
1456 if (!GetCurrentDir(cwd))
1457 {
1458 printf(" FAIL could not read current directory\n");
1459 RemoveTree(tmp);
1460 return 1;
1461 }
1462
1463 int nFail = 0;
1464 printf(" temp root: %s\n", tmp.c_str());
1465
1466 try
1467 {
1468#ifdef _WIN32
1469 string strExe = cwd + "\\bitflash.exe";
1470#else
1471 string strExe = cwd + "/bitflash-node";
1472#endif
1473 string strWalletDir = tmp + "/wallet";
1474 nFail += Check(MakeDirLocal(strWalletDir),
1475 "temporary wallet directory can be created") ? 0 : 1;
1476
1477 string strCreateOut = tmp + "/create.txt";
1478 vector<string> vCreateArgs;
1479 vCreateArgs.push_back("-datadir=" + strWalletDir);
1480 vCreateArgs.push_back("-nomanagedtor");
1481 vCreateArgs.push_back("-nogui");
1482 vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat
1483 vCreateArgs.push_back("-newphrase");
1484 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
1485 nFail += Check(nCreateRet == 0 &&
1486 FileContainsText(strCreateOut, "Write these twelve words down"),
1487 "a phrase wallet can be created for SQLite export") ? 0 : 1;
1488
1489 string strSQLite = tmp + "/wallet.sqlite";
1490 string strExportOut = tmp + "/sqlite-export.txt";
1491 vector<string> vExportArgs;
1492 vExportArgs.push_back("-datadir=" + strWalletDir);
1493 vExportArgs.push_back("-nomanagedtor");
1494 vExportArgs.push_back("-nogui");
1495 vExportArgs.push_back("-walletsqliteexport=" + strSQLite);
1496 int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut);
1497 nFail += Check(nExportRet == 0 &&
1498 FileExists(strSQLite.c_str()) &&
1499 FileContainsText(strExportOut, "SQLite wallet export written"),
1500 "wallet.dat can be exported to SQLite") ? 0 : 1;
1501
1502 string strVerifyOut = tmp + "/sqlite-verify.txt";
1503 vector<string> vVerifyArgs;
1504 vVerifyArgs.push_back("-datadir=" + strWalletDir);
1505 vVerifyArgs.push_back("-nomanagedtor");
1506 vVerifyArgs.push_back("-nogui");
1507 vVerifyArgs.push_back("-walletsqliteverify=" + strSQLite);
1508 int nVerifyRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyOut);
1509 nFail += Check(nVerifyRet == 0 &&
1510 FileContainsText(strVerifyOut, "verification: ok"),
1511 "SQLite wallet export verifies against wallet.dat") ? 0 : 1;
1512
1513 string strLoadCheckOut = tmp + "/sqlite-loadcheck.txt";
1514 vector<string> vLoadCheckArgs;
1515 vLoadCheckArgs.push_back("-datadir=" + strWalletDir);
1516 vLoadCheckArgs.push_back("-nomanagedtor");
1517 vLoadCheckArgs.push_back("-nogui");
1518 vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strSQLite);
1519 int nLoadCheckRet =
1520 RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strLoadCheckOut);
1521 nFail += Check(nLoadCheckRet == 0 &&
1522 FileContainsText(strLoadCheckOut,
1523 "load check: ok"),
1524 "SQLite wallet export passes the loader compatibility check") ? 0 : 1;
1525
1526 string strRuntimeOut = tmp + "/sqlite-runtime.txt";
1527 vector<string> vRuntimeArgs;
1528 vRuntimeArgs.push_back("-datadir=" + strWalletDir);
1529 vRuntimeArgs.push_back("-nomanagedtor");
1530 vRuntimeArgs.push_back("-nogui");
1531 vRuntimeArgs.push_back("-walletsqlite=" + strSQLite);
1532 int nRuntimeRet =
1533 RunBitflashChild(strExe, vRuntimeArgs, NULL, &strRuntimeOut);
1534 nFail += Check(nRuntimeRet == 0 &&
1535 FileContainsText(strRuntimeOut,
1536 "SQLite wallet loaded: read-only staging backend"),
1537 "SQLite wallet export loads through the runtime startup path") ? 0 : 1;
1538
1539 string strTypedSQLite = tmp + "/typed-wallet.sqlite";
1540 nFail += Check(CreateTypedSQLiteWallet(strTypedSQLite),
1541 "typed SQLite wallet can be created without Berkeley DB") ? 0 : 1;
1542
1543 string strTypedLoadCheckOut = tmp + "/sqlite-typed-loadcheck.txt";
1544 vector<string> vTypedLoadCheckArgs;
1545 vTypedLoadCheckArgs.push_back("-datadir=" + strWalletDir);
1546 vTypedLoadCheckArgs.push_back("-nomanagedtor");
1547 vTypedLoadCheckArgs.push_back("-nogui");
1548 vTypedLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strTypedSQLite);
1549 int nTypedLoadCheckRet =
1550 RunBitflashChild(strExe, vTypedLoadCheckArgs, NULL,
1551 &strTypedLoadCheckOut);
1552 nFail += Check(nTypedLoadCheckRet == 0 &&
1553 FileContainsText(strTypedLoadCheckOut,
1554 "load check: ok"),
1555 "typed SQLite wallet passes the loader compatibility check") ? 0 : 1;
1556
1557 string strTypedRuntimeDir = tmp + "/typed-runtime-wallet";
1558 nFail += Check(MakeDirLocal(strTypedRuntimeDir),
1559 "typed SQLite runtime directory can be created") ? 0 : 1;
1560 string strTypedRuntimeOut = tmp + "/sqlite-typed-runtime.txt";
1561 vector<string> vTypedRuntimeArgs;
1562 vTypedRuntimeArgs.push_back("-datadir=" + strTypedRuntimeDir);
1563 vTypedRuntimeArgs.push_back("-nomanagedtor");
1564 vTypedRuntimeArgs.push_back("-nogui");
1565 vTypedRuntimeArgs.push_back("-walletsqlite=" + strTypedSQLite);
1566 int nTypedRuntimeRet =
1567 RunBitflashChild(strExe, vTypedRuntimeArgs, NULL,
1568 &strTypedRuntimeOut);
1569 nFail += Check(nTypedRuntimeRet == 0 &&
1570 FileContainsText(strTypedRuntimeOut,
1571 "SQLite wallet loaded: read-only staging backend") &&
1572 !FileExists((strTypedRuntimeDir + "/wallet.dat").c_str()),
1573 "typed SQLite wallet loads through runtime without creating wallet.dat") ? 0 : 1;
1574
1575 string strRestoredDir = tmp + "/restored-wallet";
1576 nFail += Check(MakeDirLocal(strRestoredDir),
1577 "restored wallet directory can be created") ? 0 : 1;
1578 string strRestoreOut = tmp + "/sqlite-restore.txt";
1579 vector<string> vRestoreArgs;
1580 vRestoreArgs.push_back("-datadir=" + strRestoredDir);
1581 vRestoreArgs.push_back("-nomanagedtor");
1582 vRestoreArgs.push_back("-nogui");
1583 vRestoreArgs.push_back("-walletsqliterestore=" + strSQLite);
1584 int nRestoreRet = RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOut);
1585 nFail += Check(nRestoreRet == 0 &&
1586 FileContainsText(strRestoreOut, "records restored:"),
1587 "SQLite wallet export restores into a fresh wallet.dat") ? 0 : 1;
1588
1589 string strVerifyRestoredOut = tmp + "/sqlite-verify-restored.txt";
1590 vector<string> vVerifyRestoredArgs;
1591 vVerifyRestoredArgs.push_back("-datadir=" + strRestoredDir);
1592 vVerifyRestoredArgs.push_back("-nomanagedtor");
1593 vVerifyRestoredArgs.push_back("-nogui");
1594 vVerifyRestoredArgs.push_back("-walletsqliteverify=" + strSQLite);
1595 int nVerifyRestoredRet =
1596 RunBitflashChild(strExe, vVerifyRestoredArgs, NULL, &strVerifyRestoredOut);
1597 nFail += Check(nVerifyRestoredRet == 0 &&
1598 FileContainsText(strVerifyRestoredOut,
1599 "verification: ok"),
1600 "restored wallet.dat verifies against the SQLite export") ? 0 : 1;
1601
1602 string strRestoreOverwriteOut = tmp + "/sqlite-restore-overwrite.txt";
1603 int nRestoreOverwriteRet =
1604 RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOverwriteOut);
1605 nFail += Check(nRestoreOverwriteRet == 1 &&
1606 FileContainsText(strRestoreOverwriteOut,
1607 "Refusing to overwrite existing wallet.dat"),
1608 "SQLite wallet restore refuses to overwrite wallet.dat") ? 0 : 1;
1609
1610 string strBadSQLite = tmp + "/bad-empty-key.sqlite";
1611 nFail += Check(CreateSQLiteWalletWithEmptyKey(strBadSQLite),
1612 "bad SQLite export with an empty key can be created") ? 0 : 1;
1613 string strBadRestoreDir = tmp + "/bad-restore-wallet";
1614 nFail += Check(MakeDirLocal(strBadRestoreDir),
1615 "bad restore wallet directory can be created") ? 0 : 1;
1616 string strBadRestoreOut = tmp + "/sqlite-restore-bad.txt";
1617 vector<string> vBadRestoreArgs;
1618 vBadRestoreArgs.push_back("-datadir=" + strBadRestoreDir);
1619 vBadRestoreArgs.push_back("-nomanagedtor");
1620 vBadRestoreArgs.push_back("-nogui");
1621 vBadRestoreArgs.push_back("-walletsqliterestore=" + strBadSQLite);
1622 int nBadRestoreRet =
1623 RunBitflashChild(strExe, vBadRestoreArgs, NULL, &strBadRestoreOut);
1624 nFail += Check(nBadRestoreRet == 1 &&
1625 FileContainsText(strBadRestoreOut,
1626 "Cannot restore SQLite wallet export") &&
1627 !FileExists((strBadRestoreDir + "/wallet.dat").c_str()),
1628 "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1;
1629
1630 string strFutureSQLite = tmp + "/future-wallet.sqlite";
1631 nFail += Check(CreateSQLiteWalletWithFutureMinVersion(strFutureSQLite),
1632 "SQLite export with a future wallet format can be created") ? 0 : 1;
1633 string strFutureLoadCheckOut = tmp + "/sqlite-loadcheck-future.txt";
1634 vector<string> vFutureLoadCheckArgs;
1635 vFutureLoadCheckArgs.push_back("-datadir=" + strWalletDir);
1636 vFutureLoadCheckArgs.push_back("-nomanagedtor");
1637 vFutureLoadCheckArgs.push_back("-nogui");
1638 vFutureLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strFutureSQLite);
1639 int nFutureLoadCheckRet =
1640 RunBitflashChild(strExe, vFutureLoadCheckArgs, NULL,
1641 &strFutureLoadCheckOut);
1642 nFail += Check(nFutureLoadCheckRet == 2 &&
1643 FileContainsText(strFutureLoadCheckOut,
1644 "unsupported future wallet format") &&
1645 FileContainsText(strFutureLoadCheckOut,
1646 "load check: failed"),
1647 "SQLite wallet load check rejects future wallet formats") ? 0 : 1;
1648
1649 string strFutureRuntimeOut = tmp + "/sqlite-runtime-future.txt";
1650 vector<string> vFutureRuntimeArgs;
1651 vFutureRuntimeArgs.push_back("-datadir=" + strWalletDir);
1652 vFutureRuntimeArgs.push_back("-nomanagedtor");
1653 vFutureRuntimeArgs.push_back("-nogui");
1654 vFutureRuntimeArgs.push_back("-walletsqlite=" + strFutureSQLite);
1655 int nFutureRuntimeRet =
1656 RunBitflashChild(strExe, vFutureRuntimeArgs, NULL,
1657 &strFutureRuntimeOut);
1658 nFail += Check(nFutureRuntimeRet == 1 &&
1659 FileContainsText(strFutureRuntimeOut,
1660 "Cannot open SQLite wallet export") &&
1661 FileContainsText(strFutureRuntimeOut,
1662 "newer version of Bitflash"),
1663 "SQLite runtime startup rejects future wallet formats") ? 0 : 1;
1664
1665 string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt";
1666 int nOverwriteRet =
1667 RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut);
1668 nFail += Check(nOverwriteRet != 0 &&
1669 FileContainsText(strOverwriteOut,
1670 "Refusing to overwrite existing SQLite wallet export"),
1671 "SQLite wallet export refuses to overwrite") ? 0 : 1;
1672 remove((strSQLite + "-wal").c_str());
1673 remove((strSQLite + "-shm").c_str());
1674
1675 std::map<vector<unsigned char>, vector<unsigned char> > mapBDB;
1676 string strOldDataDir = strSetDataDir;
1677 string strScanError;
1678 strSetDataDir = strWalletDir;
1679 CWalletRecordMapVisitor visitor(mapBDB);
1680 bool fScanOk = ScanWalletRecords(visitor, strScanError);
1681 DBFlush(true);
1682 strSetDataDir = strOldDataDir;
1683 nFail += Check(fScanOk && !mapBDB.empty(),
1684 "wallet.dat records can be scanned for comparison") ? 0 : 1;
1685
1686 CWalletDBSQLite db;
1687 string strError;
1688 nFail += Check(db.Open(strSQLite, strError),
1689 "exported SQLite wallet opens") ? 0 : 1;
1690 int nSQLiteRecords = 0;
1691 nFail += Check(db.CountRecords(nSQLiteRecords, strError) &&
1692 nSQLiteRecords == (int)mapBDB.size(),
1693 "SQLite export has the same record count as wallet.dat") ? 0 : 1;
1694
1695 bool fAllMatch = true;
1696 for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it =
1697 mapBDB.begin(); it != mapBDB.end(); ++it)
1698 {
1699 vector<unsigned char> vchSQLiteValue;
1700 if (!db.ReadRecord(it->first, vchSQLiteValue, strError) ||
1701 vchSQLiteValue != it->second)
1702 {
1703 fAllMatch = false;
1704 break;
1705 }
1706 }
1707 nFail += Check(fAllMatch,
1708 "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1;
1709
1710 std::map<vector<unsigned char>, vector<unsigned char> > mapSQLite;
1711 CWalletRecordMapVisitor sqliteVisitor(mapSQLite);
1712 nFail += Check(db.ScanRecords(sqliteVisitor, strError) &&
1713 mapSQLite == mapBDB,
1714 "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1;
1715
1716 db.Close();
1717 CWalletDBSQLite dbCorrupt;
1718 vector<unsigned char> vchBadValue;
1719 vchBadValue.push_back(0xba);
1720 vchBadValue.push_back(0xad);
1721 nFail += Check(!mapBDB.empty() &&
1722 dbCorrupt.Open(strSQLite, strError) &&
1723 dbCorrupt.BeginTransaction(strError) &&
1724 dbCorrupt.WriteRecord(mapBDB.begin()->first,
1725 vchBadValue,
1726 strError) &&
1727 dbCorrupt.CommitTransaction(strError) &&
1728 dbCorrupt.Checkpoint(strError),
1729 "SQLite export can be deliberately corrupted for verifier test") ? 0 : 1;
1730 dbCorrupt.Close();
1731
1732 string strVerifyBadOut = tmp + "/sqlite-verify-bad.txt";
1733 int nVerifyBadRet =
1734 RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyBadOut);
1735 nFail += Check(nVerifyBadRet == 2 &&
1736 FileContainsText(strVerifyBadOut,
1737 "record value mismatches: 1") &&
1738 FileContainsText(strVerifyBadOut,
1739 "verification: failed"),
1740 "SQLite wallet verifier detects a mismatched record value") ? 0 : 1;
1741 }
1742 catch (const std::exception& e)
1743 {
1744 printf(" FAIL exception: %s\n", e.what());
1745 nFail++;
1746 }
1747 catch (...)
1748 {
1749 printf(" FAIL unknown exception\n");
1750 nFail++;
1751 }
1752
1753 RemoveTree(tmp);
1754 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1755 nFail, nFail == 1 ? "" : "s");
1756 fflush(stdout);
1757 return nFail == 0 ? 0 : 1;
1758}
1759
739static void ClearWalletRuntimeForTest() 1760static void ClearWalletRuntimeForTest()
740{ 1761{
741 CRITICAL_BLOCK(cs_mapKeys) 1762 CRITICAL_BLOCK(cs_mapKeys)
@@ -926,6 +1947,90 @@ static bool WriteTextFile(const string& strPath, const string& strText)
926 return fOk; 1947 return fOk;
927} 1948}
928 1949
1950static bool MakeDirLocal(const string& strPath)
1951{
1952#ifdef _WIN32
1953 return CreateDirectoryA(strPath.c_str(), NULL) || GetLastError() == ERROR_ALREADY_EXISTS;
1954#else
1955 return mkdir(strPath.c_str(), 0700) == 0 || errno == EEXIST;
1956#endif
1957}
1958
1959static bool CopyFileLocal(const string& strSrc, const string& strDst)
1960{
1961 FILE* in = fopen(strSrc.c_str(), "rb");
1962 if (!in)
1963 return false;
1964 FILE* out = fopen(strDst.c_str(), "wb");
1965 if (!out)
1966 {
1967 fclose(in);
1968 return false;
1969 }
1970
1971 bool fOk = true;
1972 char buf[65536];
1973 size_t n = 0;
1974 while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
1975 {
1976 if (fwrite(buf, 1, n, out) != n)
1977 {
1978 fOk = false;
1979 break;
1980 }
1981 }
1982 if (ferror(in))
1983 fOk = false;
1984 if (fclose(out) != 0)
1985 fOk = false;
1986 fclose(in);
1987 return fOk;
1988}
1989
1990static bool DirectoryHasFileWithPrefix(const string& strDir, const string& strPrefix)
1991{
1992#ifdef _WIN32
1993 WIN32_FIND_DATAA findData;
1994 string pattern = strDir + "\\*";
1995 HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData);
1996 if (hFind == INVALID_HANDLE_VALUE)
1997 return false;
1998 bool fFound = false;
1999 do
2000 {
2001 if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
2002 {
2003 string name = findData.cFileName;
2004 if (name.compare(0, strPrefix.size(), strPrefix) == 0)
2005 {
2006 fFound = true;
2007 break;
2008 }
2009 }
2010 }
2011 while (FindNextFileA(hFind, &findData));
2012 FindClose(hFind);
2013 return fFound;
2014#else
2015 DIR* dir = opendir(strDir.c_str());
2016 if (!dir)
2017 return false;
2018 bool fFound = false;
2019 struct dirent* ent;
2020 while ((ent = readdir(dir)) != NULL)
2021 {
2022 string name = ent->d_name;
2023 if (name.compare(0, strPrefix.size(), strPrefix) == 0)
2024 {
2025 fFound = true;
2026 break;
2027 }
2028 }
2029 closedir(dir);
2030 return fFound;
2031#endif
2032}
2033
929static string QuoteCommandArg(const string& str) 2034static string QuoteCommandArg(const string& str)
930{ 2035{
931 string out = "\""; 2036 string out = "\"";
@@ -942,7 +2047,8 @@ static string QuoteCommandArg(const string& str)
942 2047
943static int RunBitflashChild(const string& strExe, 2048static int RunBitflashChild(const string& strExe,
944 const vector<string>& vArgs, 2049 const vector<string>& vArgs,
945 const string* pStdinFile = NULL) 2050 const string* pStdinFile = NULL,
2051 const string* pOutputFile = NULL)
946{ 2052{
947#ifdef _WIN32 2053#ifdef _WIN32
948 vector<char*> argv; 2054 vector<char*> argv;
@@ -955,8 +2061,16 @@ static int RunBitflashChild(const string& strExe,
955 int nOldErr = _dup(2); 2061 int nOldErr = _dup(2);
956 int nOldIn = _dup(0); 2062 int nOldIn = _dup(0);
957 int nNull = _open("NUL", _O_WRONLY); 2063 int nNull = _open("NUL", _O_WRONLY);
2064 int nOut = -1;
958 int nIn = -1; 2065 int nIn = -1;
959 if (nNull >= 0) 2066 if (pOutputFile)
2067 nOut = _open(pOutputFile->c_str(), _O_WRONLY|_O_CREAT|_O_TRUNC|_O_BINARY, _S_IREAD|_S_IWRITE);
2068 if (nOut >= 0)
2069 {
2070 _dup2(nOut, 1);
2071 _dup2(nOut, 2);
2072 }
2073 else if (nNull >= 0)
960 { 2074 {
961 _dup2(nNull, 1); 2075 _dup2(nNull, 1);
962 _dup2(nNull, 2); 2076 _dup2(nNull, 2);
@@ -970,6 +2084,8 @@ static int RunBitflashChild(const string& strExe,
970 int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); 2084 int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]);
971 if (nIn >= 0) 2085 if (nIn >= 0)
972 _close(nIn); 2086 _close(nIn);
2087 if (nOut >= 0)
2088 _close(nOut);
973 if (nNull >= 0) 2089 if (nNull >= 0)
974 _close(nNull); 2090 _close(nNull);
975 if (nOldIn >= 0) 2091 if (nOldIn >= 0)
@@ -994,8 +2110,17 @@ static int RunBitflashChild(const string& strExe,
994 strCmd += " " + QuoteCommandArg(vArgs[i]); 2110 strCmd += " " + QuoteCommandArg(vArgs[i]);
995 if (pStdinFile) 2111 if (pStdinFile)
996 strCmd += " < " + QuoteCommandArg(*pStdinFile); 2112 strCmd += " < " + QuoteCommandArg(*pStdinFile);
997 strCmd += " > /dev/null 2>&1"; 2113 if (pOutputFile)
998 return system(strCmd.c_str()); 2114 strCmd += " > " + QuoteCommandArg(*pOutputFile) + " 2>&1";
2115 else
2116 strCmd += " > /dev/null 2>&1";
2117 // system() returns a wait status, not the exit code; unwrap it so callers
2118 // see the same value the Windows _spawnv path returns (e.g. exit code 2 is
2119 // 2, not 512). Without this every "== 2" assertion fails only on Linux.
2120 int nSysRet = system(strCmd.c_str());
2121 if (nSysRet != -1 && WIFEXITED(nSysRet))
2122 return WEXITSTATUS(nSysRet);
2123 return nSysRet;
999#endif 2124#endif
1000} 2125}
1001 2126
@@ -1036,6 +2161,13 @@ static int RunWalletEncryptSelfTest()
1036 TopUpKeyPool(); 2161 TopUpKeyPool();
1037 2162
1038 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey(); 2163 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
2164 CKey publicLabelKey;
2165 publicLabelKey.MakeNewKey();
2166 string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey());
2167 nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") &&
2168 CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242),
2169 "public wallet records can be written before encryption") ? 0 : 1;
2170
1039 CPrivKey vchDefaultPrivKey; 2171 CPrivKey vchDefaultPrivKey;
1040 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), 2172 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
1041 "the default private key is readable before encryption") ? 0 : 1; 2173 "the default private key is readable before encryption") ? 0 : 1;
@@ -1063,16 +2195,31 @@ static int RunWalletEncryptSelfTest()
1063#else 2195#else
1064 string strExe = cwd + "/bitflash-node"; 2196 string strExe = cwd + "/bitflash-node";
1065#endif 2197#endif
2198 string strStorageAudit = tmp + "/encrypted-storage-audit.txt";
1066 string strWrongDump = tmp + "/wrong-pass-dump.txt"; 2199 string strWrongDump = tmp + "/wrong-pass-dump.txt";
1067 string strRightDump = tmp + "/right-pass-dump.txt"; 2200 string strRightDump = tmp + "/right-pass-dump.txt";
1068 string strLiteralDump = tmp + "/literal-pass-dump.txt"; 2201 string strLiteralDump = tmp + "/literal-pass-dump.txt";
1069 string strStdinDump = tmp + "/stdin-pass-dump.txt"; 2202 string strStdinDump = tmp + "/stdin-pass-dump.txt";
2203 string strLockedAudit = tmp + "/locked-recovery-audit.txt";
2204 string strUnlockedAudit = tmp + "/unlocked-recovery-audit.txt";
1070 string strWrongPassFile = tmp + "/wrong-pass.txt"; 2205 string strWrongPassFile = tmp + "/wrong-pass.txt";
1071 string strRightPassFile = tmp + "/right-pass.txt"; 2206 string strRightPassFile = tmp + "/right-pass.txt";
1072 nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && 2207 nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") &&
1073 WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), 2208 WriteTextFile(strRightPassFile, "btf-test-passphrase\n"),
1074 "passphrase files can be written for restarted commands") ? 0 : 1; 2209 "passphrase files can be written for restarted commands") ? 0 : 1;
1075 2210
2211 vector<string> vStorageAuditArgs;
2212 vStorageAuditArgs.push_back("-datadir=" + tmp);
2213 vStorageAuditArgs.push_back("-walletstorageaudit");
2214 vStorageAuditArgs.push_back("-nogui");
2215 int nStorageAuditRet = RunBitflashChild(strExe, vStorageAuditArgs, NULL, &strStorageAudit);
2216 nFail += Check(nStorageAuditRet == 0 &&
2217 FileContainsText(strStorageAudit, "address book labels: 3"),
2218 "the encrypted rewrite keeps address-book records") ? 0 : 1;
2219 nFail += Check(nStorageAuditRet == 0 &&
2220 FileContainsText(strStorageAudit, "settings: 1"),
2221 "the encrypted rewrite keeps wallet setting records") ? 0 : 1;
2222
1076 vector<string> vWrongArgs; 2223 vector<string> vWrongArgs;
1077 vWrongArgs.push_back("-datadir=" + tmp); 2224 vWrongArgs.push_back("-datadir=" + tmp);
1078 vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); 2225 vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile);
@@ -1082,6 +2229,17 @@ static int RunWalletEncryptSelfTest()
1082 nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), 2229 nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()),
1083 "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; 2230 "a restarted wallet rejects the wrong-passphrase") ? 0 : 1;
1084 2231
2232 vector<string> vLockedAuditArgs;
2233 vLockedAuditArgs.push_back("-datadir=" + tmp);
2234 vLockedAuditArgs.push_back("-recoveryaudit");
2235 vLockedAuditArgs.push_back("-nogui");
2236 int nLockedAuditRet = RunBitflashChild(strExe, vLockedAuditArgs, NULL, &strLockedAudit);
2237 nFail += Check(nLockedAuditRet == 2 &&
2238 FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit") &&
2239 FileContainsText(strLockedAudit, "recovery coverage: unavailable while wallet is locked") &&
2240 FileContainsText(strLockedAudit, "Unlock the wallet with /walletpassphrase"),
2241 "a locked encrypted wallet reports that phrase coverage needs unlock") ? 0 : 1;
2242
1085 vector<string> vLiteralArgs; 2243 vector<string> vLiteralArgs;
1086 vLiteralArgs.push_back("-datadir=" + tmp); 2244 vLiteralArgs.push_back("-datadir=" + tmp);
1087 vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); 2245 vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase");
@@ -1102,6 +2260,17 @@ static int RunWalletEncryptSelfTest()
1102 nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), 2260 nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)),
1103 "the restarted wallet can decrypt and dump the original key") ? 0 : 1; 2261 "the restarted wallet can decrypt and dump the original key") ? 0 : 1;
1104 2262
2263 vector<string> vUnlockedAuditArgs;
2264 vUnlockedAuditArgs.push_back("-datadir=" + tmp);
2265 vUnlockedAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile);
2266 vUnlockedAuditArgs.push_back("-recoveryaudit");
2267 vUnlockedAuditArgs.push_back("-nogui");
2268 int nUnlockedAuditRet = RunBitflashChild(strExe, vUnlockedAuditArgs, NULL, &strUnlockedAudit);
2269 nFail += Check(nUnlockedAuditRet == 0 &&
2270 FileContainsText(strUnlockedAudit, "recovery phrase: present") &&
2271 !FileContainsText(strUnlockedAudit, "recovery phrase: not installed"),
2272 "an unlocked encrypted wallet audits the installed recovery phrase") ? 0 : 1;
2273
1105 vector<string> vStdinArgs; 2274 vector<string> vStdinArgs;
1106 vStdinArgs.push_back("-datadir=" + tmp); 2275 vStdinArgs.push_back("-datadir=" + tmp);
1107 vStdinArgs.push_back("-walletpassphrase"); 2276 vStdinArgs.push_back("-walletpassphrase");
@@ -1132,6 +2301,737 @@ static int RunWalletEncryptSelfTest()
1132 return nFail == 0 ? 0 : 1; 2301 return nFail == 0 ? 0 : 1;
1133} 2302}
1134 2303
2304// Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley
2305// DB encrypt test above, this drives the real command-line path end to end in
2306// child processes: build a Berkeley DB wallet, export it to SQLite, then run
2307// -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out
2308// encrypted, unlockable, and with wallet.dat left untouched.
2309static int RunWalletSQLiteEncryptSelfTest()
2310{
2311 fflush(stdout);
2312 printf("wallet-sqlite-encrypt self-test\n");
2313
2314 std::string tmp;
2315 std::string cwd;
2316 if (!MakeTempDir(tmp))
2317 {
2318 printf(" FAIL could not create a temporary data directory\n");
2319 return 1;
2320 }
2321 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2322 {
2323 printf(" FAIL could not move into the temporary data directory\n");
2324 RemoveTree(tmp);
2325 return 1;
2326 }
2327
2328 int nFail = 0;
2329 strSetDataDir = tmp;
2330 printf(" temp datadir: %s\n", tmp.c_str());
2331
2332 try
2333 {
2334 // Build a normal Berkeley DB wallet with a recovery phrase, a key pool,
2335 // and a couple of public records, then release it. Everything after this
2336 // runs the real command-line paths in child processes.
2337 if (!LoadWallet())
2338 throw std::runtime_error("LoadWallet failed");
2339
2340 string strError;
2341 const string strMnemonic =
2342 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
2343 nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError),
2344 "a phrase can be installed before encryption") ? 0 : 1;
2345 TopUpKeyPool();
2346
2347 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
2348 CKey publicLabelKey;
2349 publicLabelKey.MakeNewKey();
2350 string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey());
2351 nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") &&
2352 CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242),
2353 "public wallet records can be written before encryption") ? 0 : 1;
2354
2355 CPrivKey vchDefaultPrivKey;
2356 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
2357 "the default private key is readable before encryption") ? 0 : 1;
2358 vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end());
2359 vector<unsigned char> vchPlainHDMaster = vchHDMaster;
2360 vector<unsigned char> vchPlainHDChainCode = vchHDChainCode;
2361
2362 // Release wallet.dat so a child process can read it for the export.
2363 DBFlush(true);
2364
2365#ifdef _WIN32
2366 string strExe = cwd + "\\bitflash.exe";
2367#else
2368 string strExe = cwd + "/bitflash-node";
2369#endif
2370 string strSQLitePath = tmp + "/wallet.sqlite";
2371 string strWalletDatPath = tmp + "/wallet.dat";
2372 string strRightPassFile = tmp + "/right-pass.txt";
2373 string strWrongPassFile = tmp + "/wrong-pass.txt";
2374 nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") &&
2375 WriteTextFile(strWrongPassFile, "wrong-passphrase\n"),
2376 "passphrase files can be written for restarted commands") ? 0 : 1;
2377
2378 // Step 1: export the Berkeley DB wallet to SQLite.
2379 vector<string> vExportArgs;
2380 vExportArgs.push_back("-datadir=" + tmp);
2381 vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath);
2382 vExportArgs.push_back("-nogui");
2383 int nExportRet = RunBitflashChild(strExe, vExportArgs);
2384 nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()),
2385 "the wallet exports to a SQLite file") ? 0 : 1;
2386 nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes),
2387 "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1;
2388
2389 // Step 2: encrypt through the real SQLite backend command path.
2390 string strEncryptOut = tmp + "/sqlite-encrypt-out.txt";
2391 vector<string> vEncryptArgs;
2392 vEncryptArgs.push_back("-datadir=" + tmp);
2393 vEncryptArgs.push_back("-walletbackend=sqlite");
2394 vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile);
2395 vEncryptArgs.push_back("-nogui");
2396 int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut);
2397 nFail += Check(nEncryptRet == 0,
2398 "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1;
2399 nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"),
2400 "encryption took the SQLite persistence path") ? 0 : 1;
2401 nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"),
2402 "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1;
2403
2404 // The encrypted SQLite wallet must no longer hold the plaintext secrets.
2405 nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes),
2406 "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1;
2407 nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster),
2408 "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1;
2409 nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode),
2410 "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1;
2411
2412 // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path
2413 // must never touch it, so its plaintext key is still exactly there.
2414 nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes),
2415 "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1;
2416
2417 // Step 3: a locked encrypted SQLite wallet reports that phrase coverage
2418 // needs an unlock, exactly as the Berkeley DB path does (exit code 2).
2419 string strLockedAudit = tmp + "/locked-recovery-audit.txt";
2420 vector<string> vLockedArgs;
2421 vLockedArgs.push_back("-datadir=" + tmp);
2422 vLockedArgs.push_back("-walletbackend=sqlite");
2423 vLockedArgs.push_back("-recoveryaudit");
2424 vLockedArgs.push_back("-nogui");
2425 int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit);
2426 nFail += Check(nLockedRet == 2 &&
2427 FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"),
2428 "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1;
2429
2430 // The wrong passphrase is rejected and reveals nothing.
2431 string strWrongDump = tmp + "/wrong-dump.txt";
2432 vector<string> vWrongArgs;
2433 vWrongArgs.push_back("-datadir=" + tmp);
2434 vWrongArgs.push_back("-walletbackend=sqlite");
2435 vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile);
2436 vWrongArgs.push_back("-dumpwallet=" + strWrongDump);
2437 vWrongArgs.push_back("-nogui");
2438 int nWrongRet = RunBitflashChild(strExe, vWrongArgs);
2439 nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()),
2440 "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1;
2441
2442 // The right passphrase unlocks and can dump the original key.
2443 string strRightDump = tmp + "/right-dump.txt";
2444 vector<string> vRightArgs;
2445 vRightArgs.push_back("-datadir=" + tmp);
2446 vRightArgs.push_back("-walletbackend=sqlite");
2447 vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile);
2448 vRightArgs.push_back("-dumpwallet=" + strRightDump);
2449 vRightArgs.push_back("-nogui");
2450 int nRightRet = RunBitflashChild(strExe, vRightArgs);
2451 nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()),
2452 "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1;
2453 nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)),
2454 "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1;
2455
2456 // The recovery phrase survives the encryption.
2457 string strAudit = tmp + "/recovery-audit.txt";
2458 vector<string> vAuditArgs;
2459 vAuditArgs.push_back("-datadir=" + tmp);
2460 vAuditArgs.push_back("-walletbackend=sqlite");
2461 vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile);
2462 vAuditArgs.push_back("-recoveryaudit");
2463 vAuditArgs.push_back("-nogui");
2464 int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit);
2465 nFail += Check(nAuditRet == 0 &&
2466 FileContainsText(strAudit, "recovery phrase: present"),
2467 "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1;
2468
2469 // One-shot storage commands honour -walletbackend. The SQLite wallet is
2470 // encrypted; the wallet.dat beside it is still plaintext. Same datadir,
2471 // opposite answers -> the flag routes the diagnostic to the right file.
2472 string strSqliteAuditJson = tmp + "/sqlite-audit.json";
2473 vector<string> vSqliteAuditArgs;
2474 vSqliteAuditArgs.push_back("-datadir=" + tmp);
2475 vSqliteAuditArgs.push_back("-walletbackend=sqlite");
2476 vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson);
2477 vSqliteAuditArgs.push_back("-nogui");
2478 int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs);
2479 nFail += Check(nSqliteAuditRet == 0 &&
2480 FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") &&
2481 FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""),
2482 "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1;
2483
2484 string strBdbAuditJson = tmp + "/bdb-audit.json";
2485 vector<string> vBdbAuditArgs;
2486 vBdbAuditArgs.push_back("-datadir=" + tmp);
2487 vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson);
2488 vBdbAuditArgs.push_back("-nogui");
2489 int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs);
2490 nFail += Check(nBdbAuditRet == 0 &&
2491 FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") &&
2492 FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""),
2493 "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1;
2494
2495 string strSqliteCheckOut = tmp + "/sqlite-check.txt";
2496 vector<string> vSqliteCheckArgs;
2497 vSqliteCheckArgs.push_back("-datadir=" + tmp);
2498 vSqliteCheckArgs.push_back("-walletbackend=sqlite");
2499 vSqliteCheckArgs.push_back("-walletstoragecheck");
2500 vSqliteCheckArgs.push_back("-nogui");
2501 int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut);
2502 nFail += Check(nSqliteCheckRet == 0,
2503 "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1;
2504
2505 // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat.
2506 // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds
2507 // none of the plaintext key that wallet.dat still contains.
2508 string strBackupDest = tmp + "/backup.sqlite";
2509 vector<string> vBackupArgs;
2510 vBackupArgs.push_back("-datadir=" + tmp);
2511 vBackupArgs.push_back("-walletbackend=sqlite");
2512 vBackupArgs.push_back("-backupwallet=" + strBackupDest);
2513 vBackupArgs.push_back("-nogui");
2514 int nBackupRet = RunBitflashChild(strExe, vBackupArgs);
2515 nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()),
2516 "-backupwallet under SQLite writes a backup file") ? 0 : 1;
2517 nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes),
2518 "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1;
2519
2520 string strBackupLoadCheck = tmp + "/backup-loadcheck.txt";
2521 vector<string> vLoadCheckArgs;
2522 vLoadCheckArgs.push_back("-datadir=" + tmp);
2523 vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest);
2524 vLoadCheckArgs.push_back("-nogui");
2525 int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck);
2526 nFail += Check(nBackupLoadRet == 0,
2527 "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1;
2528 }
2529 catch (const std::exception& e)
2530 {
2531 printf(" FAIL exception: %s\n", e.what());
2532 nFail++;
2533 }
2534 catch (...)
2535 {
2536 printf(" FAIL unknown exception\n");
2537 nFail++;
2538 }
2539
2540 DBFlush(true);
2541 SetCurrentDir(cwd);
2542 RemoveTree(tmp);
2543 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2544 nFail, nFail == 1 ? "" : "s");
2545 fflush(stdout);
2546 return nFail == 0 ? 0 : 1;
2547}
2548
2549// Backend resolution when no -walletbackend flag is given: a fresh install
2550// starts on SQLite (the new default), an existing wallet.dat stays on Berkeley
2551// DB, the persistent marker overrides the default, and an explicit flag
2552// overrides everything. Each scenario runs a real child process.
2553static int RunWalletBackendDefaultSelfTest()
2554{
2555 fflush(stdout);
2556 printf("wallet-backend-default self-test\n");
2557
2558 std::string tmp, cwd;
2559 if (!MakeTempDir(tmp))
2560 {
2561 printf(" FAIL could not create a temporary data directory\n");
2562 return 1;
2563 }
2564 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2565 {
2566 printf(" FAIL could not move into the temporary data directory\n");
2567 RemoveTree(tmp);
2568 return 1;
2569 }
2570
2571 int nFail = 0;
2572 strSetDataDir = tmp;
2573 printf(" temp datadir: %s\n", tmp.c_str());
2574
2575#ifdef _WIN32
2576 string strExe = cwd + "\\bitflash.exe";
2577#else
2578 string strExe = cwd + "/bitflash-node";
2579#endif
2580
2581 try
2582 {
2583 // 1. Fresh datadir, no flag -> this fork refuses, where upstream creates.
2584 // An empty datadir on a headless node is far more often a mount or
2585 // backup fault than a first run, and creating a wallet also pins the
2586 // backend marker, after which restoring the real wallet is a no-op.
2587 // Nothing may be left behind: no wallet of either kind, no marker.
2588 string d1 = tmp + "/fresh";
2589 nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1;
2590 vector<string> a1;
2591 a1.push_back("-datadir=" + d1);
2592 a1.push_back("-nomanagedtor");
2593 a1.push_back("-nogui");
2594 a1.push_back("-newaddress");
2595 int r0 = RunBitflashChild(strExe, a1);
2596 nFail += Check(r0 != 0 &&
2597 !FileExists((d1 + "/wallet.sqlite").c_str()) &&
2598 !FileExists((d1 + "/wallet.dat").c_str()) &&
2599 !FileExists((d1 + "/wallet-backend").c_str()),
2600 "a flagless headless start refuses to create a wallet") ? 0 : 1;
2601
2602 // 1a. -createwallet says it was meant: new wallet.sqlite, marker
2603 // "sqlite", no wallet.dat.
2604 vector<string> a1c = a1;
2605 a1c.push_back("-createwallet");
2606 int r1 = RunBitflashChild(strExe, a1c);
2607 nFail += Check(r1 == 0 &&
2608 FileExists((d1 + "/wallet.sqlite").c_str()) &&
2609 !FileExists((d1 + "/wallet.dat").c_str()) &&
2610 FileContainsText(d1 + "/wallet-backend", "sqlite"),
2611 "-createwallet starts a fresh install on SQLite and records the marker") ? 0 : 1;
2612
2613 // 1b. Second run reuses it without the flag -- the refusal only guards
2614 // creation, so an existing wallet still starts flagless.
2615 int r1b = RunBitflashChild(strExe, a1);
2616 nFail += Check(r1b == 0 &&
2617 FileExists((d1 + "/wallet.sqlite").c_str()) &&
2618 !FileExists((d1 + "/wallet.dat").c_str()),
2619 "the recorded SQLite choice survives a flagless restart") ? 0 : 1;
2620
2621 // 2. Existing wallet.dat, no flag -> stays on Berkeley DB.
2622 string d2 = tmp + "/existing-bdb";
2623 nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1;
2624 vector<string> a2c;
2625 a2c.push_back("-datadir=" + d2);
2626 a2c.push_back("-nomanagedtor");
2627 a2c.push_back("-nogui");
2628 a2c.push_back("-walletbackend=bdb");
2629 a2c.push_back("-newphrase");
2630 RunBitflashChild(strExe, a2c);
2631 vector<string> a2;
2632 a2.push_back("-datadir=" + d2);
2633 a2.push_back("-nomanagedtor");
2634 a2.push_back("-nogui");
2635 a2.push_back("-newaddress");
2636 int r2 = RunBitflashChild(strExe, a2);
2637 nFail += Check(r2 == 0 &&
2638 FileExists((d2 + "/wallet.dat").c_str()) &&
2639 !FileExists((d2 + "/wallet.sqlite").c_str()),
2640 "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1;
2641
2642 // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir.
2643 string d3 = tmp + "/marker-bdb";
2644 nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1;
2645 nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"),
2646 "a bdb marker can be written") ? 0 : 1;
2647 vector<string> a3;
2648 a3.push_back("-datadir=" + d3);
2649 a3.push_back("-nomanagedtor");
2650 a3.push_back("-nogui");
2651 a3.push_back("-newaddress");
2652 int r3 = RunBitflashChild(strExe, a3);
2653 nFail += Check(r3 == 0 &&
2654 FileExists((d3 + "/wallet.dat").c_str()) &&
2655 !FileExists((d3 + "/wallet.sqlite").c_str()),
2656 "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1;
2657
2658 // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat.
2659 string d4 = tmp + "/explicit-bdb";
2660 nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1;
2661 vector<string> a4;
2662 a4.push_back("-datadir=" + d4);
2663 a4.push_back("-nomanagedtor");
2664 a4.push_back("-nogui");
2665 a4.push_back("-walletbackend=bdb");
2666 a4.push_back("-newaddress");
2667 int r4 = RunBitflashChild(strExe, a4);
2668 nFail += Check(r4 == 0 &&
2669 FileExists((d4 + "/wallet.dat").c_str()) &&
2670 !FileExists((d4 + "/wallet.sqlite").c_str()),
2671 "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1;
2672 }
2673 catch (const std::exception& e)
2674 {
2675 printf(" FAIL exception: %s\n", e.what());
2676 nFail++;
2677 }
2678 catch (...)
2679 {
2680 printf(" FAIL unknown exception\n");
2681 nFail++;
2682 }
2683
2684 DBFlush(true);
2685 SetCurrentDir(cwd);
2686 RemoveTree(tmp);
2687 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2688 nFail, nFail == 1 ? "" : "s");
2689 fflush(stdout);
2690 return nFail == 0 ? 0 : 1;
2691}
2692
2693// The wizard's conversion action, headless: build a Berkeley DB wallet, run
2694// DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a
2695// usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then
2696// that a flagless restart opens the converted SQLite wallet.
2697static int RunWalletConvertSelfTest()
2698{
2699 fflush(stdout);
2700 printf("wallet-convert self-test\n");
2701
2702 std::string tmp, cwd;
2703 if (!MakeTempDir(tmp))
2704 {
2705 printf(" FAIL could not create a temporary data directory\n");
2706 return 1;
2707 }
2708 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2709 {
2710 printf(" FAIL could not move into the temporary data directory\n");
2711 RemoveTree(tmp);
2712 return 1;
2713 }
2714
2715 int nFail = 0;
2716 strSetDataDir = tmp;
2717 printf(" temp datadir: %s\n", tmp.c_str());
2718
2719#ifdef _WIN32
2720 string strExe = cwd + "\\bitflash.exe";
2721#else
2722 string strExe = cwd + "/bitflash-node";
2723#endif
2724
2725 try
2726 {
2727 if (!LoadWallet())
2728 throw std::runtime_error("LoadWallet failed");
2729
2730 string strError;
2731 nFail += Check(SetHDSeedFromMnemonic(
2732 "abandon abandon abandon abandon abandon abandon abandon "
2733 "abandon abandon abandon abandon about", strError),
2734 "a phrase can be installed before conversion") ? 0 : 1;
2735 TopUpKeyPool();
2736
2737 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
2738 CPrivKey vchDefaultPrivKey;
2739 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
2740 "the default private key is readable before conversion") ? 0 : 1;
2741 vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(),
2742 vchDefaultPrivKey.end());
2743
2744 // The conversion action the GUI button invokes.
2745 int nCopied = 0;
2746 string strConvertError;
2747 bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError);
2748 nFail += Check(fConverted && nCopied > 0,
2749 "DoConvertWalletToSQLite exports the wallet") ? 0 : 1;
2750 nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) &&
2751 FileContainsText(tmp + "/wallet-backend", "sqlite"),
2752 "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1;
2753 nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes),
2754 "wallet.dat is left untouched by the conversion") ? 0 : 1;
2755
2756 // Idempotence guard: a second conversion refuses rather than overwrite.
2757 int nCopied2 = 0;
2758 string strSecondError;
2759 nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError),
2760 "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1;
2761
2762 DBFlush(true);
2763
2764 // A flagless restart now opens the converted SQLite wallet (the marker
2765 // routes it) and finds the same recovery phrase.
2766 string strAudit = tmp + "/converted-recovery-audit.txt";
2767 vector<string> vAuditArgs;
2768 vAuditArgs.push_back("-datadir=" + tmp);
2769 vAuditArgs.push_back("-nomanagedtor");
2770 vAuditArgs.push_back("-nogui");
2771 vAuditArgs.push_back("-recoveryaudit");
2772 int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit);
2773 nFail += Check(nAuditRet == 0 &&
2774 FileContainsText(strAudit, "recovery phrase: present"),
2775 "a flagless restart opens the converted SQLite wallet") ? 0 : 1;
2776 }
2777 catch (const std::exception& e)
2778 {
2779 printf(" FAIL exception: %s\n", e.what());
2780 nFail++;
2781 }
2782 catch (...)
2783 {
2784 printf(" FAIL unknown exception\n");
2785 nFail++;
2786 }
2787
2788 DBFlush(true);
2789 SetCurrentDir(cwd);
2790 RemoveTree(tmp);
2791 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2792 nFail, nFail == 1 ? "" : "s");
2793 fflush(stdout);
2794 return nFail == 0 ? 0 : 1;
2795}
2796
2797static int RunWalletPortabilitySelfTest()
2798{
2799 fflush(stdout);
2800 printf("wallet-portability self-test\n");
2801
2802 std::string tmp;
2803 std::string cwd;
2804 if (!MakeTempDir(tmp))
2805 {
2806 printf(" FAIL could not create a temporary data directory\n");
2807 return 1;
2808 }
2809 if (!GetCurrentDir(cwd))
2810 {
2811 printf(" FAIL could not read current directory\n");
2812 RemoveTree(tmp);
2813 return 1;
2814 }
2815
2816 int nFail = 0;
2817 printf(" temp root: %s\n", tmp.c_str());
2818
2819 try
2820 {
2821#ifdef _WIN32
2822 string strExe = cwd + "\\bitflash.exe";
2823#else
2824 string strExe = cwd + "/bitflash-node";
2825#endif
2826 string strOriginal = tmp + "/original";
2827 string strRawCopy = tmp + "/raw-copy";
2828 string strBackupCopy = tmp + "/backup-copy";
2829 nFail += Check(MakeDirLocal(strOriginal) &&
2830 MakeDirLocal(strRawCopy) &&
2831 MakeDirLocal(strBackupCopy),
2832 "temporary wallet directories can be created") ? 0 : 1;
2833
2834 string strCreateOut = tmp + "/create.txt";
2835 vector<string> vCreateArgs;
2836 vCreateArgs.push_back("-datadir=" + strOriginal);
2837 vCreateArgs.push_back("-nomanagedtor");
2838 vCreateArgs.push_back("-nogui");
2839 vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability
2840 vCreateArgs.push_back("-newphrase");
2841 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
2842 nFail += Check(nCreateRet == 0 &&
2843 FileContainsText(strCreateOut, "Write these twelve words down"),
2844 "a phrase wallet can be created in a fresh datadir") ? 0 : 1;
2845
2846 string strOriginalAudit = tmp + "/original-audit.txt";
2847 vector<string> vOriginalAuditArgs;
2848 vOriginalAuditArgs.push_back("-datadir=" + strOriginal);
2849 vOriginalAuditArgs.push_back("-nomanagedtor");
2850 vOriginalAuditArgs.push_back("-nogui");
2851 vOriginalAuditArgs.push_back("-recoveryaudit");
2852 int nOriginalAuditRet = RunBitflashChild(strExe, vOriginalAuditArgs, NULL, &strOriginalAudit);
2853 nFail += Check(nOriginalAuditRet == 0 &&
2854 FileContainsText(strOriginalAudit, "recovery phrase: present") &&
2855 FileContainsText(strOriginalAudit, "SLIP-0044 BITFLASH"),
2856 "the original wallet reports its recovery phrase") ? 0 : 1;
2857
2858 string strOriginalStorageAudit = tmp + "/original-storage-audit.txt";
2859 vector<string> vOriginalStorageAuditArgs;
2860 vOriginalStorageAuditArgs.push_back("-datadir=" + strOriginal);
2861 vOriginalStorageAuditArgs.push_back("-nomanagedtor");
2862 vOriginalStorageAuditArgs.push_back("-nogui");
2863 vOriginalStorageAuditArgs.push_back("-walletstorageaudit");
2864 int nOriginalStorageAuditRet =
2865 RunBitflashChild(strExe, vOriginalStorageAuditArgs, NULL, &strOriginalStorageAudit);
2866 nFail += Check(nOriginalStorageAuditRet == 0 &&
2867 FileContainsText(strOriginalStorageAudit, "Wallet storage audit") &&
2868 FileContainsText(strOriginalStorageAudit, "plain HD seed: complete") &&
2869 FileContainsText(strOriginalStorageAudit, "encrypted HD seed: none") &&
2870 FileContainsText(strOriginalStorageAudit, "malformed records: 0"),
2871 "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1;
2872
2873 string strOriginalStorageCheck = tmp + "/original-storage-check.txt";
2874 vector<string> vOriginalStorageCheckArgs;
2875 vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal);
2876 vOriginalStorageCheckArgs.push_back("-nomanagedtor");
2877 vOriginalStorageCheckArgs.push_back("-nogui");
2878 vOriginalStorageCheckArgs.push_back("-walletstoragecheck");
2879 int nOriginalStorageCheckRet =
2880 RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck);
2881 nFail += Check(nOriginalStorageCheckRet == 0 &&
2882 FileContainsText(strOriginalStorageCheck, "storage sanity: ok") &&
2883 FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"),
2884 "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1;
2885
2886 string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json";
2887 vector<string> vOriginalStorageAuditJsonArgs;
2888 vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal);
2889 vOriginalStorageAuditJsonArgs.push_back("-nomanagedtor");
2890 vOriginalStorageAuditJsonArgs.push_back("-nogui");
2891 vOriginalStorageAuditJsonArgs.push_back("-walletstorageauditjson=" +
2892 strOriginalStorageAuditJson);
2893 int nOriginalStorageAuditJsonRet =
2894 RunBitflashChild(strExe, vOriginalStorageAuditJsonArgs);
2895 nFail += Check(nOriginalStorageAuditJsonRet == 0 &&
2896 FileContainsText(strOriginalStorageAuditJson,
2897 "\"format\": \"bitflash-wallet-storage-audit-v1\"") &&
2898 FileContainsText(strOriginalStorageAuditJson,
2899 "\"plain_hd_seed\": \"complete\"") &&
2900 FileContainsText(strOriginalStorageAuditJson,
2901 "\"encrypted_hd_seed\": \"none\""),
2902 "the storage audit can write plaintext-wallet JSON") ? 0 : 1;
2903
2904 string strPortableWallet = tmp + "/portable-wallet.dat";
2905 string strBackupOut = tmp + "/backupwallet.txt";
2906 vector<string> vBackupArgs;
2907 vBackupArgs.push_back("-datadir=" + strOriginal);
2908 vBackupArgs.push_back("-nomanagedtor");
2909 vBackupArgs.push_back("-nogui");
2910 vBackupArgs.push_back("-backupwallet=" + strPortableWallet);
2911 int nBackupRet = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut);
2912 nFail += Check(nBackupRet == 0 && FileExists(strPortableWallet.c_str()),
2913 "/backupwallet writes a portable wallet.dat") ? 0 : 1;
2914
2915 string strBackupOut2 = tmp + "/backupwallet-overwrite.txt";
2916 int nBackupRet2 = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut2);
2917 nFail += Check(nBackupRet2 == 0 && FileExists(strPortableWallet.c_str()) &&
2918 !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.tmp.") &&
2919 !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.old."),
2920 "/backupwallet replaces an existing backup without leaving temp files") ? 0 : 1;
2921
2922 nFail += Check(CopyFileLocal(strOriginal + "/wallet.dat",
2923 strRawCopy + "/wallet.dat"),
2924 "a raw wallet.dat can be copied without database logs") ? 0 : 1;
2925 string strRawAudit = tmp + "/raw-copy-audit.txt";
2926 vector<string> vRawAuditArgs;
2927 vRawAuditArgs.push_back("-datadir=" + strRawCopy);
2928 vRawAuditArgs.push_back("-nomanagedtor");
2929 vRawAuditArgs.push_back("-nogui");
2930 vRawAuditArgs.push_back("-recoveryaudit");
2931 int nRawAuditRet = RunBitflashChild(strExe, vRawAuditArgs, NULL, &strRawAudit);
2932 nFail += Check(nRawAuditRet == 0 &&
2933 FileContainsText(strRawAudit, "recovery phrase: present") &&
2934 !FileContainsText(strRawAudit, "Cannot open wallet.dat"),
2935 "a raw wallet.dat copy opens in a new datadir") ? 0 : 1;
2936
2937 nFail += Check(CopyFileLocal(strPortableWallet,
2938 strBackupCopy + "/wallet.dat"),
2939 "the portable backup can be installed as wallet.dat") ? 0 : 1;
2940 string strBackupAudit = tmp + "/backup-copy-audit.txt";
2941 vector<string> vBackupAuditArgs;
2942 vBackupAuditArgs.push_back("-datadir=" + strBackupCopy);
2943 vBackupAuditArgs.push_back("-nomanagedtor");
2944 vBackupAuditArgs.push_back("-nogui");
2945 vBackupAuditArgs.push_back("-recoveryaudit");
2946 int nBackupAuditRet = RunBitflashChild(strExe, vBackupAuditArgs, NULL, &strBackupAudit);
2947 nFail += Check(nBackupAuditRet == 0 &&
2948 FileContainsText(strBackupAudit, "recovery phrase: present") &&
2949 FileContainsText(strBackupAudit, "SLIP-0044 BITFLASH"),
2950 "a /backupwallet copy opens in a new datadir") ? 0 : 1;
2951
2952 string strPassFile = tmp + "/encrypt-pass.txt";
2953 string strEncryptOut = tmp + "/encryptwallet.txt";
2954 nFail += Check(WriteTextFile(strPassFile, "portable-test-passphrase\n"),
2955 "an encryption passphrase file can be written") ? 0 : 1;
2956 vector<string> vEncryptArgs;
2957 vEncryptArgs.push_back("-datadir=" + strOriginal);
2958 vEncryptArgs.push_back("-nomanagedtor");
2959 vEncryptArgs.push_back("-nogui");
2960 vEncryptArgs.push_back("-encryptwallet=@" + strPassFile);
2961 int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut);
2962 nFail += Check(nEncryptRet == 0 &&
2963 !DirectoryHasFileWithPrefix(strOriginal + "/database", "log."),
2964 "the encryptwallet command purges Berkeley DB environment logs") ? 0 : 1;
2965
2966 string strEncryptedStorageAudit = tmp + "/encrypted-storage-audit.txt";
2967 vector<string> vEncryptedStorageAuditArgs;
2968 vEncryptedStorageAuditArgs.push_back("-datadir=" + strOriginal);
2969 vEncryptedStorageAuditArgs.push_back("-nomanagedtor");
2970 vEncryptedStorageAuditArgs.push_back("-nogui");
2971 vEncryptedStorageAuditArgs.push_back("-walletstorageaudit");
2972 int nEncryptedStorageAuditRet =
2973 RunBitflashChild(strExe, vEncryptedStorageAuditArgs, NULL, &strEncryptedStorageAudit);
2974 nFail += Check(nEncryptedStorageAuditRet == 0 &&
2975 FileContainsText(strEncryptedStorageAudit, "plain private keys: 0") &&
2976 FileContainsText(strEncryptedStorageAudit, "encrypted private keys:") &&
2977 FileContainsText(strEncryptedStorageAudit, "plain HD seed: none") &&
2978 FileContainsText(strEncryptedStorageAudit, "encrypted HD seed: complete") &&
2979 FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") &&
2980 FileContainsText(strEncryptedStorageAudit, "malformed records: 0"),
2981 "the storage audit recognizes an encrypted wallet") ? 0 : 1;
2982
2983 string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt";
2984 vector<string> vEncryptedStorageCheckArgs;
2985 vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal);
2986 vEncryptedStorageCheckArgs.push_back("-nomanagedtor");
2987 vEncryptedStorageCheckArgs.push_back("-nogui");
2988 vEncryptedStorageCheckArgs.push_back("-walletstoragecheck");
2989 int nEncryptedStorageCheckRet =
2990 RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck);
2991 nFail += Check(nEncryptedStorageCheckRet == 0 &&
2992 FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") &&
2993 FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") &&
2994 FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"),
2995 "the storage sanity check accepts an encrypted wallet") ? 0 : 1;
2996
2997 string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json";
2998 vector<string> vEncryptedStorageAuditJsonArgs;
2999 vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal);
3000 vEncryptedStorageAuditJsonArgs.push_back("-nomanagedtor");
3001 vEncryptedStorageAuditJsonArgs.push_back("-nogui");
3002 vEncryptedStorageAuditJsonArgs.push_back("-walletstorageauditjson=" +
3003 strEncryptedStorageAuditJson);
3004 int nEncryptedStorageAuditJsonRet =
3005 RunBitflashChild(strExe, vEncryptedStorageAuditJsonArgs);
3006 nFail += Check(nEncryptedStorageAuditJsonRet == 0 &&
3007 FileContainsText(strEncryptedStorageAuditJson,
3008 "\"plain_private_keys\": 0") &&
3009 FileContainsText(strEncryptedStorageAuditJson,
3010 "\"plain_hd_seed\": \"none\"") &&
3011 FileContainsText(strEncryptedStorageAuditJson,
3012 "\"encrypted_hd_seed\": \"complete\"") &&
3013 FileContainsText(strEncryptedStorageAuditJson,
3014 "\"wallet_minimum_version\": \"present\""),
3015 "the storage audit can write encrypted-wallet JSON") ? 0 : 1;
3016 }
3017 catch (const std::exception& e)
3018 {
3019 printf(" FAIL exception: %s\n", e.what());
3020 nFail++;
3021 }
3022 catch (...)
3023 {
3024 printf(" FAIL unknown exception\n");
3025 nFail++;
3026 }
3027
3028 RemoveTree(tmp);
3029 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
3030 nFail, nFail == 1 ? "" : "s");
3031 fflush(stdout);
3032 return nFail == 0 ? 0 : 1;
3033}
3034
1135static int RunNetMessageSelfTest() 3035static int RunNetMessageSelfTest()
1136{ 3036{
1137 fflush(stdout); 3037 fflush(stdout);
@@ -1794,10 +3694,26 @@ int RunSelfTest(const std::string& name)
1794 return RunWalletHDSelfTest(); 3694 return RunWalletHDSelfTest();
1795 if (name == "wallet-format") 3695 if (name == "wallet-format")
1796 return RunWalletFormatSelfTest(); 3696 return RunWalletFormatSelfTest();
3697 if (name == "wallet-storage-sanity")
3698 return RunWalletStorageSanitySelfTest();
3699 if (name == "db-env-reopen")
3700 return RunDbEnvReopenSelfTest();
3701 if (name == "wallet-sqlite")
3702 return RunWalletSQLiteSelfTest();
3703 if (name == "wallet-sqlite-migration")
3704 return RunWalletSQLiteMigrationSelfTest();
1797 if (name == "wallet-crypto") 3705 if (name == "wallet-crypto")
1798 return RunWalletCryptoSelfTest(); 3706 return RunWalletCryptoSelfTest();
1799 if (name == "wallet-encrypt") 3707 if (name == "wallet-encrypt")
1800 return RunWalletEncryptSelfTest(); 3708 return RunWalletEncryptSelfTest();
3709 if (name == "wallet-sqlite-encrypt")
3710 return RunWalletSQLiteEncryptSelfTest();
3711 if (name == "wallet-backend-default")
3712 return RunWalletBackendDefaultSelfTest();
3713 if (name == "wallet-convert")
3714 return RunWalletConvertSelfTest();
3715 if (name == "wallet-portability")
3716 return RunWalletPortabilitySelfTest();
1801 if (name == "net-message") 3717 if (name == "net-message")
1802 return RunNetMessageSelfTest(); 3718 return RunNetMessageSelfTest();
1803 if (name == "consensus-limits") 3719 if (name == "consensus-limits")
@@ -1814,6 +3730,6 @@ int RunSelfTest(const std::string& name)
1814 return RunManagedTorSelfTest(); 3730 return RunManagedTorSelfTest();
1815 3731
1816 printf("Unknown self-test '%s'\n", name.c_str()); 3732 printf("Unknown self-test '%s'\n", name.c_str());
1817 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n"); 3733 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-sqlite-encrypt, wallet-backend-default, wallet-convert, wallet-portability, net-message, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n");
1818 return 1; 3734 return 1;
1819} 3735}
diff --git a/src/selftest.h b/src/selftest.h
index d3699c6..0ffe232 100644
--- a/src/selftest.h
+++ b/src/selftest.h
@@ -7,5 +7,6 @@
7#include <string> 7#include <string>
8 8
9int RunSelfTest(const std::string& name); 9int RunSelfTest(const std::string& name);
10int RunSelfTestMutateWallet(const std::string& name);
10 11
11#endif 12#endif
diff --git a/src/wallet_sqlite.cpp b/src/wallet_sqlite.cpp
new file mode 100644
index 0000000..f4b4b37
--- /dev/null
+++ b/src/wallet_sqlite.cpp
@@ -0,0 +1,374 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers.h"
5#include "wallet_sqlite.h"
6
7static void SetSQLiteError(sqlite3* pdb, const string& strContext,
8 string& strError)
9{
10 if (pdb)
11 strError = strContext + ": " + sqlite3_errmsg(pdb);
12 else
13 strError = strContext;
14}
15
16CWalletDBSQLite::CWalletDBSQLite() : pdb(NULL)
17{
18}
19
20CWalletDBSQLite::~CWalletDBSQLite()
21{
22 Close();
23}
24
25bool CWalletDBSQLite::Open(const string& strPath, string& strError)
26{
27 Close();
28
29 int ret = sqlite3_open_v2(strPath.c_str(), &pdb,
30 SQLITE_OPEN_READWRITE |
31 SQLITE_OPEN_CREATE |
32 SQLITE_OPEN_FULLMUTEX,
33 NULL);
34 if (ret != SQLITE_OK)
35 {
36 SetSQLiteError(pdb, "cannot open SQLite wallet", strError);
37 Close();
38 return false;
39 }
40
41 sqlite3_busy_timeout(pdb, 5000);
42
43 if (!Exec("PRAGMA journal_mode=WAL;", strError))
44 {
45 Close();
46 return false;
47 }
48 if (!Exec("PRAGMA synchronous=FULL;", strError))
49 {
50 Close();
51 return false;
52 }
53 if (!InitSchema(strError))
54 {
55 Close();
56 return false;
57 }
58 return true;
59}
60
61bool CWalletDBSQLite::OpenReadOnly(const string& strPath, string& strError)
62{
63 Close();
64
65 int ret = sqlite3_open_v2(strPath.c_str(), &pdb,
66 SQLITE_OPEN_READONLY |
67 SQLITE_OPEN_FULLMUTEX,
68 NULL);
69 if (ret != SQLITE_OK)
70 {
71 SetSQLiteError(pdb, "cannot open SQLite wallet read-only", strError);
72 Close();
73 return false;
74 }
75
76 sqlite3_busy_timeout(pdb, 5000);
77 return true;
78}
79
80void CWalletDBSQLite::Close()
81{
82 if (pdb)
83 {
84 sqlite3_close(pdb);
85 pdb = NULL;
86 }
87}
88
89bool CWalletDBSQLite::BeginTransaction(string& strError)
90{
91 if (!pdb)
92 {
93 strError = "SQLite wallet is not open";
94 return false;
95 }
96 return Exec("BEGIN IMMEDIATE;", strError);
97}
98
99bool CWalletDBSQLite::CommitTransaction(string& strError)
100{
101 if (!pdb)
102 {
103 strError = "SQLite wallet is not open";
104 return false;
105 }
106 return Exec("COMMIT;", strError);
107}
108
109bool CWalletDBSQLite::RollbackTransaction(string& strError)
110{
111 if (!pdb)
112 {
113 strError = "SQLite wallet is not open";
114 return false;
115 }
116 return Exec("ROLLBACK;", strError);
117}
118
119bool CWalletDBSQLite::Checkpoint(string& strError)
120{
121 if (!pdb)
122 {
123 strError = "SQLite wallet is not open";
124 return false;
125 }
126 return Exec("PRAGMA wal_checkpoint(TRUNCATE);", strError);
127}
128
129bool CWalletDBSQLite::Exec(const char* pszSql, string& strError)
130{
131 char* pszErr = NULL;
132 int ret = sqlite3_exec(pdb, pszSql, NULL, NULL, &pszErr);
133 if (ret != SQLITE_OK)
134 {
135 if (pszErr)
136 {
137 strError = pszErr;
138 sqlite3_free(pszErr);
139 }
140 else
141 SetSQLiteError(pdb, "SQLite exec failed", strError);
142 return false;
143 }
144 return true;
145}
146
147bool CWalletDBSQLite::InitSchema(string& strError)
148{
149 return Exec("CREATE TABLE IF NOT EXISTS wallet_records ("
150 "key BLOB PRIMARY KEY NOT NULL,"
151 "value BLOB NOT NULL"
152 ");",
153 strError) &&
154 Exec("PRAGMA user_version=1;", strError);
155}
156
157bool CWalletDBSQLite::WriteRecord(const vector<unsigned char>& vchKey,
158 const vector<unsigned char>& vchValue,
159 string& strError,
160 bool fOverwrite)
161{
162 if (!pdb)
163 {
164 strError = "SQLite wallet is not open";
165 return false;
166 }
167 if (vchKey.empty())
168 {
169 strError = "SQLite wallet record key is empty";
170 return false;
171 }
172
173 sqlite3_stmt* stmt = NULL;
174 const char* pszSql = fOverwrite
175 ? "INSERT OR REPLACE INTO wallet_records(key, value) VALUES(?, ?);"
176 : "INSERT INTO wallet_records(key, value) VALUES(?, ?);";
177 int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL);
178 if (ret != SQLITE_OK)
179 {
180 SetSQLiteError(pdb, "cannot prepare SQLite wallet write", strError);
181 return false;
182 }
183
184 ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(),
185 SQLITE_TRANSIENT);
186 if (ret == SQLITE_OK)
187 ret = sqlite3_bind_blob(stmt, 2,
188 vchValue.empty() ? NULL : &vchValue[0],
189 (int)vchValue.size(),
190 SQLITE_TRANSIENT);
191 if (ret == SQLITE_OK)
192 ret = sqlite3_step(stmt);
193
194 bool fOk = (ret == SQLITE_DONE);
195 if (!fOk)
196 SetSQLiteError(pdb, "cannot write SQLite wallet record", strError);
197 sqlite3_finalize(stmt);
198 return fOk;
199}
200
201bool CWalletDBSQLite::ReadRecord(const vector<unsigned char>& vchKey,
202 vector<unsigned char>& vchValueRet,
203 string& strError)
204{
205 vchValueRet.clear();
206 if (!pdb)
207 {
208 strError = "SQLite wallet is not open";
209 return false;
210 }
211 if (vchKey.empty())
212 {
213 strError = "SQLite wallet record key is empty";
214 return false;
215 }
216
217 sqlite3_stmt* stmt = NULL;
218 const char* pszSql = "SELECT value FROM wallet_records WHERE key = ?;";
219 int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL);
220 if (ret != SQLITE_OK)
221 {
222 SetSQLiteError(pdb, "cannot prepare SQLite wallet read", strError);
223 return false;
224 }
225
226 ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(),
227 SQLITE_TRANSIENT);
228 if (ret != SQLITE_OK)
229 {
230 SetSQLiteError(pdb, "cannot bind SQLite wallet key", strError);
231 sqlite3_finalize(stmt);
232 return false;
233 }
234
235 ret = sqlite3_step(stmt);
236 if (ret == SQLITE_ROW)
237 {
238 const unsigned char* pch =
239 (const unsigned char*)sqlite3_column_blob(stmt, 0);
240 int nBytes = sqlite3_column_bytes(stmt, 0);
241 if (nBytes > 0 && pch)
242 vchValueRet.assign(pch, pch + nBytes);
243 sqlite3_finalize(stmt);
244 return true;
245 }
246
247 if (ret == SQLITE_DONE)
248 strError = "SQLite wallet record not found";
249 else
250 SetSQLiteError(pdb, "cannot read SQLite wallet record", strError);
251 sqlite3_finalize(stmt);
252 return false;
253}
254
255bool CWalletDBSQLite::EraseRecord(const vector<unsigned char>& vchKey,
256 string& strError)
257{
258 if (!pdb)
259 {
260 strError = "SQLite wallet is not open";
261 return false;
262 }
263 if (vchKey.empty())
264 {
265 strError = "SQLite wallet record key is empty";
266 return false;
267 }
268
269 sqlite3_stmt* stmt = NULL;
270 const char* pszSql = "DELETE FROM wallet_records WHERE key = ?;";
271 int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL);
272 if (ret != SQLITE_OK)
273 {
274 SetSQLiteError(pdb, "cannot prepare SQLite wallet erase", strError);
275 return false;
276 }
277
278 ret = sqlite3_bind_blob(stmt, 1, &vchKey[0], (int)vchKey.size(),
279 SQLITE_TRANSIENT);
280 if (ret == SQLITE_OK)
281 ret = sqlite3_step(stmt);
282
283 bool fOk = (ret == SQLITE_DONE);
284 if (!fOk)
285 SetSQLiteError(pdb, "cannot erase SQLite wallet record", strError);
286 sqlite3_finalize(stmt);
287 return fOk;
288}
289
290bool CWalletDBSQLite::ScanRecords(CWalletRecordVisitor& visitor,
291 string& strError)
292{
293 if (!pdb)
294 {
295 strError = "SQLite wallet is not open";
296 return false;
297 }
298
299 sqlite3_stmt* stmt = NULL;
300 const char* pszSql = "SELECT key, value FROM wallet_records ORDER BY rowid;";
301 int ret = sqlite3_prepare_v2(pdb, pszSql, -1, &stmt, NULL);
302 if (ret != SQLITE_OK)
303 {
304 SetSQLiteError(pdb, "cannot prepare SQLite wallet scan", strError);
305 return false;
306 }
307
308 while ((ret = sqlite3_step(stmt)) == SQLITE_ROW)
309 {
310 const unsigned char* pchKey =
311 (const unsigned char*)sqlite3_column_blob(stmt, 0);
312 int nKeyBytes = sqlite3_column_bytes(stmt, 0);
313 const unsigned char* pchValue =
314 (const unsigned char*)sqlite3_column_blob(stmt, 1);
315 int nValueBytes = sqlite3_column_bytes(stmt, 1);
316
317 vector<unsigned char> vchKey;
318 vector<unsigned char> vchValue;
319 if (nKeyBytes > 0 && pchKey)
320 vchKey.assign(pchKey, pchKey + nKeyBytes);
321 if (nValueBytes > 0 && pchValue)
322 vchValue.assign(pchValue, pchValue + nValueBytes);
323
324 CDataStream ssKey(vchKey, SER_DISK);
325 CDataStream ssValue(vchValue, SER_DISK);
326 if (!visitor.VisitWalletRecord(ssKey, ssValue, strError))
327 {
328 sqlite3_finalize(stmt);
329 return false;
330 }
331 }
332
333 if (ret != SQLITE_DONE)
334 {
335 SetSQLiteError(pdb, "cannot scan SQLite wallet records", strError);
336 sqlite3_finalize(stmt);
337 return false;
338 }
339
340 sqlite3_finalize(stmt);
341 return true;
342}
343
344bool CWalletDBSQLite::CountRecords(int& nRecordsRet, string& strError)
345{
346 nRecordsRet = 0;
347 if (!pdb)
348 {
349 strError = "SQLite wallet is not open";
350 return false;
351 }
352
353 sqlite3_stmt* stmt = NULL;
354 int ret = sqlite3_prepare_v2(pdb,
355 "SELECT COUNT(*) FROM wallet_records;",
356 -1, &stmt, NULL);
357 if (ret != SQLITE_OK)
358 {
359 SetSQLiteError(pdb, "cannot prepare SQLite wallet count", strError);
360 return false;
361 }
362
363 ret = sqlite3_step(stmt);
364 if (ret != SQLITE_ROW)
365 {
366 SetSQLiteError(pdb, "cannot count SQLite wallet records", strError);
367 sqlite3_finalize(stmt);
368 return false;
369 }
370
371 nRecordsRet = sqlite3_column_int(stmt, 0);
372 sqlite3_finalize(stmt);
373 return true;
374}
diff --git a/src/wallet_sqlite.h b/src/wallet_sqlite.h
new file mode 100644
index 0000000..d27e13a
--- /dev/null
+++ b/src/wallet_sqlite.h
@@ -0,0 +1,120 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#ifndef BITFLASH_WALLET_SQLITE_H
5#define BITFLASH_WALLET_SQLITE_H
6
7#include <sqlite3.h>
8
9class CWalletRecordVisitor;
10
11class CWalletDBSQLite
12{
13public:
14 CWalletDBSQLite();
15 ~CWalletDBSQLite();
16
17 bool Open(const string& strPath, string& strError);
18 bool OpenReadOnly(const string& strPath, string& strError);
19 void Close();
20
21 bool BeginTransaction(string& strError);
22 bool CommitTransaction(string& strError);
23 bool RollbackTransaction(string& strError);
24 bool Checkpoint(string& strError);
25
26 bool WriteRecord(const vector<unsigned char>& vchKey,
27 const vector<unsigned char>& vchValue,
28 string& strError,
29 bool fOverwrite=true);
30 bool ReadRecord(const vector<unsigned char>& vchKey,
31 vector<unsigned char>& vchValueRet,
32 string& strError);
33 bool EraseRecord(const vector<unsigned char>& vchKey,
34 string& strError);
35 bool ScanRecords(CWalletRecordVisitor& visitor, string& strError);
36 bool CountRecords(int& nRecordsRet, string& strError);
37
38 template<typename K, typename T>
39 bool WriteTypedRecord(const K& key, const T& value, string& strError,
40 bool fOverwrite=true)
41 {
42 CDataStream ssKey(SER_DISK);
43 ssKey.reserve(1000);
44 ssKey << key;
45
46 CDataStream ssValue(SER_DISK);
47 ssValue.reserve(10000);
48 ssValue << value;
49
50 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
51 vector<unsigned char> vchValue(ssValue.begin(), ssValue.end());
52 bool fOk = WriteRecord(vchKey, vchValue, strError, fOverwrite);
53
54 if (!vchKey.empty())
55 memset(&vchKey[0], 0, vchKey.size());
56 if (!vchValue.empty())
57 memset(&vchValue[0], 0, vchValue.size());
58 if (!ssKey.empty())
59 memset(&ssKey[0], 0, ssKey.size());
60 if (!ssValue.empty())
61 memset(&ssValue[0], 0, ssValue.size());
62 return fOk;
63 }
64
65 template<typename K, typename T>
66 bool ReadTypedRecord(const K& key, T& valueRet, string& strError)
67 {
68 CDataStream ssKey(SER_DISK);
69 ssKey.reserve(1000);
70 ssKey << key;
71 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
72
73 vector<unsigned char> vchValue;
74 bool fOk = ReadRecord(vchKey, vchValue, strError);
75 if (!vchKey.empty())
76 memset(&vchKey[0], 0, vchKey.size());
77 if (!ssKey.empty())
78 memset(&ssKey[0], 0, ssKey.size());
79 if (!fOk)
80 return false;
81
82 CDataStream ssValue(vchValue, SER_DISK);
83 ssValue >> valueRet;
84 bool fReadOk = !ssValue.fail();
85 if (!vchValue.empty())
86 memset(&vchValue[0], 0, vchValue.size());
87 if (!ssValue.empty())
88 memset(&ssValue[0], 0, ssValue.size());
89 if (!fReadOk)
90 strError = "SQLite wallet record could not be deserialized";
91 return fReadOk;
92 }
93
94 template<typename K>
95 bool EraseTypedRecord(const K& key, string& strError)
96 {
97 CDataStream ssKey(SER_DISK);
98 ssKey.reserve(1000);
99 ssKey << key;
100 vector<unsigned char> vchKey(ssKey.begin(), ssKey.end());
101
102 bool fOk = EraseRecord(vchKey, strError);
103 if (!vchKey.empty())
104 memset(&vchKey[0], 0, vchKey.size());
105 if (!ssKey.empty())
106 memset(&ssKey[0], 0, ssKey.size());
107 return fOk;
108 }
109
110private:
111 sqlite3* pdb;
112
113 bool Exec(const char* pszSql, string& strError);
114 bool InitSchema(string& strError);
115
116 CWalletDBSQLite(const CWalletDBSQLite&);
117 void operator=(const CWalletDBSQLite&);
118};
119
120#endif
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
index e8ac4bf..f59fc0c 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
@@ -400,7 +401,12 @@ int CmdRecoveryAudit()
400 int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit; 401 int64 nImmatureTotal = audit.nRecoverableImmatureCredit + audit.nLegacyImmatureCredit;
401 402
402 printf("Wallet recovery audit\n"); 403 printf("Wallet recovery audit\n");
403 printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); 404 if (audit.fHaveSeed)
405 printf(" recovery phrase: present\n");
406 else if (audit.fSeedEncryptedLocked)
407 printf(" recovery phrase: encrypted, unlock wallet to audit\n");
408 else
409 printf(" recovery phrase: not installed\n");
404 if (audit.fHaveSeed) 410 if (audit.fHaveSeed)
405 { 411 {
406 printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); 412 printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str());
@@ -412,15 +418,24 @@ int CmdRecoveryAudit()
412 if (!audit.fDeriveComplete) 418 if (!audit.fDeriveComplete)
413 printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); 419 printf(" derivation warning: %s\n", audit.strDeriveError.c_str());
414 printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str()); 420 printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str());
421 if (audit.fSeedEncryptedLocked)
422 {
423 printf(" recovery coverage: unavailable while wallet is locked\n");
424 printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str());
425 printf("\n");
426 printf("Unlock the wallet with /walletpassphrase or /walletpassphrase=@FILE to audit phrase coverage.\n");
427 fflush(stdout);
428 return 2;
429 }
415 printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n", 430 printf(" covered by recovery phrase: %s BTF (%d transaction(s))\n",
416 FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx); 431 FormatMoney(audit.nRecoverableCredit).c_str(), audit.nRecoverableTx);
417 printf(" wallet.dat-only balance: %s BTF (%d transaction(s))\n", 432 printf(" not phrase-backed balance: %s BTF (%d transaction(s))\n",
418 FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx); 433 FormatMoney(audit.nLegacyCredit).c_str(), audit.nLegacyTx);
419 printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str()); 434 printf(" immature mining rewards: %s BTF\n", FormatMoney(nImmatureTotal).c_str());
420 printf(" phrase-backed immature: %s BTF (%d transaction(s))\n", 435 printf(" phrase-backed immature: %s BTF (%d transaction(s))\n",
421 FormatMoney(audit.nRecoverableImmatureCredit).c_str(), 436 FormatMoney(audit.nRecoverableImmatureCredit).c_str(),
422 audit.nRecoverableImmatureTx); 437 audit.nRecoverableImmatureTx);
423 printf(" wallet.dat-only immature: %s BTF (%d transaction(s))\n", 438 printf(" not phrase-backed immature: %s BTF (%d transaction(s))\n",
424 FormatMoney(audit.nLegacyImmatureCredit).c_str(), 439 FormatMoney(audit.nLegacyImmatureCredit).c_str(),
425 audit.nLegacyImmatureTx); 440 audit.nLegacyImmatureTx);
426 441
@@ -442,7 +457,7 @@ int CmdRecoveryAudit()
442 { 457 {
443 printf("\n"); 458 printf("\n");
444 printf("Warning: some coins are on keys the phrase does not reproduce.\n"); 459 printf("Warning: some coins are on keys the phrase does not reproduce.\n");
445 printf("Keep wallet.dat backups until that balance has been moved to a phrase-backed address.\n"); 460 printf("Keep a file backup of this wallet until that balance has been moved to a phrase-backed address.\n");
446 fflush(stdout); 461 fflush(stdout);
447 return 2; 462 return 2;
448 } 463 }
@@ -456,10 +471,952 @@ int CmdRecoveryAudit()
456 return 0; 471 return 0;
457} 472}
458 473
474struct WalletStorageAuditCounts
475{
476 unsigned int nTotal = 0;
477 unsigned int nMalformed = 0;
478 unsigned int nUnknown = 0;
479 unsigned int nVersion = 0;
480 unsigned int nNames = 0;
481 unsigned int nTransactions = 0;
482 unsigned int nPlainKeys = 0;
483 unsigned int nEncryptedKeys = 0;
484 unsigned int nMasterKeys = 0;
485 unsigned int nDefaultKey = 0;
486 unsigned int nPlainHDMaster = 0;
487 unsigned int nPlainHDChainCode = 0;
488 unsigned int nCryptedHDMaster = 0;
489 unsigned int nCryptedHDChainCode = 0;
490 unsigned int nWalletMinVersion = 0;
491 unsigned int nPool = 0;
492 unsigned int nSettings = 0;
493 unsigned int nHDNext = 0;
494 unsigned int nHDSchema = 0;
495 unsigned int nHDCoinType = 0;
496 unsigned int nHDReceiveNext = 0;
497 unsigned int nHDChangeNext = 0;
498};
499
500static std::string HDSeedStorageState(unsigned int nMaster, unsigned int nChain)
501{
502 if (nMaster == 0 && nChain == 0)
503 return "none";
504 if (nMaster > 0 && nChain > 0)
505 return "complete";
506 return "incomplete";
507}
508
509static void CountWalletStorageType(const std::string& strType,
510 WalletStorageAuditCounts& c)
511{
512 if (strType == "version")
513 c.nVersion++;
514 else if (strType == "name")
515 c.nNames++;
516 else if (strType == "tx")
517 c.nTransactions++;
518 else if (strType == "key")
519 c.nPlainKeys++;
520 else if (strType == "ckey")
521 c.nEncryptedKeys++;
522 else if (strType == "mkey")
523 c.nMasterKeys++;
524 else if (strType == "defaultkey")
525 c.nDefaultKey++;
526 else if (strType == "hdmaster")
527 c.nPlainHDMaster++;
528 else if (strType == "hdchaincode")
529 c.nPlainHDChainCode++;
530 else if (strType == "cryptedhdmaster")
531 c.nCryptedHDMaster++;
532 else if (strType == "cryptedhdchaincode")
533 c.nCryptedHDChainCode++;
534 else if (strType == "walletminversion")
535 c.nWalletMinVersion++;
536 else if (strType == "pool")
537 c.nPool++;
538 else if (strType == "setting")
539 c.nSettings++;
540 else if (strType == "hdnext")
541 c.nHDNext++;
542 else if (strType == "hdschema")
543 c.nHDSchema++;
544 else if (strType == "hdcointype")
545 c.nHDCoinType++;
546 else if (strType == "hdreceivenext")
547 c.nHDReceiveNext++;
548 else if (strType == "hdchangenext")
549 c.nHDChangeNext++;
550 else
551 c.nUnknown++;
552}
553
554static bool ReadWalletStorageCounts(WalletStorageAuditCounts& counts,
555 std::string& strError)
556{
557 class CWalletStorageCountVisitor : public CWalletRecordVisitor
558 {
559 public:
560 WalletStorageAuditCounts& counts;
561
562 explicit CWalletStorageCountVisitor(WalletStorageAuditCounts& countsIn)
563 : counts(countsIn) { }
564
565 bool VisitWalletRecord(const CDataStream& ssKeyIn,
566 const CDataStream& ssValue,
567 std::string& strErrorRet)
568 {
569 (void)ssValue;
570 (void)strErrorRet;
571 counts.nTotal++;
572 try
573 {
574 CDataStream ssKey = ssKeyIn;
575 std::string strType;
576 ssKey >> strType;
577 if (ssKey.fail())
578 {
579 counts.nMalformed++;
580 return true;
581 }
582 CountWalletStorageType(strType, counts);
583 }
584 catch (...)
585 {
586 counts.nMalformed++;
587 }
588 return true;
589 }
590 };
591
592 CWalletStorageCountVisitor visitor(counts);
593 return ScanWalletRecords(visitor, strError);
594}
595
596static std::string WalletStorageAuditJson(const WalletStorageAuditCounts& counts)
597{
598 std::string strJson;
599 strJson += "{\n";
600 strJson += " \"format\": \"bitflash-wallet-storage-audit-v1\",\n";
601 strJson += strprintf(" \"records_total\": %u,\n", counts.nTotal);
602 strJson += strprintf(" \"malformed_records\": %u,\n", counts.nMalformed);
603 strJson += strprintf(" \"unknown_records\": %u,\n", counts.nUnknown);
604 strJson += strprintf(" \"plain_private_keys\": %u,\n", counts.nPlainKeys);
605 strJson += strprintf(" \"encrypted_private_keys\": %u,\n", counts.nEncryptedKeys);
606 strJson += strprintf(" \"encryption_master_keys\": %u,\n", counts.nMasterKeys);
607 strJson += strprintf(" \"default_public_key\": \"%s\",\n",
608 counts.nDefaultKey ? "present" : "none");
609 strJson += strprintf(" \"plain_hd_seed\": \"%s\",\n",
610 HDSeedStorageState(counts.nPlainHDMaster,
611 counts.nPlainHDChainCode).c_str());
612 strJson += strprintf(" \"encrypted_hd_seed\": \"%s\",\n",
613 HDSeedStorageState(counts.nCryptedHDMaster,
614 counts.nCryptedHDChainCode).c_str());
615 strJson += strprintf(" \"wallet_minimum_version\": \"%s\",\n",
616 counts.nWalletMinVersion ? "present" : "none");
617 strJson += strprintf(" \"keypool_entries\": %u,\n", counts.nPool);
618 strJson += strprintf(" \"wallet_transactions\": %u,\n", counts.nTransactions);
619 strJson += strprintf(" \"address_book_labels\": %u,\n", counts.nNames);
620 strJson += strprintf(" \"settings\": %u,\n", counts.nSettings);
621 strJson += strprintf(" \"hd_metadata_records\": %u,\n",
622 counts.nHDNext + counts.nHDSchema + counts.nHDCoinType +
623 counts.nHDReceiveNext + counts.nHDChangeNext);
624 strJson += strprintf(" \"database_version_records\": %u\n", counts.nVersion);
625 strJson += "}\n";
626 return strJson;
627}
628
629static bool WriteAuditTextFile(const std::string& strPath,
630 const std::string& strText,
631 std::string& strError)
632{
633 FILE* pf = fopen(strPath.c_str(), "wb");
634 if (!pf)
635 {
636 strError = "cannot open output file";
637 return false;
638 }
639 size_t nWritten = fwrite(strText.data(), 1, strText.size(), pf);
640 bool fCloseOk = fclose(pf) == 0;
641 if (nWritten != strText.size() || !fCloseOk)
642 {
643 strError = "could not write the complete output file";
644 return false;
645 }
646 return true;
647}
648
649static void AddWalletStorageFailure(std::vector<std::string>& vFailures,
650 const std::string& strFailure)
651{
652 vFailures.push_back(strFailure);
653}
654
655static void BuildWalletStorageSanityFailures(const WalletStorageAuditCounts& counts,
656 std::vector<std::string>& vFailures)
657{
658 bool fPlainHD = counts.nPlainHDMaster > 0 || counts.nPlainHDChainCode > 0;
659 bool fCryptedHD = counts.nCryptedHDMaster > 0 || counts.nCryptedHDChainCode > 0;
660 bool fEncryptedRecords = counts.nEncryptedKeys > 0 ||
661 counts.nMasterKeys > 0 ||
662 fCryptedHD;
663
664 if (counts.nMalformed > 0)
665 AddWalletStorageFailure(vFailures, "wallet.dat contains malformed records");
666 if (counts.nUnknown > 0)
667 AddWalletStorageFailure(vFailures, "wallet.dat contains unknown records");
668 if ((counts.nPlainHDMaster > 0) != (counts.nPlainHDChainCode > 0))
669 AddWalletStorageFailure(vFailures, "plain HD seed is incomplete");
670 if ((counts.nCryptedHDMaster > 0) != (counts.nCryptedHDChainCode > 0))
671 AddWalletStorageFailure(vFailures, "encrypted HD seed is incomplete");
672 if (fPlainHD && fCryptedHD)
673 AddWalletStorageFailure(vFailures, "wallet.dat contains both plain and encrypted HD seed records");
674 if (counts.nDefaultKey > 1)
675 AddWalletStorageFailure(vFailures, "wallet.dat contains more than one default public key record");
676 if (counts.nWalletMinVersion > 1)
677 AddWalletStorageFailure(vFailures, "wallet.dat contains more than one minimum-version record");
678
679 if (fEncryptedRecords)
680 {
681 if (counts.nPlainKeys > 0)
682 AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain private key records");
683 if (fPlainHD)
684 AddWalletStorageFailure(vFailures, "encrypted wallet still contains plain HD seed records");
685 if (counts.nMasterKeys == 0)
686 AddWalletStorageFailure(vFailures, "encrypted wallet has no encryption master key record");
687 if (counts.nWalletMinVersion == 0)
688 AddWalletStorageFailure(vFailures, "encrypted wallet has no minimum-version record");
689 }
690}
691
692int CmdWalletStorageCheck()
693{
694 AttachTerminal();
695
696 WalletStorageAuditCounts counts;
697 std::string strError;
698 if (!ReadWalletStorageCounts(counts, strError))
699 {
700 fprintf(stderr, "Cannot check wallet storage: %s\n", strError.c_str());
701 return 1;
702 }
703
704 std::vector<std::string> vFailures;
705 BuildWalletStorageSanityFailures(counts, vFailures);
706
707 printf("Wallet storage sanity check\n");
708 printf(" records total: %u\n", counts.nTotal);
709 printf(" encrypted records: %s\n",
710 (counts.nEncryptedKeys || counts.nMasterKeys ||
711 counts.nCryptedHDMaster || counts.nCryptedHDChainCode) ? "yes" : "no");
712 printf(" plain private keys: %u\n", counts.nPlainKeys);
713 printf(" encrypted private keys: %u\n", counts.nEncryptedKeys);
714 printf(" plain HD seed: %s\n",
715 HDSeedStorageState(counts.nPlainHDMaster,
716 counts.nPlainHDChainCode).c_str());
717 printf(" encrypted HD seed: %s\n",
718 HDSeedStorageState(counts.nCryptedHDMaster,
719 counts.nCryptedHDChainCode).c_str());
720 if (vFailures.empty())
721 {
722 printf(" storage sanity: ok\n");
723 fflush(stdout);
724 return 0;
725 }
726
727 printf(" storage sanity: failed\n");
728 for (std::vector<std::string>::const_iterator it = vFailures.begin();
729 it != vFailures.end(); ++it)
730 printf(" failure: %s\n", it->c_str());
731 fflush(stdout);
732 return 2;
733}
734
735int CmdWalletStorageAudit(const std::string& strJsonOut)
736{
737 AttachTerminal();
738
739 WalletStorageAuditCounts counts;
740 std::string strError;
741 if (!ReadWalletStorageCounts(counts, strError))
742 {
743 fprintf(stderr, "Cannot audit wallet storage: %s\n", strError.c_str());
744 return 1;
745 }
746
747 if (!strJsonOut.empty())
748 {
749 std::string strWriteError;
750 if (!WriteAuditTextFile(strJsonOut,
751 WalletStorageAuditJson(counts),
752 strWriteError))
753 {
754 fprintf(stderr, "Cannot write wallet storage audit JSON: %s\n",
755 strWriteError.c_str());
756 return 1;
757 }
758 printf("Wallet storage audit written to %s\n", strJsonOut.c_str());
759 fflush(stdout);
760 return counts.nMalformed > 0 ? 2 : 0;
761 }
762
763 printf("Wallet storage audit\n");
764 printf(" records total: %u\n", counts.nTotal);
765 printf(" malformed records: %u\n", counts.nMalformed);
766 printf(" unknown records: %u\n", counts.nUnknown);
767 printf(" plain private keys: %u\n", counts.nPlainKeys);
768 printf(" encrypted private keys: %u\n", counts.nEncryptedKeys);
769 printf(" encryption master keys: %u\n", counts.nMasterKeys);
770 printf(" default public key: %s\n", counts.nDefaultKey ? "present" : "none");
771 printf(" plain HD seed: %s\n",
772 HDSeedStorageState(counts.nPlainHDMaster,
773 counts.nPlainHDChainCode).c_str());
774 printf(" encrypted HD seed: %s\n",
775 HDSeedStorageState(counts.nCryptedHDMaster,
776 counts.nCryptedHDChainCode).c_str());
777 printf(" wallet minimum version: %s\n",
778 counts.nWalletMinVersion ? "present" : "none");
779 printf(" keypool entries: %u\n", counts.nPool);
780 printf(" wallet transactions: %u\n", counts.nTransactions);
781 printf(" address book labels: %u\n", counts.nNames);
782 printf(" settings: %u\n", counts.nSettings);
783 printf(" HD metadata records: %u\n",
784 counts.nHDNext + counts.nHDSchema + counts.nHDCoinType +
785 counts.nHDReceiveNext + counts.nHDChangeNext);
786 printf(" database version records: %u\n", counts.nVersion);
787 fflush(stdout);
788
789 if (counts.nMalformed > 0)
790 return 2;
791 return 0;
792}
793
794static vector<unsigned char> DataStreamBytesLocal(const CDataStream& ss)
795{
796 return vector<unsigned char>(ss.begin(), ss.end());
797}
798
799namespace {
800
801static void RemoveSQLiteExportFiles(const string& strPath)
802{
803 remove(strPath.c_str());
804 remove((strPath + "-wal").c_str());
805 remove((strPath + "-shm").c_str());
806}
807
808static void RemoveFailedRestoreWallet(const string& strWalletPath)
809{
810 DBFlush(true);
811 remove(strWalletPath.c_str());
812}
813
814class CWalletSQLiteExportVisitor : public CWalletRecordVisitor
815{
816public:
817 CWalletDBSQLite& db;
818 int& nCopied;
819
820 CWalletSQLiteExportVisitor(CWalletDBSQLite& dbIn, int& nCopiedIn)
821 : db(dbIn), nCopied(nCopiedIn) { }
822
823 bool VisitWalletRecord(const CDataStream& ssKey,
824 const CDataStream& ssValue,
825 string& strErrorRet)
826 {
827 if (!db.WriteRecord(DataStreamBytesLocal(ssKey),
828 DataStreamBytesLocal(ssValue),
829 strErrorRet))
830 return false;
831 nCopied++;
832 return true;
833 }
834};
835
836class CWalletRecordMapVisitor : public CWalletRecordVisitor
837{
838public:
839 std::map<vector<unsigned char>, vector<unsigned char> >& mapRecords;
840
841 explicit CWalletRecordMapVisitor(
842 std::map<vector<unsigned char>, vector<unsigned char> >& mapRecordsIn)
843 : mapRecords(mapRecordsIn) { }
844
845 bool VisitWalletRecord(const CDataStream& ssKey,
846 const CDataStream& ssValue,
847 string& strErrorRet)
848 {
849 vector<unsigned char> vchKey = DataStreamBytesLocal(ssKey);
850 if (mapRecords.count(vchKey))
851 {
852 strErrorRet = "duplicate serialized wallet key";
853 return false;
854 }
855 mapRecords[vchKey] = DataStreamBytesLocal(ssValue);
856 return true;
857 }
858};
859
860class CWalletRawRestoreDB : public CWalletDB
861{
862public:
863 CWalletRawRestoreDB(const char* pszMode="cr+", bool fTxn=true)
864 : CWalletDB(pszMode, fTxn) { }
865
866 bool WriteRaw(CDataStream ssKey, CDataStream ssValue)
867 {
868 if (!pdb || ssKey.empty())
869 return false;
870
871 Dbt datKey((void*)&ssKey[0], ssKey.size());
872 Dbt datValue(ssValue.empty() ? NULL : (void*)&ssValue[0],
873 ssValue.size());
874 int ret = pdb->put(GetTxn(), &datKey, &datValue, 0);
875
876 memset(datKey.get_data(), 0, datKey.get_size());
877 if (datValue.get_data())
878 memset(datValue.get_data(), 0, datValue.get_size());
879 return ret == 0;
880 }
881};
882
883class CWalletSQLiteRestoreVisitor : public CWalletRecordVisitor
884{
885public:
886 CWalletRawRestoreDB& db;
887 int& nCopied;
888
889 CWalletSQLiteRestoreVisitor(CWalletRawRestoreDB& dbIn, int& nCopiedIn)
890 : db(dbIn), nCopied(nCopiedIn) { }
891
892 bool VisitWalletRecord(const CDataStream& ssKey,
893 const CDataStream& ssValue,
894 string& strErrorRet)
895 {
896 if (!db.WriteRaw(ssKey, ssValue))
897 {
898 strErrorRet = "could not write raw wallet record";
899 return false;
900 }
901 nCopied++;
902 return true;
903 }
904};
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
1071} // namespace
1072
1073// Export the currently-loaded wallet to a SQLite file. Shared by the
1074// -walletsqliteexport command and the GUI conversion wizard: opens the
1075// destination, copies every record inside one transaction, checkpoints, and
1076// verifies the stored count matches. Returns false with strErrorRet set on any
1077// failure and leaves no partial file behind; the source wallet is only read.
1078bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet,
1079 std::string& strErrorRet)
1080{
1081 nCopiedRet = 0;
1082 if (strDest.empty())
1083 {
1084 strErrorRet = "no SQLite export path was given";
1085 return false;
1086 }
1087 if (FileExists(strDest.c_str()))
1088 {
1089 strErrorRet = strprintf("refusing to overwrite existing file: %s", strDest.c_str());
1090 return false;
1091 }
1092
1093 CWalletDBSQLite db;
1094 if (!db.Open(strDest, strErrorRet))
1095 {
1096 RemoveSQLiteExportFiles(strDest);
1097 return false;
1098 }
1099 if (!db.BeginTransaction(strErrorRet))
1100 {
1101 db.Close();
1102 RemoveSQLiteExportFiles(strDest);
1103 return false;
1104 }
1105
1106 CWalletSQLiteExportVisitor visitor(db, nCopiedRet);
1107 if (!ScanWalletRecords(visitor, strErrorRet))
1108 {
1109 string strRollbackError;
1110 db.RollbackTransaction(strRollbackError);
1111 db.Close();
1112 RemoveSQLiteExportFiles(strDest);
1113 return false;
1114 }
1115 if (!db.CommitTransaction(strErrorRet) || !db.Checkpoint(strErrorRet))
1116 {
1117 db.Close();
1118 RemoveSQLiteExportFiles(strDest);
1119 return false;
1120 }
1121
1122 int nCount = 0;
1123 if (!db.CountRecords(nCount, strErrorRet))
1124 {
1125 db.Close();
1126 RemoveSQLiteExportFiles(strDest);
1127 return false;
1128 }
1129 if (nCount != nCopiedRet)
1130 {
1131 strErrorRet = strprintf("export count mismatch: copied %d, stored %d",
1132 nCopiedRet, nCount);
1133 db.Close();
1134 RemoveSQLiteExportFiles(strDest);
1135 return false;
1136 }
1137 db.Close();
1138 return true;
1139}
1140
1141// Convert the running Berkeley DB wallet to SQLite for the GUI wizard: export
1142// wallet.dat to <datadir>/wallet.sqlite, then record the backend marker so the
1143// next start opens SQLite. wallet.dat is never modified -- it stays as the
1144// fallback. Refuses if already on SQLite or if a wallet.sqlite already exists.
1145bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet)
1146{
1147 nCopiedRet = 0;
1148 if (WalletSQLiteRuntimeActive())
1149 {
1150 strErrorRet = "this wallet is already on the SQLite backend";
1151 return false;
1152 }
1153 string strDest = GetAppDir() + "/wallet.sqlite";
1154 if (FileExists(strDest.c_str()))
1155 {
1156 strErrorRet = strprintf("a SQLite wallet already exists at %s", strDest.c_str());
1157 return false;
1158 }
1159 if (!ExportActiveWalletToSQLite(strDest, nCopiedRet, strErrorRet))
1160 return false;
1161 if (!WriteWalletBackendMarker("sqlite"))
1162 {
1163 strErrorRet = "the wallet was exported but the backend choice could not be saved";
1164 return false;
1165 }
1166 return true;
1167}
1168
1169int CmdWalletSQLiteExport(const std::string& strDest)
1170{
1171 AttachTerminal();
1172
1173 // Keep the command's own specific messages for the two conditions the
1174 // shared helper folds into a generic error, so scripts and the tests that
1175 // read this output see the same wording as before.
1176 if (strDest.empty())
1177 {
1178 fprintf(stderr, "Missing SQLite wallet export path.\n");
1179 return 1;
1180 }
1181 if (FileExists(strDest.c_str()))
1182 {
1183 fprintf(stderr, "Refusing to overwrite existing SQLite wallet export: %s\n",
1184 strDest.c_str());
1185 return 1;
1186 }
1187
1188 int nCopied = 0;
1189 string strError;
1190 if (!ExportActiveWalletToSQLite(strDest, nCopied, strError))
1191 {
1192 fprintf(stderr, "Cannot export wallet to SQLite: %s\n", strError.c_str());
1193 return 1;
1194 }
1195
1196 printf("SQLite wallet export written to %s\n", strDest.c_str());
1197 printf(" records copied: %d\n", nCopied);
1198 fflush(stdout);
1199 return 0;
1200}
1201
1202int CmdWalletSQLiteVerify(const std::string& strPath)
1203{
1204 AttachTerminal();
1205
1206 if (strPath.empty())
1207 {
1208 fprintf(stderr, "Missing SQLite wallet export path.\n");
1209 return 1;
1210 }
1211 if (!FileExists(strPath.c_str()))
1212 {
1213 fprintf(stderr, "SQLite wallet export does not exist: %s\n",
1214 strPath.c_str());
1215 return 1;
1216 }
1217
1218 std::map<vector<unsigned char>, vector<unsigned char> > mapBDB;
1219 std::map<vector<unsigned char>, vector<unsigned char> > mapSQLite;
1220 string strError;
1221
1222 CWalletRecordMapVisitor bdbVisitor(mapBDB);
1223 if (!ScanWalletRecords(bdbVisitor, strError))
1224 {
1225 fprintf(stderr, "Cannot scan wallet.dat records: %s\n", strError.c_str());
1226 return 1;
1227 }
1228
1229 CWalletDBSQLite db;
1230 if (!db.Open(strPath, strError))
1231 {
1232 fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str());
1233 return 1;
1234 }
1235 CWalletRecordMapVisitor sqliteVisitor(mapSQLite);
1236 if (!db.ScanRecords(sqliteVisitor, strError))
1237 {
1238 fprintf(stderr, "Cannot scan SQLite wallet export: %s\n", strError.c_str());
1239 return 1;
1240 }
1241
1242 int nMissing = 0;
1243 int nMismatched = 0;
1244 for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it =
1245 mapBDB.begin(); it != mapBDB.end(); ++it)
1246 {
1247 std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator sit =
1248 mapSQLite.find(it->first);
1249 if (sit == mapSQLite.end())
1250 nMissing++;
1251 else if (sit->second != it->second)
1252 nMismatched++;
1253 }
1254
1255 int nExtra = 0;
1256 for (std::map<vector<unsigned char>, vector<unsigned char> >::const_iterator it =
1257 mapSQLite.begin(); it != mapSQLite.end(); ++it)
1258 {
1259 if (!mapBDB.count(it->first))
1260 nExtra++;
1261 }
1262
1263 printf("SQLite wallet export verification\n");
1264 printf(" wallet.dat records: %u\n", (unsigned int)mapBDB.size());
1265 printf(" SQLite records: %u\n", (unsigned int)mapSQLite.size());
1266 printf(" missing records: %d\n", nMissing);
1267 printf(" extra records: %d\n", nExtra);
1268 printf(" record value mismatches: %d\n", nMismatched);
1269 if (nMissing == 0 && nExtra == 0 && nMismatched == 0)
1270 {
1271 printf(" verification: ok\n");
1272 fflush(stdout);
1273 return 0;
1274 }
1275
1276 printf(" verification: failed\n");
1277 fflush(stdout);
1278 return 2;
1279}
1280
1281int CmdWalletSQLiteRestore(const std::string& strPath)
1282{
1283 AttachTerminal();
1284
1285 if (strPath.empty())
1286 {
1287 fprintf(stderr, "Missing SQLite wallet export path.\n");
1288 return 1;
1289 }
1290 if (!FileExists(strPath.c_str()))
1291 {
1292 fprintf(stderr, "SQLite wallet export does not exist: %s\n",
1293 strPath.c_str());
1294 return 1;
1295 }
1296
1297 string strWalletPath = GetAppDir() + "/wallet.dat";
1298 if (FileExists(strWalletPath.c_str()))
1299 {
1300 fprintf(stderr, "Refusing to overwrite existing wallet.dat: %s\n",
1301 strWalletPath.c_str());
1302 return 1;
1303 }
1304
1305 string strError;
1306 CWalletDBSQLite sqlite;
1307 if (!sqlite.Open(strPath, strError))
1308 {
1309 fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str());
1310 return 1;
1311 }
1312
1313 int nSQLiteRecords = 0;
1314 if (!sqlite.CountRecords(nSQLiteRecords, strError))
1315 {
1316 fprintf(stderr, "Cannot count SQLite wallet export records: %s\n",
1317 strError.c_str());
1318 return 1;
1319 }
1320 if (nSQLiteRecords <= 0)
1321 {
1322 fprintf(stderr, "SQLite wallet export contains no records.\n");
1323 return 2;
1324 }
1325
1326 CWalletRawRestoreDB bdb("cr+", true);
1327 if (!bdb.TxnBegin())
1328 {
1329 fprintf(stderr, "Cannot begin Berkeley DB restore transaction.\n");
1330 return 1;
1331 }
1332
1333 int nCopied = 0;
1334 CWalletSQLiteRestoreVisitor visitor(bdb, nCopied);
1335 if (!sqlite.ScanRecords(visitor, strError))
1336 {
1337 bdb.TxnAbort();
1338 bdb.Close();
1339 RemoveFailedRestoreWallet(strWalletPath);
1340 fprintf(stderr, "Cannot restore SQLite wallet export: %s\n",
1341 strError.c_str());
1342 return 1;
1343 }
1344 if (nCopied != nSQLiteRecords)
1345 {
1346 bdb.TxnAbort();
1347 bdb.Close();
1348 RemoveFailedRestoreWallet(strWalletPath);
1349 fprintf(stderr, "SQLite wallet restore copied %d records, expected %d.\n",
1350 nCopied, nSQLiteRecords);
1351 return 2;
1352 }
1353 if (!bdb.TxnCommit())
1354 {
1355 bdb.Close();
1356 RemoveFailedRestoreWallet(strWalletPath);
1357 fprintf(stderr, "Cannot commit Berkeley DB restore transaction.\n");
1358 return 1;
1359 }
1360
1361 printf("SQLite wallet export restored to wallet.dat\n");
1362 printf(" records restored: %d\n", nCopied);
1363 fflush(stdout);
1364 return 0;
1365}
1366
1367int CmdWalletSQLiteLoadCheck(const std::string& strPath)
1368{
1369 AttachTerminal();
1370
1371 if (strPath.empty())
1372 {
1373 fprintf(stderr, "Missing SQLite wallet export path.\n");
1374 return 1;
1375 }
1376 if (!FileExists(strPath.c_str()))
1377 {
1378 fprintf(stderr, "SQLite wallet export does not exist: %s\n",
1379 strPath.c_str());
1380 return 1;
1381 }
1382
1383 string strError;
1384 CWalletDBSQLite sqlite;
1385 if (!sqlite.Open(strPath, strError))
1386 {
1387 fprintf(stderr, "Cannot open SQLite wallet export: %s\n", strError.c_str());
1388 return 1;
1389 }
1390
1391 CWalletSQLiteLoadCheckVisitor visitor;
1392 if (!sqlite.ScanRecords(visitor, strError))
1393 {
1394 printf("SQLite wallet load check\n");
1395 printf(" records checked: %d\n", visitor.nRecords);
1396 printf(" unknown records: %d\n", visitor.nUnknown);
1397 printf(" load check: failed\n");
1398 printf(" failure: %s\n", strError.c_str());
1399 fflush(stdout);
1400 return 2;
1401 }
1402
1403 printf("SQLite wallet load check\n");
1404 printf(" records checked: %d\n", visitor.nRecords);
1405 printf(" unknown records: %d\n", visitor.nUnknown);
1406 printf(" load check: ok\n");
1407 fflush(stdout);
1408 return 0;
1409}
1410
459int CmdEncryptWallet(const std::string& strPassphrase) 1411int CmdEncryptWallet(const std::string& strPassphrase)
460{ 1412{
461 AttachTerminal(); 1413 AttachTerminal();
462 1414
1415 // Capture the backend now: under SQLite, EncryptWallet() closes the runtime
1416 // store to swap the file, so WalletSQLiteRuntimeActive() reads false by the
1417 // time we would decide whether to purge Berkeley DB logs below.
1418 bool fSQLiteBackend = WalletSQLiteRuntimeActive();
1419
463 string strBackup; 1420 string strBackup;
464 string strError; 1421 string strError;
465 if (!EncryptWallet(strPassphrase, strBackup, strError)) 1422 if (!EncryptWallet(strPassphrase, strBackup, strError))
@@ -470,6 +1427,8 @@ int CmdEncryptWallet(const std::string& strPassphrase)
470 } 1427 }
471 1428
472 printf("Wallet encrypted.\n"); 1429 printf("Wallet encrypted.\n");
1430 if (fSQLiteBackend)
1431 printf("Wallet backend: SQLite (wallet.sqlite).\n");
473 printf("The old unencrypted wallet was moved to:\n"); 1432 printf("The old unencrypted wallet was moved to:\n");
474 printf(" %s\n", strBackup.c_str()); 1433 printf(" %s\n", strBackup.c_str());
475 printf("That backup still contains private keys in plain text. Move it offline or\n"); 1434 printf("That backup still contains private keys in plain text. Move it offline or\n");
@@ -482,7 +1441,13 @@ int CmdEncryptWallet(const std::string& strPassphrase)
482 // them. Closing the environment and dropping the logs is the last step of 1441 // them. Closing the environment and dropping the logs is the last step of
483 // encrypting, not an optimisation -- our own backup advice is to copy the 1442 // encrypting, not an optimisation -- our own backup advice is to copy the
484 // whole directory, and following it otherwise carries the keys along. 1443 // whole directory, and following it otherwise carries the keys along.
485 PurgeDbEnvironmentLogs(); 1444 //
1445 // The SQLite backend has no Berkeley DB environment behind the wallet, and
1446 // EncryptWallet() already closed and swapped the SQLite store; there is
1447 // nothing to purge, and calling into the environment here would only open
1448 // one needlessly.
1449 if (!fSQLiteBackend)
1450 PurgeDbEnvironmentLogs();
486 return 0; 1451 return 0;
487} 1452}
488 1453
diff --git a/src/walletcmd.h b/src/walletcmd.h
index 007b8b5..7013d44 100644
--- a/src/walletcmd.h
+++ b/src/walletcmd.h
@@ -62,6 +62,40 @@ int CmdShowDerived(int nCount);
62// by the installed recovery phrase or still depends on wallet.dat-only keys. 62// by the installed recovery phrase or still depends on wallet.dat-only keys.
63int CmdRecoveryAudit(); 63int CmdRecoveryAudit();
64 64
65// Diagnostic: count wallet.dat record types without printing keys,
66// addresses, transaction ids, labels, or other wallet values.
67int CmdWalletStorageAudit(const std::string& strJsonOut = "");
68
69// Diagnostic: fail closed when wallet.dat contains an internally inconsistent
70// or migration-unsafe mix of storage records.
71int CmdWalletStorageCheck();
72
73// Copy every raw wallet.dat record into a SQLite key/value store, preserving
74// serialized bytes. A migration/scripting tool; it does not change which backend
75// the runtime wallet opens.
76int CmdWalletSQLiteExport(const std::string& strDest);
77
78// Export the currently-loaded wallet to a SQLite file (shared by the command
79// above and the GUI wizard). Returns false with an error on any failure.
80bool ExportActiveWalletToSQLite(const std::string& strDest, int& nCopiedRet,
81 std::string& strErrorRet);
82
83// Convert the running Berkeley DB wallet to SQLite and record the backend
84// marker so the next start opens it. wallet.dat is left untouched as fallback.
85bool DoConvertWalletToSQLite(int& nCopiedRet, std::string& strErrorRet);
86
87// Diagnostic/migration staging: compare a SQLite wallet export with the current
88// wallet.dat raw records. Prints counts only, never record keys or values.
89int CmdWalletSQLiteVerify(const std::string& strPath);
90
91// Diagnostic/migration staging: rebuild wallet.dat from a SQLite export in an
92// empty data directory. Refuses to overwrite an existing wallet.dat.
93int CmdWalletSQLiteRestore(const std::string& strPath);
94
95// Diagnostic/migration staging: parse a SQLite wallet export as the runtime
96// wallet loader would. Prints counts only, never record keys or values.
97int CmdWalletSQLiteLoadCheck(const std::string& strPath);
98
65// Rewrite wallet.dat so private keys and the HD seed are encrypted with the 99// Rewrite wallet.dat so private keys and the HD seed are encrypted with the
66// given passphrase. Returns 0 on success and exits without starting the node. 100// given passphrase. Returns 0 on success and exits without starting the node.
67int CmdEncryptWallet(const std::string& strPassphrase); 101int CmdEncryptWallet(const std::string& strPassphrase);