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:
+75
-29
@@ -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<unsigned char>& extranonce1, const std::vector<unsigned char>& extranonce2,
|
||||
boost::optional<uint32_t> nVersion, uint32_t nTime, const std::vector<unsigned char>& 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<unsigned char> nonce(extranonce1);
|
||||
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.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<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);
|
||||
// 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<unsigned char> 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<unsigned char> 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");
|
||||
|
||||
Reference in New Issue
Block a user