diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 194 |
1 files changed, 160 insertions, 34 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp index df7ca05..9d98a4d 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -2488,21 +2488,26 @@ static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by thi | |||
| 2488 | // that stops hashing keeps its last estimate forever and RefreshStats sums | 2488 | // that stops hashing keeps its last estimate forever and RefreshStats sums |
| 2489 | // it into the announced pool total for as long as the connection sits open | 2489 | // it into the announced pool total for as long as the connection sits open |
| 2490 | // -- the published pool hashrate ran several times over ground truth | 2490 | // -- the published pool hashrate ran several times over ground truth |
| 2491 | // because of exactly this. The decay pass in PoolEventLoop applies this | 2491 | // because of exactly this. MinerReportedHashRate() below applies the |
| 2492 | // policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share | 2492 | // policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share |
| 2493 | // intervals (expected interval = expected hashes per share at its current | 2493 | // intervals (expected interval = expected hashes per share at its |
| 2494 | // difficulty / its last rate estimate) with no share, then its estimate | 2494 | // difficulty, divided by the estimate itself) with no share, after which |
| 2495 | // decays with a half-life of one more such interval, computed from | 2495 | // the figure we report falls off with a half-life of one more such |
| 2496 | // absolute time so the result does not depend on how often the loop ticks. | 2496 | // interval. The interval is clamped to [HASHRATE_MIN_INTERVAL, |
| 2497 | // The interval is clamped to [HASHRATE_MIN_INTERVAL, HASHRATE_MAX_INTERVAL] | 2497 | // HASHRATE_MAX_INTERVAL] so a fast miner at the vardiff floor (expected |
| 2498 | // so a fast miner at the vardiff floor (expected interval of milliseconds) | 2498 | // interval of milliseconds) is not punished for ordinary share-delivery |
| 2499 | // is not punished for ordinary share-delivery jitter, and a very slow | 2499 | // jitter, and a very slow miner's grace does not stretch past the point |
| 2500 | // miner's grace does not stretch past the point where its estimate means | 2500 | // where its estimate means anything. At the vardiff target of one share |
| 2501 | // anything. At the vardiff target of one share per 15s, a stopped miner's | 2501 | // per 15s that is a minute of grace, half the rate reported 75s in, and |
| 2502 | // announced rate is gone within minutes. | 2502 | // nothing reported after about seventeen minutes of silence. |
| 2503 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals | 2503 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals |
| 2504 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval | 2504 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval |
| 2505 | static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval | 2505 | static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval |
| 2506 | // Half-lives past the grace beyond which we report a flat zero. 2^-64 of the | ||
| 2507 | // original estimate is not a number anyone needs, and stopping here keeps | ||
| 2508 | // pow() from walking down into the denormals for a miner that has been quiet | ||
| 2509 | // for hours. | ||
| 2510 | static const double HASHRATE_DECAY_CUTOFF = 64.0; | ||
| 2506 | 2511 | ||
| 2507 | struct Miner { | 2512 | struct Miner { |
| 2508 | btf_socket_t fd; | 2513 | btf_socket_t fd; |
| @@ -2546,6 +2551,65 @@ struct Miner { | |||
| 2546 | extranonce2(gNextExtranonce2.fetch_add(1)) {} | 2551 | extranonce2(gNextExtranonce2.fetch_add(1)) {} |
| 2547 | }; | 2552 | }; |
| 2548 | 2553 | ||
| 2554 | // What we publish as a miner's H/s: the last EWMA sample, faded out if shares | ||
| 2555 | // have stopped arriving (#7, policy at HASHRATE_IDLE_MISSES). | ||
| 2556 | // | ||
| 2557 | // This reads m->hashRate and never writes it. That is the whole design. The | ||
| 2558 | // first attempt decayed the stored estimate in place from the event loop's | ||
| 2559 | // sweep, which put a running accumulator behind a figure that is only ever | ||
| 2560 | // displayed, and the accumulator misbehaved in two ways: | ||
| 2561 | // | ||
| 2562 | // - `expected` was recomputed from the already-decayed estimate on every | ||
| 2563 | // pass, so each decay raised `expected`, which raised `grace`, which is | ||
| 2564 | // the very thing gating the next decay. `grace` simply chased `silent` | ||
| 2565 | // upward and the reported rate settled into a 1/t crawl instead of the | ||
| 2566 | // half-life curve the comment above promises -- 291x too high at ten | ||
| 2567 | // half-lives past the grace. Issue #7 is about the pool over-reporting; | ||
| 2568 | // that under-corrected it for hours. | ||
| 2569 | // - Once `expected` reached HASHRATE_MAX_INTERVAL it could not rise any | ||
| 2570 | // further, the feedback that had been gating each pass was gone, and the | ||
| 2571 | // decay began compounding once a second against the total elapsed | ||
| 2572 | // silence -- taking a miner from 0.4% of its baseline to 1e-11 of it in | ||
| 2573 | // the next hundred seconds, four hours in, for no reason a reader of | ||
| 2574 | // either the code or the announced number could see. | ||
| 2575 | // | ||
| 2576 | // Computing the value instead of accumulating it removes the failure mode | ||
| 2577 | // rather than tuning it: with no stored decay state there is nothing for a | ||
| 2578 | // tick rate, a missed pass or a duplicated pass to perturb, and the answer | ||
| 2579 | // depends only on how long the miner has actually been quiet. It also keeps | ||
| 2580 | // the fade out of the estimator -- the EWMA on the share path still blends | ||
| 2581 | // against the value it last computed, not against a faded copy of it, so a | ||
| 2582 | // miner that resumes recovers on exactly the curve it would have without any | ||
| 2583 | // of this. | ||
| 2584 | // | ||
| 2585 | // `now` is a parameter rather than a GetTime() call so that one refresh fades | ||
| 2586 | // every miner to the same instant, and so the policy is directly testable. | ||
| 2587 | static double MinerReportedHashRate(const Miner* m, int64 now) | ||
| 2588 | { | ||
| 2589 | // Nothing sampled yet: a fresh connection, or one that has found a single | ||
| 2590 | // share and so has no interval to have been timed over. Nothing to fade, | ||
| 2591 | // and nothing to divide by below. | ||
| 2592 | if (m->hashRate <= 0.0 || m->lastShareTime <= 0) return m->hashRate; | ||
| 2593 | |||
| 2594 | double expected = ExpectedHashesForDifficulty(m->difficulty) / m->hashRate; | ||
| 2595 | // Written so that a NaN would land on the floor rather than sail through: | ||
| 2596 | // the clamp is what guarantees `expected` is finite and strictly positive | ||
| 2597 | // below, whatever the division produced. | ||
| 2598 | if (!(expected > HASHRATE_MIN_INTERVAL)) expected = HASHRATE_MIN_INTERVAL; | ||
| 2599 | if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL; | ||
| 2600 | |||
| 2601 | double silent = (double)(now - m->lastShareTime); | ||
| 2602 | double grace = HASHRATE_IDLE_MISSES * expected; | ||
| 2603 | // grace is at least HASHRATE_IDLE_MISSES seconds, so a clock that has | ||
| 2604 | // stepped backwards gives a negative `silent`, takes this branch, and | ||
| 2605 | // reports the sample unchanged. Nothing here can scale an estimate up. | ||
| 2606 | if (!(silent > grace)) return m->hashRate; | ||
| 2607 | |||
| 2608 | double halflives = (silent - grace) / expected; | ||
| 2609 | if (halflives >= HASHRATE_DECAY_CUTOFF) return 0.0; | ||
| 2610 | return m->hashRate * pow(0.5, halflives); | ||
| 2611 | } | ||
| 2612 | |||
| 2549 | static std::vector<Miner*> gMiners; | 2613 | static std::vector<Miner*> gMiners; |
| 2550 | 2614 | ||
| 2551 | struct FoundBlock { int height; int64 ts; std::string addr; }; | 2615 | struct FoundBlock { int height; int64 ts; std::string addr; }; |
| @@ -4672,6 +4736,82 @@ int RunPoolStratumSelfTest() | |||
| 4672 | strPoolRoundsFile = savedRoundsFile; | 4736 | strPoolRoundsFile = savedRoundsFile; |
| 4673 | gPoolRounds = savedRounds; | 4737 | gPoolRounds = savedRounds; |
| 4674 | 4738 | ||
| 4739 | // -- hashrate decay (#7) -------------------------------------------------- | ||
| 4740 | // The first version of this decayed m->hashRate in place from the event | ||
| 4741 | // loop's sweep, which made the reported figure a function of how the loop | ||
| 4742 | // happened to run rather than of how long the miner had been quiet. These | ||
| 4743 | // pin the properties that fixes: sampling changes nothing, and the answer | ||
| 4744 | // depends only on elapsed silence. | ||
| 4745 | { | ||
| 4746 | // A miner exactly on the vardiff target: 4096 hashes per share at | ||
| 4747 | // difficulty 1.0, one share per 15s, so 4096/15 H/s. Its expected | ||
| 4748 | // interval is 15s and its grace is 60s. | ||
| 4749 | const double base = 4096.0 / 15.0; | ||
| 4750 | const int64 t0 = 1766000000; // any plausible non-zero wall clock | ||
| 4751 | Miner decayed(INVALID_SOCKET); | ||
| 4752 | decayed.difficulty = 1.0; | ||
| 4753 | decayed.hashRate = base; | ||
| 4754 | decayed.lastShareTime = t0; | ||
| 4755 | |||
| 4756 | check(MinerReportedHashRate(&decayed, t0 + 60) == base, | ||
| 4757 | "hashrate decay: a miner inside its grace period reports its full estimate"); | ||
| 4758 | |||
| 4759 | // Sampling must not consume the estimate. Twenty passes is one second | ||
| 4760 | // of the event loop's old sweep; a thousand is nearly a minute of it. | ||
| 4761 | double once = MinerReportedHashRate(&decayed, t0 + 61); | ||
| 4762 | bool idempotent = (decayed.hashRate == base); | ||
| 4763 | for (int i = 0; i < 1000 && idempotent; i++) | ||
| 4764 | idempotent = (MinerReportedHashRate(&decayed, t0 + 61) == once) && | ||
| 4765 | (decayed.hashRate == base); | ||
| 4766 | check(idempotent, | ||
| 4767 | "hashrate decay: repeated sampling returns one value and leaves the estimate alone"); | ||
| 4768 | |||
| 4769 | // One second past a 60s grace is 1/15th of a half-life: ~4.5% off, not | ||
| 4770 | // the estimate gone. A miner at the vardiff target overshoots its grace | ||
| 4771 | // roughly 1.8% of the time (e^-4) purely by share-timing variance, and | ||
| 4772 | // must not be zeroed for it. | ||
| 4773 | check(once > 0.94 * base && once < 0.96 * base, | ||
| 4774 | "hashrate decay: one second past grace costs a few percent, not the estimate"); | ||
| 4775 | double three = MinerReportedHashRate(&decayed, t0 + 63); | ||
| 4776 | check(three > 0.86 * base && three < 0.88 * base, | ||
| 4777 | "hashrate decay: three seconds past grace costs ~13%"); | ||
| 4778 | |||
| 4779 | // One half-life past the grace is exactly half. | ||
| 4780 | double half = MinerReportedHashRate(&decayed, t0 + 75); | ||
| 4781 | check(half > 0.499 * base && half < 0.501 * base, | ||
| 4782 | "hashrate decay: one expected interval past grace halves the estimate"); | ||
| 4783 | |||
| 4784 | // A miner that has genuinely stopped ends up reported as nothing -- | ||
| 4785 | // the whole point of #7. The old in-place version could not get here: | ||
| 4786 | // its decay fed back into its own grace and settled into a 1/t crawl. | ||
| 4787 | check(MinerReportedHashRate(&decayed, t0 + 3600) == 0.0, | ||
| 4788 | "hashrate decay: a miner silent for an hour is reported as zero"); | ||
| 4789 | |||
| 4790 | // A clock that steps backwards must never inflate what we announce. | ||
| 4791 | check(MinerReportedHashRate(&decayed, t0 - 3600) == base, | ||
| 4792 | "hashrate decay: a backwards clock reports the estimate unchanged, never higher"); | ||
| 4793 | |||
| 4794 | // Fresh connection, and a connection with one share and so no interval | ||
| 4795 | // to have timed: both must report nothing rather than divide by zero. | ||
| 4796 | Miner fresh(INVALID_SOCKET); | ||
| 4797 | check(MinerReportedHashRate(&fresh, t0) == 0.0, | ||
| 4798 | "hashrate decay: a miner that never submitted a share reports zero"); | ||
| 4799 | Miner single(INVALID_SOCKET); | ||
| 4800 | single.lastShareTime = t0; | ||
| 4801 | check(MinerReportedHashRate(&single, t0 + 86400) == 0.0, | ||
| 4802 | "hashrate decay: a miner with no rate sample yet reports zero"); | ||
| 4803 | |||
| 4804 | // Nothing in the swept range may produce a NaN, an infinity, a negative | ||
| 4805 | // rate, or a rate above the estimate it started from. | ||
| 4806 | bool finite = true; | ||
| 4807 | for (int64 s = -100; s < 200000 && finite; s += 37) { | ||
| 4808 | double v = MinerReportedHashRate(&decayed, t0 + s); | ||
| 4809 | finite = !std::isnan(v) && !std::isinf(v) && v >= 0.0 && v <= base; | ||
| 4810 | } | ||
| 4811 | check(finite, | ||
| 4812 | "hashrate decay: stays finite, non-negative and non-amplifying across a long silence"); | ||
| 4813 | } | ||
| 4814 | |||
| 4675 | gCurrentJob = savedJob; | 4815 | gCurrentJob = savedJob; |
| 4676 | gHaveJob = savedHaveJob; | 4816 | gHaveJob = savedHaveJob; |
| 4677 | 4817 | ||
| @@ -4699,6 +4839,10 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) | |||
| 4699 | if (m->authorised) gStats.authorizedMiners++; | 4839 | if (m->authorised) gStats.authorizedMiners++; |
| 4700 | 4840 | ||
| 4701 | gWorkerStats.clear(); | 4841 | gWorkerStats.clear(); |
| 4842 | // One timestamp for the whole sweep (#7): every miner is faded to the | ||
| 4843 | // same instant, so the per-worker rows and the total they are summed | ||
| 4844 | // into describe one moment rather than a smear across the loop. | ||
| 4845 | int64 nowStats = GetTime(); | ||
| 4702 | for (Miner* m : gMiners) { | 4846 | for (Miner* m : gMiners) { |
| 4703 | if (!m->authorised || m->lastSeen <= 0) continue; | 4847 | if (!m->authorised || m->lastSeen <= 0) continue; |
| 4704 | PoolWorkerStatView v; | 4848 | PoolWorkerStatView v; |
| @@ -4707,9 +4851,11 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal) | |||
| 4707 | v.roundShares = m->roundShares; | 4851 | v.roundShares = m->roundShares; |
| 4708 | v.totalShares = m->totalShares; | 4852 | v.totalShares = m->totalShares; |
| 4709 | v.lastSeen = m->lastSeen; | 4853 | v.lastSeen = m->lastSeen; |
| 4710 | v.hashRate = m->hashRate; | 4854 | // Not m->hashRate: a miner that has stopped submitting shares must |
| 4855 | // not keep contributing its last sample to the announced total. | ||
| 4856 | v.hashRate = MinerReportedHashRate(m, nowStats); | ||
| 4711 | gWorkerStats.push_back(v); | 4857 | gWorkerStats.push_back(v); |
| 4712 | gStats.totalHashRate += m->hashRate; | 4858 | gStats.totalHashRate += v.hashRate; |
| 4713 | } | 4859 | } |
| 4714 | std::sort(gWorkerStats.begin(), gWorkerStats.end(), | 4860 | std::sort(gWorkerStats.begin(), gWorkerStats.end(), |
| 4715 | [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { | 4861 | [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { |
| @@ -4942,27 +5088,7 @@ static void PoolEventLoop() | |||
| 4942 | } | 5088 | } |
| 4943 | } | 5089 | } |
| 4944 | 5090 | ||
| 4945 | // 3.6. Hashrate decay (#7) -- see the policy comment at | 5091 | // 3.6. Idle reap (#6). Mirror the clean-disconnect path exactly -- |
| 4946 | // HASHRATE_IDLE_MISSES. The EWMA is only re-sampled on share arrival, | ||
| 4947 | // so without this pass a miner that stops hashing keeps its last | ||
| 4948 | // estimate forever and the summed pool hashrate RefreshStats (and | ||
| 4949 | // the nostr announcement built from it) publishes is fiction. | ||
| 4950 | for (Miner* m : gMiners) { | ||
| 4951 | if (m->hashRate <= 0.0 || m->lastShareTime <= 0) continue; | ||
| 4952 | double expected = ExpectedHashesForDifficulty(m->difficulty) / | ||
| 4953 | std::max(m->hashRate, 1e-9); | ||
| 4954 | if (expected < HASHRATE_MIN_INTERVAL) expected = HASHRATE_MIN_INTERVAL; | ||
| 4955 | if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL; | ||
| 4956 | double silent = (double)(GetTime() - m->lastShareTime); | ||
| 4957 | double grace = HASHRATE_IDLE_MISSES * expected; | ||
| 4958 | if (silent <= grace) continue; | ||
| 4959 | // Half-life of one expected interval past the grace period. | ||
| 4960 | // Written off absolute time, not per-tick, so the decay curve is | ||
| 4961 | // the same however often this loop runs. | ||
| 4962 | m->hashRate *= pow(0.5, (silent - grace) / expected); | ||
| 4963 | } | ||
| 4964 | |||
| 4965 | // 3.7. Idle reap (#6). Mirror the clean-disconnect path exactly -- | ||
| 4966 | // log, close, erase from gMiners via std::find, delete -- just with | 5092 | // log, close, erase from gMiners via std::find, delete -- just with |
| 4967 | // a different reason. Shares the miner already earned stay in | 5093 | // a different reason. Shares the miner already earned stay in |
| 4968 | // roundShareCount, exactly as they do when a miner cleanly | 5094 | // roundShareCount, exactly as they do when a miner cleanly |