diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 963 |
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 | // --------------------------------------------------------------------------- |
| 253 | struct PendingPayout { | 331 | struct 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 | }; |
| 263 | static std::vector<PendingPayout> gPendingPayouts; | 361 | static std::vector<PendingPayout> gPendingPayouts; |
| 264 | static std::mutex gPayoutLedgerMutex; | 362 | static 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 | ||
| 307 | static 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. | ||
| 418 | static 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 | ||
| 312 | static bool PayoutFullyPaid(const PendingPayout& pp) | 426 | static 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 | ||
| 320 | static 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. | ||
| 441 | static 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. | ||
| 466 | static 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. | ||
| 489 | static void SavePendingPayoutsLocked() | ||
| 490 | { | ||
| 491 | WritePendingPayoutsFile(SerializePendingPayoutsLocked()); | ||
| 357 | } | 492 | } |
| 358 | 493 | ||
| 359 | static void LoadPendingPayouts() | 494 | static 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. | ||
| 557 | struct 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 | }; | ||
| 578 | static std::vector<PayingRecord> gPaying; // guarded by gPayoutExecMutex | ||
| 579 | static std::mutex gPayoutExecMutex; | ||
| 580 | static std::atomic<bool> gPayoutThreadRunning{false}; | ||
| 581 | |||
| 582 | static std::string PayingFilePath() | ||
| 583 | { | ||
| 584 | return GetAppDir() + "/paying_payouts.json"; | ||
| 585 | } | ||
| 586 | |||
| 587 | // Caller must hold gPayoutExecMutex. | ||
| 588 | static 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 | |||
| 631 | static 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 | // --------------------------------------------------------------------------- | ||
| 401 | static nlohmann::json PoolRoundToJson(const PoolRoundProof& r) | 673 | static 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. | ||
| 809 | static 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 | |||
| 823 | static 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. | ||
| 845 | static 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 | |||
| 532 | static void RecordPoolRound(const PoolRoundProof& round) | 886 | static 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 |
| 565 | static std::vector<PendingPayout> gPayoutExecQueue; | 919 | // would be a second answer to "who is owed money". Only the dedupe set is new. |
| 566 | static std::mutex gPayoutExecMutex; | 920 | // |
| 567 | static 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. | |
| 569 | static std::set<std::string> gQueuedPayouts; | 923 | static std::set<std::string> gQueuedPayouts; |
| 570 | 924 | ||
| 571 | static bool FindRecordedPoolPayout(const std::string& id, | 925 | static 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. | ||
| 598 | static void QueuePayouts(int blockHeight, | 957 | static 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. | ||
| 696 | static void FlushMaturePayouts() | 1078 | static 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. | ||
| 1171 | static 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(). | ||
| 1183 | static 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. | ||
| 1202 | static 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. | ||
| 1225 | static 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. | ||
| 1260 | static 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. |
| 725 | static void PayoutThreadFn(void*) | 1281 | static 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. | ||
| 1655 | static 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 | ||