// 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 "selftest.h" // 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 #ifdef _WIN32 #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 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::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; 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 == 1, "installing a phrase reserves one derived default key") ? 0 : 1; CKey keyFirstDerived; if (!DeriveHDKey(0, keyFirstDerived, strError)) throw std::runtime_error("default derivation failed: " + strError); 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; 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; // 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 nNextBefore = nHDNext; 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(nHDNext == nNextBefore + (unsigned int)KEYPOOL_SIZE, "the derivation counter advances once per pooled key") ? 0 : 1; std::set derived; for (unsigned int i = nNextBefore; i < nHDNext; 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; } 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 RunNetMessageSelfTest() { fflush(stdout); printf("net-message self-test\n"); int nFail = 0; try { 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.nIncompleteMessageStart != 0, "an incomplete message starts a timeout clock") ? 0 : 1; nFail += Check(!partial.fDisconnect, "a fresh incomplete message does not disconnect immediately") ? 0 : 1; CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); stale.nVersion = VERSION; stale.vRecv << CMessageHeader("block", 100); stale.nIncompleteMessageStart = GetTime() - BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS - 1; stale.nIncompleteMessageSize = 100; stale.strIncompleteMessageCommand = "block"; ProcessMessages(&stale); nFail += Check(stale.fDisconnect, "a stale 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; } int RunSelfTest(const std::string& name) { AttachTerminal(); if (name == "wallet-keypool") return RunWalletKeyPoolSelfTest(); if (name == "wallet-hd") return RunWalletHDSelfTest(); if (name == "net-message") return RunNetMessageSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); printf("Known self-tests: wallet-keypool, wallet-hd, net-message\n"); return 1; }