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;}