// 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; } // A -mwindows binary starts with no console attached, so stdout goes nowhere // even when it was launched from a terminal. Borrow the caller's console for // the duration of the self-test. Nothing to do on Linux, where stdout is // already connected. static void AttachTerminal() { #ifdef _WIN32 // Only when there is nowhere for stdout to go. If the caller redirected it // to a file or a pipe -- which is what `make tests > log` and any CI does -- // that handle is inherited and already works, and reopening it on CONOUT$ // would take the output away from the file and put it on the screen. That // was the first version of this fix, and it broke the one case that // mattered. HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hOut != NULL && hOut != INVALID_HANDLE_VALUE) return; if (AttachConsole(ATTACH_PARENT_PROCESS)) { // Return values ignored on purpose: if the reopen fails there is // nowhere left to report it, and the exit status still carries the // result. freopen("CONOUT$", "w", stdout); freopen("CONOUT$", "w", stderr); } #endif } int RunSelfTest(const std::string& name) { AttachTerminal(); if (name == "wallet-keypool") return RunWalletKeyPoolSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); printf("Known self-tests: wallet-keypool\n"); return 1; }