diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 00000000..76b448e0 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,291 @@ +# 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). diff --git a/README.md b/README.md index bfa3fc39..8dae5386 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,50 @@ -# XMRig-HAC +# DRG-XMRig -XMRig-HAC is a fork of [XMRig](https://github.com/xmrig/xmrig) with added support for **DragonX** coin mining using the **rx/hush** (RandomX-HUSH) algorithm. +**DRG-XMRig** is the official [DragonX](https://dragonx.is) CPU miner — a fork of +[XMRig](https://github.com/xmrig/xmrig) that mines DragonX's dual-hash +(RandomX + double-SHA256) proof of work. -**Repository:** [git.hush.is/dragonx/xmrig-hac](https://git.hush.is/dragonx/xmrig-hac) +**Repository:** https://git.dragonx.is/DragonX/drg-xmrig -## Fork Changes +## What's different -This fork adds: -- **DragonX** coin support -- **rx/hush** algorithm (RandomX-HUSH variant) -- Customized RandomX configuration for HUSH/DragonX +DragonX block validity is `SHA256D(header + RandomX(header)) <= target`: RandomX +produces the solution, and the **double-SHA256** of `header + solution` is the +difficulty-bearing hash. -Based on XMRig v6.25.0. +DRG-XMRig filters **every** hash on that double-SHA256 pow-hash (not on the RandomX +hash), uniformly in both solo and pool mode. A block is therefore simply a share +that clears a harder target, which means: + +- the pool receives **every** block candidate — no under-submission gap, and +- the reported hashrate is block-relevant (it matches the network metric). + +It mines the `rx/dragonx` algorithm (aliases: `rx/hush`, `dragonx`) using DragonX's +customized RandomX parameters. + +The exact wire protocol (job format, 32-byte nonce / extranonce handling, submit +format, pow-hash construction) is documented in [PROTOCOL.md](PROTOCOL.md). A +DragonX pool that scores shares on the double-SHA256 pow-hash is required. + +Based on XMRig v6.25. ## Mining backends -- **CPU** (x86/x64/ARMv7/ARMv8/RISC-V) -- **OpenCL** for AMD GPUs. -- **CUDA** for NVIDIA GPUs via external [CUDA plugin](https://github.com/xmrig/xmrig-cuda). +- **CPU** (x86/x64, ARMv7/ARMv8, RISC-V) -## Download -* **[Binary releases](https://git.hush.is/dragonx/xmrig-hac/releases)** -* **[Build from source](https://xmrig.com/docs/miner/build)** +## Build +``` +./build.sh --linux-release # Linux x86_64 static release +./build.sh --win-release # Windows x86_64 (MinGW cross-compile) +``` +Dependencies are built by `scripts/build_deps.sh` on first run. ## Usage -The preferred way to configure the miner is the [JSON config file](https://xmrig.com/docs/miner/config) as it is more flexible and human friendly. The [command line interface](https://xmrig.com/docs/miner/command-line-options) does not cover all features, such as mining profiles for different algorithms. Important options can be changed during runtime without miner restart by editing the config file or executing [API](https://xmrig.com/docs/miner/api) calls. - -* **[Wizard](https://xmrig.com/wizard)** helps you create initial configuration for the miner. -* **[Workers](http://workers.xmrig.info)** helps manage your miners via HTTP API. +``` +./xmrig -o : -u -a rx/dragonx +``` +The preferred way to configure the miner is the JSON config file (`config.json`). ## Credits - -This fork is based on [XMRig](https://github.com/xmrig/xmrig) by: -* **[xmrig](https://github.com/xmrig)** -* **[sech1](https://github.com/SChernykh)** +Based on [XMRig](https://github.com/xmrig/xmrig) by +[xmrig](https://github.com/xmrig) and [sech1](https://github.com/SChernykh)). +DragonX dual-hash mining model. diff --git a/build.sh b/build.sh index 300cd502..ba0467a4 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# XMRig-HAC Release Build Script +# DRG-XMRig (DragonX miner) Release Build Script # Usage: ./build.sh [--linux-release] [--win-release] set -e @@ -31,7 +31,7 @@ if ! $BUILD_LINUX && ! $BUILD_WIN; then exit 1 fi -echo "=== XMRig-HAC Release Builder v${VERSION} ===" +echo "=== DRG-XMRig Release Builder v${VERSION} ===" mkdir -p "$RELEASE_DIR" build_linux() { @@ -65,7 +65,7 @@ build_linux() { strip xmrig # Package - local PKG_NAME="xmrig-hac-${VERSION}-linux-x64" + local PKG_NAME="drg-xmrig-${VERSION}-linux-x64" local PKG_DIR="$RELEASE_DIR/$PKG_NAME" rm -rf "$PKG_DIR" mkdir -p "$PKG_DIR" @@ -95,7 +95,7 @@ build_windows() { local BUILD_DIR="$ROOT_DIR/build-windows" # Package - local PKG_NAME="xmrig-hac-${VERSION}-win-x64" + local PKG_NAME="drg-xmrig-${VERSION}-win-x64" local PKG_DIR="$RELEASE_DIR/$PKG_NAME" rm -rf "$PKG_DIR" mkdir -p "$PKG_DIR" diff --git a/doc/build/CMAKE_OPTIONS.md b/doc/build/CMAKE_OPTIONS.md deleted file mode 100644 index 81e3914f..00000000 --- a/doc/build/CMAKE_OPTIONS.md +++ /dev/null @@ -1,41 +0,0 @@ -# CMake options -**Recent version of this document: https://xmrig.com/docs/miner/cmake-options** - -## Algorithms - -* **`-DWITH_CN_LITE=OFF`** disable all CryptoNight-Lite algorithms (`cn-lite/0`, `cn-lite/1`). -* **`-DWITH_CN_HEAVY=OFF`** disable all CryptoNight-Heavy algorithms (`cn-heavy/0`, `cn-heavy/xhv`, `cn-heavy/tube`). -* **`-DWITH_CN_PICO=OFF`** disable CryptoNight-Pico algorithm (`cn-pico`). -* **`-DWITH_RANDOMX=OFF`** disable RandomX algorithms (`rx/loki`, `rx/wow`). -* **`-DWITH_ARGON2=OFF`** disable Argon2 algorithms (`argon2/chukwa`, `argon2/wrkz`). - -## Features - -* **`-DWITH_HWLOC=OFF`** -disable [hwloc](https://github.com/xmrig/xmrig/issues/1077) support. -Disabling this feature is not recommended in most cases. -This feature add external dependency to libhwloc (1.10.0+) (except MSVC builds). -* **`-DWITH_LIBCPUID=OFF`** disable built in libcpuid support, this feature always disabled if hwloc enabled, if both hwloc and libcpuid disabled auto configuration for CPU will very limited. -* **`-DWITH_HTTP=OFF`** disable built in HTTP support, this feature used for HTTP API and daemon (solo mining) support. -* **`-DWITH_TLS=OFF`** disable SSL/TLS support (secure connections to pool). This feature add external dependency to OpenSSL. -* **`-DWITH_ASM=OFF`** disable assembly optimizations for modern CryptoNight algorithms. -* **`-DWITH_EMBEDDED_CONFIG=ON`** Enable [embedded](https://github.com/xmrig/xmrig/issues/957) config support. -* **`-DWITH_OPENCL=OFF`** Disable OpenCL backend. -* **`-DWITH_CUDA=OFF`** Disable CUDA backend. -* **`-DWITH_SSE4_1=OFF`** Disable SSE 4.1 for Blake2 (useful for arm builds). - -## Debug options - -* **`-DWITH_DEBUG_LOG=ON`** enable debug log (mostly network requests). -* **`-DHWLOC_DEBUG=ON`** enable some debug log for hwloc. -* **`-DCMAKE_BUILD_TYPE=Debug`** enable debug build, only useful for investigate crashes, this option slow down miner. - -## Special build options - -* **`-DXMRIG_DEPS=`** path to precompiled dependencies https://github.com/xmrig/xmrig-deps -* **`-DARM_TARGET=`** override ARM target, possible values `7` (ARMv7) and `8` (ARMv8). -* **`-DUV_INCLUDE_DIR=`** custom path to libuv headers. -* **`-DUV_LIBRARY=`** custom path to libuv library. -* **`-DHWLOC_INCLUDE_DIR=`** custom path to hwloc headers. -* **`-DHWLOC_LIBRARY=`** custom path to hwloc library. -* **`-DOPENSSL_ROOT_DIR=`** custom path to OpenSSL. diff --git a/package.json b/package.json index 5a17ed1c..38b907fd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "xmrig", - "version": "3.0.0", - "description": "RandomX, CryptoNight and Argon2 miner", + "name": "drg-xmrig", + "version": "6.25.1", + "description": "DragonX RandomX (rx/dragonx) CPU miner", "main": "index.js", "directories": { "doc": "doc" @@ -11,13 +11,13 @@ }, "repository": { "type": "git", - "url": "git+https://git.hush.is/dragonx/xmrig-hac.git" + "url": "git+https://git.dragonx.is/DragonX/drg-xmrig.git" }, "keywords": [], "author": "", "license": "GPLv3", "bugs": { - "url": "https://git.hush.is/dragonx/xmrig-hac/issues" + "url": "https://git.dragonx.is/DragonX/drg-xmrig/issues" }, - "homepage": "https://git.hush.is/dragonx/xmrig-hac#readme" + "homepage": "https://git.dragonx.is/DragonX/drg-xmrig#readme" } diff --git a/src/backend/cpu/CpuWorker.cpp b/src/backend/cpu/CpuWorker.cpp index bf4e4f44..004764d9 100644 --- a/src/backend/cpu/CpuWorker.cpp +++ b/src/backend/cpu/CpuWorker.cpp @@ -330,10 +330,16 @@ void xmrig::CpuWorker::start() alignas(8) uint8_t current_solo_nonces[N * 32]; for (size_t i = 0; i < N; ++i) { current_job_nonces[i] = readUnaligned(m_job.nonce(i)); - // Save solo nonces BEFORE they get incremented by nextRound() + // Save the 32-byte nonce BEFORE nextRound() increments the counter. if (m_job.isSoloMining()) { memcpy(current_solo_nonces + i * 32, m_job.soloNonce(i), 32); } + // RX_HUSH/DragonX pool mining: the 32-byte nonce lives in the blob at + // [108:140] (4-byte counter + 28 bytes of pool-assigned extraNonce). Save it + // verbatim so the submitted nonce matches the header that was hashed. + else if (job.algorithm() == Algorithm::RX_HUSH) { + memcpy(current_solo_nonces + i * 32, m_job.blob() + m_job.nonceOffset() + i * job.size(), 32); + } } # ifdef XMRIG_FEATURE_BENCHMARK @@ -412,78 +418,34 @@ void xmrig::CpuWorker::start() } # endif - if (job.algorithm() == Algorithm::RX_HUSH && m_job.isSoloMining()) { - // ── DRAGONX/HUSH solo mining: use double_sha256(173-byte header) for difficulty ── + if (job.algorithm() == Algorithm::RX_HUSH) { + // ── DRAGONX/HUSH dual-hash PoW (unified solo + pool) ── // - // The daemon checks GetHash() = double_sha256(173 bytes) < target, - // NOT the RandomX hash directly. We must filter shares the same way - // so that every submitted share is a genuine block candidate. + // The block/share metric is double_sha256(header + RandomX solution), + // NOT the RandomX hash. Filter EVERY hash on this pow-hash so that shares + // and blocks use the same metric: a block is simply a share that clears a + // harder target, so the pool receives every block candidate (no gap). // - // Reconstruct the 140-byte blob that was ACTUALLY hashed this round: - // - bytes [0:108] are unchanged by nextRound() (only nonce changes) - // - bytes [108:140] = saved nonce (current_solo_nonces, before nextRound) + // Reconstruct the 140-byte header that was actually hashed this round: + // - bytes [0:108] header base (unchanged by nextRound) + // - bytes [108:140] the 32-byte nonce saved before nextRound: + // solo: a random 32-byte nonce; + // pool: a 4-byte counter + 28 bytes of pool-assigned extraNonce, + // preserved verbatim from the blob. uint8_t blob_for_header[140]; - memcpy(blob_for_header, m_job.blob(), 108); // header base (unchanged) - memcpy(blob_for_header + 108, current_solo_nonces + i * 32, 32); // saved 32-byte nonce + memcpy(blob_for_header, m_job.blob(), 108); + memcpy(blob_for_header + 108, current_solo_nonces + i * 32, 32); - // Compute PoW hash: double_sha256(blob[140] + 0x20 + rx_hash[32]) + // PoW hash = double_sha256(blob[140] + 0x20 + rx_hash[32]) alignas(8) uint8_t pow_hash[32]; dragonx_pow_hash(blob_for_header, m_hash + (i * 32), pow_hash); - // Compare last 8 bytes of pow_hash (same field as XMRig's standard check) + // Compare last 8 bytes (same field as XMRig's standard difficulty check). const uint64_t pow_value = *reinterpret_cast(pow_hash + 24); if (pow_value < job.target()) { - // Submit full 32-byte nonce + rx_hash as result + // Submit the full 32-byte nonce + the RandomX hash as the result. JobResults::submit(JobResult(job, current_solo_nonces + i * 32, m_hash + (i * 32))); } - } else if (job.algorithm() == Algorithm::RX_HUSH && !m_job.isSoloMining()) { - // ── DRAGONX/HUSH pool mining: dual check ── - // - // DragonX uses dual PoW: the block hash is SHA256D(header + RandomX solution), - // NOT the RandomX hash itself. We must check SHA256D for EVERY hash to detect - // blocks, and also check the RandomX hash for share difficulty. - // - // Reconstruct the 140-byte header with the 4-byte nonce at offset 108. - // Bytes 112-139 come from the pool blob and may contain a per-client - // extraNonce1 (pool embeds it at offset 112 for nonce-space partitioning). - // We must preserve those bytes so pool-side re-verification matches. - uint8_t blob_for_header[140]; - memcpy(blob_for_header, m_job.blob(), 140); // full blob incl. extraNonce - memcpy(blob_for_header + 108, ¤t_job_nonces[i], 4); // overwrite only bytes 108-111 - - bool submitted = false; - - // Check SHA256D for block detection if pool sent block_target - if (job.hasBlockTarget()) { - alignas(8) uint8_t pow_hash[32]; - dragonx_pow_hash(blob_for_header, m_hash + (i * 32), pow_hash); - - // Compare pow_hash <= block_target (both in uint256 internal byte order) - // byte[31] is MSB (most significant in arith terms), compare from there down - bool isBlock = true; - for (int b = 31; b >= 0; --b) { - if (pow_hash[b] < job.blockTarget()[b]) { - break; // pow_hash < target → is a block - } else if (pow_hash[b] > job.blockTarget()[b]) { - isBlock = false; - break; // pow_hash > target → not a block - } - } - - if (isBlock) { - // SHA256D meets block target — submit immediately - JobResults::submit(job, current_job_nonces[i], m_hash + (i * 32), nullptr); - submitted = true; - } - } - - // Also check RandomX hash for normal share difficulty (if not already submitted) - if (!submitted) { - const uint64_t value = *reinterpret_cast(m_hash + (i * 32) + 24); - if (value < job.target()) { - JobResults::submit(job, current_job_nonces[i], m_hash + (i * 32), nullptr); - } - } } else { // ── Standard XMRig path (Monero, CryptoNight, etc.) ── const uint64_t value = *reinterpret_cast(m_hash + (i * 32) + 24); diff --git a/src/base/net/stratum/Client.cpp b/src/base/net/stratum/Client.cpp index f0110a96..59461779 100644 --- a/src/base/net/stratum/Client.cpp +++ b/src/base/net/stratum/Client.cpp @@ -81,7 +81,7 @@ xmrig::Client::Client(int id, const char *agent, IClientListener *listener) : BaseClient(id, listener), m_agent(agent), m_sendBuf(1024), - m_tempBuf(256) + m_tempBuf(512) // sized for RX_HUSH/DragonX 32-byte (64 hex) nonce { m_reader.setListener(this); m_key = m_storage.add(this); @@ -196,11 +196,18 @@ int64_t xmrig::Client::submit(const JobResult &result) const char *nonce = result.nonce; const char *data = result.result; # else - char *nonce = m_tempBuf.data(); - char *data = m_tempBuf.data() + 16; - char *signature = m_tempBuf.data() + 88; + // Nonce width is algorithm-dependent: RX_HUSH/DragonX uses a 32-byte nonce (64 hex), + // everything else 4 bytes (8 hex). Lay the temp buffer out dynamically so the larger + // nonce can't overrun the result/signature fields. + const size_t nonceHexSize = result.nonceSize() * 2 + 1; + const size_t dataOffset = (nonceHexSize + 7) & ~static_cast(7); // align to 8 + const size_t sigOffset = dataOffset + 72; - Cvt::toHex(nonce, sizeof(uint32_t) * 2 + 1, reinterpret_cast(&result.nonce), sizeof(uint32_t)); + char *nonce = m_tempBuf.data(); + char *data = m_tempBuf.data() + dataOffset; + char *signature = m_tempBuf.data() + sigOffset; + + Cvt::toHex(nonce, nonceHexSize, result.nonceBytes(), result.nonceSize()); Cvt::toHex(data, 65, result.result(), 32); if (result.minerSignature()) { diff --git a/src/base/net/stratum/Job.h b/src/base/net/stratum/Job.h index 3b524849..bb985e3d 100644 --- a/src/base/net/stratum/Job.h +++ b/src/base/net/stratum/Job.h @@ -77,7 +77,7 @@ public: inline const uint32_t *nonce() const { return reinterpret_cast(m_blob + nonceOffset()); } inline const uint8_t *blob() const { return m_blob; } inline size_t nonceSize() const { - if (algorithm() == Algorithm::RX_HUSH && m_isSoloMining) return 32; + if (algorithm() == Algorithm::RX_HUSH) return 32; return (algorithm().family() == Algorithm::KAWPOW) ? 8 : 4; } inline size_t size() const { return m_size; } diff --git a/src/net/JobResult.h b/src/net/JobResult.h index 01b47277..0aaf3f7e 100644 --- a/src/net/JobResult.h +++ b/src/net/JobResult.h @@ -54,6 +54,11 @@ public: { memcpy(m_result, result, sizeof(m_result)); + // Populate nonceBytes so the stratum submit (which reads nonceBytes()/nonceSize()) + // sends the correct nonce on the standard 4-byte path too. + memcpy(m_nonceBytes, &nonce, sizeof(uint32_t)); + m_nonceSize = 4; + if (header_hash) { memcpy(m_headerHash, header_hash, sizeof(m_headerHash)); } diff --git a/src/version.h b/src/version.h index 75b3ac28..12655737 100644 --- a/src/version.h +++ b/src/version.h @@ -8,10 +8,10 @@ #ifndef XMRIG_VERSION_H #define XMRIG_VERSION_H -#define APP_ID "xmrig-hac" -#define APP_NAME "XMRig-HAC" -#define APP_DESC "XMRig miner" -#define APP_VERSION "6.25.1-hac" +#define APP_ID "drg-xmrig" +#define APP_NAME "DRG-XMRig" +#define APP_DESC "DragonX RandomX miner (rx/dragonx)" +#define APP_VERSION "6.25.1-drg1" #define APP_DOMAIN "dragonx.is" #define APP_SITE "www.dragonx.is" #define APP_COPYRIGHT "Copyright (C) 2026 dragonx.is"