upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/selftest.cpp
diff options
context:
space:
mode:
authorbitflash <dev@bitflash.network>2026-08-18 14:23:38 -0300
committerbitflash <dev@bitflash.network>2026-08-18 14:23:38 -0300
commite58c4022e0ddd137b93cdb3464753bef5df1d456 (patch)
tree8adc9ec855d19e5a7308eb2a1b2be8139db47188 /src/selftest.cpp
parentbeb5c1435a0d85b7b24d4d371933758ebed90907 (diff)
wallet: encrypt atomically under the SQLite backend
EncryptWallet() had only a Berkeley DB path: it rebuilt a wallet.dat temp file and swapped it in. Under -walletbackend=sqlite that was wrong -- it would leave the live wallet.sqlite unencrypted and write a divergent wallet.dat instead. Add a SQLite persistence branch that rebuilds a fresh wallet.sqlite in one transaction (public records copied from the live store, encrypted key material serialized byte-for-byte the way the BDB path writes it), checkpoints it, and swaps it in with a .before-encrypt backup, mirroring the BDB rename dance. A fresh file rather than an in-place rewrite keeps the change atomic and leaves no plaintext key in a freed page. CmdEncryptWallet skips PurgeDbEnvironmentLogs under SQLite (no Berkeley DB environment behind the wallet) and reports the backend it used. New wallet-sqlite-encrypt self-test drives the real command path in child processes: export a wallet to SQLite, encrypt it through -walletbackend=sqlite, and prove it comes out encrypted, unlockable with the right passphrase, refusing the wrong one, with its recovery phrase intact and the original wallet.dat left untouched.
Diffstat (limited to 'src/selftest.cpp')
-rw-r--r--src/selftest.cpp189
1 files changed, 188 insertions, 1 deletions
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 098eed6..fc10947 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -2252,6 +2252,191 @@ static int RunWalletEncryptSelfTest()
2252 return nFail == 0 ? 0 : 1; 2252 return nFail == 0 ? 0 : 1;
2253} 2253}
2254 2254
2255// Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley
2256// DB encrypt test above, this drives the real command-line path end to end in
2257// child processes: build a Berkeley DB wallet, export it to SQLite, then run
2258// -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out
2259// encrypted, unlockable, and with wallet.dat left untouched.
2260static int RunWalletSQLiteEncryptSelfTest()
2261{
2262 fflush(stdout);
2263 printf("wallet-sqlite-encrypt self-test\n");
2264
2265 std::string tmp;
2266 std::string cwd;
2267 if (!MakeTempDir(tmp))
2268 {
2269 printf(" FAIL could not create a temporary data directory\n");
2270 return 1;
2271 }
2272 if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp))
2273 {
2274 printf(" FAIL could not move into the temporary data directory\n");
2275 RemoveTree(tmp);
2276 return 1;
2277 }
2278
2279 int nFail = 0;
2280 strSetDataDir = tmp;
2281 printf(" temp datadir: %s\n", tmp.c_str());
2282
2283 try
2284 {
2285 // Build a normal Berkeley DB wallet with a recovery phrase, a key pool,
2286 // and a couple of public records, then release it. Everything after this
2287 // runs the real command-line paths in child processes.
2288 if (!LoadWallet())
2289 throw std::runtime_error("LoadWallet failed");
2290
2291 string strError;
2292 const string strMnemonic =
2293 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
2294 nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError),
2295 "a phrase can be installed before encryption") ? 0 : 1;
2296 TopUpKeyPool();
2297
2298 vector<unsigned char> vchDefaultPubKey = keyUser.GetPubKey();
2299 CKey publicLabelKey;
2300 publicLabelKey.MakeNewKey();
2301 string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey());
2302 nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") &&
2303 CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242),
2304 "public wallet records can be written before encryption") ? 0 : 1;
2305
2306 CPrivKey vchDefaultPrivKey;
2307 nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError),
2308 "the default private key is readable before encryption") ? 0 : 1;
2309 vector<unsigned char> vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end());
2310 vector<unsigned char> vchPlainHDMaster = vchHDMaster;
2311 vector<unsigned char> vchPlainHDChainCode = vchHDChainCode;
2312
2313 // Release wallet.dat so a child process can read it for the export.
2314 DBFlush(true);
2315
2316#ifdef _WIN32
2317 string strExe = cwd + "\\bitflash.exe";
2318#else
2319 string strExe = cwd + "/bitflash-node";
2320#endif
2321 string strSQLitePath = tmp + "/wallet.sqlite";
2322 string strWalletDatPath = tmp + "/wallet.dat";
2323 string strRightPassFile = tmp + "/right-pass.txt";
2324 string strWrongPassFile = tmp + "/wrong-pass.txt";
2325 nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") &&
2326 WriteTextFile(strWrongPassFile, "wrong-passphrase\n"),
2327 "passphrase files can be written for restarted commands") ? 0 : 1;
2328
2329 // Step 1: export the Berkeley DB wallet to SQLite.
2330 vector<string> vExportArgs;
2331 vExportArgs.push_back("-datadir=" + tmp);
2332 vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath);
2333 vExportArgs.push_back("-nogui");
2334 int nExportRet = RunBitflashChild(strExe, vExportArgs);
2335 nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()),
2336 "the wallet exports to a SQLite file") ? 0 : 1;
2337 nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes),
2338 "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1;
2339
2340 // Step 2: encrypt through the real SQLite backend command path.
2341 string strEncryptOut = tmp + "/sqlite-encrypt-out.txt";
2342 vector<string> vEncryptArgs;
2343 vEncryptArgs.push_back("-datadir=" + tmp);
2344 vEncryptArgs.push_back("-walletbackend=sqlite");
2345 vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile);
2346 vEncryptArgs.push_back("-nogui");
2347 int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut);
2348 nFail += Check(nEncryptRet == 0,
2349 "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1;
2350 nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"),
2351 "encryption took the SQLite persistence path") ? 0 : 1;
2352 nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"),
2353 "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1;
2354
2355 // The encrypted SQLite wallet must no longer hold the plaintext secrets.
2356 nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes),
2357 "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1;
2358 nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster),
2359 "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1;
2360 nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode),
2361 "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1;
2362
2363 // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path
2364 // must never touch it, so its plaintext key is still exactly there.
2365 nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes),
2366 "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1;
2367
2368 // Step 3: a locked encrypted SQLite wallet reports that phrase coverage
2369 // needs an unlock, exactly as the Berkeley DB path does (exit code 2).
2370 string strLockedAudit = tmp + "/locked-recovery-audit.txt";
2371 vector<string> vLockedArgs;
2372 vLockedArgs.push_back("-datadir=" + tmp);
2373 vLockedArgs.push_back("-walletbackend=sqlite");
2374 vLockedArgs.push_back("-recoveryaudit");
2375 vLockedArgs.push_back("-nogui");
2376 int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit);
2377 nFail += Check(nLockedRet == 2 &&
2378 FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"),
2379 "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1;
2380
2381 // The wrong passphrase is rejected and reveals nothing.
2382 string strWrongDump = tmp + "/wrong-dump.txt";
2383 vector<string> vWrongArgs;
2384 vWrongArgs.push_back("-datadir=" + tmp);
2385 vWrongArgs.push_back("-walletbackend=sqlite");
2386 vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile);
2387 vWrongArgs.push_back("-dumpwallet=" + strWrongDump);
2388 vWrongArgs.push_back("-nogui");
2389 int nWrongRet = RunBitflashChild(strExe, vWrongArgs);
2390 nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()),
2391 "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1;
2392
2393 // The right passphrase unlocks and can dump the original key.
2394 string strRightDump = tmp + "/right-dump.txt";
2395 vector<string> vRightArgs;
2396 vRightArgs.push_back("-datadir=" + tmp);
2397 vRightArgs.push_back("-walletbackend=sqlite");
2398 vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile);
2399 vRightArgs.push_back("-dumpwallet=" + strRightDump);
2400 vRightArgs.push_back("-nogui");
2401 int nRightRet = RunBitflashChild(strExe, vRightArgs);
2402 nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()),
2403 "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1;
2404 nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)),
2405 "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1;
2406
2407 // The recovery phrase survives the encryption.
2408 string strAudit = tmp + "/recovery-audit.txt";
2409 vector<string> vAuditArgs;
2410 vAuditArgs.push_back("-datadir=" + tmp);
2411 vAuditArgs.push_back("-walletbackend=sqlite");
2412 vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile);
2413 vAuditArgs.push_back("-recoveryaudit");
2414 vAuditArgs.push_back("-nogui");
2415 int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit);
2416 nFail += Check(nAuditRet == 0 &&
2417 FileContainsText(strAudit, "recovery phrase: present"),
2418 "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1;
2419 }
2420 catch (const std::exception& e)
2421 {
2422 printf(" FAIL exception: %s\n", e.what());
2423 nFail++;
2424 }
2425 catch (...)
2426 {
2427 printf(" FAIL unknown exception\n");
2428 nFail++;
2429 }
2430
2431 DBFlush(true);
2432 SetCurrentDir(cwd);
2433 RemoveTree(tmp);
2434 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
2435 nFail, nFail == 1 ? "" : "s");
2436 fflush(stdout);
2437 return nFail == 0 ? 0 : 1;
2438}
2439
2255static int RunWalletPortabilitySelfTest() 2440static int RunWalletPortabilitySelfTest()
2256{ 2441{
2257 fflush(stdout); 2442 fflush(stdout);
@@ -2981,6 +3166,8 @@ int RunSelfTest(const std::string& name)
2981 return RunWalletCryptoSelfTest(); 3166 return RunWalletCryptoSelfTest();
2982 if (name == "wallet-encrypt") 3167 if (name == "wallet-encrypt")
2983 return RunWalletEncryptSelfTest(); 3168 return RunWalletEncryptSelfTest();
3169 if (name == "wallet-sqlite-encrypt")
3170 return RunWalletSQLiteEncryptSelfTest();
2984 if (name == "wallet-portability") 3171 if (name == "wallet-portability")
2985 return RunWalletPortabilitySelfTest(); 3172 return RunWalletPortabilitySelfTest();
2986 if (name == "net-message") 3173 if (name == "net-message")
@@ -2997,6 +3184,6 @@ int RunSelfTest(const std::string& name)
2997 return RunManagedTorSelfTest(); 3184 return RunManagedTorSelfTest();
2998 3185
2999 printf("Unknown self-test '%s'\n", name.c_str()); 3186 printf("Unknown self-test '%s'\n", name.c_str());
3000 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-storage-sanity, db-env-reopen, wallet-sqlite, wallet-sqlite-migration, wallet-crypto, wallet-encrypt, wallet-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); 3187 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-portability, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n");
3001 return 1; 3188 return 1;
3002} 3189}