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=<hex> 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) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 20:24:13 -05:00
parent 1745ee4e63
commit 2232868d9f
4 changed files with 330 additions and 33 deletions

View File

@@ -621,6 +621,7 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += HelpMessageGroup(_("Stratum server options:")); strUsage += HelpMessageGroup(_("Stratum server options:"));
strUsage += HelpMessageOpt("-stratum", _("Enable stratum server (default: off)")); strUsage += HelpMessageOpt("-stratum", _("Enable stratum server (default: off)"));
strUsage += HelpMessageOpt("-stratumtarget=<hex>", _("Pool share target (64-hex, big-endian; larger = easier). Default is the diff-1 target. Useful for solo/low-difficulty mining."));
strUsage += HelpMessageOpt("-stratumaddress=<address>", _("Mining address to use when special address of 'x' is sent by miner (default: none)")); strUsage += HelpMessageOpt("-stratumaddress=<address>", _("Mining address to use when special address of 'x' is sent by miner (default: none)"));
strUsage += HelpMessageOpt("-stratumbind=<ipaddr>", _("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("-stratumbind=<ipaddr>", _("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=<port>", strprintf(_("Listen for Stratum work requests on <port> (default: %u or testnet: %u)"), BaseParams().StratumPort(), BaseParams().StratumPort())); strUsage += HelpMessageOpt("-stratumport=<port>", strprintf(_("Listen for Stratum work requests on <port> (default: %u or testnet: %u)"), BaseParams().StratumPort(), BaseParams().StratumPort()));
@@ -991,10 +992,10 @@ bool AppInitServers(boost::thread_group& threadGroup)
RPCServer::OnPreCommand(&OnRPCPreCommand); RPCServer::OnPreCommand(&OnRPCPreCommand);
if (!InitHTTPServer()) if (!InitHTTPServer())
return false; return false;
// WARNING: the stratum server (stratum.cpp) is Equihash-era code: it assumes a 1347-byte // Stratum server (stratum.cpp) supports DragonX's RandomX PoW (32-byte solution + per-height
// Equihash solution and calls CheckEquihashSolution. It has NOT been updated for DragonX's // RandomX key conveyed to the miner) as well as legacy Equihash, branched on ASSETCHAINS_ALGO.
// 32-byte RandomX solution and must not be relied on for mining without a full revalidation. // Off by default (DEFAULT_STRATUM_ENABLE=false); only -stratum turns it on. Needs a RandomX-aware
// It stays off by default (DEFAULT_STRATUM_ENABLE=false); only -stratum turns it on. // stratum miner (see contrib/ reference miner) — stock Equihash/Monero miners won't work.
if (GetBoolArg("-stratum", DEFAULT_STRATUM_ENABLE) && !InitStratumServer()) if (GetBoolArg("-stratum", DEFAULT_STRATUM_ENABLE) && !InitStratumServer())
return false; return false;
if (!StartRPC()) if (!StartRPC())

View File

@@ -42,6 +42,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getaddednodeinfo", 0 }, { "getaddednodeinfo", 0 },
{ "setgenerate", 0 }, { "setgenerate", 0 },
{ "setgenerate", 1 }, { "setgenerate", 1 },
{ "stratummine", 1 }, // port
{ "stratummine", 3 }, // timeout
{ "generate", 0 }, { "generate", 0 },
{ "getnetworkhashps", 0 }, { "getnetworkhashps", 0 },
{ "getnetworkhashps", 1 }, { "getnetworkhashps", 1 },

View File

@@ -45,6 +45,21 @@
#include <univalue.h> #include <univalue.h>
#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 <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#endif
using namespace std; using namespace std;
#include "hush_defs.h" #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<std::string>& 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<unsigned char> 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<unsigned char> 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<std::string> 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<std::string> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<std::string> 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[] = static const CRPCCommand commands[] =
{ // category name actor (function) okSafeMode { // category name actor (function) okSafeMode
// --------------------- ------------------------ ----------------------- ---------- // --------------------- ------------------------ ----------------------- ----------
#ifndef WIN32
{ "mining", "stratummine", &stratummine, true },
#endif
{ "mining", "getlocalsolps", &getlocalsolps, true }, { "mining", "getlocalsolps", &getlocalsolps, true },
{ "mining", "getnetworksolps", &getnetworksolps, true }, { "mining", "getnetworksolps", &getnetworksolps, true },
{ "mining", "getnetworkhashps", &getnetworkhashps, true }, { "mining", "getnetworkhashps", &getnetworkhashps, true },

View File

@@ -16,6 +16,7 @@
#include "httpserver.h" #include "httpserver.h"
#include "miner.h" #include "miner.h"
#include "netbase.h" #include "netbase.h"
#include "pow.h" // RandomX PoW: CheckRandomXSolution / GetRandomXKey / GetRandomXInput; and CheckEquihashSolution
#include "net.h" #include "net.h"
#include "rpc/server.h" #include "rpc/server.h"
#include "serialize.h" #include "serialize.h"
@@ -663,6 +664,15 @@ void CustomizeWork(const StratumClient& client, const StratumWork& current_work,
// cb_branch = current_work.m_cb_branch; // 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) std::string GetWorkUnit(StratumClient& client)
{ {
// LOCK(cs_main); // LOCK(cs_main);
@@ -688,7 +698,7 @@ std::string GetWorkUnit(StratumClient& client)
throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "DragonX is not connected!"); 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__); const std::string msg = strprintf("%s: Unable to get work unit, DragonX is still downloading blocks!", __func__);
LogPrint("stratum", "%s\n", msg); LogPrint("stratum", "%s\n", msg);
throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "DragonX is downloading blocks..."); 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(); 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, new_work->block.vtx[0]->HasWitness());
work_templates[job_id] = StratumWork(*new_work, false); 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; 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("method", "mining.notify"));
mining_notify.push_back(Pair("params", params)); 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) return GetExtraNonceRequest(client, job_id)
+ randomx_key_msg
+ set_target.write() + "\n" + set_target.write() + "\n"
+ mining_notify.write() + "\n"; + mining_notify.write() + "\n";
} }
@@ -924,17 +955,15 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork
const std::vector<unsigned char>& extranonce1, const std::vector<unsigned char>& extranonce2, const std::vector<unsigned char>& extranonce1, const std::vector<unsigned char>& extranonce2,
boost::optional<uint32_t> nVersion, uint32_t nTime, const std::vector<unsigned char>& sol) boost::optional<uint32_t> nVersion, uint32_t nTime, const std::vector<unsigned char>& sol)
{ {
// ============================ WARNING (Equihash-era code) ============================ // Submit path handles BOTH proof-of-works, branched on StratumIsRandomX():
// This entire submit path is hardcoded for the legacy Equihash proof-of-work: // * RandomX (DragonX): `sol` is the 32-byte RandomX hash and IS nSolution verbatim; validated
// it expects a 1347-byte Equihash solution and calls CheckEquihashSolution() below. // via CheckRandomXSolution(&blkhdr, height). The target check (GetHash() < target) and the
// DragonX uses RandomX PoW (32-byte solution), NOT Equihash. This stratum path has // nNonce = extranonce1||extranonce2 assembly are identical to the equihash path.
// NOT been updated for RandomX and must not be relied on without full revalidation. // * Equihash (legacy): `sol` is the 1347-byte solution; the 3-byte zcash prefix is stripped
// The 1347-byte length checks, the "sol.begin()+3" solution offset, and the equihash // and CheckEquihashSolution() validates it.
// target/difficulty math are all Equihash-era and are intentionally left unchanged.
// ====================================================================================
// //
// called from stratum_mining_submit and uses following data, came from client: // 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 // all other params we have saved in other places
if (extranonce1.size() + extranonce2.size() != 32) { 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); throw JSONRPCError(RPC_INVALID_PARAMETER, msg);
} }
// WARNING (Equihash-era): 1347 is the Equihash-200,9 solution length. DragonX is RandomX const size_t expected_sol_size = StratumIsRandomX() ? RX_STRATUM_SOLUTION_SIZE : 1347;
// (32-byte solution), so this length check does not match the live PoW. Left unchanged if (sol.size() != expected_sol_size) {
// because this whole path is Equihash-era; do not repurpose without revalidating the miner protocol. std::string msg = strprintf("%s: solution is wrong length (received %d bytes; expected %d bytes)", __func__, sol.size(), (int)expected_sol_size);
if (sol.size() != 1347) {
std::string msg = strprintf("%s: solution is wrong length (received %d bytes; expected %d bytes", __func__, extranonce2.size(), 1347);
LogPrint("stratum", "%s\n", msg); LogPrint("stratum", "%s\n", msg);
throw JSONRPCError(RPC_INVALID_PARAMETER, msg); throw JSONRPCError(RPC_INVALID_PARAMETER, msg);
} }
@@ -979,7 +1006,10 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork
std::vector<unsigned char> nonce(extranonce1); std::vector<unsigned char> nonce(extranonce1);
nonce.insert(nonce.end(), extranonce2.begin(), extranonce2.end()); nonce.insert(nonce.end(), extranonce2.begin(), extranonce2.end());
blkhdr.nSolution = std::vector<unsigned char>(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<unsigned char>(sol.begin() + 3, sol.end());
blkhdr.hashFinalSaplingRoot = current_work.GetBlock().hashFinalSaplingRoot; blkhdr.hashFinalSaplingRoot = current_work.GetBlock().hashFinalSaplingRoot;
blkhdr.hashMerkleRoot = current_work.GetBlock().hashMerkleRoot; 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 // block is constructed, now it's time to VerifyEH
if (instance_of_cstratumparams.fCheckEquihashSolution && !CheckEquihashSolution(&blkhdr, Params())) 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"); throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid equihash solution");
}
arith_uint256 bnTarget; bool fNegative, fOverflow; arith_uint256 bnTarget; bool fNegative, fOverflow;
bnTarget.SetCompact(blkhdr.nBits, &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) // nNonce <<= 32; nNonce >>= 16; // clear the top and bottom 16 bits (for local use as thread flags and counters)
block.nNonce = (uint256) nonce; block.nNonce = (uint256) nonce;
block.nSolution = std::vector<unsigned char>(sol.begin() + 3, sol.end()); block.nSolution = StratumIsRandomX() ? sol
: std::vector<unsigned char>(sol.begin() + 3, sol.end());
// std::shared_ptr<const CBlock> pblock = std::make_shared<const CBlock>(block); // std::shared_ptr<const CBlock> pblock = std::make_shared<const CBlock>(block);
// res = ProcessNewBlock(Params(), pblock, true, NULL); // 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) UniValue stratum_mining_submit(StratumClient& client, const UniValue& params)
{ {
// ============================ WARNING (Equihash-era code) ============================ // Share submission. On RandomX (DragonX) the SOLUTION param is the 32-byte RandomX hash; on
// This share-submission handler is hardcoded for legacy Equihash: it parses and requires // Equihash (legacy) it is the 1347-byte solution. The size is validated below and the branch is
// a 1347-byte Equihash solution and hands it to SubmitBlock() (which calls // handled in SubmitBlock(). NONCE_2 is the miner-chosen tail of the 32-byte block nNonce.
// 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.
// ====================================================================================
// //
// {"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). // 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. // 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")); uint32_t nTime = bswap_32(ParseHexInt4(params[2], "nTime"));
std::vector<unsigned char> sol = ParseHexV(params[4], "solution"); std::vector<unsigned char> sol = ParseHexV(params[4], "solution");
if (sol.size() != 1347) { const size_t expected_sol_size = StratumIsRandomX() ? RX_STRATUM_SOLUTION_SIZE : 1347;
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("solution is wrong length (received %d bytes; expected %d bytes", sol.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<unsigned char> extranonce1 = client.ExtraNonce1(job_id); std::vector<unsigned char> extranonce1 = client.ExtraNonce1(job_id);
@@ -1790,6 +1826,16 @@ bool InitStratumServer()
int defaultPort = GetArg("-stratumport", stratumPort); int defaultPort = GetArg("-stratumport", stratumPort);
LogPrintf("%s: Starting built-in stratum server on port %d\n",__func__, defaultPort ); 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)) { if (!InitStratumAllowList(stratum_allow_subnets)) {
LogPrint("stratum", "Unable to bind stratum server to an endpoint.\n"); LogPrint("stratum", "Unable to bind stratum server to an endpoint.\n");