SubmitBlock validated in the wrong order. After two O(1) length checks it
went straight to CheckRandomXSolution() -- a full ~65ms
randomx_calculate_hash -- and only afterwards tested the share target. So
32 arbitrary bytes from any peer bought a RandomX hash, and the cost was
paid in three bad places at once: on the shared HTTP/RPC libevent thread
(stratum uses EventBase(), the same base ThreadHTTP dispatches, and RPC
replies are posted back onto it), inside the read loop that holds
cs_stratum so BlockWatcher cannot push new work to real miners, and
holding the global cs_randomx_validator that block validation also takes.
One connection writing submit lines pins that thread indefinitely.
Move the share-target test above the RandomX verify. CBlockHeader::
GetHash() is SerializeHash over the header including nSolution, so
meeting the target still requires real SHA256d grinding -- an attacker
now pays for the hash instead of the node. Semantics are unchanged,
including that an empty local_diff parses to zero and still rejects; only
the position moved, and the diagnostics are recomputed locally since the
old message used variables declared further down.
This does not make the submit path cheap, only bounded: at the default
share target the grind is small, so a submit rate limit and moving
SubmitBlock off the event loop are both still wanted.
Also add the authorization check every sibling handler has and
mining.submit lacked. Being explicit about what that is worth:
mining.authorize validates no credential, so the gate is parity and
handshake-ordering, not authentication. The reorder above is the part
that actually bounds an unknown peer.
CreateNewBlock() builds the stratum template with an OP_FALSE placeholder
in the coinbase, and CustomizeWork() substituted the miner's own payout
address only while that placeholder was still intact. GetWorkUnit() then
wrote the customized coinbase straight back into the shared template:
current_work.GetBlock().vtx[0] = cb;
current_work.GetBlock().hashMerkleRoot = ...BuildMerkleTree();
which consumed the placeholder for everyone. The first client to request
work after a tip change therefore captured the template. Every later
client on that job got a mining.notify whose merkle root already
committed to the first client's coinbase, CustomizeWork() was a no-op for
them on submit, and SubmitBlock() read the shared root back -- so a block
found by miner B was accepted paying miner A. It was silent: the daemon
logged "GOT BLOCK!!! by <B>" while the coinbase paid A.
With untrusted miners that is a reward-theft primitive, and it is cheap:
mining.authorize sets m_send_work, so re-sending it in a loop wins the
race after every tip.
Leave the template pristine and derive each client's header from a local
copy, in GetWorkUnit for the notify and again in SubmitBlock from the
coinbase CustomizeWork() just produced for that client. This is the
refactor the TODO removed here was asking for.
CustomizeWork() now stamps the payout script unconditionally, so a
coinbase that somehow arrives already customized can never be inherited
by another miner, and rejects an invalid address rather than silently
building a coinbase that pays no one.
Single-miner behaviour is unchanged, which is why this survived: it is
only observable with two miners on one template.
The "EWBF 31 bytes job_id fix" in stratum_mining_submit completes a
63-character job_id with each hex digit in turn and feeds the result
straight to uint256(). ParseHex() stops at the first non-hex character
and returns a shorter vector without signalling an error, and
base_blob(const std::vector<unsigned char>&) asserts vch.size() == 32.
So a single mining.submit whose job_id is any 63-character string
containing a non-hex byte -- 63 spaces will do -- aborts the node.
asserts are live in release builds here: -DNDEBUG appears only in
leveldb's own makefile, never in configure.ac, and the shipped binary
still carries the assertion string. The dispatch loop catches UniValue
and std::exception; abort() goes through both.
Size-check each candidate before constructing, as ParseUInt256() a few
hundred lines up already does for the ordinary path. If none of the 16
completions parse, ret stays null, misses work_templates, and the
handler returns false exactly as it does for any unknown job.
Not gated on IsHex(job_id_str): 63 is odd and IsHex() requires an even
length, so that test would disable the EWBF path this code exists for.
The stratum server was hardcoded for legacy Equihash (1347-byte solution,
sol.begin()+3 offset, CheckEquihashSolution) and was off-by-default with a
"do not use on RandomX" warning. DragonX is RandomX, so external pool mining
was impossible. This wires RandomX end-to-end.
Server (stratum.cpp), branched on ASSETCHAINS_ALGO == ASSETCHAINS_RANDOMX:
* GetWorkUnit sets StratumWork.nHeight and, for RandomX, sends the per-height
RandomX key via a new mining.set_randomx_key message (a miner cannot derive
it without the chain). The legacy mining.notify format is unchanged.
* SubmitBlock/stratum_mining_submit accept a 32-byte solution (== the RandomX
hash, used as nSolution verbatim) and validate it with CheckRandomXSolution
instead of CheckEquihashSolution. Target check (GetHash() < target) and the
nNonce = extranonce1||extranonce2 assembly are shared with the equihash path.
* -stratumtarget=<hex> overrides the pool share target (default diff-1); lets a
solo/low-difficulty test miner accept easy shares.
* GetWorkUnit's IsInitialBlockDownload guard is bypassed under -testnode=1 so an
isolated low-work test chain can serve work.
Reference miner: `stratummine "host" port ("address" timeout)` RPC (rpc/mining.cpp,
POSIX-only). A minimal stratum client that subscribes/authorizes, receives work +
the RandomX key, varies nNonce, hashes with RandomX via GetRandomXInput (byte-
identical to CheckRandomXSolution) and submits a 32-byte solution. Off-the-shelf
Equihash/Monero miners can't speak DragonX's 256-bit-nNonce Zcash header, so this
is the reference implementation. Added to the rpc client arg-conversion table.
Validated: loopback (chain 0->4, accepted every time, verifychain=true) AND a real
2-box LAN run (Linux miner -> Mac stratum server, 3 blocks accepted, verifychain=true).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fifth phase of the code-hygiene remediation, done per-file (18 files) via an
18-agent Workflow with strict guardrails. No consensus or security VALUES
changed (verified: pow/hush_utils/hush_bitcoind/net.h/crypter/stratum have
zero non-comment numeric changes); built clean, self-mined, verifychain=true.
Totals: 57 stale comments corrected, 46 user-facing branding fixes, 6
behavior-preserving named constants, 4 stratum warnings, 77 items deliberately
left (consensus/security values, copyright headers).
- Stale comments -> DragonX: corrected HUSH3/HUSH/Komodo/Equihash/Arrakis
leftovers that misdescribed the active chain (pow AWT/lwma notes, txdb
"Equihash solution" -> RandomX, wallet CLTV/overwinter/Sapling@1 notes,
util datadir provenance, crypter's stale mapSproutSpendingKeys invariant).
Replaced unprofessional/editorializing comments (profane TODOs, "developers
are elite") with neutral technical notes, preserving the real design info.
- User-facing branding: dumpwallet header "created by Hush" -> "DragonX";
RPC help text hushd/hush-cli/hushprivkey/hushaddress/Agama -> dragonx*;
rebranded provably-dead "HUSH3" symbol-fallback literals to DRAGONX.
- Named constants (same values, non-consensus/non-security): addrman
peer-selection factors (kChanceFactorGrowth/kChanceScale/deprioritize),
sietch MIN_ZOUTS, an init notarization-DB cache size.
- Left the security macros (ECC/TFM_TIMING_RESISTANT=420, comment-clarified
only), the consensus subsidy/commission literals + 128/129 TRANSITION
(comment only), and copyright headers untouched.
- Added a prominent WARNING that the stratum server is Equihash-era and NOT
updated for DragonX's RandomX PoW (flagged, not rewritten).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fourth phase of the code-hygiene remediation, addressing the ~1,082 stray
fprintf(stderr)/printf debug calls so consensus and hot paths stop spewing
to stderr/stdout. Done per-file (18 files) with conservative rules; the tree
builds clean, self-mines, and `verifychain` re-validates the whole chain
(pow/txdb/coins/miner paths) with -debug=1 enabling every converted line —
zero tinyformat/format-arg exceptions.
Net across 18 files: 174 commented-out debug lines deleted, 173 unconditional
live prints converted to LogPrint("<cat>",...)/LogPrintf (net/mining/pow/nspv/
zrpc categories, format strings + args preserved exactly), 40 pure-noise or
sensitive prints deleted, and 194 calls DELIBERATELY LEFT (already behind
fDebug/fZdebug guards, or genuine startup/fatal-error output that must reach
the console before logging init).
Notable:
- Deleted sensitive success-path dumps (nSPV SIG_TXHASH + full tx input/
output/change amounts; kvupdate privkey/pubkey hex) that were writing key
and amount material straight to stderr/stdout.
- Removed the raw 32-byte target hex dumps in the zawy adaptive-PoW helpers
and the legacy one-shot `if(height==340000)` HUSH artifact in pow.cpp.
- Converted per-tx relay + ban/banlist (net), per-setgenerate MININGTHREADS
(rpc/mining), signrawtransaction TXPOW, and per-message nSPV traces.
- Left format/arg-mismatched lines untouched (flagged) to avoid introducing
tinyformat runtime throws.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>