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-03 21:49:35 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-03 21:49:35 +0000
commit4b3069f2de140cda427627d470f2751609be6620 (patch)
treedb45b40a2c0516a2fcb439ffaca6e993da74009a /src/rpc.cpp
parentea8fe94658bf20d2b669b619231238f12e6bd8fc (diff)
parent07051b2378734d62fb94a4a62819d8150452b661 (diff)
merge: !3 fix/pool-accounting into deploy/all-2026-08-24
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp285
1 files changed, 279 insertions, 6 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index bd5fd8b..49746ee 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -115,15 +115,28 @@ static uint256 ShareTargetFromDifficulty(double difficulty)
115{ 115{
116 if (difficulty <= 0.0) difficulty = 1.0; 116 if (difficulty <= 0.0) difficulty = 1.0;
117 static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256(); 117 static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256();
118 // Representability floor. shareTarget = diff1 / difficulty, and diff1 is
119 // ~uint256(0) >> 12, i.e. a hair under 2^244 -- so the true target stops
120 // fitting in a uint256 once difficulty reaches 2^-12 (= 1/4096), and the
121 // fixed-point divide below breaks even earlier in a second way: the
122 // (uint64)(difficulty * 65536.0) divisor truncates to zero for any
123 // difficulty under 2^-16. Both thresholds sit far below VARDIFF_MIN
124 // (0.001), so live operation never comes near this, but a caller that
125 // bypasses vardiff (or a future VARDIFF_MIN) must not divide by zero or
126 // hand back getuint256()'s low-256-bit truncation of an oversized
127 // quotient -- a wrapped target is not merely wrong, it is arbitrarily
128 // HARDER than difficulty 1. At these difficulties every hash is meant to
129 // be a share, so the honest answer is the easiest representable target.
130 // (This replaces a dead branch that read `if (t < diff1 && difficulty <
131 // 1.0)`: t = diff1/difficulty, so difficulty < 1.0 implies t > diff1 and
132 // the two conditions could never both hold.)
133 if (difficulty < 1.0 / 4096.0) return ~uint256(0);
118 // shareTarget = diff1 / difficulty 134 // shareTarget = diff1 / difficulty
119 CBigNum bn; bn.setuint256(diff1); 135 CBigNum bn; bn.setuint256(diff1);
120 // Multiply by 2^16 to keep precision, divide, shift back 136 // Multiply by 2^16 to keep precision, divide, shift back
121 bn <<= 16; 137 bn <<= 16;
122 bn /= (uint64)(difficulty * 65536.0); 138 bn /= (uint64)(difficulty * 65536.0);
123 uint256 t = bn.getuint256(); 139 return bn.getuint256();
124 // Cap at all-ones (maximum, easiest possible)
125 if (t < diff1 && difficulty < 1.0) t = ~uint256(0);
126 return t;
127} 140}
128 141
129// Expected number of hashes to find one share at a given Stratum difficulty, 142// Expected number of hashes to find one share at a given Stratum difficulty,
@@ -2473,6 +2486,31 @@ static const double VARDIFF_MAX = 1e12;
2473static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance 2486static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance
2474static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much 2487static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much
2475 2488
2489// #7: m->hashRate is an EWMA sampled only when a share arrives, so a miner
2490// that stops hashing keeps its last estimate forever and RefreshStats sums
2491// it into the announced pool total for as long as the connection sits open
2492// -- the published pool hashrate ran several times over ground truth
2493// because of exactly this. MinerReportedHashRate() below applies the
2494// policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share
2495// intervals (expected interval = expected hashes per share at its
2496// difficulty, divided by the estimate itself) with no share, after which
2497// the figure we report falls off with a half-life of one more such
2498// interval. The interval is clamped to [HASHRATE_MIN_INTERVAL,
2499// HASHRATE_MAX_INTERVAL] so a fast miner at the vardiff floor (expected
2500// interval of milliseconds) is not punished for ordinary share-delivery
2501// jitter, and a very slow miner's grace does not stretch past the point
2502// where its estimate means anything. At the vardiff target of one share
2503// per 15s that is a minute of grace, half the rate reported 75s in, and
2504// nothing reported after about seventeen minutes of silence.
2505static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals
2506static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval
2507static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval
2508// Half-lives past the grace beyond which we report a flat zero. 2^-64 of the
2509// original estimate is not a number anyone needs, and stopping here keeps
2510// pow() from walking down into the denormals for a miner that has been quiet
2511// for hours.
2512static const double HASHRATE_DECAY_CUTOFF = 64.0;
2513
2476struct Miner { 2514struct Miner {
2477 btf_socket_t fd; 2515 btf_socket_t fd;
2478 bool authorised = false; 2516 bool authorised = false;
@@ -2500,11 +2538,80 @@ struct Miner {
2500 int64 lastShareTime = 0; // GetTime() of the previous accepted share 2538 int64 lastShareTime = 0; // GetTime() of the previous accepted share
2501 double hashRate = 0.0; // smoothed H/s estimate 2539 double hashRate = 0.0; // smoothed H/s estimate
2502 2540
2541 // Replay guard for mining.submit (#8): nonces already credited on this
2542 // connection for the job it is currently working. Keyed to the job id in
2543 // seenNoncesJob and cleared by SendJob whenever that id changes, so a
2544 // resubmission under a NEW job is never turned away, while a replayed
2545 // (job_id, nonce) pair under the same job is refused before it can be
2546 // credited twice into roundShareCount -- the figure the payout split
2547 // divides by.
2548 std::set<uint32_t> seenNonces;
2549 std::string seenNoncesJob;
2550
2503 explicit Miner(btf_socket_t f) 2551 explicit Miner(btf_socket_t f)
2504 : fd(f), connectTime(GetTime()), 2552 : fd(f), connectTime(GetTime()),
2505 extranonce2(gNextExtranonce2.fetch_add(1)) {} 2553 extranonce2(gNextExtranonce2.fetch_add(1)) {}
2506}; 2554};
2507 2555
2556// What we publish as a miner's H/s: the last EWMA sample, faded out if shares
2557// have stopped arriving (#7, policy at HASHRATE_IDLE_MISSES).
2558//
2559// This reads m->hashRate and never writes it. That is the whole design. The
2560// first attempt decayed the stored estimate in place from the event loop's
2561// sweep, which put a running accumulator behind a figure that is only ever
2562// displayed, and the accumulator misbehaved in two ways:
2563//
2564// - `expected` was recomputed from the already-decayed estimate on every
2565// pass, so each decay raised `expected`, which raised `grace`, which is
2566// the very thing gating the next decay. `grace` simply chased `silent`
2567// upward and the reported rate settled into a 1/t crawl instead of the
2568// half-life curve the comment above promises -- 291x too high at ten
2569// half-lives past the grace. Issue #7 is about the pool over-reporting;
2570// that under-corrected it for hours.
2571// - Once `expected` reached HASHRATE_MAX_INTERVAL it could not rise any
2572// further, the feedback that had been gating each pass was gone, and the
2573// decay began compounding once a second against the total elapsed
2574// silence -- taking a miner from 0.4% of its baseline to 1e-11 of it in
2575// the next hundred seconds, four hours in, for no reason a reader of
2576// either the code or the announced number could see.
2577//
2578// Computing the value instead of accumulating it removes the failure mode
2579// rather than tuning it: with no stored decay state there is nothing for a
2580// tick rate, a missed pass or a duplicated pass to perturb, and the answer
2581// depends only on how long the miner has actually been quiet. It also keeps
2582// the fade out of the estimator -- the EWMA on the share path still blends
2583// against the value it last computed, not against a faded copy of it, so a
2584// miner that resumes recovers on exactly the curve it would have without any
2585// of this.
2586//
2587// `now` is a parameter rather than a GetTime() call so that one refresh fades
2588// every miner to the same instant, and so the policy is directly testable.
2589static double MinerReportedHashRate(const Miner* m, int64 now)
2590{
2591 // Nothing sampled yet: a fresh connection, or one that has found a single
2592 // share and so has no interval to have been timed over. Nothing to fade,
2593 // and nothing to divide by below.
2594 if (m->hashRate <= 0.0 || m->lastShareTime <= 0) return m->hashRate;
2595
2596 double expected = ExpectedHashesForDifficulty(m->difficulty) / m->hashRate;
2597 // Written so that a NaN would land on the floor rather than sail through:
2598 // the clamp is what guarantees `expected` is finite and strictly positive
2599 // below, whatever the division produced.
2600 if (!(expected > HASHRATE_MIN_INTERVAL)) expected = HASHRATE_MIN_INTERVAL;
2601 if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL;
2602
2603 double silent = (double)(now - m->lastShareTime);
2604 double grace = HASHRATE_IDLE_MISSES * expected;
2605 // grace is at least HASHRATE_IDLE_MISSES seconds, so a clock that has
2606 // stepped backwards gives a negative `silent`, takes this branch, and
2607 // reports the sample unchanged. Nothing here can scale an estimate up.
2608 if (!(silent > grace)) return m->hashRate;
2609
2610 double halflives = (silent - grace) / expected;
2611 if (halflives >= HASHRATE_DECAY_CUTOFF) return 0.0;
2612 return m->hashRate * pow(0.5, halflives);
2613}
2614
2508static std::vector<Miner*> gMiners; 2615static std::vector<Miner*> gMiners;
2509 2616
2510struct FoundBlock { int height; int64 ts; std::string addr; }; 2617struct FoundBlock { int height; int64 ts; std::string addr; };
@@ -2555,6 +2662,18 @@ static void LocalAcceptFn(void*)
2555 break; 2662 break;
2556 continue; 2663 continue;
2557 } 2664 }
2665 // #6: let the OS probe a peer that has gone silent, so a half-open
2666 // session eventually errors out of recv and lands in the event
2667 // loop's existing reap path instead of sitting in gMiners forever.
2668 // This is a backstop, not the mechanism: the default cadence before
2669 // the first probe is hours on most stacks, so the idle timeout in
2670 // PoolEventLoop fires long before keepalive does. Miners arriving
2671 // via a relay ride a loopback socket pair whose remote transport is
2672 // the rendezvous socket owned by the tunnel pump threads; keepalive
2673 // cannot reach across that, which is part of why the idle timeout
2674 // exists.
2675 int keepalive = 1;
2676 setsockopt(c, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive));
2558 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); 2677 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
2559 PushReady((btf_socket_t)c); 2678 PushReady((btf_socket_t)c);
2560 } 2679 }
@@ -2598,6 +2717,16 @@ static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it
2598static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not 2717static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not
2599 // authorized after this long is 2718 // authorized after this long is
2600 // idle, not merely starting up 2719 // idle, not merely starting up
2720// #6: a Miner used to leave gMiners only when RecvLine reported a clean
2721// disconnect. A half-open session (miner host died, NAT state expired,
2722// cable pulled) sends no FIN and reports no error, so it sat in the table
2723// forever -- authorize outpaced disconnect 2:1 on the live pool, and the
2724// table grew toward MAX_POOL_MINERS, past which real miners are refused.
2725// lastSeen is stamped on every complete line received; a miner that has
2726// said nothing for this long is gone in every way that matters. Ten
2727// minutes is far beyond the vardiff target (a share every 15s) and the
2728// stall watchdog (45s), so no miner that is actually hashing comes close.
2729static const int64 POOL_IDLE_TIMEOUT_SECS = 600;
2601 2730
2602// Non-blocking recv: pull whatever bytes are available now into buf, then 2731// Non-blocking recv: pull whatever bytes are available now into buf, then
2603// return one complete line if one is ready. Three outcomes: 2732// return one complete line if one is ready. Three outcomes:
@@ -2682,6 +2811,17 @@ static json MakeNotifyParams(const StratumJob& job, const CBlock& b,
2682static void SendJob(Miner* m, bool clean) 2811static void SendJob(Miner* m, bool clean)
2683{ 2812{
2684 if (!gHaveJob) return; 2813 if (!gHaveJob) return;
2814 // Every path that hands a miner a new job funnels through here, so this
2815 // is the one place the replay guard needs to follow the job id: nonces
2816 // credited under the previous job mean nothing under the new one. Keying
2817 // the clear to the id (rather than clearing unconditionally) keeps a
2818 // same-job re-notify -- the vardiff retarget and stall watchdog both
2819 // re-send the CURRENT job -- from reopening the duplicate window it
2820 // exists to close.
2821 if (m->seenNoncesJob != gCurrentJob.jobId) {
2822 m->seenNonces.clear();
2823 m->seenNoncesJob = gCurrentJob.jobId;
2824 }
2685 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); 2825 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2);
2686 // Use this miner's own shareTarget (set by vardiff) in the notify params. 2826 // Use this miner's own shareTarget (set by vardiff) in the notify params.
2687 // The participant miner reads p[3] as its share target, and HandleLine 2827 // The participant miner reads p[3] as its share target, and HandleLine
@@ -2940,6 +3080,22 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2940 } 3080 }
2941 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); 3081 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
2942 3082
3083 // Reject a replayed (job, nonce) before it costs a rebuild and a
3084 // RandomX hash: nothing about the work below distinguishes a second
3085 // submission of a valid share from the first, so without this check
3086 // it passes every test and is credited again -- and roundShareCount
3087 // is what the payout split divides by, so each replay multiplies the
3088 // address's cut of the round. The set is per-connection and cleared
3089 // by SendJob on every job change, so only a same-job repeat is
3090 // refused.
3091 if (m->seenNonces.count(nNonce)) {
3092 LogPrint("worker", "[worker->pool] duplicate share from %s"
3093 " nonce=%u job=%s -- already credited, not counting again\n",
3094 m->address.c_str(), nNonce, submitJobId.c_str());
3095 reply(false, "Duplicate share");
3096 return true;
3097 }
3098
2943 // Rebuild this miner's exact block (unique merkle via extranonce2) 3099 // Rebuild this miner's exact block (unique merkle via extranonce2)
2944 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); 3100 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2);
2945 b.nNonce = nNonce; 3101 b.nNonce = nNonce;
@@ -2967,6 +3123,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2967 m->roundShares++; 3123 m->roundShares++;
2968 m->totalShares++; 3124 m->totalShares++;
2969 m->vardiffShares++; 3125 m->vardiffShares++;
3126 m->seenNonces.insert(nNonce); // replay guard: credit each nonce once per job
2970 int64 shareTime = GetTime(); 3127 int64 shareTime = GetTime();
2971 m->lastSeen = shareTime; 3128 m->lastSeen = shareTime;
2972 roundShareCount[m->address]++; 3129 roundShareCount[m->address]++;
@@ -4581,6 +4738,82 @@ int RunPoolStratumSelfTest()
4581 strPoolRoundsFile = savedRoundsFile; 4738 strPoolRoundsFile = savedRoundsFile;
4582 gPoolRounds = savedRounds; 4739 gPoolRounds = savedRounds;
4583 4740
4741 // -- hashrate decay (#7) --------------------------------------------------
4742 // The first version of this decayed m->hashRate in place from the event
4743 // loop's sweep, which made the reported figure a function of how the loop
4744 // happened to run rather than of how long the miner had been quiet. These
4745 // pin the properties that fixes: sampling changes nothing, and the answer
4746 // depends only on elapsed silence.
4747 {
4748 // A miner exactly on the vardiff target: 4096 hashes per share at
4749 // difficulty 1.0, one share per 15s, so 4096/15 H/s. Its expected
4750 // interval is 15s and its grace is 60s.
4751 const double base = 4096.0 / 15.0;
4752 const int64 t0 = 1766000000; // any plausible non-zero wall clock
4753 Miner decayed(INVALID_SOCKET);
4754 decayed.difficulty = 1.0;
4755 decayed.hashRate = base;
4756 decayed.lastShareTime = t0;
4757
4758 check(MinerReportedHashRate(&decayed, t0 + 60) == base,
4759 "hashrate decay: a miner inside its grace period reports its full estimate");
4760
4761 // Sampling must not consume the estimate. Twenty passes is one second
4762 // of the event loop's old sweep; a thousand is nearly a minute of it.
4763 double once = MinerReportedHashRate(&decayed, t0 + 61);
4764 bool idempotent = (decayed.hashRate == base);
4765 for (int i = 0; i < 1000 && idempotent; i++)
4766 idempotent = (MinerReportedHashRate(&decayed, t0 + 61) == once) &&
4767 (decayed.hashRate == base);
4768 check(idempotent,
4769 "hashrate decay: repeated sampling returns one value and leaves the estimate alone");
4770
4771 // One second past a 60s grace is 1/15th of a half-life: ~4.5% off, not
4772 // the estimate gone. A miner at the vardiff target overshoots its grace
4773 // roughly 1.8% of the time (e^-4) purely by share-timing variance, and
4774 // must not be zeroed for it.
4775 check(once > 0.94 * base && once < 0.96 * base,
4776 "hashrate decay: one second past grace costs a few percent, not the estimate");
4777 double three = MinerReportedHashRate(&decayed, t0 + 63);
4778 check(three > 0.86 * base && three < 0.88 * base,
4779 "hashrate decay: three seconds past grace costs ~13%");
4780
4781 // One half-life past the grace is exactly half.
4782 double half = MinerReportedHashRate(&decayed, t0 + 75);
4783 check(half > 0.499 * base && half < 0.501 * base,
4784 "hashrate decay: one expected interval past grace halves the estimate");
4785
4786 // A miner that has genuinely stopped ends up reported as nothing --
4787 // the whole point of #7. The old in-place version could not get here:
4788 // its decay fed back into its own grace and settled into a 1/t crawl.
4789 check(MinerReportedHashRate(&decayed, t0 + 3600) == 0.0,
4790 "hashrate decay: a miner silent for an hour is reported as zero");
4791
4792 // A clock that steps backwards must never inflate what we announce.
4793 check(MinerReportedHashRate(&decayed, t0 - 3600) == base,
4794 "hashrate decay: a backwards clock reports the estimate unchanged, never higher");
4795
4796 // Fresh connection, and a connection with one share and so no interval
4797 // to have timed: both must report nothing rather than divide by zero.
4798 Miner fresh(INVALID_SOCKET);
4799 check(MinerReportedHashRate(&fresh, t0) == 0.0,
4800 "hashrate decay: a miner that never submitted a share reports zero");
4801 Miner single(INVALID_SOCKET);
4802 single.lastShareTime = t0;
4803 check(MinerReportedHashRate(&single, t0 + 86400) == 0.0,
4804 "hashrate decay: a miner with no rate sample yet reports zero");
4805
4806 // Nothing in the swept range may produce a NaN, an infinity, a negative
4807 // rate, or a rate above the estimate it started from.
4808 bool finite = true;
4809 for (int64 s = -100; s < 200000 && finite; s += 37) {
4810 double v = MinerReportedHashRate(&decayed, t0 + s);
4811 finite = !std::isnan(v) && !std::isinf(v) && v >= 0.0 && v <= base;
4812 }
4813 check(finite,
4814 "hashrate decay: stays finite, non-negative and non-amplifying across a long silence");
4815 }
4816
4584 gCurrentJob = savedJob; 4817 gCurrentJob = savedJob;
4585 gHaveJob = savedHaveJob; 4818 gHaveJob = savedHaveJob;
4586 4819
@@ -4608,6 +4841,10 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4608 if (m->authorised) gStats.authorizedMiners++; 4841 if (m->authorised) gStats.authorizedMiners++;
4609 4842
4610 gWorkerStats.clear(); 4843 gWorkerStats.clear();
4844 // One timestamp for the whole sweep (#7): every miner is faded to the
4845 // same instant, so the per-worker rows and the total they are summed
4846 // into describe one moment rather than a smear across the loop.
4847 int64 nowStats = GetTime();
4611 for (Miner* m : gMiners) { 4848 for (Miner* m : gMiners) {
4612 if (!m->authorised || m->lastSeen <= 0) continue; 4849 if (!m->authorised || m->lastSeen <= 0) continue;
4613 PoolWorkerStatView v; 4850 PoolWorkerStatView v;
@@ -4616,9 +4853,11 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4616 v.roundShares = m->roundShares; 4853 v.roundShares = m->roundShares;
4617 v.totalShares = m->totalShares; 4854 v.totalShares = m->totalShares;
4618 v.lastSeen = m->lastSeen; 4855 v.lastSeen = m->lastSeen;
4619 v.hashRate = m->hashRate; 4856 // Not m->hashRate: a miner that has stopped submitting shares must
4857 // not keep contributing its last sample to the announced total.
4858 v.hashRate = MinerReportedHashRate(m, nowStats);
4620 gWorkerStats.push_back(v); 4859 gWorkerStats.push_back(v);
4621 gStats.totalHashRate += m->hashRate; 4860 gStats.totalHashRate += v.hashRate;
4622 } 4861 }
4623 std::sort(gWorkerStats.begin(), gWorkerStats.end(), 4862 std::sort(gWorkerStats.begin(), gWorkerStats.end(),
4624 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { 4863 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) {
@@ -4851,6 +5090,35 @@ static void PoolEventLoop()
4851 } 5090 }
4852 } 5091 }
4853 5092
5093 // 3.6. Idle reap (#6). Mirror the clean-disconnect path exactly --
5094 // log, close, erase from gMiners via std::find, delete -- just with
5095 // a different reason. Shares the miner already earned stay in
5096 // roundShareCount, exactly as they do when a miner cleanly
5097 // disconnects mid-round. connectTime stands in for lastSeen on a
5098 // connection that never completed a line. Erasing inside the loop
5099 // above would invalidate the iteration, so collect then reap.
5100 {
5101 int64 now = GetTime();
5102 std::vector<Miner*> idle;
5103 for (Miner* m : gMiners) {
5104 int64 last = (m->lastSeen > 0) ? m->lastSeen : m->connectTime;
5105 if (now - last > POOL_IDLE_TIMEOUT_SECS)
5106 idle.push_back(m);
5107 }
5108 for (Miner* m : idle) {
5109 LogPrint("worker", "[pool] dropping idle miner %s -- nothing"
5110 " received in over %llds (session shares: %llu,"
5111 " round shares: %llu)\n",
5112 m->worker.empty() ? "(never authorized)" : m->worker.c_str(),
5113 (long long)POOL_IDLE_TIMEOUT_SECS,
5114 (unsigned long long)m->sessionShares,
5115 (unsigned long long)m->roundShares);
5116 sock_close(m->fd);
5117 gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m));
5118 delete m;
5119 }
5120 }
5121
4854 // 4. Select over miner sockets 5122 // 4. Select over miner sockets
4855 if (!gMiners.empty()) { 5123 if (!gMiners.empty()) {
4856 fd_set fds; FD_ZERO(&fds); 5124 fd_set fds; FD_ZERO(&fds);
@@ -4873,6 +5141,11 @@ static void PoolEventLoop()
4873 // terminate the whole node -- not just the pool. Every 5141 // terminate the whole node -- not just the pool. Every
4874 // line arrives from an unauthenticated socket. 5142 // line arrives from an unauthenticated socket.
4875 while (RecvLine(m->fd, m->readBuf, line, disc)) { 5143 while (RecvLine(m->fd, m->readBuf, line, disc)) {
5144 // Any complete line is proof of life (#6): authorize
5145 // and valid shares already stamp lastSeen themselves,
5146 // but a miner that only ever subscribes (or sends
5147 // unrecognised methods) must count as alive too.
5148 m->lastSeen = GetTime();
4876 try { 5149 try {
4877 HandleLine(m, line, roundShareCount, 5150 HandleLine(m, line, roundShareCount,
4878 roundShareTotal, blocksFoundThisSession); 5151 roundShareTotal, blocksFoundThisSession);