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>
This commit is contained in:
2026-06-06 16:49:22 -05:00
parent f9d6bcad5e
commit 84e7d1453c
10 changed files with 385 additions and 146 deletions

View File

@@ -330,10 +330,16 @@ void xmrig::CpuWorker<N>::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<N>::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<uint64_t*>(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, &current_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<uint64_t*>(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<uint64_t*>(m_hash + (i * 32) + 24);