diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-03 21:49:35 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-03 21:49:35 +0000 |
| commit | 4b3069f2de140cda427627d470f2751609be6620 (patch) | |
| tree | db45b40a2c0516a2fcb439ffaca6e993da74009a /src | |
| parent | ea8fe94658bf20d2b669b619231238f12e6bd8fc (diff) | |
| parent | 07051b2378734d62fb94a4a62819d8150452b661 (diff) | |
merge: !3 fix/pool-accounting into deploy/all-2026-08-24
Diffstat (limited to 'src')
| -rw-r--r-- | src/rpc.cpp | 285 |
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; | |||
| 2473 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance | 2486 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance |
| 2474 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much | 2487 | static 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. | ||
| 2505 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals | ||
| 2506 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval | ||
| 2507 | static 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. | ||
| 2512 | static const double HASHRATE_DECAY_CUTOFF = 64.0; | ||
| 2513 | |||
| 2476 | struct Miner { | 2514 | struct 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. | ||
| 2589 | static 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 | |||
| 2508 | static std::vector<Miner*> gMiners; | 2615 | static std::vector<Miner*> gMiners; |
| 2509 | 2616 | ||
| 2510 | struct FoundBlock { int height; int64 ts; std::string addr; }; | 2617 | struct 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 | |||
| 2598 | static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not | 2717 | static 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. | ||
| 2729 | static 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, | |||
| 2682 | static void SendJob(Miner* m, bool clean) | 2811 | static 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); |