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-01 17:14:12 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-01 17:14:12 +0000
commitb4f0bc693ef7175702f82bcb2110b627922e8cbd (patch)
tree73435feb1a076dfba94d8143d7d7b7784908c82f /src
parenta1d867ecbcfc9e218408d0a11863a450243a01f9 (diff)
parent6aa3dade77632f13189d2a3ef29897f29a5f9572 (diff)
Merge upstream v1.2.20 into the fork
Merges upstream tag v1.2.20 (6aa3dade) into our 1.2.19+sec tree. The merge base is bc3de30's descendant 9e39a50 -- upstream's own v1.2.19 -- so this is an ordinary three-way merge, not a replay. Five conflict blocks in four files, all resolved below. No consensus change. CheckBlock, ConnectBlock, DisconnectBlock, ConnectInputs, AcceptBlock, AcceptTransaction, GetNextWorkRequired and GetBlockValue hash identical to v1.2.19; script.cpp and script.h are byte-identical on both sides. main.cpp is +3 lines and main.h +2, all wallet-backend plumbing. rpc.cpp is untouched, so the payout ledger arrives unchanged. Makefile, VERSION. OURS, renumbered. 1.2.20+sec, for the same reason 1.2.19 carried +sec: this tree is not the upstream release of that number and the string names every release artifact. Makefile, AppImage recipe. OURS for the AppDir path, THEIRS for the bundled library. Upstream's hunk reverts the icon paths to a hardcoded /tmp/bitflash.AppDir, which is the local-user pre-placement hazard the fork moved out of /tmp in the first place; taking their side wholesale would have reintroduced it silently, inside a conflict about something else. The AppDir stays $(APPDIR), and libsqlite3.so.0 joins the bundled list, which the new backend genuinely needs. src/Makefile, src/makefile.mingw, HEADERS. BOTH. Ours drops market.h, theirs adds wallet_sqlite.h; the resolution does both. NODE_OBJS auto-merged correctly in both files -- obj/wallet_sqlite.o present, obj/market.o still gone -- and was verified rather than assumed, because that line has trapped this fork before. src/selftest.cpp, known-tests list. BOTH, union. Upstream adds eight wallet-storage tests, we keep payout-ledger. The dispatch chain itself auto-merged and still routes payout-ledger to RunPayoutLedgerSelfTest. Hardening verified intact after merge: the TLS 1.2 floor in nostr.cpp, the /rvrelay pin, opt-in managed Tor, strict sig3-on-onion descriptor verification, and the marketplace removal. Upstream shipped no build logs or __pycache__ this time; the three added files are docs/storage.md and src/wallet_sqlite.{cpp,h}. The SQLite backend this release introduces is NOT enabled here. An existing wallet.dat resolves to Berkeley DB on every branch of the new selection logic, and the conversion wizard is GUI-only and not compiled into the headless node. The review findings that gate ever turning it on -- the non-atomic backend marker, the connection-wide WriteHDMaster transaction, and the encrypt-swap crash window -- are recorded on the tracker, not fixed here.
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.cpp306
-rw-r--r--src/makefile.mingw12
-rw-r--r--src/net.cpp29
-rw-r--r--src/net.h17
-rw-r--r--src/selftest.cpp1910
-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, 5298 insertions, 174 deletions
diff --git a/src/Makefile b/src/Makefile
index b068376..fe48041 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -40,7 +40,7 @@ FUZZ_LDFLAGS = -fsanitize=fuzzer,address,undefined
40 40
41LIBS = -L$(SECP_LIB) -lsecp256k1 \ 41LIBS = -L$(SECP_LIB) -lsecp256k1 \
42 -L$(RANDOMX_DIR)/build -lrandomx \ 42 -L$(RANDOMX_DIR)/build -lrandomx \
43 -lsodium -lssl -lcrypto -ldb_cxx -lpthread \ 43 -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread \
44 -lglfw -lGL 44 -lglfw -lGL
45 45
46# Every header a .cpp here includes has to be listed, not just the interesting 46# Every header a .cpp here includes has to be listed, not just the interesting
@@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \
53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
54 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 54 key.h bignum.h script.h db.h base58.h net.h nostr.h \
55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \ 55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h \
56 selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h 56 selftest.h walletcmd.h wallet_sqlite.h compat.h sockcount.h proxy.h tor.h font_roboto.h
57 57
58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o
@@ -61,7 +61,8 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
62 obj/sha.o obj/nostr.o obj/randomx_pow.o \ 62 obj/sha.o obj/nostr.o obj/randomx_pow.o \
63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/wallet_sqlite.o \
65 obj/proxy.o obj/tor.o
65 66
66GUI_OBJS = obj/gui.o obj/main_gui.o 67GUI_OBJS = obj/gui.o obj/main_gui.o
67 68
@@ -116,7 +117,7 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS)
116 117
117NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ 118NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \
118 -L$(RANDOMX_DIR)/build -lrandomx \ 119 -L$(RANDOMX_DIR)/build -lrandomx \
119 -lsodium -lssl -lcrypto -ldb_cxx -lpthread 120 -lsodium -lssl -lcrypto -ldb_cxx -lsqlite3 -lpthread
120 121
121NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ 122NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \
122 obj-node/main_gui.o obj-node/headless.o 123 obj-node/main_gui.o obj-node/headless.o
@@ -187,6 +188,8 @@ tests: test_bip32 bitflash-node
187 ./bitflash-node -selftest=wallet-keypool 188 ./bitflash-node -selftest=wallet-keypool
188 ./bitflash-node -selftest=wallet-hd 189 ./bitflash-node -selftest=wallet-hd
189 ./bitflash-node -selftest=wallet-format 190 ./bitflash-node -selftest=wallet-format
191 ./bitflash-node -selftest=wallet-sqlite
192 ./bitflash-node -selftest=wallet-sqlite-migration
190 ./bitflash-node -selftest=wallet-crypto 193 ./bitflash-node -selftest=wallet-crypto
191 ./bitflash-node -selftest=wallet-encrypt 194 ./bitflash-node -selftest=wallet-encrypt
192 ./bitflash-node -selftest=net-message 195 ./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 8dcb854..2a7f350 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>
@@ -172,7 +177,11 @@ static bool PathIsInsideDataDir(const std::string& strPath)
172 177
173static void SetDefaultBackupPath() 178static void SetDefaultBackupPath()
174{ 179{
175 std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + ".dat"; 180 // Match the extension to the live backend: BackupWallet copies wallet.sqlite
181 // under the SQLite backend and wallet.dat under Berkeley DB, so a ".dat"
182 // default name would put SQLite contents in a misleadingly-named file.
183 const char* pszExt = WalletSQLiteRuntimeActive() ? ".sqlite" : ".dat";
184 std::string path = DefaultBackupDir() + "/wallet-backup-" + BackupTimeSuffix() + pszExt;
176 strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1); 185 strncpy(g_backupPath, path.c_str(), sizeof(g_backupPath)-1);
177 g_backupPath[sizeof(g_backupPath)-1] = '\0'; 186 g_backupPath[sizeof(g_backupPath)-1] = '\0';
178} 187}
@@ -1254,11 +1263,11 @@ static void DrawWalletSafetyDialog()
1254 } 1263 }
1255 ImGui::Text("Phrase-backed spendable balance: %s BTF", 1264 ImGui::Text("Phrase-backed spendable balance: %s BTF",
1256 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); 1265 FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str());
1257 ImGui::Text("Wallet.dat-only spendable balance: %s BTF", 1266 ImGui::Text("Not phrase-backed spendable balance: %s BTF",
1258 FmtMoney(g_recoveryAudit.nLegacyCredit).c_str()); 1267 FmtMoney(g_recoveryAudit.nLegacyCredit).c_str());
1259 ImGui::Text("Phrase-backed immature mining rewards: %s BTF", 1268 ImGui::Text("Phrase-backed immature mining rewards: %s BTF",
1260 FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str()); 1269 FmtMoney(g_recoveryAudit.nRecoverableImmatureCredit).c_str());
1261 ImGui::Text("Wallet.dat-only immature mining rewards: %s BTF", 1270 ImGui::Text("Not phrase-backed immature mining rewards: %s BTF",
1262 FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str()); 1271 FmtMoney(g_recoveryAudit.nLegacyImmatureCredit).c_str());
1263 if (!g_recoveryAudit.fDeriveComplete) 1272 if (!g_recoveryAudit.fDeriveComplete)
1264 { 1273 {
@@ -1272,8 +1281,8 @@ static void DrawWalletSafetyDialog()
1272 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f)); 1281 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.75f, 0.25f, 1.0f));
1273 ImGui::TextWrapped( 1282 ImGui::TextWrapped(
1274 "Some coins are on keys the phrase does not reproduce. " 1283 "Some coins are on keys the phrase does not reproduce. "
1275 "Keep wallet.dat backups until that balance has been moved to a " 1284 "Keep a file backup of this wallet until that balance has been "
1276 "phrase-backed address."); 1285 "moved to a phrase-backed address.");
1277 ImGui::PopStyleColor(); 1286 ImGui::PopStyleColor();
1278 } 1287 }
1279 else if (HaveHDSeed() && 1288 else if (HaveHDSeed() &&
@@ -1369,16 +1378,38 @@ static void DrawDiagnosticsDialog()
1369 ImGui::End(); 1378 ImGui::End();
1370} 1379}
1371 1380
1381static void OpenURL(const char* pszUrl)
1382{
1383#ifdef _WIN32
1384 ShellExecuteA(NULL, "open", pszUrl, NULL, NULL, SW_SHOWNORMAL);
1385#else
1386 std::string cmd = std::string("xdg-open '") + pszUrl + "' >/dev/null 2>&1 &";
1387 int r = system(cmd.c_str());
1388 (void)r;
1389#endif
1390}
1391
1372static void DrawAboutDialog() 1392static void DrawAboutDialog()
1373{ 1393{
1374 if (!g_showAbout) return; 1394 if (!g_showAbout) return;
1375 ImGui::SetNextWindowSize(ImVec2(380.0f, 170.0f), ImGuiCond_Always); 1395 ImGui::SetNextWindowSize(ImVec2(380.0f, 205.0f), ImGuiCond_Always);
1376 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), 1396 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1377 ImGuiCond_Always, ImVec2(0.5f, 0.5f)); 1397 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1378 if (ImGui::Begin("About Bitflash", &g_showAbout, 1398 if (ImGui::Begin("About Bitflash", &g_showAbout,
1379 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) 1399 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1380 { 1400 {
1381 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Bitflash BTF v1.1.0"); 1401 ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
1402 "Bitflash BTF v" BITFLASH_VERSION_STRING);
1403 ImGui::Spacing();
1404 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, 1.0f));
1405 ImGui::TextUnformatted("https://bitflash.network");
1406 ImGui::PopStyleColor();
1407 if (ImGui::IsItemHovered())
1408 {
1409 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
1410 if (ImGui::IsMouseClicked(0))
1411 OpenURL("https://bitflash.network");
1412 }
1382 ImGui::Spacing(); 1413 ImGui::Spacing();
1383 ImGui::TextWrapped( 1414 ImGui::TextWrapped(
1384 "CPU-only cryptocurrency. RandomX proof of work. " 1415 "CPU-only cryptocurrency. RandomX proof of work. "
@@ -1391,6 +1422,184 @@ static void DrawAboutDialog()
1391 ImGui::End(); 1422 ImGui::End();
1392} 1423}
1393 1424
1425// First-run offer to move a Berkeley DB wallet to the SQLite backend. Only the
1426// button actions touch anything: "Convert" exports wallet.dat to wallet.sqlite
1427// and records the backend marker (wallet.dat is left untouched, so a restart
1428// picks up SQLite while the old file stays as a fallback); "Keep Berkeley DB"
1429// just records that choice so the offer does not return; "Decide later" leaves
1430// no marker and asks again next start.
1431static bool g_showBackendWizard = false;
1432static std::string g_wizardStatus;
1433static bool g_wizardConverted = false;
1434static int g_restartArgc = 0;
1435static char** g_restartArgv = NULL;
1436
1437#ifdef _WIN32
1438// Quote one argument so CommandLineToArgvW parses it back to exactly this
1439// string: only backslashes that immediately precede a double quote (including
1440// the closing one) are doubled, so ordinary Windows paths are not mangled.
1441static std::string QuoteArgWin(const std::string& a)
1442{
1443 if (!a.empty() && a.find_first_of(" \t\"") == std::string::npos)
1444 return a;
1445 std::string r = "\"";
1446 size_t bs = 0;
1447 for (size_t i = 0; i < a.size(); i++)
1448 {
1449 if (a[i] == '\\') { bs++; continue; }
1450 if (a[i] == '"') { r.append(bs * 2 + 1, '\\'); r += '"'; bs = 0; continue; }
1451 r.append(bs, '\\'); bs = 0; r += a[i];
1452 }
1453 r.append(bs * 2, '\\');
1454 r += "\"";
1455 return r;
1456}
1457#endif
1458
1459// Relaunch Bitflash with the same arguments and datadir, then ask this instance
1460// to exit -- but only once the relaunch has actually started, so a failure never
1461// leaves the user with no node running. The new instance waits (-restartwait)
1462// before it opens the network port and the block index, so this one is gone
1463// first and they never fight over the same files.
1464static void RestartApplication()
1465{
1466 bool fLaunched = false;
1467#ifdef _WIN32
1468 char exePath[MAX_PATH];
1469 if (GetModuleFileNameA(NULL, exePath, MAX_PATH) == 0)
1470 return;
1471 std::string strCmd = QuoteArgWin(exePath);
1472 bool fHasDataDir = false;
1473 for (int i = 1; i < g_restartArgc; i++)
1474 {
1475 std::string a = g_restartArgv[i] ? g_restartArgv[i] : "";
1476 if (a.find("-datadir=") == 0 || a.find("/datadir=") == 0)
1477 fHasDataDir = true;
1478 strCmd += " " + QuoteArgWin(a);
1479 }
1480 if (!fHasDataDir)
1481 strCmd += " " + QuoteArgWin("-datadir=" + GetAppDir());
1482 strCmd += " -restartwait";
1483
1484 std::vector<char> cmdBuf(strCmd.begin(), strCmd.end());
1485 cmdBuf.push_back('\0');
1486 STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si);
1487 PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi));
1488 if (CreateProcessA(NULL, &cmdBuf[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
1489 {
1490 CloseHandle(pi.hProcess);
1491 CloseHandle(pi.hThread);
1492 fLaunched = true;
1493 }
1494#else
1495 pid_t pid = fork();
1496 if (pid == 0)
1497 {
1498 // Child: wait for the parent to release the port and files, then re-exec.
1499 // /proc/self/exe is the reliable absolute path; argv[0] may be relative
1500 // or a bare name that execv() cannot resolve.
1501 sleep(2);
1502 char exePath[4096];
1503 ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
1504 if (len > 0)
1505 {
1506 exePath[len] = '\0';
1507 execv(exePath, g_restartArgv);
1508 }
1509 if (g_restartArgv)
1510 execv(g_restartArgv[0], g_restartArgv);
1511 _exit(127);
1512 }
1513 fLaunched = (pid > 0);
1514#endif
1515
1516 if (fLaunched)
1517 {
1518 fShutdown = true;
1519 if (glfwGetCurrentContext())
1520 glfwSetWindowShouldClose(glfwGetCurrentContext(), true);
1521 }
1522 else
1523 g_wizardStatus = "Could not relaunch automatically -- please restart "
1524 "Bitflash yourself to switch to the SQLite wallet.";
1525}
1526
1527static void DrawBackendWizardDialog()
1528{
1529 if (!g_showBackendWizard) return;
1530 ImGui::SetNextWindowSize(ImVec2(560.0f, 0.0f), ImGuiCond_Always);
1531 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1532 ImGuiCond_Always, ImVec2(0.5f, 0.5f));
1533 if (ImGui::Begin("Wallet Storage", &g_showBackendWizard,
1534 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse))
1535 {
1536 if (g_wizardConverted)
1537 {
1538 ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.45f, 1.0f), "Converted to SQLite.");
1539 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1540 ImGui::Spacing();
1541 ImGui::TextWrapped(
1542 "The switch takes effect when Bitflash restarts. Until then it is "
1543 "still running on the old Berkeley DB wallet. Your wallet.dat is "
1544 "kept untouched as a fallback.");
1545 ImGui::Spacing();
1546 if (ImGui::Button("Restart Now", ImVec2(140.0f, 0.0f)))
1547 RestartApplication();
1548 ImGui::SameLine();
1549 if (ImGui::Button("Later", ImVec2(120.0f, 0.0f)))
1550 g_showBackendWizard = false;
1551 ImGui::Spacing();
1552 ImGui::TextDisabled(
1553 "If you keep mining before restarting, new coins go to the old "
1554 "wallet until you switch.");
1555 }
1556 else
1557 {
1558 ImGui::TextWrapped(
1559 "Bitflash can store your wallet in a new SQLite format: a single "
1560 "file that is easier to back up and does not depend on the "
1561 "database/ directory beside it.");
1562 ImGui::Spacing();
1563 ImGui::TextWrapped(
1564 "Your current Berkeley DB wallet (wallet.dat) is kept exactly as it "
1565 "is -- nothing is deleted, and you can go back to it at any time. "
1566 "The change takes effect after you restart.");
1567 ImGui::Spacing();
1568 if (ImGui::Button("Convert to SQLite", ImVec2(180.0f, 0.0f)))
1569 {
1570 int nCopied = 0;
1571 std::string strError;
1572 if (DoConvertWalletToSQLite(nCopied, strError))
1573 {
1574 g_wizardStatus =
1575 strprintf("%d record(s) copied to wallet.sqlite.", nCopied);
1576 g_wizardConverted = true;
1577 }
1578 else
1579 g_wizardStatus = "Conversion failed: " + strError;
1580 }
1581 ImGui::SameLine();
1582 if (ImGui::Button("Keep Berkeley DB", ImVec2(160.0f, 0.0f)))
1583 {
1584 WriteWalletBackendMarker("bdb");
1585 g_showBackendWizard = false;
1586 }
1587 ImGui::SameLine();
1588 if (ImGui::Button("Decide later", ImVec2(120.0f, 0.0f)))
1589 g_showBackendWizard = false;
1590
1591 if (!g_wizardStatus.empty())
1592 {
1593 ImGui::Spacing();
1594 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.35f, 1.0f));
1595 ImGui::TextWrapped("%s", g_wizardStatus.c_str());
1596 ImGui::PopStyleColor();
1597 }
1598 }
1599 }
1600 ImGui::End();
1601}
1602
1394// --------------------------------------------------------------------------- 1603// ---------------------------------------------------------------------------
1395// Entry point 1604// Entry point
1396// --------------------------------------------------------------------------- 1605// ---------------------------------------------------------------------------
@@ -1457,6 +1666,21 @@ int RunGUI(int argc, char* argv[])
1457 g_mineRadio = nMineMode; 1666 g_mineRadio = nMineMode;
1458 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); 1667 strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1);
1459 1668
1669 // Kept for the wizard's "Restart Now": relaunch with the same command line.
1670 g_restartArgc = argc;
1671 g_restartArgv = argv;
1672
1673 // First run on a Berkeley DB wallet with no recorded choice yet: offer to
1674 // convert to SQLite. Never when already on SQLite, once wallet.sqlite exists,
1675 // or once a choice has been recorded in the backend marker.
1676 if (!WalletSQLiteRuntimeActive() &&
1677 FileExists((GetAppDir() + "/wallet.dat").c_str()) &&
1678 !FileExists((GetAppDir() + "/wallet.sqlite").c_str()) &&
1679 ReadWalletBackendMarker().empty())
1680 {
1681 g_showBackendWizard = true;
1682 }
1683
1460 int frame = 0; 1684 int frame = 0;
1461 while (!glfwWindowShouldClose(window) && !fShutdown) 1685 while (!glfwWindowShouldClose(window) && !fShutdown)
1462 { 1686 {
@@ -1480,6 +1704,7 @@ int RunGUI(int argc, char* argv[])
1480 DrawRestorePhraseDialog(); 1704 DrawRestorePhraseDialog();
1481 DrawDiagnosticsDialog(); 1705 DrawDiagnosticsDialog();
1482 DrawAboutDialog(); 1706 DrawAboutDialog();
1707 DrawBackendWizardDialog();
1483 DrawStatusBar(); 1708 DrawStatusBar();
1484 1709
1485 ImGui::Render(); 1710 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 7b3e555..3011111 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -4661,6 +4661,9 @@ WalletRecoveryAudit GetWalletRecoveryAudit()
4661 CRITICAL_BLOCK(cs_keyPool) 4661 CRITICAL_BLOCK(cs_keyPool)
4662 { 4662 {
4663 audit.fHaveSeed = HaveHDSeed(); 4663 audit.fHaveSeed = HaveHDSeed();
4664 audit.fSeedEncryptedLocked = !audit.fHaveSeed &&
4665 (!vchCryptedHDMaster.empty() ||
4666 !vchCryptedHDChainCode.empty());
4664 audit.nSchema = nHDKeySchema; 4667 audit.nSchema = nHDKeySchema;
4665 audit.nDerivedKnown = nHDNext; 4668 audit.nDerivedKnown = nHDNext;
4666 audit.nReceiveNext = nHDReceiveNext; 4669 audit.nReceiveNext = nHDReceiveNext;
diff --git a/src/main.h b/src/main.h
index 382dd9c..75b6db7 100644
--- a/src/main.h
+++ b/src/main.h
@@ -306,6 +306,7 @@ int64 GetBalance();
306struct WalletRecoveryAudit 306struct WalletRecoveryAudit
307{ 307{
308 bool fHaveSeed; 308 bool fHaveSeed;
309 bool fSeedEncryptedLocked;
309 bool fDeriveComplete; 310 bool fDeriveComplete;
310 int nSchema; 311 int nSchema;
311 unsigned int nDerivedKnown; 312 unsigned int nDerivedKnown;
@@ -325,6 +326,7 @@ struct WalletRecoveryAudit
325 WalletRecoveryAudit() 326 WalletRecoveryAudit()
326 { 327 {
327 fHaveSeed = false; 328 fHaveSeed = false;
329 fSeedEncryptedLocked = false;
328 fDeriveComplete = true; 330 fDeriveComplete = true;
329 nSchema = HD_SCHEMA_NONE; 331 nSchema = HD_SCHEMA_NONE;
330 nDerivedKnown = 0; 332 nDerivedKnown = 0;
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 211e1e2..2743429 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -113,8 +113,12 @@ static void PrintUsage()
113 printf(" /debug\n"); 113 printf(" /debug\n");
114 printf(" /gen\n"); 114 printf(" /gen\n");
115 printf(" /nogui or /daemon\n"); 115 printf(" /nogui or /daemon\n");
116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); 116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity,\n");
117 printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n"); 117 printf(" db-env-reopen, wallet-sqlite, wallet-sqlite-migration,\n");
118 printf(" wallet-crypto, wallet-encrypt, wallet-portability,\n");
119 printf(" net-message,\n");
120 printf(" consensus-limits, pool-stratum,\n");
121 printf(" parse-money, socks5-proxy, or managed-tor\n");
118 printf("\n"); 122 printf("\n");
119 printf("Mining mode:\n"); 123 printf("Mining mode:\n");
120 printf(" /operator\n"); 124 printf(" /operator\n");
@@ -175,6 +179,23 @@ static void PrintUsage()
175 printf(" /recoveryaudit (show how much spendable balance is\n"); 179 printf(" /recoveryaudit (show how much spendable balance is\n");
176 printf(" covered by the recovery phrase;\n"); 180 printf(" covered by the recovery phrase;\n");
177 printf(" exits 2 when wallet.dat is still needed)\n"); 181 printf(" exits 2 when wallet.dat is still needed)\n");
182 printf(" /walletstorageaudit (count wallet.dat record types without\n");
183 printf(" printing wallet values, then exit)\n");
184 printf(" /walletstorageauditjson=FILE\n");
185 printf(" (write the same audit as deterministic JSON)\n");
186 printf(" /walletstoragecheck (fail closed on inconsistent or unsafe\n");
187 printf(" wallet.dat storage state)\n");
188 printf(" /walletsqliteexport=FILE (copy raw wallet.dat records into an\n");
189 printf(" SQLite store, then exit)\n");
190 printf(" /walletsqliteverify=FILE (compare wallet.dat with a SQLite export,\n");
191 printf(" printing counts only, then exit)\n");
192 printf(" /walletsqliterestore=FILE (rebuild wallet.dat from a SQLite export in\n");
193 printf(" an empty data directory, then exit)\n");
194 printf(" /walletsqliteloadcheck=FILE\n");
195 printf(" (parse a SQLite export like the wallet loader)\n");
196 printf(" /walletsqlite=FILE (stage-load a SQLite wallet export, then exit)\n");
197 printf(" /walletbackend=sqlite (opt-in: run the node on <datadir>/wallet.sqlite\n");
198 printf(" instead of wallet.dat; export one first)\n");
178 printf(" /rescan (walk the chain for coins this wallet owns\n"); 199 printf(" /rescan (walk the chain for coins this wallet owns\n");
179 printf(" but never recorded, then exit)\n"); 200 printf(" but never recorded, then exit)\n");
180 printf("\n"); 201 printf("\n");
@@ -530,10 +551,119 @@ int main(int argc, char* argv[])
530 551
531 ParseStartupArguments(argc, argv); 552 ParseStartupArguments(argc, argv);
532 553
554#ifdef _WIN32
555 // Set by the GUI "Restart Now" button: give the previous instance a moment
556 // to release the network port and the block-index files before this one
557 // opens them, so a relaunch does not race the process it is replacing.
558 if (arg(argc, argv, "/restartwait") || arg(argc, argv, "-restartwait"))
559 Sleep(2500);
560#endif
561
562 // Hidden self-test helper. It must run before LoadWallet(): several
563 // storage-sanity scenarios deliberately make wallet.dat unsafe to load.
564 string strSelfTestMutateWallet =
565 argval2(argc, argv, "/selftestmutatewallet", "-selftestmutatewallet");
566 if (!strSelfTestMutateWallet.empty())
567 return RunSelfTestMutateWallet(strSelfTestMutateWallet);
568
533 string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); 569 string strSelfTest = argval2(argc, argv, "/selftest", "-selftest");
534 if (!strSelfTest.empty()) 570 if (!strSelfTest.empty())
535 return RunSelfTest(strSelfTest); 571 return RunSelfTest(strSelfTest);
536 572
573 // Storage diagnostics must run before LoadWallet(). Their job is to
574 // explain wallet.dat states that the normal wallet loader may refuse.
575 string strWalletStorageAuditJson =
576 argval2(argc, argv, "/walletstorageauditjson", "-walletstorageauditjson");
577 bool fDiagWantsAudit = arg(argc,argv,"/walletstorageaudit") ||
578 arg(argc,argv,"-walletstorageaudit") ||
579 !strWalletStorageAuditJson.empty();
580 bool fDiagWantsCheck = arg(argc,argv,"/walletstoragecheck") ||
581 arg(argc,argv,"-walletstoragecheck");
582
583 // These diagnostics normally read wallet.dat. Under -walletbackend=sqlite
584 // the live wallet is wallet.sqlite, so open it read-only as the active
585 // store and let the same scan walk the right file. This runs before the
586 // normal backend resolution further down, so validate the value here too.
587 string strDiagBackend = argval2(argc, argv, "/walletbackend", "-walletbackend");
588 if ((fDiagWantsAudit || fDiagWantsCheck) && !strDiagBackend.empty() &&
589 strDiagBackend != "sqlite" && strDiagBackend != "bdb")
590 {
591 AttachTerminal();
592 fprintf(stderr, "Unknown wallet backend '%s'. Use -walletbackend=sqlite "
593 "or -walletbackend=bdb.\n", strDiagBackend.c_str());
594 return 1;
595 }
596 bool fDiagSQLite = (strDiagBackend == "sqlite") &&
597 (fDiagWantsAudit || fDiagWantsCheck);
598 if (fDiagSQLite)
599 {
600 AttachTerminal();
601 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
602 string strOpenErr;
603 if (!WalletSQLiteRuntimeOpenReadOnly(strSQLitePath, strOpenErr))
604 {
605 fprintf(stderr, "Cannot open the SQLite wallet for diagnostics: %s\n",
606 strOpenErr.c_str());
607 return 1;
608 }
609 }
610
611 if (fDiagWantsAudit)
612 {
613 int nRet = CmdWalletStorageAudit(strWalletStorageAuditJson);
614 if (fDiagSQLite)
615 WalletSQLiteRuntimeClose();
616 else
617 DBFlush(true);
618 return nRet;
619 }
620
621 if (fDiagWantsCheck)
622 {
623 int nRet = CmdWalletStorageCheck();
624 if (fDiagSQLite)
625 WalletSQLiteRuntimeClose();
626 else
627 DBFlush(true);
628 return nRet;
629 }
630
631 string strWalletSQLiteExport =
632 argval2(argc, argv, "/walletsqliteexport", "-walletsqliteexport");
633 if (!strWalletSQLiteExport.empty())
634 {
635 int nRet = CmdWalletSQLiteExport(strWalletSQLiteExport);
636 DBFlush(true);
637 return nRet;
638 }
639
640 string strWalletSQLiteVerify =
641 argval2(argc, argv, "/walletsqliteverify", "-walletsqliteverify");
642 if (!strWalletSQLiteVerify.empty())
643 {
644 int nRet = CmdWalletSQLiteVerify(strWalletSQLiteVerify);
645 DBFlush(true);
646 return nRet;
647 }
648
649 string strWalletSQLiteRestore =
650 argval2(argc, argv, "/walletsqliterestore", "-walletsqliterestore");
651 if (!strWalletSQLiteRestore.empty())
652 {
653 int nRet = CmdWalletSQLiteRestore(strWalletSQLiteRestore);
654 DBFlush(true);
655 return nRet;
656 }
657
658 string strWalletSQLiteLoadCheck =
659 argval2(argc, argv, "/walletsqliteloadcheck", "-walletsqliteloadcheck");
660 if (!strWalletSQLiteLoadCheck.empty())
661 {
662 int nRet = CmdWalletSQLiteLoadCheck(strWalletSQLiteLoadCheck);
663 DBFlush(true);
664 return nRet;
665 }
666
537 // Berkeley DB reports failure by throwing, and CDB's constructor lets it 667 // Berkeley DB reports failure by throwing, and CDB's constructor lets it
538 // through. Nothing on this path caught anything, so an unreadable 668 // through. Nothing on this path caught anything, so an unreadable
539 // blkindex.dat or wallet.dat -- one written by another platform's Berkeley 669 // blkindex.dat or wallet.dat -- one written by another platform's Berkeley
@@ -578,18 +708,171 @@ int main(int argc, char* argv[])
578 printf("Loading wallet...\n"); 708 printf("Loading wallet...\n");
579 try 709 try
580 { 710 {
581 if (!LoadWallet()) 711 string strWalletBackend =
712 argval2(argc, argv, "/walletbackend", "-walletbackend");
713 string strWalletSQLite =
714 argval2(argc, argv, "/walletsqlite", "-walletsqlite");
715
716 // A misspelled backend must not fall through to Berkeley DB silently --
717 // someone who typed -walletbackend=sqlit meant to run on SQLite, and
718 // opening wallet.dat instead of telling them is the wrong surprise.
719 if (!strWalletBackend.empty() &&
720 strWalletBackend != "sqlite" && strWalletBackend != "bdb")
582 { 721 {
583 // LoadWallet() explains itself into strWalletLoadError when it 722 AttachTerminal();
584 // knows why -- an unsupported wallet format, for one, which is the 723 FatalStartupError(fHeadlessStartup,
585 // case this whole path exists to make visible. 724 strprintf("Unknown wallet backend '%s'.", strWalletBackend.c_str()),
586 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.", 725 "Use -walletbackend=sqlite for the SQLite backend, or "
587 strWalletLoadError); 726 "-walletbackend=bdb for the Berkeley DB wallet.dat.");
588 return 1; 727 return 1;
589 } 728 }
590 729
730 if (strWalletBackend == "sqlite")
731 {
732 // Explicit opt-in via the flag. The node runs on
733 // <datadir>/wallet.sqlite and never opens wallet.dat, but the user
734 // has to have exported one first -- there is no silent migration,
735 // and wallet.dat is left
736 // exactly where it is so a plain restart goes back to it.
737 AttachTerminal();
738 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
739 if (!FileExists(strSQLitePath.c_str()))
740 {
741 string strGuide = strprintf(
742 "No SQLite wallet was found at\n"
743 " %s\n\n"
744 "The SQLite wallet backend is opt-in and needs an exported\n"
745 "wallet first. Your Berkeley DB wallet.dat is not touched by\n"
746 "any of this.\n\n"
747 " Step 1 Export your existing wallet to SQLite:\n"
748 " bitflash -walletsqliteexport=\"%s\"\n"
749 " Step 2 Start again with the SQLite backend:\n"
750 " bitflash -walletbackend=sqlite\n\n"
751 "To go back to Berkeley DB at any time, just start without\n"
752 "-walletbackend=sqlite. wallet.dat is still your wallet.",
753 strSQLitePath.c_str(), strSQLitePath.c_str());
754 FatalStartupError(fHeadlessStartup,
755 "The SQLite wallet backend needs an exported wallet first.",
756 strGuide);
757 return 1;
758 }
759
760 if (!LoadWalletFromSQLiteRuntime(strSQLitePath))
761 {
762 FatalStartupError(fHeadlessStartup,
763 "Cannot open the SQLite wallet backend.",
764 strWalletLoadError);
765 return 1;
766 }
767
768 fprintf(stderr,
769 "Wallet backend: SQLite\n"
770 " %s\n"
771 "Your Berkeley DB wallet.dat is left untouched. Restart without\n"
772 "-walletbackend=sqlite to go back to it.\n",
773 strSQLitePath.c_str());
774 fflush(stderr);
775 // Fall through and run the node normally: every CWalletDB read,
776 // write, and erase now routes to this SQLite file.
777 }
778 else if (!strWalletSQLite.empty())
779 {
780 AttachTerminal();
781 if (!LoadWalletFromSQLite(strWalletSQLite))
782 {
783 FatalStartupError(fHeadlessStartup,
784 "Cannot open SQLite wallet export.",
785 strWalletLoadError);
786 return 1;
787 }
788
789 fprintf(stderr, "SQLite wallet loaded: read-only staging backend\n");
790 fprintf(stderr, "SQLite runtime writes are not enabled yet; exiting before "
791 "network, mining, GUI, or wallet mutation starts.\n");
792 fflush(stderr);
793 DBFlush(true);
794 return 0;
795 }
796 else if (strWalletBackend == "bdb")
797 {
798 // Explicit -walletbackend=bdb overrides the marker and the default:
799 // force Berkeley DB (LoadWallet creates wallet.dat if none exists).
800 if (!LoadWallet())
801 {
802 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.",
803 strWalletLoadError);
804 return 1;
805 }
806 }
807 else
808 {
809 // No explicit -walletbackend on the command line. Resolve it from
810 // the persistent marker, then defaults: an existing wallet.sqlite
811 // stays on SQLite, an existing wallet.dat stays on Berkeley DB (the
812 // GUI offers to convert), and a fresh install starts on SQLite --
813 // the new default. wallet.dat is never touched by any of this.
814 string strSQLitePath = GetAppDir() + "/wallet.sqlite";
815 bool fSQLiteExists = FileExists(strSQLitePath.c_str());
816 bool fWalletDatExists = FileExists((GetAppDir() + "/wallet.dat").c_str());
817 string strMarker = ReadWalletBackendMarker();
818
819 bool fUseSQLite = false;
820 bool fCreateSQLite = false;
821 if (strMarker == "sqlite")
822 {
823 if (fSQLiteExists) fUseSQLite = true;
824 else if (fWalletDatExists) fUseSQLite = false; // marker stale; real wallet is BDB
825 else { fUseSQLite = true; fCreateSQLite = true; }
826 }
827 else if (strMarker == "bdb")
828 {
829 fUseSQLite = false;
830 }
831 else // no marker recorded yet
832 {
833 if (fSQLiteExists && !fWalletDatExists) fUseSQLite = true;
834 else if (fWalletDatExists) fUseSQLite = false;
835 else { fUseSQLite = true; fCreateSQLite = true; }
836 }
837
838 if (fUseSQLite)
839 {
840 AttachTerminal();
841 bool fOk = fCreateSQLite
842 ? CreateNewSQLiteWallet(strSQLitePath)
843 : LoadWalletFromSQLiteRuntime(strSQLitePath);
844 if (!fOk)
845 {
846 FatalStartupError(fHeadlessStartup,
847 fCreateSQLite ? "Cannot create the SQLite wallet."
848 : "Cannot open the SQLite wallet.",
849 strWalletLoadError);
850 return 1;
851 }
852 // Record the choice so it is stable from here on.
853 if (ReadWalletBackendMarker() != "sqlite")
854 WriteWalletBackendMarker("sqlite");
855 fprintf(stderr, "Wallet backend: SQLite\n %s\n%s",
856 strSQLitePath.c_str(),
857 fCreateSQLite ? "A new wallet was created here.\n" : "");
858 fflush(stderr);
859 // Fall through and run the node; CWalletDB routes to this file.
860 }
861 else if (!LoadWallet())
862 {
863 // LoadWallet() explains itself into strWalletLoadError when it
864 // knows why -- an unsupported wallet format, for one, which is
865 // the case this whole path exists to make visible.
866 FatalStartupError(fHeadlessStartup, "Cannot open wallet.dat.",
867 strWalletLoadError);
868 return 1;
869 }
870 }
871
591 // After the block index, so there is a chain to compare the wallet 872 // After the block index, so there is a chain to compare the wallet
592 // against, and before anything reports a balance. 873 // against, and before anything reports a balance. Works on either
874 // backend: RescanSpentFlags writes corrected flags through CWalletDB,
875 // which routes to SQLite when that backend is active.
593 RescanSpentFlags(); 876 RescanSpentFlags();
594 } 877 }
595 catch (const std::exception& e) 878 catch (const std::exception& e)
@@ -858,6 +1141,10 @@ int main(int argc, char* argv[])
858 // called here, which is why a wallet.dat copied elsewhere would not 1141 // called here, which is why a wallet.dat copied elsewhere would not
859 // open -- issue #40. Not optional. 1142 // open -- issue #40. Not optional.
860 DBFlush(true); 1143 DBFlush(true);
1144 // Checkpoint and close the SQLite wallet backend if one is active. A
1145 // no-op on the Berkeley DB path. Safe to kill before this runs: WAL +
1146 // synchronous=FULL already made every committed write durable.
1147 WalletSQLiteRuntimeClose();
861#ifdef _WIN32 1148#ifdef _WIN32
862 // wallet.dat is safely flushed and closed now; let a pending console 1149 // wallet.dat is safely flushed and closed now; let a pending console
863 // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish. 1150 // control handler (CLOSE/LOGOFF/SHUTDOWN) return so the OS can finish.
@@ -874,6 +1161,7 @@ int main(int argc, char* argv[])
874 StopNode(); 1161 StopNode();
875 BtfStopManagedTor(); 1162 BtfStopManagedTor();
876 DBFlush(true); 1163 DBFlush(true);
1164 WalletSQLiteRuntimeClose();
877 return ret; 1165 return ret;
878#endif 1166#endif
879} 1167}
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 fed6123..4741f09 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 9f3c7e4..72e13eb 100644
--- a/src/net.h
+++ b/src/net.h
@@ -109,6 +109,23 @@ static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE;
109// 125 is Bitcoin's number and remains a conservative public-node default. 109// 125 is Bitcoin's number and remains a conservative public-node default.
110static const unsigned int MAX_CONNECTIONS = 125; 110static const unsigned int MAX_CONNECTIONS = 125;
111 111
112// Cap inbound connections from any single address. Without it one source can
113// hold every slot up to MAX_CONNECTIONS -- exactly what happened to the
114// rendezvous relays, where a single IP parked hundreds of connections and
115// starved real peers. A legitimate peer, even a relay talking to a relay,
116// needs only a handful; 32 leaves generous room while denying any one address
117// more than a quarter of the table. The firewall connlimit on the relays does
118// the same thing a layer lower; this makes every build carry the defense.
119//
120// Loopback (127.0.0.0/8) is exempt in the accept path. A managed-Tor node
121// receives every inbound onion peer over its local hidden-service listener, so
122// they all appear as 127.0.0.1 -- capping loopback would cap onion peers as a
123// group, which is exactly wrong for a network moving toward onion-only. The
124// global MAX_CONNECTIONS still bounds the total; a per-onion-identity limit,
125// applied once identity is known after accept(), is the right future tool for
126// onion floods.
127static const unsigned int MAX_CONNECTIONS_PER_IP = 32;
128
112// A node marked for disconnect is normally held until its buffers drain, so 129// A node marked for disconnect is normally held until its buffers drain, so
113// a last message still goes out. But vSend cannot drain through a socket 130// a last message still goes out. But vSend cannot drain through a socket
114// whose far end is gone, and that made such a node immortal: it stayed in 131// 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 9f7106f..2ada8f3 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,721 @@ 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 -> new wallet.sqlite, marker "sqlite", no wallet.dat.
2584 string d1 = tmp + "/fresh";
2585 nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1;
2586 vector<string> a1;
2587 a1.push_back("-datadir=" + d1);
2588 a1.push_back("-nomanagedtor");
2589 a1.push_back("-nogui");
2590 a1.push_back("-newaddress");
2591 int r1 = RunBitflashChild(strExe, a1);
2592 nFail += Check(r1 == 0 &&
2593 FileExists((d1 + "/wallet.sqlite").c_str()) &&
2594 !FileExists((d1 + "/wallet.dat").c_str()) &&
2595 FileContainsText(d1 + "/wallet-backend", "sqlite"),
2596 "a fresh install starts on SQLite (new default) and records the marker") ? 0 : 1;
2597
2598 // 1b. Second run reuses it, still SQLite, still no wallet.dat.
2599 int r1b = RunBitflashChild(strExe, a1);
2600 nFail += Check(r1b == 0 &&
2601 FileExists((d1 + "/wallet.sqlite").c_str()) &&
2602 !FileExists((d1 + "/wallet.dat").c_str()),
2603 "the recorded SQLite choice survives a restart") ? 0 : 1;
2604
2605 // 2. Existing wallet.dat, no flag -> stays on Berkeley DB.
2606 string d2 = tmp + "/existing-bdb";
2607 nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1;
2608 vector<string> a2c;
2609 a2c.push_back("-datadir=" + d2);
2610 a2c.push_back("-nomanagedtor");
2611 a2c.push_back("-nogui");
2612 a2c.push_back("-walletbackend=bdb");
2613 a2c.push_back("-newphrase");
2614 RunBitflashChild(strExe, a2c);
2615 vector<string> a2;
2616 a2.push_back("-datadir=" + d2);
2617 a2.push_back("-nomanagedtor");
2618 a2.push_back("-nogui");
2619 a2.push_back("-newaddress");
2620 int r2 = RunBitflashChild(strExe, a2);
2621 nFail += Check(r2 == 0 &&
2622 FileExists((d2 + "/wallet.dat").c_str()) &&
2623 !FileExists((d2 + "/wallet.sqlite").c_str()),
2624 "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1;
2625
2626 // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir.
2627 string d3 = tmp + "/marker-bdb";
2628 nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1;
2629 nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"),
2630 "a bdb marker can be written") ? 0 : 1;
2631 vector<string> a3;
2632 a3.push_back("-datadir=" + d3);
2633 a3.push_back("-nomanagedtor");
2634 a3.push_back("-nogui");
2635 a3.push_back("-newaddress");
2636 int r3 = RunBitflashChild(strExe, a3);
2637 nFail += Check(r3 == 0 &&
2638 FileExists((d3 + "/wallet.dat").c_str()) &&
2639 !FileExists((d3 + "/wallet.sqlite").c_str()),
2640 "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1;
2641
2642 // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat.
2643 string d4 = tmp + "/explicit-bdb";
2644 nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1;
2645 vector<string> a4;
2646 a4.push_back("-datadir=" + d4);
2647 a4.push_back("-nomanagedtor");
2648 a4.push_back("-nogui");
2649 a4.push_back("-walletbackend=bdb");
2650 a4.push_back("-newaddress");
2651 int r4 = RunBitflashChild(strExe, a4);
2652 nFail += Check(r4 == 0 &&
2653 FileExists((d4 + "/wallet.dat").c_str()) &&
2654 !FileExists((d4 + "/wallet.sqlite").c_str()),
2655 "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1;
2656 }
2657 catch (const std::exception& e)
2658 {
2659 printf(" FAIL exception: %s\n", e.what());
2660 nFail++;
2661 }
2662 catch (...)
2663 {
2664 printf(" FAIL unknown exception\n");
2665 nFail++;
2666 }
2667
2668 DBFlush(true);
2669 SetCurrentDir(cwd);
2670 RemoveTree(tmp);
2671 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2672 nFail, nFail == 1 ? "" : "s");
2673 fflush(stdout);
2674 return nFail == 0 ? 0 : 1;
2675}
2676
2677// The wizard's conversion action, headless: build a Berkeley DB wallet, run
2678// DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a
2679// usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then
2680// that a flagless restart opens the converted SQLite wallet.
2681static int RunWalletConvertSelfTest()
2682{
2683 fflush(stdout);
2684 printf("wallet-convert self-test\n");
2685
2686 std::string tmp, cwd;
2687 if (!MakeTempDir(tmp))
2688 {
2689 printf(" FAIL could not create a temporary data directory\n");
2690 return 1;
2691 }
2692 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2693 {
2694 printf(" FAIL could not move into the temporary data directory\n");
2695 RemoveTree(tmp);
2696 return 1;
2697 }
2698
2699 int nFail = 0;
2700 strSetDataDir = tmp;
2701 printf(" temp datadir: %s\n", tmp.c_str());
2702
2703#ifdef _WIN32
2704 string strExe = cwd + "\\bitflash.exe";
2705#else
2706 string strExe = cwd + "/bitflash-node";
2707#endif
2708
2709 try
2710 {
2711 if (!LoadWallet())
2712 throw std::runtime_error("LoadWallet failed");
2713
2714 string strError;
2715 nFail += Check(SetHDSeedFromMnemonic(
2716 "abandon abandon abandon abandon abandon abandon abandon "
2717 "abandon abandon abandon abandon about", strError),
2718 "a phrase can be installed before conversion") ? 0 : 1;
2719 TopUpKeyPool();
2720
2721 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
2722 CPrivKey vchDefaultPrivKey;
2723 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
2724 "the default private key is readable before conversion") ? 0 : 1;
2725 vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(),
2726 vchDefaultPrivKey.end());
2727
2728 // The conversion action the GUI button invokes.
2729 int nCopied = 0;
2730 string strConvertError;
2731 bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError);
2732 nFail += Check(fConverted && nCopied > 0,
2733 "DoConvertWalletToSQLite exports the wallet") ? 0 : 1;
2734 nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) &&
2735 FileContainsText(tmp + "/wallet-backend", "sqlite"),
2736 "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1;
2737 nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes),
2738 "wallet.dat is left untouched by the conversion") ? 0 : 1;
2739
2740 // Idempotence guard: a second conversion refuses rather than overwrite.
2741 int nCopied2 = 0;
2742 string strSecondError;
2743 nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError),
2744 "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1;
2745
2746 DBFlush(true);
2747
2748 // A flagless restart now opens the converted SQLite wallet (the marker
2749 // routes it) and finds the same recovery phrase.
2750 string strAudit = tmp + "/converted-recovery-audit.txt";
2751 vector<string> vAuditArgs;
2752 vAuditArgs.push_back("-datadir=" + tmp);
2753 vAuditArgs.push_back("-nomanagedtor");
2754 vAuditArgs.push_back("-nogui");
2755 vAuditArgs.push_back("-recoveryaudit");
2756 int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit);
2757 nFail += Check(nAuditRet == 0 &&
2758 FileContainsText(strAudit, "recovery phrase: present"),
2759 "a flagless restart opens the converted SQLite wallet") ? 0 : 1;
2760 }
2761 catch (const std::exception& e)
2762 {
2763 printf(" FAIL exception: %s\n", e.what());
2764 nFail++;
2765 }
2766 catch (...)
2767 {
2768 printf(" FAIL unknown exception\n");
2769 nFail++;
2770 }
2771
2772 DBFlush(true);
2773 SetCurrentDir(cwd);
2774 RemoveTree(tmp);
2775 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2776 nFail, nFail == 1 ? "" : "s");
2777 fflush(stdout);
2778 return nFail == 0 ? 0 : 1;
2779}
2780
2781static int RunWalletPortabilitySelfTest()
2782{
2783 fflush(stdout);
2784 printf("wallet-portability self-test\n");
2785
2786 std::string tmp;
2787 std::string cwd;
2788 if (!MakeTempDir(tmp))
2789 {
2790 printf(" FAIL could not create a temporary data directory\n");
2791 return 1;
2792 }
2793 if (!GetCurrentDir(cwd))
2794 {
2795 printf(" FAIL could not read current directory\n");
2796 RemoveTree(tmp);
2797 return 1;
2798 }
2799
2800 int nFail = 0;
2801 printf(" temp root: %s\n", tmp.c_str());
2802
2803 try
2804 {
2805#ifdef _WIN32
2806 string strExe = cwd + "\\bitflash.exe";
2807#else
2808 string strExe = cwd + "/bitflash-node";
2809#endif
2810 string strOriginal = tmp + "/original";
2811 string strRawCopy = tmp + "/raw-copy";
2812 string strBackupCopy = tmp + "/backup-copy";
2813 nFail += Check(MakeDirLocal(strOriginal) &&
2814 MakeDirLocal(strRawCopy) &&
2815 MakeDirLocal(strBackupCopy),
2816 "temporary wallet directories can be created") ? 0 : 1;
2817
2818 string strCreateOut = tmp + "/create.txt";
2819 vector<string> vCreateArgs;
2820 vCreateArgs.push_back("-datadir=" + strOriginal);
2821 vCreateArgs.push_back("-nomanagedtor");
2822 vCreateArgs.push_back("-nogui");
2823 vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability
2824 vCreateArgs.push_back("-newphrase");
2825 int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut);
2826 nFail += Check(nCreateRet == 0 &&
2827 FileContainsText(strCreateOut, "Write these twelve words down"),
2828 "a phrase wallet can be created in a fresh datadir") ? 0 : 1;
2829
2830 string strOriginalAudit = tmp + "/original-audit.txt";
2831 vector<string> vOriginalAuditArgs;
2832 vOriginalAuditArgs.push_back("-datadir=" + strOriginal);
2833 vOriginalAuditArgs.push_back("-nomanagedtor");
2834 vOriginalAuditArgs.push_back("-nogui");
2835 vOriginalAuditArgs.push_back("-recoveryaudit");
2836 int nOriginalAuditRet = RunBitflashChild(strExe, vOriginalAuditArgs, NULL, &strOriginalAudit);
2837 nFail += Check(nOriginalAuditRet == 0 &&
2838 FileContainsText(strOriginalAudit, "recovery phrase: present") &&
2839 FileContainsText(strOriginalAudit, "SLIP-0044 BITFLASH"),
2840 "the original wallet reports its recovery phrase") ? 0 : 1;
2841
2842 string strOriginalStorageAudit = tmp + "/original-storage-audit.txt";
2843 vector<string> vOriginalStorageAuditArgs;
2844 vOriginalStorageAuditArgs.push_back("-datadir=" + strOriginal);
2845 vOriginalStorageAuditArgs.push_back("-nomanagedtor");
2846 vOriginalStorageAuditArgs.push_back("-nogui");
2847 vOriginalStorageAuditArgs.push_back("-walletstorageaudit");
2848 int nOriginalStorageAuditRet =
2849 RunBitflashChild(strExe, vOriginalStorageAuditArgs, NULL, &strOriginalStorageAudit);
2850 nFail += Check(nOriginalStorageAuditRet == 0 &&
2851 FileContainsText(strOriginalStorageAudit, "Wallet storage audit") &&
2852 FileContainsText(strOriginalStorageAudit, "plain HD seed: complete") &&
2853 FileContainsText(strOriginalStorageAudit, "encrypted HD seed: none") &&
2854 FileContainsText(strOriginalStorageAudit, "malformed records: 0"),
2855 "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1;
2856
2857 string strOriginalStorageCheck = tmp + "/original-storage-check.txt";
2858 vector<string> vOriginalStorageCheckArgs;
2859 vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal);
2860 vOriginalStorageCheckArgs.push_back("-nomanagedtor");
2861 vOriginalStorageCheckArgs.push_back("-nogui");
2862 vOriginalStorageCheckArgs.push_back("-walletstoragecheck");
2863 int nOriginalStorageCheckRet =
2864 RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck);
2865 nFail += Check(nOriginalStorageCheckRet == 0 &&
2866 FileContainsText(strOriginalStorageCheck, "storage sanity: ok") &&
2867 FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"),
2868 "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1;
2869
2870 string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json";
2871 vector<string> vOriginalStorageAuditJsonArgs;
2872 vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal);
2873 vOriginalStorageAuditJsonArgs.push_back("-nomanagedtor");
2874 vOriginalStorageAuditJsonArgs.push_back("-nogui");
2875 vOriginalStorageAuditJsonArgs.push_back("-walletstorageauditjson=" +
2876 strOriginalStorageAuditJson);
2877 int nOriginalStorageAuditJsonRet =
2878 RunBitflashChild(strExe, vOriginalStorageAuditJsonArgs);
2879 nFail += Check(nOriginalStorageAuditJsonRet == 0 &&
2880 FileContainsText(strOriginalStorageAuditJson,
2881 "\"format\": \"bitflash-wallet-storage-audit-v1\"") &&
2882 FileContainsText(strOriginalStorageAuditJson,
2883 "\"plain_hd_seed\": \"complete\"") &&
2884 FileContainsText(strOriginalStorageAuditJson,
2885 "\"encrypted_hd_seed\": \"none\""),
2886 "the storage audit can write plaintext-wallet JSON") ? 0 : 1;
2887
2888 string strPortableWallet = tmp + "/portable-wallet.dat";
2889 string strBackupOut = tmp + "/backupwallet.txt";
2890 vector<string> vBackupArgs;
2891 vBackupArgs.push_back("-datadir=" + strOriginal);
2892 vBackupArgs.push_back("-nomanagedtor");
2893 vBackupArgs.push_back("-nogui");
2894 vBackupArgs.push_back("-backupwallet=" + strPortableWallet);
2895 int nBackupRet = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut);
2896 nFail += Check(nBackupRet == 0 && FileExists(strPortableWallet.c_str()),
2897 "/backupwallet writes a portable wallet.dat") ? 0 : 1;
2898
2899 string strBackupOut2 = tmp + "/backupwallet-overwrite.txt";
2900 int nBackupRet2 = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut2);
2901 nFail += Check(nBackupRet2 == 0 && FileExists(strPortableWallet.c_str()) &&
2902 !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.tmp.") &&
2903 !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.old."),
2904 "/backupwallet replaces an existing backup without leaving temp files") ? 0 : 1;
2905
2906 nFail += Check(CopyFileLocal(strOriginal + "/wallet.dat",
2907 strRawCopy + "/wallet.dat"),
2908 "a raw wallet.dat can be copied without database logs") ? 0 : 1;
2909 string strRawAudit = tmp + "/raw-copy-audit.txt";
2910 vector<string> vRawAuditArgs;
2911 vRawAuditArgs.push_back("-datadir=" + strRawCopy);
2912 vRawAuditArgs.push_back("-nomanagedtor");
2913 vRawAuditArgs.push_back("-nogui");
2914 vRawAuditArgs.push_back("-recoveryaudit");
2915 int nRawAuditRet = RunBitflashChild(strExe, vRawAuditArgs, NULL, &strRawAudit);
2916 nFail += Check(nRawAuditRet == 0 &&
2917 FileContainsText(strRawAudit, "recovery phrase: present") &&
2918 !FileContainsText(strRawAudit, "Cannot open wallet.dat"),
2919 "a raw wallet.dat copy opens in a new datadir") ? 0 : 1;
2920
2921 nFail += Check(CopyFileLocal(strPortableWallet,
2922 strBackupCopy + "/wallet.dat"),
2923 "the portable backup can be installed as wallet.dat") ? 0 : 1;
2924 string strBackupAudit = tmp + "/backup-copy-audit.txt";
2925 vector<string> vBackupAuditArgs;
2926 vBackupAuditArgs.push_back("-datadir=" + strBackupCopy);
2927 vBackupAuditArgs.push_back("-nomanagedtor");
2928 vBackupAuditArgs.push_back("-nogui");
2929 vBackupAuditArgs.push_back("-recoveryaudit");
2930 int nBackupAuditRet = RunBitflashChild(strExe, vBackupAuditArgs, NULL, &strBackupAudit);
2931 nFail += Check(nBackupAuditRet == 0 &&
2932 FileContainsText(strBackupAudit, "recovery phrase: present") &&
2933 FileContainsText(strBackupAudit, "SLIP-0044 BITFLASH"),
2934 "a /backupwallet copy opens in a new datadir") ? 0 : 1;
2935
2936 string strPassFile = tmp + "/encrypt-pass.txt";
2937 string strEncryptOut = tmp + "/encryptwallet.txt";
2938 nFail += Check(WriteTextFile(strPassFile, "portable-test-passphrase\n"),
2939 "an encryption passphrase file can be written") ? 0 : 1;
2940 vector<string> vEncryptArgs;
2941 vEncryptArgs.push_back("-datadir=" + strOriginal);
2942 vEncryptArgs.push_back("-nomanagedtor");
2943 vEncryptArgs.push_back("-nogui");
2944 vEncryptArgs.push_back("-encryptwallet=@" + strPassFile);
2945 int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut);
2946 nFail += Check(nEncryptRet == 0 &&
2947 !DirectoryHasFileWithPrefix(strOriginal + "/database", "log."),
2948 "the encryptwallet command purges Berkeley DB environment logs") ? 0 : 1;
2949
2950 string strEncryptedStorageAudit = tmp + "/encrypted-storage-audit.txt";
2951 vector<string> vEncryptedStorageAuditArgs;
2952 vEncryptedStorageAuditArgs.push_back("-datadir=" + strOriginal);
2953 vEncryptedStorageAuditArgs.push_back("-nomanagedtor");
2954 vEncryptedStorageAuditArgs.push_back("-nogui");
2955 vEncryptedStorageAuditArgs.push_back("-walletstorageaudit");
2956 int nEncryptedStorageAuditRet =
2957 RunBitflashChild(strExe, vEncryptedStorageAuditArgs, NULL, &strEncryptedStorageAudit);
2958 nFail += Check(nEncryptedStorageAuditRet == 0 &&
2959 FileContainsText(strEncryptedStorageAudit, "plain private keys: 0") &&
2960 FileContainsText(strEncryptedStorageAudit, "encrypted private keys:") &&
2961 FileContainsText(strEncryptedStorageAudit, "plain HD seed: none") &&
2962 FileContainsText(strEncryptedStorageAudit, "encrypted HD seed: complete") &&
2963 FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") &&
2964 FileContainsText(strEncryptedStorageAudit, "malformed records: 0"),
2965 "the storage audit recognizes an encrypted wallet") ? 0 : 1;
2966
2967 string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt";
2968 vector<string> vEncryptedStorageCheckArgs;
2969 vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal);
2970 vEncryptedStorageCheckArgs.push_back("-nomanagedtor");
2971 vEncryptedStorageCheckArgs.push_back("-nogui");
2972 vEncryptedStorageCheckArgs.push_back("-walletstoragecheck");
2973 int nEncryptedStorageCheckRet =
2974 RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck);
2975 nFail += Check(nEncryptedStorageCheckRet == 0 &&
2976 FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") &&
2977 FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") &&
2978 FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"),
2979 "the storage sanity check accepts an encrypted wallet") ? 0 : 1;
2980
2981 string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json";
2982 vector<string> vEncryptedStorageAuditJsonArgs;
2983 vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal);
2984 vEncryptedStorageAuditJsonArgs.push_back("-nomanagedtor");
2985 vEncryptedStorageAuditJsonArgs.push_back("-nogui");
2986 vEncryptedStorageAuditJsonArgs.push_back("-walletstorageauditjson=" +
2987 strEncryptedStorageAuditJson);
2988 int nEncryptedStorageAuditJsonRet =
2989 RunBitflashChild(strExe, vEncryptedStorageAuditJsonArgs);
2990 nFail += Check(nEncryptedStorageAuditJsonRet == 0 &&
2991 FileContainsText(strEncryptedStorageAuditJson,
2992 "\"plain_private_keys\": 0") &&
2993 FileContainsText(strEncryptedStorageAuditJson,
2994 "\"plain_hd_seed\": \"none\"") &&
2995 FileContainsText(strEncryptedStorageAuditJson,
2996 "\"encrypted_hd_seed\": \"complete\"") &&
2997 FileContainsText(strEncryptedStorageAuditJson,
2998 "\"wallet_minimum_version\": \"present\""),
2999 "the storage audit can write encrypted-wallet JSON") ? 0 : 1;
3000 }
3001 catch (const std::exception& e)
3002 {
3003 printf(" FAIL exception: %s\n", e.what());
3004 nFail++;
3005 }
3006 catch (...)
3007 {
3008 printf(" FAIL unknown exception\n");
3009 nFail++;
3010 }
3011
3012 RemoveTree(tmp);
3013 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
3014 nFail, nFail == 1 ? "" : "s");
3015 fflush(stdout);
3016 return nFail == 0 ? 0 : 1;
3017}
3018
1135static int RunNetMessageSelfTest() 3019static int RunNetMessageSelfTest()
1136{ 3020{
1137 fflush(stdout); 3021 fflush(stdout);
@@ -1637,10 +3521,26 @@ int RunSelfTest(const std::string& name)
1637 return RunWalletHDSelfTest(); 3521 return RunWalletHDSelfTest();
1638 if (name == "wallet-format") 3522 if (name == "wallet-format")
1639 return RunWalletFormatSelfTest(); 3523 return RunWalletFormatSelfTest();
3524 if (name == "wallet-storage-sanity")
3525 return RunWalletStorageSanitySelfTest();
3526 if (name == "db-env-reopen")
3527 return RunDbEnvReopenSelfTest();
3528 if (name == "wallet-sqlite")
3529 return RunWalletSQLiteSelfTest();
3530 if (name == "wallet-sqlite-migration")
3531 return RunWalletSQLiteMigrationSelfTest();
1640 if (name == "wallet-crypto") 3532 if (name == "wallet-crypto")
1641 return RunWalletCryptoSelfTest(); 3533 return RunWalletCryptoSelfTest();
1642 if (name == "wallet-encrypt") 3534 if (name == "wallet-encrypt")
1643 return RunWalletEncryptSelfTest(); 3535 return RunWalletEncryptSelfTest();
3536 if (name == "wallet-sqlite-encrypt")
3537 return RunWalletSQLiteEncryptSelfTest();
3538 if (name == "wallet-backend-default")
3539 return RunWalletBackendDefaultSelfTest();
3540 if (name == "wallet-convert")
3541 return RunWalletConvertSelfTest();
3542 if (name == "wallet-portability")
3543 return RunWalletPortabilitySelfTest();
1644 if (name == "net-message") 3544 if (name == "net-message")
1645 return RunNetMessageSelfTest(); 3545 return RunNetMessageSelfTest();
1646 if (name == "consensus-limits") 3546 if (name == "consensus-limits")
@@ -1657,6 +3557,6 @@ int RunSelfTest(const std::string& name)
1657 return RunManagedTorSelfTest(); 3557 return RunManagedTorSelfTest();
1658 3558
1659 printf("Unknown self-test '%s'\n", name.c_str()); 3559 printf("Unknown self-test '%s'\n", name.c_str());
1660 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"); 3560 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");
1661 return 1; 3561 return 1;
1662} 3562}
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);