Files
drg-xmrig/PROTOCOL.md
DanS 84e7d1453c drg-xmrig: fork of xmrig-hac with unified pow-hash share model
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>
2026-06-06 16:49:22 -05:00

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 algo string is exactly rx/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 (standard job notify) or as a Zcash-style mining.notify params 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 = 0x72151264src/base/crypto/Algorithm.h:83.
  • Family is computed as id & 0xff000000 for non-CN algos: 0x72151264 & 0xff000000 = 0x72000000 = RANDOM_X (src/base/crypto/Algorithm.h:98 RANDOM_X = 0x72000000, :180 family()). So RX_DRAGONX.family() == Algorithm::RANDOM_X. This matters: all the family()==RANDOM_X branches 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 in kAlgorithmNames at :154 via ALGO_NAME(RX_DRAGONX). Algorithm::name() returns this string, and it is what gets put into the submit algo field (§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" (see config-dragonx.json lines 8-9). When a job carries an "algo" string it is parsed by job.setAlgorithm(algo) (Client.cpp:382); otherwise the coin's algorithm is inferred from the blob version byte (Client.cpp:384-390). Note: RX_DRAGONX is not present in Coin.cpp's coin table, so coin-based inference does not map to DragonX; the "algo" string (or pool algorithm() default) is the reliable identity. The Zcash-style mining.notify path keys off m_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:
    ArgonIterations   = 5
    ArgonSalt         = "RandomXHUSH\x03"
    ProgramSize       = 512
    ProgramIterations = 4096
    ProgramCount      = 16
    
    (All other parameters inherit RandomX_ConfigurationBase: ArgonLanes=1, SuperscalarLatency=170, etc. — randomx.cpp:123+.)
  • RandomX program version: RxAlgo::version() returns 104 for everything except RX_WOW (RxAlgo.cpp:63-66), so DragonX = 104.
  • Seed: standard RandomX seed_hash (32 bytes). Delivered over stratum in the seed_hash field and stored via Job::setSeedHash() which requires exactly 64 hex chars (Job.cpp:193-206). In solo mode the seed comes from the daemon's randomxseedhash getblocktemplate 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,bits are hex strings parsed with strtoul(...,16) (Client.cpp:838 for time; version/bits parsed inside setZcashJob).
  • clean_jobs is a bool, currently ignored (Client.cpp:836 (void)cleanJobs).
  • Job built via job.setZcashJob(version, prevHash, merkleRoot, blockCommitments, time, bits) (Client.cpp:846).
  • seed_hash via job.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 from mining.set_difficulty. NOTE: there is currently NO handler for mining.set_difficulty in Client.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 standard job. The reliable, fully-wired path is 2a (standard job with a real target).

The blob: size and layout

Pool sends 140 bytes (108-byte header + 32-byte nonce placeholder). Evidence:

  • setBlob minimum-size check: minSize = nonceOffset() + nonceSize() = 108 + 32 = 140 (Job.cpp:74), and it rejects size < minSize (Job.cpp:75). So a blob shorter than 140 bytes (e.g. a 108-byte header-only blob) is rejected in path 2a.
  • setZcashJob builds the 108-byte header then sets m_size = 140 reserving the 32-byte nonce slot (Job.cpp:180).
  • setJunoHeader (solo) copies 108 bytes and sets m_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 satisfy 140 <= size < sizeof(m_blob)=408 (:74-77, kMaxBlobSize=408 Job.h:50).
  • Cvt::fromHex into m_blob (:79).
  • If the existing nonce dword (readUnaligned(nonce()), i.e. uint32 at offset 108) is non-zero, m_nicehash is 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 by setZcashJob (stratum Zcash-notify) or setJunoHeader (solo), or delivered pre-built as the 140-byte blob (standard job).

Target parsing & diff conversion — Job::setTarget Job.cpp:209-250

  • For DragonX (not RX_YADA), target hex is decoded by Cvt::fromHex and interpreted by length:
    • 4 bytes: m_target = 0xFFFFFFFFFFFFFFFF / (0xFFFFFFFF / u32) (Job.cpp:219-220) — "compact" 32-bit difficulty-style target.
    • 8 bytes: m_target = u64 read directly (little-endian) (Job.cpp:222-223).
    • other lengths => 0 => rejected.
  • After parsing, m_diff = toDiff(m_target).
  • toDiff(target) = target ? (0xFFFFFFFFFFFFFFFF / target) : 0Job.h:121.
  • setTarget64(u64) is the direct setter used by solo: m_target = u64; m_diff = toDiff(u64) (Job.h:107). Solo computes target64 from the compact bits field (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). Larger m_target = easier.

3. NONCE HANDLING (most important)

nonceOffset / nonceSize

  • nonceOffset() for RX_DRAGONX = 108Job.cpp:271-273.
  • nonceSize() for RX_DRAGONX = 32Job.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 a uint32_t* pointing at blob offset 108 (WorkerJob.h:46, and the N==1 specialization :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:27 m_nonces[2] = {0,0}) and is reset to 0 on every job change (Miner.cpp:124 Nonce::reset(job.index()), which sets m_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) | counter into the uint32 at offset 108 (:57). The nonce+1 (offset 112) write at :59-61 only happens when mask > 0xFFFFFFFF. For DragonX nonceMask() = 0xFFFFFFFF (because nonceSize()=32 != sizeof(uint64_t)=8 and not nicehash → the 0xFFFFFFFFULL branch of Job.h:93), so only bytes [108:112] are ever written; bytes [112:139] are untouched by the nonce machinery.
  • Per-round increment: WorkerJob::nextRound adds roundSize (=1) to the uint32 at 108 each hash, and every kReserveCount(=32768) hashes grabs a fresh range via Nonce::next (WorkerJob.h:69-87, specialization :156-176; CpuWorker::nextRound CpuWorker.cpp:480-500, kReserveCount CpuWorker.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 SoloNonce and WorkerJob's solo path, NOT by the global Nonce counter.
  • WorkerJob::initSoloNonces() (WorkerJob.h:99-105) calls SoloNonce::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, or BCryptGenRandom on Windows; fallback mt19937_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-bit SoloNonce::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_nonce to the nonce field. Job::setExtraNonce/extraNonce() (Job.h:103,:79) are only set in MODE_SELF_SELECT (Client.cpp:394) and consumed only by SelfSelectClient (SelfSelectClient.cpp:172) — a different code path that does not run for DragonX pool mining. The DragonX worker code (CpuWorker.cpp) never reads extraNonce().
  • nonceMask() does reference extraNonce().size() but only for the nonceSize()==sizeof(uint64_t) (KawPow) case (Job.h:93); for DragonX (nonceSize()==32) the mask is the constant 0xFFFFFFFF, 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 disjoint reserveCount(32768)-sized ranges to each thread via Nonce::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; initial m_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 no extra_nonce JSON mechanism wired for DragonX, so per-miner uniqueness has to be baked into the blob (path 2a) the pool sends.
  • 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 DragonX nonceSize()==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_tempBuf was 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 hash m_hash for that nonce (passed into the JobResult ctor in CpuWorker.cpp:449). It is not the double-SHA256 pow_hash; it is the raw RandomX output (so the pool can re-verify randomx(blob,seed)==result and recompute double_sha256 itself).

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 copies m_result=result, m_nonceBytes=nonce32 (full 32 bytes), sets m_nonceSize=32, and m_isSoloResult=true.
    • This is what CpuWorker calls 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).
  • 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 set m_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::submitsubmitBlock (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_hashCpuWorker.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 from pow_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 at CpuWorker.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() against pow_value. There is no second comparison against a distinct network/block target in CpuWorker.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 same double_sha256 against the real network target (the comments at CpuWorker.cpp:446-448 state the pool does the block-vs-share distinction). The miner submits every share whose double_sha256 beats the pool-set job.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 bitssetTarget64 (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, nextRoundnextRoundSolo :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).