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.cpp708
1 files changed, 708 insertions, 0 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
new file mode 100644
index 0000000..99d182e
--- /dev/null
+++ b/src/rpc.cpp
@@ -0,0 +1,708 @@
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-only pool listener.
5// Miners connect through rendezvous tunnels; no raw IP:port endpoints remain.
6// Pending payouts are held until maturity and shown in the GUI.
7//
8// Compiles on both Windows (mingw) and Linux unchanged.
9// Launched from ui.cpp (GUI) and headless.cpp (node) via ThreadRPCServer().
10
11// nlohmann must be included before headers.h because util.h redefines snprintf
12#pragma push_macro("snprintf")
13#undef snprintf
14#include <nlohmann/json.hpp>
15#pragma pop_macro("snprintf")
16
17#include "headers.h"
18#ifdef snprintf
19#undef snprintf
20#endif
21#include "btftunnel.h"
22
23// POSIX socket headers — already provided by compat.h on Windows,
24// and by the OS directly on Linux.
25#ifndef _WIN32
26#include <sys/socket.h>
27#include <sys/select.h>
28#include <netinet/in.h>
29#include <arpa/inet.h>
30#include <unistd.h>
31#endif
32
33#include <atomic>
34#include <mutex>
35#include <map>
36#include <deque>
37#include <sstream>
38#include <iomanip>
39#include <ctime>
40
41using json = nlohmann::json;
42
43// Cross-platform socket close / send flags
44#ifdef _WIN32
45#define sock_close(s) closesocket(s)
46#define SEND_FLAGS 0
47#else
48#define sock_close(s) ::close(s)
49#define SEND_FLAGS MSG_NOSIGNAL
50#endif
51
52// Cross-platform thread launch: wraps _beginthread (compat.h maps it to
53// pthread_create+detach on Linux, native _beginthread on Windows).
54// fn must be void(*)(void*).
55static void LaunchThread(void (*fn)(void*), void* arg)
56{
57 _beginthread(fn, 0, arg);
58}
59
60// ---------------------------------------------------------------------------
61// Config — set by headless.cpp / ui.cpp before threads launch
62// ---------------------------------------------------------------------------
63std::string gRpcUser;
64std::string gRpcPassword;
65
66// Pool server running flag — set false to stop server cleanly (all loops check it)
67volatile bool gPoolRunning = false;
68
69// ---------------------------------------------------------------------------
70// Stubs for symbols absent from Bitflash 0.1.0
71// ---------------------------------------------------------------------------
72double dHashesPerSec = 0.0;
73int64 nHPSTimerStart = 0;
74
75static bool IsInitialBlockDownload()
76{
77 return (pindexBest == NULL || nBestHeight < 1);
78}
79
80// ---------------------------------------------------------------------------
81// Hex helpers
82// ---------------------------------------------------------------------------
83static std::string ToHex(const void* p, size_t n)
84{
85 const unsigned char* b = (const unsigned char*)p;
86 std::string s; s.reserve(n*2);
87 static const char* H = "0123456789abcdef";
88 for (size_t i = 0; i < n; i++) { s += H[b[i]>>4]; s += H[b[i]&0xf]; }
89 return s;
90}
91static std::vector<unsigned char> FromHex(const std::string& s)
92{
93 std::vector<unsigned char> v;
94 for (size_t i = 0; i+1 < s.size(); i += 2) {
95 auto h=[](char c)->int{
96 if(c>='0'&&c<='9') return c-'0';
97 if(c>='a'&&c<='f') return c-'a'+10;
98 if(c>='A'&&c<='F') return c-'A'+10;
99 return 0;
100 };
101 v.push_back((unsigned char)((h(s[i])<<4)|h(s[i+1])));
102 }
103 return v;
104}
105
106// ---------------------------------------------------------------------------
107// PPLNS share tracking
108// ---------------------------------------------------------------------------
109struct Share {
110 std::string minerAddress;
111 int64 timestamp;
112 uint64 difficulty;
113};
114
115struct MinerStats {
116 std::string address;
117 std::string worker;
118 uint64 totalShares = 0;
119 uint64 roundShares = 0;
120 int64 lastSeen = 0;
121};
122static std::mutex gMinerStatsMutex;
123static std::map<std::string, MinerStats> gMinerStats;
124
125struct FoundBlock {
126 int height;
127 int64 timestamp;
128 std::string minerAddress;
129 uint256 hash;
130};
131static std::mutex gBlocksMutex;
132static std::deque<FoundBlock> gFoundBlocks;
133
134struct Miner;
135static std::mutex gMinersMutex;
136static std::vector<Miner*> gMiners;
137
138static std::string MinerKey(const std::string& address, const std::string& worker)
139{
140 return address + "\n" + worker;
141}
142
143void GetPoolWorkerStats(std::vector<PoolWorkerStatView>& out)
144{
145 out.clear();
146 int64 now = GetTime();
147 std::lock_guard<std::mutex> lk(gMinerStatsMutex);
148 for (std::map<std::string, MinerStats>::const_iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it)
149 {
150 const MinerStats& ms = it->second;
151 if (ms.lastSeen <= 0)
152 continue;
153 if (now - ms.lastSeen > 15 * 60)
154 continue;
155 PoolWorkerStatView view;
156 view.address = ms.address;
157 view.worker = ms.worker;
158 view.totalShares = ms.totalShares;
159 view.roundShares = ms.roundShares;
160 view.lastSeen = ms.lastSeen;
161 out.push_back(view);
162 }
163 std::sort(out.begin(), out.end(), [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) {
164 if (a.roundShares != b.roundShares) return a.roundShares > b.roundShares;
165 if (a.totalShares != b.totalShares) return a.totalShares > b.totalShares;
166 return a.lastSeen > b.lastSeen;
167 });
168}
169
170// ---------------------------------------------------------------------------
171// Current mining job
172// ---------------------------------------------------------------------------
173struct StratumJob {
174 std::string jobId;
175 CBlock block;
176 uint256 target;
177 uint256 shareTarget;
178 int height;
179};
180
181static std::mutex gJobMutex;
182static StratumJob gCurrentJob;
183static bool gHaveJob = false;
184static std::atomic<uint32_t> gJobSeq{0};
185
186static uint256 MakeShareTarget(const uint256& blockTarget)
187{
188 uint256 shareTarget = blockTarget;
189 shareTarget <<= 8;
190 if (shareTarget < blockTarget)
191 shareTarget = ~uint256(0);
192 return shareTarget;
193}
194
195static bool RebuildJob()
196{
197 if (!pindexBest) return false;
198
199 CBlockIndex* pindexPrev = pindexBest;
200 unsigned int nBits = GetNextWorkRequired(pindexPrev);
201
202 CKey key;
203 key.MakeNewKey();
204 static std::atomic<uint32_t> sExtra{0};
205 uint32_t extraNonce = ++sExtra;
206
207 CTransaction txNew;
208 txNew.vin.resize(1);
209 txNew.vin[0].prevout.SetNull();
210 txNew.vin[0].scriptSig << nBits << (CBigNum)extraNonce;
211 txNew.vout.resize(1);
212 // Always pay coinbase to a fresh wallet key. DoPayouts() distributes
213 // shares to miners via SendMoney() after the block is accepted.
214 txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG;
215
216 CBlock block;
217 block.vtx.push_back(txNew);
218
219 {
220 CTxDB txdb("r");
221 CRITICAL_BLOCK(cs_mapTransactions)
222 {
223 map<uint256,CTxIndex> pool;
224 unsigned int sz = 0;
225 for (auto& kv : mapTransactions) {
226 CTransaction& tx = kv.second;
227 if (tx.IsCoinBase() || !tx.IsFinal()) continue;
228 int64 nFees = 0;
229 int64 nMinFee = tx.GetMinFee(block.vtx.size() < 100);
230 map<uint256,CTxIndex> tmp(pool);
231 if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, false, true, nMinFee)) continue;
232 pool = tmp;
233 block.vtx.push_back(tx);
234 sz += ::GetSerializeSize(tx, SER_NETWORK);
235 if (sz > MAX_SIZE/2) break;
236 }
237 }
238 }
239 block.vtx[0].vout[0].nValue = block.GetBlockValue(0);
240 block.hashPrevBlock = pindexPrev->GetBlockHash();
241 block.hashMerkleRoot = block.BuildMerkleTree();
242 block.nTime = max((unsigned int)(pindexPrev->GetMedianTimePast()+1),
243 (unsigned int)GetAdjustedTime());
244 block.nBits = nBits;
245 block.nNonce = 1;
246
247 AddKey(key);
248
249 StratumJob job;
250 job.jobId = ToHex(&extraNonce, 4);
251 job.block = block;
252 job.target = CBigNum().SetCompact(nBits).getuint256();
253 job.shareTarget = MakeShareTarget(job.target);
254 job.height = nBestHeight + 1;
255
256 std::lock_guard<std::mutex> lk(gJobMutex);
257 gCurrentJob = job;
258 gHaveJob = true;
259 ++gJobSeq;
260 return true;
261}
262
263// ---------------------------------------------------------------------------
264// PPLNS payout
265// ---------------------------------------------------------------------------
266// ---------------------------------------------------------------------------
267// Deferred payouts — coinbase outputs require COINBASE_MATURITY (100) blocks
268// before they can be spent. We store the share snapshot when a block is found
269// and execute the actual SendMoney() calls once the coinbase matures.
270// ---------------------------------------------------------------------------
271struct PendingPayout {
272 int matureAtHeight; // pay out when nBestHeight >= this
273 std::map<std::string, int64> amounts; // addr -> BTF amount
274};
275
276static std::mutex gPayoutMutex;
277static std::vector<PendingPayout> gPendingPayouts;
278
279void GetPendingPayouts(std::vector<PendingPayoutView>& out)
280{
281 out.clear();
282 std::lock_guard<std::mutex> lk(gPayoutMutex);
283 for (const PendingPayout& p : gPendingPayouts)
284 {
285 PendingPayoutView view;
286 view.matureAtHeight = p.matureAtHeight;
287 view.recipients = (int)p.amounts.size();
288 view.totalAmount = 0;
289 for (const std::map<std::string, int64>::value_type& kv : p.amounts)
290 view.totalAmount += kv.second;
291 out.push_back(view);
292 }
293}
294
295// Called when a block is found: snapshot shares, schedule payout for maturity.
296static void QueuePayouts(int blockHeight)
297{
298 if (nMineMode != MINE_OPERATOR) return;
299
300 std::map<std::string, uint64> shareCount;
301 uint64 total = 0;
302 {
303 std::lock_guard<std::mutex> lk(gMinerStatsMutex);
304 for (std::map<std::string, MinerStats>::iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it)
305 {
306 MinerStats& ms = it->second;
307 if (ms.roundShares == 0)
308 continue;
309 if (ms.address.empty())
310 continue;
311 shareCount[ms.address] += ms.roundShares;
312 total += ms.roundShares;
313 ms.roundShares = 0;
314 }
315 }
316 if (total == 0) return;
317
318 int64 blockReward = CBlock().GetBlockValue(0);
319 PendingPayout pp;
320 pp.matureAtHeight = blockHeight + COINBASE_MATURITY;
321 for (auto& kv : shareCount) {
322 int64 amount = (int64)(((double)kv.second / (double)total) * (double)blockReward);
323 if (amount >= CENT)
324 pp.amounts[kv.first] = amount;
325 }
326
327 { std::lock_guard<std::mutex> lk(gPayoutMutex); gPendingPayouts.push_back(pp); }
328
329 printf("Pool: block %d payout queued for height %d (%zu miners)\n",
330 blockHeight, pp.matureAtHeight, pp.amounts.size());
331}
332
333// Called from JobWatcher every new block: execute any matured payouts.
334static void FlushMaturePayouts()
335{
336 if (nMineMode != MINE_OPERATOR) return;
337
338 std::lock_guard<std::mutex> lk(gPayoutMutex);
339 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
340 if (nBestHeight < it->matureAtHeight) { ++it; continue; }
341
342 printf("Pool: executing payout for block matured at height %d\n", it->matureAtHeight);
343 for (auto& kv : it->amounts) {
344 uint160 hash160;
345 if (!AddressToHash160(kv.first, hash160)) continue;
346 CScript scriptPubKey;
347 scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG;
348 CWalletTx wtx;
349 wtx.mapValue["comment"] = strprintf("Pool payout height %d", it->matureAtHeight);
350 if (SendMoney(scriptPubKey, kv.second, wtx))
351 printf("Pool: paid %s to %s\n", FormatMoney(kv.second).c_str(), kv.first.c_str());
352 else
353 printf("Pool: payout FAILED for %s (will not retry)\n", kv.first.c_str());
354 }
355 it = gPendingPayouts.erase(it);
356 }
357}
358
359// ---------------------------------------------------------------------------
360// Stratum protocol
361// ---------------------------------------------------------------------------
362static json MakeNotifyParams(const StratumJob& job, bool clean)
363{
364 CBlock& b = const_cast<CBlock&>(job.block);
365 unsigned char hdr[80];
366 memcpy(hdr, &b.nVersion, 4);
367 memcpy(hdr+4, &b.hashPrevBlock, 32);
368 memcpy(hdr+36, &b.hashMerkleRoot, 32);
369 memcpy(hdr+68, &b.nTime, 4);
370 memcpy(hdr+72, &b.nBits, 4);
371 memcpy(hdr+76, &b.nNonce, 4);
372 unsigned char shareTgt[32]; memcpy(shareTgt, &job.shareTarget, 32);
373 return json::array({job.jobId, ToHex(hdr,80), b.hashPrevBlock.GetHex(), ToHex(shareTgt,32), clean});
374}
375
376struct Miner {
377 SOCKET fd;
378 bool authorised;
379 std::string address;
380 std::string worker;
381 std::string readBuf;
382 int64 connectTime;
383 uint64 sessionShares;
384 Miner(SOCKET f) : fd(f), authorised(false), connectTime(GetTime()), sessionShares(0) {}
385};
386
387void GetPoolOperatorStats(int& authorizedMiners, int& blocksFound, uint64& roundShares)
388{
389 authorizedMiners = 0;
390 blocksFound = 0;
391 roundShares = 0;
392 {
393 std::lock_guard<std::mutex> lk(gMinersMutex);
394 for (Miner* m : gMiners)
395 if (m->authorised)
396 authorizedMiners++;
397 }
398 {
399 std::lock_guard<std::mutex> lk(gBlocksMutex);
400 blocksFound = (int)gFoundBlocks.size();
401 }
402 {
403 std::lock_guard<std::mutex> lk(gMinerStatsMutex);
404 for (std::map<std::string, MinerStats>::const_iterator it = gMinerStats.begin(); it != gMinerStats.end(); ++it)
405 roundShares += it->second.roundShares;
406 }
407}
408
409static bool SendLine(Miner* m, const json& j)
410{
411 std::string s = j.dump() + "\n";
412 int off = 0;
413 while (off < (int)s.size()) {
414 int n = send(m->fd, s.c_str() + off, (int)s.size() - off, SEND_FLAGS);
415 if (n <= 0)
416 return false;
417 off += n;
418 }
419 return true;
420}
421
422static void BroadcastJob(bool clean)
423{
424 StratumJob job;
425 { std::lock_guard<std::mutex> lk(gJobMutex); if(!gHaveJob) return; job=gCurrentJob; }
426 json n = {{"id",nullptr},{"method","mining.notify"},{"params",MakeNotifyParams(job,clean)}};
427 std::lock_guard<std::mutex> lk(gMinersMutex);
428 for (Miner* m : gMiners)
429 if (m->authorised && !SendLine(m, n))
430 printf("Stratum: failed to broadcast job to %s\n", m->address.c_str());
431}
432
433static void HandleLine(Miner* m, const std::string& line)
434{
435 json req;
436 try { req = json::parse(line); } catch(...){ return; }
437
438 json id = req.value("id", json(nullptr));
439 std::string method = req.value("method","");
440
441 auto reply = [&](json result, json error=nullptr){
442 if (!SendLine(m, json{{"id",id},{"result",result},{"error",error}}))
443 printf("Stratum: failed to reply to miner %s\n", m->address.c_str());
444 };
445
446 if (method == "mining.subscribe") {
447 std::string sid = ToHex(&m->fd, 4);
448 printf("Stratum: miner subscribed %s\n", sid.c_str());
449 reply(json::array({json::array({json::array({"mining.notify",sid})}),sid,4}));
450 return;
451 }
452
453 if (method == "mining.authorize") {
454 auto& p = req["params"];
455 m->address = (p.size()>0 && p[0].is_string()) ? p[0].get<std::string>() : "";
456 m->worker = (p.size()>1 && p[1].is_string()) ? p[1].get<std::string>() : "worker";
457 m->authorised = true;
458 printf("Stratum: miner authorized address=%s worker=%s\n", m->address.c_str(), m->worker.c_str());
459 reply(true);
460
461 uint160 h; bool valid = AddressToHash160(m->address, h);
462 if (!valid) printf("Stratum: miner %s invalid address '%s' — shares won't pay out\n",
463 m->worker.c_str(), m->address.c_str());
464
465 { std::lock_guard<std::mutex> lk(gMinerStatsMutex);
466 auto& ms = gMinerStats[m->address];
467 ms.lastSeen = GetTime(); ms.worker = m->worker; }
468
469 if (!SendLine(m, json{{"id",nullptr},{"method","mining.set_difficulty"},{"params",json::array({1})}}))
470 printf("Stratum: failed to send difficulty to %s\n", m->address.c_str());
471
472 StratumJob job; bool have;
473 { std::lock_guard<std::mutex> lk(gJobMutex); have=gHaveJob; if(have) job=gCurrentJob; }
474 if (have) {
475 printf("Stratum: sending job %s to %s\n", job.jobId.c_str(), m->address.c_str());
476 if (!SendLine(m, json{{"id",nullptr},{"method","mining.notify"},{"params",MakeNotifyParams(job,true)}}))
477 printf("Stratum: failed to send job %s to %s\n", job.jobId.c_str(), m->address.c_str());
478 } else {
479 printf("Stratum: no job available for %s\n", m->address.c_str());
480 }
481 return;
482 }
483
484 if (method == "mining.extranonce.subscribe") { reply(true); return; }
485
486 if (method == "mining.submit") {
487 if (!m->authorised) { reply(false,"not authorised"); return; }
488
489 auto& p = req["params"];
490 if (p.size() < 4) { reply(false,"bad params"); return; }
491
492 std::string jobId = p[1].get<std::string>();
493 std::string nonceHex = p[2].get<std::string>();
494
495 StratumJob job;
496 { std::lock_guard<std::mutex> lk(gJobMutex);
497 if (!gHaveJob || gCurrentJob.jobId != jobId) { reply(false,"stale job"); return; }
498 job = gCurrentJob; }
499
500 auto nb = FromHex(nonceHex);
501 if (nb.size()<4) { reply(false,"bad nonce"); return; }
502 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
503
504 CBlock& b = job.block;
505 b.nNonce = nNonce;
506 unsigned char hdr[80];
507 memcpy(hdr, &b.nVersion, 4);
508 memcpy(hdr+4, &b.hashPrevBlock, 32);
509 memcpy(hdr+36, &b.hashMerkleRoot,32);
510 memcpy(hdr+68, &b.nTime, 4);
511 memcpy(hdr+72, &b.nBits, 4);
512 memcpy(hdr+76, &b.nNonce, 4);
513
514 uint256 powHash = RandomXPoWHash(hdr, 80);
515
516 // Record share in the current round.
517 std::string key = MinerKey(m->address, m->worker);
518 { std::lock_guard<std::mutex> lk(gMinerStatsMutex);
519 MinerStats& ms = gMinerStats[key];
520 ms.address = m->address;
521 ms.worker = m->worker;
522 ms.roundShares++;
523 ms.totalShares++;
524 ms.lastSeen = GetTime(); }
525 m->sessionShares++;
526
527 if (powHash > job.shareTarget) { reply(true); return; } // valid share, not a full block
528
529 // Full block
530 CBlock* pblock = new CBlock(b);
531 bool accepted = false;
532 CRITICAL_BLOCK(cs_main)
533 {
534 if (ProcessBlock(NULL, pblock)) {
535 accepted = true;
536 printf("Pool: BLOCK FOUND by %s height=%d nNonce=%u\n",
537 m->address.c_str(), job.height, nNonce);
538 FoundBlock fb{job.height, GetTime(), m->address, pblock->GetHash()};
539 { std::lock_guard<std::mutex> lk(gBlocksMutex);
540 gFoundBlocks.push_front(fb);
541 if (gFoundBlocks.size()>50) gFoundBlocks.pop_back(); }
542 { std::lock_guard<std::mutex> lk(gJobMutex); gHaveJob=false; }
543 } else {
544 delete pblock;
545 }
546 }
547 reply(accepted);
548 if (accepted) { QueuePayouts(job.height); RebuildJob(); BroadcastJob(true); }
549 return;
550 }
551
552 if (method == "mining.get_transactions") { reply(json::array()); return; }
553 reply(nullptr, "unknown method");
554}
555
556static void MinerThreadFn(void* arg)
557{
558 Miner* m = (Miner*)arg;
559 while (!fShutdown && gPoolRunning) {
560 fd_set fds; FD_ZERO(&fds); FD_SET(m->fd, &fds);
561 struct timeval tv={1,0};
562 if (select((int)m->fd+1, &fds, NULL, NULL, &tv) <= 0) continue;
563 char buf[4096];
564 int n = recv(m->fd, buf, sizeof(buf)-1, 0);
565 if (n <= 0) break;
566 buf[n]=0; m->readBuf += buf;
567 size_t pos;
568 while ((pos = m->readBuf.find('\n')) != std::string::npos) {
569 std::string line = m->readBuf.substr(0, pos);
570 m->readBuf = m->readBuf.substr(pos+1);
571 if (!line.empty() && line.back()=='\r') line.pop_back();
572 if (!line.empty()) HandleLine(m, line);
573 }
574 }
575 sock_close(m->fd);
576 printf("Stratum: miner %s disconnected (%llu shares)\n",
577 m->worker.empty()?"unknown":m->worker.c_str(),
578 (unsigned long long)m->sessionShares);
579 { std::lock_guard<std::mutex> lk(gMinersMutex);
580 auto it = std::find(gMiners.begin(), gMiners.end(), m);
581 if (it != gMiners.end()) gMiners.erase(it); }
582 delete m;
583}
584
585static void JobWatcherFn(void*)
586{
587 int lastHeight = -1;
588 while (!fShutdown && gPoolRunning) {
589 Sleep(500);
590 bool newBlock = false;
591 CRITICAL_BLOCK(cs_main)
592 { if (nBestHeight != lastHeight && pindexBest) { lastHeight = nBestHeight; newBlock = true; } }
593 if (newBlock) {
594 FlushMaturePayouts();
595 if (RebuildJob()) { printf("Stratum: new job height=%d\n", lastHeight+1); BroadcastJob(true); }
596 } else if (!gHaveJob) {
597 if (RebuildJob()) BroadcastJob(false);
598 }
599 }
600}
601
602// ---------------------------------------------------------------------------
603// Main Stratum accept loop — entry point called from ui.cpp and headless.cpp
604// ---------------------------------------------------------------------------
605// .btf Stratum listener — miners connect via rendezvous relay using the
606// node's .btf address instead of a raw IP:port. Same Stratum protocol,
607// same MinerThreadFn handler, works through CGNAT with no port forwarding.
608//
609// Keeps one listener registered per relay at all times. When a miner
610// connects, a replacement listener is spawned immediately so the next
611// miner doesn't have to wait.
612// ---------------------------------------------------------------------------
613
614struct BtfAcceptCtx { unsigned char pk[32]; unsigned char sk[32]; string relay; };
615
616// Track active listener count per relay to avoid pile-up when relay is slow
617static std::map<string,int> gBtfListenerCount;
618static std::mutex gBtfListenerMutex;
619
620static void BtfStratumAcceptOneFn(void* arg)
621{
622 BtfAcceptCtx* ctx = (BtfAcceptCtx*)arg;
623 unsigned char pk[32], sk[32];
624 memcpy(pk, ctx->pk, 32); memcpy(sk, ctx->sk, 32);
625 string relay = ctx->relay;
626 delete ctx;
627
628 auto decrement = [&]() {
629 std::lock_guard<std::mutex> lk(gBtfListenerMutex);
630 gBtfListenerCount[relay]--;
631 };
632
633 size_t colon = relay.rfind(':');
634 if (colon == string::npos) { decrement(); return; }
635 string strHost = relay.substr(0, colon);
636 int nPort = atoi(relay.substr(colon+1).c_str());
637
638 btf::RvSocket rv = btf::RvServiceRegister(strHost.c_str(), (unsigned short)nPort, pk);
639 if (fShutdown || !gPoolRunning) { if (rv != btf::RV_INVALID) btf::RvClose(rv); decrement(); return; }
640 if (rv == btf::RV_INVALID) { decrement(); return; }
641
642 // Paired: spawn replacement (count transfers to new thread)
643 if (!fShutdown && gPoolRunning) {
644 BtfAcceptCtx* next = new BtfAcceptCtx();
645 memcpy(next->pk, pk, 32); memcpy(next->sk, sk, 32);
646 next->relay = relay;
647 LaunchThread(BtfStratumAcceptOneFn, next);
648 } else {
649 decrement();
650 }
651
652 btf_socket_t hSocket = btf::BtfServiceWrap(rv, sk);
653 if (hSocket == INVALID_SOCKET) return;
654
655 printf("Stratum/.btf: miner connected via %s\n", relay.c_str());
656 Miner* m = new Miner(hSocket);
657 { std::lock_guard<std::mutex> lk(gMinersMutex); gMiners.push_back(m); }
658 LaunchThread(MinerThreadFn, m);
659}
660
661static void BtfStratumAcceptFn(void*)
662{
663 printf("Stratum/.btf: starting \xe2\x80\x94 miners can connect via %s\n",
664 BtfLocalAddress().c_str());
665
666 unsigned char pk[32], sk[32];
667 while (!BtfGetIdentity(pk, sk)) {
668 if (fShutdown || !gPoolRunning) return;
669 Sleep(2000);
670 }
671
672 while (!fShutdown && gPoolRunning) {
673 vector<string> relays = BtfAllRelays();
674 if (relays.empty()) { Sleep(5000); continue; }
675
676 // Spawn one listener per relay, only if none already active
677 for (const string& relay : relays) {
678 std::lock_guard<std::mutex> lk(gBtfListenerMutex);
679 if (gBtfListenerCount[relay] == 0) {
680 gBtfListenerCount[relay]++;
681 BtfAcceptCtx* ctx = new BtfAcceptCtx();
682 memcpy(ctx->pk, pk, 32); memcpy(ctx->sk, sk, 32);
683 ctx->relay = relay;
684 LaunchThread(BtfStratumAcceptOneFn, ctx);
685 }
686 }
687 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000);
688 }
689}
690void ThreadRPCServer(void*)
691{
692 gPoolRunning = true;
693 printf("Bitflash pool server starting\n");
694 printf(" Stratum .btf: %s (no port forwarding needed)\n",
695 BtfLocalAddress().c_str());
696 printf(" Mode: operator\n");
697
698 if (RebuildJob())
699 printf("Stratum: initial job seeded height=%d\n", nBestHeight + 1);
700
701 LaunchThread(JobWatcherFn, NULL);
702 LaunchThread(BtfStratumAcceptFn, NULL);
703 LaunchThread(ThreadBtfPoolAnnouncer, NULL);
704
705 while (!fShutdown && gPoolRunning)
706 Sleep(1000);
707 printf("Pool server stopped\n");
708}