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:
@@ -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())
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
104
src/stratum.cpp
104
src/stratum.cpp
@@ -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()) {
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid equihash solution");
|
// 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;
|
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");
|
||||||
|
|||||||
Reference in New Issue
Block a user