From 51d617e70f90a972185917ea0096c1ecef2bfa8b Mon Sep 17 00:00:00 2001 From: Bitflash-sh Date: Fri, 24 Jul 2026 11:18:11 -0300 Subject: Bitflash: fair-launch release Satoshi's bitcoin-0.1.0 reborn with RandomX CPU proof of work, anonymous .btf addressing over Nostr with CGNAT rendezvous, automatic peer discovery, a headless Linux node, and Windows GUI wallet. Fair launch, no premine, 21M cap. --- src/randomx_pow.cpp | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/randomx_pow.cpp (limited to 'src/randomx_pow.cpp') diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp new file mode 100644 index 0000000..17bf455 --- /dev/null +++ b/src/randomx_pow.cpp @@ -0,0 +1,143 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// RandomX PoW implementation. See randomx_pow.h. + +#include "headers.h" +#include +#include +#include + +// Fixed key (seed) that determines the RandomX cache/dataset. Keeping it +// constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal +// this is sufficient (RandomX already neutralizes GPU/ASIC by design). +static const char* RANDOMX_KEY = "Bitflash/RandomX/v1/one-cpu-one-vote"; + +static randomx_flags g_flags = RANDOMX_FLAG_DEFAULT; +static randomx_cache* g_cache = NULL; +static randomx_dataset* g_dataset = NULL; +static randomx_vm* g_vmVerify = NULL; +static CCriticalSection g_csVerify; +static bool g_fInit = false; +static bool g_fFast = false; + + +bool RandomXInit() +{ + if (g_fInit) + return true; + + // Detect the best flags for this CPU (JIT, hardware AES, Argon2). + g_flags = randomx_get_flags(); + + g_cache = randomx_alloc_cache(g_flags); + if (!g_cache) + { + // Try without JIT as a fallback + g_flags = RANDOMX_FLAG_DEFAULT; + g_cache = randomx_alloc_cache(g_flags); + if (!g_cache) + return error("RandomX: failed to allocate cache (256 MB)"); + } + randomx_init_cache(g_cache, RANDOMX_KEY, strlen(RANDOMX_KEY)); + + // Verification VM in light mode (cache only) + g_vmVerify = randomx_create_vm(g_flags, g_cache, NULL); + if (!g_vmVerify) + return error("RandomX: failed to create verification VM"); + + g_fInit = true; + printf("RandomX: initialized (cache 256 MB, flags=%d)\n", (int)g_flags); + return true; +} + + +bool RandomXInitDataset(int nThreads) +{ + if (!g_fInit && !RandomXInit()) + return false; + if (g_fFast) + return true; + + randomx_flags fastFlags = (randomx_flags)(g_flags | RANDOMX_FLAG_FULL_MEM); + g_dataset = randomx_alloc_dataset(fastFlags); + if (!g_dataset) + { + printf("RandomX: not enough memory for the 2 GB dataset, staying in light mode\n"); + return false; + } + + unsigned long total = randomx_dataset_item_count(); + if (nThreads < 1) nThreads = 1; + printf("RandomX: initializing ~2 GB dataset with %d threads...\n", nThreads); + + std::vector workers; + unsigned long per = total / nThreads; + for (int i = 0; i < nThreads; i++) + { + unsigned long start = i * per; + unsigned long count = (i == nThreads - 1) ? (total - start) : per; + workers.emplace_back([start, count]() { + randomx_init_dataset(g_dataset, g_cache, start, count); + }); + } + for (auto& w : workers) w.join(); + + g_fFast = true; + printf("RandomX: dataset ready (fast mode active)\n"); + return true; +} + + +bool RandomXFastReady() +{ + return g_fFast; +} + + +uint256 RandomXPoWHash(const void* pHeader, size_t nSize) +{ + uint256 result = 0; + if (!g_fInit && !RandomXInit()) + return result; + CRITICAL_BLOCK(g_csVerify) + { + unsigned char hash[RANDOMX_HASH_SIZE]; + randomx_calculate_hash(g_vmVerify, pHeader, nSize, hash); + memcpy(&result, hash, RANDOMX_HASH_SIZE); + } + return result; +} + + +void* RandomXCreateMinerVM() +{ + if (!g_fInit && !RandomXInit()) + return NULL; + if (g_fFast && g_dataset) + { + randomx_flags fastFlags = (randomx_flags)(g_flags | RANDOMX_FLAG_FULL_MEM); + return randomx_create_vm(fastFlags, NULL, g_dataset); + } + // Light mode: own VM sharing the cache + return randomx_create_vm(g_flags, g_cache, NULL); +} + + +void RandomXDestroyMinerVM(void* vm) +{ + if (vm) + randomx_destroy_vm((randomx_vm*)vm); +} + + +uint256 RandomXHashWithVM(void* vm, const void* pHeader, size_t nSize) +{ + uint256 result = 0; + if (!vm) + return result; + unsigned char hash[RANDOMX_HASH_SIZE]; + randomx_calculate_hash((randomx_vm*)vm, pHeader, nSize, hash); + memcpy(&result, hash, RANDOMX_HASH_SIZE); + return result; +} -- cgit v1.2.3 From f05274afd0becc7311fa69aa60b0215f300e5f28 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Fri, 31 Jul 2026 11:27:50 -0300 Subject: randomx: allocate the dataset once, not once per miner thread (#68) Every miner thread calls RandomXInitDataset at startup. Its only guard is g_fFast, which is set at the very end -- after the ~2 GB dataset has been filled, which takes tens of seconds. So all of them sailed past the guard together and each allocated its own dataset. Measured in production on a 32-core machine running 31 miner threads: the log says "initializing ~2 GB dataset" 34 times, and the process holds 65,127 MB of private pages while its own startup line promises 2142 MB "(~2080 MB shared RandomX dataset + ~2 MB per thread)". The word shared was not true. A 4-core machine showed the same thing at its own scale: 4 allocations for 3 threads. The waste was the mild half. g_dataset is assigned the moment memory is allocated, before it holds anything, so the last thread to allocate swung the pointer out from under threads still filling their own. A miner could then hash against a dataset another thread had not finished writing, and proof of work computed that way is not reproducible by any other node -- the block would be found invalid by everyone, including this node's own verifier, which uses the cache rather than the dataset. This only became reachable with multi-threaded mining (#52); with one thread there was nobody to race. Holding the lock across the whole initialisation is deliberate: a thread arriving mid-init has nothing useful to do until the dataset exists, and blocking is how it waits for exactly that. Verified with three mining threads: one "initializing ~2 GB dataset", one "dataset ready", and 2362 MB of private memory against 2086 MB advertised -- the difference being the 256 MB cache. Before, three threads meant three datasets. --- src/randomx_pow.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'src/randomx_pow.cpp') diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index 17bf455..d617ba2 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal @@ -52,10 +53,32 @@ bool RandomXInit() } +// One dataset, once, no matter how many miners ask for it. +// +// Every miner thread calls this at startup. The g_fFast guard below is only +// set at the very end, after the ~2 GB dataset has been filled, which takes +// tens of seconds -- so without this lock all of them sail past the guard +// together and each allocates its own dataset. Measured in production: a +// 32-core machine logged "initializing ~2 GB dataset" 34 times and committed +// 65 GB of private pages against a startup message promising 2142 MB. +// +// The waste was the mild half. g_dataset is assigned the moment the memory is +// allocated, before it holds anything, so the last thread to allocate would +// swing the pointer out from under the threads still filling their own. A +// miner could then hash against a dataset another thread had not finished +// writing, producing proof of work that no other node can reproduce. +// +// Holding the lock across the whole initialisation is deliberate: a thread +// that arrives mid-init has nothing useful to do until the dataset exists, +// and blocking is how it waits for exactly that. +static std::mutex g_csDataset; + bool RandomXInitDataset(int nThreads) { if (!g_fInit && !RandomXInit()) return false; + + std::lock_guard lock(g_csDataset); if (g_fFast) return true; -- cgit v1.2.3 From b72f403c054838e4b2203f48f757df0dad017628 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 7 Aug 2026 10:56:24 -0300 Subject: randomx: serialize cache initialization (#4) --- src/randomx_pow.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/randomx_pow.cpp') diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index d617ba2..1d058d4 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp @@ -19,6 +19,7 @@ static randomx_cache* g_cache = NULL; static randomx_dataset* g_dataset = NULL; static randomx_vm* g_vmVerify = NULL; static CCriticalSection g_csVerify; +static std::mutex g_csInit; static bool g_fInit = false; static bool g_fFast = false; @@ -28,6 +29,10 @@ bool RandomXInit() if (g_fInit) return true; + std::lock_guard lock(g_csInit); + if (g_fInit) + return true; + // Detect the best flags for this CPU (JIT, hardware AES, Argon2). g_flags = randomx_get_flags(); -- cgit v1.2.3 From 3a128281cd8fec1c698346191c240a771f80d72c Mon Sep 17 00:00:00 2001 From: mischief Date: Thu, 20 Aug 2026 17:57:27 -0300 Subject: randomx: optional 2 MB large pages for the cache, dataset and scratchpads RandomX reads its 256 MB cache, 2 GB dataset and per-VM scratchpads at random, so most accesses miss the TLB on 4 KB pages. Requesting 2 MB pages recovers roughly a tenth of the hash rate. Each allocation tries large pages first and falls back to normal pages when the pool is not reserved, so nothing regresses when hugepages are unavailable. -nolargepages opts out, and the diagnostics report which allocations got large pages. --- src/main_gui.cpp | 7 ++++ src/net.cpp | 1 + src/randomx_pow.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++++++------ src/randomx_pow.h | 7 ++++ 4 files changed, 110 insertions(+), 11 deletions(-) (limited to 'src/randomx_pow.cpp') diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 0a961bf..a01bdee 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -127,6 +127,8 @@ static void PrintUsage() printf(" /stratumbridgeport=N (default 3333; listen on 127.0.0.1)\n"); printf(" /solomine\n"); printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n"); + printf(" /nolargepages (do not ask for 2 MB pages for the RandomX\n"); + printf(" cache, dataset and scratchpads)\n"); printf(" /checkblocks=N (blocks re-verified at startup, default 288, 0 = all)\n"); printf("\n"); printf("Pool operator announcement:\n"); @@ -230,6 +232,11 @@ static void ParseStartupArguments(int argc, char* argv[]) if (!strCheckBlocks.empty()) nCheckBlocksOnLoad = atoi(strCheckBlocks.c_str()); + // Must be read before LoadBlockIndex(), which is what first calls + // RandomXInit() and fixes how the cache is allocated. + if (arg(argc,argv,"/nolargepages") || arg(argc,argv,"-nolargepages")) + fRandomXLargePages = false; + // /genproclimit=N -- threads to hash with. 0 or absent means automatic, // which is every core but one. Named after Bitcoin's own option so it reads // familiarly to anyone who has run one of these before. diff --git a/src/net.cpp b/src/net.cpp index ecfd565..287a4bd 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -366,6 +366,7 @@ string GetDiagnosticsText() nMining, (RandomXFastReady() ? 2080 : 256) + 2 * nMining).c_str() : ", not mining"); + str += strprintf(" large pages %s\n", RandomXLargePagesStatus()); str += SockAccountingText(); diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index 1d058d4..5872090 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp @@ -23,6 +23,39 @@ static std::mutex g_csInit; static bool g_fInit = false; static bool g_fFast = false; +// RandomX reads a 256 MB cache, a 2 GB dataset and a 2 MB scratchpad per VM +// at random, so most reads miss the TLB on 4 KB pages. 2 MB pages recover +// roughly a tenth of the hash rate. +// +// The pool must be reserved first (vm.nr_hugepages), and RandomX returns NULL +// instead of falling back, so each allocation below retries without the flag. +bool fRandomXLargePages = true; // cleared by -nolargepages +static bool g_fLargeCache = false; +static bool g_fLargeDataset = false; +static bool g_fLargeAny = false; + +static randomx_flags WithLargePages(randomx_flags f) +{ + return (randomx_flags)(f | RANDOMX_FLAG_LARGE_PAGES); +} + +// Ask for a cache in large pages, then plain pages. Sets fLargeOut to say +// which one answered. +static randomx_cache* AllocCache(randomx_flags flags, bool& fLargeOut) +{ + fLargeOut = false; + if (fRandomXLargePages) + { + randomx_cache* c = randomx_alloc_cache(WithLargePages(flags)); + if (c) + { + fLargeOut = true; + return c; + } + } + return randomx_alloc_cache(flags); +} + bool RandomXInit() { @@ -36,28 +69,56 @@ bool RandomXInit() // Detect the best flags for this CPU (JIT, hardware AES, Argon2). g_flags = randomx_get_flags(); - g_cache = randomx_alloc_cache(g_flags); + g_cache = AllocCache(g_flags, g_fLargeCache); if (!g_cache) { // Try without JIT as a fallback g_flags = RANDOMX_FLAG_DEFAULT; - g_cache = randomx_alloc_cache(g_flags); + g_cache = AllocCache(g_flags, g_fLargeCache); if (!g_cache) return error("RandomX: failed to allocate cache (256 MB)"); } randomx_init_cache(g_cache, RANDOMX_KEY, strlen(RANDOMX_KEY)); - // Verification VM in light mode (cache only) - g_vmVerify = randomx_create_vm(g_flags, g_cache, NULL); + // Verification VM in light mode (cache only). The scratchpad is only 2 MB + // and this VM is created once, but it hashes every block the node ever + // validates, so it gets the same treatment. + bool fLargeVerify = false; + if (fRandomXLargePages) + { + g_vmVerify = randomx_create_vm(WithLargePages(g_flags), g_cache, NULL); + fLargeVerify = (g_vmVerify != NULL); + } + if (!g_vmVerify) + g_vmVerify = randomx_create_vm(g_flags, g_cache, NULL); if (!g_vmVerify) return error("RandomX: failed to create verification VM"); + g_fLargeAny = g_fLargeCache || fLargeVerify; g_fInit = true; - printf("RandomX: initialized (cache 256 MB, flags=%d)\n", (int)g_flags); + printf("RandomX: initialized (cache 256 MB, flags=%d, large pages: %s)\n", + (int)g_flags, RandomXLargePagesStatus()); + if (fRandomXLargePages && !g_fLargeCache) + printf("RandomX: large pages unavailable for the cache; " + "reserve them with sysctl vm.nr_hugepages=1280 for about 10%% more hash rate\n"); return true; } +const char* RandomXLargePagesStatus() +{ + if (!fRandomXLargePages) + return "off (-nolargepages)"; + if (g_fLargeCache && g_fLargeDataset) + return "cache + dataset"; + if (g_fLargeCache) + return "cache only"; + if (g_fLargeDataset) + return "dataset only"; + return g_fLargeAny ? "scratchpad only" : "unavailable"; +} + + // One dataset, once, no matter how many miners ask for it. // // Every miner thread calls this at startup. The g_fFast guard below is only @@ -87,8 +148,18 @@ bool RandomXInitDataset(int nThreads) if (g_fFast) return true; - randomx_flags fastFlags = (randomx_flags)(g_flags | RANDOMX_FLAG_FULL_MEM); - g_dataset = randomx_alloc_dataset(fastFlags); + // Only RANDOMX_FLAG_LARGE_PAGES means anything to randomx_alloc_dataset; + // the rest of g_flags belongs to the VMs that read it. + if (fRandomXLargePages) + { + g_dataset = randomx_alloc_dataset(WithLargePages(RANDOMX_FLAG_DEFAULT)); + g_fLargeDataset = (g_dataset != NULL); + if (!g_dataset) + printf("RandomX: large pages unavailable for the 2 GB dataset; " + "reserve 1280 with sysctl vm.nr_hugepages for about 10%% more hash rate\n"); + } + if (!g_dataset) + g_dataset = randomx_alloc_dataset(RANDOMX_FLAG_DEFAULT); if (!g_dataset) { printf("RandomX: not enough memory for the 2 GB dataset, staying in light mode\n"); @@ -142,13 +213,26 @@ void* RandomXCreateMinerVM() { if (!g_fInit && !RandomXInit()) return NULL; + + randomx_flags flags = g_flags; + randomx_cache* cache = g_cache; + randomx_dataset* dataset = NULL; if (g_fFast && g_dataset) { - randomx_flags fastFlags = (randomx_flags)(g_flags | RANDOMX_FLAG_FULL_MEM); - return randomx_create_vm(fastFlags, NULL, g_dataset); + flags = (randomx_flags)(g_flags | RANDOMX_FLAG_FULL_MEM); + cache = NULL; + dataset = g_dataset; + } + + // Each thread gets its own 2 MB scratchpad, so the large-page pool can run + // dry partway through a fleet of miners. Threads that miss out still run. + if (fRandomXLargePages) + { + randomx_vm* vm = randomx_create_vm(WithLargePages(flags), cache, dataset); + if (vm) + return vm; } - // Light mode: own VM sharing the cache - return randomx_create_vm(g_flags, g_cache, NULL); + return randomx_create_vm(flags, cache, dataset); } diff --git a/src/randomx_pow.h b/src/randomx_pow.h index 0027571..355b708 100644 --- a/src/randomx_pow.h +++ b/src/randomx_pow.h @@ -41,4 +41,11 @@ uint256 RandomXHashWithVM(void* vm, const void* pHeader, size_t nSize); // True when the 2 GB dataset is ready (fast mode active). bool RandomXFastReady(); +// Ask for large (2 MB) pages for the cache, dataset and scratchpads. Set false +// by -nolargepages. Must be set before the first RandomXInit(). +extern bool fRandomXLargePages; + +// Which allocations actually got large pages, for the status display. +const char* RandomXLargePagesStatus(); + #endif -- cgit v1.2.3 From bfce24bc542927647ae4e5af68b71e3e56e1a291 Mon Sep 17 00:00:00 2001 From: bitflash Date: Thu, 20 Aug 2026 18:03:27 -0300 Subject: randomx: make the large-pages fallback hint OS-specific The reservation instruction printed when a large-page allocation falls back was Linux-only (sysctl vm.nr_hugepages). Route it through a small helper so a Windows build points at the "Lock pages in memory" privilege instead. --- src/randomx_pow.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'src/randomx_pow.cpp') diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index 5872090..4454fd1 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp @@ -34,6 +34,17 @@ static bool g_fLargeCache = false; static bool g_fLargeDataset = false; static bool g_fLargeAny = false; +// How to reserve the large-page pool, for the hint printed when a large-page +// allocation falls back to normal pages. The mechanism is OS-specific. +static const char* LargePagesHint() +{ +#ifdef _WIN32 + return "enable the \"Lock pages in memory\" privilege and run elevated"; +#else + return "reserve them with: sysctl vm.nr_hugepages=1280"; +#endif +} + static randomx_flags WithLargePages(randomx_flags f) { return (randomx_flags)(f | RANDOMX_FLAG_LARGE_PAGES); @@ -99,8 +110,8 @@ bool RandomXInit() printf("RandomX: initialized (cache 256 MB, flags=%d, large pages: %s)\n", (int)g_flags, RandomXLargePagesStatus()); if (fRandomXLargePages && !g_fLargeCache) - printf("RandomX: large pages unavailable for the cache; " - "reserve them with sysctl vm.nr_hugepages=1280 for about 10%% more hash rate\n"); + printf("RandomX: large pages unavailable for the cache; %s " + "for about 10%% more hash rate\n", LargePagesHint()); return true; } @@ -155,8 +166,8 @@ bool RandomXInitDataset(int nThreads) g_dataset = randomx_alloc_dataset(WithLargePages(RANDOMX_FLAG_DEFAULT)); g_fLargeDataset = (g_dataset != NULL); if (!g_dataset) - printf("RandomX: large pages unavailable for the 2 GB dataset; " - "reserve 1280 with sysctl vm.nr_hugepages for about 10%% more hash rate\n"); + printf("RandomX: large pages unavailable for the 2 GB dataset; %s " + "for about 10%% more hash rate\n", LargePagesHint()); } if (!g_dataset) g_dataset = randomx_alloc_dataset(RANDOMX_FLAG_DEFAULT); -- cgit v1.2.3 From 0b566b4869b9e86c90162befd87f9882b6dd143c Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Wed, 2 Sep 2026 07:09:45 +0000 Subject: net: a refused thread spawn drops one peer, not the node std::thread throws std::system_error when pthread_create is refused (RLIMIT_NPROC, a container's pids cgroup, memory pressure), and nothing caught it, so the node died by std::terminate. Threads are spawned per peer connection, so the pressure that triggers this scales with peer count, which is attacker-influenced. Observed killing a healthy participant worker mid-mining. Every spawn site in the node build, and what a failure now does: - btftunnel.cpp: both tunnel pumps are guarded. The inbound pump starts first because it owns cleanup of the pump end and rendezvous socket; if the second spawn is refused, shutdown() wakes the first out of its read and it cleans up itself -- closing those descriptors from the failure path instead would race it onto reused descriptors. Both paths return INVALID_SOCKET, which every caller already treats as "this connection failed". - btfrv.cpp: the relay's accept loop drops the connection it cannot serve and stops accepting for a moment -- longer after consecutive failures, capped at 5s -- instead of dying at an arbitrary one. The per-pair second pump now runs on the handling thread itself, leaving exactly one spawn to guard and nothing running yet to race. - main.cpp: a participant that cannot spawn its hash workers stops the ones that did start, drops the pool connection, and returns to the reconnect loop, the same way an ordinary disconnect does. - randomx_pow.cpp: dataset init joins the workers it got, releases the dataset, and stays in light mode -- the same trade the allocation failure path above it already makes. - rpc.cpp, net.cpp: the remaining unchecked _beginthread calls (which report failure by value, they cannot throw) now say precisely what did not start. The pool's accept and payout threads failing silently left the pool running deaf -- nobody accepts, nobody gets paid, and nothing in the log says why. Left alone deliberately: the per-relay accept spawns (rpc.cpp) already check, log and retry on the next poll; startup-critical spawns in net.cpp keep failing hard; the selftest-only std::thread uses may abort a selftest run, which is acceptable where dying over one peer is not. Verified: full node rebuilds clean and links; pool-stratum selftest passes; a scratch harness under RLIMIT_NPROC=1 confirms _beginthread reports -1 and std::thread throws system_error, the premises of every path above. --- src/btfrv.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- src/btftunnel.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/main.cpp | 30 +++++++++++++++++++++++++++--- src/net.cpp | 5 +++-- src/randomx_pow.cpp | 28 ++++++++++++++++++++++------ src/rpc.cpp | 15 +++++++++++---- 6 files changed, 157 insertions(+), 22 deletions(-) (limited to 'src/randomx_pow.cpp') diff --git a/src/btfrv.cpp b/src/btfrv.cpp index 133eb89..8ab735c 100644 --- a/src/btfrv.cpp +++ b/src/btfrv.cpp @@ -40,6 +40,8 @@ typedef int SOCKET; #include #include #include +#include +#include namespace btf { @@ -209,8 +211,28 @@ static void HandleIncoming(SOCKET s) bool a = WriteN(svc, &ok, 1); bool b = WriteN(s, &ok, 1); if (!a || !b) { CLOSESOCK(svc); CLOSESOCK(s); return; } - std::thread(Forward, s, svc).detach(); - std::thread(Forward, svc, s).detach(); + // One thread pumps s -> svc; this thread pumps svc -> s. Spawning a + // thread per direction left no clean way to fail the second spawn: + // std::thread throws std::system_error when pthread_create is refused + // (NPROC limit, container pids cgroup, memory pressure), the first + // pump was already detached, and closing its sockets from here would + // race it -- so nothing caught the throw and it reached + // std::terminate, killing the relay over one pair it could not serve. + // Running one direction on this thread leaves exactly one spawn to + // guard, and a failure here has nothing running yet to race. + try + { + std::thread(Forward, s, svc).detach(); + } + catch (const std::system_error& e) + { + fprintf(stderr, "rendezvous relay: cannot spawn forward thread (%s), pair dropped\n", + e.what()); + CLOSESOCK(svc); + CLOSESOCK(s); + return; + } + Forward(svc, s); } else { @@ -236,6 +258,7 @@ bool RvRelayRun(unsigned short port) if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; } if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; } + int nSpawnFails = 0; while (!g_stop) { struct sockaddr_in cli; @@ -246,7 +269,27 @@ bool RvRelayRun(unsigned short port) if (g_stop) break; continue; } - std::thread(HandleIncoming, s).detach(); + try + { + std::thread(HandleIncoming, s).detach(); + nSpawnFails = 0; + } + catch (const std::system_error& e) + { + // A refused pthread_create is backpressure, not a reason to die: + // the condition behind it (NPROC, pids cgroup, memory) does not + // clear in milliseconds, and the connection count feeding it is + // attacker-influenced. Drop this one peer, stop accepting for a + // moment -- longer each time, capped -- and keep serving the + // connections already established. + fprintf(stderr, "rendezvous relay: cannot spawn connection thread (%s), connection dropped\n", + e.what()); + CLOSESOCK(s); + nSpawnFails++; + int nWaitMs = nSpawnFails < 5 ? nSpawnFails * 1000 : 5000; + for (int i = 0; i < nWaitMs && !g_stop; i += 100) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } } CLOSESOCK(g_listen); g_listen = INVALID_SOCKET; diff --git a/src/btftunnel.cpp b/src/btftunnel.cpp index 63d178b..2b3f818 100644 --- a/src/btftunnel.cpp +++ b/src/btftunnel.cpp @@ -25,8 +25,10 @@ typedef int SOCKET; #endif #include "sockcount.h" #include +#include #include #include +#include namespace btf { @@ -121,18 +123,60 @@ static void PumpIn(SOCKET pumpEnd, RvSocket rv, std::vector key) // Start the two pumps and hand back the app-facing socket. When both pumps // finish, close the pump end and the rendezvous socket. +// +// Every tunnel is two threads, and a tunnel is one peer connection (a .btf +// dial or accept, a pool miner, a participant's pool session) -- so the +// thread count scales with peer count, which is attacker-influenced. When +// pthread_create is refused std::thread throws std::system_error; uncaught it +// reached std::terminate and took the whole process down over one connection +// it could not serve. Both spawns are guarded, and the failure paths return +// INVALID_SOCKET, which every caller already handles by dropping just that +// connection. static btf_socket_t StartTunnel(RvSocket rv, const unsigned char shared[32]) { btf_socket_t appEnd, pumpEnd; if (!MakeLocalPair(appEnd, pumpEnd)) { RvClose(rv); return INVALID_SOCKET; } std::vector key(shared, shared + 32); - std::thread(PumpOut, pumpEnd, rv, key).detach(); - std::thread([pumpEnd, rv, key]() { - PumpIn(pumpEnd, rv, key); + + // The inbound pump starts first because it owns the cleanup: it closes + // pumpEnd and rv when it exits. That ownership is what makes the second + // spawn's failure path below race-free. + try + { + std::thread([pumpEnd, rv, key]() { + PumpIn(pumpEnd, rv, key); + BtfCloseSocket(pumpEnd); + RvClose(rv); + }).detach(); + } + catch (const std::system_error& e) + { + // Nothing is running yet, so closing everything here is safe. + fprintf(stderr, "btf tunnel: cannot spawn inbound pump (%s), tunnel dropped\n", + e.what()); BtfCloseSocket(pumpEnd); + BtfCloseSocket(appEnd); RvClose(rv); - }).detach(); + return INVALID_SOCKET; + } + + try + { + std::thread(PumpOut, pumpEnd, rv, key).detach(); + } + catch (const std::system_error& e) + { + // The inbound pump is parked in RvReadN(rv). shutdown() wakes it, and + // it closes pumpEnd and rv itself -- closing them here instead would + // race exactly that, and a reused descriptor is how an unrelated + // socket gets closed. + fprintf(stderr, "btf tunnel: cannot spawn outbound pump (%s), tunnel dropped\n", + e.what()); + shutdown((SOCKET)rv, SD_BOTH); + BtfCloseSocket(appEnd); + return INVALID_SOCKET; + } return appEnd; } diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..d154523 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "bip32.h" #include "btfaddr.h" #include "btftunnel.h" @@ -3899,9 +3900,32 @@ connected: std::vector shareHits; std::mutex shareHitsMtx; std::vector hashThreads; - for (int i = 0; i < nWorkers; i++) - hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, - &shareHits, &shareHitsMtx); + try + { + for (int i = 0; i < nWorkers; i++) + hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, + &shareHits, &shareHitsMtx); + } + catch (const std::system_error& e) + { + // A refused pthread_create used to reach std::terminate here and + // kill the worker mid-session -- observed twice on an otherwise + // healthy participant. Stop the workers that did start the same way + // the disconnect path below does, drop this pool connection, and let + // the caller's reconnect loop try again once the pressure clears. + LogPrint("worker", "[worker] cannot spawn hash threads (%s)" + " -- dropping pool connection\n", e.what()); + SetParticipantMiningStatus("hash thread spawn failed"); + { + std::lock_guard lk(jobShared.mtx); + jobShared.stop = true; + jobShared.haveJob = false; + } + for (std::thread& t : hashThreads) + t.join(); + closesocket(s); + return false; + } LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n", nWorkers); const uint64 sessionStartHashes = gParticipantHashes.load(); diff --git a/src/net.cpp b/src/net.cpp index fed6123..16038c8 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2090,7 +2090,7 @@ bool StartNode(string& strError) if (addrIncoming.ip) addrLocalHost.ip = addrIncoming.ip; - _beginthread([](void*) { + if (_beginthread([](void*) { unsigned int ip = addrLocalHost.ip; if (GetMyExternalIP(ip)) { addrLocalHost.ip = ip; @@ -2098,7 +2098,8 @@ bool StartNode(string& strError) CWalletDB().WriteSetting("addrIncoming", addrIncoming); printf("External IP updated: %s\n", addrLocalHost.ToStringIP().c_str()); } - }, 0, NULL); + }, 0, NULL) == (uintptr_t)-1) + printf("Error: _beginthread(GetMyExternalIP) failed\n"); // Peers that answered last time, dialled straight away. The relays are the // only way to find anyone otherwise, and reaching them plus walking their diff --git a/src/randomx_pow.cpp b/src/randomx_pow.cpp index 1d058d4..9c59364 100644 --- a/src/randomx_pow.cpp +++ b/src/randomx_pow.cpp @@ -8,6 +8,7 @@ #include #include #include +#include // Fixed key (seed) that determines the RandomX cache/dataset. Keeping it // constant avoids per-epoch "reseed" logic; for the CPU+RAM fairness goal @@ -101,13 +102,28 @@ bool RandomXInitDataset(int nThreads) std::vector workers; unsigned long per = total / nThreads; - for (int i = 0; i < nThreads; i++) + try { - unsigned long start = i * per; - unsigned long count = (i == nThreads - 1) ? (total - start) : per; - workers.emplace_back([start, count]() { - randomx_init_dataset(g_dataset, g_cache, start, count); - }); + for (int i = 0; i < nThreads; i++) + { + unsigned long start = i * per; + unsigned long count = (i == nThreads - 1) ? (total - start) : per; + workers.emplace_back([start, count]() { + randomx_init_dataset(g_dataset, g_cache, start, count); + }); + } + } + catch (const std::system_error& e) + { + // Same refusal as the allocation failure above: light mode is slow + // but alive, std::terminate is neither. Join the workers that did + // start before releasing the dataset out from under them. + for (auto& w : workers) w.join(); + randomx_release_dataset(g_dataset); + g_dataset = NULL; + printf("RandomX: cannot spawn dataset init threads (%s), staying in light mode\n", + e.what()); + return false; } for (auto& w : workers) w.join(); diff --git a/src/rpc.cpp b/src/rpc.cpp index a4b53ca..13cb6c9 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -5077,10 +5077,17 @@ void ThreadRPCServer(void*) // armed for its one wallet probe before it is allowed to send. ReconcilePayouts(); - _beginthread(AcceptLoopFn, 0, NULL); - _beginthread(LocalAcceptFn, 0, NULL); - _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); - _beginthread(PayoutThreadFn, 0, NULL); + // _beginthread cannot throw (it reports -1), but an unchecked failure + // leaves the pool running deaf: nobody accepts, announces, or pays, and + // nothing in the log says why. + if (_beginthread(AcceptLoopFn, 0, NULL) == (uintptr_t)-1) + LogPrint("pool", "[pool] FATAL: accept loop thread failed to spawn -- miners cannot connect\n"); + if (_beginthread(LocalAcceptFn, 0, NULL) == (uintptr_t)-1) + LogPrint("pool", "[pool] local accept thread failed to spawn\n"); + if (_beginthread(ThreadBtfPoolAnnouncer, 0, NULL) == (uintptr_t)-1) + LogPrint("pool", "[pool] announcer thread failed to spawn -- pool will not be advertised\n"); + if (_beginthread(PayoutThreadFn, 0, NULL) == (uintptr_t)-1) + LogPrint("pool", "[pool] FATAL: payout thread failed to spawn -- no one gets paid\n"); PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown -- cgit v1.2.3