diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-01 17:14:12 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-01 17:14:12 +0000 |
| commit | b4f0bc693ef7175702f82bcb2110b627922e8cbd (patch) | |
| tree | 73435feb1a076dfba94d8143d7d7b7784908c82f /src | |
| parent | a1d867ecbcfc9e218408d0a11863a450243a01f9 (diff) | |
| parent | 6aa3dade77632f13189d2a3ef29897f29a5f9572 (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/Makefile | 11 | ||||
| -rw-r--r-- | src/db.cpp | 1251 | ||||
| -rw-r--r-- | src/db.h | 183 | ||||
| -rw-r--r-- | src/gui.cpp | 239 | ||||
| -rw-r--r-- | src/headers_core.h | 5 | ||||
| -rw-r--r-- | src/main.cpp | 3 | ||||
| -rw-r--r-- | src/main.h | 2 | ||||
| -rw-r--r-- | src/main_gui.cpp | 306 | ||||
| -rw-r--r-- | src/makefile.mingw | 12 | ||||
| -rw-r--r-- | src/net.cpp | 29 | ||||
| -rw-r--r-- | src/net.h | 17 | ||||
| -rw-r--r-- | src/selftest.cpp | 1910 | ||||
| -rw-r--r-- | src/selftest.h | 1 | ||||
| -rw-r--r-- | src/wallet_sqlite.cpp | 374 | ||||
| -rw-r--r-- | src/wallet_sqlite.h | 120 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 975 | ||||
| -rw-r--r-- | src/walletcmd.h | 34 |
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 | ||
| 41 | LIBS = -L$(SECP_LIB) -lsecp256k1 \ | 41 | LIBS = -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 \ | |||
| 53 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 53 | HEADERS = 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 | ||
| 58 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 58 | IMGUI_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 \ | |||
| 61 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 61 | NODE_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 | ||
| 66 | GUI_OBJS = obj/gui.o obj/main_gui.o | 67 | GUI_OBJS = obj/gui.o obj/main_gui.o |
| 67 | 68 | ||
| @@ -116,7 +117,7 @@ bitflash: $(IMGUI_OBJS) $(NODE_OBJS) $(GUI_OBJS) | |||
| 116 | 117 | ||
| 117 | NODE_LIBS = -L$(SECP_LIB) -lsecp256k1 \ | 118 | NODE_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 | ||
| 121 | NODE_ONLY_OBJS = $(patsubst obj/%,obj-node/%,$(NODE_OBJS)) \ | 122 | NODE_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 |
| @@ -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. |
| 54 | string strWalletLoadError; | 55 | string strWalletLoadError; |
| 55 | DbEnv dbenv(0u); | 56 | static DbEnv* pdbenv = NULL; |
| 57 | static FILE* pdbErrFile = NULL; | ||
| 56 | static map<string, int> mapFileUseCount; | 58 | static map<string, int> mapFileUseCount; |
| 59 | static CWalletDBSQLite* pWalletSQLiteRuntime = NULL; | ||
| 60 | |||
| 61 | DbEnv& GetDbEnv() | ||
| 62 | { | ||
| 63 | if (!pdbenv) | ||
| 64 | pdbenv = new DbEnv(0u); | ||
| 65 | return *pdbenv; | ||
| 66 | } | ||
| 67 | |||
| 68 | static 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 | ||
| 58 | class CDBInit | 85 | class 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 | } |
| 74 | instance_of_cdbinit; | 97 | instance_of_cdbinit; |
| 75 | 98 | ||
| 99 | bool WalletSQLiteRuntimeActive() | ||
| 100 | { | ||
| 101 | return pWalletSQLiteRuntime != NULL; | ||
| 102 | } | ||
| 103 | |||
| 104 | bool 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 | |||
| 126 | void 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 | |||
| 142 | bool 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 | |||
| 151 | bool 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 | |||
| 162 | bool 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. | ||
| 174 | bool 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 | |||
| 185 | bool 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 | |||
| 196 | void 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. | ||
| 208 | bool 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 | |||
| 226 | bool 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. | ||
| 239 | static string WalletBackendMarkerPath() | ||
| 240 | { | ||
| 241 | return GetAppDir() + "/wallet-backend"; | ||
| 242 | } | ||
| 243 | |||
| 244 | string 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 | |||
| 262 | bool 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 | ||
| 77 | CDB::CDB(const char* pszFile, const char* pszMode, bool fTxn) : pdb(NULL) | 278 | CDB::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 | ||
| 825 | CWalletDB::CWalletDB(const char* pszMode, bool fTxn) : | ||
| 826 | CDB(WalletSQLiteRuntimeActive() ? NULL : "wallet.dat", pszMode, fTxn) | ||
| 827 | { | ||
| 828 | } | ||
| 829 | |||
| 830 | class CSQLiteWalletRuntimeLoadVisitor : public CWalletRecordVisitor | ||
| 831 | { | ||
| 832 | public: | ||
| 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 | |||
| 1003 | static 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 | |||
| 1039 | static 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 | |||
| 617 | bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | 1113 | bool 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 | ||
| 1423 | bool 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 | |||
| 927 | bool LoadWallet() | 1472 | bool 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 | |||
| 1481 | bool 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. | ||
| 1553 | bool 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. | ||
| 1655 | bool 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. | ||
| 1713 | static 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 | |||
| 977 | bool BackupWallet(const string& strDest) | 1774 | bool 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 | |||
| 2142 | static 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 | |||
| 2155 | class CWalletPublicRecordCopyVisitor : public CWalletRecordVisitor | ||
| 2156 | { | ||
| 2157 | public: | ||
| 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 | ||
| 1374 | static string WalletEncryptBackupPath() | 2193 | static 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 | |||
| 2202 | static 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. | ||
| 2237 | class CWalletPublicRecordCopyToSQLiteVisitor : public CWalletRecordVisitor | ||
| 2238 | { | ||
| 2239 | public: | ||
| 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. | ||
| 2292 | static 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 | |||
| 1406 | bool EncryptWallet(const string& strPassphrase, string& strBackupRet, string& strErrorRet) | 2420 | bool 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()); |
| @@ -46,7 +46,7 @@ public: | |||
| 46 | }; | 46 | }; |
| 47 | 47 | ||
| 48 | 48 | ||
| 49 | extern DbEnv dbenv; | 49 | extern DbEnv& GetDbEnv(); |
| 50 | extern void DBFlush(bool fShutdown); | 50 | extern 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 | ||
| 336 | bool WalletSQLiteRuntimeActive(); | ||
| 337 | bool WalletSQLiteRuntimeOpen(const string& strPath, string& strErrorRet, bool fMustExist=true); | ||
| 338 | void WalletSQLiteRuntimeClose(); | ||
| 339 | bool WalletSQLiteRuntimeReadRecord(const vector<unsigned char>& vchKey, | ||
| 340 | vector<unsigned char>& vchValueRet); | ||
| 341 | bool WalletSQLiteRuntimeWriteRecord(const vector<unsigned char>& vchKey, | ||
| 342 | const vector<unsigned char>& vchValue, | ||
| 343 | bool fOverwrite); | ||
| 344 | bool WalletSQLiteRuntimeEraseRecord(const vector<unsigned char>& vchKey); | ||
| 345 | bool WalletSQLiteRuntimeBeginTxn(); | ||
| 346 | bool WalletSQLiteRuntimeCommitTxn(); | ||
| 347 | void WalletSQLiteRuntimeRollbackTxn(); | ||
| 348 | bool WalletSQLiteRuntimeOpenReadOnly(const string& strPath, string& strErrorRet); | ||
| 349 | bool WalletSQLiteRuntimeCheckpoint(string& strErrorRet); | ||
| 350 | string ReadWalletBackendMarker(); | ||
| 351 | bool WriteWalletBackendMarker(const string& strBackend); | ||
| 352 | bool CreateNewSQLiteWallet(const string& strPath); | ||
| 353 | |||
| 336 | 354 | ||
| 337 | 355 | ||
| 338 | 356 | ||
| @@ -340,64 +358,141 @@ private: | |||
| 340 | class CWalletDB : public CDB | 358 | class CWalletDB : public CDB |
| 341 | { | 359 | { |
| 342 | public: | 360 | public: |
| 343 | CWalletDB(const char* pszMode="r+", bool fTxn=false) : CDB("wallet.dat", pszMode, fTxn) { } | 361 | CWalletDB(const char* pszMode="r+", bool fTxn=false); |
| 344 | private: | 362 | private: |
| 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 | } | ||
| 347 | public: | 442 | public: |
| 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 | ||
| 612 | class CWalletRecordVisitor | ||
| 613 | { | ||
| 614 | public: | ||
| 615 | virtual ~CWalletRecordVisitor() { } | ||
| 616 | virtual bool VisitWalletRecord(const CDataStream& ssKey, | ||
| 617 | const CDataStream& ssValue, | ||
| 618 | string& strErrorRet) = 0; | ||
| 619 | }; | ||
| 620 | |||
| 621 | bool ScanWalletRecords(CWalletRecordVisitor& visitor, string& strErrorRet); | ||
| 622 | |||
| 504 | bool LoadWallet(); | 623 | bool LoadWallet(); |
| 624 | bool LoadWalletFromSQLite(const string& strPath); | ||
| 625 | bool 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 | ||
| 173 | static void SetDefaultBackupPath() | 178 | static 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 | ||
| 1381 | static 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 | |||
| 1372 | static void DrawAboutDialog() | 1392 | static 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. | ||
| 1431 | static bool g_showBackendWizard = false; | ||
| 1432 | static std::string g_wizardStatus; | ||
| 1433 | static bool g_wizardConverted = false; | ||
| 1434 | static int g_restartArgc = 0; | ||
| 1435 | static 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. | ||
| 1441 | static 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. | ||
| 1464 | static 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 | |||
| 1527 | static 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; |
| @@ -306,6 +306,7 @@ int64 GetBalance(); | |||
| 306 | struct WalletRecoveryAudit | 306 | struct 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 | ||
| 25 | LIBS = -L$(DEPS)/lib -lsecp256k1 \ | 25 | LIBS = -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 \ | |||
| 37 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 37 | NODE_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 | ||
| 42 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | 43 | GUI_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 | |||
| 51 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 52 | HEADERS = 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 | ||
| 56 | all: bitflash.exe | 57 | all: 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()); |
| @@ -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. |
| 110 | static const unsigned int MAX_CONNECTIONS = 125; | 110 | static 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. | ||
| 127 | static 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 | ||
| 17 | extern int RunPoolStratumSelfTest(); | 21 | extern int RunPoolStratumSelfTest(); |
| 18 | extern int RunPayoutLedgerSelfTest(); | 22 | extern 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 | ||
| 70 | static vector<unsigned char> DataStreamBytes(const CDataStream& ss) | ||
| 71 | { | ||
| 72 | return vector<unsigned char>(ss.begin(), ss.end()); | ||
| 73 | } | ||
| 74 | |||
| 65 | static bool MakeTempDir(std::string& out) | 75 | static 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 | ||
| 749 | static bool FileContainsText(const string& strPath, const string& strNeedle); | ||
| 750 | static bool MakeDirLocal(const string& strPath); | ||
| 751 | static int RunBitflashChild(const string& strExe, | ||
| 752 | const vector<string>& vArgs, | ||
| 753 | const string* pStdinFile, | ||
| 754 | const string* pOutputFile); | ||
| 755 | |||
| 756 | class CWalletStorageSanityRawDB : public CWalletDB | ||
| 757 | { | ||
| 758 | public: | ||
| 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 | |||
| 796 | static const char* SELFTEST_MUTATE_WALLET_MARKER = | ||
| 797 | ".bitflash-selftest-wallet-mutate-ok"; | ||
| 798 | static const char* SELFTEST_MUTATE_WALLET_MARKER_TEXT = | ||
| 799 | "bitflash destructive wallet storage self-test\n"; | ||
| 800 | |||
| 801 | static string SelfTestMutationMarkerPath() | ||
| 802 | { | ||
| 803 | return GetAppDir() + "/" + SELFTEST_MUTATE_WALLET_MARKER; | ||
| 804 | } | ||
| 805 | |||
| 806 | static 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 | |||
| 820 | int 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 | |||
| 863 | static 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 | |||
| 982 | static 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 | |||
| 1044 | static 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 | |||
| 1331 | namespace { | ||
| 1332 | |||
| 1333 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | ||
| 1334 | { | ||
| 1335 | public: | ||
| 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 | |||
| 1359 | static 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 | |||
| 1392 | static 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 | |||
| 1414 | static 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 | |||
| 1444 | static 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 | |||
| 739 | static void ClearWalletRuntimeForTest() | 1760 | static 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 | ||
| 1950 | static 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 | |||
| 1959 | static 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 | |||
| 1990 | static 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 | |||
| 929 | static string QuoteCommandArg(const string& str) | 2034 | static 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 | ||
| 943 | static int RunBitflashChild(const string& strExe, | 2048 | static 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. | ||
| 2309 | static 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. | ||
| 2553 | static 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. | ||
| 2681 | static 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 | |||
| 2781 | static 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 | |||
| 1135 | static int RunNetMessageSelfTest() | 3019 | static 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 | ||
| 9 | int RunSelfTest(const std::string& name); | 9 | int RunSelfTest(const std::string& name); |
| 10 | int 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 | |||
| 7 | static 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 | |||
| 16 | CWalletDBSQLite::CWalletDBSQLite() : pdb(NULL) | ||
| 17 | { | ||
| 18 | } | ||
| 19 | |||
| 20 | CWalletDBSQLite::~CWalletDBSQLite() | ||
| 21 | { | ||
| 22 | Close(); | ||
| 23 | } | ||
| 24 | |||
| 25 | bool 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 | |||
| 61 | bool 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 | |||
| 80 | void CWalletDBSQLite::Close() | ||
| 81 | { | ||
| 82 | if (pdb) | ||
| 83 | { | ||
| 84 | sqlite3_close(pdb); | ||
| 85 | pdb = NULL; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | bool 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 | |||
| 99 | bool 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 | |||
| 109 | bool 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 | |||
| 119 | bool 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 | |||
| 129 | bool 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 | |||
| 147 | bool 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 | |||
| 157 | bool 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 | |||
| 201 | bool 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 | |||
| 255 | bool 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 | |||
| 290 | bool 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 | |||
| 344 | bool 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 | |||
| 9 | class CWalletRecordVisitor; | ||
| 10 | |||
| 11 | class CWalletDBSQLite | ||
| 12 | { | ||
| 13 | public: | ||
| 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 | |||
| 110 | private: | ||
| 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 | ||
| 474 | struct 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 | |||
| 500 | static 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 | |||
| 509 | static 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 | |||
| 554 | static 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 | |||
| 596 | static 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 | |||
| 629 | static 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 | |||
| 649 | static void AddWalletStorageFailure(std::vector<std::string>& vFailures, | ||
| 650 | const std::string& strFailure) | ||
| 651 | { | ||
| 652 | vFailures.push_back(strFailure); | ||
| 653 | } | ||
| 654 | |||
| 655 | static 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 | |||
| 692 | int 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 | |||
| 735 | int 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 | |||
| 794 | static vector<unsigned char> DataStreamBytesLocal(const CDataStream& ss) | ||
| 795 | { | ||
| 796 | return vector<unsigned char>(ss.begin(), ss.end()); | ||
| 797 | } | ||
| 798 | |||
| 799 | namespace { | ||
| 800 | |||
| 801 | static 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 | |||
| 808 | static void RemoveFailedRestoreWallet(const string& strWalletPath) | ||
| 809 | { | ||
| 810 | DBFlush(true); | ||
| 811 | remove(strWalletPath.c_str()); | ||
| 812 | } | ||
| 813 | |||
| 814 | class CWalletSQLiteExportVisitor : public CWalletRecordVisitor | ||
| 815 | { | ||
| 816 | public: | ||
| 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 | |||
| 836 | class CWalletRecordMapVisitor : public CWalletRecordVisitor | ||
| 837 | { | ||
| 838 | public: | ||
| 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 | |||
| 860 | class CWalletRawRestoreDB : public CWalletDB | ||
| 861 | { | ||
| 862 | public: | ||
| 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 | |||
| 883 | class CWalletSQLiteRestoreVisitor : public CWalletRecordVisitor | ||
| 884 | { | ||
| 885 | public: | ||
| 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 | |||
| 906 | class CWalletSQLiteLoadCheckVisitor : public CWalletRecordVisitor | ||
| 907 | { | ||
| 908 | public: | ||
| 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. | ||
| 1078 | bool 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. | ||
| 1145 | bool 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 | |||
| 1169 | int 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 | |||
| 1202 | int 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 | |||
| 1281 | int 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 | |||
| 1367 | int 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 | |||
| 459 | int CmdEncryptWallet(const std::string& strPassphrase) | 1411 | int 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. |
| 63 | int CmdRecoveryAudit(); | 63 | int CmdRecoveryAudit(); |
| 64 | 64 | ||
| 65 | // Diagnostic: count wallet.dat record types without printing keys, | ||
| 66 | // addresses, transaction ids, labels, or other wallet values. | ||
| 67 | int 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. | ||
| 71 | int 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. | ||
| 76 | int 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. | ||
| 80 | bool 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. | ||
| 85 | bool 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. | ||
| 89 | int 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. | ||
| 93 | int 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. | ||
| 97 | int 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. |
| 67 | int CmdEncryptWallet(const std::string& strPassphrase); | 101 | int CmdEncryptWallet(const std::string& strPassphrase); |