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.cpp398
1 files changed, 373 insertions, 25 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index be6c80d..1d53fb2 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,
@@ -223,6 +236,29 @@ static bool RebuildJob()
223 // From the pool, so the template's payout key is in wallet.dat before 236 // From the pool, so the template's payout key is in wallet.dat before
224 // any miner is handed work against it. 237 // any miner is handed work against it.
225 vector<unsigned char> vchPubKey = GetKeyFromPool(); 238 vector<unsigned char> vchPubKey = GetKeyFromPool();
239 if (vchPubKey.empty()) {
240 // Empty means the wallet could not produce a key: encrypted and
241 // locked, or key generation itself failed. Serialised, an empty
242 // vector is a zero-length push, so the coinbase would become
243 // `<empty> OP_CHECKSIG` -- a script nobody can satisfy. Every
244 // block mined against such a job burns its whole subsidy, while
245 // QueuePayouts still records the full obligation and the miners
246 // are then paid out of coin that block never contributed.
247 // BitcoinMiner refuses to mine in this state; refuse to hand the
248 // pool's miners work in it too. printf and not LogPrint for the
249 // reason given above the deaf-node refusal, and throttled the
250 // same way -- the no-job watchdog retries every few seconds for
251 // as long as the condition lasts.
252 static int64 nLastSaidNoKey = 0;
253 if (GetTime() - nLastSaidNoKey > 60) {
254 nLastSaidNoKey = GetTime();
255 printf("RebuildJob: no wallet key available for the coinbase "
256 "(wallet locked, or key generation failed -- the "
257 "GetKeyFromPool line above says which) -- not issuing "
258 "jobs\n");
259 }
260 return false;
261 }
226 static std::atomic<uint32_t> sExtra{0}; 262 static std::atomic<uint32_t> sExtra{0};
227 uint32_t extraNonce = ++sExtra; 263 uint32_t extraNonce = ++sExtra;
228 264
@@ -1832,7 +1868,9 @@ static void FlushMaturePayouts()
1832 } 1868 }
1833 int64 total = 0; 1869 int64 total = 0;
1834 for (auto& kv : it->amounts) total += kv.second; 1870 for (auto& kv : it->amounts) total += kv.second;
1835 LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" 1871 // Always: one line per voided entry, never a collapsed summary.
1872 // A reorg voids these in a burst from this one source line.
1873 LogPrintAlways("payout", "[payout] block %d (%s) was orphaned -- voiding %zu"
1836 " payment(s) totalling %s\n", 1874 " payment(s) totalling %s\n",
1837 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), 1875 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(),
1838 it->amounts.size(), FormatMoney(total).c_str()); 1876 it->amounts.size(), FormatMoney(total).c_str());
@@ -2031,7 +2069,7 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
2031// disagreeing. 2069// disagreeing.
2032static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) 2070static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason)
2033{ 2071{
2034 LogPrint("payout", "[payout] writing off %s owed to %s for block %d: %s\n", 2072 LogPrintAlways("payout", "[payout] writing off %s owed to %s for block %d: %s\n",
2035 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight, 2073 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight,
2036 reason.c_str()); 2074 reason.c_str());
2037 VersionedBlob blob; 2075 VersionedBlob blob;
@@ -2136,7 +2174,7 @@ static void PayoutThreadFn(void*)
2136 // funding block can land at any point in that window. Upstream has 2174 // funding block can land at any point in that window. Upstream has
2137 // no equivalent -- it decides once, when the entry matures. 2175 // no equivalent -- it decides once, when the entry matures.
2138 if (!PayoutBlockStillOnChain(r.blockHash)) { 2176 if (!PayoutBlockStillOnChain(r.blockHash)) {
2139 LogPrint("payout", "[payout] block %d (%s) orphaned while queued" 2177 LogPrintAlways("payout", "[payout] block %d (%s) orphaned while queued"
2140 " -- dropping %s to %s\n", 2178 " -- dropping %s to %s\n",
2141 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), 2179 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(),
2142 FormatMoney(r.amount).c_str(), r.address.c_str()); 2180 FormatMoney(r.amount).c_str(), r.address.c_str());
@@ -2180,7 +2218,7 @@ static void PayoutThreadFn(void*)
2180 const std::string reason = 2218 const std::string reason =
2181 strprintf("'%s' is not a valid address, so no transaction" 2219 strprintf("'%s' is not a valid address, so no transaction"
2182 " can pay it", r.address.c_str()); 2220 " can pay it", r.address.c_str());
2183 LogPrint("payout", "[payout] SKIPPED bad address '%s' --" 2221 LogPrintAlways("payout", "[payout] SKIPPED bad address '%s' --"
2184 " %s owed for block %d cannot be paid\n", 2222 " %s owed for block %d cannot be paid\n",
2185 r.address.c_str(), FormatMoney(r.amount).c_str(), 2223 r.address.c_str(), FormatMoney(r.amount).c_str(),
2186 r.blockHeight); 2224 r.blockHeight);
@@ -2316,7 +2354,7 @@ static void PayoutThreadFn(void*)
2316 // described by at least one of them at every instant. 2354 // described by at least one of them at every instant.
2317 RecordPayoutTxid(r, wtx.GetHash().GetHex()); 2355 RecordPayoutTxid(r, wtx.GetHash().GetHex());
2318 ForgetPayment(r); 2356 ForgetPayment(r);
2319 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", 2357 LogPrintAlways("payout", "[payout] paid %s to %s (tx %s)\n",
2320 FormatMoney(r.amount).c_str(), r.address.c_str(), 2358 FormatMoney(r.amount).c_str(), r.address.c_str(),
2321 wtx.GetHash().ToString().substr(0, 12).c_str()); 2359 wtx.GetHash().ToString().substr(0, 12).c_str());
2322 } else { 2360 } else {
@@ -2374,7 +2412,7 @@ static void PayoutThreadFn(void*)
2374 // going away must not turn the log into one line a minute 2412 // going away must not turn the log into one line a minute
2375 // forever, and must not go quiet either. 2413 // forever, and must not go quiet either.
2376 if (r.retries <= 5 || r.retries % 60 == 0) 2414 if (r.retries <= 5 || r.retries % 60 == 0)
2377 LogPrint("payout", "[payout] FAILED to pay %s to %s" 2415 LogPrintAlways("payout", "[payout] FAILED to pay %s to %s"
2378 " (%s spendable, wallet unlocked, block %d on" 2416 " (%s spendable, wallet unlocked, block %d on"
2379 " the main chain) -- attempt %d, retrying in" 2417 " the main chain) -- attempt %d, retrying in"
2380 " 60s; this debt stands until it is paid\n", 2418 " 60s; this debt stands until it is paid\n",
@@ -2582,6 +2620,31 @@ static const double VARDIFF_MAX = 1e12;
2582static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance 2620static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance
2583static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much 2621static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much
2584 2622
2623// #7: m->hashRate is an EWMA sampled only when a share arrives, so a miner
2624// that stops hashing keeps its last estimate forever and RefreshStats sums
2625// it into the announced pool total for as long as the connection sits open
2626// -- the published pool hashrate ran several times over ground truth
2627// because of exactly this. MinerReportedHashRate() below applies the
2628// policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share
2629// intervals (expected interval = expected hashes per share at its
2630// difficulty, divided by the estimate itself) with no share, after which
2631// the figure we report falls off with a half-life of one more such
2632// interval. The interval is clamped to [HASHRATE_MIN_INTERVAL,
2633// HASHRATE_MAX_INTERVAL] so a fast miner at the vardiff floor (expected
2634// interval of milliseconds) is not punished for ordinary share-delivery
2635// jitter, and a very slow miner's grace does not stretch past the point
2636// where its estimate means anything. At the vardiff target of one share
2637// per 15s that is a minute of grace, half the rate reported 75s in, and
2638// nothing reported after about seventeen minutes of silence.
2639static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals
2640static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval
2641static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval
2642// Half-lives past the grace beyond which we report a flat zero. 2^-64 of the
2643// original estimate is not a number anyone needs, and stopping here keeps
2644// pow() from walking down into the denormals for a miner that has been quiet
2645// for hours.
2646static const double HASHRATE_DECAY_CUTOFF = 64.0;
2647
2585struct Miner { 2648struct Miner {
2586 btf_socket_t fd; 2649 btf_socket_t fd;
2587 bool authorised = false; 2650 bool authorised = false;
@@ -2609,11 +2672,80 @@ struct Miner {
2609 int64 lastShareTime = 0; // GetTime() of the previous accepted share 2672 int64 lastShareTime = 0; // GetTime() of the previous accepted share
2610 double hashRate = 0.0; // smoothed H/s estimate 2673 double hashRate = 0.0; // smoothed H/s estimate
2611 2674
2675 // Replay guard for mining.submit (#8): nonces already credited on this
2676 // connection for the job it is currently working. Keyed to the job id in
2677 // seenNoncesJob and cleared by SendJob whenever that id changes, so a
2678 // resubmission under a NEW job is never turned away, while a replayed
2679 // (job_id, nonce) pair under the same job is refused before it can be
2680 // credited twice into roundShareCount -- the figure the payout split
2681 // divides by.
2682 std::set<uint32_t> seenNonces;
2683 std::string seenNoncesJob;
2684
2612 explicit Miner(btf_socket_t f) 2685 explicit Miner(btf_socket_t f)
2613 : fd(f), connectTime(GetTime()), 2686 : fd(f), connectTime(GetTime()),
2614 extranonce2(gNextExtranonce2.fetch_add(1)) {} 2687 extranonce2(gNextExtranonce2.fetch_add(1)) {}
2615}; 2688};
2616 2689
2690// What we publish as a miner's H/s: the last EWMA sample, faded out if shares
2691// have stopped arriving (#7, policy at HASHRATE_IDLE_MISSES).
2692//
2693// This reads m->hashRate and never writes it. That is the whole design. The
2694// first attempt decayed the stored estimate in place from the event loop's
2695// sweep, which put a running accumulator behind a figure that is only ever
2696// displayed, and the accumulator misbehaved in two ways:
2697//
2698// - `expected` was recomputed from the already-decayed estimate on every
2699// pass, so each decay raised `expected`, which raised `grace`, which is
2700// the very thing gating the next decay. `grace` simply chased `silent`
2701// upward and the reported rate settled into a 1/t crawl instead of the
2702// half-life curve the comment above promises -- 291x too high at ten
2703// half-lives past the grace. Issue #7 is about the pool over-reporting;
2704// that under-corrected it for hours.
2705// - Once `expected` reached HASHRATE_MAX_INTERVAL it could not rise any
2706// further, the feedback that had been gating each pass was gone, and the
2707// decay began compounding once a second against the total elapsed
2708// silence -- taking a miner from 0.4% of its baseline to 1e-11 of it in
2709// the next hundred seconds, four hours in, for no reason a reader of
2710// either the code or the announced number could see.
2711//
2712// Computing the value instead of accumulating it removes the failure mode
2713// rather than tuning it: with no stored decay state there is nothing for a
2714// tick rate, a missed pass or a duplicated pass to perturb, and the answer
2715// depends only on how long the miner has actually been quiet. It also keeps
2716// the fade out of the estimator -- the EWMA on the share path still blends
2717// against the value it last computed, not against a faded copy of it, so a
2718// miner that resumes recovers on exactly the curve it would have without any
2719// of this.
2720//
2721// `now` is a parameter rather than a GetTime() call so that one refresh fades
2722// every miner to the same instant, and so the policy is directly testable.
2723static double MinerReportedHashRate(const Miner* m, int64 now)
2724{
2725 // Nothing sampled yet: a fresh connection, or one that has found a single
2726 // share and so has no interval to have been timed over. Nothing to fade,
2727 // and nothing to divide by below.
2728 if (m->hashRate <= 0.0 || m->lastShareTime <= 0) return m->hashRate;
2729
2730 double expected = ExpectedHashesForDifficulty(m->difficulty) / m->hashRate;
2731 // Written so that a NaN would land on the floor rather than sail through:
2732 // the clamp is what guarantees `expected` is finite and strictly positive
2733 // below, whatever the division produced.
2734 if (!(expected > HASHRATE_MIN_INTERVAL)) expected = HASHRATE_MIN_INTERVAL;
2735 if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL;
2736
2737 double silent = (double)(now - m->lastShareTime);
2738 double grace = HASHRATE_IDLE_MISSES * expected;
2739 // grace is at least HASHRATE_IDLE_MISSES seconds, so a clock that has
2740 // stepped backwards gives a negative `silent`, takes this branch, and
2741 // reports the sample unchanged. Nothing here can scale an estimate up.
2742 if (!(silent > grace)) return m->hashRate;
2743
2744 double halflives = (silent - grace) / expected;
2745 if (halflives >= HASHRATE_DECAY_CUTOFF) return 0.0;
2746 return m->hashRate * pow(0.5, halflives);
2747}
2748
2617static std::vector<Miner*> gMiners; 2749static std::vector<Miner*> gMiners;
2618 2750
2619struct FoundBlock { int height; int64 ts; std::string addr; }; 2751struct FoundBlock { int height; int64 ts; std::string addr; };
@@ -2664,6 +2796,18 @@ static void LocalAcceptFn(void*)
2664 break; 2796 break;
2665 continue; 2797 continue;
2666 } 2798 }
2799 // #6: let the OS probe a peer that has gone silent, so a half-open
2800 // session eventually errors out of recv and lands in the event
2801 // loop's existing reap path instead of sitting in gMiners forever.
2802 // This is a backstop, not the mechanism: the default cadence before
2803 // the first probe is hours on most stacks, so the idle timeout in
2804 // PoolEventLoop fires long before keepalive does. Miners arriving
2805 // via a relay ride a loopback socket pair whose remote transport is
2806 // the rendezvous socket owned by the tunnel pump threads; keepalive
2807 // cannot reach across that, which is part of why the idle timeout
2808 // exists.
2809 int keepalive = 1;
2810 setsockopt(c, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive));
2667 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); 2811 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
2668 PushReady((btf_socket_t)c); 2812 PushReady((btf_socket_t)c);
2669 } 2813 }
@@ -2707,6 +2851,16 @@ static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it
2707static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not 2851static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not
2708 // authorized after this long is 2852 // authorized after this long is
2709 // idle, not merely starting up 2853 // idle, not merely starting up
2854// #6: a Miner used to leave gMiners only when RecvLine reported a clean
2855// disconnect. A half-open session (miner host died, NAT state expired,
2856// cable pulled) sends no FIN and reports no error, so it sat in the table
2857// forever -- authorize outpaced disconnect 2:1 on the live pool, and the
2858// table grew toward MAX_POOL_MINERS, past which real miners are refused.
2859// lastSeen is stamped on every complete line received; a miner that has
2860// said nothing for this long is gone in every way that matters. Ten
2861// minutes is far beyond the vardiff target (a share every 15s) and the
2862// stall watchdog (45s), so no miner that is actually hashing comes close.
2863static const int64 POOL_IDLE_TIMEOUT_SECS = 600;
2710 2864
2711// Non-blocking recv: pull whatever bytes are available now into buf, then 2865// Non-blocking recv: pull whatever bytes are available now into buf, then
2712// return one complete line if one is ready. Three outcomes: 2866// return one complete line if one is ready. Three outcomes:
@@ -2791,6 +2945,17 @@ static json MakeNotifyParams(const StratumJob& job, const CBlock& b,
2791static void SendJob(Miner* m, bool clean) 2945static void SendJob(Miner* m, bool clean)
2792{ 2946{
2793 if (!gHaveJob) return; 2947 if (!gHaveJob) return;
2948 // Every path that hands a miner a new job funnels through here, so this
2949 // is the one place the replay guard needs to follow the job id: nonces
2950 // credited under the previous job mean nothing under the new one. Keying
2951 // the clear to the id (rather than clearing unconditionally) keeps a
2952 // same-job re-notify -- the vardiff retarget and stall watchdog both
2953 // re-send the CURRENT job -- from reopening the duplicate window it
2954 // exists to close.
2955 if (m->seenNoncesJob != gCurrentJob.jobId) {
2956 m->seenNonces.clear();
2957 m->seenNoncesJob = gCurrentJob.jobId;
2958 }
2794 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); 2959 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2);
2795 // Use this miner's own shareTarget (set by vardiff) in the notify params. 2960 // Use this miner's own shareTarget (set by vardiff) in the notify params.
2796 // The participant miner reads p[3] as its share target, and HandleLine 2961 // The participant miner reads p[3] as its share target, and HandleLine
@@ -3049,6 +3214,22 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
3049 } 3214 }
3050 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); 3215 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
3051 3216
3217 // Reject a replayed (job, nonce) before it costs a rebuild and a
3218 // RandomX hash: nothing about the work below distinguishes a second
3219 // submission of a valid share from the first, so without this check
3220 // it passes every test and is credited again -- and roundShareCount
3221 // is what the payout split divides by, so each replay multiplies the
3222 // address's cut of the round. The set is per-connection and cleared
3223 // by SendJob on every job change, so only a same-job repeat is
3224 // refused.
3225 if (m->seenNonces.count(nNonce)) {
3226 LogPrint("worker", "[worker->pool] duplicate share from %s"
3227 " nonce=%u job=%s -- already credited, not counting again\n",
3228 m->address.c_str(), nNonce, submitJobId.c_str());
3229 reply(false, "Duplicate share");
3230 return true;
3231 }
3232
3052 // Rebuild this miner's exact block (unique merkle via extranonce2) 3233 // Rebuild this miner's exact block (unique merkle via extranonce2)
3053 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); 3234 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2);
3054 b.nNonce = nNonce; 3235 b.nNonce = nNonce;
@@ -3076,6 +3257,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
3076 m->roundShares++; 3257 m->roundShares++;
3077 m->totalShares++; 3258 m->totalShares++;
3078 m->vardiffShares++; 3259 m->vardiffShares++;
3260 m->seenNonces.insert(nNonce); // replay guard: credit each nonce once per job
3079 int64 shareTime = GetTime(); 3261 int64 shareTime = GetTime();
3080 m->lastSeen = shareTime; 3262 m->lastSeen = shareTime;
3081 roundShareCount[m->address]++; 3263 roundShareCount[m->address]++;
@@ -4938,6 +5120,82 @@ int RunPoolStratumSelfTest()
4938 strPoolRoundsFile = savedRoundsFile; 5120 strPoolRoundsFile = savedRoundsFile;
4939 gPoolRounds = savedRounds; 5121 gPoolRounds = savedRounds;
4940 5122
5123 // -- hashrate decay (#7) --------------------------------------------------
5124 // The first version of this decayed m->hashRate in place from the event
5125 // loop's sweep, which made the reported figure a function of how the loop
5126 // happened to run rather than of how long the miner had been quiet. These
5127 // pin the properties that fixes: sampling changes nothing, and the answer
5128 // depends only on elapsed silence.
5129 {
5130 // A miner exactly on the vardiff target: 4096 hashes per share at
5131 // difficulty 1.0, one share per 15s, so 4096/15 H/s. Its expected
5132 // interval is 15s and its grace is 60s.
5133 const double base = 4096.0 / 15.0;
5134 const int64 t0 = 1766000000; // any plausible non-zero wall clock
5135 Miner decayed(INVALID_SOCKET);
5136 decayed.difficulty = 1.0;
5137 decayed.hashRate = base;
5138 decayed.lastShareTime = t0;
5139
5140 check(MinerReportedHashRate(&decayed, t0 + 60) == base,
5141 "hashrate decay: a miner inside its grace period reports its full estimate");
5142
5143 // Sampling must not consume the estimate. Twenty passes is one second
5144 // of the event loop's old sweep; a thousand is nearly a minute of it.
5145 double once = MinerReportedHashRate(&decayed, t0 + 61);
5146 bool idempotent = (decayed.hashRate == base);
5147 for (int i = 0; i < 1000 && idempotent; i++)
5148 idempotent = (MinerReportedHashRate(&decayed, t0 + 61) == once) &&
5149 (decayed.hashRate == base);
5150 check(idempotent,
5151 "hashrate decay: repeated sampling returns one value and leaves the estimate alone");
5152
5153 // One second past a 60s grace is 1/15th of a half-life: ~4.5% off, not
5154 // the estimate gone. A miner at the vardiff target overshoots its grace
5155 // roughly 1.8% of the time (e^-4) purely by share-timing variance, and
5156 // must not be zeroed for it.
5157 check(once > 0.94 * base && once < 0.96 * base,
5158 "hashrate decay: one second past grace costs a few percent, not the estimate");
5159 double three = MinerReportedHashRate(&decayed, t0 + 63);
5160 check(three > 0.86 * base && three < 0.88 * base,
5161 "hashrate decay: three seconds past grace costs ~13%");
5162
5163 // One half-life past the grace is exactly half.
5164 double half = MinerReportedHashRate(&decayed, t0 + 75);
5165 check(half > 0.499 * base && half < 0.501 * base,
5166 "hashrate decay: one expected interval past grace halves the estimate");
5167
5168 // A miner that has genuinely stopped ends up reported as nothing --
5169 // the whole point of #7. The old in-place version could not get here:
5170 // its decay fed back into its own grace and settled into a 1/t crawl.
5171 check(MinerReportedHashRate(&decayed, t0 + 3600) == 0.0,
5172 "hashrate decay: a miner silent for an hour is reported as zero");
5173
5174 // A clock that steps backwards must never inflate what we announce.
5175 check(MinerReportedHashRate(&decayed, t0 - 3600) == base,
5176 "hashrate decay: a backwards clock reports the estimate unchanged, never higher");
5177
5178 // Fresh connection, and a connection with one share and so no interval
5179 // to have timed: both must report nothing rather than divide by zero.
5180 Miner fresh(INVALID_SOCKET);
5181 check(MinerReportedHashRate(&fresh, t0) == 0.0,
5182 "hashrate decay: a miner that never submitted a share reports zero");
5183 Miner single(INVALID_SOCKET);
5184 single.lastShareTime = t0;
5185 check(MinerReportedHashRate(&single, t0 + 86400) == 0.0,
5186 "hashrate decay: a miner with no rate sample yet reports zero");
5187
5188 // Nothing in the swept range may produce a NaN, an infinity, a negative
5189 // rate, or a rate above the estimate it started from.
5190 bool finite = true;
5191 for (int64 s = -100; s < 200000 && finite; s += 37) {
5192 double v = MinerReportedHashRate(&decayed, t0 + s);
5193 finite = !std::isnan(v) && !std::isinf(v) && v >= 0.0 && v <= base;
5194 }
5195 check(finite,
5196 "hashrate decay: stays finite, non-negative and non-amplifying across a long silence");
5197 }
5198
4941 gCurrentJob = savedJob; 5199 gCurrentJob = savedJob;
4942 gHaveJob = savedHaveJob; 5200 gHaveJob = savedHaveJob;
4943 5201
@@ -4965,6 +5223,10 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4965 if (m->authorised) gStats.authorizedMiners++; 5223 if (m->authorised) gStats.authorizedMiners++;
4966 5224
4967 gWorkerStats.clear(); 5225 gWorkerStats.clear();
5226 // One timestamp for the whole sweep (#7): every miner is faded to the
5227 // same instant, so the per-worker rows and the total they are summed
5228 // into describe one moment rather than a smear across the loop.
5229 int64 nowStats = GetTime();
4968 for (Miner* m : gMiners) { 5230 for (Miner* m : gMiners) {
4969 if (!m->authorised || m->lastSeen <= 0) continue; 5231 if (!m->authorised || m->lastSeen <= 0) continue;
4970 PoolWorkerStatView v; 5232 PoolWorkerStatView v;
@@ -4973,9 +5235,11 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4973 v.roundShares = m->roundShares; 5235 v.roundShares = m->roundShares;
4974 v.totalShares = m->totalShares; 5236 v.totalShares = m->totalShares;
4975 v.lastSeen = m->lastSeen; 5237 v.lastSeen = m->lastSeen;
4976 v.hashRate = m->hashRate; 5238 // Not m->hashRate: a miner that has stopped submitting shares must
5239 // not keep contributing its last sample to the announced total.
5240 v.hashRate = MinerReportedHashRate(m, nowStats);
4977 gWorkerStats.push_back(v); 5241 gWorkerStats.push_back(v);
4978 gStats.totalHashRate += m->hashRate; 5242 gStats.totalHashRate += v.hashRate;
4979 } 5243 }
4980 std::sort(gWorkerStats.begin(), gWorkerStats.end(), 5244 std::sort(gWorkerStats.begin(), gWorkerStats.end(),
4981 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { 5245 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) {
@@ -5208,6 +5472,35 @@ static void PoolEventLoop()
5208 } 5472 }
5209 } 5473 }
5210 5474
5475 // 3.6. Idle reap (#6). Mirror the clean-disconnect path exactly --
5476 // log, close, erase from gMiners via std::find, delete -- just with
5477 // a different reason. Shares the miner already earned stay in
5478 // roundShareCount, exactly as they do when a miner cleanly
5479 // disconnects mid-round. connectTime stands in for lastSeen on a
5480 // connection that never completed a line. Erasing inside the loop
5481 // above would invalidate the iteration, so collect then reap.
5482 {
5483 int64 now = GetTime();
5484 std::vector<Miner*> idle;
5485 for (Miner* m : gMiners) {
5486 int64 last = (m->lastSeen > 0) ? m->lastSeen : m->connectTime;
5487 if (now - last > POOL_IDLE_TIMEOUT_SECS)
5488 idle.push_back(m);
5489 }
5490 for (Miner* m : idle) {
5491 LogPrint("worker", "[pool] dropping idle miner %s -- nothing"
5492 " received in over %llds (session shares: %llu,"
5493 " round shares: %llu)\n",
5494 m->worker.empty() ? "(never authorized)" : m->worker.c_str(),
5495 (long long)POOL_IDLE_TIMEOUT_SECS,
5496 (unsigned long long)m->sessionShares,
5497 (unsigned long long)m->roundShares);
5498 sock_close(m->fd);
5499 gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m));
5500 delete m;
5501 }
5502 }
5503
5211 // 4. Select over miner sockets 5504 // 4. Select over miner sockets
5212 if (!gMiners.empty()) { 5505 if (!gMiners.empty()) {
5213 fd_set fds; FD_ZERO(&fds); 5506 fd_set fds; FD_ZERO(&fds);
@@ -5230,6 +5523,11 @@ static void PoolEventLoop()
5230 // terminate the whole node -- not just the pool. Every 5523 // terminate the whole node -- not just the pool. Every
5231 // line arrives from an unauthenticated socket. 5524 // line arrives from an unauthenticated socket.
5232 while (RecvLine(m->fd, m->readBuf, line, disc)) { 5525 while (RecvLine(m->fd, m->readBuf, line, disc)) {
5526 // Any complete line is proof of life (#6): authorize
5527 // and valid shares already stamp lastSeen themselves,
5528 // but a miner that only ever subscribes (or sends
5529 // unrecognised methods) must count as alive too.
5530 m->lastSeen = GetTime();
5233 try { 5531 try {
5234 HandleLine(m, line, roundShareCount, 5532 HandleLine(m, line, roundShareCount,
5235 roundShareTotal, blocksFoundThisSession); 5533 roundShareTotal, blocksFoundThisSession);
@@ -5306,13 +5604,19 @@ static void AcceptOneFn(void* arg)
5306 std::string host = relay.substr(0, colon); 5604 std::string host = relay.substr(0, colon);
5307 int port = atoi(relay.substr(colon + 1).c_str()); 5605 int port = atoi(relay.substr(colon + 1).c_str());
5308 5606
5309 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 5607 LogPrint("pool", "[pool] accept thread started for relay %s%s\n", relay.c_str(),
5608 fBtfRelayPinned ? " (/rvrelay pin)" : "");
5609
5610 // Registration health for this relay. A pin has no fallback, so a pin that
5611 // never registers means the pool is simply unreachable -- and that has to
5612 // be loud. The 2026-07-30 incident went unnoticed because this loop logged
5613 // failures at one flat line and success not at all, so "which relay is this
5614 // pool actually registered at" could not be answered from the log.
5615 int nConsecFail = 0;
5616 int64 nFirstFail = 0;
5617 bool fEverRegistered = false;
5310 5618
5311 while (!fShutdown && gPoolRunning) { 5619 while (!fShutdown && gPoolRunning) {
5312 // Advertise this relay as the pool's meeting node BEFORE registering,
5313 // so that miners which resolve our descriptor find a relay where the
5314 // pool (not the node's peer-accept) holds the registration.
5315 BtfSetActiveRelay(relay);
5316 // Returns as soon as the relay lists us -- registered, not yet paired. 5620 // Returns as soon as the relay lists us -- registered, not yet paired.
5317 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 5621 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
5318 5622
@@ -5321,12 +5625,36 @@ static void AcceptOneFn(void* arg)
5321 break; 5625 break;
5322 } 5626 }
5323 if (rv == btf::RV_INVALID) { 5627 if (rv == btf::RV_INVALID) {
5324 LogPrint("pool", "[pool] relay %s: RvServiceRegister failed" 5628 nConsecFail++;
5325 " -- backing off 30s before retry\n", relay.c_str()); 5629 if (nFirstFail == 0) nFirstFail = GetTime();
5630 LogPrint("pool", "[pool] relay %s: registration failed (attempt %d,"
5631 " failing for %llds) -- backing off 30s before retry\n",
5632 relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail));
5633 if (fBtfRelayPinned && (nConsecFail == 3 || nConsecFail % 10 == 0))
5634 LogPrint("pool", "[pool] WARNING: pinned relay %s refused registration"
5635 " %d times running (%llds); /rvrelay leaves no fallback, so"
5636 " miners cannot reach this pool%s\n",
5637 relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail),
5638 fEverRegistered ? "" : " -- it has never registered once");
5326 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); 5639 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000);
5327 continue; 5640 continue;
5328 } 5641 }
5329 5642
5643 // Advertise THIS relay only now. Setting it before the call published a
5644 // meeting node we were not yet listed at, so a miner resolving the
5645 // descriptor inside that window burned its dial on a relay holding no
5646 // registration for us. net.cpp's peer-accept has always ordered it this
5647 // way round; the pool had not.
5648 BtfSetActiveRelay(relay);
5649 if (!fEverRegistered || nConsecFail > 0)
5650 LogPrint("pool", "[pool] registered at relay %s%s%s -- advertising it as"
5651 " the pool's meeting node\n", relay.c_str(),
5652 fBtfRelayPinned ? " (/rvrelay pin)" : "",
5653 nConsecFail > 0 ? ", recovered" : "");
5654 fEverRegistered = true;
5655 nConsecFail = 0;
5656 nFirstFail = 0;
5657
5330 // Block for a miner to actually arrive. RvServiceRegister used to do 5658 // Block for a miner to actually arrive. RvServiceRegister used to do
5331 // this itself and returned with the relay's 0x01 pairing byte already 5659 // this itself and returned with the relay's 0x01 pairing byte already
5332 // consumed; it no longer does. Without waiting here, BtfServiceWrap 5660 // consumed; it no longer does. Without waiting here, BtfServiceWrap
@@ -5381,9 +5709,21 @@ static void AcceptLoopFn(void*)
5381 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); 5709 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2);
5382 5710
5383 std::set<std::string> spawned; // relays we've already started a thread for 5711 std::set<std::string> spawned; // relays we've already started a thread for
5712 std::set<std::string> skipped; // discovered relays the pin excluded, logged once each
5384 5713
5385 while (!fShutdown && gPoolRunning) { 5714 while (!fShutdown && gPoolRunning) {
5715 // BtfAllRelays() already honours the pin: with /rvrelay set it returns
5716 // vBtfMeetingRelays alone, so no thread is ever spawned for a
5717 // discovered relay. Name the ones it drops -- a pool registering at a
5718 // stranger's relay is exactly the thing that should not be silent.
5386 vector<string> relays = BtfAllRelays(); 5719 vector<string> relays = BtfAllRelays();
5720 if (fBtfRelayPinned) {
5721 for (const string& r : BtfDiscoveredRelays())
5722 if (std::find(relays.begin(), relays.end(), r) == relays.end() &&
5723 skipped.insert(r).second)
5724 LogPrint("pool", "[pool] not registering at discovered relay %s"
5725 " -- /rvrelay pins the relay list\n", r.c_str());
5726 }
5387 if (relays.empty()) { 5727 if (relays.empty()) {
5388 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); 5728 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n");
5389 } else { 5729 } else {
@@ -5399,8 +5739,9 @@ static void AcceptLoopFn(void*)
5399 relay.c_str()); 5739 relay.c_str());
5400 } else { 5740 } else {
5401 spawned.insert(relay); 5741 spawned.insert(relay);
5402 LogPrint("pool", "[pool] accept thread spawned for relay %s" 5742 LogPrint("pool", "[pool] accept thread spawned for relay %s%s"
5403 " (%zu total)\n", relay.c_str(), spawned.size()); 5743 " (%zu total)\n", relay.c_str(),
5744 fBtfRelayPinned ? " (/rvrelay pin)" : "", spawned.size());
5404 } 5745 }
5405 } 5746 }
5406 } 5747 }
@@ -5436,10 +5777,17 @@ void ThreadRPCServer(void*)
5436 // armed for its one wallet probe before it is allowed to send. 5777 // armed for its one wallet probe before it is allowed to send.
5437 ReconcilePayouts(); 5778 ReconcilePayouts();
5438 5779
5439 _beginthread(AcceptLoopFn, 0, NULL); 5780 // _beginthread cannot throw (it reports -1), but an unchecked failure
5440 _beginthread(LocalAcceptFn, 0, NULL); 5781 // leaves the pool running deaf: nobody accepts, announces, or pays, and
5441 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 5782 // nothing in the log says why.
5442 _beginthread(PayoutThreadFn, 0, NULL); 5783 if (_beginthread(AcceptLoopFn, 0, NULL) == (uintptr_t)-1)
5784 LogPrint("pool", "[pool] FATAL: accept loop thread failed to spawn -- miners cannot connect\n");
5785 if (_beginthread(LocalAcceptFn, 0, NULL) == (uintptr_t)-1)
5786 LogPrint("pool", "[pool] local accept thread failed to spawn\n");
5787 if (_beginthread(ThreadBtfPoolAnnouncer, 0, NULL) == (uintptr_t)-1)
5788 LogPrint("pool", "[pool] announcer thread failed to spawn -- pool will not be advertised\n");
5789 if (_beginthread(PayoutThreadFn, 0, NULL) == (uintptr_t)-1)
5790 LogPrint("pool", "[pool] FATAL: payout thread failed to spawn -- no one gets paid\n");
5443 5791
5444 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown 5792 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown
5445 5793