Port miner113's RX_DRAGONX mining model into the RX_HUSH path so DragonX mining is identical in solo and pool mode: - CpuWorker: filter EVERY hash on SHA256D(header + RandomX solution) (the block-bearing pow-hash) instead of the RandomX hash; submit the full 32-byte nonce + rx_hash. Removes the fragile pool-mode dual-check that was dropping ~half of block candidates. - Job: 32-byte nonce for RX_HUSH in pool mode too (was solo-only). - JobResult: populate nonceBytes() on the standard 4-byte path. - Client: submit a variable-width nonce (32-byte for DragonX) with a dynamically laid-out temp buffer. Effect: shares and blocks use one metric, so the pool receives every block candidate (no under-submission gap) and the hashrate is block-relevant. Rebrand to drg-xmrig (version.h, build.sh, package.json, README) + add PROTOCOL.md wire spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
RX_DRAGONX Wire Protocol & Nonce Handling Spec (miner113 xmrig fork)
Source tree analyzed: /home/dev/xmrig (C++ xmrig fork, "miner113"/"Duke Leto" DragonX additions).
Scope: ONLY the RX_DRAGONX (DragonX / Hush Smart Chain) algorithm path.
All line references are to files under /home/dev/xmrig/.
TL;DR for the pool implementer:
- Stratum
algostring is exactlyrx/dragonx.- DragonX header (pre-nonce) is 108 bytes; the full hashed blob is 140 bytes (108 header + 32-byte nonce). Pool may send the blob either as a 140-byte hex
blob(standardjobnotify) or as a Zcash-stylemining.notifyparams array.- Nonce is a 32-byte field at offset 108 (
nonceOffset()=108,nonceSize()=32).- In pool/stratum mode only the uint32 at bytes [108:112] of the nonce varies, starting from 0 with no randomization, driven by the global atomic counter. Bytes [112:140] stay exactly as the pool sent them in the blob (normally all zero). There is no pool-assigned extra_nonce applied to the DragonX nonce. => Two miners on the same job WILL scan identical nonce values unless the pool differentiates the blob per-connection. See §3 "BOTTOM LINE".
- Submit JSON:
{id, job_id, nonce(64 hex = 32 bytes), result(64 hex = 32-byte RandomX hash), algo:"rx/dragonx"}.- Share filter =
double_sha256(140-byte header || 0x20 || rx_hash); submit if the last 8 bytes (little-endian uint64 at offset 24)< job.target(). Same metric for shares and blocks (no separate block-target check).
1. ALGORITHM IDENTITY
Enum / id
Algorithm::RX_DRAGONX = 0x72151264—src/base/crypto/Algorithm.h:83.- Family is computed as
id & 0xff000000for non-CN algos:0x72151264 & 0xff000000 = 0x72000000 = RANDOM_X(src/base/crypto/Algorithm.h:98RANDOM_X = 0x72000000,:180family()). SoRX_DRAGONX.family() == Algorithm::RANDOM_X. This matters: all thefamily()==RANDOM_Xbranches in the miner apply to DragonX. l2()/l3()derive from the id bytes like other RandomX algos (Algorithm.h:178); DragonX uses the standard RandomX scratchpad size (2 MiB) via its config (see below).
Stratum algo string (the exact "algo" JSON value)
- Canonical name string:
"rx/dragonx"—src/base/crypto/Algorithm.cpp:87(kRX_DRAGONX = "rx/dragonx"), registered inkAlgorithmNamesat:154viaALGO_NAME(RX_DRAGONX).Algorithm::name()returns this string, and it is what gets put into the submitalgofield (§4). - Accepted aliases (case-insensitive,
kAlgorithmAliases,src/base/crypto/Algorithm.cpp:270-272):"rx/dragonx"(auto, from name)"randomx/dragonx""randomdragonx""dragonx"
- The pool may name the algo via the pool config
"algo": "rx/dragonx"and/or"coin": "dragonx"(seeconfig-dragonx.jsonlines 8-9). When ajobcarries an"algo"string it is parsed byjob.setAlgorithm(algo)(Client.cpp:382); otherwise the coin's algorithm is inferred from the blob version byte (Client.cpp:384-390). Note:RX_DRAGONXis not present inCoin.cpp's coin table, so coin-based inference does not map to DragonX; the"algo"string (or poolalgorithm()default) is the reliable identity. The Zcash-stylemining.notifypath keys offm_pool.algorithm() == RX_DRAGONX(Client.cpp:815).
RandomX parameters / seed handling (customized)
- Config selected in
RxAlgo::base():case Algorithm::RX_DRAGONX: return &RandomX_DragonXConfig;—src/crypto/rx/RxAlgo.cpp:52-53. - DragonX RandomX configuration —
src/crypto/randomx/randomx.cpp:94-101:(All other parameters inheritArgonIterations = 5 ArgonSalt = "RandomXHUSH\x03" ProgramSize = 512 ProgramIterations = 4096 ProgramCount = 16RandomX_ConfigurationBase:ArgonLanes=1,SuperscalarLatency=170, etc. —randomx.cpp:123+.) - RandomX program version:
RxAlgo::version()returns104for everything except RX_WOW (RxAlgo.cpp:63-66), so DragonX = 104. - Seed: standard RandomX
seed_hash(32 bytes). Delivered over stratum in theseed_hashfield and stored viaJob::setSeedHash()which requires exactly 64 hex chars (Job.cpp:193-206). In solo mode the seed comes from the daemon'srandomxseedhashgetblocktemplate field (JunoRpcClient.cpp:539). No DragonX-specific seed transformation — the seed bytes are used directly as the RandomX cache key.
2. JOB FORMAT (pool -> miner)
There are two delivery formats handled by Client.cpp:
2a. Standard JSON job notify (method "job" / login result) — Client.cpp:365 parseJob
JSON fields consumed (object params):
| field | required | code | how consumed |
|---|---|---|---|
job_id |
yes | 3 | job.setId() (Client.cpp:374) |
algo |
optional | — | job.setAlgorithm(algo) (Client.cpp:379-382); should be "rx/dragonx" |
blob |
yes | 4 | job.setBlob(blobData) (Client.cpp:405) — full 140-byte hex blob |
target |
yes | 5 | job.setTarget() (Client.cpp:411) |
height |
optional | — | job.setHeight(Json::getUint64(...)) (Client.cpp:416) |
seed_hash |
yes (RANDOM_X) | 7 | job.setSeedHash() (Client.cpp:423) — DragonX is RANDOM_X family, so required |
sig_key |
optional | — | job.setSigKey() (Client.cpp:428) — 128 hex (64-byte) miner-signature key; normally absent for DragonX |
extra_nonce, pool_wallet |
only MODE_SELF_SELECT | 4 | Client.cpp:393-401; not used in normal DragonX pool mode |
In this format the pool supplies the entire blob (header + nonce placeholder) as hex.
2b. Zcash-style mining.notify (DragonX-specific) — Client.cpp:814-873
Only taken when method == "mining.notify" AND m_pool.algorithm() == RX_DRAGONX. params is an array with >= 9 elements (Client.cpp:816):
[ job_id, version, prevhash, merkleroot, blockcommitments, time, bits, clean_jobs, seed_hash ]
[0] [1] [2] [3] [4] [5] [6] [7] [8]
version,time,bitsare hex strings parsed withstrtoul(...,16)(Client.cpp:838for time; version/bits parsed insidesetZcashJob).clean_jobsis a bool, currently ignored (Client.cpp:836(void)cleanJobs).- Job built via
job.setZcashJob(version, prevHash, merkleRoot, blockCommitments, time, bits)(Client.cpp:846). seed_hashviajob.setSeedHash()(Client.cpp:852).- Target: this path sets a placeholder target of all-
fs (Client.cpp:860) or default diff 1 (:861). Real target is expected frommining.set_difficulty. NOTE: there is currently NO handler formining.set_difficultyinClient.cpp(grep shows only the comment) — i.e. in pure Zcash-notify mode the effective target stays at the placeholder unless the pool also sends a standardjob. The reliable, fully-wired path is 2a (standardjobwith a realtarget).
The blob: size and layout
Pool sends 140 bytes (108-byte header + 32-byte nonce placeholder). Evidence:
setBlobminimum-size check:minSize = nonceOffset() + nonceSize()=108 + 32 = 140(Job.cpp:74), and it rejectssize < minSize(Job.cpp:75). So a blob shorter than 140 bytes (e.g. a 108-byte header-only blob) is rejected in path 2a.setZcashJobbuilds the 108-byte header then setsm_size = 140reserving the 32-byte nonce slot (Job.cpp:180).setJunoHeader(solo) copies 108 bytes and setsm_size = 140(Job.cpp:185-191).
So: the on-wire blob for path 2a should be 140 bytes = 280 hex chars, with bytes [108:140] (the nonce) normally zero.
setBlob details (Job.cpp:61-94):
- hex string length must be even (
:68),size = len/2. minSize = 140, must satisfy140 <= size < sizeof(m_blob)=408(:74-77,kMaxBlobSize=408Job.h:50).Cvt::fromHexintom_blob(:79).- If the existing nonce dword (
readUnaligned(nonce()), i.e. uint32 at offset 108) is non-zero,m_nicehashis force-enabled (:83-85). For DragonX the placeholder is zero so this stays false. - stores
m_size = size(:92).
Header layout (the 108-byte DragonX header) — setZcashJob Job.cpp:96-183
| offset | size | field | byte order in blob |
|---|---|---|---|
| 0 | 4 | version (nVersion) | little-endian (raw strtoul value memcpy'd) Job.cpp:115-117 |
| 4 | 32 | prevHash | byte-reversed display->internal Job.cpp:119-133 |
| 36 | 32 | merkleRoot | byte-reversed display->internal Job.cpp:135-149 |
| 68 | 32 | blockCommitments | byte-reversed display->internal Job.cpp:151-165 |
| 100 | 4 | time (nTime) | little-endian Job.cpp:167-169 |
| 104 | 4 | bits (nBits compact) | little-endian Job.cpp:171-174 |
| 108 | 32 | nonce | (mining field; see §3) |
Byte-order rule: the three 32-byte hashes arrive from getblocktemplate / pool in display (big-endian) order and are reversed to internal (little-endian) order for hashing (each m_blob[pos+i] = temp[31-i]). version/time/bits are stored little-endian directly. The solo JunoRpcClient builds the identical 108-byte layout manually (JunoRpcClient.cpp:548-583) — note it copies the already-internal-order hashes (m_headerPrevHash etc.) straight in without re-reversing, because those buffers were stored in internal order when parsed.
setJunoHeader(const uint8_t* header108) (Job.cpp:185-191): bypasses hex/reverse logic, memcpy's a ready-made 108-byte header, sets m_size=140. Used only by solo JunoRpcClient (JunoRpcClient.cpp:587).
There is no function named
setDragonxHeader. The DragonX header is built either bysetZcashJob(stratum Zcash-notify) orsetJunoHeader(solo), or delivered pre-built as the 140-byteblob(standardjob).
Target parsing & diff conversion — Job::setTarget Job.cpp:209-250
- For DragonX (not RX_YADA),
targethex is decoded byCvt::fromHexand interpreted by length:- 4 bytes:
m_target = 0xFFFFFFFFFFFFFFFF / (0xFFFFFFFF / u32)(Job.cpp:219-220) — "compact" 32-bit difficulty-style target. - 8 bytes:
m_target = u64read directly (little-endian) (Job.cpp:222-223). - other lengths => 0 => rejected.
- 4 bytes:
- After parsing,
m_diff = toDiff(m_target). toDiff(target) = target ? (0xFFFFFFFFFFFFFFFF / target) : 0—Job.h:121.setTarget64(u64)is the direct setter used by solo:m_target = u64; m_diff = toDiff(u64)(Job.h:107). Solo computestarget64from the compactbitsfield (JunoRpcClient.cpp:604-635), extracting the 64-bit window at byte offset 24 of the 256-bit target.- The mining comparison is always
pow_value(uint64) < m_target(see §5). Largerm_target= easier.
3. NONCE HANDLING (most important)
nonceOffset / nonceSize
nonceOffset()for RX_DRAGONX = 108 —Job.cpp:271-273.nonceSize()for RX_DRAGONX = 32 —Job.h:84-86(if (algorithm()==RX_DRAGONX) return 32;).- The DragonX nonce field therefore occupies blob bytes [108:140] (32 bytes).
Two distinct nonce regimes
DragonX uses a different nonce strategy depending on mining mode (Job::isSoloMining() / WorkerJob::isSoloMining()):
(A) POOL / STRATUM mode (isSoloMining()==false) — the standard incrementing 32-bit counter
WorkerJob::nonce()for DragonX returns auint32_t*pointing at blob offset 108 (WorkerJob.h:46, and theN==1specialization:150-153). It only ever treats the nonce as a uint32.- Starting value & seeding: the global atomic counter
Nonce::m_nonces[index]starts at 0 (Nonce.cpp:27m_nonces[2] = {0,0}) and is reset to 0 on every job change (Miner.cpp:124Nonce::reset(job.index()), which setsm_nonces[index]=0,Nonce.h:44). There is no randomization of the starting nonce in pool mode. Nonce::next()(Nonce.cpp:33-65):fetch_add(reserveCount)on the shared counter, then writes(*nonce & ~mask) | counterinto the uint32 at offset 108 (:57). Thenonce+1(offset 112) write at:59-61only happens whenmask > 0xFFFFFFFF. For DragonXnonceMask()=0xFFFFFFFF(becausenonceSize()=32 != sizeof(uint64_t)=8and not nicehash → the0xFFFFFFFFULLbranch ofJob.h:93), so only bytes [108:112] are ever written; bytes [112:139] are untouched by the nonce machinery.- Per-round increment:
WorkerJob::nextRoundaddsroundSize(=1) to the uint32 at 108 each hash, and everykReserveCount(=32768) hashes grabs a fresh range viaNonce::next(WorkerJob.h:69-87, specialization:156-176;CpuWorker::nextRoundCpuWorker.cpp:480-500,kReserveCountCpuWorker.cpp:82). - WHICH bytes vary: only the 4 bytes [108:112] (a little-endian uint32) increment. Bytes [112:139] stay exactly as they were in the blob the pool sent (all zero for a normal placeholder).
- The full 32-byte nonce that gets hashed and submitted is read from the blob at offset 108 each round and saved before increment:
CpuWorker.cpp:342-344(memcpy(current_solo_nonces + i*32, m_job.blob() + nonceOffset() + i*job.size(), 32)). So the submitted nonce =[ varying uint32 LE | 28 bytes copied verbatim from the blob (normally zero) ].
(B) SOLO mode (isSoloMining()==true) — random 256-bit nonce per thread
- Solo nonces are managed by
SoloNonceandWorkerJob's solo path, NOT by the globalNoncecounter. WorkerJob::initSoloNonces()(WorkerJob.h:99-105) callsSoloNonce::initialize()per thread then copies the 32 bytes into the blob at offset 108.SoloNonce::initialize(SoloNonce.cpp:39-61): fills all 32 bytes with cryptographically secure random (/dev/urandom, orBCryptGenRandomon Windows; fallbackmt19937_64), then zeros bytes [0:2] (low 16 bits, increment space) and zeros bytes [30:32] (top 16 bits, safety margin). Net: ~224 random bits per thread.WorkerJob::nextRoundSolo(WorkerJob.h:107-114) does a full little-endian 256-bitSoloNonce::increment(SoloNonce.cpp:64-72) each round and re-copies to the blob — never exhausts.- So in solo every thread/instance gets a distinct random high-entropy nonce, and the whole 32-byte field is meaningful and submitted (
result.soloNonce()).
extra_nonce application
- DragonX standard stratum path does NOT apply any
extra_nonceto the nonce field.Job::setExtraNonce/extraNonce()(Job.h:103,:79) are only set inMODE_SELF_SELECT(Client.cpp:394) and consumed only bySelfSelectClient(SelfSelectClient.cpp:172) — a different code path that does not run for DragonX pool mining. The DragonX worker code (CpuWorker.cpp) never readsextraNonce(). nonceMask()does referenceextraNonce().size()but only for thenonceSize()==sizeof(uint64_t)(KawPow) case (Job.h:93); for DragonX (nonceSize()==32) the mask is the constant0xFFFFFFFF, independent of extra_nonce.
BOTTOM LINE (miner-to-miner / thread-to-thread overlap)
- Across threads of one miner instance: no overlap. The shared atomic
Nonce::m_nonces[index]hands out disjointreserveCount(32768)-sized ranges to each thread viaNonce::next(Nonce.cpp:40). Threads scan different uint32 ranges of [108:112]. - Across two separate miners (or one miner reconnecting) on the SAME job from a pool, in stratum/pool mode: they WILL scan the exact same nonce values. Both start their counter at 0 (reset on job change,
Miner.cpp:124; initialm_nonces={0,0}), there is no per-connection randomization, and bytes [112:139] are identical (the zero placeholder from the blob). Nothing in the miner prevents the overlap.- => To avoid duplicate-share work, the pool MUST differentiate the job per connection, e.g. by writing a unique value into the nonce placeholder bytes [112:139] of the per-miner
blob(those bytes are preserved verbatim by the miner and included in the hash + submitted nonce), and/or into other header fields. There is noextra_nonceJSON mechanism wired for DragonX, so per-miner uniqueness has to be baked into theblob(path 2a) the pool sends.
- => To avoid duplicate-share work, the pool MUST differentiate the job per connection, e.g. by writing a unique value into the nonce placeholder bytes [112:139] of the per-miner
- Solo mode is the only mode with built-in cross-instance collision avoidance, via the 224-bit random
SoloNonce(§3B). This randomization does not apply to pool mode.
4. SUBMIT FORMAT (miner -> pool)
Stratum submit (Client::submit, Client.cpp:181-251)
JSON-RPC "submit" with params object:
| field | value | reference |
|---|---|---|
id |
m_rpcId (login id string) |
Client.cpp:223 |
job_id |
result.jobId |
Client.cpp:224 |
nonce |
hex of result.nonceBytes(), length nonceSize() bytes |
Client.cpp:211,:225 |
result |
hex of the 32-byte hash result.result() |
Client.cpp:212,:226 |
sig |
hex of 64-byte miner signature, only if present | Client.cpp:214-216,:229-231 (absent for normal DragonX) |
algo |
result.algorithm.name() = "rx/dragonx", only if EXT_ALGO negotiated and algo valid |
Client.cpp:238-240 |
nonce field for DragonX = 32 bytes => 64 hex chars. Confirmed.
nonceHexSize = result.nonceSize()*2 + 1(Client.cpp:203); for DragonXnonceSize()==32→ 64 hex chars (+NUL).Cvt::toHex(nonce, nonceHexSize, result.nonceBytes(), result.nonceSize())writes all 32 bytes (Client.cpp:211).- Contents = the full 32-byte nonce field from the blob as hashed: in pool mode
[ uint32 LE counter | 28 zero bytes ]; in solo[ random 256-bit nonce ]. (result.nonceBytes()/m_nonceBytes, populated per constructor — see below.) m_tempBufwas enlarged to 512 bytes specifically to hold the 64-char nonce + result + sig (Client.cpp:86, comment "Increased for RX_DRAGONX 32-byte nonce support").
result field = the 32-byte RandomX hash (PoW solution) => 64 hex chars. Confirmed.
Cvt::toHex(data, 65, result.result(), 32)(Client.cpp:212).result.result()=m_result[32], the RandomX hashm_hashfor that nonce (passed into the JobResult ctor inCpuWorker.cpp:449). It is not the double-SHA256 pow_hash; it is the raw RandomX output (so the pool can re-verifyrandomx(blob,seed)==resultand recomputedouble_sha256itself).
JobResult constructors used — src/net/JobResult.h
- DragonX uses the 32-byte-nonce / solo-style constructor:
JobResult(const Job&, const uint8_t* nonce32, const uint8_t* result)(JobResult.h:94-107). It copiesm_result=result,m_nonceBytes=nonce32(full 32 bytes), setsm_nonceSize=32, andm_isSoloResult=true.- This is what
CpuWorkercalls for DragonX:JobResults::submit(JobResult(job, current_solo_nonces + i*32, m_hash + i*32))(CpuWorker.cpp:449). Same call shape is used for solo (CpuWorker.cpp:457).
- This is what
- The uint64-nonce constructor
JobResult(job, uint64 nonce, result, ...)(JobResult.h:46-80) is the standard XMRig path for non-DragonX algos (CpuWorker.cpp:459). NOTE: that constructor does contain a DragonX special-case (JobResult.h:70-79: for RX_DRAGONX setm_nonceSize=32, zero 32 bytes, write only first 8 from the uint64), but the DragonX worker branch never uses it — it always uses the 32-byte ctor. So the effective DragonX submit nonce always carries the true 32-byte field from the blob (low 4 bytes vary, rest as-sent), not a truncated 8-byte value.
Solo submit (JunoRpcClient)
Solo does NOT use the stratum submit JSON. JunoRpcClient::submit → submitBlock (JunoRpcClient.cpp:141-149, :358-443) assembles a full raw block hex and calls daemon submitblock (JSON-RPC 1.0). The block = 108-byte header + result.soloNonce() (32 bytes, :394-395) + 0x20 solution-length varint + result.result() (32-byte rx hash, :399-400) + tx count + coinbase + txs. The nonce/result semantics match the stratum submit (32-byte nonce, 32-byte rx hash).
5. POW HASH / SHARE FILTER (src/backend/cpu/CpuWorker.cpp)
dragonx_pow_hash — CpuWorker.cpp:106-117
static inline void dragonx_pow_hash(const uint8_t* blob, const uint8_t* rx_hash, uint8_t* out) {
uint8_t full_header[173];
memcpy(full_header, blob, 140); // header(108) + nonce(32)
full_header[140] = 0x20; // compact_size = 32 (solution length)
memcpy(full_header + 141, rx_hash, 32); // RandomX hash = PoW solution
uint8_t tmp[32];
SHA256(full_header, 173, tmp);
SHA256(tmp, 32, out); // double SHA256
}
Exact 173-byte layout:
| offset | size | content |
|---|---|---|
| 0 | 108 | header base (version, prevHash, merkleRoot, blockCommitments, time, bits) |
| 108 | 32 | 32-byte nonce (this round's nonce) |
| 140 | 1 | 0x20 (CompactSize = 32, the solution length) |
| 141 | 32 | RandomX hash result (m_hash for this nonce) |
Output = SHA256(SHA256(full_header[173])). SHA256 is OpenSSL when TLS is built, else sph_sha256 (Linux) / BCrypt (Windows) (CpuWorker.cpp:41-67). Confirmed byte layout matches the daemon's CBlockHeader::GetHash().
The blob_for_header[140] fed to dragonx_pow_hash is reconstructed as m_job.blob()[0:108] (header, unchanged by nextRound) + current_solo_nonces[i][0:32] (the saved 32-byte nonce before increment) — CpuWorker.cpp:435-441. This guarantees the hashed nonce == the submitted nonce.
Submit condition for RX_DRAGONX — CpuWorker.cpp:425-450
alignas(8) uint8_t pow_hash[32];
dragonx_pow_hash(blob_for_header, m_hash + i*32, pow_hash);
const uint64_t pow_value = *reinterpret_cast<uint64_t*>(pow_hash + 24); // last 8 bytes, LE
if (pow_value < job.target()) {
JobResults::submit(JobResult(job, current_solo_nonces + i*32, m_hash + i*32));
}
pow_value= little-endian uint64 read frompow_hash[24..31](the last 8 bytes) —CpuWorker.cpp:444.- Submit iff
pow_value < job.target()—CpuWorker.cpp:445. - This is uniform: there is exactly one comparison, identical in solo and pool mode. No
m_job.isSoloMining()branch inside the DragonX block (contrast the standard path atCpuWorker.cpp:454-460, which does branch solo vs pool only to pick the JobResult constructor — DragonX skips that and always uses the 32-byte ctor).
No separate block_target check. Confirmed.
- The only target compared anywhere on the DragonX path is
job.target()againstpow_value. There is no second comparison against a distinct network/block target inCpuWorker.cpp(grep: the DragonX block has a single< job.target()test at:445). Shares and blocks use the same double-SHA256 metric; whether a submitted share is also a block is determined by the pool/daemon comparing the samedouble_sha256against the real network target (the comments atCpuWorker.cpp:446-448state the pool does the block-vs-share distinction). The miner submits every share whosedouble_sha256beats the pool-setjob.target().
6. SOLO vs POOL behavior
| aspect | POOL / stratum (Client) |
SOLO (JunoRpcClient / daemon) |
|---|---|---|
| job source | parseJob (140-byte blob) or Zcash mining.notify (setZcashJob) |
getblocktemplate → setJunoHeader (JunoRpcClient.cpp:587) |
isSoloMining() |
false | true (JunoRpcClient.cpp:640) |
| nonce field | uint32 counter at [108:112], rest = blob bytes (zero); starts 0, no randomization | random 224-bit SoloNonce, per-thread, 256-bit increment |
| nonce bytes scanned | only [108:112] vary | full 32 bytes meaningful |
| RandomX hashing | randomx_calculate_hash_first/next, family()==RANDOM_X branch (CpuWorker.cpp:366-384) — identical |
same |
| pow hash + filter | dragonx_pow_hash, pow_value < job.target() (CpuWorker.cpp:425-450) — identical |
identical (same code, no branch) |
| JobResult ctor | 32-byte ctor JobResult(job, nonce32, result) (CpuWorker.cpp:449) |
same 32-byte ctor (CpuWorker.cpp:449) |
| target derivation | from pool target hex (Job::setTarget) |
from compact bits → setTarget64 (JunoRpcClient.cpp:604-635) |
| submit transport | JSON-RPC submit {id,job_id,nonce,result,algo} (Client::submit) |
daemon submitblock with full serialized block (JunoRpcClient::submitBlock) |
Key point: the CpuWorker hashing and the share/PoW filter are byte-for-byte identical in solo and pool for DragonX (same randomx_calculate_hash_*, same dragonx_pow_hash, same pow_value < job.target(), same 32-byte JobResult). The differences are entirely in (a) nonce seeding (random 256-bit solo vs zero-start uint32 pool) and (b) submission transport (raw submitblock vs stratum submit). The m_isSoloMining flag only switches nonce management (CpuWorker.cpp:337-344, nextRound→nextRoundSolo :483-484) and submit transport — not the hash math or the difficulty test.
Quick reference — exact JSON shapes
Pool -> miner (standard job)
{
"jsonrpc": "2.0",
"method": "job",
"params": {
"job_id": "<id>",
"algo": "rx/dragonx",
"blob": "<280 hex chars = 140 bytes: 108-byte header + 32-byte nonce placeholder>",
"target": "<8 or 16 hex chars (4 or 8 LE bytes)>",
"height": <int>,
"seed_hash":"<64 hex chars = 32-byte RandomX seed>"
}
}
(For per-miner nonce-space separation, set distinct values in blob bytes [112:140], i.e. hex chars 224..280.)
Miner -> pool (submit)
{
"id": <seq>,
"jsonrpc": "2.0",
"method": "submit",
"params": {
"id": "<rpcId from login>",
"job_id": "<id>",
"nonce": "<64 hex chars = full 32-byte nonce field>",
"result": "<64 hex chars = 32-byte RandomX hash>",
"algo": "rx/dragonx"
}
}
Pool verification (per the miner's contract): recompute rx = randomx(seed_hash, header[0:108] || nonce) and require rx == result; then compute double_sha256(header[0:108] || nonce || 0x20 || rx) and compare its last 8 bytes (LE uint64 at offset 24) against the share target (and against the network target for block detection).