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