From a2726cad9e9e11c73ae341ec627a696cdeb63edc Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:15:15 -0300 Subject: Add wallet keypool self-test (#91) Co-authored-by: Gilcimar Duarte --- src/selftest.cpp | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 src/selftest.cpp (limited to 'src/selftest.cpp') diff --git a/src/selftest.cpp b/src/selftest.cpp new file mode 100644 index 0000000..d649b23 --- /dev/null +++ b/src/selftest.cpp @@ -0,0 +1,275 @@ +// 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" + +#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() +{ + 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; + 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"); + return nFail == 0 ? 0 : 1; +} + +int RunSelfTest(const std::string& name) +{ + if (name == "wallet-keypool") + return RunWalletKeyPoolSelfTest(); + + printf("Unknown self-test '%s'\n", name.c_str()); + printf("Known self-tests: wallet-keypool\n"); + return 1; +} -- cgit v1.2.3