From da278163f053c1c4499d1cc4b2a52c369f8b4088 Mon Sep 17 00:00:00 2001 From: dan_s Date: Tue, 17 Feb 2026 16:43:41 -0600 Subject: [PATCH] TestBlockValidity() triggers CheckRandomXSolution() which allocates ~256MB of cache to recompute hash which is redundant since local mining thread would be checking it's own work against itself. Rather than miner allocating its own dataset, a single shared dataset is allocated and shared accross all threads. This is explicitly supported by RandomX. --- src/miner.cpp | 281 ++++++++++++++++++++++++++++++++++++-------------- src/pow.cpp | 36 ++++++- src/pow.h | 3 + 3 files changed, 243 insertions(+), 77 deletions(-) diff --git a/src/miner.cpp b/src/miner.cpp index 398dcc430..acf678dcc 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -51,6 +51,7 @@ #include "transaction_builder.h" #include "sodium.h" #include +#include #include #ifdef ENABLE_MINING #include @@ -1014,6 +1015,126 @@ enum RandomXSolverCancelCheck int GetRandomXInterval(); int GetRandomXBlockLag(); +// Shared RandomX dataset manager — all miner threads share a single ~2GB dataset +// instead of each allocating their own. The dataset is read-only after initialization +// and RandomX explicitly supports multiple VMs sharing one dataset. +struct RandomXDatasetManager { + randomx_flags flags; + randomx_cache *cache; + randomx_dataset *dataset; + unsigned long datasetItemCount; + std::string currentKey; + std::mutex mtx; // protects Init/Shutdown/CreateVM + boost::shared_mutex datasetMtx; // readers-writer lock: shared for hashing, exclusive for rebuild + bool initialized; + + RandomXDatasetManager() : flags(randomx_get_flags()), cache(nullptr), dataset(nullptr), + datasetItemCount(0), initialized(false) {} + + bool Init() { + std::lock_guard lock(mtx); + if (initialized) return true; + + flags |= RANDOMX_FLAG_FULL_MEM; + + cache = randomx_alloc_cache(flags | RANDOMX_FLAG_LARGE_PAGES | RANDOMX_FLAG_SECURE); + if (cache == nullptr) { + LogPrintf("RandomXDatasetManager: cache alloc failed with large pages, trying without...\n"); + cache = randomx_alloc_cache(flags | RANDOMX_FLAG_SECURE); + if (cache == nullptr) { + LogPrintf("RandomXDatasetManager: cache alloc failed with secure, trying basic...\n"); + } + cache = randomx_alloc_cache(flags); + if (cache == nullptr) { + LogPrintf("RandomXDatasetManager: cannot allocate cache!\n"); + return false; + } + } + + dataset = randomx_alloc_dataset(flags); + if (dataset == nullptr) { + LogPrintf("RandomXDatasetManager: cannot allocate dataset!\n"); + randomx_release_cache(cache); + cache = nullptr; + return false; + } + + datasetItemCount = randomx_dataset_item_count(); + initialized = true; + LogPrintf("RandomXDatasetManager: allocated shared cache + dataset (%lu items)\n", datasetItemCount); + return true; + } + + // Initialize cache with a key and rebuild the dataset. + // Thread-safe: acquires exclusive lock so all hashing threads must finish first. + void UpdateKey(const void *key, size_t keySize) { + std::string newKey((const char*)key, keySize); + + // Fast check with shared lock — skip if key hasn't changed + { + boost::shared_lock readLock(datasetMtx); + if (newKey == currentKey) return; // already up to date + } + + // Acquire exclusive lock — blocks until all hashing threads release their shared locks + boost::unique_lock writeLock(datasetMtx); + // Double-check after acquiring exclusive lock (another thread may have rebuilt first) + if (newKey == currentKey) return; + + LogPrintf("RandomXDatasetManager: updating key (size=%lu)\n", keySize); + randomx_init_cache(cache, key, keySize); + currentKey = newKey; + + // Rebuild dataset using all available CPU threads + const int initThreadCount = std::thread::hardware_concurrency(); + if (initThreadCount > 1) { + std::vector threads; + uint32_t startItem = 0; + const auto perThread = datasetItemCount / initThreadCount; + const auto remainder = datasetItemCount % initThreadCount; + for (int i = 0; i < initThreadCount; ++i) { + const auto count = perThread + (i == initThreadCount - 1 ? remainder : 0); + threads.push_back(std::thread(&randomx_init_dataset, dataset, cache, startItem, count)); + startItem += count; + } + for (unsigned i = 0; i < threads.size(); ++i) { + threads[i].join(); + } + } else { + randomx_init_dataset(dataset, cache, 0, datasetItemCount); + } + LogPrintf("RandomXDatasetManager: dataset rebuilt\n"); + } + + // Creates a per-thread VM using the shared dataset. + // Caller must hold a shared lock on datasetMtx. + randomx_vm *CreateVM() { + return randomx_create_vm(flags, nullptr, dataset); + } + + void Shutdown() { + std::lock_guard lock(mtx); + if (dataset != nullptr) { + randomx_release_dataset(dataset); + dataset = nullptr; + } + if (cache != nullptr) { + randomx_release_cache(cache); + cache = nullptr; + } + initialized = false; + currentKey.clear(); + LogPrintf("RandomXDatasetManager: shutdown complete\n"); + } + + ~RandomXDatasetManager() { + Shutdown(); + } +}; + +// Global shared dataset manager, created by GenerateBitcoins before spawning miner threads +static RandomXDatasetManager *g_rxDatasetManager = nullptr; + #ifdef ENABLE_WALLET void static RandomXMiner(CWallet *pwallet) #else @@ -1050,33 +1171,12 @@ void static RandomXMiner() ); miningTimer.start(); - randomx_flags flags = randomx_get_flags(); - flags |= RANDOMX_FLAG_FULL_MEM; - randomx_cache *randomxCache = randomx_alloc_cache(flags | RANDOMX_FLAG_LARGE_PAGES | RANDOMX_FLAG_SECURE ); - if (randomxCache == NULL) { - LogPrintf("RandomX cache is null, trying without large pages...\n"); - randomxCache = randomx_alloc_cache(flags | RANDOMX_FLAG_SECURE); - if (randomxCache == NULL) { - LogPrintf("RandomX cache is null, trying without secure...\n"); - } - randomxCache = randomx_alloc_cache(flags); - if (randomxCache == NULL) { - LogPrintf("RandomX cache is null, cannot mine!\n"); - } - } - - rxdebug("%s: created randomx flags + cache\n"); - randomx_dataset *randomxDataset = randomx_alloc_dataset(flags); - rxdebug("%s: created dataset\n"); - - if( randomxDataset == nullptr) { - LogPrintf("%s: allocating randomx dataset failed!\n", __func__); + // Use the shared dataset manager — no per-thread dataset allocation + if (g_rxDatasetManager == nullptr || !g_rxDatasetManager->initialized) { + LogPrintf("HushRandomXMiner: shared dataset manager not initialized, aborting!\n"); return; } - auto datasetItemCount = randomx_dataset_item_count(); - rxdebug("%s: dataset items=%lu\n", datasetItemCount); - char randomxHash[RANDOMX_HASH_SIZE]; rxdebug("%s: created randomxHash of size %d\n", RANDOMX_HASH_SIZE); char randomxKey[82]; // randomx spec says keysize of >60 bytes is implementation-specific @@ -1147,44 +1247,23 @@ void static RandomXMiner() // fprintf(stderr,"RandomXMiner: using initial key with interval=%d and lag=%d\n", randomxInterval, randomxBlockLag); rxdebug("%s: using initial key, interval=%d, lag=%d, Mining_height=%u\n", randomxInterval, randomxBlockLag, Mining_height); - // Use the initial key at the start of the chain, until the first key block + // Update the shared dataset key — only one thread will actually rebuild, + // others will see the key is already current and skip. if( (Mining_height) < randomxInterval + randomxBlockLag) { - randomx_init_cache(randomxCache, randomxKey, strlen(randomxKey)); - rxdebug("%s: initialized cache with initial key\n"); + g_rxDatasetManager->UpdateKey(randomxKey, strlen(randomxKey)); + rxdebug("%s: updated shared dataset with initial key\n"); } else { rxdebug("%s: calculating keyHeight with randomxInterval=%d\n", randomxInterval); - // At heights between intervals, we use the same block key and wait randomxBlockLag blocks until changing const int keyHeight = ((Mining_height - randomxBlockLag) / randomxInterval) * randomxInterval; uint256 randomxBlockKey = chainActive[keyHeight]->GetBlockHash(); - - randomx_init_cache(randomxCache, &randomxBlockKey, sizeof randomxBlockKey); - rxdebug("%s: initialized cache with keyHeight=%d, randomxBlockKey=%s\n", keyHeight, randomxBlockKey.ToString().c_str()); + g_rxDatasetManager->UpdateKey(&randomxBlockKey, sizeof randomxBlockKey); + rxdebug("%s: updated shared dataset with keyHeight=%d, randomxBlockKey=%s\n", keyHeight, randomxBlockKey.ToString().c_str()); } - const int initThreadCount = std::thread::hardware_concurrency(); - if(initThreadCount > 1) { - rxdebug("%s: initializing dataset with %d threads\n", initThreadCount); - std::vector threads; - uint32_t startItem = 0; - const auto perThread = datasetItemCount / initThreadCount; - const auto remainder = datasetItemCount % initThreadCount; - for (int i = 0; i < initThreadCount; ++i) { - const auto count = perThread + (i == initThreadCount - 1 ? remainder : 0); - threads.push_back(std::thread(&randomx_init_dataset, randomxDataset, randomxCache, startItem, count)); - startItem += count; - } - for (unsigned i = 0; i < threads.size(); ++i) { - threads[i].join(); - } - threads.clear(); - } else { - rxdebug("%s: initializing dataset with 1 thread\n"); - randomx_init_dataset(randomxDataset, randomxCache, 0, datasetItemCount); - } - - rxdebug("%s: dataset initialized\n"); - - myVM = randomx_create_vm(flags, nullptr, randomxDataset); + // Create a per-thread VM that uses the shared dataset (read-only, thread-safe) + // Acquire shared lock to prevent dataset rebuild while we're hashing + boost::shared_lock datasetLock(g_rxDatasetManager->datasetMtx); + myVM = g_rxDatasetManager->CreateVM(); if(myVM == NULL) { LogPrintf("RandomXMiner: Cannot create RandomX VM, aborting!\n"); return; @@ -1325,14 +1404,28 @@ void static RandomXMiner() CValidationState state; //{ LOCK(cs_main); - if ( !TestBlockValidity(state,B, chainActive.LastTip(), true, false)) + // Skip RandomX re-validation during TestBlockValidity — we already + // computed the correct hash, and re-verifying allocates ~256MB which + // can trigger the OOM killer on memory-constrained systems. + SetSkipRandomXValidation(true); + bool fValid = TestBlockValidity(state,B, chainActive.LastTip(), true, false); + SetSkipRandomXValidation(false); + if ( !fValid ) { h = UintToArith256(B.GetHash()); - fprintf(stderr,"RandomXMiner: Invalid randomx block mined, try again "); + fprintf(stderr,"RandomXMiner: TestBlockValidity FAILED at ht.%d nNonce=%s hash=", + Mining_height, pblock->nNonce.ToString().c_str()); for (z=31; z>=0; z--) fprintf(stderr,"%02x",((uint8_t *)&h)[z]); - gotinvalid = 1; + fprintf(stderr," nSolution.size=%lu\n", B.nSolution.size()); + // Dump nSolution hex for comparison with validator + fprintf(stderr,"RandomXMiner: nSolution="); + for (unsigned i = 0; i < B.nSolution.size(); i++) + fprintf(stderr,"%02x", B.nSolution[i]); fprintf(stderr,"\n"); + LogPrintf("RandomXMiner: TestBlockValidity FAILED at ht.%d, gotinvalid=1, state=%s\n", + Mining_height, state.GetRejectReason()); + gotinvalid = 1; return(false); } //} @@ -1401,20 +1494,32 @@ void static RandomXMiner() } rxdebug("%s: going to destroy rx VM\n"); - randomx_destroy_vm(myVM); - rxdebug("%s: destroyed VM\n"); + if (myVM != nullptr) { + randomx_destroy_vm(myVM); + myVM = nullptr; + LogPrintf("RandomXMiner: destroyed VM after inner loop\n"); + fprintf(stderr, "RandomXMiner: destroyed VM after inner loop\n"); + } else { + LogPrintf("RandomXMiner: WARNING myVM already null after inner loop, skipping destroy (would double-free)\n"); + fprintf(stderr, "RandomXMiner: WARNING myVM already null after inner loop, skipping destroy (would double-free)\n"); + } + // Release shared lock so UpdateKey can acquire exclusive lock for dataset rebuild + datasetLock.unlock(); } } catch (const boost::thread_interrupted&) { miningTimer.stop(); c.disconnect(); - randomx_destroy_vm(myVM); - LogPrintf("%s: destroyed vm via thread interrupt\n", __func__); - randomx_release_dataset(randomxDataset); - rxdebug("%s: released dataset via thread interrupt\n"); - randomx_release_cache(randomxCache); - rxdebug("%s: released cache via thread interrupt\n"); + if (myVM != nullptr) { + randomx_destroy_vm(myVM); + myVM = nullptr; + LogPrintf("%s: destroyed vm via thread interrupt\n", __func__); + } else { + LogPrintf("%s: WARNING myVM already null in thread interrupt handler, skipping destroy (would double-free)\n", __func__); + fprintf(stderr, "%s: WARNING myVM already null in thread interrupt, would have double-freed!\n", __func__); + } + // Dataset and cache are owned by g_rxDatasetManager — do NOT release here LogPrintf("HushRandomXMiner terminated\n"); throw; @@ -1423,20 +1528,21 @@ void static RandomXMiner() c.disconnect(); fprintf(stderr,"RandomXMiner: runtime error: %s\n", e.what()); - randomx_destroy_vm(myVM); - LogPrintf("%s: destroyed vm because of error\n", __func__); - randomx_release_dataset(randomxDataset); - rxdebug("%s: released dataset because of error\n"); - randomx_release_cache(randomxCache); - rxdebug("%s: released cache because of error\n"); + if (myVM != nullptr) { + randomx_destroy_vm(myVM); + myVM = nullptr; + LogPrintf("%s: destroyed vm because of error\n", __func__); + } + // Dataset and cache are owned by g_rxDatasetManager — do NOT release here return; } - randomx_release_dataset(randomxDataset); - rxdebug("%s: released dataset in normal exit\n"); - randomx_release_cache(randomxCache); - rxdebug("%s: released cache in normal exit\n"); + // Only destroy per-thread VM, dataset/cache are shared + if (myVM != nullptr) { + randomx_destroy_vm(myVM); + myVM = nullptr; + } miningTimer.stop(); c.disconnect(); } @@ -1882,6 +1988,14 @@ void static BitcoinMiner() minerThreads->interrupt_all(); delete minerThreads; minerThreads = NULL; + + // Shutdown shared RandomX dataset manager after all threads are done + if (g_rxDatasetManager != nullptr) { + g_rxDatasetManager->Shutdown(); + delete g_rxDatasetManager; + g_rxDatasetManager = nullptr; + LogPrintf("%s: destroyed shared RandomX dataset manager\n", __func__); + } } if(fDebug) @@ -1896,6 +2010,21 @@ void static BitcoinMiner() minerThreads = new boost::thread_group(); + // Initialize shared RandomX dataset manager before spawning miner threads + if (ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX) { + g_rxDatasetManager = new RandomXDatasetManager(); + if (!g_rxDatasetManager->Init()) { + LogPrintf("%s: FATAL - Failed to initialize shared RandomX dataset manager\n", __func__); + fprintf(stderr, "%s: FATAL - Failed to initialize shared RandomX dataset manager\n", __func__); + delete g_rxDatasetManager; + g_rxDatasetManager = nullptr; + delete minerThreads; + minerThreads = NULL; + return; + } + LogPrintf("%s: shared RandomX dataset manager initialized\n", __func__); + } + for (int i = 0; i < nThreads; i++) { #ifdef ENABLE_WALLET if ( ASSETCHAINS_ALGO == ASSETCHAINS_EQUIHASH ) { diff --git a/src/pow.cpp b/src/pow.cpp index ac7205795..e7ae2804b 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -694,6 +694,13 @@ static randomx_cache *s_rxCache = nullptr; static randomx_vm *s_rxVM = nullptr; static std::string s_rxCurrentKey; // tracks current key to avoid re-init +// Thread-local flag: skip CheckRandomXSolution when the miner is validating its own block +// The miner already computed the correct RandomX hash — re-verifying with a separate +// cache+VM would allocate ~256MB extra memory and can trigger the OOM killer. +thread_local bool fSkipRandomXValidation = false; + +void SetSkipRandomXValidation(bool skip) { fSkipRandomXValidation = skip; } + CBlockIndex *hush_chainactive(int32_t height); bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height) @@ -715,6 +722,10 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height) if (HUSH_LOADINGBLOCKS != 0) return true; + // Skip when miner is validating its own block via TestBlockValidity + if (fSkipRandomXValidation) + return true; + // nSolution must be exactly RANDOMX_HASH_SIZE (32) bytes if (pblock->nSolution.size() != RANDOMX_HASH_SIZE) { return error("CheckRandomXSolution(): nSolution size %u != expected %d at height %d", @@ -779,9 +790,32 @@ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height) // Compare computed hash against nSolution if (memcmp(computedHash, pblock->nSolution.data(), RANDOMX_HASH_SIZE) != 0) { - return error("CheckRandomXSolution(): RandomX hash mismatch at height %d", height); + // Debug: dump both hashes for diagnosis + std::string computedHex, solutionHex; + for (int i = 0; i < RANDOMX_HASH_SIZE; i++) { + char buf[4]; + snprintf(buf, sizeof(buf), "%02x", (uint8_t)computedHash[i]); + computedHex += buf; + snprintf(buf, sizeof(buf), "%02x", pblock->nSolution[i]); + solutionHex += buf; + } + fprintf(stderr, "CheckRandomXSolution(): HASH MISMATCH at height %d\n", height); + fprintf(stderr, " computed : %s\n", computedHex.c_str()); + fprintf(stderr, " nSolution: %s\n", solutionHex.c_str()); + fprintf(stderr, " rxKey size=%lu, input size=%lu, nNonce=%s\n", + rxKey.size(), ss.size(), pblock->nNonce.ToString().c_str()); + fprintf(stderr, " nSolution.size()=%lu, RANDOMX_HASH_SIZE=%d\n", + pblock->nSolution.size(), RANDOMX_HASH_SIZE); + // Also log to debug.log + LogPrintf("CheckRandomXSolution(): HASH MISMATCH at height %d\n", height); + LogPrintf(" computed : %s\n", computedHex); + LogPrintf(" nSolution: %s\n", solutionHex); + LogPrintf(" rxKey size=%lu, input size=%lu, nNonce=%s\n", + rxKey.size(), ss.size(), pblock->nNonce.ToString()); + return false; } + LogPrint("randomx", "CheckRandomXSolution(): valid at height %d\n", height); return true; } diff --git a/src/pow.h b/src/pow.h index 208ce5d06..6027c45f9 100644 --- a/src/pow.h +++ b/src/pow.h @@ -41,6 +41,9 @@ bool CheckEquihashSolution(const CBlockHeader *pblock, const CChainParams&); /** Check whether a block header contains a valid RandomX solution */ bool CheckRandomXSolution(const CBlockHeader *pblock, int32_t height); +/** Set thread-local flag to skip RandomX validation (used by miner during TestBlockValidity) */ +void SetSkipRandomXValidation(bool skip); + /** Return the RandomX key rotation interval in blocks */ int GetRandomXInterval();