From fa16e740b602ee94de5a194e1be25583f340e968 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 28 Aug 2026 23:35:06 -0500 Subject: [PATCH] stratum: reject low-diff shares before spending a RandomX hash on them 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. --- src/stratum.cpp | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/stratum.cpp b/src/stratum.cpp index 7877dd171..d9cfffb44 100644 --- a/src/stratum.cpp +++ b/src/stratum.cpp @@ -1035,6 +1035,22 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork } blkhdr.nNonce = (uint256) nonce; + // Cheap SHA256d filter first. This test used to sit *below* the RandomX verify, so 32 + // arbitrary bytes from any peer bought a full ~65ms randomx_calculate_hash before anything + // rejected them -- on the shared HTTP/RPC libevent thread, and holding the global + // cs_randomx_validator that block validation also takes. GetHash() is SerializeHash over the + // header including nSolution, so passing this costs real grinding. Semantics are unchanged: + // an empty local_diff still parses to zero and still rejects, exactly as before. + if (!instance_of_cstratumparams.fAllowLowDiffShares && + UintToArith256(blkhdr.GetHash()) > arith_uint256(current_work.local_diff)) { + CBlockIndex diff_index; + diff_index.nBits = UintToArith256(blkhdr.GetHash()).GetCompact(); + const double share_diff = GetDifficulty(&diff_index); + diff_index.nBits = arith_uint256(current_work.local_diff).GetCompact(); + const double target_diff = GetDifficulty(&diff_index); + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Low diff share (diff %g, local %g)", share_diff, target_diff)); + } + // block is constructed, now it's time to VerifyEH if (StratumIsRandomX()) { @@ -1092,10 +1108,8 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork std::chrono::duration elapsed; uint64_t shares_accepted_since_last; - // TODO: we need to check hash > local port diff, and if it's true -> throw an exception -> diff too low (!) - if (!instance_of_cstratumparams.fAllowLowDiffShares) - if (UintToArith256(blkhdr.GetHash()) > arith_uint256(current_work.local_diff)) - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Low diff share (diff %g, local %g)", hush_real_diff, hush_local_diff)); + // (the low-diff share check moved above the RandomX verify -- see SubmitBlock's cheap + // SHA256d filter -- so that attacker-controlled bytes cannot buy a RandomX hash) if (finish > start) { @@ -1343,6 +1357,18 @@ UniValue stratum_mining_submit(StratumClient& client, const UniValue& params) const std::string method("mining.submit"); BoundParams(method, params, 5,5); + + // Parity with every other handler (GetWorkUnit, mining.aux.*, mining.extranonce.*), which all + // refuse an unauthorized client. NOTE this is not authentication: mining.authorize validates no + // credential, so it only costs an attacker one extra line. It is here so the submit path cannot + // be reached without at least completing the handshake; the cheap-target check below is what + // actually bounds the work an unknown peer can force. + if (!client.m_authorized && client.m_aux_addr.empty()) { + const std::string msg = strprintf("%s: share submitted by an unauthorized client", __func__); + LogPrint("stratum", "%s\n", msg); + throw JSONRPCError(RPC_INVALID_REQUEST, "Stratum client not authorized. Use mining.authorize first, with a DragonX R.. address as the username or 'x' to mine to the default address."); + } + // First parameter is the client username, which is ignored. /* EWBF 31 bytes job_id fix */