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.cpp963
1 files changed, 818 insertions, 145 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 813f1d1..8af2fbe 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -6,8 +6,8 @@
6// Architecture: 6// Architecture:
7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout 7// PoolEventLoop -- single thread, owns all miner fds, all job/share/payout
8// state. No locks needed on any of that data. 8// state. No locks needed on any of that data.
9// AcceptOneFn -- one thread per relay. Blocks on RvServiceRegister + 9// AcceptOneFn -- one thread per relay. Registers, waits to be paired,
10// BtfServiceWrap, pushes finished socket to gReadyQueue. 10// wraps the tunnel, pushes finished socket to gReadyQueue.
11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every 11// AcceptLoopFn -- spawns AcceptOneFn threads, re-polls relay list every
12// 60s so dynamically discovered relays get picked up. 12// 60s so dynamically discovered relays get picked up.
13// 13//
@@ -143,6 +143,75 @@ static bool RebuildJob()
143 return false; 143 return false;
144 } 144 }
145 145
146 // Don't issue jobs while we are behind the network: mining against a
147 // stale tip produces blocks the network will orphan, and payout
148 // entries that can never be paid (their coinbases never exist on the
149 // main chain). Require at least one peer, and require that no peer
150 // claims a longer chain than ours.
151 if (vNodes.empty()) {
152 LogPrint("pool", "RebuildJob: no peers yet -- not issuing jobs\n");
153 return false;
154 }
155 // This used to compare the tip's timestamp against 30 minutes, which
156 // was wrong in both directions and cost real money.
157 //
158 // It cannot tell "we are not synced" from "the network is slow". A
159 // pool mining its own fork keeps its tip timestamp fresh forever, so
160 // the check passed for the whole of heights 3966-4085 -- 120
161 // consecutive blocks, every one of them orphaned, 5890.50 BTF of
162 // payouts voided. Then, once the pool stopped finding blocks, the
163 // same check refused to issue jobs against a tip that was perfectly
164 // valid and was the network's own best, and the pool -- roughly 90%
165 // of the hashrate -- could not restart the chain it was the majority
166 // of. Stale tip, so no jobs; no jobs, so the tip stays stale.
167 //
168 // The height peers announce in their version message answers the
169 // question the age check was only guessing at. Median, so one peer
170 // claiming an absurd height cannot stall us, and one block of slack
171 // to match WarnIfBehind, because somebody is always mid-relay. A
172 // peer that predates the announcement reports -1 and is ignored; if
173 // nobody has told us anything we mine our best chain, which is what
174 // the node would do without a pool attached.
175 int nPeerHeight = GetPeerMedianHeight();
176 if (nPeerHeight >= 0 && nBestHeight < nPeerHeight - 1) {
177 LogPrint("pool", "RebuildJob: %d blocks behind the network "
178 "(height %d, peers report %d) -- not issuing jobs\n",
179 nPeerHeight - nBestHeight, nBestHeight, nPeerHeight);
180 return false;
181 }
182
183 // The comparison above is blind in the one case that matters most.
184 //
185 // When the node goes deaf -- peers still in vNodes, nothing arriving --
186 // no peer announces anything, GetPeerMedianHeight() returns -1, and the
187 // fallback ("nobody has told us anything, so mine our best chain") let
188 // the pool build a private fork for as long as the deafness lasted.
189 // That fallback is right for a node that has simply not been told yet
190 // and wrong for a node that has stopped being able to hear, and a
191 // height nobody sent cannot tell those apart. Time can.
192 //
193 // Deliberately not behind a debug category, and deliberately not a
194 // silent refusal: an operator whose pool has stopped issuing jobs needs
195 // the reason in the log. /poolmineblind is the escape hatch for a node
196 // that really is alone -- without one, a genuinely isolated pool that is
197 // the majority of the hashrate could never restart its own chain, which
198 // is the deadlock the tip-age check used to cause.
199 if (nPeerHeight < 0 && !fPoolMineBlind) {
200 int64 nAge = GetPeerHeightAgeSecs();
201 if (nAge >= BTF_NO_PEER_HEIGHT_MAX_SECS) {
202 static int64 nLastSaid = 0;
203 if (GetTime() - nLastSaid > 60) {
204 nLastSaid = GetTime();
205 printf("RebuildJob: no peer has reported a height for %d minutes "
206 "-- not issuing jobs (our height %d, %d peers connected). "
207 "This node is deaf; mining now would fork it. "
208 "Pass /poolmineblind to override.\n",
209 (int)(nAge / 60), nBestHeight, (int)vNodes.size());
210 }
211 return false;
212 }
213 }
214
146 CBlockIndex* pindexPrev = pindexBest; 215 CBlockIndex* pindexPrev = pindexBest;
147 unsigned int nBits = GetNextWorkRequired(pindexPrev); 216 unsigned int nBits = GetNextWorkRequired(pindexPrev);
148 217
@@ -194,7 +263,15 @@ static bool RebuildJob()
194 vector<char> vfAlreadyAdded(mapTransactions.size()); 263 vector<char> vfAlreadyAdded(mapTransactions.size());
195 bool fFoundSomething = true; 264 bool fFoundSomething = true;
196 unsigned int sz = 0; 265 unsigned int sz = 0;
197 while (fFoundSomething && sz < MAX_SIZE / 2) 266 // The cap is MAX_BLOCK_SIZE/2, not MAX_SIZE/2. MAX_SIZE is the
267 // 32 MB serialisation ceiling, so half of it is 16 MB -- this
268 // builder would happily assemble a template that its own
269 // CheckBlock() rejects at the 1 MB consensus cap. This is the
270 // pool's job builder, so a block over the cap is not one
271 // wasted solve, it is every miner on the pool working a
272 // template the network will reject. The inner break bounds the
273 // overshoot to a single transaction within a pass.
274 while (fFoundSomething && sz < MAX_BLOCK_SIZE / 2)
198 { 275 {
199 fFoundSomething = false; 276 fFoundSomething = false;
200 unsigned int n = 0; 277 unsigned int n = 0;
@@ -214,6 +291,7 @@ static bool RebuildJob()
214 sz += ::GetSerializeSize(tx, SER_NETWORK); 291 sz += ::GetSerializeSize(tx, SER_NETWORK);
215 vfAlreadyAdded[n] = true; 292 vfAlreadyAdded[n] = true;
216 fFoundSomething = true; 293 fFoundSomething = true;
294 if (sz > MAX_BLOCK_SIZE / 2) break;
217 } 295 }
218 } 296 }
219 } 297 }
@@ -251,14 +329,34 @@ static bool RebuildJob()
251// Payouts -- event loop thread only 329// Payouts -- event loop thread only
252// --------------------------------------------------------------------------- 330// ---------------------------------------------------------------------------
253struct PendingPayout { 331struct PendingPayout {
254 std::string id; 332 // The block that funds this payout, identified by hash and not by height
255 int blockHeight; 333 // alone. A reorg replaces the block at a height without changing the
256 int matureAtHeight; 334 // height, so a height-keyed obligation outlives the coinbase that was
335 // supposed to pay it: a 280-block reorg once left ~14.4k BTF of debt
336 // owed against blocks that no longer existed, which then drained the
337 // wallet paying fork-era miners out of real-chain coin.
338 //
339 // Upstream keys these on the string "block-<height>". That is the same
340 // bug this fork already paid for, and height keying additionally lets
341 // FindRecordedPoolPayout match the *previous* round's transaction after a
342 // reorg and silently skip a miner, so their `id` is deliberately dropped.
343 int blockHeight = 0;
344 // Must be explicitly zeroed: uint256's default constructor is empty
345 // (uint256.h:514), so leaving this unassigned gives every ledger entry
346 // loaded from an older file a garbage hash, which then matches no block
347 // and gets voided as "orphaned". That mistake voided a live payout.
348 uint256 blockHash = 0; // zero = entry predates hash keying
349 int matureAtHeight = 0;
257 std::map<std::string, int64> amounts; // address -> satoshis 350 std::map<std::string, int64> amounts; // address -> satoshis
351 // From upstream: which recipients already have a transaction. This is what
352 // lets an entry stay on disk until every address in `amounts` is paid,
353 // instead of being erased the moment the batch is handed off.
258 std::map<std::string, std::string> paidTxids; // address -> txid 354 std::map<std::string, std::string> paidTxids; // address -> txid
259 int64 nextAttempt; 355 int retries = 0; // exec-thread only: SendMoney attempts so far
260 356 int deferrals = 0; // exec-thread only: waits for spendable coin
261 PendingPayout() : blockHeight(0), matureAtHeight(0), nextAttempt(0) {} 357 // Upstream's `nextAttempt` is this field under another name; ours is kept
358 // so the retry/deferral policy above stays coherent with it.
359 int64 retryAfter = 0; // exec-thread only: unix time, 0 = ready now
262}; 360};
263static std::vector<PendingPayout> gPendingPayouts; 361static std::vector<PendingPayout> gPendingPayouts;
264static std::mutex gPayoutLedgerMutex; 362static std::mutex gPayoutLedgerMutex;
@@ -304,9 +402,25 @@ static std::string PoolRoundsFilePath()
304 return GetAppDir() + "/pool_rounds.json"; 402 return GetAppDir() + "/pool_rounds.json";
305} 403}
306 404
307static std::string PayoutIdForBlock(int blockHeight) 405// Identity of a payout round. Used as the pool-round proof id and, more
406// importantly, as the `pool_payout_id` written into wallet transaction
407// metadata and matched by FindRecordedPoolPayout().
408//
409// Derived from the block HASH, not from its height alone. Upstream uses
410// "block-<height>", which names two different blocks identically across a
411// reorg -- and because this id is what the crash-recovery probe matches on, a
412// height-keyed id lets that probe find the *previous* round's transaction and
413// conclude a miner has already been paid when they have not. The height stays
414// in the string for readability only.
415//
416// A zero hash means the entry predates hash keying; those keep the old form so
417// anything already recorded against them still matches.
418static std::string PayoutIdForBlock(int blockHeight, const uint256& blockHash)
308{ 419{
309 return strprintf("block-%d", blockHeight); 420 if (blockHash == 0)
421 return strprintf("block-%d", blockHeight);
422 return strprintf("block-%d-%s", blockHeight,
423 blockHash.ToString().substr(0, 16).c_str());
310} 424}
311 425
312static bool PayoutFullyPaid(const PendingPayout& pp) 426static bool PayoutFullyPaid(const PendingPayout& pp)
@@ -317,15 +431,22 @@ static bool PayoutFullyPaid(const PendingPayout& pp)
317 return true; 431 return true;
318} 432}
319 433
320static void SavePendingPayoutsLocked() 434// Serialise the ledger. Caller must hold gPayoutLedgerMutex.
435//
436// Split from the file write so the payout thread can hold the mutex just long
437// enough to read the structure and then do the I/O with it released. Upstream
438// writes the file inside the per-recipient loop with gPayoutLedgerMutex held,
439// while the event loop's FlushMaturePayouts blocks on that same mutex -- one
440// slow fsync per recipient, on the thread the miners' I/O shares.
441static std::string SerializePendingPayoutsLocked()
321{ 442{
322 nlohmann::json arr = nlohmann::json::array(); 443 nlohmann::json arr = nlohmann::json::array();
323 for (const auto& pp : gPendingPayouts) { 444 for (const auto& pp : gPendingPayouts) {
324 nlohmann::json obj; 445 nlohmann::json obj;
325 obj["id"] = pp.id; 446 obj["blockHeight"] = pp.blockHeight;
326 obj["blockHeight"] = pp.blockHeight; 447 obj["blockHash"] = pp.blockHash.ToString();
327 obj["matureAtHeight"] = pp.matureAtHeight; 448 obj["matureAtHeight"] = pp.matureAtHeight;
328 obj["nextAttempt"] = pp.nextAttempt; 449 obj["retryAfter"] = pp.retryAfter;
329 nlohmann::json amounts = nlohmann::json::object(); 450 nlohmann::json amounts = nlohmann::json::object();
330 for (const auto& kv : pp.amounts) 451 for (const auto& kv : pp.amounts)
331 amounts[kv.first] = kv.second; 452 amounts[kv.first] = kv.second;
@@ -336,24 +457,38 @@ static void SavePendingPayoutsLocked()
336 obj["paidTxids"] = paid; 457 obj["paidTxids"] = paid;
337 arr.push_back(obj); 458 arr.push_back(obj);
338 } 459 }
460 return arr.dump(2);
461}
462
463// Write the serialised ledger. Holds no lock; safe to call with the mutex
464// released. Atomic tmp+rename so a crash mid-write cannot truncate the record
465// of who is still owed money.
466static void WritePendingPayoutsFile(const std::string& s)
467{
339 std::string path = PayoutFilePath(); 468 std::string path = PayoutFilePath();
340 std::string tmp = path + ".tmp"; 469 std::string tmp = path + ".tmp";
341 FILE* f = fopen(tmp.c_str(), "w"); 470 FILE* f = fopen(tmp.c_str(), "w");
342 if (f) { 471 if (!f) {
343 std::string s = arr.dump(2);
344 fwrite(s.c_str(), 1, s.size(), f);
345 if (fclose(f) != 0) {
346 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
347 return;
348 }
349#ifdef _WIN32
350 remove(path.c_str());
351#endif
352 if (rename(tmp.c_str(), path.c_str()) != 0)
353 LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str());
354 } else {
355 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str()); 472 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str());
473 return;
474 }
475 fwrite(s.c_str(), 1, s.size(), f);
476 if (fclose(f) != 0) {
477 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
478 return;
356 } 479 }
480#ifdef _WIN32
481 remove(path.c_str());
482#endif
483 if (rename(tmp.c_str(), path.c_str()) != 0)
484 LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str());
485}
486
487// Convenience for the cold paths (startup, queueing) where holding the mutex
488// across the write costs nothing. Caller must hold gPayoutLedgerMutex.
489static void SavePendingPayoutsLocked()
490{
491 WritePendingPayoutsFile(SerializePendingPayoutsLocked());
357} 492}
358 493
359static void LoadPendingPayouts() 494static void LoadPendingPayouts()
@@ -372,17 +507,27 @@ static void LoadPendingPayouts()
372 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 507 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
373 for (const auto& obj : arr) { 508 for (const auto& obj : arr) {
374 PendingPayout pp; 509 PendingPayout pp;
375 if (obj.contains("blockHeight"))
376 pp.blockHeight = obj["blockHeight"].get<int>();
377 pp.matureAtHeight = obj["matureAtHeight"].get<int>(); 510 pp.matureAtHeight = obj["matureAtHeight"].get<int>();
378 if (obj.contains("id") && obj["id"].is_string()) 511 // Entries written before payouts were keyed by hash carry neither
379 pp.id = obj["id"].get<std::string>(); 512 // field. Leave blockHash zero and they are grandfathered past the
380 if (pp.blockHeight == 0) 513 // orphan check rather than voided on a guess.
514 if (obj.contains("blockHash") && obj["blockHash"].is_string())
515 pp.blockHash.SetHex(obj["blockHash"].get<std::string>());
516 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
517 pp.blockHeight = obj["blockHeight"].get<int>();
518 // Upstream's tolerance for an entry with no usable height, kept
519 // because it costs nothing and an entry that cannot name its block
520 // is worse than one derived from its maturity. COINBASE_MATURITY
521 // and not COINBASE_SPENDABLE_DEPTH: this reverses whatever the
522 // writer of *that* file used, and only files old enough to lack
523 // blockHeight are affected -- they were written when maturity was
524 // still COINBASE_MATURITY.
525 if (pp.blockHeight == 0 && pp.matureAtHeight > COINBASE_MATURITY)
381 pp.blockHeight = pp.matureAtHeight - COINBASE_MATURITY; 526 pp.blockHeight = pp.matureAtHeight - COINBASE_MATURITY;
382 if (pp.id.empty()) 527 // Absent in every file written before this merge; default 0 means
383 pp.id = PayoutIdForBlock(pp.blockHeight); 528 // "ready now", which is the right answer for a restored entry.
384 if (obj.contains("nextAttempt")) 529 if (obj.contains("retryAfter") && obj["retryAfter"].is_number())
385 pp.nextAttempt = obj["nextAttempt"].get<int64>(); 530 pp.retryAfter = obj["retryAfter"].get<int64>();
386 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it) 531 for (auto it = obj["amounts"].begin(); it != obj["amounts"].end(); ++it)
387 pp.amounts[it.key()] = it.value().get<int64>(); 532 pp.amounts[it.key()] = it.value().get<int64>();
388 if (obj.contains("paidTxids") && obj["paidTxids"].is_object()) 533 if (obj.contains("paidTxids") && obj["paidTxids"].is_object())
@@ -398,6 +543,133 @@ static void LoadPendingPayouts()
398 } 543 }
399} 544}
400 545
546// A payment in flight: one record per (payout entry, address) pair, which is
547// the granularity SendMoney works at and therefore the granularity at which a
548// payment can be half-done when the process dies.
549//
550// This list is persisted. It used to be a memory-only queue, and matured
551// entries were erased from pending_payouts.json *before* being handed to it,
552// so any restart in between lost those payments with no record that anyone
553// was owed anything: a container restart on 2026-07-30 silently dropped
554// ~1,156 BTF that way. Writing the records before removing the ledger entry,
555// and reconciling against the wallet on startup, is what makes a restart
556// survivable.
557struct PayingRecord {
558 int blockHeight = 0;
559 uint256 blockHash = 0; // zero = predates hash keying, see PendingPayout
560 int matureAtHeight = 0;
561 std::string address;
562 int64 amount = 0;
563 int retries = 0;
564 int deferrals = 0;
565 int64 retryAfter = 0;
566 // Ask the wallet whether this payment already went out, once, before the
567 // first send attempt. Upstream asks on every attempt: FindRecordedPoolPayout
568 // is a linear scan of mapWallet under cs_mapWallet, per recipient, so with
569 // 2000 rotating payout addresses and a retry budget of 1440 that is a
570 // scan per address per minute for a day. The question it answers -- "did a
571 // previous process send this before it died?" -- can only change across a
572 // restart, and every restart re-sets this flag (ReconcilePayouts), so
573 // asking once per record is the whole of the answer.
574 //
575 // Not persisted: a record read back from disk is by definition a restart.
576 bool fProbeWallet = true;
577};
578static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex
579static std::mutex gPayoutExecMutex;
580static std::atomic<bool> gPayoutThreadRunning{false};
581
582static std::string PayingFilePath()
583{
584 return GetAppDir() + "/paying_payouts.json";
585}
586
587// Caller must hold gPayoutExecMutex.
588static void SavePayingLocked()
589{
590 nlohmann::json arr = nlohmann::json::array();
591 for (const auto& r : gPaying) {
592 nlohmann::json obj;
593 obj["blockHeight"] = r.blockHeight;
594 obj["blockHash"] = r.blockHash.ToString();
595 obj["matureAtHeight"] = r.matureAtHeight;
596 obj["address"] = r.address;
597 obj["amount"] = r.amount;
598 obj["retries"] = r.retries;
599 obj["deferrals"] = r.deferrals;
600 arr.push_back(obj);
601 }
602 std::string path = PayingFilePath();
603 // Write to a temporary file and rename, so a crash mid-write cannot leave
604 // a truncated list of who is still owed money.
605 std::string tmp = path + ".tmp";
606 FILE* f = fopen(tmp.c_str(), "w");
607 if (!f) {
608 LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str());
609 return;
610 }
611 std::string s = arr.dump(2);
612 fwrite(s.c_str(), 1, s.size(), f);
613 // fclose() is where buffered data actually reaches the OS, so a failure
614 // here means the .tmp is short -- renaming it over the real file would
615 // destroy the good copy. Upstream's check, taken because a truncated
616 // list of who is owed money is exactly what this function exists to
617 // prevent. rename() on Windows will not replace an existing file, hence
618 // the remove() first.
619 if (fclose(f) != 0) {
620 LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str());
621 return;
622 }
623#ifdef _WIN32
624 remove(path.c_str());
625#endif
626 if (rename(tmp.c_str(), path.c_str()) != 0)
627 LogPrint("payout", "[payout] WARNING: could not rename %s -> %s\n",
628 tmp.c_str(), path.c_str());
629}
630
631static void LoadPaying()
632{
633 std::string path = PayingFilePath();
634 FILE* f = fopen(path.c_str(), "r");
635 if (!f) return;
636 fseek(f, 0, SEEK_END);
637 long sz = ftell(f);
638 fseek(f, 0, SEEK_SET);
639 std::string s(sz, '\0');
640 fread(&s[0], 1, sz, f);
641 fclose(f);
642 try {
643 nlohmann::json arr = nlohmann::json::parse(s);
644 for (const auto& obj : arr) {
645 PayingRecord r;
646 r.address = obj["address"].get<std::string>();
647 r.amount = obj["amount"].get<int64>();
648 r.matureAtHeight = obj["matureAtHeight"].get<int>();
649 if (obj.contains("blockHash") && obj["blockHash"].is_string())
650 r.blockHash.SetHex(obj["blockHash"].get<std::string>());
651 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
652 r.blockHeight = obj["blockHeight"].get<int>();
653 if (obj.contains("retries") && obj["retries"].is_number())
654 r.retries = obj["retries"].get<int>();
655 if (obj.contains("deferrals") && obj["deferrals"].is_number())
656 r.deferrals = obj["deferrals"].get<int>();
657 gPaying.push_back(r);
658 }
659 LogPrint("payout", "[payout] loaded %zu in-flight payment(s) from disk\n",
660 gPaying.size());
661 } catch (const std::exception& e) {
662 LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n",
663 path.c_str(), e.what());
664 }
665}
666
667// ---------------------------------------------------------------------------
668// Pool round proofs (upstream cbdd06d). Disjoint from the in-flight ledger
669// above -- this is the operator's published record of how a round was split,
670// not an obligation to pay. Both sides added code at this offset; both are
671// kept.
672// ---------------------------------------------------------------------------
401static nlohmann::json PoolRoundToJson(const PoolRoundProof& r) 673static nlohmann::json PoolRoundToJson(const PoolRoundProof& r)
402{ 674{
403 nlohmann::json j; 675 nlohmann::json j;
@@ -529,6 +801,88 @@ static void LoadPoolRounds()
529 } 801 }
530} 802}
531 803
804// Is the block that funds this payout still on the main chain?
805//
806// A zero hash means the entry was written before payouts recorded one, so
807// there is nothing to check and we assume it is good -- voiding those would
808// mean refusing to pay miners on the strength of a missing field.
809static bool PayoutBlockStillOnChain(const uint256& blockHash)
810{
811 if (blockHash == 0)
812 return true;
813 CRITICAL_BLOCK(cs_main)
814 {
815 std::map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(blockHash);
816 if (mi == mapBlockIndex.end() || mi->second == NULL)
817 return false;
818 return mi->second->IsInMainChain();
819 }
820 return false;
821}
822
823static std::string PayoutComment(int blockHeight)
824{
825 return strprintf("Pool payout height %d", blockHeight);
826}
827
828// Startup reconciliation.
829//
830// This used to be the crash-recovery mechanism: it scanned the wallet with
831// WalletAlreadyPaid() and then dropped ledger entries that had already been
832// exploded into in-flight records. Neither job is right any more.
833//
834// The ledger is now authoritative and an entry survives until every recipient
835// has a txid recorded against it (upstream cbdd06d), so dropping ledger
836// entries here would reintroduce exactly the loss this merge is adopting that
837// model to prevent. And the wallet scan is now FindRecordedPoolPayout(), done
838// once per record on its first attempt, which is the same question asked in
839// the one place that can act on the answer. Running both would be two
840// authorities for the same debt.
841//
842// What is left is the useful part: the in-flight list restored from disk may
843// contain records the ledger already shows as paid (we died between the send
844// and the in-flight file being rewritten). The ledger wins.
845static void ReconcilePayouts()
846{
847 std::lock_guard<std::mutex> exec(gPayoutExecMutex);
848 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
849
850 size_t settled = 0;
851 int64 settledAmount = 0;
852 for (auto it = gPaying.begin(); it != gPaying.end(); ) {
853 bool paid = false;
854 for (const PendingPayout& pp : gPendingPayouts)
855 if (pp.blockHash == it->blockHash && pp.paidTxids.count(it->address)) {
856 paid = true;
857 break;
858 }
859 if (paid) {
860 settledAmount += it->amount;
861 settled++;
862 it = gPaying.erase(it);
863 } else {
864 // Every restored record gets one wallet probe before it is allowed
865 // to send: this is precisely the window where the wallet may hold
866 // a transaction the ledger has not recorded yet.
867 it->fProbeWallet = true;
868 ++it;
869 }
870 }
871 if (settled) {
872 LogPrint("payout", "[payout] %zu in-flight payment(s) totalling %s are"
873 " already recorded paid in the ledger -- not resending\n",
874 settled, FormatMoney(settledAmount).c_str());
875 }
876 SavePayingLocked();
877
878 if (!gPaying.empty()) {
879 int64 owed = 0;
880 for (const auto& r : gPaying) owed += r.amount;
881 LogPrint("payout", "[payout] resuming %zu in-flight payment(s) totalling %s\n",
882 gPaying.size(), FormatMoney(owed).c_str());
883 }
884}
885
532static void RecordPoolRound(const PoolRoundProof& round) 886static void RecordPoolRound(const PoolRoundProof& round)
533{ 887{
534 std::lock_guard<std::mutex> lk(gPoolRoundsMutex); 888 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
@@ -559,13 +913,13 @@ static void MarkPoolRoundPaid(const std::string& id,
559 } 913 }
560} 914}
561 915
562// Payouts ready to execute are moved here under gPayoutExecMutex. 916// Upstream's gPayoutExecQueue/gPayoutExecMutex/gPayoutThreadRunning are
563// A dedicated thread drains this list so SendMoney (which takes cs_main) 917// declared above with gPaying: the in-flight list IS the exec queue in this
564// never runs on the event loop thread and can't stall miner I/O. 918// tree, at per-recipient rather than per-round granularity, so a second queue
565static std::vector<PendingPayout> gPayoutExecQueue; 919// would be a second answer to "who is owed money". Only the dedupe set is new.
566static std::mutex gPayoutExecMutex; 920//
567static std::atomic<bool> gPayoutThreadRunning{false}; 921// Ids of rounds currently exploded into gPaying, so a flush cannot queue the
568 922// same round twice. Guarded by gPayoutExecMutex.
569static std::set<std::string> gQueuedPayouts; 923static std::set<std::string> gQueuedPayouts;
570 924
571static bool FindRecordedPoolPayout(const std::string& id, 925static bool FindRecordedPoolPayout(const std::string& id,
@@ -595,8 +949,13 @@ static bool FindRecordedPoolPayout(const std::string& id,
595 return false; 949 return false;
596} 950}
597 951
952// blockHash stays a uint256 and not upstream's display string: it is the key
953// this fork's payout ledger is built on, and a key that has been through
954// ToString() is a key that can be compared wrong. It is converted to text only
955// where the round proof needs text. The other three arguments are upstream's,
956// carried for the round-proof data.
598static void QueuePayouts(int blockHeight, 957static void QueuePayouts(int blockHeight,
599 const std::string& blockHash, 958 const uint256& blockHash,
600 const std::string& coinbaseTxid, 959 const std::string& coinbaseTxid,
601 int64 coinbaseValue, 960 int64 coinbaseValue,
602 const std::string& foundBy, 961 const std::string& foundBy,
@@ -604,11 +963,11 @@ static void QueuePayouts(int blockHeight,
604 uint64 total) 963 uint64 total)
605{ 964{
606 PoolRoundProof round; 965 PoolRoundProof round;
607 round.id = PayoutIdForBlock(blockHeight); 966 round.id = PayoutIdForBlock(blockHeight, blockHash);
608 round.blockHeight = blockHeight; 967 round.blockHeight = blockHeight;
609 round.matureAtHeight = blockHeight + COINBASE_MATURITY; 968 round.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH;
610 round.createdAt = GetTime(); 969 round.createdAt = GetTime();
611 round.blockHash = blockHash; 970 round.blockHash = blockHash.ToString();
612 round.coinbaseTxid = coinbaseTxid; 971 round.coinbaseTxid = coinbaseTxid;
613 round.coinbaseValue = coinbaseValue; 972 round.coinbaseValue = coinbaseValue;
614 round.foundBy = foundBy; 973 round.foundBy = foundBy;
@@ -639,9 +998,21 @@ static void QueuePayouts(int blockHeight,
639 round.operatorReward = operatorCut; 998 round.operatorReward = operatorCut;
640 999
641 PendingPayout pp; 1000 PendingPayout pp;
642 pp.id = round.id;
643 pp.blockHeight = blockHeight; 1001 pp.blockHeight = blockHeight;
644 pp.matureAtHeight = blockHeight + COINBASE_MATURITY; 1002 pp.blockHash = blockHash;
1003 // COINBASE_SPENDABLE_DEPTH, not COINBASE_MATURITY. Paying at depth 100
1004 // means the block's own coinbase is still 20 blocks short of spendable, so
1005 // the payment has to come out of some *other* mature coin -- the pool runs
1006 // a permanent ~19-block float and every payout fails until unrelated coin
1007 // matures. The earlier "96% of 934 payments went through at depth 100"
1008 // measurement that justified the shorter wait was taken while this node was
1009 // alone on a private fork paying itself, so it says nothing about a pool
1010 // competing on the real chain. Waiting the extra 20 blocks makes each
1011 // payout funded by construction, and puts the maturity gate past the
1012 // reorg depth that orphaned 280 blocks of obligations. Upstream uses
1013 // COINBASE_MATURITY here; this is a considered disagreement, not an
1014 // oversight -- see the merge commit message.
1015 pp.matureAtHeight = blockHeight + COINBASE_SPENDABLE_DEPTH;
645 1016
646 int skipped = 0; 1017 int skipped = 0;
647 int64 burned = 0; // amount owed to miners with bad/empty addresses 1018 int64 burned = 0; // amount owed to miners with bad/empty addresses
@@ -689,37 +1060,222 @@ static void QueuePayouts(int blockHeight,
689 : ""); 1060 : "");
690} 1061}
691 1062
692// Called from event loop: copy matured entries to the exec queue (fast). 1063// Called from the event loop: explode matured ledger entries into per-recipient
693// The ledger entry stays on disk until every recipient has a txid recorded. 1064// in-flight records (fast; no wallet or disk work beyond one small write).
694// On restart, FindRecordedPoolPayout() checks wallet metadata before sending, 1065//
695// so a crash after SendMoney but before the ledger update does not double-pay. 1066// The ledger entry SURVIVES here. That is upstream's model and it is the
1067// crash-safe one: this fork used to erase the entry as soon as its payments
1068// were handed off, so a death in that window left the debt described by
1069// nothing at all -- a container restart on 2026-07-30 silently dropped
1070// ~1,156 BTF exactly that way. An entry is removed only once every recipient
1071// in it has a txid recorded (PayoutFullyPaid), which the payout thread does.
1072//
1073// gPaying is kept on top of that as the in-flight working set: it carries the
1074// per-recipient retry/deferral state that a per-round `nextAttempt` cannot
1075// express, and it is what the payout thread iterates. It is a cache of work
1076// derived from the ledger, never an independent record of debt -- if it were
1077// lost entirely the ledger would rebuild it on the next flush.
696static void FlushMaturePayouts() 1078static void FlushMaturePayouts()
697{ 1079{
1080 // Which funding blocks are still on the main chain? Asked with no payout
1081 // lock held: PayoutBlockStillOnChain takes cs_main, and cs_main is held by
1082 // ProcessBlock and SendMoney for long stretches. Holding the ledger mutex
1083 // across it would let the event loop park the payout thread behind a chain
1084 // lock it has no interest in. (It is not a deadlock either way -- nothing
1085 // takes the ledger mutex while holding cs_main -- but it is exactly the
1086 // kind of stall this merge is removing from the ledger path.)
1087 std::vector<uint256> orphaned;
1088 {
1089 std::vector<uint256> hashes;
1090 {
1091 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
1092 for (const PendingPayout& pp : gPendingPayouts)
1093 hashes.push_back(pp.blockHash);
1094 }
1095 for (const uint256& h : hashes)
1096 if (!PayoutBlockStillOnChain(h))
1097 orphaned.push_back(h);
1098 }
1099
698 std::vector<PendingPayout> ready; 1100 std::vector<PendingPayout> ready;
699 { 1101 {
700 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex); 1102 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex);
701 std::lock_guard<std::mutex> queue(gPayoutExecMutex); 1103
1104 // Void obligations whose funding block lost a reorg. The coin that was
1105 // meant to pay them does not exist, so paying anyway spends real-chain
1106 // coin the pool owes to somebody else.
1107 bool voided = false;
1108 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) {
1109 if (std::find(orphaned.begin(), orphaned.end(), it->blockHash) == orphaned.end()) {
1110 ++it;
1111 continue;
1112 }
1113 int64 total = 0;
1114 for (auto& kv : it->amounts) total += kv.second;
1115 LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu"
1116 " payment(s) totalling %s\n",
1117 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(),
1118 it->amounts.size(), FormatMoney(total).c_str());
1119 it = gPendingPayouts.erase(it);
1120 voided = true;
1121 }
1122 if (voided) SavePendingPayoutsLocked();
1123
702 int64 now = GetTime(); 1124 int64 now = GetTime();
703 for (const PendingPayout& pp : gPendingPayouts) { 1125 for (const PendingPayout& pp : gPendingPayouts) {
704 if (nBestHeight < pp.matureAtHeight) 1126 if (nBestHeight < pp.matureAtHeight)
705 continue; 1127 continue;
706 if (pp.nextAttempt > now) 1128 if (pp.retryAfter > now)
707 continue; 1129 continue;
708 if (PayoutFullyPaid(pp)) 1130 if (PayoutFullyPaid(pp))
709 continue; 1131 continue;
710 if (gQueuedPayouts.count(pp.id))
711 continue;
712 LogPrint("payout", "[payout] block height %d matured -- queuing payout %s (%zu recipients)\n",
713 pp.matureAtHeight, pp.id.c_str(), pp.amounts.size());
714 ready.push_back(pp); 1132 ready.push_back(pp);
715 gQueuedPayouts.insert(pp.id);
716 } 1133 }
717 for (auto& pp : ready)
718 gPayoutExecQueue.push_back(pp);
719 } 1134 }
1135
1136 if (ready.empty())
1137 return;
1138
1139 std::lock_guard<std::mutex> queue(gPayoutExecMutex);
1140 size_t added = 0;
1141 for (const PendingPayout& pp : ready) {
1142 const std::string id = PayoutIdForBlock(pp.blockHeight, pp.blockHash);
1143 if (gQueuedPayouts.count(id))
1144 continue; // already exploded into gPaying and still in flight
1145 size_t n = 0;
1146 for (const auto& kv : pp.amounts) {
1147 // Upstream's per-recipient dedupe: a recipient already carrying a
1148 // txid is done, even though the entry as a whole is not.
1149 if (pp.paidTxids.count(kv.first))
1150 continue;
1151 PayingRecord r;
1152 r.blockHeight = pp.blockHeight;
1153 r.blockHash = pp.blockHash;
1154 r.matureAtHeight = pp.matureAtHeight;
1155 r.address = kv.first;
1156 r.amount = kv.second;
1157 gPaying.push_back(r);
1158 n++;
1159 }
1160 gQueuedPayouts.insert(id);
1161 added += n;
1162 LogPrint("payout", "[payout] block height %d matured -- queuing payout %s"
1163 " (%zu of %zu recipients still owed)\n",
1164 pp.matureAtHeight, id.c_str(), n, pp.amounts.size());
1165 }
1166 if (added) SavePayingLocked();
1167}
1168
1169// Identity of a payment, for finding its record again after SendMoney.
1170// amounts is a map keyed by address, so (block, address) is already unique.
1171static bool SamePayment(const PayingRecord& a, const PayingRecord& b)
1172{
1173 return a.matureAtHeight == b.matureAtHeight
1174 && a.blockHash == b.blockHash
1175 && a.address == b.address
1176 && a.amount == b.amount;
1177}
1178
1179// Settle a payment: it is either done or abandoned, either way it stops being
1180// in flight. Note this only retires the working record -- whether the miner is
1181// still OWED the money is the ledger's business, and the ledger is updated
1182// separately by RecordPayoutTxid().
1183static void ForgetPayment(const PayingRecord& r)
1184{
1185 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
1186 for (auto it = gPaying.begin(); it != gPaying.end(); ++it)
1187 if (SamePayment(*it, r)) { gPaying.erase(it); break; }
1188
1189 // When the last record for a round is gone the round is no longer in
1190 // flight, so a later flush may explode it again if the ledger still shows
1191 // recipients owed (a give-up, for instance, leaves the entry standing).
1192 bool stillInFlight = false;
1193 for (const auto& e : gPaying)
1194 if (e.blockHash == r.blockHash) { stillInFlight = true; break; }
1195 if (!stillInFlight)
1196 gQueuedPayouts.erase(PayoutIdForBlock(r.blockHeight, r.blockHash));
1197
1198 SavePayingLocked();
1199}
1200
1201// Persist an updated attempt count / backoff for a payment still owed.
1202static void ReschedulePayment(const PayingRecord& r)
1203{
1204 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
1205 for (auto& e : gPaying)
1206 if (SamePayment(e, r)) {
1207 e.retries = r.retries;
1208 e.deferrals = r.deferrals;
1209 e.retryAfter = r.retryAfter;
1210 // Carry the cleared probe flag back, or every retry re-scans the
1211 // whole wallet -- the cost this merge is specifically removing.
1212 e.fProbeWallet = r.fProbeWallet;
1213 break;
1214 }
1215 SavePayingLocked();
1216}
1217
1218// Record that a recipient has been paid, in the ledger, which is the only
1219// durable statement that the debt is settled.
1220//
1221// The file write happens with gPayoutLedgerMutex RELEASED. Upstream calls
1222// SavePendingPayoutsLocked() inside the per-recipient loop with the mutex
1223// held, so every recipient costs the event loop a full serialise+fsync it has
1224// to wait for in FlushMaturePayouts.
1225static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1226{
1227 std::string blob;
1228 bool complete = false;
1229 {
1230 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1231 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1232 if (it->blockHash != r.blockHash)
1233 continue;
1234 it->paidTxids[r.address] = txid;
1235 it->retryAfter = 0;
1236 if (PayoutFullyPaid(*it)) {
1237 LogPrint("payout", "[payout] payout for block %d complete --"
1238 " removing from ledger\n", it->blockHeight);
1239 gPendingPayouts.erase(it);
1240 complete = true;
1241 }
1242 break;
1243 }
1244 blob = SerializePendingPayoutsLocked();
1245 }
1246 WritePendingPayoutsFile(blob);
1247 // The round proof records a txid per address, so this is per recipient and
1248 // not only on completion.
1249 MarkPoolRoundPaid(PayoutIdForBlock(r.blockHeight, r.blockHash), r.address, txid);
1250 if (complete)
1251 LogPrint("payout", "[payout] block %d fully settled\n", r.blockHeight);
1252}
1253
1254// Write a recipient off. Only reached after the full retry budget against a
1255// block that is still on the main chain with coin available to spend, i.e. a
1256// wallet fault rather than a maturity or reorg question.
1257//
1258// The amount is removed from the entry rather than marked paid, so the money
1259// is not reported as sent. Once nothing is left owed the entry retires.
1260static void ForfeitPayoutRecipient(const PayingRecord& r)
1261{
1262 std::string blob;
1263 {
1264 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
1265 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ++it) {
1266 if (it->blockHash != r.blockHash)
1267 continue;
1268 it->amounts.erase(r.address);
1269 if (it->amounts.empty() || PayoutFullyPaid(*it))
1270 gPendingPayouts.erase(it);
1271 break;
1272 }
1273 blob = SerializePendingPayoutsLocked();
1274 }
1275 WritePendingPayoutsFile(blob);
720} 1276}
721 1277
722// Payout execution thread: drains gPayoutExecQueue, calls SendMoney. 1278// Payout execution thread: works through gPaying, calls SendMoney.
723// Runs independently of the event loop so slow wallet operations 1279// Runs independently of the event loop so slow wallet operations
724// (SendMoney takes cs_main) never stall miner I/O. 1280// (SendMoney takes cs_main) never stall miner I/O.
725static void PayoutThreadFn(void*) 1281static void PayoutThreadFn(void*)
@@ -728,103 +1284,150 @@ static void PayoutThreadFn(void*)
728 LogPrint("payout", "[payout] payout thread started\n"); 1284 LogPrint("payout", "[payout] payout thread started\n");
729 1285
730 while (!fShutdown && gPoolRunning) { 1286 while (!fShutdown && gPoolRunning) {
731 std::vector<PendingPayout> batch; 1287 std::vector<PayingRecord> batch;
732 { 1288 {
733 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 1289 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
734 batch.swap(gPayoutExecQueue); 1290 int64 now = GetTime();
1291 for (const auto& r : gPaying)
1292 if (r.retryAfter <= now)
1293 batch.push_back(r);
735 } 1294 }
736 1295
737 for (auto& ppQueued : batch) { 1296 for (PayingRecord r : batch) {
738 LogPrint("payout", "[payout] executing payments for block matured at %d" 1297 const std::string id = PayoutIdForBlock(r.blockHeight, r.blockHash);
739 " (%zu miners)\n", ppQueued.matureAtHeight, ppQueued.amounts.size()); 1298
740 bool fAbortBatch = false; 1299 // Re-check here and not only at maturity: a payment can sit in this
741 for (auto& kv : ppQueued.amounts) { 1300 // list for hours across retries, and the reorg that kills its
1301 // funding block can land at any point in that window. Upstream has
1302 // no equivalent -- it decides once, when the entry matures.
1303 if (!PayoutBlockStillOnChain(r.blockHash)) {
1304 LogPrint("payout", "[payout] block %d (%s) orphaned while queued"
1305 " -- dropping %s to %s\n",
1306 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(),
1307 FormatMoney(r.amount).c_str(), r.address.c_str());
1308 ForgetPayment(r);
1309 continue;
1310 }
1311
1312 // Upstream's per-recipient dedupe, against the ledger rather than
1313 // against the batch: if a txid is already recorded for this
1314 // address, the money went out and nothing more is owed.
1315 {
742 bool alreadyPaid = false; 1316 bool alreadyPaid = false;
743 { 1317 {
744 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1318 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
745 for (const PendingPayout& pp : gPendingPayouts) 1319 for (const PendingPayout& pp : gPendingPayouts)
746 if (pp.id == ppQueued.id && pp.paidTxids.count(kv.first)) 1320 if (pp.blockHash == r.blockHash && pp.paidTxids.count(r.address)) {
747 alreadyPaid = true; 1321 alreadyPaid = true;
1322 break;
1323 }
748 } 1324 }
749 if (alreadyPaid) 1325 if (alreadyPaid) {
1326 ForgetPayment(r);
750 continue; 1327 continue;
751
752 uint160 h160;
753 if (!AddressToHash160(kv.first, h160)) {
754 // Should not happen -- bad addresses are filtered in QueuePayouts
755 LogPrint("payout", "[payout] SKIPPED bad address '%s'\n",
756 kv.first.c_str());
757 continue;
758 }
759 CScript sc;
760 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
761 CWalletTx wtx;
762 wtx.mapValue["comment"] = strprintf("Pool payout height %d", ppQueued.matureAtHeight);
763 wtx.mapValue["pool_payout_id"] = ppQueued.id;
764 wtx.mapValue["pool_payout_address"] = kv.first;
765 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", kv.second);
766
767 std::string txid;
768 if (FindRecordedPoolPayout(ppQueued.id, kv.first, kv.second, txid))
769 {
770 LogPrint("payout", "[payout] recovered recorded payout %s to %s"
771 " from wallet tx %s\n",
772 FormatMoney(kv.second).c_str(), kv.first.c_str(), txid.c_str());
773 MarkPoolRoundPaid(ppQueued.id, kv.first, txid);
774 }
775 else if (SendMoney(sc, kv.second, wtx))
776 {
777 txid = wtx.GetHash().GetHex();
778 LogPrint("payout", "[payout] paid %s to %s\n",
779 FormatMoney(kv.second).c_str(), kv.first.c_str());
780 MarkPoolRoundPaid(ppQueued.id, kv.first, txid);
781 }
782 else
783 {
784 LogPrint("payout", "[payout] FAILED to pay %s to %s"
785 " (insufficient funds or wallet locked?)\n",
786 FormatMoney(kv.second).c_str(), kv.first.c_str());
787 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
788 for (PendingPayout& pp : gPendingPayouts)
789 if (pp.id == ppQueued.id)
790 pp.nextAttempt = GetTime() + 60;
791 SavePendingPayoutsLocked();
792 fAbortBatch = true;
793 break;
794 } 1328 }
1329 }
795 1330
796 { 1331 uint160 h160;
797 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex); 1332 if (!AddressToHash160(r.address, h160)) {
798 for (PendingPayout& pp : gPendingPayouts) { 1333 // Should not happen -- bad addresses are filtered in QueuePayouts
799 if (pp.id != ppQueued.id) 1334 LogPrint("payout", "[payout] SKIPPED bad address '%s'\n",
800 continue; 1335 r.address.c_str());
801 pp.paidTxids[kv.first] = txid; 1336 ForgetPayment(r);
802 pp.nextAttempt = 0; 1337 continue;
803 SavePendingPayoutsLocked(); 1338 }
804 break; 1339 CScript sc;
805 } 1340 sc << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
1341
1342 // Did a previous process send this and die before recording it?
1343 // Only asked once per record -- see PayingRecord::fProbeWallet.
1344 if (r.fProbeWallet) {
1345 std::string recovered;
1346 r.fProbeWallet = false;
1347 if (FindRecordedPoolPayout(id, r.address, r.amount, recovered)) {
1348 LogPrint("payout", "[payout] recovered recorded payout %s to %s"
1349 " from wallet tx %s -- not resending\n",
1350 FormatMoney(r.amount).c_str(), r.address.c_str(),
1351 recovered.c_str());
1352 RecordPayoutTxid(r, recovered);
1353 ForgetPayment(r);
1354 continue;
806 } 1355 }
807 } 1356 }
808 1357
809 { 1358 // Waiting for coin to mature is not a failure, so it must not
810 std::lock_guard<std::mutex> ledger(gPayoutLedgerMutex); 1359 // consume the retry budget. Burning a retry every 60s means a
811 for (auto it = gPendingPayouts.begin(); it != gPendingPayouts.end(); ) { 1360 // payment the pool fully intends to honour can hit GIVING UP
812 if (it->id == ppQueued.id && PayoutFullyPaid(*it)) { 1361 // 24h later purely because the subsidy funding it had not
813 LogPrint("payout", "[payout] payout %s complete -- removing from ledger\n", 1362 // matured yet -- the miner is then silently never paid.
814 it->id.c_str()); 1363 int64 nSpendable = GetBalance();
815 it = gPendingPayouts.erase(it); 1364 if (nSpendable < r.amount) {
816 SavePendingPayoutsLocked(); 1365 r.deferrals++;
817 } else { 1366 r.retryAfter = GetTime() + 60;
818 ++it; 1367 // Hourly, not every minute: a dry pool would otherwise
819 } 1368 // write one line per payment per minute forever.
820 } 1369 if (r.deferrals == 1 || r.deferrals % 60 == 0)
1370 LogPrint("payout", "[payout] deferring %s to %s -- only %s"
1371 " spendable (waiting %d min so far)\n",
1372 FormatMoney(r.amount).c_str(), r.address.c_str(),
1373 FormatMoney(nSpendable).c_str(), r.deferrals);
1374 ReschedulePayment(r);
1375 continue;
821 } 1376 }
822 { 1377
823 std::lock_guard<std::mutex> lk(gPayoutExecMutex); 1378 CWalletTx wtx;
824 gQueuedPayouts.erase(ppQueued.id); 1379 wtx.mapValue["comment"] = PayoutComment(r.blockHeight);
1380 // Upstream's structured metadata. This is what makes the wallet
1381 // probe above possible at all -- the old comment-only form could
1382 // not distinguish two rounds paying the same address the same
1383 // amount. Keyed by the hash-derived id, so a reorg cannot make one
1384 // round's transaction answer for another's.
1385 wtx.mapValue["pool_payout_id"] = id;
1386 wtx.mapValue["pool_payout_address"] = r.address;
1387 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount);
1388
1389 if (SendMoney(sc, r.amount, wtx)) {
1390 // The wallet holds the transaction before this returns, so a
1391 // crash between here and the ledger write is recoverable: the
1392 // probe above finds it by id+address+amount and does not
1393 // resend. Ledger first, in-flight record second -- the debt is
1394 // described by at least one of them at every instant.
1395 RecordPayoutTxid(r, wtx.GetHash().GetHex());
1396 ForgetPayment(r);
1397 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n",
1398 FormatMoney(r.amount).c_str(), r.address.c_str(),
1399 wtx.GetHash().ToString().substr(0, 12).c_str());
1400 } else if (r.retries < 1440) {
1401 // Transient failures (reorg, locked wallet) must not silently
1402 // burn the payment -- retry with backoff. Insufficient funds
1403 // never lands here; it defers above.
1404 r.retries++;
1405 r.retryAfter = GetTime() + 60;
1406 ReschedulePayment(r);
1407 LogPrint("payout", "[payout] FAILED to pay %s to %s"
1408 " (%s spendable at the time) -- retry %d/1440 in 60s\n",
1409 FormatMoney(r.amount).c_str(), r.address.c_str(),
1410 FormatMoney(nSpendable).c_str(), r.retries);
1411 } else {
1412 // Reaching here means 1440 attempts that were NOT
1413 // insufficient-funds waits (those defer instead) against a
1414 // block still on the main chain -- i.e. a real wallet fault,
1415 // not a maturity or reorg question.
1416 LogPrint("payout", "[payout] GIVING UP on %s to %s after %d"
1417 " retries -- block %d is still on-chain and %s was"
1418 " spendable, so this is a wallet fault, not maturity"
1419 " -- payment forfeited\n",
1420 FormatMoney(r.amount).c_str(), r.address.c_str(),
1421 r.retries, r.blockHeight,
1422 FormatMoney(nSpendable).c_str());
1423 // Write the debt off in the ledger as well as dropping the
1424 // in-flight record. Under the survives-until-fully-paid model
1425 // the entry would otherwise never complete, and every later
1426 // flush would explode it again -- a forfeit that silently
1427 // becomes an infinite retry.
1428 ForfeitPayoutRecipient(r);
1429 ForgetPayment(r);
825 } 1430 }
826 if (fAbortBatch)
827 continue;
828 } 1431 }
829 1432
830 Sleep(500); // check for new work every 500ms 1433 Sleep(500); // check for new work every 500ms
@@ -1044,6 +1647,45 @@ static void PushReady(btf_socket_t s)
1044 gReadyQueue.push_back(s); 1647 gReadyQueue.push_back(s);
1045} 1648}
1046 1649
1650// Loopback stratum listener: lets a miner running on the SAME machine reach
1651// this pool over plain TCP (127.0.0.1:8334), skipping the .btf rendezvous
1652// entirely. The rendezvous relay only allows one service registration per
1653// pubkey, so the pool service and the node's own peer-accept fight over the
1654// node identity at the relay -- loopback avoids that fight for local miners.
1655static void LocalAcceptFn(void*)
1656{
1657 SOCKET ls = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1658 if (ls == INVALID_SOCKET)
1659 return;
1660 int yes = 1;
1661 setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes));
1662 struct sockaddr_in addr;
1663 memset(&addr, 0, sizeof(addr));
1664 addr.sin_family = AF_INET;
1665 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1666 addr.sin_port = htons(8334);
1667 if (bind(ls, (struct sockaddr*)&addr, sizeof(addr)) != 0 || listen(ls, 16) != 0)
1668 {
1669 LogPrint("pool", "[pool] loopback stratum bind/listen failed on 127.0.0.1:8334\n");
1670 closesocket(ls);
1671 return;
1672 }
1673 LogPrint("pool", "[pool] loopback stratum listening on 127.0.0.1:8334\n");
1674 while (!fShutdown && gPoolRunning)
1675 {
1676 SOCKET c = accept(ls, NULL, NULL);
1677 if (c == INVALID_SOCKET)
1678 {
1679 if (fShutdown || !gPoolRunning)
1680 break;
1681 continue;
1682 }
1683 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
1684 PushReady((btf_socket_t)c);
1685 }
1686 closesocket(ls);
1687}
1688
1047// --------------------------------------------------------------------------- 1689// ---------------------------------------------------------------------------
1048// I/O helpers -- event loop thread only 1690// I/O helpers -- event loop thread only
1049// --------------------------------------------------------------------------- 1691// ---------------------------------------------------------------------------
@@ -1480,7 +2122,11 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
1480 if (accepted) { 2122 if (accepted) {
1481 // Snapshot height before RebuildJob() (which increments it) 2123 // Snapshot height before RebuildJob() (which increments it)
1482 int foundHeight = gCurrentJob.height; 2124 int foundHeight = gCurrentJob.height;
1483 QueuePayouts(foundHeight, b.GetHash().GetHex(), b.vtx[0].GetHash().GetHex(), 2125 // b is the block we submitted, untouched by ProcessBlock (which
2126 // took a copy), so its hash is what the payout must be keyed on.
2127 // Passed as a uint256, not hex: the ledger key must stay a real
2128 // hash. The remaining arguments are upstream's round-proof data.
2129 QueuePayouts(foundHeight, b.GetHash(), b.vtx[0].GetHash().GetHex(),
1484 b.vtx[0].vout[0].nValue, m->address, 2130 b.vtx[0].vout[0].nValue, m->address,
1485 roundShareCount, roundShareTotal); 2131 roundShareCount, roundShareTotal);
1486 // Reset round shares 2132 // Reset round shares
@@ -1962,7 +2608,11 @@ static void AcceptOneFn(void* arg)
1962 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 2608 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str());
1963 2609
1964 while (!fShutdown && gPoolRunning) { 2610 while (!fShutdown && gPoolRunning) {
1965 // Blocks until a miner connects to us at this relay 2611 // Advertise this relay as the pool's meeting node BEFORE registering,
2612 // so that miners which resolve our descriptor find a relay where the
2613 // pool (not the node's peer-accept) holds the registration.
2614 BtfSetActiveRelay(relay);
2615 // Returns as soon as the relay lists us -- registered, not yet paired.
1966 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 2616 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
1967 2617
1968 if (fShutdown || !gPoolRunning) { 2618 if (fShutdown || !gPoolRunning) {
@@ -1976,6 +2626,23 @@ static void AcceptOneFn(void* arg)
1976 continue; 2626 continue;
1977 } 2627 }
1978 2628
2629 // Block for a miner to actually arrive. RvServiceRegister used to do
2630 // this itself and returned with the relay's 0x01 pairing byte already
2631 // consumed; it no longer does. Without waiting here, BtfServiceWrap
2632 // reads that 0x01 as the first byte of the miner's 32-byte ephemeral
2633 // key -- every subsequent byte is shifted by one, so the X25519
2634 // handshake completes against a garbage shared secret (it has no
2635 // confirmation step to catch it), the tunnel is handed to the event
2636 // loop, and the miner's mining.subscribe never decrypts into a valid
2637 // line. The miner sees the pool close on it; the pool sees a peer that
2638 // never authorized.
2639 if (!btf::RvServiceWaitPaired(rv)) {
2640 btf::RvClose(rv);
2641 LogPrint("pool", "[pool] relay %s: dropped us before any miner dialed"
2642 " -- re-registering\n", relay.c_str());
2643 continue;
2644 }
2645
1979 // Echan handshake -- blocking but fast (local + relay round-trip) 2646 // Echan handshake -- blocking but fast (local + relay round-trip)
1980 btf_socket_t fd = btf::BtfServiceWrap(rv, sk); 2647 btf_socket_t fd = btf::BtfServiceWrap(rv, sk);
1981 if (fd == INVALID_SOCKET) { 2648 if (fd == INVALID_SOCKET) {
@@ -2062,8 +2729,14 @@ void ThreadRPCServer(void*)
2062 2729
2063 LoadPendingPayouts(); 2730 LoadPendingPayouts();
2064 LoadPoolRounds(); 2731 LoadPoolRounds();
2732 LoadPaying();
2733 // Must run before the payout thread starts, so nothing is resent that the
2734 // ledger already shows as sent, and every restored in-flight record is
2735 // armed for its one wallet probe before it is allowed to send.
2736 ReconcilePayouts();
2065 2737
2066 _beginthread(AcceptLoopFn, 0, NULL); 2738 _beginthread(AcceptLoopFn, 0, NULL);
2739 _beginthread(LocalAcceptFn, 0, NULL);
2067 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 2740 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL);
2068 _beginthread(PayoutThreadFn, 0, NULL); 2741 _beginthread(PayoutThreadFn, 0, NULL);
2069 2742