diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 375 |
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. |
| 1955 | static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) | 1970 | static 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; | |||
| 2494 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance | 2509 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance |
| 2495 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much | 2510 | static 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. | ||
| 2528 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals | ||
| 2529 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval | ||
| 2530 | static 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. | ||
| 2535 | static const double HASHRATE_DECAY_CUTOFF = 64.0; | ||
| 2536 | |||
| 2497 | struct Miner { | 2537 | struct 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. | ||
| 2612 | static 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 | |||
| 2529 | static std::vector<Miner*> gMiners; | 2638 | static std::vector<Miner*> gMiners; |
| 2530 | 2639 | ||
| 2531 | struct FoundBlock { int height; int64 ts; std::string addr; }; | 2640 | struct 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 | |||
| 2619 | static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not | 2740 | static 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. | ||
| 2752 | static 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, | |||
| 2703 | static void SendJob(Miner* m, bool clean) | 2834 | static 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 | ||