// Copyright (c) 2026 Bitflash developers // Distributed under the MIT/X11 software license. // // Integration self-tests that need the real wallet/database code linked into // the node. These are intentionally run behind an explicit command-line flag // and against a temporary data directory. #include "headers_core.h" #include "btfaddr.h" #include "bip32.h" #include "proxy.h" #include "selftest.h" #ifndef _WIN32 #include // WIFEXITED/WEXITSTATUS for the child-process helper #endif #include "sockcount.h" #include "tor.h" #include "walletcmd.h" #include "wallet_sqlite.h" extern int RunPoolStratumSelfTest(); extern int RunPayoutLedgerSelfTest(); // Test results go to the terminal, not to debug.log. // // printf in this tree is OutputDebugStringF, which writes into the data // directory's debug.log and binds that path, once, on its first call. For a // self-test that produced two bad outcomes at the same time. On Windows the // binary is linked -mwindows and has no console, so `make tests` printed the // bip32 results, echoed the self-test command, and then showed nothing at all -- // a failure was still caught, because a non-zero return stops make, but it // arrived with no way to tell which check failed. And because the first line // printed before strSetDataDir was pointed at the temporary directory, every // line landed in the developer's real debug.log: the same file users are asked // to paste into issues. // // Undefining the macro here gives this file the real printf, so results reach // stdout. Nothing else in the tree is affected. #undef printf #include #include #include #include #ifdef _WIN32 #include #include #include #else #include #include #include #include #endif static std::string HexStrLocal(const std::vector& v) { static const char* hex = "0123456789abcdef"; std::string out; out.reserve(v.size() * 2); for (unsigned char c : v) { out.push_back(hex[c >> 4]); out.push_back(hex[c & 0x0f]); } return out; } static vector DataStreamBytes(const CDataStream& ss) { return vector(ss.begin(), ss.end()); } static bool MakeTempDir(std::string& out) { #ifdef _WIN32 char base[MAX_PATH + 1] = {0}; if (!GetTempPathA(MAX_PATH, base)) return false; for (int i = 0; i < 100; i++) { std::string path = strprintf("%sbitflash-selftest-%lu-%lld-%d", base, (unsigned long)GetCurrentProcessId(), (long long)GetTime(), i); if (CreateDirectoryA(path.c_str(), NULL)) { out = path; return true; } } return false; #else char tmpl[256]; snprintf(tmpl, sizeof(tmpl), "/tmp/bitflash-selftest-%ld-XXXXXX", (long)getpid()); char* p = mkdtemp(tmpl); if (!p) return false; out = p; return true; #endif } static bool GetCurrentDir(std::string& out) { #ifdef _WIN32 char buf[MAX_PATH + 1] = {0}; DWORD n = GetCurrentDirectoryA(MAX_PATH, buf); if (n == 0 || n > MAX_PATH) return false; out = buf; return true; #else char buf[4096]; if (!getcwd(buf, sizeof(buf))) return false; out = buf; return true; #endif } static bool SetCurrentDir(const std::string& path) { #ifdef _WIN32 return SetCurrentDirectoryA(path.c_str()) != 0; #else return chdir(path.c_str()) == 0; #endif } static void RemoveTree(const std::string& path) { #ifdef _WIN32 DWORD attr = GetFileAttributesA(path.c_str()); if (attr == INVALID_FILE_ATTRIBUTES) return; if (!(attr & FILE_ATTRIBUTE_DIRECTORY)) { DeleteFileA(path.c_str()); return; } WIN32_FIND_DATAA findData; std::string pattern = path + "\\*"; HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); if (hFind != INVALID_HANDLE_VALUE) { do { std::string name = findData.cFileName; if (name == "." || name == "..") continue; RemoveTree(path + "\\" + name); } while (FindNextFileA(hFind, &findData)); FindClose(hFind); } RemoveDirectoryA(path.c_str()); #else struct stat st; if (lstat(path.c_str(), &st) != 0) return; if (!S_ISDIR(st.st_mode)) { unlink(path.c_str()); return; } DIR* dir = opendir(path.c_str()); if (dir) { struct dirent* ent; while ((ent = readdir(dir)) != NULL) { std::string name = ent->d_name; if (name == "." || name == "..") continue; RemoveTree(path + "/" + name); } closedir(dir); } rmdir(path.c_str()); #endif } static bool WalletHasPrivateKey(const std::vector& vchPubKey) { CPrivKey priv; return CWalletDB("r").ReadKey(vchPubKey, priv) && !priv.empty(); } static bool Check(bool condition, const char* message) { if (condition) { printf(" ok %s\n", message); return true; } printf(" FAIL %s\n", message); return false; } static int RunWalletKeyPoolSelfTest() { fflush(stdout); printf("wallet-keypool self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; // Before anything can open the database or resolve the log path: what the // node itself prints during the test belongs in the temporary directory, // and dies with it. The test's own results are on stdout, above. strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); try { if (!LoadWallet()) throw std::runtime_error("LoadWallet failed"); TopUpKeyPool(); int nInitialPoolSize = 0; CRITICAL_BLOCK(cs_keyPool) nInitialPoolSize = (int)mapKeyPool.size(); nFail += Check(nInitialPoolSize == KEYPOOL_SIZE, "TopUpKeyPool fills the pool") ? 0 : 1; bool fPooledKeysStored = true; CRITICAL_BLOCK(cs_keyPool) { for (map >::const_iterator mi = mapKeyPool.begin(); mi != mapKeyPool.end(); ++mi) if (!WalletHasPrivateKey(mi->second)) fPooledKeysStored = false; } nFail += Check(fPooledKeysStored, "every pooled public key has a stored private key") ? 0 : 1; std::vector > drawn; std::mutex drawnMutex; const int nThreads = 8; const int nDrawsPerThread = 32; std::vector threads; for (int t = 0; t < nThreads; t++) { threads.emplace_back([&drawn, &drawnMutex, nDrawsPerThread]() { for (int i = 0; i < nDrawsPerThread; i++) { std::vector key = GetKeyFromPool(); std::lock_guard lock(drawnMutex); drawn.push_back(key); } }); } for (std::thread& t : threads) t.join(); nFail += Check((int)drawn.size() == nThreads * nDrawsPerThread, "all concurrent draws returned a key") ? 0 : 1; std::set seen; bool fUnique = true; bool fAllStored = true; for (const std::vector& key : drawn) { if (!seen.insert(HexStrLocal(key)).second) fUnique = false; if (!WalletHasPrivateKey(key)) fAllStored = false; } nFail += Check(fUnique, "concurrent draws never return the same key") ? 0 : 1; nFail += Check(fAllStored, "each drawn key was already stored in wallet.dat") ? 0 : 1; int nPoolSize = 0; CRITICAL_BLOCK(cs_keyPool) nPoolSize = (int)mapKeyPool.size(); nFail += Check(nPoolSize == KEYPOOL_SIZE, "drawing leaves the key pool topped up") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } // AttachTerminal() moved to util.h: the wallet phrase commands need it too, // and two copies of a console-attach that must get its one condition right is // one copy too many. static int RunWalletHDSelfTest() { fflush(stdout); printf("wallet-hd self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); // Test-vector phrases, so nothing here depends on randomness. const std::string strPhraseA = "abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon about"; const std::string strPhraseB = "legal winner thank year wave sausage worth useful " "legal winner thank yellow"; try { if (!LoadWallet()) throw std::runtime_error("LoadWallet failed"); std::string strError; TopUpKeyPool(); int nRandomPoolSize = 0; CRITICAL_BLOCK(cs_keyPool) nRandomPoolSize = (int)mapKeyPool.size(); nFail += Check(nRandomPoolSize == KEYPOOL_SIZE, "a wallet can start with a random key pool") ? 0 : 1; // The address the wallet was showing before any of this. It has to // survive under a name that says it is not covered by the phrase. std::vector vchPreSeedKey = keyUser.GetPubKey(); std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey()); nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), "a valid phrase installs a seed") ? 0 : 1; nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; nFail += Check(nHDKeySchema == HD_SCHEMA_BIP44, "a new recovery phrase records the BIP44 HD schema") ? 0 : 1; nFail += Check(nHDCoinType == HD_BIP44_COIN_TYPE_BITFLASH, "a new recovery phrase records the SLIP-0044 BIP44 coin type") ? 0 : 1; nFail += Check(nHDReceiveNext == 1 && nHDChangeNext == 0, "BIP44 receive/change counters reserve the default receive key") ? 0 : 1; std::vector vBIP44Path = HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH, HD_BIP44_ACCOUNT, HD_BIP44_CHAIN_RECEIVE, 0); nFail += Check(vBIP44Path.size() == 5 && vBIP44Path[0] == (HD_BIP44_PURPOSE | bitflash::BIP32_HARDENED) && vBIP44Path[1] == (HD_BIP44_COIN_TYPE_BITFLASH | bitflash::BIP32_HARDENED) && vBIP44Path[2] == (HD_BIP44_ACCOUNT | bitflash::BIP32_HARDENED) && vBIP44Path[3] == HD_BIP44_CHAIN_RECEIVE && vBIP44Path[4] == 0, "the BIP44 helper builds m/44'/coin_type'/account'/change/index") ? 0 : 1; int nPoolAfterSeed = 0; CRITICAL_BLOCK(cs_keyPool) nPoolAfterSeed = (int)mapKeyPool.size(); nFail += Check(nPoolAfterSeed == 0, "installing a phrase clears the old random key pool") ? 0 : 1; nFail += Check(nHDNext == 0, "installing a BIP44 phrase leaves the legacy counter unused") ? 0 : 1; CKey keyFirstDerived; if (!DeriveHDKey(0, keyFirstDerived, strError)) throw std::runtime_error("default derivation failed: " + strError); bitflash::BIP32PrivateNode hdParent; hdParent.privateKey = vchHDMaster; hdParent.chainCode = vchHDChainCode; bitflash::BIP32PrivateNode hdBIP44ReceiveChild; if (!bitflash::BIP32DerivePath(hdParent, HDBIP44Path(HD_BIP44_COIN_TYPE_BITFLASH, HD_BIP44_ACCOUNT, HD_BIP44_CHAIN_RECEIVE, 0), hdBIP44ReceiveChild, strError)) throw std::runtime_error("BIP44 receive path derivation failed: " + strError); CKey keyBIP44ReceivePath; if (!keyBIP44ReceivePath.SetSecret(hdBIP44ReceiveChild.privateKey)) throw std::runtime_error("BIP44 receive path produced an unusable key"); nFail += Check(keyBIP44ReceivePath.GetPubKey() == keyFirstDerived.GetPubKey(), "the default HD key path is m/44'/coin_type'/0'/0/0") ? 0 : 1; bitflash::BIP32PrivateNode hdLegacyChild; int nSchemaForLegacyCheck = nHDKeySchema; nHDKeySchema = HD_SCHEMA_LEGACY; CKey keyLegacyDerived; if (!DeriveHDKey(0, keyLegacyDerived, strError)) throw std::runtime_error("legacy derivation failed: " + strError); nHDKeySchema = nSchemaForLegacyCheck; if (!bitflash::BIP32DerivePath(hdParent, HDLegacyPath(0), hdLegacyChild, strError)) throw std::runtime_error("legacy path derivation failed: " + strError); CKey keyLegacyPath; if (!keyLegacyPath.SetSecret(hdLegacyChild.privateKey)) throw std::runtime_error("legacy path produced an unusable key"); nFail += Check(keyLegacyPath.GetPubKey() == keyLegacyDerived.GetPubKey(), "the legacy HD key path remains m/index'") ? 0 : 1; std::vector vchDefaultKey; bool fDefaultRead = CWalletDB("r").ReadDefaultKey(vchDefaultKey); nFail += Check(fDefaultRead && vchDefaultKey == keyFirstDerived.GetPubKey(), "the default receiving key is derived from the phrase") ? 0 : 1; nFail += Check(WalletHasPrivateKey(vchDefaultKey), "the derived default key is stored in wallet.dat") ? 0 : 1; std::string strNewAddr = PubKeyToAddress(vchDefaultKey); nFail += Check(strPreSeedAddr != strNewAddr, "the visible address stops being the pre-seed one") ? 0 : 1; nFail += Check(mapAddressBook.count(strPreSeedAddr) > 0 && mapAddressBook[strPreSeedAddr] != mapAddressBook[strNewAddr], "the pre-seed address is kept, named apart from the new one") ? 0 : 1; std::vector vchBefore = vchHDMaster; int nSchemaBefore = nHDKeySchema; unsigned int nCoinTypeBefore = nHDCoinType; nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), "an invalid phrase is refused") ? 0 : 1; nFail += Check(vchHDMaster == vchBefore, "a refused phrase leaves the existing seed alone") ? 0 : 1; nFail += Check(nHDKeySchema == nSchemaBefore, "a refused phrase leaves the derivation schema alone") ? 0 : 1; nFail += Check(nHDCoinType == nCoinTypeBefore, "a refused phrase leaves the BIP44 coin type alone") ? 0 : 1; // The property the whole feature exists for: the same words give back // the same keys, in the same order. std::vector first; for (unsigned int i = 0; i < 5; i++) { CKey key; if (!DeriveHDKey(i, key, strError)) throw std::runtime_error("derivation failed: " + strError); first.push_back(HexStrLocal(key.GetPubKey())); } SetHDSeedFromMnemonic(strPhraseA, strError); std::vector again; for (unsigned int i = 0; i < 5; i++) { CKey key; if (!DeriveHDKey(i, key, strError)) throw std::runtime_error("derivation failed: " + strError); again.push_back(HexStrLocal(key.GetPubKey())); } nFail += Check(first == again, "the same phrase derives the same keys in the same order") ? 0 : 1; SetHDSeedFromMnemonic(strPhraseB, strError); std::vector other; for (unsigned int i = 0; i < 5; i++) { CKey key; if (!DeriveHDKey(i, key, strError)) throw std::runtime_error("derivation failed: " + strError); other.push_back(HexStrLocal(key.GetPubKey())); } nFail += Check(first != other, "a different phrase derives different keys") ? 0 : 1; std::set distinct(first.begin(), first.end()); nFail += Check(distinct.size() == first.size(), "consecutive indices give distinct keys") ? 0 : 1; // With a seed installed the pool must be derived from it, and the // counter must move exactly once per key. SetHDSeedFromMnemonic(strPhraseA, strError); unsigned int nReceiveNextBefore = nHDReceiveNext; TopUpKeyPool(); int nPool = 0; CRITICAL_BLOCK(cs_keyPool) nPool = (int)mapKeyPool.size(); nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; nFail += Check(nHDReceiveNext == nReceiveNextBefore + (unsigned int)KEYPOOL_SIZE, "the BIP44 receive counter advances once per pooled key") ? 0 : 1; nFail += Check(nHDChangeNext == 0, "filling the receive pool leaves the BIP44 change counter alone") ? 0 : 1; nFail += Check(!RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 400, 600, 600), "BIP44 restore depth is not satisfied by receive plus legacy alone") ? 0 : 1; nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_BIP44, 600, 600, 600, 600), "BIP44 restore depth is satisfied on each branch") ? 0 : 1; nFail += Check(RestoreScanReachedDepth(HD_SCHEMA_LEGACY, 0, 0, 600, 600), "legacy restore depth still follows the legacy counter") ? 0 : 1; std::set derived; for (unsigned int i = nReceiveNextBefore; i < nHDReceiveNext; i++) { CKey key; if (!DeriveHDKey(i, key, strError)) throw std::runtime_error("derivation failed: " + strError); derived.insert(HexStrLocal(key.GetPubKey())); } bool fStored = true; bool fFromSeed = true; CRITICAL_BLOCK(cs_keyPool) { for (map >::const_iterator mi = mapKeyPool.begin(); mi != mapKeyPool.end(); ++mi) { if (!WalletHasPrivateKey(mi->second)) fStored = false; if (!derived.count(HexStrLocal(mi->second))) fFromSeed = false; } } nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; // A recovery phrase is only useful if the wallet can tell the user // what today's spendable balance would actually come back from it. CKey keyAuditDerived; if (!DeriveHDKey(0, keyAuditDerived, strError)) throw std::runtime_error("audit derivation failed: " + strError); CKey keyAuditChange; if (!DeriveHDChangeKey(0, keyAuditChange, strError)) throw std::runtime_error("audit change derivation failed: " + strError); nFail += Check(keyAuditChange.GetPubKey() != keyAuditDerived.GetPubKey(), "BIP44 receive and change chains derive different keys") ? 0 : 1; if (!AddKey(keyAuditChange)) throw std::runtime_error("could not store the audit change key"); nHDChangeNext = 1; int nSchemaForLegacyAudit = nHDKeySchema; nHDKeySchema = HD_SCHEMA_LEGACY; CKey keyAuditLegacyHD; if (!DeriveHDKey(0, keyAuditLegacyHD, strError)) throw std::runtime_error("audit legacy compatibility derivation failed: " + strError); nHDKeySchema = nSchemaForLegacyAudit; if (!AddKey(keyAuditLegacyHD)) throw std::runtime_error("could not store the audit legacy compatibility key"); nHDNext = 1; CWalletTx wtxLegacy; wtxLegacy.vout.push_back(CTxOut(5 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); CWalletTx wtxDerived; wtxDerived.vout.push_back(CTxOut(7 * COIN, CScript() << keyAuditDerived.GetPubKey() << OP_CHECKSIG)); CWalletTx wtxChange; wtxChange.vout.push_back(CTxOut(13 * COIN, CScript() << keyAuditChange.GetPubKey() << OP_CHECKSIG)); CWalletTx wtxLegacyHD; wtxLegacyHD.vout.push_back(CTxOut(17 * COIN, CScript() << keyAuditLegacyHD.GetPubKey() << OP_CHECKSIG)); CWalletTx wtxImmatureLegacy; wtxImmatureLegacy.vin.push_back(CTxIn()); wtxImmatureLegacy.vout.push_back(CTxOut(11 * COIN, CScript() << vchPreSeedKey << OP_CHECKSIG)); CRITICAL_BLOCK(cs_mapWallet) { mapWallet.clear(); mapWallet[wtxLegacy.GetHash()] = wtxLegacy; mapWallet[wtxDerived.GetHash()] = wtxDerived; mapWallet[wtxChange.GetHash()] = wtxChange; mapWallet[wtxLegacyHD.GetHash()] = wtxLegacyHD; mapWallet[wtxImmatureLegacy.GetHash()] = wtxImmatureLegacy; } WalletRecoveryAudit audit = GetWalletRecoveryAudit(); nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; nFail += Check(audit.nSchema == HD_SCHEMA_BIP44, "the recovery audit reports the derivation schema") ? 0 : 1; nFail += Check(audit.nCoinType == HD_BIP44_COIN_TYPE_BITFLASH, "the recovery audit reports the BIP44 coin type") ? 0 : 1; nFail += Check(audit.nReceiveNext == nHDReceiveNext && audit.nChangeNext == 1, "the recovery audit reports receive/change counters") ? 0 : 1; nFail += Check(audit.nLegacyCredit == 5 * COIN, "the recovery audit finds wallet.dat-only balance") ? 0 : 1; nFail += Check(audit.nRecoverableCredit == 37 * COIN, "the recovery audit finds BIP44 and legacy-HD phrase balance") ? 0 : 1; nFail += Check(audit.nLegacyTx == 1 && audit.nRecoverableTx == 3, "the recovery audit counts wallet.dat-only and phrase-backed transactions") ? 0 : 1; nFail += Check(audit.nLegacyImmatureCredit == 11 * COIN, "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; nFail += Check(audit.nLegacyImmatureTx == 1, "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; CKey keyLegacyHD11; nSchemaForLegacyAudit = nHDKeySchema; nHDKeySchema = HD_SCHEMA_LEGACY; if (!DeriveHDKey(11, keyLegacyHD11, strError)) throw std::runtime_error("legacy compatibility index 11 derivation failed: " + strError); nHDKeySchema = nSchemaForLegacyAudit; if (!AddKey(keyLegacyHD11)) throw std::runtime_error("could not store the legacy compatibility index 11 key"); std::map > mapLegacyProbe; mapLegacyProbe[0] = keyAuditLegacyHD.GetPubKey(); mapLegacyProbe[11] = keyLegacyHD11.GetPubKey(); CWalletTx wtxLegacyHD11; wtxLegacyHD11.vout.push_back(CTxOut(19 * COIN, CScript() << keyLegacyHD11.GetPubKey() << OP_CHECKSIG)); CRITICAL_BLOCK(cs_mapWallet) { mapWallet.clear(); mapWallet[wtxLegacyHD11.GetHash()] = wtxLegacyHD11; } nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 12, "restore collapses legacy hdnext to the last used compatibility index plus one") ? 0 : 1; CRITICAL_BLOCK(cs_mapWallet) mapWallet.clear(); nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 0, "restore does not persist legacy hdnext at the scan depth when no compatibility key was used") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunWalletFormatSelfTest() { fflush(stdout); printf("wallet-format self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); try { class CWalletDBRaw : public CWalletDB { public: CWalletDBRaw(const char* pszMode="r+") : CWalletDB(pszMode) { } bool WriteStringRecord(const string& strType, int nValue) { return Write(strType, nValue); } bool EraseStringRecord(const string& strType) { return Erase(strType); } }; if (!LoadWallet()) throw std::runtime_error("LoadWallet failed"); { CWalletDB walletdb; nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED), "the current wallet format marker can be written") ? 0 : 1; } std::vector vchDefaultKey; nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), "the current wallet format marker loads") ? 0 : 1; { CWalletDBRaw walletdb; nFail += Check(walletdb.WriteStringRecord("mkey", 1), "a malformed encrypted master-key record can be written") ? 0 : 1; } vchDefaultKey.clear(); nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), "a malformed encrypted master-key record is refused") ? 0 : 1; { CWalletDBRaw walletdb; nFail += Check(walletdb.EraseStringRecord("mkey"), "the malformed encrypted master-key record can be removed") ? 0 : 1; } { CWalletDB walletdb; nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_SUPPORTED + 1), "a future wallet format marker can be written") ? 0 : 1; } vchDefaultKey.clear(); nFail += Check(!CWalletDB("r").LoadWallet(vchDefaultKey), "a future wallet format marker is refused") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static bool FileContainsText(const string& strPath, const string& strNeedle); static bool MakeDirLocal(const string& strPath); static int RunBitflashChild(const string& strExe, const vector& vArgs, const string* pStdinFile, const string* pOutputFile); class CWalletStorageSanityRawDB : public CWalletDB { public: CWalletStorageSanityRawDB(const char* pszMode="r+") : CWalletDB(pszMode) { } bool WriteUnknownRecord() { return Write(string("storage-sanity-unknown"), 1); } bool WriteMalformedKeyRecord() { if (!pdb) return false; unsigned char chKey = 0xff; unsigned char chValue = 0x01; Dbt datKey(&chKey, 1); Dbt datValue(&chValue, 1); return pdb->put(GetTxn(), &datKey, &datValue, 0) == 0; } bool MakePlainHDSeedIncomplete() { return Erase(string("hdchaincode")); } bool AddEncryptedMarkerToPlainWallet() { CWalletMasterKey kMasterKey; kMasterKey.vchCryptedKey.resize(48); for (size_t i = 0; i < kMasterKey.vchCryptedKey.size(); i++) kMasterKey.vchCryptedKey[i] = (unsigned char)(0x80 + (i & 0x3f)); for (size_t i = 0; i < kMasterKey.vchSalt.size(); i++) kMasterKey.vchSalt[i] = (unsigned char)(0x40 + i); return WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED) && WriteMasterKey(1, kMasterKey); } }; static const char* SELFTEST_MUTATE_WALLET_MARKER = ".bitflash-selftest-wallet-mutate-ok"; static const char* SELFTEST_MUTATE_WALLET_MARKER_TEXT = "bitflash destructive wallet storage self-test\n"; static string SelfTestMutationMarkerPath() { return GetAppDir() + "/" + SELFTEST_MUTATE_WALLET_MARKER; } static bool WriteSelfTestMutationMarker(const string& strDir) { string strPath = strDir + "/" + SELFTEST_MUTATE_WALLET_MARKER; FILE* pf = fopen(strPath.c_str(), "wb"); if (!pf) return false; bool fOk = fwrite(SELFTEST_MUTATE_WALLET_MARKER_TEXT, 1, strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT), pf) == strlen(SELFTEST_MUTATE_WALLET_MARKER_TEXT); if (fclose(pf) != 0) fOk = false; return fOk; } int RunSelfTestMutateWallet(const std::string& name) { if (!FileContainsText(SelfTestMutationMarkerPath(), SELFTEST_MUTATE_WALLET_MARKER_TEXT)) { fprintf(stderr, "Refusing destructive wallet storage mutation outside " "a marked self-test datadir.\n"); return 1; } bool fOk = false; try { CWalletStorageSanityRawDB walletdb; if (name == "unknown-record") fOk = walletdb.WriteUnknownRecord(); else if (name == "malformed-key") fOk = walletdb.WriteMalformedKeyRecord(); else if (name == "incomplete-plain-hd") fOk = walletdb.MakePlainHDSeedIncomplete(); else if (name == "encrypted-marker-with-plain-keys") fOk = walletdb.AddEncryptedMarkerToPlainWallet(); else { fprintf(stderr, "Unknown wallet storage mutation '%s'\n", name.c_str()); return 1; } } catch (const std::exception& e) { fprintf(stderr, "Cannot mutate wallet storage: %s\n", e.what()); return 1; } catch (...) { fprintf(stderr, "Cannot mutate wallet storage: unknown exception\n"); return 1; } DBFlush(true); return fOk ? 0 : 1; } static int RunWalletStorageSanitySelfTest() { fflush(stdout); printf("wallet-storage-sanity self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd)) { printf(" FAIL could not read current directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; printf(" temp root: %s\n", tmp.c_str()); try { #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif struct StorageScenario { const char* pszName; const char* pszExpectedFailure; }; StorageScenario scenarios[] = { { "unknown-record", "wallet.dat contains unknown records" }, { "malformed-key", "wallet.dat contains malformed records" }, { "incomplete-plain-hd", "plain HD seed is incomplete" }, { "encrypted-marker-with-plain-keys", "encrypted wallet still contains plain private key records" }, }; for (size_t i = 0; i < sizeof(scenarios)/sizeof(scenarios[0]); i++) { string strScenarioDir = tmp + "/" + scenarios[i].pszName; nFail += Check(MakeDirLocal(strScenarioDir), "scenario datadir can be created") ? 0 : 1; string strCreateOut = tmp + "/" + scenarios[i].pszName + "-create.txt"; vector vCreateArgs; vCreateArgs.push_back("-datadir=" + strScenarioDir); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && FileContainsText(strCreateOut, "Write these twelve words down"), "scenario phrase wallet can be created") ? 0 : 1; if (i == 0) { string strBlockedOut = tmp + "/" + scenarios[i].pszName + "-blocked-mutate.txt"; vector vBlockedArgs; vBlockedArgs.push_back("-datadir=" + strScenarioDir); vBlockedArgs.push_back("-nomanagedtor"); vBlockedArgs.push_back("-nogui"); vBlockedArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); int nBlockedRet = RunBitflashChild(strExe, vBlockedArgs, NULL, &strBlockedOut); nFail += Check(nBlockedRet != 0 && FileContainsText(strBlockedOut, "Refusing destructive wallet storage mutation"), "wallet mutation helper is blocked without marker") ? 0 : 1; } nFail += Check(WriteSelfTestMutationMarker(strScenarioDir), "scenario mutation marker can be written") ? 0 : 1; string strMutateOut = tmp + "/" + scenarios[i].pszName + "-mutate.txt"; vector vMutateArgs; vMutateArgs.push_back("-datadir=" + strScenarioDir); vMutateArgs.push_back("-nomanagedtor"); vMutateArgs.push_back("-nogui"); vMutateArgs.push_back("-selftestmutatewallet=" + string(scenarios[i].pszName)); int nMutateRet = RunBitflashChild(strExe, vMutateArgs, NULL, &strMutateOut); nFail += Check(nMutateRet == 0, scenarios[i].pszName) ? 0 : 1; string strCheckOut = tmp + "/" + scenarios[i].pszName + "-check.txt"; vector vCheckArgs; vCheckArgs.push_back("-datadir=" + strScenarioDir); vCheckArgs.push_back("-nomanagedtor"); vCheckArgs.push_back("-nogui"); vCheckArgs.push_back("-walletstoragecheck"); int nCheckRet = RunBitflashChild(strExe, vCheckArgs, NULL, &strCheckOut); nFail += Check(nCheckRet == 2 && FileContainsText(strCheckOut, "storage sanity: failed") && FileContainsText(strCheckOut, scenarios[i].pszExpectedFailure), scenarios[i].pszExpectedFailure) ? 0 : 1; } } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunDbEnvReopenSelfTest() { fflush(stdout); printf("db-env-reopen self-test\n"); std::string tmp; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } int nFail = 0; std::string strOldDataDir = strSetDataDir; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); try { for (int i = 0; i < 5; i++) { int64 nValue = 1000 + i; { CWalletDB walletdb("cr+"); nFail += Check(walletdb.WriteSetting("db-env-reopen-cycle", nValue), "wallet setting can be written before shutdown") ? 0 : 1; } DBFlush(true); int64 nRead = 0; { CWalletDB walletdb("r"); nFail += Check(walletdb.ReadSetting("db-env-reopen-cycle", nRead), "wallet setting can be read after env reopen") ? 0 : 1; } nFail += Check(nRead == nValue, "reopened environment reads the last committed value") ? 0 : 1; DBFlush(true); } } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); strSetDataDir = strOldDataDir; RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunWalletSQLiteSelfTest() { fflush(stdout); printf("wallet-sqlite self-test\n"); std::string tmp; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } int nFail = 0; string strPath = tmp + "/wallet.sqlite"; string strError; string strOldRuntimeDataDir; bool fRestoreRuntimeDataDir = false; try { CDataStream ssKeySetting(SER_DISK); ssKeySetting << make_pair(string("setting"), string("sqlite-roundtrip")); CDataStream ssValueSetting(SER_DISK); ssValueSetting << (int64)424242; vector vchPubKey; vchPubKey.push_back(0x04); for (int i = 0; i < 64; i++) vchPubKey.push_back((unsigned char)i); CDataStream ssKeyDefault(SER_DISK); ssKeyDefault << string("defaultkey"); CDataStream ssValueDefault(SER_DISK); ssValueDefault << vchPubKey; { CWalletDBSQLite db; nFail += Check(db.Open(strPath, strError), "SQLite wallet opens and creates schema") ? 0 : 1; nFail += Check(db.BeginTransaction(strError), "SQLite wallet begins a write transaction") ? 0 : 1; nFail += Check(db.WriteRecord(DataStreamBytes(ssKeySetting), DataStreamBytes(ssValueSetting), strError), "SQLite wallet writes a serialized setting record") ? 0 : 1; nFail += Check(db.WriteRecord(DataStreamBytes(ssKeyDefault), DataStreamBytes(ssValueDefault), strError), "SQLite wallet writes a serialized defaultkey record") ? 0 : 1; nFail += Check(db.CommitTransaction(strError), "SQLite wallet commits a write transaction") ? 0 : 1; int nCount = 0; nFail += Check(db.CountRecords(nCount, strError) && nCount == 2, "SQLite wallet counts raw records") ? 0 : 1; } { CWalletDBSQLite db; nFail += Check(db.Open(strPath, strError), "SQLite wallet reopens") ? 0 : 1; vector vchRead; nFail += Check(db.ReadRecord(DataStreamBytes(ssKeySetting), vchRead, strError), "SQLite wallet reads a serialized setting record") ? 0 : 1; int64 nReadValue = 0; CDataStream ssRead(vchRead, SER_DISK); ssRead >> nReadValue; nFail += Check(!ssRead.fail() && nReadValue == 424242, "SQLite wallet preserves serialized values byte-for-byte") ? 0 : 1; vchRead.clear(); nFail += Check(db.ReadRecord(DataStreamBytes(ssKeyDefault), vchRead, strError), "SQLite wallet reads a serialized defaultkey record") ? 0 : 1; vector vchReadPubKey; CDataStream ssReadDefault(vchRead, SER_DISK); ssReadDefault >> vchReadPubKey; nFail += Check(!ssReadDefault.fail() && vchReadPubKey == vchPubKey, "SQLite wallet preserves public key blobs") ? 0 : 1; } { CWalletDBSQLite db; nFail += Check(db.Open(strPath, strError), "SQLite wallet reopens for typed records") ? 0 : 1; nFail += Check(db.WriteTypedRecord(make_pair(string("setting"), string("sqlite-typed")), (int64)777, strError), "SQLite wallet writes a typed setting record") ? 0 : 1; int64 nTypedRead = 0; nFail += Check(db.ReadTypedRecord(make_pair(string("setting"), string("sqlite-typed")), nTypedRead, strError) && nTypedRead == 777, "SQLite wallet reads a typed setting record") ? 0 : 1; nFail += Check(!db.WriteTypedRecord(make_pair(string("setting"), string("sqlite-typed")), (int64)888, strError, false), "SQLite wallet refuses typed overwrite when asked") ? 0 : 1; nTypedRead = 0; nFail += Check(db.ReadTypedRecord(make_pair(string("setting"), string("sqlite-typed")), nTypedRead, strError) && nTypedRead == 777, "SQLite wallet preserves the old typed value after overwrite refusal") ? 0 : 1; nFail += Check(db.EraseTypedRecord(make_pair(string("setting"), string("sqlite-typed")), strError), "SQLite wallet erases a typed record") ? 0 : 1; nTypedRead = 0; nFail += Check(!db.ReadTypedRecord(make_pair(string("setting"), string("sqlite-typed")), nTypedRead, strError), "SQLite wallet typed erase removes the record") ? 0 : 1; nFail += Check(db.WriteTypedRecord(string("defaultkey"), vchPubKey, strError), "SQLite wallet writes a typed defaultkey record") ? 0 : 1; vector vchTypedPubKey; nFail += Check(db.ReadTypedRecord(string("defaultkey"), vchTypedPubKey, strError) && vchTypedPubKey == vchPubKey, "SQLite wallet reads a typed defaultkey record") ? 0 : 1; } { string strRuntimePath = tmp + "/runtime-wallet.sqlite"; string strRuntimeDir = tmp + "/runtime-datadir"; nFail += Check(MakeDirLocal(strRuntimeDir), "SQLite runtime datadir can be created") ? 0 : 1; { CWalletDBSQLite db; nFail += Check(db.Open(strRuntimePath, strError), "SQLite runtime wallet file can be created") ? 0 : 1; } string strOldDataDir = strSetDataDir; strSetDataDir = strRuntimeDir; strOldRuntimeDataDir = strOldDataDir; fRestoreRuntimeDataDir = true; bool fRuntimeOpened = WalletSQLiteRuntimeOpen(strRuntimePath, strError); nFail += Check(fRuntimeOpened, "SQLite runtime wallet can be activated") ? 0 : 1; if (fRuntimeOpened) { CKey key; key.MakeNewKey(); vector vchRuntimePubKey = key.GetPubKey(); CPrivKey vchRuntimePrivKey = key.GetPrivKey(); string strRuntimeAddress = PubKeyToAddress(vchRuntimePubKey); nFail += Check(CWalletDB().WriteSetting("runtime-sqlite-setting", (int64)909090) && CWalletDB().WriteDefaultKey(vchRuntimePubKey) && CWalletDB().WriteKey(vchRuntimePubKey, vchRuntimePrivKey) && CWalletDB().WriteName(strRuntimeAddress, "SQLite Runtime Address"), "CWalletDB writes route to active SQLite runtime") ? 0 : 1; int64 nRuntimeSetting = 0; vector vchRuntimeDefaultKey; CPrivKey vchRuntimeReadPrivKey; string strRuntimeName; nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", nRuntimeSetting) && nRuntimeSetting == 909090 && CWalletDB("r").ReadDefaultKey(vchRuntimeDefaultKey) && vchRuntimeDefaultKey == vchRuntimePubKey && CWalletDB("r").ReadKey(vchRuntimePubKey, vchRuntimeReadPrivKey) && vchRuntimeReadPrivKey == vchRuntimePrivKey && CWalletDB("r").ReadName(strRuntimeAddress, strRuntimeName) && strRuntimeName == "SQLite Runtime Address", "CWalletDB reads route to active SQLite runtime") ? 0 : 1; nFail += Check(CWalletDB().EraseName(strRuntimeAddress) && !CWalletDB("r").ReadName(strRuntimeAddress, strRuntimeName), "CWalletDB erases route to active SQLite runtime") ? 0 : 1; nFail += Check(!FileExists((strRuntimeDir + "/wallet.dat").c_str()), "active SQLite runtime does not create wallet.dat") ? 0 : 1; // Transaction bracket: a rolled-back group leaves nothing, a // committed group persists. This is the all-or-nothing that // WriteHDMaster relies on so a half-written seed cannot survive // a failure between its two records. bool fTxnBegan = WalletSQLiteRuntimeBeginTxn(); bool fTxnWrote = fTxnBegan && CWalletDB().WriteSetting("runtime-txn-rollback", (int64)111); if (fTxnBegan) WalletSQLiteRuntimeRollbackTxn(); nFail += Check(fTxnBegan && fTxnWrote, "a SQLite runtime transaction opens and rolls back") ? 0 : 1; int64 nRolledBack = 0; nFail += Check(!CWalletDB("r").ReadSetting("runtime-txn-rollback", nRolledBack), "a rolled-back SQLite runtime write leaves no record") ? 0 : 1; bool fCommitOk = WalletSQLiteRuntimeBeginTxn() && CWalletDB().WriteSetting("runtime-txn-commit", (int64)222) && WalletSQLiteRuntimeCommitTxn(); int64 nCommitted = 0; nFail += Check(fCommitOk && CWalletDB("r").ReadSetting("runtime-txn-commit", nCommitted) && nCommitted == 222, "a committed SQLite runtime write persists") ? 0 : 1; // WriteHDMaster brackets its two records in that transaction; // both halves of the seed must land. vector vchTxnMaster(32, 0x5a); vector vchTxnChainCode(32, 0xa5); vector vchReadMaster, vchReadChainCode; nFail += Check(CWalletDB().WriteHDMaster(vchTxnMaster, vchTxnChainCode) && CWalletDB("r").ReadHDMaster(vchReadMaster, vchReadChainCode) && vchReadMaster == vchTxnMaster && vchReadChainCode == vchTxnChainCode, "WriteHDMaster commits both halves of the seed under SQLite") ? 0 : 1; // Durability: close the backend (checkpoints the WAL), reopen // the same file, and confirm what CWalletDB wrote is still // there. This is the write-survives-a-restart property the real // -walletbackend=sqlite node depends on. WalletSQLiteRuntimeClose(); bool fReopened = WalletSQLiteRuntimeOpen(strRuntimePath, strError); nFail += Check(fReopened, "SQLite runtime wallet reopens after close") ? 0 : 1; if (fReopened) { int64 nPersisted = 0; CPrivKey vchPersistedPrivKey; vector vchPersistedDefaultKey; nFail += Check(CWalletDB("r").ReadSetting("runtime-sqlite-setting", nPersisted) && nPersisted == 909090 && CWalletDB("r").ReadDefaultKey(vchPersistedDefaultKey) && vchPersistedDefaultKey == vchRuntimePubKey && CWalletDB("r").ReadKey(vchRuntimePubKey, vchPersistedPrivKey) && vchPersistedPrivKey == vchRuntimePrivKey, "SQLite runtime writes persist across a close and reopen") ? 0 : 1; } } WalletSQLiteRuntimeClose(); strSetDataDir = strOldDataDir; fRestoreRuntimeDataDir = false; } } catch (const std::exception& e) { WalletSQLiteRuntimeClose(); if (fRestoreRuntimeDataDir) strSetDataDir = strOldRuntimeDataDir; printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { WalletSQLiteRuntimeClose(); if (fRestoreRuntimeDataDir) strSetDataDir = strOldRuntimeDataDir; printf(" FAIL unknown exception\n"); nFail++; } RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } namespace { class CWalletRecordMapVisitor : public CWalletRecordVisitor { public: std::map, vector >& mapRecords; explicit CWalletRecordMapVisitor( std::map, vector >& mapRecordsIn) : mapRecords(mapRecordsIn) { } bool VisitWalletRecord(const CDataStream& ssKey, const CDataStream& ssValue, string& strErrorRet) { vector vchKey = DataStreamBytes(ssKey); if (mapRecords.count(vchKey)) { strErrorRet = "duplicate serialized wallet key"; return false; } mapRecords[vchKey] = DataStreamBytes(ssValue); return true; } }; } // namespace static bool CreateSQLiteWalletWithEmptyKey(const string& strPath) { sqlite3* pdb = NULL; int ret = sqlite3_open_v2(strPath.c_str(), &pdb, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); if (ret != SQLITE_OK || !pdb) { if (pdb) sqlite3_close(pdb); return false; } char* pszErr = NULL; bool fOk = sqlite3_exec(pdb, "CREATE TABLE wallet_records (" "key BLOB PRIMARY KEY NOT NULL," "value BLOB NOT NULL" ");", NULL, NULL, &pszErr) == SQLITE_OK && sqlite3_exec(pdb, "INSERT INTO wallet_records(key, value) " "VALUES(x'', x'01');", NULL, NULL, &pszErr) == SQLITE_OK; if (pszErr) sqlite3_free(pszErr); sqlite3_close(pdb); return fOk; } static bool CreateSQLiteWalletWithFutureMinVersion(const string& strPath) { string strError; CWalletDBSQLite db; CDataStream ssKey(SER_DISK); ssKey << string("walletminversion"); CDataStream ssValue(SER_DISK); ssValue << (WALLET_FORMAT_SUPPORTED + 1); if (!db.Open(strPath, strError)) return false; if (!db.BeginTransaction(strError)) return false; if (!db.WriteRecord(DataStreamBytes(ssKey), DataStreamBytes(ssValue), strError)) return false; if (!db.CommitTransaction(strError)) return false; if (!db.Checkpoint(strError)) return false; return true; } static bool CreateTypedSQLiteWallet(const string& strPath) { string strError; CWalletDBSQLite db; CKey key; key.MakeNewKey(); vector vchPubKey = key.GetPubKey(); CPrivKey vchPrivKey = key.GetPrivKey(); string strAddress = PubKeyToAddress(vchPubKey); if (!db.Open(strPath, strError)) return false; if (!db.BeginTransaction(strError)) return false; if (!db.WriteTypedRecord(make_pair(string("key"), vchPubKey), vchPrivKey, strError, false)) return false; if (!db.WriteTypedRecord(string("defaultkey"), vchPubKey, strError)) return false; if (!db.WriteTypedRecord(make_pair(string("name"), strAddress), string("Typed SQLite Address"), strError)) return false; if (!db.CommitTransaction(strError)) return false; if (!db.Checkpoint(strError)) return false; return true; } static int RunWalletSQLiteMigrationSelfTest() { fflush(stdout); printf("wallet-sqlite-migration self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd)) { printf(" FAIL could not read current directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; printf(" temp root: %s\n", tmp.c_str()); try { #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif string strWalletDir = tmp + "/wallet"; nFail += Check(MakeDirLocal(strWalletDir), "temporary wallet directory can be created") ? 0 : 1; string strCreateOut = tmp + "/create.txt"; vector vCreateArgs; vCreateArgs.push_back("-datadir=" + strWalletDir); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); vCreateArgs.push_back("-walletbackend=bdb"); // migrate FROM a Berkeley DB wallet.dat vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && FileContainsText(strCreateOut, "Write these twelve words down"), "a phrase wallet can be created for SQLite export") ? 0 : 1; string strSQLite = tmp + "/wallet.sqlite"; string strExportOut = tmp + "/sqlite-export.txt"; vector vExportArgs; vExportArgs.push_back("-datadir=" + strWalletDir); vExportArgs.push_back("-nomanagedtor"); vExportArgs.push_back("-nogui"); vExportArgs.push_back("-walletsqliteexport=" + strSQLite); int nExportRet = RunBitflashChild(strExe, vExportArgs, NULL, &strExportOut); nFail += Check(nExportRet == 0 && FileExists(strSQLite.c_str()) && FileContainsText(strExportOut, "SQLite wallet export written"), "wallet.dat can be exported to SQLite") ? 0 : 1; string strVerifyOut = tmp + "/sqlite-verify.txt"; vector vVerifyArgs; vVerifyArgs.push_back("-datadir=" + strWalletDir); vVerifyArgs.push_back("-nomanagedtor"); vVerifyArgs.push_back("-nogui"); vVerifyArgs.push_back("-walletsqliteverify=" + strSQLite); int nVerifyRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyOut); nFail += Check(nVerifyRet == 0 && FileContainsText(strVerifyOut, "verification: ok"), "SQLite wallet export verifies against wallet.dat") ? 0 : 1; string strLoadCheckOut = tmp + "/sqlite-loadcheck.txt"; vector vLoadCheckArgs; vLoadCheckArgs.push_back("-datadir=" + strWalletDir); vLoadCheckArgs.push_back("-nomanagedtor"); vLoadCheckArgs.push_back("-nogui"); vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strSQLite); int nLoadCheckRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strLoadCheckOut); nFail += Check(nLoadCheckRet == 0 && FileContainsText(strLoadCheckOut, "load check: ok"), "SQLite wallet export passes the loader compatibility check") ? 0 : 1; string strRuntimeOut = tmp + "/sqlite-runtime.txt"; vector vRuntimeArgs; vRuntimeArgs.push_back("-datadir=" + strWalletDir); vRuntimeArgs.push_back("-nomanagedtor"); vRuntimeArgs.push_back("-nogui"); vRuntimeArgs.push_back("-walletsqlite=" + strSQLite); int nRuntimeRet = RunBitflashChild(strExe, vRuntimeArgs, NULL, &strRuntimeOut); nFail += Check(nRuntimeRet == 0 && FileContainsText(strRuntimeOut, "SQLite wallet loaded: read-only staging backend"), "SQLite wallet export loads through the runtime startup path") ? 0 : 1; string strTypedSQLite = tmp + "/typed-wallet.sqlite"; nFail += Check(CreateTypedSQLiteWallet(strTypedSQLite), "typed SQLite wallet can be created without Berkeley DB") ? 0 : 1; string strTypedLoadCheckOut = tmp + "/sqlite-typed-loadcheck.txt"; vector vTypedLoadCheckArgs; vTypedLoadCheckArgs.push_back("-datadir=" + strWalletDir); vTypedLoadCheckArgs.push_back("-nomanagedtor"); vTypedLoadCheckArgs.push_back("-nogui"); vTypedLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strTypedSQLite); int nTypedLoadCheckRet = RunBitflashChild(strExe, vTypedLoadCheckArgs, NULL, &strTypedLoadCheckOut); nFail += Check(nTypedLoadCheckRet == 0 && FileContainsText(strTypedLoadCheckOut, "load check: ok"), "typed SQLite wallet passes the loader compatibility check") ? 0 : 1; string strTypedRuntimeDir = tmp + "/typed-runtime-wallet"; nFail += Check(MakeDirLocal(strTypedRuntimeDir), "typed SQLite runtime directory can be created") ? 0 : 1; string strTypedRuntimeOut = tmp + "/sqlite-typed-runtime.txt"; vector vTypedRuntimeArgs; vTypedRuntimeArgs.push_back("-datadir=" + strTypedRuntimeDir); vTypedRuntimeArgs.push_back("-nomanagedtor"); vTypedRuntimeArgs.push_back("-nogui"); vTypedRuntimeArgs.push_back("-walletsqlite=" + strTypedSQLite); int nTypedRuntimeRet = RunBitflashChild(strExe, vTypedRuntimeArgs, NULL, &strTypedRuntimeOut); nFail += Check(nTypedRuntimeRet == 0 && FileContainsText(strTypedRuntimeOut, "SQLite wallet loaded: read-only staging backend") && !FileExists((strTypedRuntimeDir + "/wallet.dat").c_str()), "typed SQLite wallet loads through runtime without creating wallet.dat") ? 0 : 1; string strRestoredDir = tmp + "/restored-wallet"; nFail += Check(MakeDirLocal(strRestoredDir), "restored wallet directory can be created") ? 0 : 1; string strRestoreOut = tmp + "/sqlite-restore.txt"; vector vRestoreArgs; vRestoreArgs.push_back("-datadir=" + strRestoredDir); vRestoreArgs.push_back("-nomanagedtor"); vRestoreArgs.push_back("-nogui"); vRestoreArgs.push_back("-walletsqliterestore=" + strSQLite); int nRestoreRet = RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOut); nFail += Check(nRestoreRet == 0 && FileContainsText(strRestoreOut, "records restored:"), "SQLite wallet export restores into a fresh wallet.dat") ? 0 : 1; string strVerifyRestoredOut = tmp + "/sqlite-verify-restored.txt"; vector vVerifyRestoredArgs; vVerifyRestoredArgs.push_back("-datadir=" + strRestoredDir); vVerifyRestoredArgs.push_back("-nomanagedtor"); vVerifyRestoredArgs.push_back("-nogui"); vVerifyRestoredArgs.push_back("-walletsqliteverify=" + strSQLite); int nVerifyRestoredRet = RunBitflashChild(strExe, vVerifyRestoredArgs, NULL, &strVerifyRestoredOut); nFail += Check(nVerifyRestoredRet == 0 && FileContainsText(strVerifyRestoredOut, "verification: ok"), "restored wallet.dat verifies against the SQLite export") ? 0 : 1; string strRestoreOverwriteOut = tmp + "/sqlite-restore-overwrite.txt"; int nRestoreOverwriteRet = RunBitflashChild(strExe, vRestoreArgs, NULL, &strRestoreOverwriteOut); nFail += Check(nRestoreOverwriteRet == 1 && FileContainsText(strRestoreOverwriteOut, "Refusing to overwrite existing wallet.dat"), "SQLite wallet restore refuses to overwrite wallet.dat") ? 0 : 1; string strBadSQLite = tmp + "/bad-empty-key.sqlite"; nFail += Check(CreateSQLiteWalletWithEmptyKey(strBadSQLite), "bad SQLite export with an empty key can be created") ? 0 : 1; string strBadRestoreDir = tmp + "/bad-restore-wallet"; nFail += Check(MakeDirLocal(strBadRestoreDir), "bad restore wallet directory can be created") ? 0 : 1; string strBadRestoreOut = tmp + "/sqlite-restore-bad.txt"; vector vBadRestoreArgs; vBadRestoreArgs.push_back("-datadir=" + strBadRestoreDir); vBadRestoreArgs.push_back("-nomanagedtor"); vBadRestoreArgs.push_back("-nogui"); vBadRestoreArgs.push_back("-walletsqliterestore=" + strBadSQLite); int nBadRestoreRet = RunBitflashChild(strExe, vBadRestoreArgs, NULL, &strBadRestoreOut); nFail += Check(nBadRestoreRet == 1 && FileContainsText(strBadRestoreOut, "Cannot restore SQLite wallet export") && !FileExists((strBadRestoreDir + "/wallet.dat").c_str()), "failed SQLite wallet restore removes incomplete wallet.dat") ? 0 : 1; string strFutureSQLite = tmp + "/future-wallet.sqlite"; nFail += Check(CreateSQLiteWalletWithFutureMinVersion(strFutureSQLite), "SQLite export with a future wallet format can be created") ? 0 : 1; string strFutureLoadCheckOut = tmp + "/sqlite-loadcheck-future.txt"; vector vFutureLoadCheckArgs; vFutureLoadCheckArgs.push_back("-datadir=" + strWalletDir); vFutureLoadCheckArgs.push_back("-nomanagedtor"); vFutureLoadCheckArgs.push_back("-nogui"); vFutureLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strFutureSQLite); int nFutureLoadCheckRet = RunBitflashChild(strExe, vFutureLoadCheckArgs, NULL, &strFutureLoadCheckOut); nFail += Check(nFutureLoadCheckRet == 2 && FileContainsText(strFutureLoadCheckOut, "unsupported future wallet format") && FileContainsText(strFutureLoadCheckOut, "load check: failed"), "SQLite wallet load check rejects future wallet formats") ? 0 : 1; string strFutureRuntimeOut = tmp + "/sqlite-runtime-future.txt"; vector vFutureRuntimeArgs; vFutureRuntimeArgs.push_back("-datadir=" + strWalletDir); vFutureRuntimeArgs.push_back("-nomanagedtor"); vFutureRuntimeArgs.push_back("-nogui"); vFutureRuntimeArgs.push_back("-walletsqlite=" + strFutureSQLite); int nFutureRuntimeRet = RunBitflashChild(strExe, vFutureRuntimeArgs, NULL, &strFutureRuntimeOut); nFail += Check(nFutureRuntimeRet == 1 && FileContainsText(strFutureRuntimeOut, "Cannot open SQLite wallet export") && FileContainsText(strFutureRuntimeOut, "newer version of Bitflash"), "SQLite runtime startup rejects future wallet formats") ? 0 : 1; string strOverwriteOut = tmp + "/sqlite-export-overwrite.txt"; int nOverwriteRet = RunBitflashChild(strExe, vExportArgs, NULL, &strOverwriteOut); nFail += Check(nOverwriteRet != 0 && FileContainsText(strOverwriteOut, "Refusing to overwrite existing SQLite wallet export"), "SQLite wallet export refuses to overwrite") ? 0 : 1; remove((strSQLite + "-wal").c_str()); remove((strSQLite + "-shm").c_str()); std::map, vector > mapBDB; string strOldDataDir = strSetDataDir; string strScanError; strSetDataDir = strWalletDir; CWalletRecordMapVisitor visitor(mapBDB); bool fScanOk = ScanWalletRecords(visitor, strScanError); DBFlush(true); strSetDataDir = strOldDataDir; nFail += Check(fScanOk && !mapBDB.empty(), "wallet.dat records can be scanned for comparison") ? 0 : 1; CWalletDBSQLite db; string strError; nFail += Check(db.Open(strSQLite, strError), "exported SQLite wallet opens") ? 0 : 1; int nSQLiteRecords = 0; nFail += Check(db.CountRecords(nSQLiteRecords, strError) && nSQLiteRecords == (int)mapBDB.size(), "SQLite export has the same record count as wallet.dat") ? 0 : 1; bool fAllMatch = true; for (std::map, vector >::const_iterator it = mapBDB.begin(); it != mapBDB.end(); ++it) { vector vchSQLiteValue; if (!db.ReadRecord(it->first, vchSQLiteValue, strError) || vchSQLiteValue != it->second) { fAllMatch = false; break; } } nFail += Check(fAllMatch, "SQLite export preserves every wallet.dat key/value byte-for-byte") ? 0 : 1; std::map, vector > mapSQLite; CWalletRecordMapVisitor sqliteVisitor(mapSQLite); nFail += Check(db.ScanRecords(sqliteVisitor, strError) && mapSQLite == mapBDB, "SQLite wallet scanner streams every exported record byte-for-byte") ? 0 : 1; db.Close(); CWalletDBSQLite dbCorrupt; vector vchBadValue; vchBadValue.push_back(0xba); vchBadValue.push_back(0xad); nFail += Check(!mapBDB.empty() && dbCorrupt.Open(strSQLite, strError) && dbCorrupt.BeginTransaction(strError) && dbCorrupt.WriteRecord(mapBDB.begin()->first, vchBadValue, strError) && dbCorrupt.CommitTransaction(strError) && dbCorrupt.Checkpoint(strError), "SQLite export can be deliberately corrupted for verifier test") ? 0 : 1; dbCorrupt.Close(); string strVerifyBadOut = tmp + "/sqlite-verify-bad.txt"; int nVerifyBadRet = RunBitflashChild(strExe, vVerifyArgs, NULL, &strVerifyBadOut); nFail += Check(nVerifyBadRet == 2 && FileContainsText(strVerifyBadOut, "record value mismatches: 1") && FileContainsText(strVerifyBadOut, "verification: failed"), "SQLite wallet verifier detects a mismatched record value") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static void ClearWalletRuntimeForTest() { CRITICAL_BLOCK(cs_mapKeys) { mapKeys.clear(); mapCryptedKeys.clear(); mapPubKeys.clear(); mapMasterKeys.clear(); vWalletMasterKey.clear(); vchCryptedHDMaster.clear(); vchCryptedHDChainCode.clear(); fWalletEncrypted = false; fWalletLocked = true; } CRITICAL_BLOCK(cs_keyPool) mapKeyPool.clear(); vchHDMaster.clear(); vchHDChainCode.clear(); nHDNext = 0; } static int RunWalletCryptoSelfTest() { fflush(stdout); printf("wallet-crypto self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); try { const string strPassphrase = "btf-test-passphrase"; const string strWrongPassphrase = "btf-test-passphraser"; CKey key; key.MakeNewKey(); vector vchPubKey = key.GetPubKey(); CPrivKey vchPrivKey = key.GetPrivKey(); CKeyingMaterial vchMasterKey; for (int i = 0; i < 32; i++) vchMasterKey.push_back((unsigned char)(i + 1)); CWalletMasterKey kMasterKey; kMasterKey.vchSalt.clear(); for (int i = 0; i < 8; i++) kMasterKey.vchSalt.push_back((unsigned char)(0xa0 + i)); kMasterKey.nDeriveIterations = 2500; CKeyingMaterial vchPassKey; vector vchPassIV; nFail += Check(DeriveWalletPassphraseKey(strPassphrase, kMasterKey.vchSalt, kMasterKey.nDeriveIterations, vchPassKey, vchPassIV), "a passphrase key can be derived") ? 0 : 1; nFail += Check(EncryptSecret(vchPassKey, vector(vchMasterKey.begin(), vchMasterKey.end()), vchPassIV, kMasterKey.vchCryptedKey), "the wallet master key can be encrypted") ? 0 : 1; vector vchCryptedKey; nFail += Check(EncryptSecret(vchMasterKey, vector(vchPrivKey.begin(), vchPrivKey.end()), WalletKeyIV(vchPubKey), vchCryptedKey), "a private key can be encrypted under the wallet master key") ? 0 : 1; { CWalletDB walletdb("cr"); nFail += Check(walletdb.WriteWalletMinVersion(WALLET_FORMAT_ENCRYPTED), "an encrypted wallet format marker can be written") ? 0 : 1; nFail += Check(walletdb.WriteMasterKey(0, kMasterKey), "the encrypted master key can be written") ? 0 : 1; nFail += Check(walletdb.WriteCryptedKey(vchPubKey, vchCryptedKey), "the encrypted private key can be written") ? 0 : 1; nFail += Check(walletdb.WriteDefaultKey(vchPubKey), "the encrypted wallet default key can be written") ? 0 : 1; } ClearWalletRuntimeForTest(); vector vchDefaultKey; nFail += Check(CWalletDB("r").LoadWallet(vchDefaultKey), "an encrypted wallet can be loaded") ? 0 : 1; nFail += Check(vchDefaultKey == vchPubKey, "the encrypted wallet default public key loads") ? 0 : 1; nFail += Check(IsWalletEncrypted() && IsWalletLocked(), "an encrypted wallet loads locked") ? 0 : 1; nFail += Check(WalletCanSpendKey(vchPubKey), "an encrypted key is recognized as wallet-owned while locked") ? 0 : 1; CPrivKey vchDecrypted; string strError; nFail += Check(!GetWalletPrivKey(vchPubKey, vchDecrypted, strError), "a locked encrypted key cannot be read") ? 0 : 1; nFail += Check(!UnlockWallet(strWrongPassphrase, strError), "the wrong-passphrase does not unlock") ? 0 : 1; nFail += Check(UnlockWallet(strPassphrase, strError), "the right passphrase unlocks") ? 0 : 1; nFail += Check(!IsWalletLocked(), "the wallet reports unlocked") ? 0 : 1; nFail += Check(GetWalletPrivKey(vchPubKey, vchDecrypted, strError) && vchDecrypted == vchPrivKey, "an unlocked encrypted key decrypts to the original private key") ? 0 : 1; LockWallet(); nFail += Check(IsWalletLocked(), "locking clears the unlocked state") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static bool FileContainsBytes(const string& strPath, const vector& vchNeedle) { if (vchNeedle.empty()) return false; FILE* pf = fopen(strPath.c_str(), "rb"); if (!pf) return false; vector vchHaystack; unsigned char buf[4096]; size_t n = 0; while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) vchHaystack.insert(vchHaystack.end(), buf, buf + n); fclose(pf); if (vchHaystack.size() < vchNeedle.size()) return false; return search(vchHaystack.begin(), vchHaystack.end(), vchNeedle.begin(), vchNeedle.end()) != vchHaystack.end(); } static bool FileContainsText(const string& strPath, const string& strNeedle) { FILE* pf = fopen(strPath.c_str(), "rb"); if (!pf) return false; string strHaystack; char buf[4096]; size_t n = 0; while ((n = fread(buf, 1, sizeof(buf), pf)) > 0) strHaystack.append(buf, buf + n); fclose(pf); return strHaystack.find(strNeedle) != string::npos; } static bool WriteTextFile(const string& strPath, const string& strText) { FILE* pf = fopen(strPath.c_str(), "wb"); if (!pf) return false; bool fOk = fwrite(strText.data(), 1, strText.size(), pf) == strText.size(); if (fclose(pf) != 0) fOk = false; return fOk; } static bool MakeDirLocal(const string& strPath) { #ifdef _WIN32 return CreateDirectoryA(strPath.c_str(), NULL) || GetLastError() == ERROR_ALREADY_EXISTS; #else return mkdir(strPath.c_str(), 0700) == 0 || errno == EEXIST; #endif } static bool CopyFileLocal(const string& strSrc, const string& strDst) { FILE* in = fopen(strSrc.c_str(), "rb"); if (!in) return false; FILE* out = fopen(strDst.c_str(), "wb"); if (!out) { fclose(in); return false; } bool fOk = true; char buf[65536]; size_t n = 0; while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { if (fwrite(buf, 1, n, out) != n) { fOk = false; break; } } if (ferror(in)) fOk = false; if (fclose(out) != 0) fOk = false; fclose(in); return fOk; } static bool DirectoryHasFileWithPrefix(const string& strDir, const string& strPrefix) { #ifdef _WIN32 WIN32_FIND_DATAA findData; string pattern = strDir + "\\*"; HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); if (hFind == INVALID_HANDLE_VALUE) return false; bool fFound = false; do { if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { string name = findData.cFileName; if (name.compare(0, strPrefix.size(), strPrefix) == 0) { fFound = true; break; } } } while (FindNextFileA(hFind, &findData)); FindClose(hFind); return fFound; #else DIR* dir = opendir(strDir.c_str()); if (!dir) return false; bool fFound = false; struct dirent* ent; while ((ent = readdir(dir)) != NULL) { string name = ent->d_name; if (name.compare(0, strPrefix.size(), strPrefix) == 0) { fFound = true; break; } } closedir(dir); return fFound; #endif } static string QuoteCommandArg(const string& str) { string out = "\""; for (size_t i = 0; i < str.size(); i++) { if (str[i] == '"') out += "\\\""; else out += str[i]; } out += "\""; return out; } static int RunBitflashChild(const string& strExe, const vector& vArgs, const string* pStdinFile = NULL, const string* pOutputFile = NULL) { #ifdef _WIN32 vector argv; argv.push_back((char*)strExe.c_str()); for (size_t i = 0; i < vArgs.size(); i++) argv.push_back((char*)vArgs[i].c_str()); argv.push_back(NULL); int nOldOut = _dup(1); int nOldErr = _dup(2); int nOldIn = _dup(0); int nNull = _open("NUL", _O_WRONLY); int nOut = -1; int nIn = -1; if (pOutputFile) nOut = _open(pOutputFile->c_str(), _O_WRONLY|_O_CREAT|_O_TRUNC|_O_BINARY, _S_IREAD|_S_IWRITE); if (nOut >= 0) { _dup2(nOut, 1); _dup2(nOut, 2); } else if (nNull >= 0) { _dup2(nNull, 1); _dup2(nNull, 2); } if (pStdinFile) { nIn = _open(pStdinFile->c_str(), _O_RDONLY); if (nIn >= 0) _dup2(nIn, 0); } int nRet = _spawnv(_P_WAIT, strExe.c_str(), &argv[0]); if (nIn >= 0) _close(nIn); if (nOut >= 0) _close(nOut); if (nNull >= 0) _close(nNull); if (nOldIn >= 0) { _dup2(nOldIn, 0); _close(nOldIn); } if (nOldOut >= 0) { _dup2(nOldOut, 1); _close(nOldOut); } if (nOldErr >= 0) { _dup2(nOldErr, 2); _close(nOldErr); } return nRet; #else string strCmd = QuoteCommandArg(strExe); for (size_t i = 0; i < vArgs.size(); i++) strCmd += " " + QuoteCommandArg(vArgs[i]); if (pStdinFile) strCmd += " < " + QuoteCommandArg(*pStdinFile); if (pOutputFile) strCmd += " > " + QuoteCommandArg(*pOutputFile) + " 2>&1"; else strCmd += " > /dev/null 2>&1"; // system() returns a wait status, not the exit code; unwrap it so callers // see the same value the Windows _spawnv path returns (e.g. exit code 2 is // 2, not 512). Without this every "== 2" assertion fails only on Linux. int nSysRet = system(strCmd.c_str()); if (nSysRet != -1 && WIFEXITED(nSysRet)) return WEXITSTATUS(nSysRet); return nSysRet; #endif } static int RunWalletEncryptSelfTest() { fflush(stdout); printf("wallet-encrypt self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; bool fWalletEnvClosed = false; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); try { if (!LoadWallet()) throw std::runtime_error("LoadWallet failed"); string strError; const string strMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), "a phrase can be installed before encryption") ? 0 : 1; TopUpKeyPool(); vector vchDefaultPubKey = keyUser.GetPubKey(); CKey publicLabelKey; publicLabelKey.MakeNewKey(); string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), "public wallet records can be written before encryption") ? 0 : 1; CPrivKey vchDefaultPrivKey; nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), "the default private key is readable before encryption") ? 0 : 1; vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); vector vchPlainHDMaster = vchHDMaster; vector vchPlainHDChainCode = vchHDChainCode; string strBackup; nFail += Check(EncryptWallet("btf-test-passphrase", strBackup, strError), "wallet.dat can be rewritten encrypted") ? 0 : 1; fWalletEnvClosed = true; nFail += Check(FileExists(strBackup.c_str()), "the original wallet is preserved as an explicit backup") ? 0 : 1; string strWalletPath = GetAppDir() + "/wallet.dat"; nFail += Check(!FileContainsBytes(strWalletPath, vchDefaultPrivBytes), "the active wallet no longer contains the default private key bytes") ? 0 : 1; nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDMaster), "the active wallet no longer contains the HD master key bytes") ? 0 : 1; nFail += Check(!FileContainsBytes(strWalletPath, vchPlainHDChainCode), "the active wallet no longer contains the HD chain-code bytes") ? 0 : 1; #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif string strStorageAudit = tmp + "/encrypted-storage-audit.txt"; string strWrongDump = tmp + "/wrong-pass-dump.txt"; string strRightDump = tmp + "/right-pass-dump.txt"; string strLiteralDump = tmp + "/literal-pass-dump.txt"; string strStdinDump = tmp + "/stdin-pass-dump.txt"; string strLockedAudit = tmp + "/locked-recovery-audit.txt"; string strUnlockedAudit = tmp + "/unlocked-recovery-audit.txt"; string strWrongPassFile = tmp + "/wrong-pass.txt"; string strRightPassFile = tmp + "/right-pass.txt"; nFail += Check(WriteTextFile(strWrongPassFile, "wrong-passphrase\n") && WriteTextFile(strRightPassFile, "btf-test-passphrase\n"), "passphrase files can be written for restarted commands") ? 0 : 1; vector vStorageAuditArgs; vStorageAuditArgs.push_back("-datadir=" + tmp); vStorageAuditArgs.push_back("-walletstorageaudit"); vStorageAuditArgs.push_back("-nogui"); int nStorageAuditRet = RunBitflashChild(strExe, vStorageAuditArgs, NULL, &strStorageAudit); nFail += Check(nStorageAuditRet == 0 && FileContainsText(strStorageAudit, "address book labels: 3"), "the encrypted rewrite keeps address-book records") ? 0 : 1; nFail += Check(nStorageAuditRet == 0 && FileContainsText(strStorageAudit, "settings: 1"), "the encrypted rewrite keeps wallet setting records") ? 0 : 1; vector vWrongArgs; vWrongArgs.push_back("-datadir=" + tmp); vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); vWrongArgs.push_back("-dumpwallet=" + strWrongDump); vWrongArgs.push_back("-nogui"); int nWrongRet = RunBitflashChild(strExe, vWrongArgs); nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), "a restarted wallet rejects the wrong-passphrase") ? 0 : 1; vector vLockedAuditArgs; vLockedAuditArgs.push_back("-datadir=" + tmp); vLockedAuditArgs.push_back("-recoveryaudit"); vLockedAuditArgs.push_back("-nogui"); int nLockedAuditRet = RunBitflashChild(strExe, vLockedAuditArgs, NULL, &strLockedAudit); nFail += Check(nLockedAuditRet == 2 && FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit") && FileContainsText(strLockedAudit, "recovery coverage: unavailable while wallet is locked") && FileContainsText(strLockedAudit, "Unlock the wallet with /walletpassphrase"), "a locked encrypted wallet reports that phrase coverage needs unlock") ? 0 : 1; vector vLiteralArgs; vLiteralArgs.push_back("-datadir=" + tmp); vLiteralArgs.push_back("-walletpassphrase=btf-test-passphrase"); vLiteralArgs.push_back("-dumpwallet=" + strLiteralDump); vLiteralArgs.push_back("-nogui"); int nLiteralRet = RunBitflashChild(strExe, vLiteralArgs); nFail += Check(nLiteralRet != 0 && !FileExists(strLiteralDump.c_str()), "literal passphrases on the command line are refused") ? 0 : 1; vector vRightArgs; vRightArgs.push_back("-datadir=" + tmp); vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); vRightArgs.push_back("-dumpwallet=" + strRightDump); vRightArgs.push_back("-nogui"); int nRightRet = RunBitflashChild(strExe, vRightArgs); nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), "a restarted wallet unlocks with the right passphrase") ? 0 : 1; nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), "the restarted wallet can decrypt and dump the original key") ? 0 : 1; vector vUnlockedAuditArgs; vUnlockedAuditArgs.push_back("-datadir=" + tmp); vUnlockedAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); vUnlockedAuditArgs.push_back("-recoveryaudit"); vUnlockedAuditArgs.push_back("-nogui"); int nUnlockedAuditRet = RunBitflashChild(strExe, vUnlockedAuditArgs, NULL, &strUnlockedAudit); nFail += Check(nUnlockedAuditRet == 0 && FileContainsText(strUnlockedAudit, "recovery phrase: present") && !FileContainsText(strUnlockedAudit, "recovery phrase: not installed"), "an unlocked encrypted wallet audits the installed recovery phrase") ? 0 : 1; vector vStdinArgs; vStdinArgs.push_back("-datadir=" + tmp); vStdinArgs.push_back("-walletpassphrase"); vStdinArgs.push_back("-dumpwallet=" + strStdinDump); vStdinArgs.push_back("-nogui"); int nStdinRet = RunBitflashChild(strExe, vStdinArgs, &strRightPassFile); nFail += Check(nStdinRet == 0 && FileExists(strStdinDump.c_str()), "a restarted wallet can read the passphrase from stdin") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } if (!fWalletEnvClosed) DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } // Encrypting a wallet while it runs on the SQLite backend. Unlike the Berkeley // DB encrypt test above, this drives the real command-line path end to end in // child processes: build a Berkeley DB wallet, export it to SQLite, then run // -walletbackend=sqlite -encryptwallet and prove the SQLite wallet came out // encrypted, unlockable, and with wallet.dat left untouched. static int RunWalletSQLiteEncryptSelfTest() { fflush(stdout); printf("wallet-sqlite-encrypt self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); try { // Build a normal Berkeley DB wallet with a recovery phrase, a key pool, // and a couple of public records, then release it. Everything after this // runs the real command-line paths in child processes. if (!LoadWallet()) throw std::runtime_error("LoadWallet failed"); string strError; const string strMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; nFail += Check(SetHDSeedFromMnemonic(strMnemonic, strError), "a phrase can be installed before encryption") ? 0 : 1; TopUpKeyPool(); vector vchDefaultPubKey = keyUser.GetPubKey(); CKey publicLabelKey; publicLabelKey.MakeNewKey(); string strPublicLabelAddress = PubKeyToAddress(publicLabelKey.GetPubKey()); nFail += Check(CWalletDB().WriteName(strPublicLabelAddress, "rewrite-public-name") && CWalletDB().WriteSetting("rewrite-public-setting", (int64)424242), "public wallet records can be written before encryption") ? 0 : 1; CPrivKey vchDefaultPrivKey; nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), "the default private key is readable before encryption") ? 0 : 1; vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); vector vchPlainHDMaster = vchHDMaster; vector vchPlainHDChainCode = vchHDChainCode; // Release wallet.dat so a child process can read it for the export. DBFlush(true); #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif string strSQLitePath = tmp + "/wallet.sqlite"; string strWalletDatPath = tmp + "/wallet.dat"; string strRightPassFile = tmp + "/right-pass.txt"; string strWrongPassFile = tmp + "/wrong-pass.txt"; nFail += Check(WriteTextFile(strRightPassFile, "btf-test-passphrase\n") && WriteTextFile(strWrongPassFile, "wrong-passphrase\n"), "passphrase files can be written for restarted commands") ? 0 : 1; // Step 1: export the Berkeley DB wallet to SQLite. vector vExportArgs; vExportArgs.push_back("-datadir=" + tmp); vExportArgs.push_back("-walletsqliteexport=" + strSQLitePath); vExportArgs.push_back("-nogui"); int nExportRet = RunBitflashChild(strExe, vExportArgs); nFail += Check(nExportRet == 0 && FileExists(strSQLitePath.c_str()), "the wallet exports to a SQLite file") ? 0 : 1; nFail += Check(FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), "the exported SQLite wallet holds the plaintext key before encryption") ? 0 : 1; // Step 2: encrypt through the real SQLite backend command path. string strEncryptOut = tmp + "/sqlite-encrypt-out.txt"; vector vEncryptArgs; vEncryptArgs.push_back("-datadir=" + tmp); vEncryptArgs.push_back("-walletbackend=sqlite"); vEncryptArgs.push_back("-encryptwallet=@" + strRightPassFile); vEncryptArgs.push_back("-nogui"); int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); nFail += Check(nEncryptRet == 0, "the SQLite wallet can be encrypted through -walletbackend=sqlite") ? 0 : 1; nFail += Check(FileContainsText(strEncryptOut, "Wallet backend: SQLite"), "encryption took the SQLite persistence path") ? 0 : 1; nFail += Check(FileContainsText(strEncryptOut, "wallet.sqlite.before-encrypt"), "the plaintext SQLite wallet is preserved as an explicit backup") ? 0 : 1; // The encrypted SQLite wallet must no longer hold the plaintext secrets. nFail += Check(!FileContainsBytes(strSQLitePath, vchDefaultPrivBytes), "the encrypted SQLite wallet no longer contains the default private key bytes") ? 0 : 1; nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDMaster), "the encrypted SQLite wallet no longer contains the HD master key bytes") ? 0 : 1; nFail += Check(!FileContainsBytes(strSQLitePath, vchPlainHDChainCode), "the encrypted SQLite wallet no longer contains the HD chain-code bytes") ? 0 : 1; // The Berkeley DB wallet.dat is a separate file: the SQLite encrypt path // must never touch it, so its plaintext key is still exactly there. nFail += Check(FileContainsBytes(strWalletDatPath, vchDefaultPrivBytes), "the original Berkeley DB wallet.dat is left untouched by the SQLite encrypt") ? 0 : 1; // Step 3: a locked encrypted SQLite wallet reports that phrase coverage // needs an unlock, exactly as the Berkeley DB path does (exit code 2). string strLockedAudit = tmp + "/locked-recovery-audit.txt"; vector vLockedArgs; vLockedArgs.push_back("-datadir=" + tmp); vLockedArgs.push_back("-walletbackend=sqlite"); vLockedArgs.push_back("-recoveryaudit"); vLockedArgs.push_back("-nogui"); int nLockedRet = RunBitflashChild(strExe, vLockedArgs, NULL, &strLockedAudit); nFail += Check(nLockedRet == 2 && FileContainsText(strLockedAudit, "recovery phrase: encrypted, unlock wallet to audit"), "a locked encrypted SQLite wallet reports that phrase coverage needs unlock") ? 0 : 1; // The wrong passphrase is rejected and reveals nothing. string strWrongDump = tmp + "/wrong-dump.txt"; vector vWrongArgs; vWrongArgs.push_back("-datadir=" + tmp); vWrongArgs.push_back("-walletbackend=sqlite"); vWrongArgs.push_back("-walletpassphrase=@" + strWrongPassFile); vWrongArgs.push_back("-dumpwallet=" + strWrongDump); vWrongArgs.push_back("-nogui"); int nWrongRet = RunBitflashChild(strExe, vWrongArgs); nFail += Check(nWrongRet != 0 && !FileExists(strWrongDump.c_str()), "a restarted SQLite wallet rejects the wrong passphrase") ? 0 : 1; // The right passphrase unlocks and can dump the original key. string strRightDump = tmp + "/right-dump.txt"; vector vRightArgs; vRightArgs.push_back("-datadir=" + tmp); vRightArgs.push_back("-walletbackend=sqlite"); vRightArgs.push_back("-walletpassphrase=@" + strRightPassFile); vRightArgs.push_back("-dumpwallet=" + strRightDump); vRightArgs.push_back("-nogui"); int nRightRet = RunBitflashChild(strExe, vRightArgs); nFail += Check(nRightRet == 0 && FileExists(strRightDump.c_str()), "a restarted SQLite wallet unlocks with the right passphrase") ? 0 : 1; nFail += Check(FileContainsText(strRightDump, HexStrLocal(vchDefaultPrivBytes)), "the unlocked SQLite wallet decrypts and dumps the original key") ? 0 : 1; // The recovery phrase survives the encryption. string strAudit = tmp + "/recovery-audit.txt"; vector vAuditArgs; vAuditArgs.push_back("-datadir=" + tmp); vAuditArgs.push_back("-walletbackend=sqlite"); vAuditArgs.push_back("-walletpassphrase=@" + strRightPassFile); vAuditArgs.push_back("-recoveryaudit"); vAuditArgs.push_back("-nogui"); int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); nFail += Check(nAuditRet == 0 && FileContainsText(strAudit, "recovery phrase: present"), "the encrypted SQLite wallet still has its recovery phrase") ? 0 : 1; // One-shot storage commands honour -walletbackend. The SQLite wallet is // encrypted; the wallet.dat beside it is still plaintext. Same datadir, // opposite answers -> the flag routes the diagnostic to the right file. string strSqliteAuditJson = tmp + "/sqlite-audit.json"; vector vSqliteAuditArgs; vSqliteAuditArgs.push_back("-datadir=" + tmp); vSqliteAuditArgs.push_back("-walletbackend=sqlite"); vSqliteAuditArgs.push_back("-walletstorageauditjson=" + strSqliteAuditJson); vSqliteAuditArgs.push_back("-nogui"); int nSqliteAuditRet = RunBitflashChild(strExe, vSqliteAuditArgs); nFail += Check(nSqliteAuditRet == 0 && FileContainsText(strSqliteAuditJson, "\"encrypted_hd_seed\": \"complete\"") && FileContainsText(strSqliteAuditJson, "\"plain_hd_seed\": \"none\""), "-walletstorageaudit under SQLite reads the encrypted wallet.sqlite") ? 0 : 1; string strBdbAuditJson = tmp + "/bdb-audit.json"; vector vBdbAuditArgs; vBdbAuditArgs.push_back("-datadir=" + tmp); vBdbAuditArgs.push_back("-walletstorageauditjson=" + strBdbAuditJson); vBdbAuditArgs.push_back("-nogui"); int nBdbAuditRet = RunBitflashChild(strExe, vBdbAuditArgs); nFail += Check(nBdbAuditRet == 0 && FileContainsText(strBdbAuditJson, "\"plain_hd_seed\": \"complete\"") && FileContainsText(strBdbAuditJson, "\"encrypted_hd_seed\": \"none\""), "-walletstorageaudit without the flag still reads the plaintext wallet.dat") ? 0 : 1; string strSqliteCheckOut = tmp + "/sqlite-check.txt"; vector vSqliteCheckArgs; vSqliteCheckArgs.push_back("-datadir=" + tmp); vSqliteCheckArgs.push_back("-walletbackend=sqlite"); vSqliteCheckArgs.push_back("-walletstoragecheck"); vSqliteCheckArgs.push_back("-nogui"); int nSqliteCheckRet = RunBitflashChild(strExe, vSqliteCheckArgs, NULL, &strSqliteCheckOut); nFail += Check(nSqliteCheckRet == 0, "-walletstoragecheck under SQLite passes on the encrypted wallet.sqlite") ? 0 : 1; // Backup under SQLite copies wallet.sqlite, not the plaintext wallet.dat. // The copy is a standalone SQLite wallet (loadcheck accepts it) and holds // none of the plaintext key that wallet.dat still contains. string strBackupDest = tmp + "/backup.sqlite"; vector vBackupArgs; vBackupArgs.push_back("-datadir=" + tmp); vBackupArgs.push_back("-walletbackend=sqlite"); vBackupArgs.push_back("-backupwallet=" + strBackupDest); vBackupArgs.push_back("-nogui"); int nBackupRet = RunBitflashChild(strExe, vBackupArgs); nFail += Check(nBackupRet == 0 && FileExists(strBackupDest.c_str()), "-backupwallet under SQLite writes a backup file") ? 0 : 1; nFail += Check(!FileContainsBytes(strBackupDest, vchDefaultPrivBytes), "the SQLite backup holds no plaintext key (it copied wallet.sqlite, not wallet.dat)") ? 0 : 1; string strBackupLoadCheck = tmp + "/backup-loadcheck.txt"; vector vLoadCheckArgs; vLoadCheckArgs.push_back("-datadir=" + tmp); vLoadCheckArgs.push_back("-walletsqliteloadcheck=" + strBackupDest); vLoadCheckArgs.push_back("-nogui"); int nBackupLoadRet = RunBitflashChild(strExe, vLoadCheckArgs, NULL, &strBackupLoadCheck); nFail += Check(nBackupLoadRet == 0, "the SQLite backup is a valid standalone SQLite wallet") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } // Backend resolution when no -walletbackend flag is given: a fresh install // starts on SQLite (the new default), an existing wallet.dat stays on Berkeley // DB, the persistent marker overrides the default, and an explicit flag // overrides everything. Each scenario runs a real child process. static int RunWalletBackendDefaultSelfTest() { fflush(stdout); printf("wallet-backend-default self-test\n"); std::string tmp, cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif try { // 1. Fresh datadir, no flag -> this fork refuses, where upstream creates. // An empty datadir on a headless node is far more often a mount or // backup fault than a first run, and creating a wallet also pins the // backend marker, after which restoring the real wallet is a no-op. // Nothing may be left behind: no wallet of either kind, no marker. string d1 = tmp + "/fresh"; nFail += Check(MakeDirLocal(d1), "fresh scenario dir can be created") ? 0 : 1; vector a1; a1.push_back("-datadir=" + d1); a1.push_back("-nomanagedtor"); a1.push_back("-nogui"); a1.push_back("-newaddress"); int r0 = RunBitflashChild(strExe, a1); nFail += Check(r0 != 0 && !FileExists((d1 + "/wallet.sqlite").c_str()) && !FileExists((d1 + "/wallet.dat").c_str()) && !FileExists((d1 + "/wallet-backend").c_str()), "a flagless headless start refuses to create a wallet") ? 0 : 1; // 1a. -createwallet says it was meant: new wallet.sqlite, marker // "sqlite", no wallet.dat. vector a1c = a1; a1c.push_back("-createwallet"); int r1 = RunBitflashChild(strExe, a1c); nFail += Check(r1 == 0 && FileExists((d1 + "/wallet.sqlite").c_str()) && !FileExists((d1 + "/wallet.dat").c_str()) && FileContainsText(d1 + "/wallet-backend", "sqlite"), "-createwallet starts a fresh install on SQLite and records the marker") ? 0 : 1; // 1b. Second run reuses it without the flag -- the refusal only guards // creation, so an existing wallet still starts flagless. int r1b = RunBitflashChild(strExe, a1); nFail += Check(r1b == 0 && FileExists((d1 + "/wallet.sqlite").c_str()) && !FileExists((d1 + "/wallet.dat").c_str()), "the recorded SQLite choice survives a flagless restart") ? 0 : 1; // 2. Existing wallet.dat, no flag -> stays on Berkeley DB. string d2 = tmp + "/existing-bdb"; nFail += Check(MakeDirLocal(d2), "existing-bdb scenario dir can be created") ? 0 : 1; vector a2c; a2c.push_back("-datadir=" + d2); a2c.push_back("-nomanagedtor"); a2c.push_back("-nogui"); a2c.push_back("-walletbackend=bdb"); a2c.push_back("-newphrase"); RunBitflashChild(strExe, a2c); vector a2; a2.push_back("-datadir=" + d2); a2.push_back("-nomanagedtor"); a2.push_back("-nogui"); a2.push_back("-newaddress"); int r2 = RunBitflashChild(strExe, a2); nFail += Check(r2 == 0 && FileExists((d2 + "/wallet.dat").c_str()) && !FileExists((d2 + "/wallet.sqlite").c_str()), "an existing wallet.dat keeps a flagless start on Berkeley DB") ? 0 : 1; // 3. Marker "bdb" forces Berkeley DB even on a fresh datadir. string d3 = tmp + "/marker-bdb"; nFail += Check(MakeDirLocal(d3), "marker-bdb scenario dir can be created") ? 0 : 1; nFail += Check(WriteTextFile(d3 + "/wallet-backend", "bdb\n"), "a bdb marker can be written") ? 0 : 1; vector a3; a3.push_back("-datadir=" + d3); a3.push_back("-nomanagedtor"); a3.push_back("-nogui"); a3.push_back("-newaddress"); int r3 = RunBitflashChild(strExe, a3); nFail += Check(r3 == 0 && FileExists((d3 + "/wallet.dat").c_str()) && !FileExists((d3 + "/wallet.sqlite").c_str()), "a bdb marker forces Berkeley DB on a fresh datadir") ? 0 : 1; // 4. Explicit -walletbackend=bdb on a fresh datadir -> wallet.dat. string d4 = tmp + "/explicit-bdb"; nFail += Check(MakeDirLocal(d4), "explicit-bdb scenario dir can be created") ? 0 : 1; vector a4; a4.push_back("-datadir=" + d4); a4.push_back("-nomanagedtor"); a4.push_back("-nogui"); a4.push_back("-walletbackend=bdb"); a4.push_back("-newaddress"); int r4 = RunBitflashChild(strExe, a4); nFail += Check(r4 == 0 && FileExists((d4 + "/wallet.dat").c_str()) && !FileExists((d4 + "/wallet.sqlite").c_str()), "an explicit -walletbackend=bdb creates wallet.dat on a fresh datadir") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } // The wizard's conversion action, headless: build a Berkeley DB wallet, run // DoConvertWalletToSQLite() (what the GUI button calls), and prove it produced a // usable wallet.sqlite + backend marker while leaving wallet.dat untouched, then // that a flagless restart opens the converted SQLite wallet. static int RunWalletConvertSelfTest() { fflush(stdout); printf("wallet-convert self-test\n"); std::string tmp, cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) { printf(" FAIL could not move into the temporary data directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; strSetDataDir = tmp; printf(" temp datadir: %s\n", tmp.c_str()); #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif try { if (!LoadWallet()) throw std::runtime_error("LoadWallet failed"); string strError; nFail += Check(SetHDSeedFromMnemonic( "abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon about", strError), "a phrase can be installed before conversion") ? 0 : 1; TopUpKeyPool(); vector vchDefaultPubKey = keyUser.GetPubKey(); CPrivKey vchDefaultPrivKey; nFail += Check(GetWalletPrivKey(vchDefaultPubKey, vchDefaultPrivKey, strError), "the default private key is readable before conversion") ? 0 : 1; vector vchDefaultPrivBytes(vchDefaultPrivKey.begin(), vchDefaultPrivKey.end()); // The conversion action the GUI button invokes. int nCopied = 0; string strConvertError; bool fConverted = DoConvertWalletToSQLite(nCopied, strConvertError); nFail += Check(fConverted && nCopied > 0, "DoConvertWalletToSQLite exports the wallet") ? 0 : 1; nFail += Check(FileExists((tmp + "/wallet.sqlite").c_str()) && FileContainsText(tmp + "/wallet-backend", "sqlite"), "conversion writes wallet.sqlite and records the sqlite marker") ? 0 : 1; nFail += Check(FileContainsBytes(tmp + "/wallet.dat", vchDefaultPrivBytes), "wallet.dat is left untouched by the conversion") ? 0 : 1; // Idempotence guard: a second conversion refuses rather than overwrite. int nCopied2 = 0; string strSecondError; nFail += Check(!DoConvertWalletToSQLite(nCopied2, strSecondError), "a second conversion refuses to overwrite wallet.sqlite") ? 0 : 1; DBFlush(true); // A flagless restart now opens the converted SQLite wallet (the marker // routes it) and finds the same recovery phrase. string strAudit = tmp + "/converted-recovery-audit.txt"; vector vAuditArgs; vAuditArgs.push_back("-datadir=" + tmp); vAuditArgs.push_back("-nomanagedtor"); vAuditArgs.push_back("-nogui"); vAuditArgs.push_back("-recoveryaudit"); int nAuditRet = RunBitflashChild(strExe, vAuditArgs, NULL, &strAudit); nFail += Check(nAuditRet == 0 && FileContainsText(strAudit, "recovery phrase: present"), "a flagless restart opens the converted SQLite wallet") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } DBFlush(true); SetCurrentDir(cwd); RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunWalletPortabilitySelfTest() { fflush(stdout); printf("wallet-portability self-test\n"); std::string tmp; std::string cwd; if (!MakeTempDir(tmp)) { printf(" FAIL could not create a temporary data directory\n"); return 1; } if (!GetCurrentDir(cwd)) { printf(" FAIL could not read current directory\n"); RemoveTree(tmp); return 1; } int nFail = 0; printf(" temp root: %s\n", tmp.c_str()); try { #ifdef _WIN32 string strExe = cwd + "\\bitflash.exe"; #else string strExe = cwd + "/bitflash-node"; #endif string strOriginal = tmp + "/original"; string strRawCopy = tmp + "/raw-copy"; string strBackupCopy = tmp + "/backup-copy"; nFail += Check(MakeDirLocal(strOriginal) && MakeDirLocal(strRawCopy) && MakeDirLocal(strBackupCopy), "temporary wallet directories can be created") ? 0 : 1; string strCreateOut = tmp + "/create.txt"; vector vCreateArgs; vCreateArgs.push_back("-datadir=" + strOriginal); vCreateArgs.push_back("-nomanagedtor"); vCreateArgs.push_back("-nogui"); vCreateArgs.push_back("-walletbackend=bdb"); // this suite is about wallet.dat portability vCreateArgs.push_back("-newphrase"); int nCreateRet = RunBitflashChild(strExe, vCreateArgs, NULL, &strCreateOut); nFail += Check(nCreateRet == 0 && FileContainsText(strCreateOut, "Write these twelve words down"), "a phrase wallet can be created in a fresh datadir") ? 0 : 1; string strOriginalAudit = tmp + "/original-audit.txt"; vector vOriginalAuditArgs; vOriginalAuditArgs.push_back("-datadir=" + strOriginal); vOriginalAuditArgs.push_back("-nomanagedtor"); vOriginalAuditArgs.push_back("-nogui"); vOriginalAuditArgs.push_back("-recoveryaudit"); int nOriginalAuditRet = RunBitflashChild(strExe, vOriginalAuditArgs, NULL, &strOriginalAudit); nFail += Check(nOriginalAuditRet == 0 && FileContainsText(strOriginalAudit, "recovery phrase: present") && FileContainsText(strOriginalAudit, "SLIP-0044 BITFLASH"), "the original wallet reports its recovery phrase") ? 0 : 1; string strOriginalStorageAudit = tmp + "/original-storage-audit.txt"; vector vOriginalStorageAuditArgs; vOriginalStorageAuditArgs.push_back("-datadir=" + strOriginal); vOriginalStorageAuditArgs.push_back("-nomanagedtor"); vOriginalStorageAuditArgs.push_back("-nogui"); vOriginalStorageAuditArgs.push_back("-walletstorageaudit"); int nOriginalStorageAuditRet = RunBitflashChild(strExe, vOriginalStorageAuditArgs, NULL, &strOriginalStorageAudit); nFail += Check(nOriginalStorageAuditRet == 0 && FileContainsText(strOriginalStorageAudit, "Wallet storage audit") && FileContainsText(strOriginalStorageAudit, "plain HD seed: complete") && FileContainsText(strOriginalStorageAudit, "encrypted HD seed: none") && FileContainsText(strOriginalStorageAudit, "malformed records: 0"), "the storage audit recognizes a plaintext phrase wallet") ? 0 : 1; string strOriginalStorageCheck = tmp + "/original-storage-check.txt"; vector vOriginalStorageCheckArgs; vOriginalStorageCheckArgs.push_back("-datadir=" + strOriginal); vOriginalStorageCheckArgs.push_back("-nomanagedtor"); vOriginalStorageCheckArgs.push_back("-nogui"); vOriginalStorageCheckArgs.push_back("-walletstoragecheck"); int nOriginalStorageCheckRet = RunBitflashChild(strExe, vOriginalStorageCheckArgs, NULL, &strOriginalStorageCheck); nFail += Check(nOriginalStorageCheckRet == 0 && FileContainsText(strOriginalStorageCheck, "storage sanity: ok") && FileContainsText(strOriginalStorageCheck, "plain HD seed: complete"), "the storage sanity check accepts a plaintext phrase wallet") ? 0 : 1; string strOriginalStorageAuditJson = tmp + "/original-storage-audit.json"; vector vOriginalStorageAuditJsonArgs; vOriginalStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); vOriginalStorageAuditJsonArgs.push_back("-nomanagedtor"); vOriginalStorageAuditJsonArgs.push_back("-nogui"); vOriginalStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + strOriginalStorageAuditJson); int nOriginalStorageAuditJsonRet = RunBitflashChild(strExe, vOriginalStorageAuditJsonArgs); nFail += Check(nOriginalStorageAuditJsonRet == 0 && FileContainsText(strOriginalStorageAuditJson, "\"format\": \"bitflash-wallet-storage-audit-v1\"") && FileContainsText(strOriginalStorageAuditJson, "\"plain_hd_seed\": \"complete\"") && FileContainsText(strOriginalStorageAuditJson, "\"encrypted_hd_seed\": \"none\""), "the storage audit can write plaintext-wallet JSON") ? 0 : 1; string strPortableWallet = tmp + "/portable-wallet.dat"; string strBackupOut = tmp + "/backupwallet.txt"; vector vBackupArgs; vBackupArgs.push_back("-datadir=" + strOriginal); vBackupArgs.push_back("-nomanagedtor"); vBackupArgs.push_back("-nogui"); vBackupArgs.push_back("-backupwallet=" + strPortableWallet); int nBackupRet = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut); nFail += Check(nBackupRet == 0 && FileExists(strPortableWallet.c_str()), "/backupwallet writes a portable wallet.dat") ? 0 : 1; string strBackupOut2 = tmp + "/backupwallet-overwrite.txt"; int nBackupRet2 = RunBitflashChild(strExe, vBackupArgs, NULL, &strBackupOut2); nFail += Check(nBackupRet2 == 0 && FileExists(strPortableWallet.c_str()) && !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.tmp.") && !DirectoryHasFileWithPrefix(tmp, "portable-wallet.dat.old."), "/backupwallet replaces an existing backup without leaving temp files") ? 0 : 1; nFail += Check(CopyFileLocal(strOriginal + "/wallet.dat", strRawCopy + "/wallet.dat"), "a raw wallet.dat can be copied without database logs") ? 0 : 1; string strRawAudit = tmp + "/raw-copy-audit.txt"; vector vRawAuditArgs; vRawAuditArgs.push_back("-datadir=" + strRawCopy); vRawAuditArgs.push_back("-nomanagedtor"); vRawAuditArgs.push_back("-nogui"); vRawAuditArgs.push_back("-recoveryaudit"); int nRawAuditRet = RunBitflashChild(strExe, vRawAuditArgs, NULL, &strRawAudit); nFail += Check(nRawAuditRet == 0 && FileContainsText(strRawAudit, "recovery phrase: present") && !FileContainsText(strRawAudit, "Cannot open wallet.dat"), "a raw wallet.dat copy opens in a new datadir") ? 0 : 1; nFail += Check(CopyFileLocal(strPortableWallet, strBackupCopy + "/wallet.dat"), "the portable backup can be installed as wallet.dat") ? 0 : 1; string strBackupAudit = tmp + "/backup-copy-audit.txt"; vector vBackupAuditArgs; vBackupAuditArgs.push_back("-datadir=" + strBackupCopy); vBackupAuditArgs.push_back("-nomanagedtor"); vBackupAuditArgs.push_back("-nogui"); vBackupAuditArgs.push_back("-recoveryaudit"); int nBackupAuditRet = RunBitflashChild(strExe, vBackupAuditArgs, NULL, &strBackupAudit); nFail += Check(nBackupAuditRet == 0 && FileContainsText(strBackupAudit, "recovery phrase: present") && FileContainsText(strBackupAudit, "SLIP-0044 BITFLASH"), "a /backupwallet copy opens in a new datadir") ? 0 : 1; string strPassFile = tmp + "/encrypt-pass.txt"; string strEncryptOut = tmp + "/encryptwallet.txt"; nFail += Check(WriteTextFile(strPassFile, "portable-test-passphrase\n"), "an encryption passphrase file can be written") ? 0 : 1; vector vEncryptArgs; vEncryptArgs.push_back("-datadir=" + strOriginal); vEncryptArgs.push_back("-nomanagedtor"); vEncryptArgs.push_back("-nogui"); vEncryptArgs.push_back("-encryptwallet=@" + strPassFile); int nEncryptRet = RunBitflashChild(strExe, vEncryptArgs, NULL, &strEncryptOut); nFail += Check(nEncryptRet == 0 && !DirectoryHasFileWithPrefix(strOriginal + "/database", "log."), "the encryptwallet command purges Berkeley DB environment logs") ? 0 : 1; string strEncryptedStorageAudit = tmp + "/encrypted-storage-audit.txt"; vector vEncryptedStorageAuditArgs; vEncryptedStorageAuditArgs.push_back("-datadir=" + strOriginal); vEncryptedStorageAuditArgs.push_back("-nomanagedtor"); vEncryptedStorageAuditArgs.push_back("-nogui"); vEncryptedStorageAuditArgs.push_back("-walletstorageaudit"); int nEncryptedStorageAuditRet = RunBitflashChild(strExe, vEncryptedStorageAuditArgs, NULL, &strEncryptedStorageAudit); nFail += Check(nEncryptedStorageAuditRet == 0 && FileContainsText(strEncryptedStorageAudit, "plain private keys: 0") && FileContainsText(strEncryptedStorageAudit, "encrypted private keys:") && FileContainsText(strEncryptedStorageAudit, "plain HD seed: none") && FileContainsText(strEncryptedStorageAudit, "encrypted HD seed: complete") && FileContainsText(strEncryptedStorageAudit, "wallet minimum version: present") && FileContainsText(strEncryptedStorageAudit, "malformed records: 0"), "the storage audit recognizes an encrypted wallet") ? 0 : 1; string strEncryptedStorageCheck = tmp + "/encrypted-storage-check.txt"; vector vEncryptedStorageCheckArgs; vEncryptedStorageCheckArgs.push_back("-datadir=" + strOriginal); vEncryptedStorageCheckArgs.push_back("-nomanagedtor"); vEncryptedStorageCheckArgs.push_back("-nogui"); vEncryptedStorageCheckArgs.push_back("-walletstoragecheck"); int nEncryptedStorageCheckRet = RunBitflashChild(strExe, vEncryptedStorageCheckArgs, NULL, &strEncryptedStorageCheck); nFail += Check(nEncryptedStorageCheckRet == 0 && FileContainsText(strEncryptedStorageCheck, "storage sanity: ok") && FileContainsText(strEncryptedStorageCheck, "plain private keys: 0") && FileContainsText(strEncryptedStorageCheck, "encrypted HD seed: complete"), "the storage sanity check accepts an encrypted wallet") ? 0 : 1; string strEncryptedStorageAuditJson = tmp + "/encrypted-storage-audit.json"; vector vEncryptedStorageAuditJsonArgs; vEncryptedStorageAuditJsonArgs.push_back("-datadir=" + strOriginal); vEncryptedStorageAuditJsonArgs.push_back("-nomanagedtor"); vEncryptedStorageAuditJsonArgs.push_back("-nogui"); vEncryptedStorageAuditJsonArgs.push_back("-walletstorageauditjson=" + strEncryptedStorageAuditJson); int nEncryptedStorageAuditJsonRet = RunBitflashChild(strExe, vEncryptedStorageAuditJsonArgs); nFail += Check(nEncryptedStorageAuditJsonRet == 0 && FileContainsText(strEncryptedStorageAuditJson, "\"plain_private_keys\": 0") && FileContainsText(strEncryptedStorageAuditJson, "\"plain_hd_seed\": \"none\"") && FileContainsText(strEncryptedStorageAuditJson, "\"encrypted_hd_seed\": \"complete\"") && FileContainsText(strEncryptedStorageAuditJson, "\"wallet_minimum_version\": \"present\""), "the storage audit can write encrypted-wallet JSON") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } RemoveTree(tmp); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunNetMessageSelfTest() { fflush(stdout); printf("net-message self-test\n"); int nFail = 0; try { // A peer that has not yet been kept waiting must not look like one that // has been waiting since the epoch. This is the whole bug: the stall // clock lives in CNode, so only the constructor can start it at zero. CNode opened(INVALID_SOCKET, CAddress("127.0.0.1")); nFail += Check(opened.nMsgWaitSince == 0 && opened.nMsgWaitSize == 0, "a new peer starts with its stall clock stopped") ? 0 : 1; CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); complete.nVersion = VERSION; complete.vRecv << CMessageHeader("ping", 0); nFail += Check(ProcessMessages(&complete), "a complete empty message processes") ? 0 : 1; nFail += Check(complete.vRecv.empty(), "a complete message is consumed") ? 0 : 1; nFail += Check(!complete.fDisconnect, "a valid ping does not disconnect the peer") ? 0 : 1; CNode partial(INVALID_SOCKET, CAddress("127.0.0.1")); partial.nVersion = VERSION; partial.vRecv << CMessageHeader("block", 100); unsigned int nPartialBefore = partial.vRecv.size(); nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; nFail += Check(partial.vRecv.size() == nPartialBefore, "an incomplete message keeps one header in the buffer") ? 0 : 1; nFail += Check(partial.nMsgWaitSince != 0, "an incomplete message starts the stall clock") ? 0 : 1; nFail += Check(partial.nMsgWaitSize == 0, "the stall clock records how much payload had arrived") ? 0 : 1; nFail += Check(!partial.fDisconnect, "a fresh incomplete message does not disconnect immediately") ? 0 : 1; // Still arriving, just slowly: more payload is here than when the clock // was started, so the clock restarts however old it was. Timing the // wait instead of the stall would drop this peer. CNode slow(INVALID_SOCKET, CAddress("127.0.0.1")); slow.nVersion = VERSION; slow.vRecv << CMessageHeader("block", 100); slow.vRecv.insert(slow.vRecv.end(), 50, '\0'); slow.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1; slow.nMsgWaitSize = 10; ProcessMessages(&slow); nFail += Check(!slow.fDisconnect, "a peer still sending payload is not dropped") ? 0 : 1; nFail += Check(slow.nMsgWaitSize == 50, "progress restarts the stall clock") ? 0 : 1; // Nothing more has arrived since the clock started, and the clock is // older than the limit. CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); stale.nVersion = VERSION; stale.vRecv << CMessageHeader("block", 100); stale.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1; stale.nMsgWaitSize = 0; ProcessMessages(&stale); nFail += Check(stale.fDisconnect, "a stalled incomplete message disconnects the peer") ? 0 : 1; CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); oversized.nVersion = VERSION; oversized.vRecv << CMessageHeader("block", MAX_PROTOCOL_MESSAGE_SIZE + 1); ProcessMessages(&oversized); nFail += Check(oversized.fDisconnect, "an oversized message header disconnects the peer") ? 0 : 1; } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunNetworkParamsSelfTest() { fflush(stdout); printf("network-params self-test\n"); int nFail = 0; std::string strOldDataDir = strSetDataDir; try { SelectChainParams(false); nFail += Check(!IsTestNet(), "starts on mainnet after explicit selection") ? 0 : 1; nFail += Check(ntohs(GetDefaultPort()) == MAINNET_PORT, "mainnet default P2P port is 8433") ? 0 : 1; CAddress mainAddr("127.0.0.1"); nFail += Check(mainAddr.port == htons(MAINNET_PORT), "mainnet address parsing uses the mainnet default port") ? 0 : 1; CMessageHeader mainHeader("ping", 0); char mainMagic[4]; memcpy(mainMagic, mainHeader.pchMessageStart, sizeof(mainMagic)); uint256 mainGenesis = hashGenesisBlock; uint256 mainMerkle = hashGenesisMerkleRoot; unsigned int mainNonce = GENESIS_NONCE; SelectChainParams(true); nFail += Check(IsTestNet(), "testnet selection is sticky") ? 0 : 1; nFail += Check(ntohs(GetDefaultPort()) == TESTNET_PORT, "testnet default P2P port is 18433") ? 0 : 1; CAddress testAddr("127.0.0.1"); nFail += Check(testAddr.port == htons(TESTNET_PORT), "testnet address parsing uses the testnet default port") ? 0 : 1; CMessageHeader testHeader("ping", 0); nFail += Check(memcmp(mainMagic, testHeader.pchMessageStart, sizeof(mainMagic)) != 0, "testnet message magic differs from mainnet") ? 0 : 1; nFail += Check(hashGenesisBlock != mainGenesis && hashGenesisMerkleRoot != mainMerkle && GENESIS_NONCE != mainNonce, "testnet genesis constants differ from mainnet") ? 0 : 1; nFail += Check(hashGenesisBlock == uint256("0xa9d11c6d697bcb7aea0653bc088b12dcf9f44e732df205162cf085fd9d9963eb") && hashGenesisMerkleRoot == uint256("0x510b293105e3ff85b8d4c3920e357f85d1a86bf199eb4e7d11f04f380030ec5c") && GENESIS_NONCE == 3199, "testnet genesis constants are pinned") ? 0 : 1; std::string tmp; if (!MakeTempDir(tmp)) { nFail += Check(false, "creates a temporary data directory") ? 0 : 1; } else { strSetDataDir = tmp; SelectChainParams(false); std::string mainDir = GetAppDir(); SelectChainParams(true); std::string testDir = GetAppDir(); nFail += Check(mainDir == tmp, "mainnet uses the selected data directory") ? 0 : 1; nFail += Check(testDir == tmp + "/testnet", "testnet stores data below a testnet subdirectory") ? 0 : 1; RemoveTree(tmp); } } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } strSetDataDir = strOldDataDir; SelectChainParams(false); printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static CBlock MakeSizedConsensusBlock(size_t nScriptBytes) { CTransaction tx; tx.vin.push_back(CTxIn(COutPoint(), CScript() << 1 << 1)); tx.vout.push_back(CTxOut(0, CScript())); tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nScriptBytes, 0); CBlock block; block.vtx.push_back(tx); block.hashMerkleRoot = block.BuildMerkleTree(); block.nVersion = 1; block.nTime = GetAdjustedTime(); block.nBits = bnProofOfWorkLimit.GetCompact(); block.nNonce = 0; return block; } // A distinct transaction of a chosen weight and verification cost. nUnique // only keeps the txids apart, so a mempool built from these is not a pile of // duplicates a block could never carry anyway. static CTransaction MakeSizedTx(size_t nPadBytes, unsigned int nSigOps, int nUnique) { CTransaction tx; tx.vin.push_back(CTxIn(COutPoint(), CScript() << (int64)nUnique << 1)); tx.vout.push_back(CTxOut(0, CScript())); for (unsigned int i = 0; i < nSigOps; i++) tx.vout[0].scriptPubKey << OP_CHECKSIG; tx.vout[0].scriptPubKey.insert(tx.vout[0].scriptPubKey.end(), nPadBytes, 0); return tx; } static CBlock MakeCoinbaseOnlyTemplate() { CTransaction txNew; txNew.vin.resize(1); txNew.vin[0].prevout.SetNull(); txNew.vin[0].scriptSig << 1 << 1 << 1; txNew.vout.resize(1); txNew.vout[0].scriptPubKey << OP_CHECKSIG; CBlock block; block.vtx.push_back(txNew); block.nVersion = 1; block.nTime = GetAdjustedTime(); block.nBits = bnProofOfWorkLimit.GetCompact(); block.nNonce = 0; return block; } // The sum CheckBlock() takes, spelled the same way. static unsigned int BlockSigOpCount(const CBlock& block) { unsigned int n = 0; for (int i = 0; i < (int)block.vtx.size(); i++) n += block.vtx[i].GetSigOpCount(); return n; } // Drive the selection rule the way both assemblers drive it: ask // BlockTemplateFits before appending, skip what does not fit, never stop early // on one that does not. // // This mirrors the loops in BitcoinMiner() and RebuildJob() rather than calling // them -- one needs a CTxDB, a wallet key and cs_main, the other is static in // rpc.cpp, so neither is reachable from here. What that leaves proven is the // rule the two of them share and the property it is meant to guarantee, which // is where the defect actually lived: the budget was right and the enforcement // was not. static void SelectIntoTemplate(CBlock& block, const vector& vMempool) { unsigned int nBlockSize = ::GetSerializeSize(block, SER_DISK); unsigned int nBlockSigOps = block.vtx[0].GetSigOpCount(); for (int i = 0; i < (int)vMempool.size(); i++) { const CTransaction& tx = vMempool[i]; unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK); unsigned int nTxSigOps = tx.GetSigOpCount(); if (!BlockTemplateFits(nBlockSize, nBlockSigOps, nTxSize, nTxSigOps)) continue; block.vtx.push_back(tx); nBlockSize += nTxSize; nBlockSigOps += nTxSigOps; } } static int RunConsensusLimitsSelfTest() { fflush(stdout); printf("consensus-limits self-test\n"); int nFail = 0; try { nFail += Check(MAX_BLOCK_SIZE == 1000000, "the consensus block-size cap is 1 MB") ? 0 : 1; nFail += Check(MAX_BLOCK_SIZE < MAX_SIZE, "the block cap is tighter than the serializer cap") ? 0 : 1; CBlock small = MakeSizedConsensusBlock(100); nFail += Check(small.CheckSizeLimits(), "a small block is within the consensus size limit") ? 0 : 1; CBlock oversized = MakeSizedConsensusBlock(MAX_BLOCK_SIZE); unsigned int nSerialized = ::GetSerializeSize(oversized, SER_DISK); nFail += Check(nSerialized > MAX_BLOCK_SIZE && nSerialized <= MAX_SIZE, "the test block sits between 1 MB and the old 32 MB cap") ? 0 : 1; nFail += Check(!oversized.CheckSizeLimits(), "a block above 1 MB is outside the consensus size limit") ? 0 : 1; // The other half of the cap. Everything above asks what validation // refuses; nothing asked what assembly produces, which is why a miner // that could overshoot the cap by an entire mempool went unnoticed. nFail += Check(MAX_BLOCK_SIZE_GEN < MAX_BLOCK_SIZE, "the assembly budget stays under the consensus cap") ? 0 : 1; // The rule itself, at its edges. The sigop comparison has to match // CheckBlock(), which fails on `> MAX_BLOCK_SIGOPS` and so accepts a // block sitting exactly on the limit. nFail += Check(BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 100, 0), "a transaction that exactly fills the size budget fits") ? 0 : 1; nFail += Check(!BlockTemplateFits(MAX_BLOCK_SIZE_GEN - 100, 0, 101, 0), "one byte past the size budget does not fit") ? 0 : 1; nFail += Check(BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS), "sigops exactly on the limit fit, as CheckBlock() allows") ? 0 : 1; nFail += Check(!BlockTemplateFits(0, 0, 0, MAX_BLOCK_SIGOPS + 1), "one sigop past the limit does not fit") ? 0 : 1; // A mempool several times the consensus cap, in ordinary-sized // transactions. The old miner tested its budget only on the outer // loop, so a single pass appended all of this. { CBlock block = MakeCoinbaseOnlyTemplate(); vector vMempool; for (int i = 0; i < 300; i++) vMempool.push_back(MakeSizedTx(10000, 1, i)); SelectIntoTemplate(block, vMempool); block.hashMerkleRoot = block.BuildMerkleTree(); nFail += Check(block.vtx.size() > 1, "a full mempool still fills a template") ? 0 : 1; nFail += Check(::GetSerializeSize(block, SER_DISK) <= MAX_BLOCK_SIZE_GEN, "a template built from a 3 MB mempool respects the budget") ? 0 : 1; nFail += Check(block.CheckSizeLimits(), "that template is one CheckBlock() would accept on size") ? 0 : 1; } // One transaction too big for the budget. Nothing on the path into the // mempool caps transaction size, so this is a transaction a peer can // simply send. It must be skipped and the rest still taken -- skipped, // not stopped on. { CBlock block = MakeCoinbaseOnlyTemplate(); vector vMempool; vMempool.push_back(MakeSizedTx(1500000, 1, 9999)); for (int i = 0; i < 10; i++) vMempool.push_back(MakeSizedTx(1000, 1, i)); unsigned int nHugeSize = ::GetSerializeSize(vMempool[0], SER_NETWORK); nFail += Check(nHugeSize > MAX_BLOCK_SIZE && nHugeSize <= MAX_SIZE, "the outsized transaction is one the mempool would hold") ? 0 : 1; SelectIntoTemplate(block, vMempool); block.hashMerkleRoot = block.BuildMerkleTree(); bool fTookHuge = false; for (int i = 0; i < (int)block.vtx.size(); i++) if (block.vtx[i].GetHash() == vMempool[0].GetHash()) fTookHuge = true; nFail += Check(!fTookHuge, "a transaction larger than the budget is left out") ? 0 : 1; nFail += Check(block.vtx.size() == 11, "the ten that do fit are taken anyway") ? 0 : 1; nFail += Check(block.CheckSizeLimits(), "one outsized transaction cannot invalidate a template") ? 0 : 1; } // Transactions that are cheap in bytes and expensive to verify. Size // alone does not bound what a block costs, and CheckBlock() refuses on // the count regardless of how small the block is. { CBlock block = MakeCoinbaseOnlyTemplate(); vector vMempool; for (int i = 0; i < 100; i++) vMempool.push_back(MakeSizedTx(0, 1000, i)); SelectIntoTemplate(block, vMempool); block.hashMerkleRoot = block.BuildMerkleTree(); nFail += Check(block.vtx.size() > 1, "sigop-heavy transactions are still selected") ? 0 : 1; nFail += Check(BlockSigOpCount(block) <= MAX_BLOCK_SIGOPS, "selection stops at the sigop cap CheckBlock() enforces") ? 0 : 1; nFail += Check(::GetSerializeSize(block, SER_DISK) < MAX_BLOCK_SIZE_GEN, "and stops on sigops while still well inside the size budget") ? 0 : 1; nFail += Check(block.CheckSizeLimits(), "the sigop-bounded template is within the size limit too") ? 0 : 1; } } catch (const std::exception& e) { printf(" FAIL exception: %s\n", e.what()); nFail++; } catch (...) { printf(" FAIL unknown exception\n"); nFail++; } printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunParseMoneySelfTest() { printf("parse-money self-test\n"); int nFail = 0; struct Case { const char* in; bool ok; int64 val; }; Case cases[] = { {"0.5", true, 50000000LL}, {"1", true, 100000000LL}, {"1.23456789", true, 123456789LL}, {"0.00000001", true, 1LL}, {"10.00000000", true, 1000000000LL}, {"1,000", true, 100000000000LL}, {"1.5 ", true, 150000000LL}, {"0.123456789", false, 0LL}, {"1.2.3", false, 0LL}, {"abc", false, 0LL}, {"-1", false, 0LL}, {"1e5", false, 0LL}, }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { int64 v = -1; bool r = ParseMoney(cases[i].in, v); bool pass = (r == cases[i].ok) && (!r || v == cases[i].val); char desc[160]; snprintf(desc, sizeof(desc), "ParseMoney(\"%s\") %s and value matches", cases[i].in, cases[i].ok ? "accepts" : "rejects"); nFail += Check(pass, desc) ? 0 : 1; } printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static bool SelfTestReadN(SOCKET s, void* buf, int n) { char* p = (char*)buf; int off = 0; while (off < n) { int r = recv(s, p + off, n - off, 0); if (r <= 0) return false; off += r; } return true; } static bool SelfTestWriteN(SOCKET s, const void* buf, int n) { const char* p = (const char*)buf; int off = 0; while (off < n) { int r = send(s, p + off, n - off, 0); if (r <= 0) return false; off += r; } return true; } static void SelfTestSetSocketTimeout(SOCKET s, int nTimeoutSecs) { #ifdef _WIN32 DWORD tv = (DWORD)nTimeoutSecs * 1000; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)); #else struct timeval tv; tv.tv_sec = nTimeoutSecs; tv.tv_usec = 0; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); #endif } #ifdef _WIN32 struct SelfTestWinsock { bool fStarted; SelfTestWinsock() : fStarted(false) { WSADATA wsadata; fStarted = (WSAStartup(MAKEWORD(2,2), &wsadata) == 0); } ~SelfTestWinsock() { if (fStarted) WSACleanup(); } }; #endif static bool RunSocks5HandshakeProbe(std::string& errOut) { errOut.clear(); #ifdef _WIN32 SelfTestWinsock winsock; if (!winsock.fStarted) { errOut = "could not start Winsock"; return false; } #endif SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listener == INVALID_SOCKET) { errOut = "could not create listener"; return false; } SelfTestSetSocketTimeout(listener, 5); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) != 0 || listen(listener, 1) != 0) { closesocket(listener); errOut = "could not bind/listen on loopback"; return false; } #ifdef _WIN32 int addrLen = sizeof(addr); #else socklen_t addrLen = sizeof(addr); #endif if (getsockname(listener, (struct sockaddr*)&addr, &addrLen) != 0) { closesocket(listener); errOut = "could not read listener port"; return false; } unsigned short proxyPort = ntohs(addr.sin_port); bool fServerOk = false; std::string serverErr; std::string requestedHost; unsigned short requestedPort = 0; std::thread server([&]() { SOCKET s = accept(listener, NULL, NULL); if (s == INVALID_SOCKET) { serverErr = "accept failed"; return; } SelfTestSetSocketTimeout(s, 5); unsigned char hello[3] = {0,0,0}; if (!SelfTestReadN(s, hello, sizeof(hello)) || hello[0] != 0x05 || hello[1] != 0x01 || hello[2] != 0x00) { serverErr = "bad SOCKS5 greeting"; closesocket(s); return; } unsigned char choice[2] = {0x05, 0x00}; if (!SelfTestWriteN(s, choice, sizeof(choice))) { serverErr = "could not write method choice"; closesocket(s); return; } unsigned char hdr[5] = {0,0,0,0,0}; if (!SelfTestReadN(s, hdr, sizeof(hdr)) || hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00 || hdr[3] != 0x03) { serverErr = "CONNECT did not use domain-name address type"; closesocket(s); return; } unsigned char len = hdr[4]; std::vector rest((size_t)len + 2); if (!SelfTestReadN(s, &rest[0], (int)rest.size())) { serverErr = "could not read CONNECT target"; closesocket(s); return; } requestedHost.assign((const char*)&rest[0], (size_t)len); requestedPort = ((unsigned short)rest[len] << 8) | rest[len + 1]; unsigned char reply[10] = {0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0}; if (!SelfTestWriteN(s, reply, sizeof(reply))) { serverErr = "could not write CONNECT reply"; closesocket(s); return; } fServerOk = true; closesocket(s); }); std::string err; BtfClearSocks5Proxy(); bool fClientOk = BtfSetSocks5Proxy(strprintf("127.0.0.1:%u", (unsigned)proxyPort), err); SOCKET client = INVALID_SOCKET; if (fClientOk) client = BtfConnectSocket("relay.example", 443, SOCK_NOSTR, 5); if (client != INVALID_SOCKET) BtfCloseSocket(client); BtfClearSocks5Proxy(); closesocket(listener); server.join(); if (!fClientOk) errOut = strprintf("could not enable proxy: %s", err.c_str()); else if (client == INVALID_SOCKET) errOut = "client SOCKS5 connect failed"; else if (!fServerOk) errOut = serverErr.empty() ? "mock server failed" : serverErr; else if (requestedHost != "relay.example" || requestedPort != 443) errOut = strprintf("unexpected CONNECT target %s:%u", requestedHost.c_str(), (unsigned)requestedPort); else return true; return false; } static int RunSocks5ProxySelfTest() { printf("socks5-proxy self-test\n"); int nFail = 0; std::string host, err; unsigned short port = 0; nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) && host == "127.0.0.1" && port == 9050, "parses local Tor proxy endpoint") ? 0 : 1; nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) && host == "localhost" && port == 9050, "parses hostname proxy endpoint") ? 0 : 1; nFail += Check(BtfParseSocks5Proxy("[::1]:9050", host, port, err) && host == "::1" && port == 9050, "parses bracketed IPv6 proxy endpoint") ? 0 : 1; nFail += Check(!BtfParseSocks5Proxy("::1:9050", host, port, err), "rejects unbracketed IPv6 proxy endpoint") ? 0 : 1; nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err), "rejects missing port") ? 0 : 1; nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err), "rejects zero port") ? 0 : 1; nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err), "rejects out-of-range port") ? 0 : 1; nFail += Check(!BtfParseSocks5Proxy("localhost:999999999999999999999999999999", host, port, err), "rejects overflowing port") ? 0 : 1; nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err), "rejects unsupported authenticated proxy syntax") ? 0 : 1; nFail += Check(BtfIsTorOnionHost("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion") && BtfIsTorOnionHost("Relay.Example.ONION.") && !BtfIsTorOnionHost("relay.example") && !BtfIsTorOnionHost(".onion"), "detects .onion hosts without accepting lookalikes") ? 0 : 1; std::string onion; nFail += Check(btf::NormalizeOnionEndpoint("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD.onion.:8433", onion) && onion == "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433", "normalizes v3 onion peer endpoint") ? 0 : 1; nFail += Check(!btf::NormalizeOnionEndpoint("short.onion:8433", onion) && !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:0", onion) && !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc1.onion:8433", onion), "rejects malformed onion peer endpoints") ? 0 : 1; BtfClearSocks5Proxy(); nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) && BtfSocks5ProxyEnabled() && BtfSocks5ProxyName() == "127.0.0.1:9050", "enables parsed SOCKS5 proxy") ? 0 : 1; BtfClearSocks5Proxy(); nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1; nFail += Check(BtfSetSocks5Proxy("[::1]:9050", err) && BtfSocks5ProxyEnabled() && BtfSocks5ProxyName() == "[::1]:9050", "enables bracketed IPv6 proxy endpoint") ? 0 : 1; BtfClearSocks5Proxy(); nFail += Check(BtfEnableTorProxy("", err) && BtfSocks5ProxyEnabled() && BtfTorProxyEnabled() && BtfSocks5ProxyName() == "127.0.0.1:9050", "enables Tor mode on the default local SOCKS5 endpoint") ? 0 : 1; BtfClearSocks5Proxy(); nFail += Check(BtfEnableTorProxy("[::1]:9050", err) && BtfSocks5ProxyEnabled() && BtfTorProxyEnabled() && BtfSocks5ProxyName() == "[::1]:9050", "enables Tor mode on a custom IPv6 endpoint") ? 0 : 1; BtfClearSocks5Proxy(); nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9051", err) && BtfSocks5ProxyEnabled() && !BtfTorProxyEnabled(), "plain SOCKS5 mode is distinct from Tor mode") ? 0 : 1; BtfClearSocks5Proxy(); std::string handshakeErr; bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr); nFail += Check(fHandshakeOk, handshakeErr.empty() ? "completes SOCKS5 domain-name CONNECT handshake" : strprintf("completes SOCKS5 domain-name CONNECT handshake (%s)", handshakeErr.c_str()).c_str()) ? 0 : 1; printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } static int RunManagedTorSelfTest() { printf("managed-tor self-test\n"); int nFail = 0; std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data", "C:/Bitflash Managed Tor/onion-service", 19050, 19051, 8433); nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos, "quotes the Tor data directory") ? 0 : 1; nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos, "binds SOCKS5 to localhost only") ? 0 : 1; nFail += Check(torrc.find("ControlPort 127.0.0.1:19051") != std::string::npos && torrc.find("CookieAuthentication 1") != std::string::npos, "binds an authenticated Tor control port to localhost only") ? 0 : 1; nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos && torrc.find("IsolateClientAddr") != std::string::npos && torrc.find("IsolateDestAddr") != std::string::npos && torrc.find("IsolateDestPort") != std::string::npos, "enables strict Tor stream isolation") ? 0 : 1; nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos, "writes a hidden service directory") ? 0 : 1; nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos, "requests a v3 onion service") ? 0 : 1; nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos, "maps the onion service to the Bitflash P2P listener") ? 0 : 1; nFail += Check(BtfManagedTorStatus() == "disabled", "managed Tor starts disabled") ? 0 : 1; printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", nFail, nFail == 1 ? "" : "s"); fflush(stdout); return nFail == 0 ? 0 : 1; } int RunSelfTest(const std::string& name) { AttachTerminal(); if (name == "wallet-keypool") return RunWalletKeyPoolSelfTest(); if (name == "wallet-hd") return RunWalletHDSelfTest(); if (name == "wallet-format") return RunWalletFormatSelfTest(); if (name == "wallet-storage-sanity") return RunWalletStorageSanitySelfTest(); if (name == "db-env-reopen") return RunDbEnvReopenSelfTest(); if (name == "wallet-sqlite") return RunWalletSQLiteSelfTest(); if (name == "wallet-sqlite-migration") return RunWalletSQLiteMigrationSelfTest(); if (name == "wallet-crypto") return RunWalletCryptoSelfTest(); if (name == "wallet-encrypt") return RunWalletEncryptSelfTest(); if (name == "wallet-sqlite-encrypt") return RunWalletSQLiteEncryptSelfTest(); if (name == "wallet-backend-default") return RunWalletBackendDefaultSelfTest(); if (name == "wallet-convert") return RunWalletConvertSelfTest(); if (name == "wallet-portability") return RunWalletPortabilitySelfTest(); if (name == "net-message") return RunNetMessageSelfTest(); if (name == "network-params") return RunNetworkParamsSelfTest(); if (name == "consensus-limits") return RunConsensusLimitsSelfTest(); if (name == "pool-stratum") return RunPoolStratumSelfTest(); if (name == "payout-ledger") return RunPayoutLedgerSelfTest(); if (name == "parse-money") return RunParseMoneySelfTest(); if (name == "socks5-proxy") return RunSocks5ProxySelfTest(); if (name == "managed-tor") return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); 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, network-params, consensus-limits, pool-stratum, payout-ledger, parse-money, socks5-proxy, managed-tor\n"); return 1; }