git / moonbite-core / src/crypto/randomx_miner.cpp · raw
// Copyright (c) 2026 The MoonBite Core developers// Distributed under the MIT software license, see the accompanying// file COPYING or http://www.opensource.org/licenses/mit-license.php.#include <crypto/randomx_miner.h>#include <crypto/randomx_hasher.h>#include <crypto/randomx/randomx.h>#include <atomic>#include <chrono>#include <cstdlib>#include <cstring>#include <mutex>#include <thread>#include <vector>namespace {// The dataset key comes from randomx_hasher.h (MBITE_RX_SEED). Mining and// validation MUST key the dataset identically or found nonces are rejected.std::mutex g_init_mutex;randomx_flags g_flags;randomx_cache* g_cache = nullptr;randomx_dataset* g_dataset = nullptr;bool g_ready = false;// Light mode uses the ~256 MB cache only (no ~2 GB dataset), so the miner fits// on small/low-RAM hosts (e.g. a 512 MB container) instead of being OOM-killed// while initialising the dataset. Hashes are identical to fast mode, so found// nonces still satisfy consensus. Enable by setting MBITE_MINER_LIGHT=1 (any// non-empty value other than "0"). Slower per-hash, fine for low-difficulty// bootstrap mining.bool LightModeRequested(){ const char* v = std::getenv("MBITE_MINER_LIGHT"); return v != nullptr && v[0] != '\0' && !(v[0] == '0' && v[1] == '\0');}void InitDatasetWorker(unsigned long start, unsigned long count){ randomx_init_dataset(g_dataset, g_cache, start, count);}// Allocate the fast-mode cache + full dataset once, initialising the dataset in// parallel. Guarded by g_init_mutex so concurrent generate calls build it once.bool EnsureDataset(int nThreads){ std::lock_guard<std::mutex> lock(g_init_mutex); if (g_ready) return true; // Light mode: cache-only, no ~2 GB dataset. Do NOT set RANDOMX_FLAG_FULL_MEM // and leave g_dataset null so each worker VM runs against the cache. This // caps RAM near ~256 MB so the miner cannot be OOM-killed on small hosts. if (LightModeRequested()) { g_flags = randomx_get_flags(); g_cache = randomx_alloc_cache(g_flags); if (g_cache == nullptr) { g_flags = RANDOMX_FLAG_DEFAULT; g_cache = randomx_alloc_cache(g_flags); if (g_cache == nullptr) return false; } randomx_init_cache(g_cache, MBITE_RX_SEED, MBITE_RX_SEED_LEN); g_dataset = nullptr; g_ready = true; return true; } // Fast mode. Try, in order: autodetected flags + large pages (2 MB pages // roughly +40% H/s: the 2 GB dataset and per-VM scratchpads stop thrashing // the TLB), the same without large pages, then the portable interpreter. // Large pages need pre-reserved hugepages (Linux: vm.nr_hugepages >= ~1200) // or the allocation simply fails and we fall through. const randomx_flags base = static_cast<randomx_flags>(randomx_get_flags() | RANDOMX_FLAG_FULL_MEM); const randomx_flags candidates[3] = { static_cast<randomx_flags>(base | RANDOMX_FLAG_LARGE_PAGES), base, static_cast<randomx_flags>(RANDOMX_FLAG_DEFAULT | RANDOMX_FLAG_FULL_MEM), }; g_cache = nullptr; g_dataset = nullptr; for (const randomx_flags f : candidates) { randomx_cache* c = randomx_alloc_cache(f); if (c == nullptr) continue; randomx_dataset* d = randomx_alloc_dataset(f); if (d == nullptr) { randomx_release_cache(c); continue; } g_flags = f; g_cache = c; g_dataset = d; break; } if (g_cache == nullptr || g_dataset == nullptr) return false; randomx_init_cache(g_cache, MBITE_RX_SEED, MBITE_RX_SEED_LEN); const unsigned long total = randomx_dataset_item_count(); if (nThreads < 1) nThreads = 1; std::vector<std::thread> pool; const unsigned long per = total / static_cast<unsigned long>(nThreads); unsigned long start = 0; for (int i = 0; i < nThreads; ++i) { const unsigned long cnt = (i == nThreads - 1) ? (total - start) : per; pool.emplace_back(InitDatasetWorker, start, cnt); start += cnt; } for (auto& t : pool) t.join(); g_ready = true; return true;}// Per-call search state, shared only by that call's workers. (A process-wide// flag let one generate call's find, or its reset, cut short another's.)struct SearchState { std::atomic<bool> stop{false}; // a nonce was found, or the caller asked to stop std::atomic<bool> found{false}; std::atomic<uint32_t> nonce{0}; std::atomic<uint64_t> tried{0}; std::atomic<int> running{0};};// Compare 256-bit little-endian values: return true iff hash <= target. Mirrors// the arith_uint256 ordering used by consensus CheckProofOfWork().inline bool HashLEleTargetLE(const unsigned char* h, const unsigned char* t){ for (int i = 31; i >= 0; --i) { if (h[i] < t[i]) return true; if (h[i] > t[i]) return false; } return true; // equal is acceptable}void SearchWorker(SearchState* st, const unsigned char* base80, const unsigned char* target32, uint32_t nonceStart, uint64_t count, uint32_t step, uint32_t offset){ uint64_t tried = 0; randomx_vm* vm = randomx_create_vm(g_flags, g_cache, g_dataset); if (vm == nullptr && (g_flags & RANDOMX_FLAG_LARGE_PAGES)) { // Hugepage pool exhausted by the dataset: a VM scratchpad on normal // pages hashes identically, just a little slower. vm = randomx_create_vm(static_cast<randomx_flags>(g_flags & ~RANDOMX_FLAG_LARGE_PAGES), g_cache, g_dataset); } if (vm != nullptr) { unsigned char hdr[80]; std::memcpy(hdr, base80, 80); unsigned char hash[32]; for (uint64_t k = offset; k < count; k += step) { if (st->stop.load(std::memory_order_relaxed)) break; const uint32_t nonce = nonceStart + static_cast<uint32_t>(k); hdr[76] = static_cast<unsigned char>(nonce & 0xff); hdr[77] = static_cast<unsigned char>((nonce >> 8) & 0xff); hdr[78] = static_cast<unsigned char>((nonce >> 16) & 0xff); hdr[79] = static_cast<unsigned char>((nonce >> 24) & 0xff); randomx_calculate_hash(vm, hdr, 80, hash); ++tried; if (HashLEleTargetLE(hash, target32)) { bool expect = false; if (st->found.compare_exchange_strong(expect, true)) { st->nonce.store(nonce, std::memory_order_relaxed); } st->stop.store(true); break; } } randomx_destroy_vm(vm); } st->tried.fetch_add(tried); st->running.fetch_sub(1);}} // namespacebool RandomXMineHeader(const unsigned char header80[80], const unsigned char target32[32], uint32_t nonceStart, uint64_t count, int nThreads, uint32_t& foundNonce, const std::function<bool()>& should_stop, uint64_t* hashes_done){ if (hashes_done) *hashes_done = 0; if (nThreads < 1) nThreads = 1; if (count == 0) return false; // Never let nonceStart + count run past the 32-bit nonce space. const uint64_t space = static_cast<uint64_t>(0x100000000ULL) - nonceStart; if (count > space) count = space; if (count == 0) return false; if (!EnsureDataset(nThreads)) return false; SearchState st; st.running.store(nThreads); std::vector<std::thread> pool; pool.reserve(nThreads); for (int i = 0; i < nThreads; ++i) { pool.emplace_back(SearchWorker, &st, header80, target32, nonceStart, count, static_cast<uint32_t>(nThreads), static_cast<uint32_t>(i)); } // While the workers hash, poll the caller's stop condition (e.g. a new chain // tip arrived, making this template stale) and end the search when it fires. while (st.running.load() > 0) { if (should_stop && !st.stop.load() && should_stop()) { st.stop.store(true); break; } std::this_thread::sleep_for(std::chrono::milliseconds(50)); } for (auto& t : pool) t.join(); if (hashes_done) *hashes_done = st.tried.load(); if (st.found.load()) { foundNonce = st.nonce.load(); return true; } return false;}