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.cpp1419
1 files changed, 1419 insertions, 0 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
new file mode 100644
index 0000000..41c0328
--- /dev/null
+++ b/src/rpc.cpp
@@ -0,0 +1,1419 @@
1// Copyright (c) 2009 Satoshi Nakamoto / Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Bitflash pool server -- .btf Stratum pool, operator side.
5//
6// Architecture:
7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout
8// state. No locks needed on any of that data.
9// AcceptOneFn -- one thread per relay. Blocks on RvServiceRegister +
10// BtfServiceWrap, pushes finished socket to gReadyQueue.
11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every
12// 60s so dynamically discovered relays get picked up.
13//
14// GUI-visible state is snapshotted under gStatsMutex every 2s.
15// Accept queue handoff uses gReadyMutex (held for microseconds).
16
17#pragma push_macro("snprintf")
18#undef snprintf
19#include <nlohmann/json.hpp>
20#pragma pop_macro("snprintf")
21
22#include "headers.h"
23#ifdef snprintf
24#undef snprintf
25#endif
26#include "btftunnel.h"
27
28#ifndef _WIN32
29#include <sys/socket.h>
30#include <sys/select.h>
31#include <netinet/in.h>
32#include <arpa/inet.h>
33#include <unistd.h>
34#endif
35
36#include <atomic>
37#include <mutex>
38#include <map>
39#include <set>
40#include <deque>
41#include <vector>
42#include <algorithm>
43#include <sstream>
44#include <iomanip>
45
46using json = nlohmann::json;
47
48#ifdef _WIN32
49#define sock_close(s) BtfCloseSocket(s)
50#define SEND_FLAGS 0
51#else
52#define sock_close(s) BtfCloseSocket(s)
53#define SEND_FLAGS MSG_NOSIGNAL
54#endif
55
56volatile bool gPoolRunning = false;
57
58// ---------------------------------------------------------------------------
59// Hex helpers
60// ---------------------------------------------------------------------------
61static std::string ToHex(const void* p, size_t n)
62{
63 const unsigned char* b = (const unsigned char*)p;
64 std::string s; s.reserve(n * 2);
65 static const char* H = "0123456789abcdef";
66 for (size_t i = 0; i < n; i++) { s += H[b[i] >> 4]; s += H[b[i] & 0xf]; }
67 return s;
68}
69
70static std::vector<unsigned char> FromHex(const std::string& s)
71{
72 std::vector<unsigned char> v;
73 for (size_t i = 0; i + 1 < s.size(); i += 2) {
74 auto h = [](char c) -> int {
75 if (c >= '0' && c <= '9') return c - '0';
76 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
77 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
78 return 0;
79 };
80 v.push_back((unsigned char)((h(s[i]) << 4) | h(s[i + 1])));
81 }
82 return v;
83}
84
85// ---------------------------------------------------------------------------
86// Job -- written and read exclusively by the event loop thread
87// ---------------------------------------------------------------------------
88struct StratumJob {
89 std::string jobId;
90 CBlock block;
91 uint256 target; // full block target; share targets are per-miner via vardiff
92 int height;
93};
94static StratumJob gCurrentJob;
95static bool gHaveJob = false;
96
97// Compute a share target from a Stratum difficulty value.
98// Stratum difficulty 1 must correspond to THIS chain's easiest allowed target
99// (bnProofOfWorkLimit = ~uint256(0) >> 12, compact 0x1f0fffff), not Bitcoin's
100// SHA256 diff-1 target (0x1d00ffff). Those differ by about 2^20 (~1e6x):
101// Bitcoin's diff-1 needs ~2^32 hashes/share on average, while this chain's PoW
102// floor needs only ~2^12. Using the Bitcoin constant made every miner's
103// "difficulty 1" about a million times harder than the chain's own floor, so a
104// worker could hash for minutes without ever finding a share -- and vardiff
105// never gets the samples it needs to correct course, because no share ever
106// comes in for it to measure. Base off the actual chain limit instead so
107// difficulty 1 is genuinely easy and vardiff has real data to retarget from.
108// Higher difficulty = smaller (harder) target.
109static uint256 ShareTargetFromDifficulty(double difficulty)
110{
111 if (difficulty <= 0.0) difficulty = 1.0;
112 static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256();
113 // shareTarget = diff1 / difficulty
114 CBigNum bn; bn.setuint256(diff1);
115 // Multiply by 2^16 to keep precision, divide, shift back
116 bn <<= 16;
117 bn /= (uint64)(difficulty * 65536.0);
118 uint256 t = bn.getuint256();
119 // Cap at all-ones (maximum, easiest possible)
120 if (t < diff1 && difficulty < 1.0) t = ~uint256(0);
121 return t;
122}
123
124// Expected number of hashes to find one share at a given Stratum difficulty,
125// on this chain's scale (difficulty 1 == bnProofOfWorkLimit, which needs on
126// average 2^12 = 4096 hashes). Used to turn "shares per second" into H/s.
127static const double SHARE_HASHES_AT_DIFF_1 = 4096.0;
128static double ExpectedHashesForDifficulty(double difficulty)
129{
130 if (difficulty <= 0.0) difficulty = 1.0;
131 return difficulty * SHARE_HASHES_AT_DIFF_1;
132}
133
134// Rebuild the current job from the best chain tip.
135// Must be called from the event loop thread (or startup, before the loop).
136// Returns true on success. Logs the specific failure reason on false.
137static bool RebuildJob()
138{
139 CRITICAL_BLOCK(cs_main)
140 {
141 if (!pindexBest) {
142 LogPrint("pool", "RebuildJob: no chain tip (pindexBest is null)\n");
143 return false;
144 }
145
146 CBlockIndex* pindexPrev = pindexBest;
147 unsigned int nBits = GetNextWorkRequired(pindexPrev);
148
149 // From the pool, so the template's payout key is in wallet.dat before
150 // any miner is handed work against it.
151 vector<unsigned char> vchPubKey = GetKeyFromPool();
152 static std::atomic<uint32_t> sExtra{0};
153 uint32_t extraNonce = ++sExtra;
154
155 CTransaction txNew;
156 txNew.vin.resize(1);
157 txNew.vin[0].prevout.SetNull();
158 // Height first, for the reason given at the matching line in
159 // BitcoinMiner: it is what keeps two coinbases at different heights
160 // from ever being byte-identical. This builder was missed when that
161 // went in, and it produces real blocks -- a pool operator's template
162 // is mined and submitted like any other. #58.
163 txNew.vin[0].scriptSig << (pindexPrev ? pindexPrev->nHeight + 1 : 0) << nBits << (CBigNum)extraNonce;
164 txNew.vout.resize(1);
165 txNew.vout[0].scriptPubKey << vchPubKey << OP_CHECKSIG;
166
167 CBlock block;
168 block.vtx.push_back(txNew);
169
170 {
171 CTxDB txdb("r");
172 CRITICAL_BLOCK(cs_mapTransactions)
173 {
174 map<uint256, CTxIndex> pool;
175 unsigned int sz = 0;
176 for (auto& kv : mapTransactions) {
177 CTransaction& tx = kv.second;
178 if (tx.IsCoinBase() || !tx.IsFinal()) continue;
179 int64 nFees = 0;
180 map<uint256, CTxIndex> tmp(pool);
181 if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees,
182 false, true, tx.GetMinFee(block.vtx.size() < 100)))
183 continue;
184 pool = tmp;
185 block.vtx.push_back(tx);
186 sz += ::GetSerializeSize(tx, SER_NETWORK);
187 if (sz > MAX_SIZE / 2) break;
188 }
189 }
190 }
191
192 block.vtx[0].vout[0].nValue = block.GetBlockValue(pindexPrev->nHeight + 1, 0);
193 block.hashPrevBlock = pindexPrev->GetBlockHash();
194 block.hashMerkleRoot = block.BuildMerkleTree();
195 block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast() + 1),
196 (unsigned int)GetAdjustedTime());
197 block.nBits = nBits;
198 block.nNonce = 1;
199 // No AddKey here: the pool wrote this key to wallet.dat before it was
200 // handed out.
201
202 StratumJob job;
203 job.jobId = ToHex(&extraNonce, 4);
204 job.block = block;
205 job.target = CBigNum().SetCompact(nBits).getuint256();
206 job.height = nBestHeight + 1;
207
208 gCurrentJob = job;
209 gHaveJob = true;
210 return true;
211 }
212 return false;
213}
214
215// ---------------------------------------------------------------------------
216// Payouts -- event loop thread only
217// ---------------------------------------------------------------------------
218struct PendingPayout {
219 int matureAtHeight;
220 std::map<std::string, int64> amounts; // address -> satoshis
221};
222static std::vector<PendingPayout> gPendingPayouts;
223
224static std::string PayoutFilePath()
225{
226 return GetAppDir() + "/pending_payouts.json";
227}
228
229static void SavePendingPayouts()
230{
231 nlohmann::json arr = nlohmann::json::array();
232 for (const auto& pp : gPendingPayouts) {
233 nlohmann::json obj;
234 obj["matureAtHeight"] = pp.matureAtHeight;
235 nlohmann::json amounts = nlohmann::json::object();
236 for (const auto& kv : pp.amounts)
237 amounts[kv.first] = kv.second;
238 obj["amounts"] = amounts;
239 arr.push_back(obj);
240 }
241 std::string path = PayoutFilePath();
242 FILE* f = fopen(path.c_str(), "w");
243 if (f) {
244 std::string s = arr.dump(2);
245 fwrite(s.c_str(), 1, s.size(), f);
246 fclose(f);
247 } else {
248 LogPrint("payout", "[payout] WARNING: could not write %s\n", path.c_str());
249 }
250}
251
252static void LoadPendingPayouts()
253{
254 std::string path = PayoutFilePath();
255 FILE* f = fopen(path.c_str(), "r");
256 if (!f) return; // no file yet -- first run, nothing to load
257 fseek(f, 0, SEEK_END);
258 long sz = ftell(f);
259 fseek(f, 0, SEEK_SET);
260 std::string s(sz, '\0');
261 fread(&s[0], 1, sz, f);
262 fclose(f);
263 try {
264 nlohmann::json arr = nlohmann::json::parse(s);
265 for (const auto& obj : arr) {
266 PendingPayout pp;
267 pp.matureAtHeight = obj["matureAtHeight"].get<int>();
268 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it)
269 pp.amounts[it.key()] = it.value().get<int64>();
270 gPendingPayouts.push_back(pp);
271 }
272 LogPrint("payout", "[payout] loaded %zu pending payout(s) from disk\n",
273 gPendingPayouts.size());
274 } catch (const std::exception& e) {
275 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n",
276 path.c_str(), e.what());
277 }
278}
279
280// Payouts ready to execute are moved here under gPayoutExecMutex.
281// A dedicated thread drains this list so SendMoney (which takes cs_main)
282// never runs on the event loop thread and can't stall miner I/O.
283static std::vector<PendingPayout> gPayoutExecQueue;
284static std::mutex gPayoutExecMutex;
285static std::atomic<bool> gPayoutThreadRunning{false};
286
287static void QueuePayouts(int blockHeight,
288 const std::map<std::string, uint64>& shareCount,
289 uint64 total)
290{
291 if (total == 0) {
292 LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n",
293 blockHeight);
294 return;
295 }
296
297 double feeFrac = dPoolFeePercent / 100.0;
298 if (feeFrac < 0.0) feeFrac = 0.0;
299 if (feeFrac > 1.0) feeFrac = 1.0;
300
301 // Compute the block subsidy at the height that was just found.
302 // We do not include tx fees in miner payout -- those stay in the operator
303 // wallet as part of the coinbase, which is the operator's cut on top of feeFrac.
304 int64 nSubsidy = 50 * COIN;
305 nSubsidy >>= (blockHeight / 210000); // halving schedule
306 int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac));
307
308 PendingPayout pp;
309 pp.matureAtHeight = blockHeight + COINBASE_MATURITY;
310
311 int skipped = 0;
312 int64 burned = 0; // amount owed to miners with bad/empty addresses
313 for (auto& kv : shareCount) {
314 int64 amount = (int64)(((double)kv.second / (double)total) * (double)minerReward);
315 if (amount < CENT) {
316 skipped++;
317 continue;
318 }
319 uint160 addrCheck;
320 if (kv.first.empty() || !AddressToHash160(kv.first, addrCheck)) {
321 // Address was invalid at authorize time (warned then). Their share
322 // of the reward cannot be paid -- log the amount so it's visible.
323 burned += amount;
324 LogPrint("payout", "[payout] WARNING: %s owed to miner with invalid"
325 " address '%s' -- cannot pay out, amount forfeited\n",
326 FormatMoney(amount).c_str(), kv.first.c_str());
327 continue;
328 }
329 pp.amounts[kv.first] = amount;
330 }
331
332 gPendingPayouts.push_back(pp);
333 SavePendingPayouts();
334
335 int64 operatorCut = nSubsidy - minerReward;
336 LogPrint("payout",
337 "[payout] block %d: subsidy %s BTF, miners get %s"
338 " (fee %.2f%% = %s to operator),"
339 " %zu recipients, matures height %d,"
340 " %d below dust%s\n",
341 blockHeight,
342 FormatMoney(nSubsidy).c_str(),
343 FormatMoney(minerReward).c_str(),
344 dPoolFeePercent,
345 FormatMoney(operatorCut).c_str(),
346 pp.amounts.size(), pp.matureAtHeight, skipped,
347 burned > 0 ? (", " + FormatMoney(burned) + " forfeited (bad address)").c_str()
348 : "");
349}
350
351// Called from event loop: move matured entries to the exec queue (fast).
352// NOT crash-safe, despite the ordering below looking like it is.
353//
354// A matured payout is erased from gPendingPayouts, the file is rewritten
355// without it, and only then does it reach the exec queue -- which lives in
356// memory alone. SavePendingPayouts writes gPendingPayouts and nothing else;
357// there is no exec-queue section of the file to recover from. Die anywhere
358// between the rewrite and SendMoney and those miners are simply never paid,
359// with no record that they were owed anything.
360//
361// Making this safe needs the payment to be idempotent -- record the txid and
362// check the wallet for it on restart -- rather than a reordering, since
363// re-queueing blindly would double-pay whoever was already sent to.
364static void FlushMaturePayouts()
365{
366 std::vector<PendingPayout> ready;
367 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
368 if (nBestHeight < it->matureAtHeight) { ++it; continue; }
369 LogPrint("payout", "[payout] block height %d matured -- queuing %zu payments\n",
370 it->matureAtHeight, it->amounts.size());
371 ready.push_back(*it); // copy first; erase after save
372 ++it;
373 }
374 if (!ready.empty()) {
375 // Erase from pending then save before handing off to exec thread,
376 // so the file never contains an entry that is already being sent.
377 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
378 if (nBestHeight >= it->matureAtHeight)
379 it = gPendingPayouts.erase(it);
380 else
381 ++it;
382 }
383 SavePendingPayouts();
384 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
385 for (auto& pp : ready)
386 gPayoutExecQueue.push_back(std::move(pp));
387 }
388}
389
390// Payout execution thread: drains gPayoutExecQueue, calls SendMoney.
391// Runs independently of the event loop so slow wallet operations
392// (SendMoney takes cs_main) never stall miner I/O.
393static void PayoutThreadFn(void*)
394{
395 gPayoutThreadRunning = true;
396 LogPrint("payout", "[payout] payout thread started\n");
397
398 while (!fShutdown && gPoolRunning) {
399 std::vector<PendingPayout> batch;
400 {
401 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
402 batch.swap(gPayoutExecQueue);
403 }
404
405 for (auto& pp : batch) {
406 LogPrint("payout", "[payout] executing payments for block matured at %d"
407 " (%zu miners)\n", pp.matureAtHeight, pp.amounts.size());
408 for (auto& kv : pp.amounts) {
409 uint160 h160;
410 if (!AddressToHash160(kv.first, h160)) {
411 // Should not happen -- bad addresses are filtered in QueuePayouts
412 LogPrint("payout", "[payout] SKIPPED bad address '%s'\n",
413 kv.first.c_str());
414 continue;
415 }
416 CScript sc;
417 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
418 CWalletTx wtx;
419 wtx.mapValue["comment"] = strprintf("Pool payout height %d",
420 pp.matureAtHeight);
421 if (SendMoney(sc, kv.second, wtx))
422 LogPrint("payout", "[payout] paid %s to %s\n",
423 FormatMoney(kv.second).c_str(), kv.first.c_str());
424 else
425 LogPrint("payout", "[payout] FAILED to pay %s to %s"
426 " (insufficient funds or wallet locked?)\n",
427 FormatMoney(kv.second).c_str(), kv.first.c_str());
428 }
429 }
430
431 Sleep(500); // check for new work every 500ms
432 }
433
434 gPayoutThreadRunning = false;
435 LogPrint("payout", "[payout] payout thread stopped\n");
436}
437
438// ---------------------------------------------------------------------------
439// GUI stats snapshot -- event loop writes, GUI threads read
440// ---------------------------------------------------------------------------
441struct StatsSnapshot {
442 int authorizedMiners = 0;
443 int blocksFound = 0;
444 uint64 roundShares = 0;
445 double totalHashRate = 0.0;
446};
447static StatsSnapshot gStats;
448static std::vector<PoolWorkerStatView> gWorkerStats;
449static std::vector<PendingPayoutView> gPayoutViews;
450static std::mutex gStatsMutex;
451
452void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares, double& totalHashRate)
453{
454 std::lock_guard<std::mutex> lk(gStatsMutex);
455 authorizedMiners = gStats.authorizedMiners;
456 blocksFound = gStats.blocksFound;
457 roundShares = gStats.roundShares;
458 totalHashRate = gStats.totalHashRate;
459}
460
461void GetPoolWorkerStats(std::vector<PoolWorkerStatView>& out)
462{
463 std::lock_guard<std::mutex> lk(gStatsMutex);
464 out = gWorkerStats;
465}
466
467void GetPendingPayouts(std::vector<PendingPayoutView>& out)
468{
469 std::lock_guard<std::mutex> lk(gStatsMutex);
470 out = gPayoutViews;
471}
472
473// ---------------------------------------------------------------------------
474// Miner state -- event loop thread only
475// ---------------------------------------------------------------------------
476static std::atomic<uint32_t> gNextExtranonce2{1};
477
478// Vardiff target: aim for one share every VARDIFF_TARGET_SECS seconds per miner.
479// After VARDIFF_SHARES_PER_CHECK shares, recalculate and adjust if rate is off
480// by more than 50%. Shares are the stable unit; difficulty floats to match.
481static const int64 VARDIFF_TARGET_SECS = 15; // one share every 15s
482static const uint64 VARDIFF_SHARES_PER_CHECK = 5; // re-evaluate every 5 shares
483static const double VARDIFF_RETARGET_RATIO = 1.5; // adjust if >50% off target
484static const double VARDIFF_MIN = 0.001;
485static const double VARDIFF_MAX = 1e12;
486// If a miner has gone this long since its vardiff window opened with ZERO
487// shares, the normal vardiff retarget (which only fires after
488// VARDIFF_SHARES_PER_CHECK shares) can never trigger -- it has no samples to
489// act on. This is the residual gap the ShareTargetFromDifficulty fix above
490// doesn't cover: even with a correctly-easy diff-1 target, a miner stuck in
491// RandomX light mode (or otherwise much slower than expected) can go
492// indefinitely without a single share, and difficulty would never drop to
493// meet it. Force a difficulty cut on a timeout instead of waiting for shares
494// that will never come.
495static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance
496static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much
497
498struct Miner {
499 btf_socket_t fd;
500 bool authorised = false;
501 std::string address;
502 std::string worker;
503 std::string readBuf;
504 int64 connectTime;
505 uint64 sessionShares = 0;
506 uint64 roundShares = 0;
507 uint64 totalShares = 0;
508 int64 lastSeen = 0;
509 uint32_t extranonce2;
510
511 // Vardiff state
512 double difficulty = 1.0; // current share difficulty sent to this miner
513 uint256 shareTarget; // derived from difficulty; what we check against
514 int64 vardiffWindowStart = 0; // when the current vardiff window opened
515 uint64 vardiffShares = 0; // shares found in current window
516
517 // Hashrate estimate -- EWMA of (expected hashes per share at current
518 // difficulty) / (seconds since the previous share). We had all the data
519 // to compute this (difficulty + share timing) but never actually did,
520 // so pool/GUI hashrate always showed as zero/missing.
521 int64 lastShareTime = 0; // GetTime() of the previous accepted share
522 double hashRate = 0.0; // smoothed H/s estimate
523
524 explicit Miner(btf_socket_t f)
525 : fd(f), connectTime(GetTime()),
526 extranonce2(gNextExtranonce2.fetch_add(1)) {}
527};
528
529static std::vector<Miner*> gMiners;
530
531struct FoundBlock { int height; int64 ts; std::string addr; };
532static std::deque<FoundBlock> gFoundBlocks;
533
534// ---------------------------------------------------------------------------
535// Accept queue -- accept threads push; event loop pops at top of each tick
536// ---------------------------------------------------------------------------
537static std::vector<btf_socket_t> gReadyQueue;
538static std::mutex gReadyMutex;
539
540static void PushReady(btf_socket_t s)
541{
542 std::lock_guard<std::mutex> lk(gReadyMutex);
543 gReadyQueue.push_back(s);
544}
545
546// ---------------------------------------------------------------------------
547// I/O helpers -- event loop thread only
548// ---------------------------------------------------------------------------
549
550// Blocking send of a complete JSON line. Called only from the event loop so
551// no other thread is writing to the same fd concurrently.
552static bool SendLine(btf_socket_t fd, const json& j)
553{
554 std::string s = j.dump() + "\n";
555 int off = 0, total = (int)s.size();
556 while (off < total) {
557 int r = send(fd, s.c_str() + off, total - off, SEND_FLAGS);
558 if (r <= 0) return false;
559 off += r;
560 }
561 return true;
562}
563
564// Ceilings on what an unauthenticated peer can make the pool operator hold.
565// Stratum lines are a few hundred bytes and a real pool this size will not see
566// anything near a thousand miners.
567static const size_t MAX_STRATUM_LINE = 64 * 1024;
568static const size_t MAX_POOL_MINERS = 1000;
569
570// Non-blocking recv: pull whatever bytes are available now into buf, then
571// return one complete line if one is ready. Three outcomes:
572// true -- line is filled; buf may still contain more lines
573// false, errno EAGAIN/EWOULDBLOCK (or FIONREAD==0) -- no data right now
574// false, r==0 -- clean disconnect; caller must treat fd as dead
575// Caller distinguishes disconnect from "not yet" by checking whether
576// line.empty() after a false return (line is "" on both paths, so we
577// use a separate out-param for the disconnect signal).
578static bool RecvLine(btf_socket_t fd, std::string& buf,
579 std::string& line, bool& disconnected)
580{
581 disconnected = false;
582 char tmp[4096];
583
584#ifdef _WIN32
585 u_long avail = 0;
586 ioctlsocket(fd, FIONREAD, &avail);
587 if (avail > 0) {
588 int r = recv(fd, tmp, (int)std::min((u_long)(sizeof(tmp)-1), avail), 0);
589 if (r > 0) { tmp[r] = 0; buf += tmp; }
590 else if (r == 0){ disconnected = true; return false; }
591 // r < 0: WSAEWOULDBLOCK -- fall through with what we have
592 }
593#else
594 {
595 int r = recv(fd, tmp, sizeof(tmp) - 1, MSG_DONTWAIT);
596 if (r > 0) { tmp[r] = 0; buf += tmp; }
597 else if (r == 0) { disconnected = true; return false; }
598 // r < 0: EAGAIN/EWOULDBLOCK -- fall through with what we have
599 }
600#endif
601
602 // A peer that never sends a newline would otherwise grow this buffer for
603 // as long as it keeps writing, and it does not have to authorize first --
604 // the read happens before any of that. Stratum lines are a few hundred
605 // bytes; anything past the cap is not a slow line, it is a flood.
606 if (buf.size() > MAX_STRATUM_LINE) {
607 disconnected = true;
608 buf.clear();
609 return false;
610 }
611
612 size_t pos = buf.find('\n');
613 if (pos == std::string::npos) return false; // no complete line yet
614
615 line = buf.substr(0, pos);
616 if (!line.empty() && line.back() == '\r') line.pop_back();
617 buf = buf.substr(pos + 1);
618 return true;
619}
620
621// ---------------------------------------------------------------------------
622// Job helpers
623// ---------------------------------------------------------------------------
624static CBlock BuildMinerBlock(const StratumJob& job, uint32_t extranonce2)
625{
626 CBlock blk = job.block;
627 blk.vtx[0].vin[0].scriptSig << (CBigNum)extranonce2;
628 blk.hashMerkleRoot = blk.BuildMerkleTree();
629 return blk;
630}
631
632// shareTarget is per-miner (set by vardiff) and passed explicitly so the
633// value in p[3] of mining.notify always matches what HandleLine checks on submit.
634// This is the single source of truth for share difficulty.
635static json MakeNotifyParams(const StratumJob& job, const CBlock& b,
636 const uint256& shareTarget, bool clean)
637{
638 unsigned char hdr[80];
639 memcpy(hdr, &b.nVersion, 4);
640 memcpy(hdr+4, &b.hashPrevBlock, 32);
641 memcpy(hdr+36, &b.hashMerkleRoot, 32);
642 memcpy(hdr+68, &b.nTime, 4);
643 memcpy(hdr+72, &b.nBits, 4);
644 memcpy(hdr+76, &b.nNonce, 4);
645 unsigned char tgt[32]; memcpy(tgt, &shareTarget, 32);
646 return json::array({job.jobId, ToHex(hdr, 80),
647 b.hashPrevBlock.GetHex(), ToHex(tgt, 32), clean});
648}
649
650static void SendJob(Miner* m, bool clean)
651{
652 if (!gHaveJob) return;
653 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2);
654 // Use this miner's own shareTarget (set by vardiff) in the notify params.
655 // The participant miner reads p[3] as its share target, and HandleLine
656 // checks m->shareTarget on submit -- both sides now use the same value.
657 json n = {{"id",nullptr},{"method","mining.notify"},
658 {"params", MakeNotifyParams(gCurrentJob, mb, m->shareTarget, clean)}};
659 if (!SendLine(m->fd, n))
660 LogPrint("worker", "[pool->worker] job send FAILED for %s -- miner will be dropped\n",
661 m->address.empty() ? "(not yet authorized)" : m->address.c_str());
662}
663
664static void BroadcastJob(bool clean)
665{
666 if (!gHaveJob) return;
667 int sent = 0;
668 for (Miner* m : gMiners) {
669 if (!m->authorised) continue;
670 SendJob(m, clean);
671 sent++;
672 }
673 LogPrint("pool", "[pool] job %s broadcast to %d miner(s)\n",
674 gCurrentJob.jobId.c_str(), sent);
675}
676
677// ---------------------------------------------------------------------------
678// Stratum message handler -- called for each complete line from a miner
679// Returns false if the miner should be disconnected immediately.
680// ---------------------------------------------------------------------------
681static bool HandleLine(Miner* m, const std::string& rawLine,
682 std::map<std::string, uint64>& roundShareCount,
683 uint64& roundShareTotal,
684 int& blocksFoundThisSession)
685{
686 json req;
687 try {
688 // Fast pre-parse depth check to prevent stack overflow in json::parse.
689 // Legitimate Stratum requests have a depth of at most 4.
690 int depth = 0;
691 bool in_string = false;
692 bool escape = false;
693 for (char c : rawLine) {
694 if (in_string) {
695 if (escape) escape = false;
696 else if (c == '\\') escape = true;
697 else if (c == '"') in_string = false;
698 } else {
699 if (c == '"') in_string = true;
700 else if (c == '{' || c == '[') {
701 if (++depth > 20) {
702 LogPrint("worker", "[worker] JSON too deeply nested from %s\n",
703 m->address.empty() ? "(unauth)" : m->address.c_str());
704 return true;
705 }
706 } else if (c == '}' || c == ']') {
707 depth--;
708 }
709 }
710 }
711 req = json::parse(rawLine);
712 }
713 catch (...) {
714 LogPrint("worker", "[worker] JSON parse error from %s: %.80s\n",
715 m->address.empty() ? "(unauth)" : m->address.c_str(),
716 rawLine.c_str());
717 return true; // don't disconnect on bad JSON, just ignore the line
718 }
719
720 // value() throws when the document is not an object, and again when a key
721 // is present with the wrong type -- "[1,2,3]" and {"method":1} are both
722 // valid JSON that parse cleanly and then blow up on the next two lines.
723 if (!req.is_object() || (req.contains("method") && !req["method"].is_string())) {
724 LogPrint("worker", "[worker] malformed request from %s: %.80s\n",
725 m->address.empty() ? "(unauth)" : m->address.c_str(),
726 rawLine.c_str());
727 return true;
728 }
729
730 json id = req.value("id", json(nullptr));
731 std::string method = req.value("method", "");
732
733 // Reply helper: always sends result+error pair as Stratum requires
734 auto reply = [&](json result, std::string error = "") {
735 json err = error.empty() ? json(nullptr) : json(error);
736 if (!SendLine(m->fd, json{{"id",id},{"result",result},{"error",err}}))
737 LogPrint("worker", "[pool->worker] reply send FAILED for %s (method=%s)\n",
738 m->address.empty() ? "(unauth)" : m->address.c_str(),
739 method.c_str());
740 };
741
742 // ------------------------------------------------------------------
743 if (method == "mining.subscribe") {
744 std::string sid = ToHex(&m->fd, 4);
745 LogPrint("worker", "[worker->pool] subscribe from fd=%d sid=%s\n",
746 (int)m->fd, sid.c_str());
747 reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4}));
748 return true;
749 }
750
751 // ------------------------------------------------------------------
752 if (method == "mining.authorize") {
753 // is_array() matters as much as the element checks: indexing a json
754 // object with a number throws, and size() on an object returns its key
755 // count, so "params":{"a":1} would pass a bare size() test.
756 auto& p = req["params"];
757 bool fArr = p.is_array();
758 m->address = (fArr && p.size() > 0 && p[0].is_string()) ? p[0].get<std::string>() : "";
759 m->worker = (fArr && p.size() > 1 && p[1].is_string()) ? p[1].get<std::string>() : "worker";
760 m->lastSeen = GetTime();
761
762 // Validate address before authorising
763 uint160 h160; bool validAddr = AddressToHash160(m->address, h160);
764
765 m->authorised = true; // authorise regardless -- shares just won't pay out
766 LogPrint("worker", "[worker->pool] authorize address=%s worker=%s addr_valid=%s\n",
767 m->address.c_str(), m->worker.c_str(), validAddr ? "yes" : "NO");
768
769 if (!validAddr)
770 LogPrint("worker", "[pool] WARNING: address '%s' is invalid -- "
771 "miner will hash but shares cannot be paid out\n",
772 m->address.c_str());
773
774 reply(true);
775
776 // Initialize vardiff state and send initial difficulty
777 m->difficulty = 1.0;
778 m->shareTarget = ShareTargetFromDifficulty(m->difficulty);
779 m->vardiffWindowStart = GetTime();
780 m->vardiffShares = 0;
781 LogPrint("worker", "[pool->worker] initial difficulty=%.4f for %s\n",
782 m->difficulty, m->address.c_str());
783 if (!SendLine(m->fd, json{{"id",nullptr},{"method","mining.set_difficulty"},
784 {"params",json::array({m->difficulty})}}))
785 LogPrint("worker", "[pool->worker] set_difficulty send FAILED for %s\n",
786 m->address.c_str());
787
788 if (gHaveJob) {
789 LogPrint("worker", "[pool->worker] sending job %s to %s\n",
790 gCurrentJob.jobId.c_str(), m->address.c_str());
791 SendJob(m, true);
792 } else {
793 LogPrint("worker", "[pool->worker] no job available yet for %s"
794 " -- will be sent one as soon as the pool builds it\n",
795 m->address.c_str());
796 }
797 return true;
798 }
799
800 // ------------------------------------------------------------------
801 if (method == "mining.extranonce.subscribe") { reply(true); return true; }
802 if (method == "mining.get_transactions") { reply(json::array()); return true; }
803
804 // ------------------------------------------------------------------
805 if (method == "mining.submit") {
806 if (!m->authorised) {
807 LogPrint("worker", "[worker->pool] submit from unauthorized miner (fd=%d)\n",
808 (int)m->fd);
809 reply(false, "not authorised");
810 return true;
811 }
812
813 auto& p = req["params"];
814 // Check the shape as well as the length: a submit carrying
815 // "params":[1,2,3] would otherwise reach get<std::string>() and throw
816 // a json::type_error that nothing above catches, taking the pool down
817 // with it. Authorising costs an attacker nothing but an address.
818 if (!p.is_array() || p.size() < 3 || !p[1].is_string() || !p[2].is_string()) {
819 LogPrint("worker", "[worker->pool] submit bad params from %s\n",
820 m->address.c_str());
821 reply(false, "bad params");
822 return true;
823 }
824
825 std::string submitJobId = p[1].get<std::string>();
826 std::string nonceHex = p[2].get<std::string>();
827
828 if (!gHaveJob) {
829 LogPrint("worker", "[worker->pool] submit from %s but pool has no current job\n",
830 m->address.c_str());
831 reply(false, "stale job");
832 return true;
833 }
834 if (gCurrentJob.jobId != submitJobId) {
835 LogPrint("worker", "[worker->pool] submit stale job from %s"
836 " (submitted=%s current=%s)\n",
837 m->address.c_str(), submitJobId.c_str(), gCurrentJob.jobId.c_str());
838 reply(false, "stale job");
839 return true;
840 }
841
842 auto nb = FromHex(nonceHex);
843 if (nb.size() < 4) {
844 LogPrint("worker", "[worker->pool] submit bad nonce from %s: '%s'\n",
845 m->address.c_str(), nonceHex.c_str());
846 reply(false, "bad nonce");
847 return true;
848 }
849 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
850
851 // Rebuild this miner's exact block (unique merkle via extranonce2)
852 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2);
853 b.nNonce = nNonce;
854 unsigned char hdr[80];
855 memcpy(hdr, &b.nVersion, 4);
856 memcpy(hdr+4, &b.hashPrevBlock, 32);
857 memcpy(hdr+36, &b.hashMerkleRoot, 32);
858 memcpy(hdr+68, &b.nTime, 4);
859 memcpy(hdr+72, &b.nBits, 4);
860 memcpy(hdr+76, &b.nNonce, 4);
861
862 uint256 powHash = RandomXPoWHash(hdr, 80);
863
864 // Check against this miner's personal share target (set by vardiff)
865 if (powHash > m->shareTarget) {
866 LogPrint("worker", "[worker->pool] high-hash from %s"
867 " nonce=%u (difficulty=%.4f)\n",
868 m->address.c_str(), nNonce, m->difficulty);
869 reply(false, "high-hash");
870 return true;
871 }
872
873 // Valid share -- record it
874 m->sessionShares++;
875 m->roundShares++;
876 m->totalShares++;
877 m->vardiffShares++;
878 int64 shareTime = GetTime();
879 m->lastSeen = shareTime;
880 roundShareCount[m->address]++;
881 roundShareTotal++;
882
883 // Update hashrate estimate: expected hashes for this share divided by
884 // the time since the last one, smoothed with an EWMA so a single fast
885 // or slow share doesn't make the number jump around.
886 if (m->lastShareTime > 0) {
887 int64 dt = shareTime - m->lastShareTime;
888 if (dt <= 0) dt = 1;
889 double inst = ExpectedHashesForDifficulty(m->difficulty) / (double)dt;
890 const double alpha = 0.25;
891 m->hashRate = (m->hashRate <= 0.0) ? inst
892 : alpha * inst + (1.0 - alpha) * m->hashRate;
893 }
894 m->lastShareTime = shareTime;
895
896 LogPrint("worker", "[worker->pool] valid share from %s"
897 " nonce=%u job=%s difficulty=%.4f (session: %llu)\n",
898 m->address.c_str(), nNonce,
899 gCurrentJob.jobId.c_str(), m->difficulty,
900 (unsigned long long)m->sessionShares);
901 reply(true);
902
903 // Vardiff: after every VARDIFF_SHARES_PER_CHECK shares, check rate
904 if (m->vardiffShares >= VARDIFF_SHARES_PER_CHECK) {
905 int64 elapsed = GetTime() - m->vardiffWindowStart;
906 if (elapsed <= 0) elapsed = 1;
907 double actualSecsPerShare = (double)elapsed / (double)m->vardiffShares;
908 double ratio = actualSecsPerShare / (double)VARDIFF_TARGET_SECS;
909 // ratio < 1 means shares arriving faster than target (too easy)
910 // ratio > 1 means shares arriving slower than target (too hard)
911 if (ratio < (1.0 / VARDIFF_RETARGET_RATIO) ||
912 ratio > VARDIFF_RETARGET_RATIO) {
913 // New difficulty scales inversely with actual share rate
914 double newDiff = m->difficulty / ratio;
915 if (newDiff < VARDIFF_MIN) newDiff = VARDIFF_MIN;
916 if (newDiff > VARDIFF_MAX) newDiff = VARDIFF_MAX;
917 // Only apply if the change is meaningful (>10%)
918 if (newDiff / m->difficulty > 1.1 || m->difficulty / newDiff > 1.1) {
919 m->difficulty = newDiff;
920 m->shareTarget = ShareTargetFromDifficulty(m->difficulty);
921 LogPrint("worker", "[pool] vardiff: %s new difficulty=%.4f"
922 " (%.1fs/share actual vs %ds target)\n",
923 m->address.c_str(), m->difficulty,
924 actualSecsPerShare, (int)VARDIFF_TARGET_SECS);
925 SendLine(m->fd, json{{"id",nullptr},
926 {"method","mining.set_difficulty"},
927 {"params",json::array({m->difficulty})}});
928 // set_difficulty alone doesn't change what the worker
929 // actually hashes against -- it only reads its share
930 // target from mining.notify params[3]. Without this, the
931 // new difficulty wouldn't take effect until the next
932 // natural job broadcast (new block, or the 60s trickle
933 // refresh), so push a fresh notify for this miner now.
934 SendJob(m, false);
935 }
936 }
937 // Reset window
938 m->vardiffWindowStart = GetTime();
939 m->vardiffShares = 0;
940 }
941
942 if (powHash > gCurrentJob.target) return true; // valid share, not a block
943
944 // Full difficulty met -- submit block to the chain
945 LogPrint("pool", "[pool] BLOCK CANDIDATE from %s height=%d nNonce=%u -- submitting\n",
946 m->address.c_str(), gCurrentJob.height, nNonce);
947
948 CBlock* pblock = new CBlock(b);
949 bool accepted = false;
950 CRITICAL_BLOCK(cs_main)
951 {
952 int heightBefore = nBestHeight;
953 if (ProcessBlock(NULL, pblock)) {
954 // ProcessBlock returns true for orphans too -- verify the chain
955 // actually advanced. If nBestHeight didn't increase, the block
956 // was shelved as an orphan (pool was behind the chain tip) and
957 // no coinbase was added to the wallet. Do not pay out.
958 if (nBestHeight > heightBefore) {
959 accepted = true;
960 LogPrint("pool", "[pool] BLOCK ACCEPTED height=%d by %s\n",
961 gCurrentJob.height, m->address.c_str());
962 gFoundBlocks.push_front({gCurrentJob.height, GetTime(), m->address});
963 if (gFoundBlocks.size() > 50) gFoundBlocks.pop_back();
964 blocksFoundThisSession++;
965 gHaveJob = false; // invalidate job immediately
966 } else {
967 LogPrint("pool", "[pool] BLOCK ORPHANED height=%d by %s"
968 " -- pool was behind chain tip, no payout\n",
969 gCurrentJob.height, m->address.c_str());
970 gHaveJob = false; // job is stale regardless
971 }
972 } else {
973 LogPrint("pool", "[pool] BLOCK REJECTED height=%d (ProcessBlock returned false)\n",
974 gCurrentJob.height);
975 delete pblock;
976 }
977 }
978
979 if (accepted) {
980 // Snapshot height before RebuildJob() (which increments it)
981 int foundHeight = gCurrentJob.height;
982 QueuePayouts(foundHeight, roundShareCount, roundShareTotal);
983 // Reset round shares
984 roundShareCount.clear();
985 roundShareTotal = 0;
986 for (Miner* mi : gMiners) mi->roundShares = 0;
987 // Build and broadcast new job immediately
988 if (RebuildJob()) {
989 LogPrint("pool", "[pool] new job %s built after block %d\n",
990 gCurrentJob.jobId.c_str(), foundHeight);
991 BroadcastJob(true);
992 } else {
993 LogPrint("pool", "[pool] WARNING: RebuildJob() failed after block %d"
994 " -- miners will wait\n", foundHeight);
995 }
996 }
997 return true;
998 }
999
1000 // Unknown method -- log it (can reveal miner software quirks)
1001 LogPrint("worker", "[worker->pool] unknown method '%s' from %s\n",
1002 method.c_str(),
1003 m->address.empty() ? "(unauth)" : m->address.c_str());
1004 reply(nullptr, "unknown method");
1005 return true;
1006}
1007
1008// ---------------------------------------------------------------------------
1009// Stats refresh -- called every 2s from event loop
1010// ---------------------------------------------------------------------------
1011static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
1012{
1013 std::lock_guard<std::mutex> lk(gStatsMutex);
1014
1015 gStats.authorizedMiners = 0;
1016 gStats.blocksFound = blocksFoundThisSession;
1017 gStats.roundShares = roundShareTotal;
1018 gStats.totalHashRate = 0.0;
1019 for (Miner* m : gMiners)
1020 if (m->authorised) gStats.authorizedMiners++;
1021
1022 gWorkerStats.clear();
1023 for (Miner* m : gMiners) {
1024 if (!m->authorised || m->lastSeen <= 0) continue;
1025 PoolWorkerStatView v;
1026 v.address = m->address;
1027 v.worker = m->worker;
1028 v.roundShares = m->roundShares;
1029 v.totalShares = m->totalShares;
1030 v.lastSeen = m->lastSeen;
1031 v.hashRate = m->hashRate;
1032 gWorkerStats.push_back(v);
1033 gStats.totalHashRate += m->hashRate;
1034 }
1035 std::sort(gWorkerStats.begin(), gWorkerStats.end(),
1036 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) {
1037 return a.roundShares != b.roundShares ? a.roundShares > b.roundShares
1038 : a.lastSeen > b.lastSeen;
1039 });
1040
1041 // gPendingPayouts is event-loop-only data (no lock needed here since
1042 // RefreshStats is always called from the event loop thread).
1043 gPayoutViews.clear();
1044 for (const PendingPayout& pp : gPendingPayouts) {
1045 PendingPayoutView pv;
1046 pv.matureAtHeight = pp.matureAtHeight;
1047 pv.recipients = (int)pp.amounts.size();
1048 pv.totalAmount = 0;
1049 for (auto& kv : pp.amounts) pv.totalAmount += kv.second;
1050 gPayoutViews.push_back(pv);
1051 }
1052}
1053
1054// ---------------------------------------------------------------------------
1055// Event loop
1056// ---------------------------------------------------------------------------
1057static void PoolEventLoop()
1058{
1059 int lastHeight = -1;
1060 int64 lastRefresh = GetTime();
1061 int64 lastStatsRefresh = 0;
1062 int blocksFoundThisSession = 0;
1063 int64 noJobSince = 0; // 0 = have a job
1064 bool warnedNoJob = false;
1065 int64 lastNoJobRetry = 0; // throttle RebuildJob calls when no tip
1066
1067 std::map<std::string, uint64> roundShareCount;
1068 uint64 roundShareTotal = 0;
1069
1070 static const int64 JOB_REFRESH_SECS = 60; // re-stamp nTime periodically
1071 static const int64 NO_JOB_WARN_SECS = 15; // warn after this long with no job
1072 static const int64 NO_JOB_RETRY_SECS = 5; // how often to retry RebuildJob
1073
1074 while (!fShutdown && gPoolRunning) {
1075
1076 // 1. Drain accept queue
1077 {
1078 std::lock_guard<std::mutex> lk(gReadyMutex);
1079 for (btf_socket_t fd : gReadyQueue) {
1080 // Connecting costs nothing and every accepted socket carries a
1081 // read buffer, so an unbounded miner list is an unbounded
1082 // allocation for whoever runs the pool.
1083 if (gMiners.size() >= MAX_POOL_MINERS) {
1084 LogPrint("worker", "[pool] refusing miner fd=%d --"
1085 " already at %zu connections\n",
1086 (int)fd, gMiners.size());
1087 sock_close(fd);
1088 continue;
1089 }
1090 gMiners.push_back(new Miner(fd));
1091 LogPrint("worker", "[pool] miner fd=%d entered event loop"
1092 " -- awaiting subscribe/authorize\n", (int)fd);
1093 }
1094 gReadyQueue.clear();
1095 }
1096
1097 // 2. Detect new block.
1098 // When our own miner found the block, HandleLine already called
1099 // RebuildJob() + BroadcastJob(true) and set gHaveJob=true.
1100 // We still flush mature payouts and update bookkeeping, but skip
1101 // rebuilding if a fresh job already exists for this height.
1102 {
1103 bool newBlock = false;
1104 CRITICAL_BLOCK(cs_main)
1105 {
1106 if (pindexBest && nBestHeight != lastHeight) {
1107 lastHeight = nBestHeight;
1108 newBlock = true;
1109 }
1110 }
1111 if (newBlock) {
1112 FlushMaturePayouts();
1113 if (!gHaveJob) {
1114 // External block (not found by our miners), or HandleLine's
1115 // RebuildJob failed. Either way this round is over with no
1116 // payout from us, so clear round shares before starting the
1117 // next one -- otherwise they'd keep accumulating across
1118 // rounds that our own miners didn't win.
1119 roundShareCount.clear();
1120 roundShareTotal = 0;
1121 for (Miner* mi : gMiners) mi->roundShares = 0;
1122
1123 // Build and broadcast now.
1124 if (RebuildJob()) {
1125 LogPrint("pool", "[pool] new block height=%d -- job %s ready,"
1126 " %zu miner(s) connected\n",
1127 lastHeight, gCurrentJob.jobId.c_str(), gMiners.size());
1128 BroadcastJob(true);
1129 } else {
1130 LogPrint("pool", "[pool] new block height=%d but RebuildJob() failed"
1131 " -- miners will wait\n", lastHeight);
1132 }
1133 } else {
1134 // HandleLine already built and broadcast for this height.
1135 LogPrint("pool", "[pool] new block height=%d -- job already"
1136 " broadcast by submit handler\n", lastHeight);
1137 }
1138 lastRefresh = GetTime();
1139 noJobSince = 0;
1140 warnedNoJob = false;
1141 }
1142 }
1143
1144 // 3. No-job watchdog (throttled: only retry every NO_JOB_RETRY_SECS)
1145 if (!gHaveJob) {
1146 int64 now = GetTime();
1147 if (noJobSince == 0) noJobSince = now;
1148
1149 if (now - lastNoJobRetry >= NO_JOB_RETRY_SECS) {
1150 lastNoJobRetry = now;
1151 if (RebuildJob()) {
1152 LogPrint("pool", "[pool] job built after %llds without one\n",
1153 (long long)(now - noJobSince));
1154 BroadcastJob(false);
1155 noJobSince = 0;
1156 warnedNoJob = false;
1157 lastRefresh = now;
1158 } else if (!warnedNoJob && now - noJobSince >= NO_JOB_WARN_SECS) {
1159 LogPrint("pool", "[pool] STUCK: no job for %llds -- %s."
1160 " Miners are waiting. Will keep retrying every %llds.\n",
1161 (long long)(now - noJobSince),
1162 pindexBest ? "RebuildJob() keeps failing (check above for why)"
1163 : "no chain tip yet (node still syncing)",
1164 (long long)NO_JOB_RETRY_SECS);
1165 warnedNoJob = true;
1166 }
1167 }
1168 } else if (GetTime() - lastRefresh >= JOB_REFRESH_SECS) {
1169 // Periodic nTime refresh -- stale timestamps cause valid hashes to be rejected
1170 if (RebuildJob()) BroadcastJob(false);
1171 lastRefresh = GetTime();
1172 }
1173
1174 // 3.5. Vardiff stall watchdog -- runs independently of share count.
1175 // The normal vardiff check in HandleLine only fires once
1176 // VARDIFF_SHARES_PER_CHECK shares have arrived; a miner that never
1177 // finds a single share never reaches that check, so it can be stuck
1178 // at its initial (or last) difficulty forever. Catch that here.
1179 for (Miner* m : gMiners) {
1180 if (!m->authorised || m->vardiffShares != 0 || m->vardiffWindowStart <= 0)
1181 continue;
1182 int64 stalled = GetTime() - m->vardiffWindowStart;
1183 if (stalled < VARDIFF_STALL_SECS) continue;
1184
1185 double newDiff = m->difficulty / VARDIFF_STALL_CUT;
1186 if (newDiff < VARDIFF_MIN) newDiff = VARDIFF_MIN;
1187
1188 if (newDiff < m->difficulty) {
1189 m->difficulty = newDiff;
1190 m->shareTarget = ShareTargetFromDifficulty(m->difficulty);
1191 m->vardiffWindowStart = GetTime();
1192 LogPrint("worker", "[pool] vardiff STALL: %s sent zero shares in"
1193 " %llds -- forcing difficulty down to %.6f (no share data"
1194 " to retarget from normally)\n",
1195 m->address.c_str(), (long long)stalled, m->difficulty);
1196 SendLine(m->fd, json{{"id",nullptr},
1197 {"method","mining.set_difficulty"},
1198 {"params",json::array({m->difficulty})}});
1199 // Same reasoning as the normal vardiff retarget above: the
1200 // worker only reads its share target from mining.notify, so
1201 // push a fresh job now instead of waiting on the next
1202 // natural broadcast.
1203 SendJob(m, false);
1204 } else {
1205 // Already at the difficulty floor -- this miner's problem isn't
1206 // difficulty, it's something else (slow hashing, bad connection,
1207 // stuck RandomX light mode, etc). Log once per stall window so
1208 // it's visible without spamming.
1209 LogPrint("worker", "[pool] vardiff STALL: %s still at zero shares"
1210 " after %llds at floor difficulty %.6f -- likely a"
1211 " hashrate/connectivity problem, not a difficulty problem\n",
1212 m->address.c_str(), (long long)stalled, m->difficulty);
1213 m->vardiffWindowStart = GetTime(); // avoid re-logging every tick
1214 }
1215 }
1216
1217 // 4. Select over miner sockets
1218 if (!gMiners.empty()) {
1219 fd_set fds; FD_ZERO(&fds);
1220 int maxfd = 0;
1221 for (Miner* m : gMiners) {
1222 FD_SET(m->fd, &fds);
1223 if ((int)m->fd > maxfd) maxfd = (int)m->fd;
1224 }
1225 struct timeval tv = {0, 50000}; // 50ms
1226 if (select(maxfd + 1, &fds, NULL, NULL, &tv) > 0) {
1227 std::vector<Miner*> dead;
1228 for (Miner* m : gMiners) {
1229 if (!FD_ISSET(m->fd, &fds)) continue;
1230 std::string line;
1231 bool disc = false;
1232 // Drain all complete lines buffered for this miner.
1233 // HandleLine always returns true; disc signals clean disconnect.
1234 // ThreadRPCServer has no handler of its own, so anything
1235 // thrown here would reach the top of the thread and
1236 // terminate the whole node -- not just the pool. Every
1237 // line arrives from an unauthenticated socket.
1238 while (RecvLine(m->fd, m->readBuf, line, disc)) {
1239 try {
1240 HandleLine(m, line, roundShareCount,
1241 roundShareTotal, blocksFoundThisSession);
1242 } catch (const std::exception& e) {
1243 LogPrint("worker", "[pool] dropping miner %s --"
1244 " exception handling line: %s\n",
1245 m->address.empty() ? "(unauth)" : m->address.c_str(),
1246 e.what());
1247 disc = true;
1248 break;
1249 } catch (...) {
1250 LogPrint("worker", "[pool] dropping miner %s --"
1251 " unknown exception handling line\n",
1252 m->address.empty() ? "(unauth)" : m->address.c_str());
1253 disc = true;
1254 break;
1255 }
1256 }
1257 if (disc) dead.push_back(m);
1258 }
1259 for (Miner* m : dead) {
1260 LogPrint("worker", "[pool] miner %s disconnected"
1261 " (session shares: %llu, round shares: %llu)\n",
1262 m->worker.empty() ? "(never authorized)" : m->worker.c_str(),
1263 (unsigned long long)m->sessionShares,
1264 (unsigned long long)m->roundShares);
1265 sock_close(m->fd);
1266 gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m));
1267 delete m;
1268 }
1269 }
1270 } else {
1271 Sleep(50);
1272 }
1273
1274 // 5. Refresh GUI stats every 2s
1275 if (GetTime() - lastStatsRefresh >= 2) {
1276 RefreshStats(blocksFoundThisSession, roundShareTotal);
1277 lastStatsRefresh = GetTime();
1278 }
1279 }
1280
1281 for (Miner* m : gMiners) { sock_close(m->fd); delete m; }
1282 gMiners.clear();
1283}
1284
1285// ---------------------------------------------------------------------------
1286// Accept threads
1287// ---------------------------------------------------------------------------
1288struct AcceptCtx { unsigned char pk[32]; unsigned char sk[32]; std::string relay; };
1289
1290// One persistent thread per relay. Loops: register -> wrap -> push -> repeat.
1291static void AcceptOneFn(void* arg)
1292{
1293 AcceptCtx* ctx = (AcceptCtx*)arg;
1294 unsigned char pk[32], sk[32];
1295 memcpy(pk, ctx->pk, 32); memcpy(sk, ctx->sk, 32);
1296 std::string relay = ctx->relay;
1297 delete ctx;
1298
1299 size_t colon = relay.rfind(':');
1300 if (colon == std::string::npos) {
1301 LogPrint("pool", "[pool] AcceptOneFn: bad relay address '%s'\n", relay.c_str());
1302 return;
1303 }
1304 std::string host = relay.substr(0, colon);
1305 int port = atoi(relay.substr(colon + 1).c_str());
1306
1307 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str());
1308
1309 while (!fShutdown && gPoolRunning) {
1310 // Blocks until a miner connects to us at this relay
1311 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
1312
1313 if (fShutdown || !gPoolRunning) {
1314 if (rv != btf::RV_INVALID) btf::RvClose(rv);
1315 break;
1316 }
1317 if (rv == btf::RV_INVALID) {
1318 LogPrint("pool", "[pool] relay %s: RvServiceRegister failed"
1319 " -- backing off 30s before retry\n", relay.c_str());
1320 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000);
1321 continue;
1322 }
1323
1324 // Echan handshake -- blocking but fast (local + relay round-trip)
1325 btf_socket_t fd = btf::BtfServiceWrap(rv, sk);
1326 if (fd == INVALID_SOCKET) {
1327 LogPrint("pool", "[pool] relay %s: BtfServiceWrap failed (echan handshake)"
1328 " -- miner dropped, will accept next\n", relay.c_str());
1329 continue;
1330 }
1331
1332 LogPrint("worker", "[pool] miner arrived via relay %s -- pushing to event loop\n",
1333 relay.c_str());
1334 PushReady(fd);
1335 // Loop immediately: register again so the next miner can connect
1336 }
1337
1338 LogPrint("pool", "[pool] accept thread stopped for relay %s\n", relay.c_str());
1339}
1340
1341// Spawns one AcceptOneFn per relay. Re-polls relay list every 60s to pick up
1342// newly discovered relays. Uses a set to avoid spawning duplicate threads.
1343static void AcceptLoopFn(void*)
1344{
1345 LogPrint("pool", "[pool] AcceptLoopFn started -- miners can connect via %s\n",
1346 BtfLocalAddress().c_str());
1347
1348 // Wait for our identity to be ready
1349 unsigned char pk[32], sk[32];
1350 int waitSecs = 0;
1351 while (!BtfGetIdentity(pk, sk)) {
1352 if (fShutdown || !gPoolRunning) return;
1353 if (waitSecs == 0)
1354 LogPrint("pool", "[pool] waiting for .btf identity...\n");
1355 waitSecs++;
1356 Sleep(2000);
1357 }
1358 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2);
1359
1360 std::set<std::string> spawned; // relays we've already started a thread for
1361
1362 while (!fShutdown && gPoolRunning) {
1363 vector<string> relays = BtfAllRelays();
1364 if (relays.empty()) {
1365 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n");
1366 } else {
1367 for (const string& relay : relays) {
1368 if (spawned.count(relay)) continue; // already running
1369 AcceptCtx* ctx = new AcceptCtx();
1370 memcpy(ctx->pk, pk, 32); memcpy(ctx->sk, sk, 32);
1371 ctx->relay = relay;
1372 uintptr_t tid = _beginthread(AcceptOneFn, 0, ctx);
1373 if (tid == (uintptr_t)-1) {
1374 delete ctx;
1375 LogPrint("pool", "[pool] failed to spawn accept thread for %s\n",
1376 relay.c_str());
1377 } else {
1378 spawned.insert(relay);
1379 LogPrint("pool", "[pool] accept thread spawned for relay %s"
1380 " (%zu total)\n", relay.c_str(), spawned.size());
1381 }
1382 }
1383 }
1384 // Re-poll every 60s -- newly discovered relays get picked up
1385 for (int i = 0; i < 60 && !fShutdown && gPoolRunning; i++) Sleep(1000);
1386 }
1387}
1388
1389// ---------------------------------------------------------------------------
1390// Entry point
1391// ---------------------------------------------------------------------------
1392void ThreadRPCServer(void*)
1393{
1394 gPoolRunning = true;
1395
1396 LogPrint("pool", "[pool] ============================================\n");
1397 LogPrint("pool", "[pool] Pool server starting\n");
1398 LogPrint("pool", "[pool] .btf address: %s\n", BtfLocalAddress().c_str());
1399 LogPrint("pool", "[pool] fee: %.2f%%\n", dPoolFeePercent);
1400
1401 if (RebuildJob()) {
1402 LogPrint("pool", "[pool] initial job %s seeded at height %d\n",
1403 gCurrentJob.jobId.c_str(), nBestHeight + 1);
1404 } else {
1405 LogPrint("pool", "[pool] no chain tip yet -- job will build on first block\n");
1406 }
1407
1408 LoadPendingPayouts();
1409
1410 _beginthread(AcceptLoopFn, 0, NULL);
1411 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL);
1412 _beginthread(PayoutThreadFn, 0, NULL);
1413
1414 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown
1415
1416 LogPrint("pool", "[pool] pool server stopped\n");
1417 LogPrint("pool", "[pool] ============================================\n");
1418 gPoolRunning = false;
1419}