diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 05:46:57 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-02 05:46:57 +0000 |
| commit | 2bca62a6a9984297633af42d9e19d1f328d2f7f2 (patch) | |
| tree | 4c0907cb08dca81d17e0eacac171ba120cb3b6ef /src/rpc.cpp | |
| parent | a3666bded1553b101b631d87e99961bf3ccd0a3e (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
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 40 |
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; | |||
| 2484 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance | 2484 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance |
| 2485 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much | 2485 | static 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. | ||
| 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 | ||
| 2505 | static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval | ||
| 2506 | |||
| 2487 | struct Miner { | 2507 | struct 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); |