diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 398 |
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. |
| 2032 | static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) | 2070 | static 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; | |||
| 2582 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance | 2620 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance |
| 2583 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much | 2621 | static 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. | ||
| 2639 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals | ||
| 2640 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval | ||
| 2641 | static 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. | ||
| 2646 | static const double HASHRATE_DECAY_CUTOFF = 64.0; | ||
| 2647 | |||
| 2585 | struct Miner { | 2648 | struct 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. | ||
| 2723 | static 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 | |||
| 2617 | static std::vector<Miner*> gMiners; | 2749 | static std::vector<Miner*> gMiners; |
| 2618 | 2750 | ||
| 2619 | struct FoundBlock { int height; int64 ts; std::string addr; }; | 2751 | struct 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 | |||
| 2707 | static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not | 2851 | static 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. | ||
| 2863 | static 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, | |||
| 2791 | static void SendJob(Miner* m, bool clean) | 2945 | static 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 | ||