upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 05:46:57 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-02 05:46:57 +0000
commit2bca62a6a9984297633af42d9e19d1f328d2f7f2 (patch)
tree4c0907cb08dca81d17e0eacac171ba120cb3b6ef
parenta3666bded1553b101b631d87e99961bf3ccd0a3e (diff)
pool: decay a miner's hashrate estimate when its shares stop arriving
m->hashRate is an EWMA re-sampled only on the valid-share path, with no time-based term: a miner that stops hashing keeps its last estimate for as long as the connection stays open, and RefreshStats sums every such stale estimate into gStats.totalHashRate, which is what the pool status and the nostr announcement publish. The announced pool hashrate ran several times over ground truth because miners that had gone quiet were still contributing their last sample. Decay on time in the event loop's miner sweep. Per miner, compute the expected share interval -- ExpectedHashesForDifficulty(m->difficulty) divided by its current rate estimate -- and give it a grace of four such intervals with no share (at the vardiff target of one share per 15s that is about a minute of silence). Past the grace, decay the estimate with a half-life of one more expected interval, computed from absolute time so the curve does not depend on the loop's tick rate. The interval is clamped to [1s, 1h] so a fast miner at the vardiff floor, whose expected interval is milliseconds, is not decayed for ordinary share-delivery jitter, and a very slow miner's grace cannot stretch past the point where its estimate means anything. Per-share crediting and ExpectedHashesForDifficulty are untouched: this changes only what is reported about a miner that has stopped reporting itself. Closes #7
-rw-r--r--src/rpc.cpp40
1 files changed, 40 insertions, 0 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index f1a7153..23e25ef 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -2484,6 +2484,26 @@ static const double VARDIFF_MAX = 1e12;
2484static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance 2484static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance
2485static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much 2485static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much
2486 2486
2487// #7: m->hashRate is an EWMA sampled only when a share arrives, so a miner
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
2490// -- the published pool hashrate ran several times over ground truth
2491// because of exactly this. The decay pass in PoolEventLoop applies this
2492// policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share
2493// intervals (expected interval = expected hashes per share at its current
2494// difficulty / its last rate estimate) with no share, then its estimate
2495// decays with a half-life of one more such interval, computed from
2496// absolute time so the result does not depend on how often the loop ticks.
2497// The interval is clamped to [HASHRATE_MIN_INTERVAL, HASHRATE_MAX_INTERVAL]
2498// so a fast miner at the vardiff floor (expected interval of milliseconds)
2499// is not punished for ordinary share-delivery jitter, and a very slow
2500// miner's grace does not stretch past the point where its estimate means
2501// anything. At the vardiff target of one share per 15s, a stopped miner's
2502// announced rate is gone within minutes.
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
2505static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval
2506
2487struct Miner { 2507struct Miner {
2488 btf_socket_t fd; 2508 btf_socket_t fd;
2489 bool authorised = false; 2509 bool authorised = false;
@@ -4900,6 +4920,26 @@ static void PoolEventLoop()
4900 } 4920 }
4901 } 4921 }
4902 4922
4923 // 3.6. Hashrate decay (#7) -- see the policy comment at
4924 // HASHRATE_IDLE_MISSES. The EWMA is only re-sampled on share arrival,
4925 // so without this pass a miner that stops hashing keeps its last
4926 // estimate forever and the summed pool hashrate RefreshStats (and
4927 // the nostr announcement built from it) publishes is fiction.
4928 for (Miner* m : gMiners) {
4929 if (m->hashRate <= 0.0 || m->lastShareTime <= 0) continue;
4930 double expected = ExpectedHashesForDifficulty(m->difficulty) /
4931 std::max(m->hashRate, 1e-9);
4932 if (expected < HASHRATE_MIN_INTERVAL) expected = HASHRATE_MIN_INTERVAL;
4933 if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL;
4934 double silent = (double)(GetTime() - m->lastShareTime);
4935 double grace = HASHRATE_IDLE_MISSES * expected;
4936 if (silent <= grace) continue;
4937 // Half-life of one expected interval past the grace period.
4938 // Written off absolute time, not per-tick, so the decay curve is
4939 // the same however often this loop runs.
4940 m->hashRate *= pow(0.5, (silent - grace) / expected);
4941 }
4942
4903 // 4. Select over miner sockets 4943 // 4. Select over miner sockets
4904 if (!gMiners.empty()) { 4944 if (!gMiners.empty()) {
4905 fd_set fds; FD_ZERO(&fds); 4945 fd_set fds; FD_ZERO(&fds);