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.cpp852
1 files changed, 791 insertions, 61 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index a4b53ca..3827cd5 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -115,15 +115,28 @@ static uint256 ShareTargetFromDifficulty(double difficulty)
115{ 115{
116 if (difficulty <= 0.0) difficulty = 1.0; 116 if (difficulty <= 0.0) difficulty = 1.0;
117 static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256(); 117 static const uint256 diff1 = CBigNum(bnProofOfWorkLimit).getuint256();
118 // Representability floor. shareTarget = diff1 / difficulty, and diff1 is
119 // ~uint256(0) >> 12, i.e. a hair under 2^244 -- so the true target stops
120 // fitting in a uint256 once difficulty reaches 2^-12 (= 1/4096), and the
121 // fixed-point divide below breaks even earlier in a second way: the
122 // (uint64)(difficulty * 65536.0) divisor truncates to zero for any
123 // difficulty under 2^-16. Both thresholds sit far below VARDIFF_MIN
124 // (0.001), so live operation never comes near this, but a caller that
125 // bypasses vardiff (or a future VARDIFF_MIN) must not divide by zero or
126 // hand back getuint256()'s low-256-bit truncation of an oversized
127 // quotient -- a wrapped target is not merely wrong, it is arbitrarily
128 // HARDER than difficulty 1. At these difficulties every hash is meant to
129 // be a share, so the honest answer is the easiest representable target.
130 // (This replaces a dead branch that read `if (t < diff1 && difficulty <
131 // 1.0)`: t = diff1/difficulty, so difficulty < 1.0 implies t > diff1 and
132 // the two conditions could never both hold.)
133 if (difficulty < 1.0 / 4096.0) return ~uint256(0);
118 // shareTarget = diff1 / difficulty 134 // shareTarget = diff1 / difficulty
119 CBigNum bn; bn.setuint256(diff1); 135 CBigNum bn; bn.setuint256(diff1);
120 // Multiply by 2^16 to keep precision, divide, shift back 136 // Multiply by 2^16 to keep precision, divide, shift back
121 bn <<= 16; 137 bn <<= 16;
122 bn /= (uint64)(difficulty * 65536.0); 138 bn /= (uint64)(difficulty * 65536.0);
123 uint256 t = bn.getuint256(); 139 return bn.getuint256();
124 // Cap at all-ones (maximum, easiest possible)
125 if (t < diff1 && difficulty < 1.0) t = ~uint256(0);
126 return t;
127} 140}
128 141
129// Expected number of hashes to find one share at a given Stratum difficulty, 142// Expected number of hashes to find one share at a given Stratum difficulty,
@@ -223,6 +236,29 @@ static bool RebuildJob()
223 // From the pool, so the template's payout key is in wallet.dat before 236 // From the pool, so the template's payout key is in wallet.dat before
224 // any miner is handed work against it. 237 // any miner is handed work against it.
225 vector<unsigned char> vchPubKey = GetKeyFromPool(); 238 vector<unsigned char> vchPubKey = GetKeyFromPool();
239 if (vchPubKey.empty()) {
240 // Empty means the wallet could not produce a key: encrypted and
241 // locked, or key generation itself failed. Serialised, an empty
242 // vector is a zero-length push, so the coinbase would become
243 // `<empty> OP_CHECKSIG` -- a script nobody can satisfy. Every
244 // block mined against such a job burns its whole subsidy, while
245 // QueuePayouts still records the full obligation and the miners
246 // are then paid out of coin that block never contributed.
247 // BitcoinMiner refuses to mine in this state; refuse to hand the
248 // pool's miners work in it too. printf and not LogPrint for the
249 // reason given above the deaf-node refusal, and throttled the
250 // same way -- the no-job watchdog retries every few seconds for
251 // as long as the condition lasts.
252 static int64 nLastSaidNoKey = 0;
253 if (GetTime() - nLastSaidNoKey > 60) {
254 nLastSaidNoKey = GetTime();
255 printf("RebuildJob: no wallet key available for the coinbase "
256 "(wallet locked, or key generation failed -- the "
257 "GetKeyFromPool line above says which) -- not issuing "
258 "jobs\n");
259 }
260 return false;
261 }
226 static std::atomic<uint32_t> sExtra{0}; 262 static std::atomic<uint32_t> sExtra{0};
227 uint32_t extraNonce = ++sExtra; 263 uint32_t extraNonce = ++sExtra;
228 264
@@ -267,16 +303,29 @@ static bool RebuildJob()
267 map<uint256, CTxIndex> pool; 303 map<uint256, CTxIndex> pool;
268 vector<char> vfAlreadyAdded(mapTransactions.size()); 304 vector<char> vfAlreadyAdded(mapTransactions.size());
269 bool fFoundSomething = true; 305 bool fFoundSomething = true;
270 unsigned int sz = 0; 306 // The cap is MAX_BLOCK_SIZE_GEN, not MAX_SIZE/2. MAX_SIZE is
271 // The cap is MAX_BLOCK_SIZE/2, not MAX_SIZE/2. MAX_SIZE is the 307 // the 32 MB serialisation ceiling, so half of it is 16 MB --
272 // 32 MB serialisation ceiling, so half of it is 16 MB -- this 308 // this builder would happily assemble a template that its own
273 // builder would happily assemble a template that its own
274 // CheckBlock() rejects at the 1 MB consensus cap. This is the 309 // CheckBlock() rejects at the 1 MB consensus cap. This is the
275 // pool's job builder, so a block over the cap is not one 310 // pool's job builder, so a block over the cap is not one
276 // wasted solve, it is every miner on the pool working a 311 // wasted solve, it is every miner on the pool working a
277 // template the network will reject. The inner break bounds the 312 // template the network will reject.
278 // overshoot to a single transaction within a pass. 313 //
279 while (fFoundSomething && sz < MAX_BLOCK_SIZE / 2) 314 // The inner break used to bound the overshoot to one
315 // transaction per pass. One transaction is still too many:
316 // nothing caps how big a single transaction may be, so that
317 // bound was worth up to MAX_SIZE. Asking whether each one fits
318 // before appending it bounds the overshoot at zero instead.
319 //
320 // Seeded with the header, tx count and coinbase, because
321 // CheckBlock() weighs the whole serialized block.
322 unsigned int sz = ::GetSerializeSize(block, SER_DISK);
323 // And the sigop budget, which CheckBlock() enforces and this
324 // builder never counted against. Same counting function
325 // CheckBlock() sums, seeded from the coinbase because it sums
326 // over all of vtx.
327 unsigned int nSigOps = block.vtx[0].GetSigOpCount();
328 while (fFoundSomething && sz < MAX_BLOCK_SIZE_GEN)
280 { 329 {
281 fFoundSomething = false; 330 fFoundSomething = false;
282 unsigned int n = 0; 331 unsigned int n = 0;
@@ -287,16 +336,26 @@ static bool RebuildJob()
287 continue; 336 continue;
288 CTransaction& tx = (*mi).second; 337 CTransaction& tx = (*mi).second;
289 if (tx.IsCoinBase() || !tx.IsFinal()) continue; 338 if (tx.IsCoinBase() || !tx.IsFinal()) continue;
339 // Skip rather than stop: one transaction too big for
340 // the room left does not mean the next is. Ordered
341 // before ConnectInputs because ConnectInputs credits
342 // nFees by reference on success, and a transaction we
343 // then decline would leave the coinbase claiming a fee
344 // this block does not collect.
345 unsigned int nTxSize = ::GetSerializeSize(tx, SER_NETWORK);
346 unsigned int nTxSigOps = tx.GetSigOpCount();
347 if (!BlockTemplateFits(sz, nSigOps, nTxSize, nTxSigOps))
348 continue;
290 map<uint256, CTxIndex> tmp(pool); 349 map<uint256, CTxIndex> tmp(pool);
291 if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees, 350 if (!tx.ConnectInputs(txdb, tmp, CDiskTxPos(1,1,1), 0, nFees,
292 false, true, tx.GetMinFee(block.vtx.size() < 100))) 351 false, true, tx.GetMinFee(block.vtx.size() < 100)))
293 continue; 352 continue;
294 swap(pool, tmp); 353 swap(pool, tmp);
295 block.vtx.push_back(tx); 354 block.vtx.push_back(tx);
296 sz += ::GetSerializeSize(tx, SER_NETWORK); 355 sz += nTxSize;
356 nSigOps += nTxSigOps;
297 vfAlreadyAdded[n] = true; 357 vfAlreadyAdded[n] = true;
298 fFoundSomething = true; 358 fFoundSomething = true;
299 if (sz > MAX_BLOCK_SIZE / 2) break;
300 } 359 }
301 } 360 }
302 } 361 }
@@ -776,6 +835,29 @@ static void PublishPendingPayouts(const VersionedBlob& v)
776 gPayoutLedgerPublishedSeq, "pending payout ledger"); 835 gPayoutLedgerPublishedSeq, "pending payout ledger");
777} 836}
778 837
838// Parse a persisted block-hash field, refusing to guess. SetHex() reads
839// whatever hex prefix it finds and stops at the first character it does not
840// recognise, so a corrupt value does not fail to parse -- it parses as a
841// DIFFERENT hash. Both shapes of that are wrong in a dangerous direction: a
842// value with a hex-looking prefix ("deadbeef...") becomes a hash no block
843// ever had, which PayoutBlockStillOnChain reads as an orphaned block and
844// FlushMaturePayouts then VOIDS the debt; a value with no hex prefix at all
845// becomes 0, which is grandfathered past the orphan check on the strength
846// of garbage. A field that is not the 64-digit hex string ToString() wrote
847// is corruption, and corruption is a load failure like any other parse
848// error -- the file is quarantined, not read around.
849static uint256 ParsePayoutBlockHash(const std::string& str)
850{
851 if (str.size() != 64 ||
852 str.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos)
853 throw std::runtime_error(
854 strprintf("blockHash \"%s\" is not a 64-digit hex string",
855 str.c_str()));
856 uint256 h;
857 h.SetHex(str);
858 return h;
859}
860
779static void LoadPendingPayouts() 861static void LoadPendingPayouts()
780{ 862{
781 std::string path = PayoutFilePath(); 863 std::string path = PayoutFilePath();
@@ -797,7 +879,8 @@ static void LoadPendingPayouts()
797 // field. Leave blockHash zero and they are grandfathered past the 879 // field. Leave blockHash zero and they are grandfathered past the
798 // orphan check rather than voided on a guess. 880 // orphan check rather than voided on a guess.
799 if (obj.contains("blockHash") && obj["blockHash"].is_string()) 881 if (obj.contains("blockHash") && obj["blockHash"].is_string())
800 pp.blockHash.SetHex(obj["blockHash"].get<std::string>()); 882 pp.blockHash =
883 ParsePayoutBlockHash(obj["blockHash"].get<std::string>());
801 if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) 884 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
802 pp.blockHeight = obj["blockHeight"].get<int>(); 885 pp.blockHeight = obj["blockHeight"].get<int>();
803 // Upstream's tolerance for an entry with no usable height, kept 886 // Upstream's tolerance for an entry with no usable height, kept
@@ -976,7 +1059,8 @@ static void LoadPaying()
976 r.amount = obj["amount"].get<int64>(); 1059 r.amount = obj["amount"].get<int64>();
977 r.matureAtHeight = obj["matureAtHeight"].get<int>(); 1060 r.matureAtHeight = obj["matureAtHeight"].get<int>();
978 if (obj.contains("blockHash") && obj["blockHash"].is_string()) 1061 if (obj.contains("blockHash") && obj["blockHash"].is_string())
979 r.blockHash.SetHex(obj["blockHash"].get<std::string>()); 1062 r.blockHash =
1063 ParsePayoutBlockHash(obj["blockHash"].get<std::string>());
980 if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) 1064 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
981 r.blockHeight = obj["blockHeight"].get<int>(); 1065 r.blockHeight = obj["blockHeight"].get<int>();
982 if (obj.contains("retries") && obj["retries"].is_number()) 1066 if (obj.contains("retries") && obj["retries"].is_number())
@@ -1186,11 +1270,35 @@ static bool PayoutBlockStillOnChain(const uint256& blockHash)
1186 return false; 1270 return false;
1187} 1271}
1188 1272
1189static std::string PayoutComment(int blockHeight) 1273// The comment every pre-upgrade payout transaction carries, and therefore the
1274// ONLY needle the legacy fallback in FindRecordedPoolPayout may search with.
1275// This format is frozen: change it and every payment those builds made stops
1276// matching, every one of those debts looks unpaid, and reconciliation pays
1277// them all a second time.
1278static std::string LegacyPayoutComment(int blockHeight)
1190{ 1279{
1191 return strprintf("Pool payout height %d", blockHeight); 1280 return strprintf("Pool payout height %d", blockHeight);
1192} 1281}
1193 1282
1283// The comment written on new sends. It names the funding block, because the
1284// height alone cannot: a height that is won, lost to a reorg, and won again
1285// yields two rounds whose height-only comments are identical -- and identical
1286// amounts too, whenever the same miner held both rounds. The structured
1287// pool_payout_* metadata already keys the payment by hash for the code; this
1288// puts the same fact where an operator reading the wallet can see it, and
1289// makes a new transaction unmatchable by the legacy needle even if the
1290// structured keys are ever lost.
1291//
1292// A zero hash keeps the legacy form: those records predate hash keying, and
1293// there is no hash to name.
1294static std::string PayoutComment(int blockHeight, const uint256& blockHash)
1295{
1296 if (blockHash == 0)
1297 return LegacyPayoutComment(blockHeight);
1298 return strprintf("Pool payout height %d block %s", blockHeight,
1299 blockHash.ToString().c_str());
1300}
1301
1194// Startup reconciliation. 1302// Startup reconciliation.
1195// 1303//
1196// This used to be the crash-recovery mechanism: it scanned the wallet with 1304// This used to be the crash-recovery mechanism: it scanned the wallet with
@@ -1345,14 +1453,37 @@ static void MarkPoolRoundForfeited(const std::string& id,
1345 PublishPoolRounds(snap); 1453 PublishPoolRounds(snap);
1346} 1454}
1347 1455
1456// Does this round have a published round proof?
1457//
1458// Existence answers a temporal question the legacy fallback below needs:
1459// which builds could have paid this round. RecordPoolRound writes the proof
1460// when the round is created, and structured payout metadata (upstream
1461// cbdd06d) is an ancestor of round proofs (upstream 83ba822) -- no build
1462// able to write a proof has ever sent a payment that only a comment can
1463// identify. A round with a proof therefore cannot have a comment-only
1464// payment; one that matches it is another round's, colliding on the height.
1465static bool PoolRoundHasProof(const std::string& id)
1466{
1467 std::lock_guard<std::mutex> lk(gPoolRoundsMutex);
1468 for (const PoolRoundProof& r : gPoolRounds)
1469 if (r.id == id)
1470 return true;
1471 return false;
1472}
1473
1348// Which wallet transaction, if any, already paid this? 1474// Which wallet transaction, if any, already paid this?
1349// 1475//
1350// Two kinds of evidence, in order of quality. 1476// Two kinds of evidence, in order of quality.
1351// 1477//
1352// Structured (pool_payout_id / pool_payout_address / pool_payout_amount) is 1478// Structured (pool_payout_id / pool_payout_address / pool_payout_amount,
1353// upstream's, and it is exact: the id is derived from the block hash, so it 1479// plus pool_payout_block since this series) is upstream's, extended. The id
1354// names one round and one round only. Every transaction this build sends 1480// is derived from the block hash, but only from the FIRST sixteen hex digits
1355// carries it. 1481// of it -- and proof-of-work concentrates zeros exactly there, so the id can
1482// name two same-height rounds identically. New sends therefore record the
1483// full hash as pool_payout_block, and a structured match must agree with it
1484// wherever both sides have one; a transaction without the key was sent
1485// before it existed, and for those the id is the best evidence there is.
1486// Every transaction this build sends carries all four keys.
1356// 1487//
1357// But NO transaction this pool has ever sent carries it. Every payout in the 1488// But NO transaction this pool has ever sent carries it. Every payout in the
1358// existing wallet has only a "comment". So on the first boot after this 1489// existing wallet has only a "comment". So on the first boot after this
@@ -1375,11 +1506,22 @@ static void MarkPoolRoundForfeited(const std::string& id,
1375// (round, address) pair -- from the ledger's paidTxids, which is durable, 1506// (round, address) pair -- from the ledger's paidTxids, which is durable,
1376// and from what this process has already claimed. One legacy transaction 1507// and from what this process has already claimed. One legacy transaction
1377// can therefore settle at most one debt, so N transactions can never 1508// can therefore settle at most one debt, so N transactions can never
1378// satisfy more than N obligations. 1509// satisfy more than N obligations;
1379// 1510//
1380// What is left unresolved by construction is two pre-upgrade rounds at the 1511// 4. it is not consulted at all for a round that has a round proof
1381// SAME height paying the same address the same amount, since the comment 1512// (fAllowLegacy, decided by the caller from PoolRoundHasProof). Proofs
1382// carries only the height. That needs a reorg. 1513// are written when a round is created, and only by builds whose every
1514// payment names its round exactly -- so a comment-only transaction
1515// cannot be a payment for such a round, only a same-height collision
1516// from an older one, and skipping the fallback cannot hide a real
1517// payment: the structured probe above already found any that exist.
1518//
1519// What is left unresolved by construction is a comment-only transaction and
1520// a PROOFLESS round at the same height paying the same address the same
1521// amount: two grandfathered zero-hash twins, or a twin pair from the builds
1522// that keyed the ledger by hash before any metadata reached the wallet. The
1523// comment carries only the height, so nothing ties the transaction to one
1524// twin rather than the other. That needs a reorg.
1383// 1525//
1384// This used to say the losing twin's ledger entry is voided by the orphan 1526// This used to say the losing twin's ledger entry is voided by the orphan
1385// check before it can be paid. That is wrong: PayoutBlockStillOnChain() 1527// check before it can be paid. That is wrong: PayoutBlockStillOnChain()
@@ -1413,8 +1555,10 @@ enum PayoutProbeResult {
1413 1555
1414static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, 1556static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1415 int blockHeight, 1557 int blockHeight,
1558 const uint256& blockHash,
1416 const std::string& address, 1559 const std::string& address,
1417 int64 amount, 1560 int64 amount,
1561 bool fAllowLegacy,
1418 const std::set<std::string>& excluded, 1562 const std::set<std::string>& excluded,
1419 std::string& txidRet) 1563 std::string& txidRet)
1420{ 1564{
@@ -1425,7 +1569,7 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1425 CScript want; 1569 CScript want;
1426 if (fHaveScript) 1570 if (fHaveScript)
1427 want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG; 1571 want << OP_DUP << OP_HASH160 << h160 << OP_EQUALVERIFY << OP_CHECKSIG;
1428 const std::string comment = PayoutComment(blockHeight); 1572 const std::string comment = LegacyPayoutComment(blockHeight);
1429 1573
1430 // Candidate legacy matches, adjudicated after cs_mapWallet is released. 1574 // Candidate legacy matches, adjudicated after cs_mapWallet is released.
1431 // 1575 //
@@ -1458,10 +1602,25 @@ static PayoutProbeResult FindRecordedPoolPayout(const std::string& id,
1458 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount"); 1602 map<string, string>::const_iterator amtIt = wtx.mapValue.find("pool_payout_amount");
1459 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount)) 1603 if (amtIt == wtx.mapValue.end() || amtIt->second != strprintf("%lld", amount))
1460 continue; 1604 continue;
1605 // The id alone cannot tell twin rounds at one height apart
1606 // (see above); the full hash can, where the transaction
1607 // recorded one. Where it did not, the id decides, as it
1608 // always has -- refusing those transactions would make every
1609 // payment sent before the key existed look unpaid and pay it
1610 // again.
1611 if (blockHash != 0) {
1612 map<string, string>::const_iterator blkIt =
1613 wtx.mapValue.find("pool_payout_block");
1614 if (blkIt != wtx.mapValue.end()
1615 && blkIt->second != blockHash.ToString())
1616 continue; // a twin round's payment, not this round's
1617 }
1461 txidRet = wtx.GetHash().GetHex(); 1618 txidRet = wtx.GetHash().GetHex();
1462 return PAYOUT_PROBE_STRUCTURED; 1619 return PAYOUT_PROBE_STRUCTURED;
1463 } 1620 }
1464 1621
1622 if (!fAllowLegacy)
1623 continue; // comment-only evidence is inadmissible: rule 4
1465 if (!fHaveScript) 1624 if (!fHaveScript)
1466 continue; 1625 continue;
1467 // The comment is written only by the payout path, so its presence 1626 // The comment is written only by the payout path, so its presence
@@ -1732,7 +1891,9 @@ static void FlushMaturePayouts()
1732 } 1891 }
1733 int64 total = 0; 1892 int64 total = 0;
1734 for (auto& kv : it->amounts) total += kv.second; 1893 for (auto& kv : it->amounts) total += kv.second;
1735 LogPrint("payout", "[payout] block %d (%s) was orphaned -- voiding %zu" 1894 // Always: one line per voided entry, never a collapsed summary.
1895 // A reorg voids these in a burst from this one source line.
1896 LogPrintAlways("payout", "[payout] block %d (%s) was orphaned -- voiding %zu"
1736 " payment(s) totalling %s\n", 1897 " payment(s) totalling %s\n",
1737 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(), 1898 it->blockHeight, it->blockHash.ToString().substr(0, 14).c_str(),
1738 it->amounts.size(), FormatMoney(total).c_str()); 1899 it->amounts.size(), FormatMoney(total).c_str());
@@ -1931,7 +2092,7 @@ static void RecordPayoutTxid(const PayingRecord& r, const std::string& txid)
1931// disagreeing. 2092// disagreeing.
1932static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) 2093static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason)
1933{ 2094{
1934 LogPrint("payout", "[payout] writing off %s owed to %s for block %d: %s\n", 2095 LogPrintAlways("payout", "[payout] writing off %s owed to %s for block %d: %s\n",
1935 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight, 2096 FormatMoney(r.amount).c_str(), r.address.c_str(), r.blockHeight,
1936 reason.c_str()); 2097 reason.c_str());
1937 VersionedBlob blob; 2098 VersionedBlob blob;
@@ -2036,7 +2197,7 @@ static void PayoutThreadFn(void*)
2036 // funding block can land at any point in that window. Upstream has 2197 // funding block can land at any point in that window. Upstream has
2037 // no equivalent -- it decides once, when the entry matures. 2198 // no equivalent -- it decides once, when the entry matures.
2038 if (!PayoutBlockStillOnChain(r.blockHash)) { 2199 if (!PayoutBlockStillOnChain(r.blockHash)) {
2039 LogPrint("payout", "[payout] block %d (%s) orphaned while queued" 2200 LogPrintAlways("payout", "[payout] block %d (%s) orphaned while queued"
2040 " -- dropping %s to %s\n", 2201 " -- dropping %s to %s\n",
2041 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(), 2202 r.blockHeight, r.blockHash.ToString().substr(0, 14).c_str(),
2042 FormatMoney(r.amount).c_str(), r.address.c_str()); 2203 FormatMoney(r.amount).c_str(), r.address.c_str());
@@ -2080,7 +2241,7 @@ static void PayoutThreadFn(void*)
2080 const std::string reason = 2241 const std::string reason =
2081 strprintf("'%s' is not a valid address, so no transaction" 2242 strprintf("'%s' is not a valid address, so no transaction"
2082 " can pay it", r.address.c_str()); 2243 " can pay it", r.address.c_str());
2083 LogPrint("payout", "[payout] SKIPPED bad address '%s' --" 2244 LogPrintAlways("payout", "[payout] SKIPPED bad address '%s' --"
2084 " %s owed for block %d cannot be paid\n", 2245 " %s owed for block %d cannot be paid\n",
2085 r.address.c_str(), FormatMoney(r.amount).c_str(), 2246 r.address.c_str(), FormatMoney(r.amount).c_str(),
2086 r.blockHeight); 2247 r.blockHeight);
@@ -2099,9 +2260,15 @@ static void PayoutThreadFn(void*)
2099 std::set<std::string> excluded; 2260 std::set<std::string> excluded;
2100 CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address, 2261 CollectForeignPayoutTxids(r.blockHeight, r.blockHash, r.address,
2101 claimKey, excluded); 2262 claimKey, excluded);
2263 // A round with a published proof was created by a build whose
2264 // every payment names its round exactly, so only structured
2265 // evidence can settle it -- a comment match would be another
2266 // round's payment colliding on the height.
2267 const bool fAllowLegacy = !PoolRoundHasProof(id);
2102 r.fProbeWallet = false; 2268 r.fProbeWallet = false;
2103 PayoutProbeResult probe = FindRecordedPoolPayout( 2269 PayoutProbeResult probe = FindRecordedPoolPayout(
2104 id, r.blockHeight, r.address, r.amount, excluded, recovered); 2270 id, r.blockHeight, r.blockHash, r.address, r.amount,
2271 fAllowLegacy, excluded, recovered);
2105 if (probe != PAYOUT_PROBE_NONE) { 2272 if (probe != PAYOUT_PROBE_NONE) {
2106 if (probe == PAYOUT_PROBE_LEGACY) { 2273 if (probe == PAYOUT_PROBE_LEGACY) {
2107 // Loud on purpose: this is the upgrade path, where the 2274 // Loud on purpose: this is the upgrade path, where the
@@ -2186,7 +2353,7 @@ static void PayoutThreadFn(void*)
2186 } 2353 }
2187 2354
2188 CWalletTx wtx; 2355 CWalletTx wtx;
2189 wtx.mapValue["comment"] = PayoutComment(r.blockHeight); 2356 wtx.mapValue["comment"] = PayoutComment(r.blockHeight, r.blockHash);
2190 // Upstream's structured metadata. This is what makes the wallet 2357 // Upstream's structured metadata. This is what makes the wallet
2191 // probe above possible at all -- the old comment-only form could 2358 // probe above possible at all -- the old comment-only form could
2192 // not distinguish two rounds paying the same address the same 2359 // not distinguish two rounds paying the same address the same
@@ -2195,6 +2362,11 @@ static void PayoutThreadFn(void*)
2195 wtx.mapValue["pool_payout_id"] = id; 2362 wtx.mapValue["pool_payout_id"] = id;
2196 wtx.mapValue["pool_payout_address"] = r.address; 2363 wtx.mapValue["pool_payout_address"] = r.address;
2197 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount); 2364 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", r.amount);
2365 // The full hash, not only the id's sixteen-digit prefix: the
2366 // prefix of a proof-of-work hash is where the zeros live, and
2367 // the probe needs to tell twin rounds at one height apart.
2368 if (r.blockHash != 0)
2369 wtx.mapValue["pool_payout_block"] = r.blockHash.ToString();
2198 2370
2199 SendMoneyOutcome outcome; 2371 SendMoneyOutcome outcome;
2200 if (SendMoney(sc, r.amount, wtx, outcome)) { 2372 if (SendMoney(sc, r.amount, wtx, outcome)) {
@@ -2205,7 +2377,7 @@ static void PayoutThreadFn(void*)
2205 // described by at least one of them at every instant. 2377 // described by at least one of them at every instant.
2206 RecordPayoutTxid(r, wtx.GetHash().GetHex()); 2378 RecordPayoutTxid(r, wtx.GetHash().GetHex());
2207 ForgetPayment(r); 2379 ForgetPayment(r);
2208 LogPrint("payout", "[payout] paid %s to %s (tx %s)\n", 2380 LogPrintAlways("payout", "[payout] paid %s to %s (tx %s)\n",
2209 FormatMoney(r.amount).c_str(), r.address.c_str(), 2381 FormatMoney(r.amount).c_str(), r.address.c_str(),
2210 wtx.GetHash().ToString().substr(0, 12).c_str()); 2382 wtx.GetHash().ToString().substr(0, 12).c_str());
2211 } else { 2383 } else {
@@ -2263,7 +2435,7 @@ static void PayoutThreadFn(void*)
2263 // going away must not turn the log into one line a minute 2435 // going away must not turn the log into one line a minute
2264 // forever, and must not go quiet either. 2436 // forever, and must not go quiet either.
2265 if (r.retries <= 5 || r.retries % 60 == 0) 2437 if (r.retries <= 5 || r.retries % 60 == 0)
2266 LogPrint("payout", "[payout] FAILED to pay %s to %s" 2438 LogPrintAlways("payout", "[payout] FAILED to pay %s to %s"
2267 " (%s spendable, wallet unlocked, block %d on" 2439 " (%s spendable, wallet unlocked, block %d on"
2268 " the main chain) -- attempt %d, retrying in" 2440 " the main chain) -- attempt %d, retrying in"
2269 " 60s; this debt stands until it is paid\n", 2441 " 60s; this debt stands until it is paid\n",
@@ -2471,6 +2643,31 @@ static const double VARDIFF_MAX = 1e12;
2471static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance 2643static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance
2472static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much 2644static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much
2473 2645
2646// #7: m->hashRate is an EWMA sampled only when a share arrives, so a miner
2647// that stops hashing keeps its last estimate forever and RefreshStats sums
2648// it into the announced pool total for as long as the connection sits open
2649// -- the published pool hashrate ran several times over ground truth
2650// because of exactly this. MinerReportedHashRate() below applies the
2651// policy: a miner gets a grace of HASHRATE_IDLE_MISSES expected share
2652// intervals (expected interval = expected hashes per share at its
2653// difficulty, divided by the estimate itself) with no share, after which
2654// the figure we report falls off with a half-life of one more such
2655// interval. The interval is clamped to [HASHRATE_MIN_INTERVAL,
2656// HASHRATE_MAX_INTERVAL] so a fast miner at the vardiff floor (expected
2657// interval of milliseconds) is not punished for ordinary share-delivery
2658// jitter, and a very slow miner's grace does not stretch past the point
2659// where its estimate means anything. At the vardiff target of one share
2660// per 15s that is a minute of grace, half the rate reported 75s in, and
2661// nothing reported after about seventeen minutes of silence.
2662static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals
2663static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval
2664static const double HASHRATE_MAX_INTERVAL = 3600.0; // seconds; ceiling for the expected interval
2665// Half-lives past the grace beyond which we report a flat zero. 2^-64 of the
2666// original estimate is not a number anyone needs, and stopping here keeps
2667// pow() from walking down into the denormals for a miner that has been quiet
2668// for hours.
2669static const double HASHRATE_DECAY_CUTOFF = 64.0;
2670
2474struct Miner { 2671struct Miner {
2475 btf_socket_t fd; 2672 btf_socket_t fd;
2476 bool authorised = false; 2673 bool authorised = false;
@@ -2498,11 +2695,80 @@ struct Miner {
2498 int64 lastShareTime = 0; // GetTime() of the previous accepted share 2695 int64 lastShareTime = 0; // GetTime() of the previous accepted share
2499 double hashRate = 0.0; // smoothed H/s estimate 2696 double hashRate = 0.0; // smoothed H/s estimate
2500 2697
2698 // Replay guard for mining.submit (#8): nonces already credited on this
2699 // connection for the job it is currently working. Keyed to the job id in
2700 // seenNoncesJob and cleared by SendJob whenever that id changes, so a
2701 // resubmission under a NEW job is never turned away, while a replayed
2702 // (job_id, nonce) pair under the same job is refused before it can be
2703 // credited twice into roundShareCount -- the figure the payout split
2704 // divides by.
2705 std::set<uint32_t> seenNonces;
2706 std::string seenNoncesJob;
2707
2501 explicit Miner(btf_socket_t f) 2708 explicit Miner(btf_socket_t f)
2502 : fd(f), connectTime(GetTime()), 2709 : fd(f), connectTime(GetTime()),
2503 extranonce2(gNextExtranonce2.fetch_add(1)) {} 2710 extranonce2(gNextExtranonce2.fetch_add(1)) {}
2504}; 2711};
2505 2712
2713// What we publish as a miner's H/s: the last EWMA sample, faded out if shares
2714// have stopped arriving (#7, policy at HASHRATE_IDLE_MISSES).
2715//
2716// This reads m->hashRate and never writes it. That is the whole design. The
2717// first attempt decayed the stored estimate in place from the event loop's
2718// sweep, which put a running accumulator behind a figure that is only ever
2719// displayed, and the accumulator misbehaved in two ways:
2720//
2721// - `expected` was recomputed from the already-decayed estimate on every
2722// pass, so each decay raised `expected`, which raised `grace`, which is
2723// the very thing gating the next decay. `grace` simply chased `silent`
2724// upward and the reported rate settled into a 1/t crawl instead of the
2725// half-life curve the comment above promises -- 291x too high at ten
2726// half-lives past the grace. Issue #7 is about the pool over-reporting;
2727// that under-corrected it for hours.
2728// - Once `expected` reached HASHRATE_MAX_INTERVAL it could not rise any
2729// further, the feedback that had been gating each pass was gone, and the
2730// decay began compounding once a second against the total elapsed
2731// silence -- taking a miner from 0.4% of its baseline to 1e-11 of it in
2732// the next hundred seconds, four hours in, for no reason a reader of
2733// either the code or the announced number could see.
2734//
2735// Computing the value instead of accumulating it removes the failure mode
2736// rather than tuning it: with no stored decay state there is nothing for a
2737// tick rate, a missed pass or a duplicated pass to perturb, and the answer
2738// depends only on how long the miner has actually been quiet. It also keeps
2739// the fade out of the estimator -- the EWMA on the share path still blends
2740// against the value it last computed, not against a faded copy of it, so a
2741// miner that resumes recovers on exactly the curve it would have without any
2742// of this.
2743//
2744// `now` is a parameter rather than a GetTime() call so that one refresh fades
2745// every miner to the same instant, and so the policy is directly testable.
2746static double MinerReportedHashRate(const Miner* m, int64 now)
2747{
2748 // Nothing sampled yet: a fresh connection, or one that has found a single
2749 // share and so has no interval to have been timed over. Nothing to fade,
2750 // and nothing to divide by below.
2751 if (m->hashRate <= 0.0 || m->lastShareTime <= 0) return m->hashRate;
2752
2753 double expected = ExpectedHashesForDifficulty(m->difficulty) / m->hashRate;
2754 // Written so that a NaN would land on the floor rather than sail through:
2755 // the clamp is what guarantees `expected` is finite and strictly positive
2756 // below, whatever the division produced.
2757 if (!(expected > HASHRATE_MIN_INTERVAL)) expected = HASHRATE_MIN_INTERVAL;
2758 if (expected > HASHRATE_MAX_INTERVAL) expected = HASHRATE_MAX_INTERVAL;
2759
2760 double silent = (double)(now - m->lastShareTime);
2761 double grace = HASHRATE_IDLE_MISSES * expected;
2762 // grace is at least HASHRATE_IDLE_MISSES seconds, so a clock that has
2763 // stepped backwards gives a negative `silent`, takes this branch, and
2764 // reports the sample unchanged. Nothing here can scale an estimate up.
2765 if (!(silent > grace)) return m->hashRate;
2766
2767 double halflives = (silent - grace) / expected;
2768 if (halflives >= HASHRATE_DECAY_CUTOFF) return 0.0;
2769 return m->hashRate * pow(0.5, halflives);
2770}
2771
2506static std::vector<Miner*> gMiners; 2772static std::vector<Miner*> gMiners;
2507 2773
2508struct FoundBlock { int height; int64 ts; std::string addr; }; 2774struct FoundBlock { int height; int64 ts; std::string addr; };
@@ -2553,6 +2819,18 @@ static void LocalAcceptFn(void*)
2553 break; 2819 break;
2554 continue; 2820 continue;
2555 } 2821 }
2822 // #6: let the OS probe a peer that has gone silent, so a half-open
2823 // session eventually errors out of recv and lands in the event
2824 // loop's existing reap path instead of sitting in gMiners forever.
2825 // This is a backstop, not the mechanism: the default cadence before
2826 // the first probe is hours on most stacks, so the idle timeout in
2827 // PoolEventLoop fires long before keepalive does. Miners arriving
2828 // via a relay ride a loopback socket pair whose remote transport is
2829 // the rendezvous socket owned by the tunnel pump threads; keepalive
2830 // cannot reach across that, which is part of why the idle timeout
2831 // exists.
2832 int keepalive = 1;
2833 setsockopt(c, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive));
2556 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n"); 2834 LogPrint("pool", "[pool] miner arrived via loopback -- pushing to event loop\n");
2557 PushReady((btf_socket_t)c); 2835 PushReady((btf_socket_t)c);
2558 } 2836 }
@@ -2596,6 +2874,16 @@ static const int64 POOL_ACTIVE_WINDOW = 300; // a miner is "active" if it
2596static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not 2874static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not
2597 // authorized after this long is 2875 // authorized after this long is
2598 // idle, not merely starting up 2876 // idle, not merely starting up
2877// #6: a Miner used to leave gMiners only when RecvLine reported a clean
2878// disconnect. A half-open session (miner host died, NAT state expired,
2879// cable pulled) sends no FIN and reports no error, so it sat in the table
2880// forever -- authorize outpaced disconnect 2:1 on the live pool, and the
2881// table grew toward MAX_POOL_MINERS, past which real miners are refused.
2882// lastSeen is stamped on every complete line received; a miner that has
2883// said nothing for this long is gone in every way that matters. Ten
2884// minutes is far beyond the vardiff target (a share every 15s) and the
2885// stall watchdog (45s), so no miner that is actually hashing comes close.
2886static const int64 POOL_IDLE_TIMEOUT_SECS = 600;
2599 2887
2600// Non-blocking recv: pull whatever bytes are available now into buf, then 2888// Non-blocking recv: pull whatever bytes are available now into buf, then
2601// return one complete line if one is ready. Three outcomes: 2889// return one complete line if one is ready. Three outcomes:
@@ -2680,6 +2968,17 @@ static json MakeNotifyParams(const StratumJob& job, const CBlock& b,
2680static void SendJob(Miner* m, bool clean) 2968static void SendJob(Miner* m, bool clean)
2681{ 2969{
2682 if (!gHaveJob) return; 2970 if (!gHaveJob) return;
2971 // Every path that hands a miner a new job funnels through here, so this
2972 // is the one place the replay guard needs to follow the job id: nonces
2973 // credited under the previous job mean nothing under the new one. Keying
2974 // the clear to the id (rather than clearing unconditionally) keeps a
2975 // same-job re-notify -- the vardiff retarget and stall watchdog both
2976 // re-send the CURRENT job -- from reopening the duplicate window it
2977 // exists to close.
2978 if (m->seenNoncesJob != gCurrentJob.jobId) {
2979 m->seenNonces.clear();
2980 m->seenNoncesJob = gCurrentJob.jobId;
2981 }
2683 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2); 2982 CBlock mb = BuildMinerBlock(gCurrentJob, m->extranonce2);
2684 // Use this miner's own shareTarget (set by vardiff) in the notify params. 2983 // Use this miner's own shareTarget (set by vardiff) in the notify params.
2685 // The participant miner reads p[3] as its share target, and HandleLine 2984 // The participant miner reads p[3] as its share target, and HandleLine
@@ -2938,6 +3237,22 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2938 } 3237 }
2939 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4); 3238 unsigned int nNonce; memcpy(&nNonce, nb.data(), 4);
2940 3239
3240 // Reject a replayed (job, nonce) before it costs a rebuild and a
3241 // RandomX hash: nothing about the work below distinguishes a second
3242 // submission of a valid share from the first, so without this check
3243 // it passes every test and is credited again -- and roundShareCount
3244 // is what the payout split divides by, so each replay multiplies the
3245 // address's cut of the round. The set is per-connection and cleared
3246 // by SendJob on every job change, so only a same-job repeat is
3247 // refused.
3248 if (m->seenNonces.count(nNonce)) {
3249 LogPrint("worker", "[worker->pool] duplicate share from %s"
3250 " nonce=%u job=%s -- already credited, not counting again\n",
3251 m->address.c_str(), nNonce, submitJobId.c_str());
3252 reply(false, "Duplicate share");
3253 return true;
3254 }
3255
2941 // Rebuild this miner's exact block (unique merkle via extranonce2) 3256 // Rebuild this miner's exact block (unique merkle via extranonce2)
2942 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2); 3257 CBlock b = BuildMinerBlock(gCurrentJob, m->extranonce2);
2943 b.nNonce = nNonce; 3258 b.nNonce = nNonce;
@@ -2965,6 +3280,7 @@ static bool HandleLine(Miner* m, const std::string& rawLine,
2965 m->roundShares++; 3280 m->roundShares++;
2966 m->totalShares++; 3281 m->totalShares++;
2967 m->vardiffShares++; 3282 m->vardiffShares++;
3283 m->seenNonces.insert(nNonce); // replay guard: credit each nonce once per job
2968 int64 shareTime = GetTime(); 3284 int64 shareTime = GetTime();
2969 m->lastSeen = shareTime; 3285 m->lastSeen = shareTime;
2970 roundShareCount[m->address]++; 3286 roundShareCount[m->address]++;
@@ -3261,7 +3577,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment,
3261 int64 amount, 3577 int64 amount,
3262 int nonce, 3578 int nonce,
3263 const std::string& structuredId, 3579 const std::string& structuredId,
3264 bool fConfirmed = true) 3580 bool fConfirmed = true,
3581 const std::string& structuredBlock = "")
3265{ 3582{
3266 CWalletTx wtx; 3583 CWalletTx wtx;
3267 wtx.vin.resize(1); 3584 wtx.vin.resize(1);
@@ -3276,6 +3593,8 @@ static std::string PayoutTestAddWalletTx(const std::string& comment,
3276 wtx.mapValue["pool_payout_id"] = structuredId; 3593 wtx.mapValue["pool_payout_id"] = structuredId;
3277 wtx.mapValue["pool_payout_address"] = address; 3594 wtx.mapValue["pool_payout_address"] = address;
3278 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount); 3595 wtx.mapValue["pool_payout_amount"] = strprintf("%lld", amount);
3596 if (!structuredBlock.empty())
3597 wtx.mapValue["pool_payout_block"] = structuredBlock;
3279 } 3598 }
3280 uint256 hash = wtx.GetHash(); 3599 uint256 hash = wtx.GetHash();
3281 if (fConfirmed) { 3600 if (fConfirmed) {
@@ -3825,34 +4144,37 @@ int RunPayoutLedgerSelfTest()
3825 const int height = 8080; 4144 const int height = 8080;
3826 const int64 amount = 7 * COIN; 4145 const int64 amount = 7 * COIN;
3827 const std::string id = PayoutIdForBlock(height, 0); 4146 const std::string id = PayoutIdForBlock(height, 0);
3828 const std::string comment = PayoutComment(height); 4147 const std::string comment = LegacyPayoutComment(height);
3829 std::set<std::string> none; 4148 std::set<std::string> none;
3830 std::string found; 4149 std::string found;
4150 // No round proof exists for these rounds, so legacy evidence is
4151 // admissible -- computed exactly as the payout thread computes it.
4152 const bool fAllowLegacy = !PoolRoundHasProof(id);
3831 4153
3832 check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) 4154 check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found)
3833 == PAYOUT_PROBE_NONE, 4155 == PAYOUT_PROBE_NONE,
3834 "an empty wallet is not evidence that anything was paid"); 4156 "an empty wallet is not evidence that anything was paid");
3835 4157
3836 const std::string legacyTxid = 4158 const std::string legacyTxid =
3837 PayoutTestAddWalletTx(comment, addrA, amount, 1, ""); 4159 PayoutTestAddWalletTx(comment, addrA, amount, 1, "");
3838 check(FindRecordedPoolPayout(id, height, addrA, amount, none, found) 4160 check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, none, found)
3839 == PAYOUT_PROBE_LEGACY && found == legacyTxid, 4161 == PAYOUT_PROBE_LEGACY && found == legacyTxid,
3840 "a payment carrying only a pre-upgrade comment is recognised as" 4162 "a payment carrying only a pre-upgrade comment is recognised as"
3841 " already paid"); 4163 " already paid");
3842 4164
3843 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) 4165 check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found)
3844 == PAYOUT_PROBE_NONE, 4166 == PAYOUT_PROBE_NONE,
3845 "the legacy match requires the output to pay THIS recipient"); 4167 "the legacy match requires the output to pay THIS recipient");
3846 check(FindRecordedPoolPayout(id, height, addrA, amount + COIN, none, found) 4168 check(FindRecordedPoolPayout(id, height, 0, addrA, amount + COIN, fAllowLegacy, none, found)
3847 == PAYOUT_PROBE_NONE, 4169 == PAYOUT_PROBE_NONE,
3848 "the legacy match requires the output to pay exactly this amount"); 4170 "the legacy match requires the output to pay exactly this amount");
3849 check(FindRecordedPoolPayout(id, height + 1, addrA, amount, none, found) 4171 check(FindRecordedPoolPayout(id, height + 1, 0, addrA, amount, fAllowLegacy, none, found)
3850 == PAYOUT_PROBE_NONE, 4172 == PAYOUT_PROBE_NONE,
3851 "the legacy match requires the comment for this block height"); 4173 "the legacy match requires the comment for this block height");
3852 4174
3853 std::set<std::string> spent; 4175 std::set<std::string> spent;
3854 spent.insert(legacyTxid); 4176 spent.insert(legacyTxid);
3855 check(FindRecordedPoolPayout(id, height, addrA, amount, spent, found) 4177 check(FindRecordedPoolPayout(id, height, 0, addrA, amount, fAllowLegacy, spent, found)
3856 == PAYOUT_PROBE_NONE, 4178 == PAYOUT_PROBE_NONE,
3857 "one legacy transaction cannot settle a second debt"); 4179 "one legacy transaction cannot settle a second debt");
3858 4180
@@ -3861,7 +4183,7 @@ int RunPayoutLedgerSelfTest()
3861 // exactly what the structured metadata exists to prevent. 4183 // exactly what the structured metadata exists to prevent.
3862 const std::string otherId = PayoutIdForBlock(height, 0) + "-other"; 4184 const std::string otherId = PayoutIdForBlock(height, 0) + "-other";
3863 PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId); 4185 PayoutTestAddWalletTx(comment, addrB, amount, 2, otherId);
3864 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) 4186 check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found)
3865 == PAYOUT_PROBE_NONE, 4187 == PAYOUT_PROBE_NONE,
3866 "another round's structured payment is not legacy evidence for" 4188 "another round's structured payment is not legacy evidence for"
3867 " this one"); 4189 " this one");
@@ -3869,7 +4191,7 @@ int RunPayoutLedgerSelfTest()
3869 // The structured path still wins where it applies. 4191 // The structured path still wins where it applies.
3870 const std::string structuredTxid = 4192 const std::string structuredTxid =
3871 PayoutTestAddWalletTx(comment, addrB, amount, 3, id); 4193 PayoutTestAddWalletTx(comment, addrB, amount, 3, id);
3872 check(FindRecordedPoolPayout(id, height, addrB, amount, none, found) 4194 check(FindRecordedPoolPayout(id, height, 0, addrB, amount, fAllowLegacy, none, found)
3873 == PAYOUT_PROBE_STRUCTURED && found == structuredTxid, 4195 == PAYOUT_PROBE_STRUCTURED && found == structuredTxid,
3874 "structured payout metadata is matched in preference to a comment"); 4196 "structured payout metadata is matched in preference to a comment");
3875 4197
@@ -3883,7 +4205,7 @@ int RunPayoutLedgerSelfTest()
3883 PayoutTestClearBlockIndex(); 4205 PayoutTestClearBlockIndex();
3884 const std::string addrC = PayoutTestAddress(0x33); 4206 const std::string addrC = PayoutTestAddress(0x33);
3885 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false); 4207 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", false);
3886 check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) 4208 check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found)
3887 == PAYOUT_PROBE_NONE, 4209 == PAYOUT_PROBE_NONE,
3888 "a pre-upgrade payout that never confirmed is not read as" 4210 "a pre-upgrade payout that never confirmed is not read as"
3889 " payment"); 4211 " payment");
@@ -3893,7 +4215,7 @@ int RunPayoutLedgerSelfTest()
3893 mapWallet.clear(); 4215 mapWallet.clear();
3894 const std::string confirmedTxid = 4216 const std::string confirmedTxid =
3895 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true); 4217 PayoutTestAddWalletTx(comment, addrC, amount, 4, "", true);
3896 check(FindRecordedPoolPayout(id, height, addrC, amount, none, found) 4218 check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, none, found)
3897 == PAYOUT_PROBE_LEGACY && found == confirmedTxid, 4219 == PAYOUT_PROBE_LEGACY && found == confirmedTxid,
3898 "the same payout, confirmed, is"); 4220 "the same payout, confirmed, is");
3899 4221
@@ -3904,7 +4226,7 @@ int RunPayoutLedgerSelfTest()
3904 PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false); 4226 PayoutTestAddWalletTx(comment, addrC, amount, 5, "", false);
3905 std::set<std::string> spentFirst; 4227 std::set<std::string> spentFirst;
3906 spentFirst.insert(confirmedTxid); 4228 spentFirst.insert(confirmedTxid);
3907 check(FindRecordedPoolPayout(id, height, addrC, amount, spentFirst, found) 4229 check(FindRecordedPoolPayout(id, height, 0, addrC, amount, fAllowLegacy, spentFirst, found)
3908 == PAYOUT_PROBE_LEGACY && found == alsoConfirmed, 4230 == PAYOUT_PROBE_LEGACY && found == alsoConfirmed,
3909 "an unconfirmed transaction does not hide a confirmed one behind" 4231 "an unconfirmed transaction does not hide a confirmed one behind"
3910 " it"); 4232 " it");
@@ -4419,6 +4741,248 @@ int RunPayoutLedgerSelfTest()
4419 } 4741 }
4420#endif 4742#endif
4421 4743
4744 // -- 14. The payout comment names the funding block ----------------------
4745 //
4746 // Same height, two hashes: the shape a reorg leaves when the pool wins a
4747 // height, loses the block, and wins the height again. The legacy needle is
4748 // frozen at the pre-upgrade format, so pre-upgrade payments keep matching;
4749 // a new-format comment never matches it, so a new send can never be read
4750 // as a pre-upgrade payment for a twin round.
4751 {
4752 PayoutTestResetState();
4753 CRITICAL_BLOCK(cs_mapWallet)
4754 mapWallet.clear();
4755 PayoutTestClearBlockIndex();
4756
4757 const int height = 9090;
4758 const int64 amount = 3 * COIN;
4759 uint256 hashA = 0, hashB = 0;
4760 hashA.SetHex("00000000000000000000000000000000000000000000000000000000000000aa");
4761 hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000bb");
4762
4763 check(LegacyPayoutComment(height) ==
4764 strprintf("Pool payout height %d", height),
4765 "the legacy needle is frozen at the format pre-upgrade builds"
4766 " wrote");
4767 check(PayoutComment(height, 0) == LegacyPayoutComment(height),
4768 "a grandfathered zero-hash record still writes the legacy form");
4769 check(PayoutComment(height, hashA) ==
4770 strprintf("Pool payout height %d block %s", height,
4771 hashA.ToString().c_str()),
4772 "a hash-keyed record's comment names its funding block");
4773 check(PayoutComment(height, hashA) != PayoutComment(height, hashB),
4774 "twin rounds at one height carry distinguishable comments");
4775
4776 // A transaction wearing the NEW comment format and -- hypothetically
4777 // -- no structured metadata must not be matched by the legacy needle:
4778 // the fallback exists for pre-upgrade transactions, and the new format
4779 // proves this is not one.
4780 const std::string idA = PayoutIdForBlock(height, hashA);
4781 const std::string idB = PayoutIdForBlock(height, hashB);
4782 std::set<std::string> none;
4783 std::string found;
4784 // Neither twin has a round proof here (section 15 covers the gate),
4785 // and their ids collide, so one answer serves both.
4786 const bool fAllowLegacy = !PoolRoundHasProof(idA);
4787 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount, 1, "");
4788 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found)
4789 == PAYOUT_PROBE_NONE,
4790 "a new-format comment is never legacy evidence for a twin round");
4791 check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found)
4792 == PAYOUT_PROBE_NONE,
4793 "nor a match channel for its own round -- structured metadata is");
4794
4795 // Round A's own payment, sent by this build, is found by its
4796 // structured id.
4797 CRITICAL_BLOCK(cs_mapWallet)
4798 mapWallet.clear();
4799 PayoutTestClearBlockIndex();
4800 const std::string paidA =
4801 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount,
4802 2, idA);
4803 check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found)
4804 == PAYOUT_PROBE_STRUCTURED && found == paidA,
4805 "the winning twin's own payment is found by its structured id");
4806
4807 // The id is derived from the FIRST sixteen hex digits of the hash --
4808 // the end proof-of-work fills with zeros -- so twin hashes that agree
4809 // there collide in id, and a payment carrying no pool_payout_block
4810 // answers for either twin. That is the accepted trade for payments
4811 // sent before the key existed, pinned here so it stays a decision.
4812 check(idA == idB,
4813 "twin hashes differing only past the sixteenth hex digit collide"
4814 " in id -- the reason the full hash must decide");
4815 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found)
4816 == PAYOUT_PROBE_STRUCTURED,
4817 "a payment without the block key answers for a colliding twin --"
4818 " the accepted residual for pre-key transactions");
4819
4820 // With the full hash recorded -- as every send now records it -- the
4821 // twins are told apart and the re-won height is paid.
4822 CRITICAL_BLOCK(cs_mapWallet)
4823 mapWallet.clear();
4824 PayoutTestClearBlockIndex();
4825 const std::string keyedA =
4826 PayoutTestAddWalletTx(PayoutComment(height, hashA), addrA, amount,
4827 3, idA, true, hashA.ToString());
4828 check(FindRecordedPoolPayout(idA, height, hashA, addrA, amount, fAllowLegacy, none, found)
4829 == PAYOUT_PROBE_STRUCTURED && found == keyedA,
4830 "a payment carrying the full block hash settles its own round");
4831 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount, fAllowLegacy, none, found)
4832 == PAYOUT_PROBE_NONE,
4833 "and is no evidence for the twin, so the re-won height is paid");
4834
4835 CRITICAL_BLOCK(cs_mapWallet)
4836 mapWallet.clear();
4837 PayoutTestClearBlockIndex();
4838 }
4839
4840 // -- 15. A proof-bearing round rejects comment-only evidence -------------
4841 //
4842 // The surviving half of the same-height collision: a pre-upgrade
4843 // transaction carries only "Pool payout height H", so if H is won again
4844 // after a reorg, it matches the new round's (height, address, amount)
4845 // exactly and would settle a debt it never paid. The new round has a
4846 // round proof -- written at creation, and only by builds whose payments
4847 // all name their round -- so for it, comment-only evidence is
4848 // inadmissible and the miner is paid. A round without a proof keeps
4849 // today's bias: the evidence is admissible and doubt resolves to
4850 // "already paid".
4851 {
4852 PayoutTestResetState();
4853 CRITICAL_BLOCK(cs_mapWallet)
4854 mapWallet.clear();
4855 PayoutTestClearBlockIndex();
4856 const std::string roundsPath = dir + "/pool_rounds_gate_selftest.json";
4857 const std::string savedRoundsFile = strPoolRoundsFile;
4858 strPoolRoundsFile = roundsPath;
4859 remove(roundsPath.c_str());
4860
4861 const int height = 7171;
4862 const int64 amount = 4 * COIN;
4863 uint256 hashB = 0;
4864 hashB.SetHex("00000000000000000000000000000000000000000000000000000000000000cc");
4865 const std::string idB = PayoutIdForBlock(height, hashB);
4866 std::set<std::string> none;
4867 std::string found;
4868
4869 // The colliding pre-upgrade payment: confirmed, comment-only, same
4870 // height, same address, same amount -- the strongest wrong evidence
4871 // there is.
4872 PayoutTestAddWalletTx(LegacyPayoutComment(height), addrA, amount, 1, "");
4873
4874 check(!PoolRoundHasProof(idB),
4875 "a round with no recorded proof does not claim one");
4876 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount,
4877 !PoolRoundHasProof(idB), none, found)
4878 == PAYOUT_PROBE_LEGACY,
4879 "without a proof the doubt is genuine and still resolves to"
4880 " already-paid");
4881
4882 PoolRoundProof round;
4883 round.id = idB;
4884 round.blockHeight = height;
4885 round.matureAtHeight = height + COINBASE_SPENDABLE_DEPTH;
4886 round.payouts[addrA] = amount;
4887 RecordPoolRound(round);
4888 check(PoolRoundHasProof(idB),
4889 "recording the round proof marks the round as this lineage's");
4890 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount,
4891 !PoolRoundHasProof(idB), none, found)
4892 == PAYOUT_PROBE_NONE,
4893 "with a proof, a same-height comment-only transaction is not"
4894 " evidence, and the re-won height is paid");
4895
4896 // Structured evidence still is: the round's own payment, recovered.
4897 const std::string paidB =
4898 PayoutTestAddWalletTx(PayoutComment(height, hashB), addrA, amount,
4899 2, idB, true, hashB.ToString());
4900 check(FindRecordedPoolPayout(idB, height, hashB, addrA, amount,
4901 !PoolRoundHasProof(idB), none, found)
4902 == PAYOUT_PROBE_STRUCTURED && found == paidB,
4903 "the gate does not blind the probe to the round's own structured"
4904 " payment");
4905
4906 CRITICAL_BLOCK(cs_mapWallet)
4907 mapWallet.clear();
4908 PayoutTestClearBlockIndex();
4909 remove(roundsPath.c_str());
4910 remove((roundsPath + ".tmp").c_str());
4911 strPoolRoundsFile = savedRoundsFile;
4912 }
4913
4914 // -- 16. A malformed block hash is corruption, not a hash ----------------
4915 //
4916 // SetHex() parses whatever hex prefix it finds and stops, so a corrupt
4917 // blockHash did not fail to load -- it loaded as a DIFFERENT hash.
4918 // "deadbeef..." became 0xdeadbeef, a hash no block ever had, which the
4919 // orphan check reads as an orphaned block and the next flush VOIDS the
4920 // debt; a value with no hex prefix became 0, which is grandfathered past
4921 // the orphan check instead. Either way a corrupt file silently changed
4922 // what the pool owed. It is a load failure now, handled like every other
4923 // parse error: quarantine, and never rewrite.
4924 {
4925 PayoutTestResetState();
4926 remove(ledgerPath.c_str());
4927 remove(payingPath.c_str());
4928
4929 const std::string badLedger = strprintf(
4930 "[{\"amounts\":{\"%s\":100000000},"
4931 "\"blockHash\":\"deadbeef oops, not a hash\","
4932 "\"blockHeight\":10,\"matureAtHeight\":10}]", addrA.c_str());
4933 check(PayoutTestWriteFile(ledgerPath, badLedger),
4934 "wrote a ledger whose blockHash is not a 64-digit hex string");
4935 LoadPendingPayouts();
4936 {
4937 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
4938 check(gPendingPayouts.empty(),
4939 "no entry is loaded with a guessed hash");
4940 }
4941 std::string gone;
4942 check(!PayoutTestReadFile(ledgerPath, gone),
4943 "the ledger with the corrupt hash is quarantined");
4944
4945 const std::string badPaying = strprintf(
4946 "[{\"address\":\"%s\",\"amount\":100000000,"
4947 "\"blockHash\":\"00000000cafe\",\"blockHeight\":10,"
4948 "\"matureAtHeight\":10}]", addrA.c_str());
4949 check(PayoutTestWriteFile(payingPath, badPaying),
4950 "wrote an in-flight list whose blockHash is truncated");
4951 LoadPaying();
4952 {
4953 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
4954 check(gPaying.empty(),
4955 "no in-flight record is loaded with a guessed hash");
4956 }
4957 check(!PayoutTestReadFile(payingPath, gone),
4958 "the in-flight list with the corrupt hash is quarantined");
4959
4960 // The two spellings every real file uses still load: the 64-digit
4961 // hash of a keyed round, and the 64 zeros this build writes for a
4962 // grandfathered entry.
4963 PayoutTestResetState();
4964 remove(ledgerPath.c_str());
4965 const std::string goodLedger = strprintf(
4966 "[{\"amounts\":{\"%s\":100000000},"
4967 "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\","
4968 "\"blockHeight\":10,\"matureAtHeight\":10},"
4969 "{\"amounts\":{\"%s\":200000000},"
4970 "\"blockHash\":\"%s\","
4971 "\"blockHeight\":20,\"matureAtHeight\":20}]",
4972 addrA.c_str(), addrB.c_str(), uint256(0).ToString().c_str());
4973 check(PayoutTestWriteFile(ledgerPath, goodLedger),
4974 "wrote a ledger in both real hash spellings");
4975 LoadPendingPayouts();
4976 {
4977 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
4978 check(gPendingPayouts.size() == 2 &&
4979 gPendingPayouts[0].blockHash != 0 &&
4980 gPendingPayouts[1].blockHash == 0,
4981 "a real hash and a serialised zero hash both still load");
4982 }
4983 remove(ledgerPath.c_str());
4984 }
4985
4422 // -- clean up ------------------------------------------------------------ 4986 // -- clean up ------------------------------------------------------------
4423 PayoutTestResetState(); 4987 PayoutTestResetState();
4424 CRITICAL_BLOCK(cs_mapWallet) 4988 CRITICAL_BLOCK(cs_mapWallet)
@@ -4579,6 +5143,82 @@ int RunPoolStratumSelfTest()
4579 strPoolRoundsFile = savedRoundsFile; 5143 strPoolRoundsFile = savedRoundsFile;
4580 gPoolRounds = savedRounds; 5144 gPoolRounds = savedRounds;
4581 5145
5146 // -- hashrate decay (#7) --------------------------------------------------
5147 // The first version of this decayed m->hashRate in place from the event
5148 // loop's sweep, which made the reported figure a function of how the loop
5149 // happened to run rather than of how long the miner had been quiet. These
5150 // pin the properties that fixes: sampling changes nothing, and the answer
5151 // depends only on elapsed silence.
5152 {
5153 // A miner exactly on the vardiff target: 4096 hashes per share at
5154 // difficulty 1.0, one share per 15s, so 4096/15 H/s. Its expected
5155 // interval is 15s and its grace is 60s.
5156 const double base = 4096.0 / 15.0;
5157 const int64 t0 = 1766000000; // any plausible non-zero wall clock
5158 Miner decayed(INVALID_SOCKET);
5159 decayed.difficulty = 1.0;
5160 decayed.hashRate = base;
5161 decayed.lastShareTime = t0;
5162
5163 check(MinerReportedHashRate(&decayed, t0 + 60) == base,
5164 "hashrate decay: a miner inside its grace period reports its full estimate");
5165
5166 // Sampling must not consume the estimate. Twenty passes is one second
5167 // of the event loop's old sweep; a thousand is nearly a minute of it.
5168 double once = MinerReportedHashRate(&decayed, t0 + 61);
5169 bool idempotent = (decayed.hashRate == base);
5170 for (int i = 0; i < 1000 && idempotent; i++)
5171 idempotent = (MinerReportedHashRate(&decayed, t0 + 61) == once) &&
5172 (decayed.hashRate == base);
5173 check(idempotent,
5174 "hashrate decay: repeated sampling returns one value and leaves the estimate alone");
5175
5176 // One second past a 60s grace is 1/15th of a half-life: ~4.5% off, not
5177 // the estimate gone. A miner at the vardiff target overshoots its grace
5178 // roughly 1.8% of the time (e^-4) purely by share-timing variance, and
5179 // must not be zeroed for it.
5180 check(once > 0.94 * base && once < 0.96 * base,
5181 "hashrate decay: one second past grace costs a few percent, not the estimate");
5182 double three = MinerReportedHashRate(&decayed, t0 + 63);
5183 check(three > 0.86 * base && three < 0.88 * base,
5184 "hashrate decay: three seconds past grace costs ~13%");
5185
5186 // One half-life past the grace is exactly half.
5187 double half = MinerReportedHashRate(&decayed, t0 + 75);
5188 check(half > 0.499 * base && half < 0.501 * base,
5189 "hashrate decay: one expected interval past grace halves the estimate");
5190
5191 // A miner that has genuinely stopped ends up reported as nothing --
5192 // the whole point of #7. The old in-place version could not get here:
5193 // its decay fed back into its own grace and settled into a 1/t crawl.
5194 check(MinerReportedHashRate(&decayed, t0 + 3600) == 0.0,
5195 "hashrate decay: a miner silent for an hour is reported as zero");
5196
5197 // A clock that steps backwards must never inflate what we announce.
5198 check(MinerReportedHashRate(&decayed, t0 - 3600) == base,
5199 "hashrate decay: a backwards clock reports the estimate unchanged, never higher");
5200
5201 // Fresh connection, and a connection with one share and so no interval
5202 // to have timed: both must report nothing rather than divide by zero.
5203 Miner fresh(INVALID_SOCKET);
5204 check(MinerReportedHashRate(&fresh, t0) == 0.0,
5205 "hashrate decay: a miner that never submitted a share reports zero");
5206 Miner single(INVALID_SOCKET);
5207 single.lastShareTime = t0;
5208 check(MinerReportedHashRate(&single, t0 + 86400) == 0.0,
5209 "hashrate decay: a miner with no rate sample yet reports zero");
5210
5211 // Nothing in the swept range may produce a NaN, an infinity, a negative
5212 // rate, or a rate above the estimate it started from.
5213 bool finite = true;
5214 for (int64 s = -100; s < 200000 && finite; s += 37) {
5215 double v = MinerReportedHashRate(&decayed, t0 + s);
5216 finite = !std::isnan(v) && !std::isinf(v) && v >= 0.0 && v <= base;
5217 }
5218 check(finite,
5219 "hashrate decay: stays finite, non-negative and non-amplifying across a long silence");
5220 }
5221
4582 gCurrentJob = savedJob; 5222 gCurrentJob = savedJob;
4583 gHaveJob = savedHaveJob; 5223 gHaveJob = savedHaveJob;
4584 5224
@@ -4606,6 +5246,10 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4606 if (m->authorised) gStats.authorizedMiners++; 5246 if (m->authorised) gStats.authorizedMiners++;
4607 5247
4608 gWorkerStats.clear(); 5248 gWorkerStats.clear();
5249 // One timestamp for the whole sweep (#7): every miner is faded to the
5250 // same instant, so the per-worker rows and the total they are summed
5251 // into describe one moment rather than a smear across the loop.
5252 int64 nowStats = GetTime();
4609 for (Miner* m : gMiners) { 5253 for (Miner* m : gMiners) {
4610 if (!m->authorised || m->lastSeen <= 0) continue; 5254 if (!m->authorised || m->lastSeen <= 0) continue;
4611 PoolWorkerStatView v; 5255 PoolWorkerStatView v;
@@ -4614,9 +5258,11 @@ static void RefreshStats(int blocksFoundThisSession, uint64 roundShareTotal)
4614 v.roundShares = m->roundShares; 5258 v.roundShares = m->roundShares;
4615 v.totalShares = m->totalShares; 5259 v.totalShares = m->totalShares;
4616 v.lastSeen = m->lastSeen; 5260 v.lastSeen = m->lastSeen;
4617 v.hashRate = m->hashRate; 5261 // Not m->hashRate: a miner that has stopped submitting shares must
5262 // not keep contributing its last sample to the announced total.
5263 v.hashRate = MinerReportedHashRate(m, nowStats);
4618 gWorkerStats.push_back(v); 5264 gWorkerStats.push_back(v);
4619 gStats.totalHashRate += m->hashRate; 5265 gStats.totalHashRate += v.hashRate;
4620 } 5266 }
4621 std::sort(gWorkerStats.begin(), gWorkerStats.end(), 5267 std::sort(gWorkerStats.begin(), gWorkerStats.end(),
4622 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) { 5268 [](const PoolWorkerStatView& a, const PoolWorkerStatView& b) {
@@ -4849,6 +5495,35 @@ static void PoolEventLoop()
4849 } 5495 }
4850 } 5496 }
4851 5497
5498 // 3.6. Idle reap (#6). Mirror the clean-disconnect path exactly --
5499 // log, close, erase from gMiners via std::find, delete -- just with
5500 // a different reason. Shares the miner already earned stay in
5501 // roundShareCount, exactly as they do when a miner cleanly
5502 // disconnects mid-round. connectTime stands in for lastSeen on a
5503 // connection that never completed a line. Erasing inside the loop
5504 // above would invalidate the iteration, so collect then reap.
5505 {
5506 int64 now = GetTime();
5507 std::vector<Miner*> idle;
5508 for (Miner* m : gMiners) {
5509 int64 last = (m->lastSeen > 0) ? m->lastSeen : m->connectTime;
5510 if (now - last > POOL_IDLE_TIMEOUT_SECS)
5511 idle.push_back(m);
5512 }
5513 for (Miner* m : idle) {
5514 LogPrint("worker", "[pool] dropping idle miner %s -- nothing"
5515 " received in over %llds (session shares: %llu,"
5516 " round shares: %llu)\n",
5517 m->worker.empty() ? "(never authorized)" : m->worker.c_str(),
5518 (long long)POOL_IDLE_TIMEOUT_SECS,
5519 (unsigned long long)m->sessionShares,
5520 (unsigned long long)m->roundShares);
5521 sock_close(m->fd);
5522 gMiners.erase(std::find(gMiners.begin(), gMiners.end(), m));
5523 delete m;
5524 }
5525 }
5526
4852 // 4. Select over miner sockets 5527 // 4. Select over miner sockets
4853 if (!gMiners.empty()) { 5528 if (!gMiners.empty()) {
4854 fd_set fds; FD_ZERO(&fds); 5529 fd_set fds; FD_ZERO(&fds);
@@ -4871,6 +5546,11 @@ static void PoolEventLoop()
4871 // terminate the whole node -- not just the pool. Every 5546 // terminate the whole node -- not just the pool. Every
4872 // line arrives from an unauthenticated socket. 5547 // line arrives from an unauthenticated socket.
4873 while (RecvLine(m->fd, m->readBuf, line, disc)) { 5548 while (RecvLine(m->fd, m->readBuf, line, disc)) {
5549 // Any complete line is proof of life (#6): authorize
5550 // and valid shares already stamp lastSeen themselves,
5551 // but a miner that only ever subscribes (or sends
5552 // unrecognised methods) must count as alive too.
5553 m->lastSeen = GetTime();
4874 try { 5554 try {
4875 HandleLine(m, line, roundShareCount, 5555 HandleLine(m, line, roundShareCount,
4876 roundShareTotal, blocksFoundThisSession); 5556 roundShareTotal, blocksFoundThisSession);
@@ -4947,13 +5627,19 @@ static void AcceptOneFn(void* arg)
4947 std::string host = relay.substr(0, colon); 5627 std::string host = relay.substr(0, colon);
4948 int port = atoi(relay.substr(colon + 1).c_str()); 5628 int port = atoi(relay.substr(colon + 1).c_str());
4949 5629
4950 LogPrint("pool", "[pool] accept thread started for relay %s\n", relay.c_str()); 5630 LogPrint("pool", "[pool] accept thread started for relay %s%s\n", relay.c_str(),
5631 fBtfRelayPinned ? " (/rvrelay pin)" : "");
5632
5633 // Registration health for this relay. A pin has no fallback, so a pin that
5634 // never registers means the pool is simply unreachable -- and that has to
5635 // be loud. The 2026-07-30 incident went unnoticed because this loop logged
5636 // failures at one flat line and success not at all, so "which relay is this
5637 // pool actually registered at" could not be answered from the log.
5638 int nConsecFail = 0;
5639 int64 nFirstFail = 0;
5640 bool fEverRegistered = false;
4951 5641
4952 while (!fShutdown && gPoolRunning) { 5642 while (!fShutdown && gPoolRunning) {
4953 // Advertise this relay as the pool's meeting node BEFORE registering,
4954 // so that miners which resolve our descriptor find a relay where the
4955 // pool (not the node's peer-accept) holds the registration.
4956 BtfSetActiveRelay(relay);
4957 // Returns as soon as the relay lists us -- registered, not yet paired. 5643 // Returns as soon as the relay lists us -- registered, not yet paired.
4958 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk); 5644 btf::RvSocket rv = btf::RvServiceRegister(host.c_str(), (unsigned short)port, pk);
4959 5645
@@ -4962,12 +5648,36 @@ static void AcceptOneFn(void* arg)
4962 break; 5648 break;
4963 } 5649 }
4964 if (rv == btf::RV_INVALID) { 5650 if (rv == btf::RV_INVALID) {
4965 LogPrint("pool", "[pool] relay %s: RvServiceRegister failed" 5651 nConsecFail++;
4966 " -- backing off 30s before retry\n", relay.c_str()); 5652 if (nFirstFail == 0) nFirstFail = GetTime();
5653 LogPrint("pool", "[pool] relay %s: registration failed (attempt %d,"
5654 " failing for %llds) -- backing off 30s before retry\n",
5655 relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail));
5656 if (fBtfRelayPinned && (nConsecFail == 3 || nConsecFail % 10 == 0))
5657 LogPrint("pool", "[pool] WARNING: pinned relay %s refused registration"
5658 " %d times running (%llds); /rvrelay leaves no fallback, so"
5659 " miners cannot reach this pool%s\n",
5660 relay.c_str(), nConsecFail, (long long)(GetTime() - nFirstFail),
5661 fEverRegistered ? "" : " -- it has never registered once");
4967 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000); 5662 for (int i = 0; i < 30 && !fShutdown && gPoolRunning; i++) Sleep(1000);
4968 continue; 5663 continue;
4969 } 5664 }
4970 5665
5666 // Advertise THIS relay only now. Setting it before the call published a
5667 // meeting node we were not yet listed at, so a miner resolving the
5668 // descriptor inside that window burned its dial on a relay holding no
5669 // registration for us. net.cpp's peer-accept has always ordered it this
5670 // way round; the pool had not.
5671 BtfSetActiveRelay(relay);
5672 if (!fEverRegistered || nConsecFail > 0)
5673 LogPrint("pool", "[pool] registered at relay %s%s%s -- advertising it as"
5674 " the pool's meeting node\n", relay.c_str(),
5675 fBtfRelayPinned ? " (/rvrelay pin)" : "",
5676 nConsecFail > 0 ? ", recovered" : "");
5677 fEverRegistered = true;
5678 nConsecFail = 0;
5679 nFirstFail = 0;
5680
4971 // Block for a miner to actually arrive. RvServiceRegister used to do 5681 // Block for a miner to actually arrive. RvServiceRegister used to do
4972 // this itself and returned with the relay's 0x01 pairing byte already 5682 // this itself and returned with the relay's 0x01 pairing byte already
4973 // consumed; it no longer does. Without waiting here, BtfServiceWrap 5683 // consumed; it no longer does. Without waiting here, BtfServiceWrap
@@ -5022,9 +5732,21 @@ static void AcceptLoopFn(void*)
5022 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2); 5732 LogPrint("pool", "[pool] .btf identity ready after %ds\n", waitSecs * 2);
5023 5733
5024 std::set<std::string> spawned; // relays we've already started a thread for 5734 std::set<std::string> spawned; // relays we've already started a thread for
5735 std::set<std::string> skipped; // discovered relays the pin excluded, logged once each
5025 5736
5026 while (!fShutdown && gPoolRunning) { 5737 while (!fShutdown && gPoolRunning) {
5738 // BtfAllRelays() already honours the pin: with /rvrelay set it returns
5739 // vBtfMeetingRelays alone, so no thread is ever spawned for a
5740 // discovered relay. Name the ones it drops -- a pool registering at a
5741 // stranger's relay is exactly the thing that should not be silent.
5027 vector<string> relays = BtfAllRelays(); 5742 vector<string> relays = BtfAllRelays();
5743 if (fBtfRelayPinned) {
5744 for (const string& r : BtfDiscoveredRelays())
5745 if (std::find(relays.begin(), relays.end(), r) == relays.end() &&
5746 skipped.insert(r).second)
5747 LogPrint("pool", "[pool] not registering at discovered relay %s"
5748 " -- /rvrelay pins the relay list\n", r.c_str());
5749 }
5028 if (relays.empty()) { 5750 if (relays.empty()) {
5029 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n"); 5751 LogPrint("pool", "[pool] no relays available -- miners cannot connect yet\n");
5030 } else { 5752 } else {
@@ -5040,8 +5762,9 @@ static void AcceptLoopFn(void*)
5040 relay.c_str()); 5762 relay.c_str());
5041 } else { 5763 } else {
5042 spawned.insert(relay); 5764 spawned.insert(relay);
5043 LogPrint("pool", "[pool] accept thread spawned for relay %s" 5765 LogPrint("pool", "[pool] accept thread spawned for relay %s%s"
5044 " (%zu total)\n", relay.c_str(), spawned.size()); 5766 " (%zu total)\n", relay.c_str(),
5767 fBtfRelayPinned ? " (/rvrelay pin)" : "", spawned.size());
5045 } 5768 }
5046 } 5769 }
5047 } 5770 }
@@ -5077,10 +5800,17 @@ void ThreadRPCServer(void*)
5077 // armed for its one wallet probe before it is allowed to send. 5800 // armed for its one wallet probe before it is allowed to send.
5078 ReconcilePayouts(); 5801 ReconcilePayouts();
5079 5802
5080 _beginthread(AcceptLoopFn, 0, NULL); 5803 // _beginthread cannot throw (it reports -1), but an unchecked failure
5081 _beginthread(LocalAcceptFn, 0, NULL); 5804 // leaves the pool running deaf: nobody accepts, announces, or pays, and
5082 _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); 5805 // nothing in the log says why.
5083 _beginthread(PayoutThreadFn, 0, NULL); 5806 if (_beginthread(AcceptLoopFn, 0, NULL) == (uintptr_t)-1)
5807 LogPrint("pool", "[pool] FATAL: accept loop thread failed to spawn -- miners cannot connect\n");
5808 if (_beginthread(LocalAcceptFn, 0, NULL) == (uintptr_t)-1)
5809 LogPrint("pool", "[pool] local accept thread failed to spawn\n");
5810 if (_beginthread(ThreadBtfPoolAnnouncer, 0, NULL) == (uintptr_t)-1)
5811 LogPrint("pool", "[pool] announcer thread failed to spawn -- pool will not be advertised\n");
5812 if (_beginthread(PayoutThreadFn, 0, NULL) == (uintptr_t)-1)
5813 LogPrint("pool", "[pool] FATAL: payout thread failed to spawn -- no one gets paid\n");
5084 5814
5085 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown 5815 PoolEventLoop(); // blocks until gPoolRunning = false or fShutdown
5086 5816