diff options
| author | mtg0001 <106211915+mtg0001@users.noreply.github.com> | 2026-08-01 15:15:15 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 15:15:15 -0300 |
| commit | a2726cad9e9e11c73ae341ec627a696cdeb63edc (patch) | |
| tree | e8bdce8982ecf38b5a1aeda4aee4d5c000626309 | |
| parent | b65c19e559e2fec511f40a4d5481bcf6a35c311c (diff) | |
Add wallet keypool self-test (#91)
Co-authored-by: Gilcimar Duarte <ti@gpm.infra>
| -rw-r--r-- | src/Makefile | 9 | ||||
| -rw-r--r-- | src/main.cpp | 5 | ||||
| -rw-r--r-- | src/main_gui.cpp | 6 | ||||
| -rw-r--r-- | src/makefile.mingw | 9 | ||||
| -rw-r--r-- | src/selftest.cpp | 275 | ||||
| -rw-r--r-- | src/selftest.h | 11 |
6 files changed, 309 insertions, 6 deletions
diff --git a/src/Makefile b/src/Makefile index 4e104c0..248e62c 100644 --- a/src/Makefile +++ b/src/Makefile | |||
| @@ -41,14 +41,16 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ | |||
| 41 | 41 | ||
| 42 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 42 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 43 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ | 43 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ |
| 44 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h compat.h | 44 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ |
| 45 | selftest.h compat.h | ||
| 45 | 46 | ||
| 46 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | 47 | IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ |
| 47 | obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o | 48 | obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o |
| 48 | 49 | ||
| 49 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 50 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 50 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 51 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 51 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o | 52 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 53 | obj/selftest.o | ||
| 52 | 54 | ||
| 53 | GUI_OBJS = obj/gui.o obj/main_gui.o | 55 | GUI_OBJS = obj/gui.o obj/main_gui.o |
| 54 | 56 | ||
| @@ -126,5 +128,6 @@ clean: | |||
| 126 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | 128 | test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h |
| 127 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto | 129 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto |
| 128 | 130 | ||
| 129 | tests: test_bip32 | 131 | tests: test_bip32 bitflash-node |
| 130 | ./test_bip32 | 132 | ./test_bip32 |
| 133 | ./bitflash-node /selftest=wallet-keypool | ||
diff --git a/src/main.cpp b/src/main.cpp index 68ae44c..82a98d2 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -217,6 +217,11 @@ vector<unsigned char> GetKeyFromPool() | |||
| 217 | vector<unsigned char> vchPubKey = mi->second; | 217 | vector<unsigned char> vchPubKey = mi->second; |
| 218 | mapKeyPool.erase(mi); | 218 | mapKeyPool.erase(mi); |
| 219 | CWalletDB().ErasePool(nIndex); | 219 | CWalletDB().ErasePool(nIndex); |
| 220 | // Leave the wallet with a full backup window after each draw too, | ||
| 221 | // not only immediately before a draw. If topping up fails, the key | ||
| 222 | // already chosen is still safe to use -- its private half was | ||
| 223 | // written before it entered the pool. | ||
| 224 | TopUpKeyPool(); | ||
| 220 | return vchPubKey; | 225 | return vchPubKey; |
| 221 | } | 226 | } |
| 222 | } | 227 | } |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index c9e27a9..2bcdb83 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | // Bitflash entry point -- starts node threads then runs GUI (or headless). | 1 | // Bitflash entry point -- starts node threads then runs GUI (or headless). |
| 2 | 2 | ||
| 3 | #include "headers_core.h" | 3 | #include "headers_core.h" |
| 4 | #include "selftest.h" | ||
| 4 | #include <thread> // hardware_concurrency, to sanity-check /genproclimit | 5 | #include <thread> // hardware_concurrency, to sanity-check /genproclimit |
| 5 | #ifndef _WIN32 | 6 | #ifndef _WIN32 |
| 6 | #include <csignal> | 7 | #include <csignal> |
| @@ -48,6 +49,7 @@ static void PrintUsage() | |||
| 48 | printf(" /debug\n"); | 49 | printf(" /debug\n"); |
| 49 | printf(" /gen\n"); | 50 | printf(" /gen\n"); |
| 50 | printf(" /nogui or /daemon\n"); | 51 | printf(" /nogui or /daemon\n"); |
| 52 | printf(" /selftest=wallet-keypool\n"); | ||
| 51 | printf("\n"); | 53 | printf("\n"); |
| 52 | printf("Mining mode:\n"); | 54 | printf("Mining mode:\n"); |
| 53 | printf(" /operator\n"); | 55 | printf(" /operator\n"); |
| @@ -219,6 +221,10 @@ int main(int argc, char* argv[]) | |||
| 219 | 221 | ||
| 220 | ParseStartupArguments(argc, argv); | 222 | ParseStartupArguments(argc, argv); |
| 221 | 223 | ||
| 224 | string strSelfTest = argval2(argc, argv, "/selftest", "-selftest"); | ||
| 225 | if (!strSelfTest.empty()) | ||
| 226 | return RunSelfTest(strSelfTest); | ||
| 227 | |||
| 222 | // Berkeley DB reports failure by throwing, and CDB's constructor lets it | 228 | // Berkeley DB reports failure by throwing, and CDB's constructor lets it |
| 223 | // through. Nothing on this path caught anything, so an unreadable | 229 | // through. Nothing on this path caught anything, so an unreadable |
| 224 | // blkindex.dat or wallet.dat -- one written by another platform's Berkeley | 230 | // blkindex.dat or wallet.dat -- one written by another platform's Berkeley |
diff --git a/src/makefile.mingw b/src/makefile.mingw index e4dee59..c263f4e 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw | |||
| @@ -41,13 +41,15 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ | |||
| 41 | 41 | ||
| 42 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ | 42 | NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ |
| 43 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ | 43 | obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ |
| 44 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o | 44 | obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ |
| 45 | obj/selftest.o | ||
| 45 | 46 | ||
| 46 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o | 47 | GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o |
| 47 | 48 | ||
| 48 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ | 49 | HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ |
| 49 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ | 50 | key.h bignum.h script.h db.h base58.h net.h nostr.h \ |
| 50 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h | 51 | randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ |
| 52 | selftest.h | ||
| 51 | 53 | ||
| 52 | all: bitflash.exe | 54 | all: bitflash.exe |
| 53 | 55 | ||
| @@ -98,5 +100,6 @@ test_bip32.exe: test_bip32.cpp bip32.cpp bip32.h bip39_english.h | |||
| 98 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto -lcrypt32 | 100 | $(CXX) $(CXXFLAGS) -o $@ test_bip32.cpp bip32.cpp -lssl -lcrypto -lcrypt32 |
| 99 | @echo "Built: test_bip32.exe" | 101 | @echo "Built: test_bip32.exe" |
| 100 | 102 | ||
| 101 | tests: test_bip32.exe | 103 | tests: test_bip32.exe bitflash.exe |
| 102 | ./test_bip32.exe | 104 | ./test_bip32.exe |
| 105 | ./bitflash.exe /selftest=wallet-keypool | ||
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 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // Integration self-tests that need the real wallet/database code linked into | ||
| 5 | // the node. These are intentionally run behind an explicit command-line flag | ||
| 6 | // and against a temporary data directory. | ||
| 7 | |||
| 8 | #include "headers_core.h" | ||
| 9 | #include "selftest.h" | ||
| 10 | |||
| 11 | #include <mutex> | ||
| 12 | #include <stdexcept> | ||
| 13 | #include <thread> | ||
| 14 | |||
| 15 | #ifdef _WIN32 | ||
| 16 | #include <direct.h> | ||
| 17 | #else | ||
| 18 | #include <dirent.h> | ||
| 19 | #include <sys/stat.h> | ||
| 20 | #include <sys/types.h> | ||
| 21 | #include <unistd.h> | ||
| 22 | #endif | ||
| 23 | |||
| 24 | static std::string HexStrLocal(const std::vector<unsigned char>& v) | ||
| 25 | { | ||
| 26 | static const char* hex = "0123456789abcdef"; | ||
| 27 | std::string out; | ||
| 28 | out.reserve(v.size() * 2); | ||
| 29 | for (unsigned char c : v) | ||
| 30 | { | ||
| 31 | out.push_back(hex[c >> 4]); | ||
| 32 | out.push_back(hex[c & 0x0f]); | ||
| 33 | } | ||
| 34 | return out; | ||
| 35 | } | ||
| 36 | |||
| 37 | static bool MakeTempDir(std::string& out) | ||
| 38 | { | ||
| 39 | #ifdef _WIN32 | ||
| 40 | char base[MAX_PATH + 1] = {0}; | ||
| 41 | if (!GetTempPathA(MAX_PATH, base)) | ||
| 42 | return false; | ||
| 43 | for (int i = 0; i < 100; i++) | ||
| 44 | { | ||
| 45 | std::string path = strprintf("%sbitflash-selftest-%lu-%lld-%d", | ||
| 46 | base, (unsigned long)GetCurrentProcessId(), (long long)GetTime(), i); | ||
| 47 | if (CreateDirectoryA(path.c_str(), NULL)) | ||
| 48 | { | ||
| 49 | out = path; | ||
| 50 | return true; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | return false; | ||
| 54 | #else | ||
| 55 | char tmpl[256]; | ||
| 56 | snprintf(tmpl, sizeof(tmpl), "/tmp/bitflash-selftest-%ld-XXXXXX", (long)getpid()); | ||
| 57 | char* p = mkdtemp(tmpl); | ||
| 58 | if (!p) | ||
| 59 | return false; | ||
| 60 | out = p; | ||
| 61 | return true; | ||
| 62 | #endif | ||
| 63 | } | ||
| 64 | |||
| 65 | static bool GetCurrentDir(std::string& out) | ||
| 66 | { | ||
| 67 | #ifdef _WIN32 | ||
| 68 | char buf[MAX_PATH + 1] = {0}; | ||
| 69 | DWORD n = GetCurrentDirectoryA(MAX_PATH, buf); | ||
| 70 | if (n == 0 || n > MAX_PATH) | ||
| 71 | return false; | ||
| 72 | out = buf; | ||
| 73 | return true; | ||
| 74 | #else | ||
| 75 | char buf[4096]; | ||
| 76 | if (!getcwd(buf, sizeof(buf))) | ||
| 77 | return false; | ||
| 78 | out = buf; | ||
| 79 | return true; | ||
| 80 | #endif | ||
| 81 | } | ||
| 82 | |||
| 83 | static bool SetCurrentDir(const std::string& path) | ||
| 84 | { | ||
| 85 | #ifdef _WIN32 | ||
| 86 | return SetCurrentDirectoryA(path.c_str()) != 0; | ||
| 87 | #else | ||
| 88 | return chdir(path.c_str()) == 0; | ||
| 89 | #endif | ||
| 90 | } | ||
| 91 | |||
| 92 | static void RemoveTree(const std::string& path) | ||
| 93 | { | ||
| 94 | #ifdef _WIN32 | ||
| 95 | DWORD attr = GetFileAttributesA(path.c_str()); | ||
| 96 | if (attr == INVALID_FILE_ATTRIBUTES) | ||
| 97 | return; | ||
| 98 | if (!(attr & FILE_ATTRIBUTE_DIRECTORY)) | ||
| 99 | { | ||
| 100 | DeleteFileA(path.c_str()); | ||
| 101 | return; | ||
| 102 | } | ||
| 103 | |||
| 104 | WIN32_FIND_DATAA findData; | ||
| 105 | std::string pattern = path + "\\*"; | ||
| 106 | HANDLE hFind = FindFirstFileA(pattern.c_str(), &findData); | ||
| 107 | if (hFind != INVALID_HANDLE_VALUE) | ||
| 108 | { | ||
| 109 | do | ||
| 110 | { | ||
| 111 | std::string name = findData.cFileName; | ||
| 112 | if (name == "." || name == "..") | ||
| 113 | continue; | ||
| 114 | RemoveTree(path + "\\" + name); | ||
| 115 | } | ||
| 116 | while (FindNextFileA(hFind, &findData)); | ||
| 117 | FindClose(hFind); | ||
| 118 | } | ||
| 119 | RemoveDirectoryA(path.c_str()); | ||
| 120 | #else | ||
| 121 | struct stat st; | ||
| 122 | if (lstat(path.c_str(), &st) != 0) | ||
| 123 | return; | ||
| 124 | if (!S_ISDIR(st.st_mode)) | ||
| 125 | { | ||
| 126 | unlink(path.c_str()); | ||
| 127 | return; | ||
| 128 | } | ||
| 129 | |||
| 130 | DIR* dir = opendir(path.c_str()); | ||
| 131 | if (dir) | ||
| 132 | { | ||
| 133 | struct dirent* ent; | ||
| 134 | while ((ent = readdir(dir)) != NULL) | ||
| 135 | { | ||
| 136 | std::string name = ent->d_name; | ||
| 137 | if (name == "." || name == "..") | ||
| 138 | continue; | ||
| 139 | RemoveTree(path + "/" + name); | ||
| 140 | } | ||
| 141 | closedir(dir); | ||
| 142 | } | ||
| 143 | rmdir(path.c_str()); | ||
| 144 | #endif | ||
| 145 | } | ||
| 146 | |||
| 147 | static bool WalletHasPrivateKey(const std::vector<unsigned char>& vchPubKey) | ||
| 148 | { | ||
| 149 | CPrivKey priv; | ||
| 150 | return CWalletDB("r").ReadKey(vchPubKey, priv) && !priv.empty(); | ||
| 151 | } | ||
| 152 | |||
| 153 | static bool Check(bool condition, const char* message) | ||
| 154 | { | ||
| 155 | if (condition) | ||
| 156 | { | ||
| 157 | printf(" ok %s\n", message); | ||
| 158 | return true; | ||
| 159 | } | ||
| 160 | printf(" FAIL %s\n", message); | ||
| 161 | return false; | ||
| 162 | } | ||
| 163 | |||
| 164 | static int RunWalletKeyPoolSelfTest() | ||
| 165 | { | ||
| 166 | printf("wallet-keypool self-test\n"); | ||
| 167 | |||
| 168 | std::string tmp; | ||
| 169 | std::string cwd; | ||
| 170 | if (!MakeTempDir(tmp)) | ||
| 171 | { | ||
| 172 | printf(" FAIL could not create a temporary data directory\n"); | ||
| 173 | return 1; | ||
| 174 | } | ||
| 175 | if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) | ||
| 176 | { | ||
| 177 | printf(" FAIL could not move into the temporary data directory\n"); | ||
| 178 | RemoveTree(tmp); | ||
| 179 | return 1; | ||
| 180 | } | ||
| 181 | |||
| 182 | int nFail = 0; | ||
| 183 | strSetDataDir = tmp; | ||
| 184 | printf(" temp datadir: %s\n", tmp.c_str()); | ||
| 185 | |||
| 186 | try | ||
| 187 | { | ||
| 188 | if (!LoadWallet()) | ||
| 189 | throw std::runtime_error("LoadWallet failed"); | ||
| 190 | |||
| 191 | TopUpKeyPool(); | ||
| 192 | int nInitialPoolSize = 0; | ||
| 193 | CRITICAL_BLOCK(cs_keyPool) | ||
| 194 | nInitialPoolSize = (int)mapKeyPool.size(); | ||
| 195 | nFail += Check(nInitialPoolSize == KEYPOOL_SIZE, "TopUpKeyPool fills the pool") ? 0 : 1; | ||
| 196 | |||
| 197 | bool fPooledKeysStored = true; | ||
| 198 | CRITICAL_BLOCK(cs_keyPool) | ||
| 199 | { | ||
| 200 | for (map<int64, vector<unsigned char> >::const_iterator mi = mapKeyPool.begin(); | ||
| 201 | mi != mapKeyPool.end(); ++mi) | ||
| 202 | if (!WalletHasPrivateKey(mi->second)) | ||
| 203 | fPooledKeysStored = false; | ||
| 204 | } | ||
| 205 | nFail += Check(fPooledKeysStored, "every pooled public key has a stored private key") ? 0 : 1; | ||
| 206 | |||
| 207 | std::vector<std::vector<unsigned char> > drawn; | ||
| 208 | std::mutex drawnMutex; | ||
| 209 | const int nThreads = 8; | ||
| 210 | const int nDrawsPerThread = 32; | ||
| 211 | std::vector<std::thread> threads; | ||
| 212 | |||
| 213 | for (int t = 0; t < nThreads; t++) | ||
| 214 | { | ||
| 215 | threads.emplace_back([&drawn, &drawnMutex, nDrawsPerThread]() { | ||
| 216 | for (int i = 0; i < nDrawsPerThread; i++) | ||
| 217 | { | ||
| 218 | std::vector<unsigned char> key = GetKeyFromPool(); | ||
| 219 | std::lock_guard<std::mutex> lock(drawnMutex); | ||
| 220 | drawn.push_back(key); | ||
| 221 | } | ||
| 222 | }); | ||
| 223 | } | ||
| 224 | for (std::thread& t : threads) | ||
| 225 | t.join(); | ||
| 226 | |||
| 227 | nFail += Check((int)drawn.size() == nThreads * nDrawsPerThread, | ||
| 228 | "all concurrent draws returned a key") ? 0 : 1; | ||
| 229 | |||
| 230 | std::set<std::string> seen; | ||
| 231 | bool fUnique = true; | ||
| 232 | bool fAllStored = true; | ||
| 233 | for (const std::vector<unsigned char>& key : drawn) | ||
| 234 | { | ||
| 235 | if (!seen.insert(HexStrLocal(key)).second) | ||
| 236 | fUnique = false; | ||
| 237 | if (!WalletHasPrivateKey(key)) | ||
| 238 | fAllStored = false; | ||
| 239 | } | ||
| 240 | |||
| 241 | nFail += Check(fUnique, "concurrent draws never return the same key") ? 0 : 1; | ||
| 242 | nFail += Check(fAllStored, "each drawn key was already stored in wallet.dat") ? 0 : 1; | ||
| 243 | int nPoolSize = 0; | ||
| 244 | CRITICAL_BLOCK(cs_keyPool) | ||
| 245 | nPoolSize = (int)mapKeyPool.size(); | ||
| 246 | nFail += Check(nPoolSize == KEYPOOL_SIZE, "drawing leaves the key pool topped up") ? 0 : 1; | ||
| 247 | } | ||
| 248 | catch (const std::exception& e) | ||
| 249 | { | ||
| 250 | printf(" FAIL exception: %s\n", e.what()); | ||
| 251 | nFail++; | ||
| 252 | } | ||
| 253 | catch (...) | ||
| 254 | { | ||
| 255 | printf(" FAIL unknown exception\n"); | ||
| 256 | nFail++; | ||
| 257 | } | ||
| 258 | |||
| 259 | DBFlush(true); | ||
| 260 | SetCurrentDir(cwd); | ||
| 261 | RemoveTree(tmp); | ||
| 262 | printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", | ||
| 263 | nFail, nFail == 1 ? "" : "s"); | ||
| 264 | return nFail == 0 ? 0 : 1; | ||
| 265 | } | ||
| 266 | |||
| 267 | int RunSelfTest(const std::string& name) | ||
| 268 | { | ||
| 269 | if (name == "wallet-keypool") | ||
| 270 | return RunWalletKeyPoolSelfTest(); | ||
| 271 | |||
| 272 | printf("Unknown self-test '%s'\n", name.c_str()); | ||
| 273 | printf("Known self-tests: wallet-keypool\n"); | ||
| 274 | return 1; | ||
| 275 | } | ||
diff --git a/src/selftest.h b/src/selftest.h new file mode 100644 index 0000000..d3699c6 --- /dev/null +++ b/src/selftest.h | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | |||
| 4 | #ifndef BITFLASH_SELFTEST_H | ||
| 5 | #define BITFLASH_SELFTEST_H | ||
| 6 | |||
| 7 | #include <string> | ||
| 8 | |||
| 9 | int RunSelfTest(const std::string& name); | ||
| 10 | |||
| 11 | #endif | ||