diff options
Diffstat (limited to 'src/rpc.cpp')
| -rw-r--r-- | src/rpc.cpp | 852 |
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. | ||
| 849 | static 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 | |||
| 779 | static void LoadPendingPayouts() | 861 | static 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 | ||
| 1189 | static 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. | ||
| 1278 | static 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. | ||
| 1294 | static 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. | ||
| 1465 | static 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 | ||
| 1414 | static PayoutProbeResult FindRecordedPoolPayout(const std::string& id, | 1556 | static 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. |
| 1932 | static void ForfeitPayoutRecipient(const PayingRecord& r, const std::string& reason) | 2093 | static 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; | |||
| 2471 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance | 2643 | static const int64 VARDIFF_STALL_SECS = 45; // 3x share target -- reacts faster without false-triggering on normal variance |
| 2472 | static const double VARDIFF_STALL_CUT = 4.0; // divide difficulty by this much | 2644 | static 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. | ||
| 2662 | static const double HASHRATE_IDLE_MISSES = 4.0; // grace, in expected share intervals | ||
| 2663 | static const double HASHRATE_MIN_INTERVAL = 1.0; // seconds; floor for the expected interval | ||
| 2664 | static 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. | ||
| 2669 | static const double HASHRATE_DECAY_CUTOFF = 64.0; | ||
| 2670 | |||
| 2474 | struct Miner { | 2671 | struct 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. | ||
| 2746 | static 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 | |||
| 2506 | static std::vector<Miner*> gMiners; | 2772 | static std::vector<Miner*> gMiners; |
| 2507 | 2773 | ||
| 2508 | struct FoundBlock { int height; int64 ts; std::string addr; }; | 2774 | struct 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 | |||
| 2596 | static const int64 POOL_UNAUTH_GRACE_SECS = 60; // a connection still not | 2874 | static 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. | ||
| 2886 | static 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, | |||
| 2680 | static void SendJob(Miner* m, bool clean) | 2968 | static 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 | ||