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:
Diffstat (limited to 'src/rpc.cpp')
-rw-r--r--src/rpc.cpp194
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.
2503static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals 2503static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals
2504static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval 2504static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval
2505static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval 2505static 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.
2510static const double HASHRATE_DECAY_CUTOFF = 64.0;
2506 2511
2507struct Miner { 2512struct 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.
2587static 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
2549static std::vector<Miner*> gMiners; 2613static std::vector<Miner*> gMiners;
2550 2614
2551struct FoundBlock { int height; int64 ts; std::string addr; }; 2615struct 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