upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/rpc.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 05:44:41 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 05:44:41 +0000
commita3666bded1553b101b631d87e99961bf3ccd0a3e (patch)
tree7b351f42899bb5f38d6b1efdaf54cf04b8456d69 /src/rpc.cpp
parentd7b1e8a1d54192df2fcf633fbcdffb43389ff8e2 (diff)
pool: refuse a (job, nonce) pair this connection already submitted
mining.submit validated the nonce, rebuilt the block, re-hashed it with RandomX -- and never asked whether it had seen this exact (job_id, nonce) pair before. Nothing in that pipeline distinguishes a replay from a first submission: a duplicated valid share passes every check and lands in roundShareCount[address]++ a second time, and roundShareCount is the divisor the payout split uses, so replaying one valid share N times multiplies that address's cut of the round by N. Keep a per-connection set of credited nonces on the Miner, reject a repeat with a "Duplicate share" stratum error before the rebuild and hash it would waste, and record the nonce at the moment the share is credited. The set is keyed to the job id the miner is currently working and cleared inside SendJob -- the single funnel every new-job path (initial authorize, block found, new block, 60s refresh, no-job watchdog, vardiff retargets) goes through -- so legitimate resubmissions across jobs are never refused. The clear is keyed on the id rather than unconditional because the vardiff retarget and stall watchdog re-send the CURRENT job, and an unconditional clear there would reopen the very replay window this closes. Job ids come from a monotonic atomic counter in RebuildJob, so an id is never recycled within a process. Closes #8
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp38
1 files changed, 38 insertions, 0 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 82f895b..f1a7153 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -2511,6 +2511,16 @@ struct Miner {
2511 int64 lastShareTime = 0; // GetTime() of the previous accepted share 2511 int64 lastShareTime = 0; // GetTime() of the previous accepted share
2512 double hashRate = 0.0; // smoothed H/s estimate 2512 double hashRate = 0.0; // smoothed H/s estimate
2513 2513
2514 // Replay guard for mining.submit (#8): nonces already credited on this
2515 // connection for the job it is currently working. Keyed to the job id in
2516 // seenNoncesJob and cleared by SendJob whenever that id changes, so a
2517 // resubmission under a NEW job is never turned away, while a replayed
2518 // (job_id, nonce) pair under the same job is refused before it can be
2519 // credited twice into roundShareCount -- the figure the payout split
2520 // divides by.
2521 std::set<uint32_t> seenNonces;
2522 std::string seenNoncesJob;
2523
2514 explicit Miner(btf_socket_t f) 2524 explicit Miner(btf_socket_t f)
2515 : fd(f), connectTime(GetTime()), 2525 : fd(f), connectTime(GetTime()),
2516 extranonce2(gNextExtranonce2.fetch_add(1)) {} 2526 extranonce2(gNextExtranonce2.fetch_add(1)) {}
@@ -2693,6 +2703,17 @@ static json MakeNotifyParams(const StratumJob& job, const CBlock& b,
2693static void SendJob(Miner* m, bool clean) 2703static void SendJob(Miner* m, bool clean)
2694{ 2704{
2695 if (!gHaveJob) return; 2705 if (!gHaveJob) return;
2706 // Every path that hands a miner a new job funnels through here, so this
2707 // is the one place the replay guard needs to follow the job id: nonces
2708 // credited under the previous job mean nothing under the new one. Keying
2709 // the clear to the id (rather than clearing unconditionally) keeps a
2710 // same-job re-notify -- the vardiff retarget and stall watchdog both
2711 // re-send the CURRENT job -- from reopening the duplicate window it
2712 // exists to close.
2713 if (m->seenNoncesJob != gCurrentJob.jobId) {
2714 m->seenNonces.clear();
2715 m->seenNoncesJob = gCurrentJob.jobId;
2716 }
2696 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); 2717 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2);
2697 // Use this miner's own shareTarget (set by vardiff) in the notify params. 2718 // Use this miner's own shareTarget (set by vardiff) in the notify params.
2698 // The participant miner reads p[3] as its share target, and HandleLine 2719 // The participant miner reads p[3] as its share target, and HandleLine
@@ -2951,6 +2972,22 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2951 } 2972 }
2952 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); 2973 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
2953 2974
2975 // Reject a replayed (job, nonce) before it costs a rebuild and a
2976 // RandomX hash: nothing about the work below distinguishes a second
2977 // submission of a valid share from the first, so without this check
2978 // it passes every test and is credited again -- and roundShareCount
2979 // is what the payout split divides by, so each replay multiplies the
2980 // address's cut of the round. The set is per-connection and cleared
2981 // by SendJob on every job change, so only a same-job repeat is
2982 // refused.
2983 if (m->seenNonces.count(nNonce)) {
2984 LogPrint("worker", "[worker->pool] duplicate share from %s"
2985 " nonce=%u job=%s -- already credited, not counting again\n",
2986 m->address.c_str(), nNonce, submitJobId.c_str());
2987 reply(false, "Duplicate share");
2988 return true;
2989 }
2990
2954 // Rebuild this miner's exact block (unique merkle via extranonce2) 2991 // Rebuild this miner's exact block (unique merkle via extranonce2)
2955 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); 2992 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2);
2956 b.nNonce = nNonce; 2993 b.nNonce = nNonce;
@@ -2978,6 +3015,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2978 m->roundShares++; 3015 m->roundShares++;
2979 m->totalShares++; 3016 m->totalShares++;
2980 m->vardiffShares++; 3017 m->vardiffShares++;
3018 m->seenNonces.insert(nNonce); // replay guard: credit each nonce once per job
2981 int64 shareTime = GetTime(); 3019 int64 shareTime = GetTime();
2982 m->lastSeen = shareTime; 3020 m->lastSeen = shareTime;
2983 roundShareCount[m->address]++; 3021 roundShareCount[m->address]++;