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.cpp375
1 files changed, 350 insertions, 25 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index cc3fffa..579c01c 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,
@@ -1755,7 +1768,9 @@ static void FlushMaturePayouts()
1755 } 1768 }
1756 int64 total = 0; 1769 int64 total = 0;
1757 for (auto& kv : it->amounts) total += kv.second; 1770 for (auto& kv : it->amounts) total += kv.second;
1758 LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" 1771 // Always: one line per voided entry, never a collapsed summary.
1772 // A reorg voids these in a burst from this one source line.
1773 LogPrintAlways("payout", "[payout] block %d (%s) was orphaned -- voiding %zu"
1759 " payment(s) totalling %s\n", 1774 " payment(s) totalling %s\n",
1760 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), 1775 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(),
1761 it->amounts.size(), FormatMoney(total).c_str()); 1776 it->amounts.size(), FormatMoney(total).c_str());
@@ -1954,7 +1969,7 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1954// disagreeing. 1969// disagreeing.
1955static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) 1970static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason)
1956{ 1971{
1957 LogPrint("payout", "[payout] writing off %s owed to %s for block %d: %s\n", 1972 LogPrintAlways("payout", "[payout] writing off %s owed to %s for block %d: %s\n",
1958 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight, 1973 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight,
1959 reason.c_str()); 1974 reason.c_str());
1960 VersionedBlob blob; 1975 VersionedBlob blob;
@@ -2059,7 +2074,7 @@ static void PayoutThreadFn(void*)
2059 // funding block can land at any point in that window. Upstream has 2074 // funding block can land at any point in that window. Upstream has
2060 // no equivalent -- it decides once, when the entry matures. 2075 // no equivalent -- it decides once, when the entry matures.
2061 if (!PayoutBlockStillOnChain(r.blockHash)) { 2076 if (!PayoutBlockStillOnChain(r.blockHash)) {
2062 LogPrint("payout", "[payout] block %d (%s) orphaned while queued" 2077 LogPrintAlways("payout", "[payout] block %d (%s) orphaned while queued"
2063 " -- dropping %s to %s\n", 2078 " -- dropping %s to %s\n",
2064 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), 2079 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(),
2065 FormatMoney(r.amount).c_str(), r.address.c_str()); 2080 FormatMoney(r.amount).c_str(), r.address.c_str());
@@ -2103,7 +2118,7 @@ static void PayoutThreadFn(void*)
2103 const std::string reason = 2118 const std::string reason =
2104 strprintf("'%s' is not a valid address, so no transaction" 2119 strprintf("'%s' is not a valid address, so no transaction"
2105 " can pay it", r.address.c_str()); 2120 " can pay it", r.address.c_str());
2106 LogPrint("payout", "[payout] SKIPPED bad address '%s' --" 2121 LogPrintAlways("payout", "[payout] SKIPPED bad address '%s' --"
2107 " %s owed for block %d cannot be paid\n", 2122 " %s owed for block %d cannot be paid\n",
2108 r.address.c_str(), FormatMoney(r.amount).c_str(), 2123 r.address.c_str(), FormatMoney(r.amount).c_str(),
2109 r.blockHeight); 2124 r.blockHeight);
@@ -2228,7 +2243,7 @@ static void PayoutThreadFn(void*)
2228 // described by at least one of them at every instant. 2243 // described by at least one of them at every instant.
2229 RecordPayoutTxid(r, wtx.GetHash().GetHex()); 2244 RecordPayoutTxid(r, wtx.GetHash().GetHex());
2230 ForgetPayment(r); 2245 ForgetPayment(r);
2231 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", 2246 LogPrintAlways("payout", "[payout] paid %s to %s (tx %s)\n",
2232 FormatMoney(r.amount).c_str(), r.address.c_str(), 2247 FormatMoney(r.amount).c_str(), r.address.c_str(),
2233 wtx.GetHash().ToString().substr(0, 12).c_str()); 2248 wtx.GetHash().ToString().substr(0, 12).c_str());
2234 } else { 2249 } else {
@@ -2286,7 +2301,7 @@ static void PayoutThreadFn(void*)
2286 // going away must not turn the log into one line a minute 2301 // going away must not turn the log into one line a minute
2287 // forever, and must not go quiet either. 2302 // forever, and must not go quiet either.
2288 if (r.retries <= 5 || r.retries % 60 == 0) 2303 if (r.retries <= 5 || r.retries % 60 == 0)
2289 LogPrint("payout", "[payout] FAILED to pay %s to %s" 2304 LogPrintAlways("payout", "[payout] FAILED to pay %s to %s"
2290 " (%s spendable, wallet unlocked, block %d on" 2305 " (%s spendable, wallet unlocked, block %d on"
2291 " the main chain) -- attempt %d, retrying in" 2306 " the main chain) -- attempt %d, retrying in"
2292 " 60s; this debt stands until it is paid\n", 2307 " 60s; this debt stands until it is paid\n",
@@ -2494,6 +2509,31 @@ static const double VARDIFF_MAX = 1e12;
2494static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance 2509static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance
2495static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much 2510static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much
2496 2511
2512// #7: m->hashRate is an EWMA sampled only when a share arrives, so a miner
2513// that stops hashing keeps its last estimate forever and RefreshStats sums
2514// it into the announced pool total for as long as the connection sits open
2515// -- the published pool hashrate ran several times over ground truth
2516// because of exactly this. MinerReportedHashRate() below applies the
2517// policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share
2518// intervals (expected interval = expected hashes per share at its
2519// difficulty, divided by the estimate itself) with no share, after which
2520// the figure we report falls off with a half-life of one more such
2521// interval. The interval is clamped to [HASHRATE_MIN_INTERVAL,
2522// HASHRATE_MAX_INTERVAL] so a fast miner at the vardiff floor (expected
2523// interval of milliseconds) is not punished for ordinary share-delivery
2524// jitter, and a very slow miner's grace does not stretch past the point
2525// where its estimate means anything. At the vardiff target of one share
2526// per 15s that is a minute of grace, half the rate reported 75s in, and
2527// nothing reported after about seventeen minutes of silence.
2528static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals
2529static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval
2530static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval
2531// Half-lives past the grace beyond which we report a flat zero. 2^-64 of the
2532// original estimate is not a number anyone needs, and stopping here keeps
2533// pow() from walking down into the denormals for a miner that has been quiet
2534// for hours.
2535static const double HASHRATE_DECAY_CUTOFF = 64.0;
2536
2497struct Miner { 2537struct Miner {
2498 btf_socket_t fd; 2538 btf_socket_t fd;
2499 bool authorised = false; 2539 bool authorised = false;
@@ -2521,11 +2561,80 @@ struct Miner {
2521 int64 lastShareTime = 0; // GetTime() of the previous accepted share 2561 int64 lastShareTime = 0; // GetTime() of the previous accepted share
2522 double hashRate = 0.0; // smoothed H/s estimate 2562 double hashRate = 0.0; // smoothed H/s estimate
2523 2563
2564 // Replay guard for mining.submit (#8): nonces already credited on this
2565 // connection for the job it is currently working. Keyed to the job id in
2566 // seenNoncesJob and cleared by SendJob whenever that id changes, so a
2567 // resubmission under a NEW job is never turned away, while a replayed
2568 // (job_id, nonce) pair under the same job is refused before it can be
2569 // credited twice into roundShareCount -- the figure the payout split
2570 // divides by.
2571 std::set<uint32_t> seenNonces;
2572 std::string seenNoncesJob;
2573
2524 explicit Miner(btf_socket_t f) 2574 explicit Miner(btf_socket_t f)
2525 : fd(f), connectTime(GetTime()), 2575 : fd(f), connectTime(GetTime()),
2526 extranonce2(gNextExtranonce2.fetch_add(1)) {} 2576 extranonce2(gNextExtranonce2.fetch_add(1)) {}
2527}; 2577};
2528 2578
2579// What we publish as a miner's H/s: the last EWMA sample, faded out if shares
2580// have stopped arriving (#7, policy at HASHRATE_IDLE_MISSES).
2581//
2582// This reads m->hashRate and never writes it. That is the whole design. The
2583// first attempt decayed the stored estimate in place from the event loop's
2584// sweep, which put a running accumulator behind a figure that is only ever
2585// displayed, and the accumulator misbehaved in two ways:
2586//
2587// - `expected` was recomputed from the already-decayed estimate on every
2588// pass, so each decay raised `expected`, which raised `grace`, which is
2589// the very thing gating the next decay. `grace` simply chased `silent`
2590// upward and the reported rate settled into a 1/t crawl instead of the
2591// half-life curve the comment above promises -- 291x too high at ten
2592// half-lives past the grace. Issue #7 is about the pool over-reporting;
2593// that under-corrected it for hours.
2594// - Once `expected` reached HASHRATE_MAX_INTERVAL it could not rise any
2595// further, the feedback that had been gating each pass was gone, and the
2596// decay began compounding once a second against the total elapsed
2597// silence -- taking a miner from 0.4% of its baseline to 1e-11 of it in
2598// the next hundred seconds, four hours in, for no reason a reader of
2599// either the code or the announced number could see.
2600//
2601// Computing the value instead of accumulating it removes the failure mode
2602// rather than tuning it: with no stored decay state there is nothing for a
2603// tick rate, a missed pass or a duplicated pass to perturb, and the answer
2604// depends only on how long the miner has actually been quiet. It also keeps
2605// the fade out of the estimator -- the EWMA on the share path still blends
2606// against the value it last computed, not against a faded copy of it, so a
2607// miner that resumes recovers on exactly the curve it would have without any
2608// of this.
2609//
2610// `now` is a parameter rather than a GetTime() call so that one refresh fades
2611// every miner to the same instant, and so the policy is directly testable.
2612static double MinerReportedHashRate(const Miner* m, int64 now)
2613{
2614 // Nothing sampled yet: a fresh connection, or one that has found a single
2615 // share and so has no interval to have been timed over. Nothing to fade,
2616 // and nothing to divide by below.
2617 if (m->hashRate <= 0.0 || m->lastShareTime <= 0) return m->hashRate;
2618
2619 double expected = ExpectedHashesForDifficulty(m->difficulty) / m->hashRate;
2620 // Written so that a NaN would land on the floor rather than sail through:
2621 // the clamp is what guarantees `expected` is finite and strictly positive
2622 // below, whatever the division produced.
2623 if (!(expected > HASHRATE_MIN_INTERVAL)) expected = HASHRATE_MIN_INTERVAL;
2624 if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL;
2625
2626 double silent = (double)(now - m->lastShareTime);
2627 double grace = HASHRATE_IDLE_MISSES * expected;
2628 // grace is at least HASHRATE_IDLE_MISSES seconds, so a clock that has
2629 // stepped backwards gives a negative `silent`, takes this branch, and
2630 // reports the sample unchanged. Nothing here can scale an estimate up.
2631 if (!(silent > grace)) return m->hashRate;
2632
2633 double halflives = (silent - grace) / expected;
2634 if (halflives >= HASHRATE_DECAY_CUTOFF) return 0.0;
2635 return m->hashRate * pow(0.5, halflives);
2636}
2637
2529static std::vector<Miner*> gMiners; 2638static std::vector<Miner*> gMiners;
2530 2639
2531struct FoundBlock { int height; int64 ts; std::string addr; }; 2640struct FoundBlock { int height; int64 ts; std::string addr; };
@@ -2576,6 +2685,18 @@ static void LocalAcceptFn(void*)
2576 break; 2685 break;
2577 continue; 2686 continue;
2578 } 2687 }
2688 // #6: let the OS probe a peer that has gone silent, so a half-open
2689 // session eventually errors out of recv and lands in the event
2690 // loop's existing reap path instead of sitting in gMiners forever.
2691 // This is a backstop, not the mechanism: the default cadence before
2692 // the first probe is hours on most stacks, so the idle timeout in
2693 // PoolEventLoop fires long before keepalive does. Miners arriving
2694 // via a relay ride a loopback socket pair whose remote transport is
2695 // the rendezvous socket owned by the tunnel pump threads; keepalive
2696 // cannot reach across that, which is part of why the idle timeout
2697 // exists.
2698 int keepalive = 1;
2699 setsockopt(c, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive));
2579 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); 2700 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
2580 PushReady((btf_socket_t)c); 2701 PushReady((btf_socket_t)c);
2581 } 2702 }
@@ -2619,6 +2740,16 @@ static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it
2619static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not 2740static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not
2620 // authorized after this long is 2741 // authorized after this long is
2621 // idle, not merely starting up 2742 // idle, not merely starting up
2743// #6: a Miner used to leave gMiners only when RecvLine reported a clean
2744// disconnect. A half-open session (miner host died, NAT state expired,
2745// cable pulled) sends no FIN and reports no error, so it sat in the table
2746// forever -- authorize outpaced disconnect 2:1 on the live pool, and the
2747// table grew toward MAX_POOL_MINERS, past which real miners are refused.
2748// lastSeen is stamped on every complete line received; a miner that has
2749// said nothing for this long is gone in every way that matters. Ten
2750// minutes is far beyond the vardiff target (a share every 15s) and the
2751// stall watchdog (45s), so no miner that is actually hashing comes close.
2752static const int64 POOL_IDLE_TIMEOUT_SECS = 600;
2622 2753
2623// Non-blocking recv: pull whatever bytes are available now into buf, then 2754// Non-blocking recv: pull whatever bytes are available now into buf, then
2624// return one complete line if one is ready. Three outcomes: 2755// return one complete line if one is ready. Three outcomes:
@@ -2703,6 +2834,17 @@ static json MakeNotifyParams(const StratumJob& job, const CBlock& b,
2703static void SendJob(Miner* m, bool clean) 2834static void SendJob(Miner* m, bool clean)
2704{ 2835{
2705 if (!gHaveJob) return; 2836 if (!gHaveJob) return;
2837 // Every path that hands a miner a new job funnels through here, so this
2838 // is the one place the replay guard needs to follow the job id: nonces
2839 // credited under the previous job mean nothing under the new one. Keying
2840 // the clear to the id (rather than clearing unconditionally) keeps a
2841 // same-job re-notify -- the vardiff retarget and stall watchdog both
2842 // re-send the CURRENT job -- from reopening the duplicate window it
2843 // exists to close.
2844 if (m->seenNoncesJob != gCurrentJob.jobId) {
2845 m->seenNonces.clear();
2846 m->seenNoncesJob = gCurrentJob.jobId;
2847 }
2706 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); 2848 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2);
2707 // Use this miner's own shareTarget (set by vardiff) in the notify params. 2849 // Use this miner's own shareTarget (set by vardiff) in the notify params.
2708 // The participant miner reads p[3] as its share target, and HandleLine 2850 // The participant miner reads p[3] as its share target, and HandleLine
@@ -2961,6 +3103,22 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2961 } 3103 }
2962 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); 3104 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
2963 3105
3106 // Reject a replayed (job, nonce) before it costs a rebuild and a
3107 // RandomX hash: nothing about the work below distinguishes a second
3108 // submission of a valid share from the first, so without this check
3109 // it passes every test and is credited again -- and roundShareCount
3110 // is what the payout split divides by, so each replay multiplies the
3111 // address's cut of the round. The set is per-connection and cleared
3112 // by SendJob on every job change, so only a same-job repeat is
3113 // refused.
3114 if (m->seenNonces.count(nNonce)) {
3115 LogPrint("worker", "[worker->pool] duplicate share from %s"
3116 " nonce=%u job=%s -- already credited, not counting again\n",
3117 m->address.c_str(), nNonce, submitJobId.c_str());
3118 reply(false, "Duplicate share");
3119 return true;
3120 }
3121
2964 // Rebuild this miner's exact block (unique merkle via extranonce2) 3122 // Rebuild this miner's exact block (unique merkle via extranonce2)
2965 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); 3123 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2);
2966 b.nNonce = nNonce; 3124 b.nNonce = nNonce;
@@ -2988,6 +3146,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2988 m->roundShares++; 3146 m->roundShares++;
2989 m->totalShares++; 3147 m->totalShares++;
2990 m->vardiffShares++; 3148 m->vardiffShares++;
3149 m->seenNonces.insert(nNonce); // replay guard: credit each nonce once per job
2991 int64 shareTime = GetTime(); 3150 int64 shareTime = GetTime();
2992 m->lastSeen = shareTime; 3151 m->lastSeen = shareTime;
2993 roundShareCount[m->address]++; 3152 roundShareCount[m->address]++;
@@ -4602,6 +4761,82 @@ int RunPoolStratumSelfTest()
4602 strPoolRoundsFile = savedRoundsFile; 4761 strPoolRoundsFile = savedRoundsFile;
4603 gPoolRounds = savedRounds; 4762 gPoolRounds = savedRounds;
4604 4763
4764 // -- hashrate decay (#7) --------------------------------------------------
4765 // The first version of this decayed m->hashRate in place from the event
4766 // loop's sweep, which made the reported figure a function of how the loop
4767 // happened to run rather than of how long the miner had been quiet. These
4768 // pin the properties that fixes: sampling changes nothing, and the answer
4769 // depends only on elapsed silence.
4770 {
4771 // A miner exactly on the vardiff target: 4096 hashes per share at
4772 // difficulty 1.0, one share per 15s, so 4096/15 H/s. Its expected
4773 // interval is 15s and its grace is 60s.
4774 const double base = 4096.0 / 15.0;
4775 const int64 t0 = 1766000000; // any plausible non-zero wall clock
4776 Miner decayed(INVALID_SOCKET);
4777 decayed.difficulty = 1.0;
4778 decayed.hashRate = base;
4779 decayed.lastShareTime = t0;
4780
4781 check(MinerReportedHashRate(&decayed, t0 + 60) == base,
4782 "hashrate decay: a miner inside its grace period reports its full estimate");
4783
4784 // Sampling must not consume the estimate. Twenty passes is one second
4785 // of the event loop's old sweep; a thousand is nearly a minute of it.
4786 double once = MinerReportedHashRate(&decayed, t0 + 61);
4787 bool idempotent = (decayed.hashRate == base);
4788 for (int i = 0; i < 1000 && idempotent; i++)
4789 idempotent = (MinerReportedHashRate(&decayed, t0 + 61) == once) &&
4790 (decayed.hashRate == base);
4791 check(idempotent,
4792 "hashrate decay: repeated sampling returns one value and leaves the estimate alone");
4793
4794 // One second past a 60s grace is 1/15th of a half-life: ~4.5% off, not
4795 // the estimate gone. A miner at the vardiff target overshoots its grace
4796 // roughly 1.8% of the time (e^-4) purely by share-timing variance, and
4797 // must not be zeroed for it.
4798 check(once > 0.94 * base && once < 0.96 * base,
4799 "hashrate decay: one second past grace costs a few percent, not the estimate");
4800 double three = MinerReportedHashRate(&decayed, t0 + 63);
4801 check(three > 0.86 * base && three < 0.88 * base,
4802 "hashrate decay: three seconds past grace costs ~13%");
4803
4804 // One half-life past the grace is exactly half.
4805 double half = MinerReportedHashRate(&decayed, t0 + 75);
4806 check(half > 0.499 * base && half < 0.501 * base,
4807 "hashrate decay: one expected interval past grace halves the estimate");
4808
4809 // A miner that has genuinely stopped ends up reported as nothing --
4810 // the whole point of #7. The old in-place version could not get here:
4811 // its decay fed back into its own grace and settled into a 1/t crawl.
4812 check(MinerReportedHashRate(&decayed, t0 + 3600) == 0.0,
4813 "hashrate decay: a miner silent for an hour is reported as zero");
4814
4815 // A clock that steps backwards must never inflate what we announce.
4816 check(MinerReportedHashRate(&decayed, t0 - 3600) == base,
4817 "hashrate decay: a backwards clock reports the estimate unchanged, never higher");
4818
4819 // Fresh connection, and a connection with one share and so no interval
4820 // to have timed: both must report nothing rather than divide by zero.
4821 Miner fresh(INVALID_SOCKET);
4822 check(MinerReportedHashRate(&fresh, t0) == 0.0,
4823 "hashrate decay: a miner that never submitted a share reports zero");
4824 Miner single(INVALID_SOCKET);
4825 single.lastShareTime = t0;
4826 check(MinerReportedHashRate(&single, t0 + 86400) == 0.0,
4827 "hashrate decay: a miner with no rate sample yet reports zero");
4828
4829 // Nothing in the swept range may produce a NaN, an infinity, a negative
4830 // rate, or a rate above the estimate it started from.
4831 bool finite = true;
4832 for (int64 s = -100; s < 200000 && finite; s += 37) {
4833 double v = MinerReportedHashRate(&decayed, t0 + s);
4834 finite = !std::isnan(v) && !std::isinf(v) && v >= 0.0 && v <= base;
4835 }
4836 check(finite,
4837 "hashrate decay: stays finite, non-negative and non-amplifying across a long silence");
4838 }
4839
4605 gCurrentJob = savedJob; 4840 gCurrentJob = savedJob;
4606 gHaveJob = savedHaveJob; 4841 gHaveJob = savedHaveJob;
4607 4842
@@ -4629,6 +4864,10 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4629 if (m->authorised) gStats.authorizedMiners++; 4864 if (m->authorised) gStats.authorizedMiners++;
4630 4865
4631 gWorkerStats.clear(); 4866 gWorkerStats.clear();
4867 // One timestamp for the whole sweep (#7): every miner is faded to the
4868 // same instant, so the per-worker rows and the total they are summed
4869 // into describe one moment rather than a smear across the loop.
4870 int64 nowStats = GetTime();
4632 for (Miner* m : gMiners) { 4871 for (Miner* m : gMiners) {
4633 if (!m->authorised || m->lastSeen <= 0) continue; 4872 if (!m->authorised || m->lastSeen <= 0) continue;
4634 PoolWorkerStatView v; 4873 PoolWorkerStatView v;
@@ -4637,9 +4876,11 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4637 v.roundShares = m->roundShares; 4876 v.roundShares = m->roundShares;
4638 v.totalShares = m->totalShares; 4877 v.totalShares = m->totalShares;
4639 v.lastSeen = m->lastSeen; 4878 v.lastSeen = m->lastSeen;
4640 v.hashRate = m->hashRate; 4879 // Not m->hashRate: a miner that has stopped submitting shares must
4880 // not keep contributing its last sample to the announced total.
4881 v.hashRate = MinerReportedHashRate(m, nowStats);
4641 gWorkerStats.push_back(v); 4882 gWorkerStats.push_back(v);
4642 gStats.totalHashRate += m->hashRate; 4883 gStats.totalHashRate += v.hashRate;
4643 } 4884 }
4644 std::sort(gWorkerStats.begin(), gWorkerStats.end(), 4885 std::sort(gWorkerStats.begin(), gWorkerStats.end(),
4645 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { 4886 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) {
@@ -4872,6 +5113,35 @@ static void PoolEventLoop()
4872 } 5113 }
4873 } 5114 }
4874 5115
5116 // 3.6. Idle reap (#6). Mirror the clean-disconnect path exactly --
5117 // log, close, erase from gMiners via std::find, delete -- just with
5118 // a different reason. Shares the miner already earned stay in
5119 // roundShareCount, exactly as they do when a miner cleanly
5120 // disconnects mid-round. connectTime stands in for lastSeen on a
5121 // connection that never completed a line. Erasing inside the loop
5122 // above would invalidate the iteration, so collect then reap.
5123 {
5124 int64 now = GetTime();
5125 std::vector<Miner*> idle;
5126 for (Miner* m : gMiners) {
5127 int64 last = (m->lastSeen > 0) ? m->lastSeen : m->connectTime;
5128 if (now - last > POOL_IDLE_TIMEOUT_SECS)
5129 idle.push_back(m);
5130 }
5131 for (Miner* m : idle) {
5132 LogPrint("worker", "[pool] dropping idle miner %s -- nothing"
5133 " received in over %llds (session shares: %llu,"
5134 " round shares: %llu)\n",
5135 m->worker.empty() ? "(never authorized)" : m->worker.c_str(),
5136 (long long)POOL_IDLE_TIMEOUT_SECS,
5137 (unsigned long long)m->sessionShares,
5138 (unsigned long long)m->roundShares);
5139 sock_close(m->fd);
5140 gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m));
5141 delete m;
5142 }
5143 }
5144
4875 // 4. Select over miner sockets 5145 // 4. Select over miner sockets
4876 if (!gMiners.empty()) { 5146 if (!gMiners.empty()) {
4877 fd_set fds; FD_ZERO(&fds); 5147 fd_set fds; FD_ZERO(&fds);
@@ -4894,6 +5164,11 @@ static void PoolEventLoop()
4894 // terminate the whole node -- not just the pool. Every 5164 // terminate the whole node -- not just the pool. Every
4895 // line arrives from an unauthenticated socket. 5165 // line arrives from an unauthenticated socket.
4896 while (RecvLine(m->fd, m->readBuf, line, disc)) { 5166 while (RecvLine(m->fd, m->readBuf, line, disc)) {
5167 // Any complete line is proof of life (#6): authorize
5168 // and valid shares already stamp lastSeen themselves,
5169 // but a miner that only ever subscribes (or sends
5170 // unrecognised methods) must count as alive too.
5171 m->lastSeen = GetTime();
4897 try { 5172 try {
4898 HandleLine(m, line, roundShareCount, 5173 HandleLine(m, line, roundShareCount,
4899 roundShareTotal, blocksFoundThisSession); 5174 roundShareTotal, blocksFoundThisSession);
@@ -4970,13 +5245,19 @@ static void AcceptOneFn(void* arg)
4970 std::string host = relay.substr(0, colon); 5245 std::string host = relay.substr(0, colon);
4971 int port = atoi(relay.substr(colon + 1).c_str()); 5246 int port = atoi(relay.substr(colon + 1).c_str());
4972 5247
4973 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 5248 LogPrint("pool", "[pool] accept thread started for relay %s%s\n", relay.c_str(),
5249 fBtfRelayPinned ? " (/rvrelay pin)" : "");
5250
5251 // Registration health for this relay. A pin has no fallback, so a pin that
5252 // never registers means the pool is simply unreachable -- and that has to
5253 // be loud. The 2026-07-30 incident went unnoticed because this loop logged
5254 // failures at one flat line and success not at all, so "which relay is this
5255 // pool actually registered at" could not be answered from the log.
5256 int nConsecFail = 0;
5257 int64 nFirstFail = 0;
5258 bool fEverRegistered = false;
4974 5259
4975 while (!fShutdown && gPoolRunning) { 5260 while (!fShutdown && gPoolRunning) {
4976 // Advertise this relay as the pool's meeting node BEFORE registering,
4977 // so that miners which resolve our descriptor find a relay where the
4978 // pool (not the node's peer-accept) holds the registration.
4979 BtfSetActiveRelay(relay);
4980 // Returns as soon as the relay lists us -- registered, not yet paired. 5261 // Returns as soon as the relay lists us -- registered, not yet paired.
4981 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 5262 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
4982 5263
@@ -4985,12 +5266,36 @@ static void AcceptOneFn(void* arg)
4985 break; 5266 break;
4986 } 5267 }
4987 if (rv == btf::RV_INVALID) { 5268 if (rv == btf::RV_INVALID) {
4988 LogPrint("pool", "[pool] relay %s: RvServiceRegister failed" 5269 nConsecFail++;
4989 " -- backing off 30s before retry\n", relay.c_str()); 5270 if (nFirstFail == 0) nFirstFail = GetTime();
5271 LogPrint("pool", "[pool] relay %s: registration failed (attempt %d,"
5272 " failing for %llds) -- backing off 30s before retry\n",
5273 relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail));
5274 if (fBtfRelayPinned && (nConsecFail == 3 || nConsecFail % 10 == 0))
5275 LogPrint("pool", "[pool] WARNING: pinned relay %s refused registration"
5276 " %d times running (%llds); /rvrelay leaves no fallback, so"
5277 " miners cannot reach this pool%s\n",
5278 relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail),
5279 fEverRegistered ? "" : " -- it has never registered once");
4990 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); 5280 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000);
4991 continue; 5281 continue;
4992 } 5282 }
4993 5283
5284 // Advertise THIS relay only now. Setting it before the call published a
5285 // meeting node we were not yet listed at, so a miner resolving the
5286 // descriptor inside that window burned its dial on a relay holding no
5287 // registration for us. net.cpp's peer-accept has always ordered it this
5288 // way round; the pool had not.
5289 BtfSetActiveRelay(relay);
5290 if (!fEverRegistered || nConsecFail > 0)
5291 LogPrint("pool", "[pool] registered at relay %s%s%s -- advertising it as"
5292 " the pool's meeting node\n", relay.c_str(),
5293 fBtfRelayPinned ? " (/rvrelay pin)" : "",
5294 nConsecFail > 0 ? ", recovered" : "");
5295 fEverRegistered = true;
5296 nConsecFail = 0;
5297 nFirstFail = 0;
5298
4994 // Block for a miner to actually arrive. RvServiceRegister used to do 5299 // Block for a miner to actually arrive. RvServiceRegister used to do
4995 // this itself and returned with the relay's 0x01 pairing byte already 5300 // this itself and returned with the relay's 0x01 pairing byte already
4996 // consumed; it no longer does. Without waiting here, BtfServiceWrap 5301 // consumed; it no longer does. Without waiting here, BtfServiceWrap
@@ -5045,9 +5350,21 @@ static void AcceptLoopFn(void*)
5045 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); 5350 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2);
5046 5351
5047 std::set<std::string> spawned; // relays we've already started a thread for 5352 std::set<std::string> spawned; // relays we've already started a thread for
5353 std::set<std::string> skipped; // discovered relays the pin excluded, logged once each
5048 5354
5049 while (!fShutdown && gPoolRunning) { 5355 while (!fShutdown && gPoolRunning) {
5356 // BtfAllRelays() already honours the pin: with /rvrelay set it returns
5357 // vBtfMeetingRelays alone, so no thread is ever spawned for a
5358 // discovered relay. Name the ones it drops -- a pool registering at a
5359 // stranger's relay is exactly the thing that should not be silent.
5050 vector<string> relays = BtfAllRelays(); 5360 vector<string> relays = BtfAllRelays();
5361 if (fBtfRelayPinned) {
5362 for (const string& r : BtfDiscoveredRelays())
5363 if (std::find(relays.begin(), relays.end(), r) == relays.end() &&
5364 skipped.insert(r).second)
5365 LogPrint("pool", "[pool] not registering at discovered relay %s"
5366 " -- /rvrelay pins the relay list\n", r.c_str());
5367 }
5051 if (relays.empty()) { 5368 if (relays.empty()) {
5052 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); 5369 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n");
5053 } else { 5370 } else {
@@ -5063,8 +5380,9 @@ static void AcceptLoopFn(void*)
5063 relay.c_str()); 5380 relay.c_str());
5064 } else { 5381 } else {
5065 spawned.insert(relay); 5382 spawned.insert(relay);
5066 LogPrint("pool", "[pool] accept thread spawned for relay %s" 5383 LogPrint("pool", "[pool] accept thread spawned for relay %s%s"
5067 " (%zu total)\n", relay.c_str(), spawned.size()); 5384 " (%zu total)\n", relay.c_str(),
5385 fBtfRelayPinned ? " (/rvrelay pin)" : "", spawned.size());
5068 } 5386 }
5069 } 5387 }
5070 } 5388 }
@@ -5100,10 +5418,17 @@ void ThreadRPCServer(void*)
5100 // armed for its one wallet probe before it is allowed to send. 5418 // armed for its one wallet probe before it is allowed to send.
5101 ReconcilePayouts(); 5419 ReconcilePayouts();
5102 5420
5103 _beginthread(AcceptLoopFn, 0, NULL); 5421 // _beginthread cannot throw (it reports -1), but an unchecked failure
5104 _beginthread(LocalAcceptFn, 0, NULL); 5422 // leaves the pool running deaf: nobody accepts, announces, or pays, and
5105 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 5423 // nothing in the log says why.
5106 _beginthread(PayoutThreadFn, 0, NULL); 5424 if (_beginthread(AcceptLoopFn, 0, NULL) == (uintptr_t)-1)
5425 LogPrint("pool", "[pool] FATAL: accept loop thread failed to spawn -- miners cannot connect\n");
5426 if (_beginthread(LocalAcceptFn, 0, NULL) == (uintptr_t)-1)
5427 LogPrint("pool", "[pool] local accept thread failed to spawn\n");
5428 if (_beginthread(ThreadBtfPoolAnnouncer, 0, NULL) == (uintptr_t)-1)
5429 LogPrint("pool", "[pool] announcer thread failed to spawn -- pool will not be advertised\n");
5430 if (_beginthread(PayoutThreadFn, 0, NULL) == (uintptr_t)-1)
5431 LogPrint("pool", "[pool] FATAL: payout thread failed to spawn -- no one gets paid\n");
5107 5432
5108 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown 5433 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown
5109 5434