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

292 lines
25 KiB
Markdown

# 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<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 `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": "<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`)
```json
{
"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).