From 2232868d9ff75ba7753f75a7913802226602fdf0 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 27 Aug 2026 20:24:13 -0500 Subject: [PATCH] stratum: RandomX pool mining support + reference miner (stratummine) The stratum server was hardcoded for legacy Equihash (1347-byte solution, sol.begin()+3 offset, CheckEquihashSolution) and was off-by-default with a "do not use on RandomX" warning. DragonX is RandomX, so external pool mining was impossible. This wires RandomX end-to-end. Server (stratum.cpp), branched on ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX: * GetWorkUnit sets StratumWork.nHeight and, for RandomX, sends the per-height RandomX key via a new mining.set_randomx_key message (a miner cannot derive it without the chain). The legacy mining.notify format is unchanged. * SubmitBlock/stratum_mining_submit accept a 32-byte solution (== the RandomX hash, used as nSolution verbatim) and validate it with CheckRandomXSolution instead of CheckEquihashSolution. Target check (GetHash() < target) and the nNonce = extranonce1||extranonce2 assembly are shared with the equihash path. * -stratumtarget= overrides the pool share target (default diff-1); lets a solo/low-difficulty test miner accept easy shares. * GetWorkUnit's IsInitialBlockDownload guard is bypassed under -testnode=1 so an isolated low-work test chain can serve work. Reference miner: `stratummine "host" port ("address" timeout)` RPC (rpc/mining.cpp, POSIX-only). A minimal stratum client that subscribes/authorizes, receives work + the RandomX key, varies nNonce, hashes with RandomX via GetRandomXInput (byte- identical to CheckRandomXSolution) and submits a 32-byte solution. Off-the-shelf Equihash/Monero miners can't speak DragonX's 256-bit-nNonce Zcash header, so this is the reference implementation. Added to the rpc client arg-conversion table. Validated: loopback (chain 0->4, accepted every time, verifychain=true) AND a real 2-box LAN run (Linux miner -> Mac stratum server, 3 blocks accepted, verifychain=true). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/init.cpp | 9 +- src/rpc/client.cpp | 2 + src/rpc/mining.cpp | 248 +++++++++++++++++++++++++++++++++++++++++++++ src/stratum.cpp | 104 +++++++++++++------ 4 files changed, 330 insertions(+), 33 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 242309d99..df83709af 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -621,6 +621,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageGroup(_("Stratum server options:")); strUsage += HelpMessageOpt("-stratum", _("Enable stratum server (default: off)")); + strUsage += HelpMessageOpt("-stratumtarget=", _("Pool share target (64-hex, big-endian; larger = easier). Default is the diff-1 target. Useful for solo/low-difficulty mining.")); strUsage += HelpMessageOpt("-stratumaddress=
", _("Mining address to use when special address of 'x' is sent by miner (default: none)")); strUsage += HelpMessageOpt("-stratumbind=", _("Bind to given address to listen for Stratum work requests. Use [host]:port notation for IPv6. This option can be specified multiple times (default: bind to all interfaces)")); strUsage += HelpMessageOpt("-stratumport=", strprintf(_("Listen for Stratum work requests on (default: %u or testnet: %u)"), BaseParams().StratumPort(), BaseParams().StratumPort())); @@ -991,10 +992,10 @@ bool AppInitServers(boost::thread_group& threadGroup) RPCServer::OnPreCommand(&OnRPCPreCommand); if (!InitHTTPServer()) return false; - // WARNING: the stratum server (stratum.cpp) is Equihash-era code: it assumes a 1347-byte - // Equihash solution and calls CheckEquihashSolution. It has NOT been updated for DragonX's - // 32-byte RandomX solution and must not be relied on for mining without a full revalidation. - // It stays off by default (DEFAULT_STRATUM_ENABLE=false); only -stratum turns it on. + // Stratum server (stratum.cpp) supports DragonX's RandomX PoW (32-byte solution + per-height + // RandomX key conveyed to the miner) as well as legacy Equihash, branched on ASSETCHAINS_ALGO. + // Off by default (DEFAULT_STRATUM_ENABLE=false); only -stratum turns it on. Needs a RandomX-aware + // stratum miner (see contrib/ reference miner) — stock Equihash/Monero miners won't work. if (GetBoolArg("-stratum", DEFAULT_STRATUM_ENABLE) && !InitStratumServer()) return false; if (!StartRPC()) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 970cf704e..3daf6738e 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -42,6 +42,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getaddednodeinfo", 0 }, { "setgenerate", 0 }, { "setgenerate", 1 }, + { "stratummine", 1 }, // port + { "stratummine", 3 }, // timeout { "generate", 0 }, { "getnetworkhashps", 0 }, { "getnetworkhashps", 1 }, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 92e7adc8a..5d6fb318e 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -45,6 +45,21 @@ #include +#include "compat/byteswap.h" // bswap_32 for the stratum wire fields (version/time/bits) +#ifndef WIN32 +// stratummine (below) is a POSIX-only reference RandomX stratum miner used to exercise the pool +// path end-to-end. It reuses DragonX's own RandomX + GetRandomXInput so its hash is byte-identical +// to CheckRandomXSolution. Not built on Windows (raw POSIX sockets). +#include "RandomX/src/randomx.h" +#include +#include +#include +#include +#include +#include +#include +#endif + using namespace std; #include "hush_defs.h" @@ -1053,9 +1068,242 @@ UniValue getblocksubsidy(const UniValue& params, bool fHelp, const CPubKey& mypk } +#ifndef WIN32 +extern uint32_t ASSETCHAINS_ALGO, ASSETCHAINS_RANDOMX; // hush_defs.h — active PoW algorithm + +// Send one newline-terminated JSON line on a blocking socket. +static bool StratumMinerSend(int fd, const std::string& s) +{ + std::string line = s; + if (line.empty() || line.back() != '\n') line += '\n'; + size_t off = 0; + while (off < line.size()) { + ssize_t n = send(fd, line.data() + off, line.size() - off, 0); + if (n <= 0) return false; + off += (size_t)n; + } + return true; +} + +// Wait up to timeout_ms for data, then split all completed lines out of buf into out. +// Returns false only on socket error/close (a timeout with no data is success with out empty). +static bool StratumMinerRecvLines(int fd, std::string& buf, int timeout_ms, std::vector& out) +{ + fd_set rfds; FD_ZERO(&rfds); FD_SET(fd, &rfds); + struct timeval tv; tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; + int r = select(fd + 1, &rfds, NULL, NULL, &tv); + if (r < 0) return false; + if (r == 0) return true; + char tmp[8192]; + ssize_t n = recv(fd, tmp, sizeof(tmp), 0); + if (n <= 0) return false; + buf.append(tmp, tmp + n); + size_t pos; + while ((pos = buf.find('\n')) != std::string::npos) { + std::string line = buf.substr(0, pos); + buf.erase(0, pos + 1); + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (!line.empty()) out.push_back(line); + } + return true; +} + +// Reference RandomX stratum miner (test utility): connect to a DragonX stratum server, subscribe + +// authorize, receive work + the per-height RandomX key, then vary the block nNonce, hash with +// RandomX (byte-identical to CheckRandomXSolution via GetRandomXInput), and submit a 32-byte +// solution when the block hash meets target. Exists to validate the -stratum RandomX pool path. +UniValue stratummine(const UniValue& params, bool fHelp, const CPubKey& mypk) +{ + if (fHelp || params.size() < 2 || params.size() > 4) + throw runtime_error( + "stratummine \"host\" port ( \"address\" timeout )\n" + "\nReference RandomX stratum miner: connect to a DragonX stratum server, solve RandomX,\n" + "and submit until one share/block is accepted or the timeout elapses. For testing -stratum.\n" + "\nArguments:\n" + "1. \"host\" (string, required) stratum server host or IP\n" + "2. port (numeric, required) stratum server port\n" + "3. \"address\" (string, optional, default=\"x\") payout R-address, or \"x\" for the server default\n" + "4. timeout (numeric, optional, default=120) seconds to mine before giving up\n" + "\nResult: {\"found\":bool,\"accepted\":bool,\"hash\":\"..\",\"hashes\":n,\"seconds\":n}\n"); + + if (ASSETCHAINS_ALGO != ASSETCHAINS_RANDOMX) + throw JSONRPCError(RPC_MISC_ERROR, "stratummine only supports RandomX chains"); + + const std::string host = params[0].get_str(); + const int port = params[1].get_int(); + const std::string addr = params.size() > 2 ? params[2].get_str() : "x"; + const int64_t timeout = params.size() > 3 ? params[3].get_int64() : 120; + const int64_t deadline = GetTime() + timeout; + + // connect (blocking TCP) + struct addrinfo hints; memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; + struct addrinfo* ai = NULL; + if (getaddrinfo(host.c_str(), strprintf("%d", port).c_str(), &hints, &ai) != 0 || !ai) + throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, strprintf("cannot resolve %s:%d", host, port)); + int fd = -1; + for (struct addrinfo* p = ai; p; p = p->ai_next) { + fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (fd < 0) continue; + if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) break; + close(fd); fd = -1; + } + freeaddrinfo(ai); + if (fd < 0) + throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, strprintf("cannot connect to %s:%d", host, port)); + { int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char*)&one, sizeof(one)); } + + StratumMinerSend(fd, "{\"id\":1,\"method\":\"mining.subscribe\",\"params\":[\"dragonx-refminer/1.0\"]}"); + StratumMinerSend(fd, strprintf("{\"id\":2,\"method\":\"mining.authorize\",\"params\":[\"%s\",\"x\"]}", addr)); + + // state accumulated from the server + std::vector extranonce1; + std::string rxKey; + bool haveKey = false, haveTarget = false, haveJob = false; + arith_uint256 poolTarget; + std::string jobId, timeHex; + uint32_t nVersion = 4, nTime = 0, nBits = 0; + uint256 hashPrevBlock, hashMerkleRoot, hashReserved; + + auto processLine = [&](const std::string& line) { + UniValue v; + if (!v.read(line)) return; + const UniValue& id = find_value(v, "id"); + const UniValue& result = find_value(v, "result"); + if (id.isNum() && id.get_int() == 1 && result.isArray() && result.size() >= 2 && result[1].isStr()) + extranonce1 = ParseHex(result[1].get_str()); + const UniValue& method = find_value(v, "method"); + if (!method.isStr()) return; + const UniValue& p = find_value(v, "params"); + if (!p.isArray()) return; + const std::string m = method.get_str(); + if (m == "mining.set_randomx_key" && p.size() >= 1) { + std::vector kb = ParseHex(p[0].get_str()); + rxKey.assign(kb.begin(), kb.end()); + haveKey = true; + } else if (m == "mining.set_target" && p.size() >= 1) { + poolTarget = UintToArith256(uint256S(p[0].get_str())); + haveTarget = true; + } else if (m == "mining.notify" && p.size() >= 7) { + jobId = p[0].get_str(); + nVersion = bswap_32((uint32_t)strtoul(p[1].get_str().c_str(), NULL, 16)); + hashPrevBlock = uint256(ParseHex(p[2].get_str())); + hashMerkleRoot = uint256(ParseHex(p[3].get_str())); + hashReserved = uint256(ParseHex(p[4].get_str())); + timeHex = p[5].get_str(); + nTime = bswap_32((uint32_t)strtoul(timeHex.c_str(), NULL, 16)); + nBits = bswap_32((uint32_t)strtoul(p[6].get_str().c_str(), NULL, 16)); + haveJob = true; + } + }; + + std::string buf; + for (int i = 0; i < 120 && !(haveJob && haveTarget && haveKey && !extranonce1.empty()); i++) { + std::vector lines; + if (!StratumMinerRecvLines(fd, buf, 250, lines)) { close(fd); throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "stratum connection closed during handshake"); } + for (const std::string& l : lines) processLine(l); + if (GetTime() > deadline) break; + } + if (!(haveJob && haveTarget && haveKey && !extranonce1.empty())) { + close(fd); + throw JSONRPCError(RPC_MISC_ERROR, "did not receive complete RandomX work (need job + target + randomx key + extranonce)"); + } + + randomx_flags flags = randomx_get_flags(); + randomx_cache* cache = randomx_alloc_cache(flags); + if (!cache) { close(fd); throw JSONRPCError(RPC_MISC_ERROR, "randomx_alloc_cache failed"); } + randomx_init_cache(cache, rxKey.data(), rxKey.size()); + std::string vmKey = rxKey; + randomx_vm* vm = randomx_create_vm(flags, cache, NULL); + if (!vm) { randomx_release_cache(cache); close(fd); throw JSONRPCError(RPC_MISC_ERROR, "randomx_create_vm failed"); } + + UniValue res(UniValue::VOBJ); + bool found = false, accepted = false, submitted = false; + uint64_t hashes = 0, en2ctr = 0; + std::string foundHash; + const int64_t started = GetTime(); + + while (GetTime() <= deadline && !found) { + std::string prevJob = jobId; + std::vector lines; + if (!StratumMinerRecvLines(fd, buf, 0, lines)) break; + for (const std::string& l : lines) processLine(l); + if (jobId != prevJob) en2ctr = 0; // new tip -> restart the nonce search + if (rxKey != vmKey) { randomx_init_cache(cache, rxKey.data(), rxKey.size()); randomx_vm_set_cache(vm, cache); vmKey = rxKey; } + + arith_uint256 blockTarget; bool fNeg, fOver; + blockTarget.SetCompact(nBits, &fNeg, &fOver); + // Mine to the harder of (block target, pool share target) so a solution is a real block AND + // passes the server's low-diff share check. + arith_uint256 tgt = (haveTarget && poolTarget < blockTarget) ? poolTarget : blockTarget; + + CBlockHeader hdr; + hdr.nVersion = nVersion; + hdr.hashPrevBlock = hashPrevBlock; + hdr.hashMerkleRoot = hashMerkleRoot; + hdr.hashFinalSaplingRoot = hashReserved; + hdr.nTime = nTime; + hdr.nBits = nBits; + + for (int i = 0; i < 2000 && GetTime() <= deadline; i++) { + std::vector nonce = extranonce1; + nonce.resize(32, 0); + for (int b = 0; b < 8; b++) nonce[8 + b] = (unsigned char)((en2ctr >> (8 * b)) & 0xff); + en2ctr++; hashes++; + hdr.nNonce = uint256(nonce); + std::vector input = GetRandomXInput(hdr); + unsigned char h[RANDOMX_HASH_SIZE]; + randomx_calculate_hash(vm, input.data(), input.size(), h); + hdr.nSolution.assign(h, h + RANDOMX_HASH_SIZE); + if (UintToArith256(hdr.GetHash()) <= tgt) { + std::vector en2(nonce.begin() + 8, nonce.end()); + std::string submit = strprintf( + "{\"id\":4,\"method\":\"mining.submit\",\"params\":[\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"]}", + addr, jobId, timeHex, HexStr(en2), HexStr(hdr.nSolution)); + StratumMinerSend(fd, submit); + submitted = true; + foundHash = hdr.GetHash().ToString(); + bool sawResult = false; + for (int k = 0; k < 40 && !sawResult; k++) { + std::vector rl; + if (!StratumMinerRecvLines(fd, buf, 250, rl)) break; + for (const std::string& l : rl) { + processLine(l); + UniValue rv; if (!rv.read(l)) continue; + const UniValue& rid = find_value(rv, "id"); + if (rid.isNum() && rid.get_int() == 4) { + sawResult = true; + const UniValue& r = find_value(rv, "result"); + accepted = r.isBool() ? r.get_bool() : find_value(rv, "error").isNull(); + } + } + } + found = true; + break; + } + } + } + + randomx_destroy_vm(vm); + randomx_release_cache(cache); + close(fd); + + res.push_back(Pair("found", found)); + res.push_back(Pair("submitted", submitted)); + res.push_back(Pair("accepted", accepted)); + res.push_back(Pair("hashes", (uint64_t)hashes)); + res.push_back(Pair("seconds", (int64_t)(GetTime() - started))); + if (!foundHash.empty()) res.push_back(Pair("hash", foundHash)); + return res; +} +#endif // !WIN32 + static const CRPCCommand commands[] = { // category name actor (function) okSafeMode // --------------------- ------------------------ ----------------------- ---------- +#ifndef WIN32 + { "mining", "stratummine", &stratummine, true }, +#endif { "mining", "getlocalsolps", &getlocalsolps, true }, { "mining", "getnetworksolps", &getnetworksolps, true }, { "mining", "getnetworkhashps", &getnetworkhashps, true }, diff --git a/src/stratum.cpp b/src/stratum.cpp index 9b14289bd..42e31a10d 100644 --- a/src/stratum.cpp +++ b/src/stratum.cpp @@ -16,6 +16,7 @@ #include "httpserver.h" #include "miner.h" #include "netbase.h" +#include "pow.h" // RandomX PoW: CheckRandomXSolution / GetRandomXKey / GetRandomXInput; and CheckEquihashSolution #include "net.h" #include "rpc/server.h" #include "serialize.h" @@ -663,6 +664,15 @@ void CustomizeWork(const StratumClient& client, const StratumWork& current_work, // cb_branch = current_work.m_cb_branch; } +// DragonX PoW is RandomX (32-byte solution); Equihash is legacy (1347-byte solution). The stratum +// work and submit paths branch on this: RandomX hands the miner the per-height RandomX key (which it +// cannot derive without the chain) and validates a 32-byte solution via CheckRandomXSolution(); +// Equihash keeps the legacy path (1347-byte solution + the 3-byte prefix + CheckEquihashSolution). +extern uint32_t ASSETCHAINS_ALGO, ASSETCHAINS_RANDOMX; // hush_defs.h — active PoW algorithm selector +extern int32_t HUSH_TESTNODE; // hush_globals.h — -testnode: relax IBD/sync guards for isolated test nodes +static inline bool StratumIsRandomX() { return ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX; } +static const size_t RX_STRATUM_SOLUTION_SIZE = 32; // == RANDOMX_HASH_SIZE (kept local to avoid pulling randomx.h into stratum) + std::string GetWorkUnit(StratumClient& client) { // LOCK(cs_main); @@ -688,7 +698,7 @@ std::string GetWorkUnit(StratumClient& client) throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "DragonX is not connected!"); } - if (IsInitialBlockDownload()) { + if (IsInitialBlockDownload() && HUSH_TESTNODE == 0) { const std::string msg = strprintf("%s: Unable to get work unit, DragonX is still downloading blocks!", __func__); LogPrint("stratum", "%s\n", msg); throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "DragonX is downloading blocks..."); @@ -743,6 +753,9 @@ std::string GetWorkUnit(StratumClient& client) job_id = new_work->block.GetHash(); //work_templates[job_id] = StratumWork(*new_work, new_work->block.vtx[0]->HasWitness()); work_templates[job_id] = StratumWork(*new_work, false); + // Height of the block being mined — used for RandomX key derivation (GetRandomXKey) and + // CheckRandomXSolution/CheckProofOfWork on submit. Previously left 0 (Equihash didn't need it). + work_templates[job_id].nHeight = tip_new->GetHeight() + 1; tip = tip_new; @@ -916,7 +929,25 @@ std::string GetWorkUnit(StratumClient& client) mining_notify.push_back(Pair("method", "mining.notify")); mining_notify.push_back(Pair("params", params)); + // RandomX: the miner cannot derive the per-height RandomX key on its own (it depends on a block + // hash deep in the chain), so hand it the key bytes + height explicitly. Sent as its own + // mining.set_randomx_key message so the equihash-format mining.notify above stays byte-compatible + // with legacy miners; a RandomX miner reads this before hashing. + std::string randomx_key_msg; + if (StratumIsRandomX()) { + const std::string rxKey = GetRandomXKey(current_work.nHeight); + UniValue set_rxkey(UniValue::VOBJ); + set_rxkey.push_back(Pair("id", client.m_nextid++)); + set_rxkey.push_back(Pair("method", "mining.set_randomx_key")); + UniValue rxparams(UniValue::VARR); + rxparams.push_back(HexStr(rxKey.begin(), rxKey.end())); // RandomX key bytes (hex) + rxparams.push_back(current_work.nHeight); // block height (sanity/logging) + set_rxkey.push_back(Pair("params", rxparams)); + randomx_key_msg = set_rxkey.write() + "\n"; + } + return GetExtraNonceRequest(client, job_id) + + randomx_key_msg + set_target.write() + "\n" + mining_notify.write() + "\n"; } @@ -924,17 +955,15 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork const std::vector& extranonce1, const std::vector& extranonce2, boost::optional nVersion, uint32_t nTime, const std::vector& sol) { - // ============================ WARNING (Equihash-era code) ============================ - // This entire submit path is hardcoded for the legacy Equihash proof-of-work: - // it expects a 1347-byte Equihash solution and calls CheckEquihashSolution() below. - // DragonX uses RandomX PoW (32-byte solution), NOT Equihash. This stratum path has - // NOT been updated for RandomX and must not be relied on without full revalidation. - // The 1347-byte length checks, the "sol.begin()+3" solution offset, and the equihash - // target/difficulty math are all Equihash-era and are intentionally left unchanged. - // ==================================================================================== + // Submit path handles BOTH proof-of-works, branched on StratumIsRandomX(): + // * RandomX (DragonX): `sol` is the 32-byte RandomX hash and IS nSolution verbatim; validated + // via CheckRandomXSolution(&blkhdr, height). The target check (GetHash() < target) and the + // nNonce = extranonce1||extranonce2 assembly are identical to the equihash path. + // * Equihash (legacy): `sol` is the 1347-byte solution; the 3-byte zcash prefix is stripped + // and CheckEquihashSolution() validates it. // // called from stratum_mining_submit and uses following data, came from client: - // ["WORKER_NAME", "JOB_ID", "TIME", "NONCE_2", "EQUIHASH_SOLUTION"] + // ["WORKER_NAME", "JOB_ID", "TIME", "NONCE_2", "SOLUTION"] // all other params we have saved in other places if (extranonce1.size() + extranonce2.size() != 32) { @@ -943,11 +972,9 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork throw JSONRPCError(RPC_INVALID_PARAMETER, msg); } - // WARNING (Equihash-era): 1347 is the Equihash-200,9 solution length. DragonX is RandomX - // (32-byte solution), so this length check does not match the live PoW. Left unchanged - // because this whole path is Equihash-era; do not repurpose without revalidating the miner protocol. - if (sol.size() != 1347) { - std::string msg = strprintf("%s: solution is wrong length (received %d bytes; expected %d bytes", __func__, extranonce2.size(), 1347); + const size_t expected_sol_size = StratumIsRandomX() ? RX_STRATUM_SOLUTION_SIZE : 1347; + if (sol.size() != expected_sol_size) { + std::string msg = strprintf("%s: solution is wrong length (received %d bytes; expected %d bytes)", __func__, sol.size(), (int)expected_sol_size); LogPrint("stratum", "%s\n", msg); throw JSONRPCError(RPC_INVALID_PARAMETER, msg); } @@ -979,7 +1006,10 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork std::vector nonce(extranonce1); nonce.insert(nonce.end(), extranonce2.begin(), extranonce2.end()); - blkhdr.nSolution = std::vector(sol.begin() + 3, sol.end()); + // RandomX: nSolution IS the 32-byte RandomX hash (verbatim). Equihash: strip the 3-byte + // zcash solution-size prefix. + blkhdr.nSolution = StratumIsRandomX() ? sol + : std::vector(sol.begin() + 3, sol.end()); blkhdr.hashFinalSaplingRoot = current_work.GetBlock().hashFinalSaplingRoot; blkhdr.hashMerkleRoot = current_work.GetBlock().hashMerkleRoot; @@ -987,8 +1017,16 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork // block is constructed, now it's time to VerifyEH - if (instance_of_cstratumparams.fCheckEquihashSolution && !CheckEquihashSolution(&blkhdr, Params())) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid equihash solution"); + if (StratumIsRandomX()) { + // Verify the submitted 32-byte solution really is the RandomX hash of this header + // (nSolution == randomx_hash(GetRandomXInput(blkhdr), GetRandomXKey(height))). This is the + // consensus authority for the solution; without it a miner could submit a low-GetHash() + // block with a bogus nSolution. Rejects fake shares before we count/relay them. + if (!CheckRandomXSolution(&blkhdr, current_work.nHeight)) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid RandomX solution"); + } else if (instance_of_cstratumparams.fCheckEquihashSolution && !CheckEquihashSolution(&blkhdr, Params())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid equihash solution"); + } arith_uint256 bnTarget; bool fNegative, fOverflow; bnTarget.SetCompact(blkhdr.nBits, &fNegative, &fOverflow); @@ -1068,7 +1106,8 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork // nNonce <<= 32; nNonce >>= 16; // clear the top and bottom 16 bits (for local use as thread flags and counters) block.nNonce = (uint256) nonce; - block.nSolution = std::vector(sol.begin() + 3, sol.end()); + block.nSolution = StratumIsRandomX() ? sol + : std::vector(sol.begin() + 3, sol.end()); // std::shared_ptr pblock = std::make_shared(block); // res = ProcessNewBlock(Params(), pblock, true, NULL); @@ -1261,15 +1300,11 @@ UniValue stratum_mining_configure(StratumClient& client, const UniValue& params) UniValue stratum_mining_submit(StratumClient& client, const UniValue& params) { - // ============================ WARNING (Equihash-era code) ============================ - // This share-submission handler is hardcoded for legacy Equihash: it parses and requires - // a 1347-byte Equihash solution and hands it to SubmitBlock() (which calls - // CheckEquihashSolution). DragonX uses RandomX PoW (32-byte solution), NOT Equihash. - // This path has NOT been updated for RandomX and must not be relied on without full - // revalidation of the miner-facing stratum protocol. - // ==================================================================================== + // Share submission. On RandomX (DragonX) the SOLUTION param is the 32-byte RandomX hash; on + // Equihash (legacy) it is the 1347-byte solution. The size is validated below and the branch is + // handled in SubmitBlock(). NONCE_2 is the miner-chosen tail of the 32-byte block nNonce. // - // {"id": 4, "method": "mining.submit", "params": ["WORKER_NAME", "JOB_ID", "TIME", "NONCE_2", "EQUIHASH_SOLUTION"]}\n + // {"id": 4, "method": "mining.submit", "params": ["WORKER_NAME", "JOB_ID", "TIME", "NONCE_2", "SOLUTION"]}\n // NONCE_1 is first part of the block header nonce (in hex). // By protocol, Zcash's nonce is 32 bytes long. The miner will pick NONCE_2 such that len(NONCE_2) = 32 - len(NONCE_1). Please note that Stratum use hex encoding, so you have to convert NONCE_1 from hex to binary before. @@ -1322,8 +1357,9 @@ UniValue stratum_mining_submit(StratumClient& client, const UniValue& params) uint32_t nTime = bswap_32(ParseHexInt4(params[2], "nTime")); std::vector sol = ParseHexV(params[4], "solution"); - if (sol.size() != 1347) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("solution is wrong length (received %d bytes; expected %d bytes", sol.size(), 1347)); + const size_t expected_sol_size = StratumIsRandomX() ? RX_STRATUM_SOLUTION_SIZE : 1347; + if (sol.size() != expected_sol_size) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("solution is wrong length (received %d bytes; expected %d bytes)", sol.size(), (int)expected_sol_size)); } std::vector extranonce1 = client.ExtraNonce1(job_id); @@ -1790,6 +1826,16 @@ bool InitStratumServer() int defaultPort = GetArg("-stratumport", stratumPort); LogPrintf("%s: Starting built-in stratum server on port %d\n",__func__, defaultPort ); + // Optional pool share-target override (64-hex, big-endian like getblocktemplate's "target"). + // Loosens/tightens the accepted share difficulty; also lets a solo/test miner accept easy shares + // on a low-difficulty chain (default is the diff-1 target 00ffff00..). Larger value = easier. + if (mapArgs.count("-stratumtarget")) { + const std::string t = GetArg("-stratumtarget", ""); + if (!t.empty()) { + instance_of_cstratumparams.setTarget(arith_uint256(t)); + LogPrintf("%s: stratum pool share target overridden to %s\n", __func__, t); + } + } if (!InitStratumAllowList(stratum_allow_subnets)) { LogPrint("stratum", "Unable to bind stratum server to an endpoint.\n");