upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile9
-rw-r--r--src/main.cpp5
-rw-r--r--src/main_gui.cpp6
-rw-r--r--src/makefile.mingw9
-rw-r--r--src/selftest.cpp275
-rw-r--r--src/selftest.h11
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
42HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 42HEADERS = 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
46IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 47IMGUI_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
49NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 50NODE_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
53GUI_OBJS = obj/gui.o obj/main_gui.o 55GUI_OBJS = obj/gui.o obj/main_gui.o
54 56
@@ -126,5 +128,6 @@ clean:
126test_bip32: test_bip32.cpp bip32.cpp bip32.h bip39_english.h 128test_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
129tests: test_bip32 131tests: 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
42NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 42NODE_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
46GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 47GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
47 48
48HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 49HEADERS = 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
52all: bitflash.exe 54all: 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
101tests: test_bip32.exe 103tests: 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
24static 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
37static 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
65static 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
83static 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
92static 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
147static bool WalletHasPrivateKey(const std::vector<unsigned char>& vchPubKey)
148{
149 CPrivKey priv;
150 return CWalletDB("r").ReadKey(vchPubKey, priv) && !priv.empty();
151}
152
153static 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
164static 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
267int 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
9int RunSelfTest(const std::string& name);
10
11#endif