# 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 = 0x72151264` — `src/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-`f`s (`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) : 0` — `Job.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 = **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 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::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` ```c 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` ```c 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(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 `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`) ```json { "jsonrpc": "2.0", "method": "job", "params": { "job_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": , "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`) ```json { "id": , "jsonrpc": "2.0", "method": "submit", "params": { "id": "", "job_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).