diff options
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 521 |
1 files changed, 500 insertions, 21 deletions
diff --git a/src/main.cpp b/src/main.cpp index 7b3e555..6b8e0de 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | #include <atomic> | 14 | #include <atomic> |
| 15 | #include <mutex> | 15 | #include <mutex> |
| 16 | #include <thread> | 16 | #include <thread> |
| 17 | #include <system_error> | ||
| 17 | #include "bip32.h" | 18 | #include "bip32.h" |
| 18 | #include "btfaddr.h" | 19 | #include "btfaddr.h" |
| 19 | #include "btftunnel.h" | 20 | #include "btftunnel.h" |
| @@ -64,6 +65,137 @@ CBlockIndex* pindexBest = NULL; | |||
| 64 | map<uint256, CBlock*> mapOrphanBlocks; | 65 | map<uint256, CBlock*> mapOrphanBlocks; |
| 65 | multimap<uint256, CBlock*> mapOrphanBlocksByPrev; | 66 | multimap<uint256, CBlock*> mapOrphanBlocksByPrev; |
| 66 | 67 | ||
| 68 | // Block bodies we have sent a getdata for and not yet received, so the | ||
| 69 | // failover pass in SendMessages can tell "requested and being served" from | ||
| 70 | // "requested and gone quiet" and re-issue the request to a different peer | ||
| 71 | // (#33). Keyed by block hash; setAsked is the peers already tried since the | ||
| 72 | // round last restarted, so the retry rotates instead of hammering the | ||
| 73 | // announcer that is not serving. Guarded by cs_main, which both the message | ||
| 74 | // handlers and SendMessages already hold. | ||
| 75 | // | ||
| 76 | // Capped small on purpose. At the tip there is one block in flight; during | ||
| 77 | // initial download there are legitimately hundreds outstanding to one peer, | ||
| 78 | // and rotating those would duplicate the whole download to every other peer. | ||
| 79 | // | ||
| 80 | // The cap is a shared resource and nothing on the path from inv to insertion | ||
| 81 | // can tell a real block hash from 32 random bytes -- AlreadyHave only asks | ||
| 82 | // whether we already hold the block, and there is no header to check work | ||
| 83 | // against. So the table does not refuse a new hash when it is full; it makes | ||
| 84 | // room. See MakeRoomInBlockFetch for the rule, which is what stops one peer | ||
| 85 | // filling all eight slots and switching failover off for everybody. | ||
| 86 | // | ||
| 87 | // The initialisers matter: the insertion path uses operator[] and then tests | ||
| 88 | // nTimeFirstAsked == 0 to detect a fresh entry. That worked on | ||
| 89 | // value-initialisation alone, but only for as long as nobody gave this struct | ||
| 90 | // a constructor. | ||
| 91 | struct CBlockFetch | ||
| 92 | { | ||
| 93 | int64 nTimeFirstAsked = 0; // when the first getdata for this block went out | ||
| 94 | int64 nTimeAsked = 0; // when the latest getdata went out | ||
| 95 | CAddress addrAnnounced; // peer whose announcement put this hash here | ||
| 96 | set<CAddress> setAsked; // peers tried since the round last restarted | ||
| 97 | }; | ||
| 98 | map<uint256, CBlockFetch> mapBlockFetch; | ||
| 99 | static const int BLOCK_FETCH_TRACK_MAX = 8; | ||
| 100 | // A block nobody has served for this long is not a fetch problem: the hash may | ||
| 101 | // have been announced and never existed. Drop the entry rather than getdata | ||
| 102 | // forever; a re-announcement recreates it. | ||
| 103 | static const int BLOCK_FETCH_GIVEUP_SECS = 10 * 60; | ||
| 104 | |||
| 105 | // Failover firing at all is worth saying out loud: it means peers are | ||
| 106 | // announcing blocks they then do not serve, which is the pathology of #33 and | ||
| 107 | // the thing nobody could otherwise see. Naming the peer on every re-request is | ||
| 108 | // a different matter. A peer can drive that line as often as it likes by | ||
| 109 | // announcing hashes it will not serve, and what it prints is a peer address, | ||
| 110 | // which is the one thing the .btf transport otherwise takes trouble not to | ||
| 111 | // spread around. So the per-peer detail sits behind the net category and the | ||
| 112 | // unconditional line is a count over a window no peer can shorten: the first | ||
| 113 | // failover after a quiet spell prints at once, because the change from "blocks | ||
| 114 | // arrive" to "blocks are announced and not served" is the event worth seeing, | ||
| 115 | // and everything after it is summarised once a minute. | ||
| 116 | static const int BLOCK_FETCH_REPORT_SECS = 60; | ||
| 117 | static int64 nBlockFetchWindowStart = 0; | ||
| 118 | static int nBlockFetchFailovers = 0; | ||
| 119 | |||
| 120 | // How stale our own best block may be before we stop calling this the tip. | ||
| 121 | // Blocks target two minutes, so this is thirty intervals: far longer than any | ||
| 122 | // stall worth failing over from -- the one in #33 ran sixteen minutes -- and | ||
| 123 | // far shorter than catching up, which starts hours or days back. | ||
| 124 | static const int BLOCK_FETCH_TIP_AGE_SECS = 60 * 60; | ||
| 125 | |||
| 126 | // Whether this node is tracking the tip rather than still catching up. | ||
| 127 | // | ||
| 128 | // Measured off the age of our own best block, not off any height a peer told | ||
| 129 | // us. nStartingHeight is optional in the version message and every node | ||
| 130 | // released so far sends the short form, so it is -1 for essentially every | ||
| 131 | // peer; and where it is filled in later it is filled in from the height of a | ||
| 132 | // block that peer served us, which during catch-up tracks our own height and | ||
| 133 | // would report "at the tip" for the whole download. Block timestamps are the | ||
| 134 | // one clock here that peers cannot move: CheckBlock refuses anything more than | ||
| 135 | // two hours ahead and AcceptBlock refuses anything at or before the previous | ||
| 136 | // median, so the only way to make this read "at the tip" falsely is to mine, | ||
| 137 | // and all that buys is the 15s spacing this code had unconditionally anyway. | ||
| 138 | bool IsBlockFetchNearTip() | ||
| 139 | { | ||
| 140 | return pindexBest && (int64)pindexBest->nTime > GetTime() - BLOCK_FETCH_TIP_AGE_SECS; | ||
| 141 | } | ||
| 142 | |||
| 143 | // Drop one entry so a hash announced by addrNew can be tracked. | ||
| 144 | // | ||
| 145 | // Refusing the insertion instead was a remote off-switch for the whole | ||
| 146 | // feature. An inv carrying eight hashes that never existed filled every slot | ||
| 147 | // -- nothing between inv and here can tell those from real blocks -- and no | ||
| 148 | // genuinely new block was tracked again until the give-up timer expired ten | ||
| 149 | // minutes later, re-armable for one message per ten minutes from a single | ||
| 150 | // connection. | ||
| 151 | // | ||
| 152 | // So evict instead, by occupancy: a peer may only take a slot from a peer | ||
| 153 | // holding strictly more slots than itself, and otherwise recycles one of its | ||
| 154 | // own. That is what keeps eviction from becoming the same attack in a new | ||
| 155 | // shape. A peer that has filled the table is by definition the heaviest | ||
| 156 | // holder, so its own entries are the only ones it can displace, while a peer | ||
| 157 | // announcing its first block holds nothing and therefore always evicts from | ||
| 158 | // the heaviest -- a genuine announcement is never refused a slot, whatever the | ||
| 159 | // attacker holds or however many connections it holds it from. The one case | ||
| 160 | // that costs a real fetch is eight distinct peers holding one entry each, | ||
| 161 | // where a ninth takes the oldest; that is a busy healthy node, not an attack, | ||
| 162 | // and losing the oldest of eight beats refusing the ninth. | ||
| 163 | static void MakeRoomInBlockFetch(const CAddress& addrNew) | ||
| 164 | { | ||
| 165 | if (mapBlockFetch.empty()) | ||
| 166 | return; | ||
| 167 | |||
| 168 | map<CAddress, int> mapHeld; | ||
| 169 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end(); ++mi) | ||
| 170 | mapHeld[(*mi).second.addrAnnounced]++; | ||
| 171 | |||
| 172 | int nMine = mapHeld.count(addrNew) ? mapHeld[addrNew] : 0; | ||
| 173 | |||
| 174 | int nMax = 0; | ||
| 175 | CAddress addrVictim; | ||
| 176 | for (map<CAddress, int>::iterator mi = mapHeld.begin(); mi != mapHeld.end(); ++mi) | ||
| 177 | if ((*mi).second > nMax) | ||
| 178 | { | ||
| 179 | nMax = (*mi).second; | ||
| 180 | addrVictim = (*mi).first; | ||
| 181 | } | ||
| 182 | |||
| 183 | // Nobody is holding more than this peer, so it recycles its own oldest | ||
| 184 | // rather than taking a slot off an equal. | ||
| 185 | if (nMax <= nMine) | ||
| 186 | addrVictim = addrNew; | ||
| 187 | |||
| 188 | map<uint256, CBlockFetch>::iterator miVictim = mapBlockFetch.end(); | ||
| 189 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end(); ++mi) | ||
| 190 | if ((*mi).second.addrAnnounced == addrVictim && | ||
| 191 | (miVictim == mapBlockFetch.end() || | ||
| 192 | (*mi).second.nTimeFirstAsked < (*miVictim).second.nTimeFirstAsked)) | ||
| 193 | miVictim = mi; | ||
| 194 | |||
| 195 | if (miVictim != mapBlockFetch.end()) | ||
| 196 | mapBlockFetch.erase(miVictim); | ||
| 197 | } | ||
| 198 | |||
| 67 | map<uint256, CDataStream*> mapOrphanTransactions; | 199 | map<uint256, CDataStream*> mapOrphanTransactions; |
| 68 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; | 200 | multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; |
| 69 | 201 | ||
| @@ -1712,7 +1844,7 @@ void CWalletTx::RelayWalletTransaction(CTxDB& txdb) | |||
| 1712 | uint256 hash = GetHash(); | 1844 | uint256 hash = GetHash(); |
| 1713 | if (!txdb.ContainsTx(hash)) | 1845 | if (!txdb.ContainsTx(hash)) |
| 1714 | { | 1846 | { |
| 1715 | if (LogAcceptsCategory("net")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); | 1847 | if (LogAcceptsCategory("relay")) printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str()); |
| 1716 | RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); | 1848 | RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this); |
| 1717 | } | 1849 | } |
| 1718 | } | 1850 | } |
| @@ -2678,7 +2810,18 @@ bool LoadBlockIndex(bool fAllowNew) | |||
| 2678 | // CTxOut(nValue=50.00000000, scriptPubKey=0x5F1DF16B2B704C8A578D0B) | 2810 | // CTxOut(nValue=50.00000000, scriptPubKey=0x5F1DF16B2B704C8A578D0B) |
| 2679 | // vMerkleTree: 4a5e1e | 2811 | // vMerkleTree: 4a5e1e |
| 2680 | 2812 | ||
| 2681 | // Bitflash genesis block -- own header (fair launch, 2026 stable relaunch) | 2813 | // Bitflash genesis block -- own header (fair launch) |
| 2814 | // | ||
| 2815 | // Do not change pszTimestamp, the scriptPubKey, nTime or nNonce. They | ||
| 2816 | // are the inputs to the genesis hash, and the genesis hash is | ||
| 2817 | // consensus. Changing one on its own trips the assert below, which is | ||
| 2818 | // loud and reverts cleanly; changing one and regenerating | ||
| 2819 | // GENESIS_NONCE and hashGenesisBlock to match -- which is exactly what | ||
| 2820 | // the re-mining branch below prints out and invites -- gives a node | ||
| 2821 | // that starts without complaint, builds its own chain from height 0, | ||
| 2822 | // and rejects every real peer as being on a different chain. That one | ||
| 2823 | // fails silently, and the damage scales with how many people run the | ||
| 2824 | // "fixed" build. | ||
| 2682 | char* pszTimestamp = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"; | 2825 | char* pszTimestamp = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine"; |
| 2683 | CTransaction txNew; | 2826 | CTransaction txNew; |
| 2684 | txNew.vin.resize(1); | 2827 | txNew.vin.resize(1); |
| @@ -2691,7 +2834,14 @@ bool LoadBlockIndex(bool fAllowNew) | |||
| 2691 | block.hashPrevBlock = 0; | 2834 | block.hashPrevBlock = 0; |
| 2692 | block.hashMerkleRoot = block.BuildMerkleTree(); | 2835 | block.hashMerkleRoot = block.BuildMerkleTree(); |
| 2693 | block.nVersion = 1; | 2836 | block.nVersion = 1; |
| 2694 | block.nTime = 1753315200; // 2026-07-24 00:00:00 UTC (stable relaunch) | 2837 | // 1753315200 is 2025-07-24T00:00:00Z. The headline in pszTimestamp |
| 2838 | // above reads 2026 and does not match it. This is a known discrepancy | ||
| 2839 | // and not a typo in the constant: the value is only consensus-critical | ||
| 2840 | // as an input to the genesis hash, the block hashes to | ||
| 2841 | // hashGenesisBlock with it, and every node on the network agrees. The | ||
| 2842 | // mismatch is in the launched chain and cannot be corrected -- see the | ||
| 2843 | // warning above before deciding otherwise. | ||
| 2844 | block.nTime = 1753315200; | ||
| 2695 | block.nBits = 0x1f0fffff; // = bnProofOfWorkLimit (Bitflash RandomX floor) | 2845 | block.nBits = 0x1f0fffff; // = bnProofOfWorkLimit (Bitflash RandomX floor) |
| 2696 | block.nNonce = GENESIS_NONCE; | 2846 | block.nNonce = GENESIS_NONCE; |
| 2697 | 2847 | ||
| @@ -2718,6 +2868,11 @@ bool LoadBlockIndex(bool fAllowNew) | |||
| 2718 | printf("powhash = %s\n", block.GetPoWHash().ToString().c_str()); | 2868 | printf("powhash = %s\n", block.GetPoWHash().ToString().c_str()); |
| 2719 | printf("hash = %s\n", block.GetHash().ToString().c_str()); | 2869 | printf("hash = %s\n", block.GetHash().ToString().c_str()); |
| 2720 | printf("merkle = %s\n", block.hashMerkleRoot.ToString().c_str()); | 2870 | printf("merkle = %s\n", block.hashMerkleRoot.ToString().c_str()); |
| 2871 | printf("----------------------------------------\n"); | ||
| 2872 | printf("These constants bring up a NEW chain. Do not paste them\n"); | ||
| 2873 | printf("into a build for this network to make an edited nTime or\n"); | ||
| 2874 | printf("pszTimestamp fit -- a node whose genesis differs starts\n"); | ||
| 2875 | printf("cleanly on a chain of its own and rejects every real peer.\n"); | ||
| 2721 | printf("========================================\n"); | 2876 | printf("========================================\n"); |
| 2722 | } | 2877 | } |
| 2723 | 2878 | ||
| @@ -3263,15 +3418,61 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 3263 | vRecv >> *pblock; | 3418 | vRecv >> *pblock; |
| 3264 | 3419 | ||
| 3265 | //// debug print | 3420 | //// debug print |
| 3266 | if (LogAcceptsCategory("net")) { printf("received block:\n"); pblock->print(); } | 3421 | if (LogAcceptsCategory("relay")) { printf("received block:\n"); pblock->print(); } |
| 3267 | 3422 | ||
| 3268 | CInv inv(MSG_BLOCK, pblock->GetHash()); | 3423 | CInv inv(MSG_BLOCK, pblock->GetHash()); |
| 3269 | pfrom->AddInventoryKnown(inv); | 3424 | pfrom->AddInventoryKnown(inv); |
| 3270 | 3425 | ||
| 3426 | // GetHash() covers the 80-byte header and nothing else, so the hash | ||
| 3427 | // coming out right does not make this the block we asked for. Every | ||
| 3428 | // peer that saw X announced holds X's header, and can answer our | ||
| 3429 | // getdata with that header bolted onto a transaction list of its own | ||
| 3430 | // invention -- the reply still hashes to X. hashMerkleRoot is the only | ||
| 3431 | // thing binding the header to the body, so check it here, before the | ||
| 3432 | // mapBlockFetch entry is touched and before CheckBlock spends a | ||
| 3433 | // RandomX hash on a body already known to be forged. | ||
| 3434 | // | ||
| 3435 | // There is no honest way to arrive here. Peer traffic rides the .btf | ||
| 3436 | // channel, which is XChaCha20-Poly1305, so a body that arrives at all | ||
| 3437 | // arrived intact: a mismatch is the peer's own construction, not line | ||
| 3438 | // noise. Drop the peer and leave the fetch outstanding so the failover | ||
| 3439 | // sweep asks somebody else for the real X, rather than recording X as | ||
| 3440 | // delivered on the strength of a header anyone could copy. | ||
| 3441 | if (pblock->hashMerkleRoot != pblock->BuildMerkleTree()) | ||
| 3442 | { | ||
| 3443 | if (LogAcceptsCategory("net")) | ||
| 3444 | printf("peer %s served a forged body for %s\n", | ||
| 3445 | pfrom->addr.ToString().c_str(), inv.ToString().c_str()); | ||
| 3446 | // One line per connection: ProcessMessages keeps draining the | ||
| 3447 | // receive buffer after fDisconnect is set, so a peer that packs | ||
| 3448 | // many forged bodies into one buffer must not get one log line | ||
| 3449 | // for each of them. | ||
| 3450 | if (!pfrom->fDisconnect) | ||
| 3451 | printf("block fetch: a peer answered the request for %s with a body that is not " | ||
| 3452 | "that block -- dropping it\n", inv.hash.ToString().substr(0,14).c_str()); | ||
| 3453 | pfrom->fDisconnect = true; | ||
| 3454 | return error("ProcessMessage() : forged block body for %s", | ||
| 3455 | inv.hash.ToString().substr(0,14).c_str()); | ||
| 3456 | } | ||
| 3457 | |||
| 3271 | if (ProcessBlock(pfrom, pblock.release())) | 3458 | if (ProcessBlock(pfrom, pblock.release())) |
| 3272 | { | 3459 | { |
| 3273 | mapAlreadyAskedFor.erase(inv); | 3460 | mapAlreadyAskedFor.erase(inv); |
| 3274 | 3461 | ||
| 3462 | // The body arrived and validated, so the fetch really is over: | ||
| 3463 | // the block is now in mapBlockIndex, or parked in mapOrphanBlocks, | ||
| 3464 | // and either way AlreadyHave answers for it from here on. | ||
| 3465 | // | ||
| 3466 | // This has to stay inside the success branch, next to the erase | ||
| 3467 | // above. When validation rejects a body nothing is stored, | ||
| 3468 | // AlreadyHave stays false, and the mapAskFor entry was already | ||
| 3469 | // consumed when the getdata went out -- so erasing the tracking | ||
| 3470 | // here would leave the block requested from nobody at all until | ||
| 3471 | // some peer happened to re-announce it. That is exactly the stall | ||
| 3472 | // of #33, and erasing before ProcessBlock made it reachable on | ||
| 3473 | // demand. | ||
| 3474 | mapBlockFetch.erase(inv.hash); | ||
| 3475 | |||
| 3275 | // A peer that just handed us a block is demonstrably at least at | 3476 | // A peer that just handed us a block is demonstrably at least at |
| 3276 | // that block's height. Without this, nStartingHeight is a snapshot | 3477 | // that block's height. Without this, nStartingHeight is a snapshot |
| 3277 | // taken once during the version handshake and never touched again, | 3478 | // taken once during the version handshake and never touched again, |
| @@ -3396,12 +3597,119 @@ bool SendMessages(CNode* pto) | |||
| 3396 | const CInv& inv = (*pto->mapAskFor.begin()).second; | 3597 | const CInv& inv = (*pto->mapAskFor.begin()).second; |
| 3397 | if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); | 3598 | if (LogAcceptsCategory("net")) printf("sending getdata: %s\n", inv.ToString().c_str()); |
| 3398 | if (!AlreadyHave(txdb, inv)) | 3599 | if (!AlreadyHave(txdb, inv)) |
| 3600 | { | ||
| 3399 | vAskFor.push_back(inv); | 3601 | vAskFor.push_back(inv); |
| 3602 | // Blocks get their delivery tracked so the failover pass | ||
| 3603 | // below can chase the ones that go quiet. | ||
| 3604 | if (inv.type == MSG_BLOCK) | ||
| 3605 | { | ||
| 3606 | // Full, and this is a hash we are not already tracking: | ||
| 3607 | // make room rather than drop it on the floor, or one peer | ||
| 3608 | // announcing eight hashes turns failover off for everyone. | ||
| 3609 | if (!mapBlockFetch.count(inv.hash) && | ||
| 3610 | mapBlockFetch.size() >= BLOCK_FETCH_TRACK_MAX) | ||
| 3611 | MakeRoomInBlockFetch(pto->addr); | ||
| 3612 | |||
| 3613 | if (mapBlockFetch.count(inv.hash) || | ||
| 3614 | mapBlockFetch.size() < BLOCK_FETCH_TRACK_MAX) | ||
| 3615 | { | ||
| 3616 | CBlockFetch& fetch = mapBlockFetch[inv.hash]; | ||
| 3617 | if (fetch.nTimeFirstAsked == 0) | ||
| 3618 | { | ||
| 3619 | fetch.nTimeFirstAsked = GetTime(); | ||
| 3620 | // Who is answerable for this hash existing. Set | ||
| 3621 | // once, on creation: the failover sweep asks other | ||
| 3622 | // peers later, and none of them chose to announce | ||
| 3623 | // it, so none of them should be charged for it. | ||
| 3624 | fetch.addrAnnounced = pto->addr; | ||
| 3625 | } | ||
| 3626 | fetch.nTimeAsked = GetTime(); | ||
| 3627 | fetch.setAsked.insert(pto->addr); | ||
| 3628 | } | ||
| 3629 | } | ||
| 3630 | } | ||
| 3400 | pto->mapAskFor.erase(pto->mapAskFor.begin()); | 3631 | pto->mapAskFor.erase(pto->mapAskFor.begin()); |
| 3401 | } | 3632 | } |
| 3402 | if (!vAskFor.empty()) | 3633 | if (!vAskFor.empty()) |
| 3403 | pto->PushMessage("getdata", vAskFor); | 3634 | pto->PushMessage("getdata", vAskFor); |
| 3404 | 3635 | ||
| 3636 | |||
| 3637 | // | ||
| 3638 | // Message: getdata (block fetch failover) | ||
| 3639 | // | ||
| 3640 | // A requested block that has not arrived in BLOCK_FETCH_RETRY_SECS is | ||
| 3641 | // not still in transit -- a body is at most 1 MB and somebody | ||
| 3642 | // demonstrably has it -- so ask somebody else instead of waiting on | ||
| 3643 | // whoever was asked first (#33). Each peer the message handler visits | ||
| 3644 | // takes at most one turn per block per round; once every connected | ||
| 3645 | // peer has had its turn and a further interval passes with nobody | ||
| 3646 | // new to ask, the round restarts. A peer that declared itself a | ||
| 3647 | // client cannot serve block bodies and is no use as a target. | ||
| 3648 | if (!pto->fClient) | ||
| 3649 | { | ||
| 3650 | vector<CInv> vFailover; | ||
| 3651 | int64 nNowSecs = GetTime(); | ||
| 3652 | for (map<uint256, CBlockFetch>::iterator mi = mapBlockFetch.begin(); mi != mapBlockFetch.end();) | ||
| 3653 | { | ||
| 3654 | CBlockFetch& fetch = (*mi).second; | ||
| 3655 | CInv inv(MSG_BLOCK, (*mi).first); | ||
| 3656 | if (AlreadyHave(txdb, inv)) | ||
| 3657 | { | ||
| 3658 | // Arrived by some other road -- our own miner, an orphan | ||
| 3659 | // parked in mapOrphanBlocks. Nothing left to chase. | ||
| 3660 | mapBlockFetch.erase(mi++); | ||
| 3661 | continue; | ||
| 3662 | } | ||
| 3663 | if (nNowSecs - fetch.nTimeFirstAsked > BLOCK_FETCH_GIVEUP_SECS) | ||
| 3664 | { | ||
| 3665 | printf("block fetch: giving up on %s -- no peer served it in %d s\n", | ||
| 3666 | inv.ToString().c_str(), (int)(nNowSecs - fetch.nTimeFirstAsked)); | ||
| 3667 | mapBlockFetch.erase(mi++); | ||
| 3668 | continue; | ||
| 3669 | } | ||
| 3670 | if (nNowSecs - fetch.nTimeAsked >= BLOCK_FETCH_RETRY_SECS) | ||
| 3671 | { | ||
| 3672 | // The handler loop visits every peer within ~100ms, so a | ||
| 3673 | // request stale by two full intervals means nobody outside | ||
| 3674 | // setAsked was there to ask. Start the round over. | ||
| 3675 | if (nNowSecs - fetch.nTimeAsked >= 2 * BLOCK_FETCH_RETRY_SECS) | ||
| 3676 | fetch.setAsked.clear(); | ||
| 3677 | if (!fetch.setAsked.count(pto->addr)) | ||
| 3678 | { | ||
| 3679 | // Which block, how long, and which peer we are trying | ||
| 3680 | // next. Behind the category because it names a peer | ||
| 3681 | // and a peer decides how often it is written. | ||
| 3682 | if (LogAcceptsCategory("net")) | ||
| 3683 | printf("block fetch failover: %s unanswered for %d s, re-requesting from %s\n", | ||
| 3684 | inv.ToString().c_str(), (int)(nNowSecs - fetch.nTimeAsked), | ||
| 3685 | pto->addr.ToString().c_str()); | ||
| 3686 | |||
| 3687 | // The unconditional half: no addresses, and at most | ||
| 3688 | // one line per BLOCK_FETCH_REPORT_SECS however hard a | ||
| 3689 | // peer tries. See the counters for the reasoning. | ||
| 3690 | nBlockFetchFailovers++; | ||
| 3691 | if (nNowSecs - nBlockFetchWindowStart >= BLOCK_FETCH_REPORT_SECS) | ||
| 3692 | { | ||
| 3693 | printf("block fetch: %d block request%s went unanswered and %s been " | ||
| 3694 | "re-asked of other peers (%d tracked)\n", | ||
| 3695 | nBlockFetchFailovers, nBlockFetchFailovers == 1 ? "" : "s", | ||
| 3696 | nBlockFetchFailovers == 1 ? "has" : "have", | ||
| 3697 | (int)mapBlockFetch.size()); | ||
| 3698 | nBlockFetchWindowStart = nNowSecs; | ||
| 3699 | nBlockFetchFailovers = 0; | ||
| 3700 | } | ||
| 3701 | |||
| 3702 | vFailover.push_back(inv); | ||
| 3703 | fetch.nTimeAsked = nNowSecs; | ||
| 3704 | fetch.setAsked.insert(pto->addr); | ||
| 3705 | } | ||
| 3706 | } | ||
| 3707 | ++mi; | ||
| 3708 | } | ||
| 3709 | if (!vFailover.empty()) | ||
| 3710 | pto->PushMessage("getdata", vFailover); | ||
| 3711 | } | ||
| 3712 | |||
| 3405 | } | 3713 | } |
| 3406 | return true; | 3714 | return true; |
| 3407 | } | 3715 | } |
| @@ -3662,6 +3970,14 @@ static void ParticipantHashWorker(ParticipantJobShared* sh, int idx, int nWorker | |||
| 3662 | RandomXDestroyMinerVM(vm); | 3970 | RandomXDestroyMinerVM(vm); |
| 3663 | } | 3971 | } |
| 3664 | 3972 | ||
| 3973 | // Run one participant session: resolve the pool, open the tunnel, subscribe, | ||
| 3974 | // hash, submit shares. Returns true only if the pool actually handed out work | ||
| 3975 | // -- at least one well-formed mining.notify arrived. Everything else is false, | ||
| 3976 | // including a tunnel that opened and was dropped before any job. | ||
| 3977 | // | ||
| 3978 | // The caller's reconnect backoff keys off this return value, so any path added | ||
| 3979 | // here that leaves the session without work MUST return false. "The session | ||
| 3980 | // ended tidily" is not the question being asked. | ||
| 3665 | static bool PoolParticipantMiner() | 3981 | static bool PoolParticipantMiner() |
| 3666 | { | 3982 | { |
| 3667 | // ------------------------------------------------------------------ | 3983 | // ------------------------------------------------------------------ |
| @@ -3739,6 +4055,33 @@ static bool PoolParticipantMiner() | |||
| 3739 | } | 4055 | } |
| 3740 | LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); | 4056 | LogPrint("worker", "[worker] pool resolved to meeting node %s\n", meetingHostPort.c_str()); |
| 3741 | 4057 | ||
| 4058 | // The descriptor names ONE meeting node, but the pool parks a | ||
| 4059 | // registration at every relay it knows (one accept thread per relay, | ||
| 4060 | // rpc.cpp:AcceptOneFn), so any seed relay can pair us. Insisting on the | ||
| 4061 | // advertised entry forever turns one bad path into a miner that idles | ||
| 4062 | // indefinitely while a working path exists (issue #17); after a few | ||
| 4063 | // consecutive tunnel failures, rotate the seed relays into the attempts | ||
| 4064 | // too. Only ONE participant session runs (StartMinerThreads), so these | ||
| 4065 | // statics are single-threaded. | ||
| 4066 | static const int TUNNEL_FAILOVER_AFTER = 3; | ||
| 4067 | static int nTunnelFails = 0; // consecutive tunnel failures, this pool | ||
| 4068 | static size_t iFailoverRelay = 0; // rotation cursor over vBtfMeetingRelays | ||
| 4069 | static std::string strFailPool; // pool the counters belong to | ||
| 4070 | if (strFailPool != strParticipantPool) { | ||
| 4071 | strFailPool = strParticipantPool; | ||
| 4072 | nTunnelFails = 0; | ||
| 4073 | iFailoverRelay = 0; | ||
| 4074 | } | ||
| 4075 | if (nTunnelFails >= TUNNEL_FAILOVER_AFTER && !vBtfMeetingRelays.empty()) { | ||
| 4076 | std::string alt = vBtfMeetingRelays[iFailoverRelay++ % vBtfMeetingRelays.size()]; | ||
| 4077 | if (alt != meetingHostPort) { | ||
| 4078 | LogPrint("worker", "[worker] advertised meeting node %s failed %d" | ||
| 4079 | " times in a row -- trying seed relay %s instead\n", | ||
| 4080 | meetingHostPort.c_str(), nTunnelFails, alt.c_str()); | ||
| 4081 | meetingHostPort = alt; | ||
| 4082 | } | ||
| 4083 | } | ||
| 4084 | |||
| 3742 | size_t colon = meetingHostPort.rfind(':'); | 4085 | size_t colon = meetingHostPort.rfind(':'); |
| 3743 | if (colon == std::string::npos) { | 4086 | if (colon == std::string::npos) { |
| 3744 | LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); | 4087 | LogPrint("worker", "[worker] bad meeting node format '%s'\n", meetingHostPort.c_str()); |
| @@ -3758,15 +4101,18 @@ static bool PoolParticipantMiner() | |||
| 3758 | // Step 2b: open encrypted tunnel to pool | 4101 | // Step 2b: open encrypted tunnel to pool |
| 3759 | // ------------------------------------------------------------------ | 4102 | // ------------------------------------------------------------------ |
| 3760 | SetParticipantMiningStatus("opening tunnel"); | 4103 | SetParticipantMiningStatus("opening tunnel"); |
| 4104 | btf::TunnelFail tunnelFail = btf::TUNNEL_FAIL_NONE; | ||
| 3761 | s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, | 4105 | s = btf::BtfClientTunnel(host.c_str(), (unsigned short)port, |
| 3762 | target_pubkey, service_enc_pub); | 4106 | target_pubkey, service_enc_pub, &tunnelFail); |
| 3763 | if (s == INVALID_SOCKET) { | 4107 | if (s == INVALID_SOCKET) { |
| 3764 | LogPrint("worker", "[worker] tunnel connect FAILED via %s" | 4108 | nTunnelFails++; |
| 3765 | " (relay unreachable or pool not listening)\n", | 4109 | LogPrint("worker", "[worker] tunnel connect FAILED via %s: %s\n", |
| 3766 | meetingHostPort.c_str()); | 4110 | meetingHostPort.c_str(), btf::TunnelFailText(tunnelFail)); |
| 3767 | SetParticipantMiningStatus("tunnel connect failed"); | 4111 | SetParticipantMiningStatus(std::string("tunnel connect failed: ") |
| 4112 | + btf::TunnelFailText(tunnelFail)); | ||
| 3768 | return false; | 4113 | return false; |
| 3769 | } | 4114 | } |
| 4115 | nTunnelFails = 0; | ||
| 3770 | LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); | 4116 | LogPrint("worker", "[worker] tunnel open via %s\n", meetingHostPort.c_str()); |
| 3771 | SetParticipantMiningStatus("tunnel open"); | 4117 | SetParticipantMiningStatus("tunnel open"); |
| 3772 | } | 4118 | } |
| @@ -3899,9 +4245,32 @@ connected: | |||
| 3899 | std::vector<ParticipantShareHit> shareHits; | 4245 | std::vector<ParticipantShareHit> shareHits; |
| 3900 | std::mutex shareHitsMtx; | 4246 | std::mutex shareHitsMtx; |
| 3901 | std::vector<std::thread> hashThreads; | 4247 | std::vector<std::thread> hashThreads; |
| 3902 | for (int i = 0; i < nWorkers; i++) | 4248 | try |
| 3903 | hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, | 4249 | { |
| 3904 | &shareHits, &shareHitsMtx); | 4250 | for (int i = 0; i < nWorkers; i++) |
| 4251 | hashThreads.emplace_back(ParticipantHashWorker, &jobShared, i, nWorkers, | ||
| 4252 | &shareHits, &shareHitsMtx); | ||
| 4253 | } | ||
| 4254 | catch (const std::system_error& e) | ||
| 4255 | { | ||
| 4256 | // A refused pthread_create used to reach std::terminate here and | ||
| 4257 | // kill the worker mid-session -- observed twice on an otherwise | ||
| 4258 | // healthy participant. Stop the workers that did start the same way | ||
| 4259 | // the disconnect path below does, drop this pool connection, and let | ||
| 4260 | // the caller's reconnect loop try again once the pressure clears. | ||
| 4261 | LogPrint("worker", "[worker] cannot spawn hash threads (%s)" | ||
| 4262 | " -- dropping pool connection\n", e.what()); | ||
| 4263 | SetParticipantMiningStatus("hash thread spawn failed"); | ||
| 4264 | { | ||
| 4265 | std::lock_guard<std::mutex> lk(jobShared.mtx); | ||
| 4266 | jobShared.stop = true; | ||
| 4267 | jobShared.haveJob = false; | ||
| 4268 | } | ||
| 4269 | for (std::thread& t : hashThreads) | ||
| 4270 | t.join(); | ||
| 4271 | closesocket(s); | ||
| 4272 | return false; | ||
| 4273 | } | ||
| 3905 | LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n", | 4274 | LogPrint("worker", "[worker] spawned %d hash threads on shared RandomX dataset\n", |
| 3906 | nWorkers); | 4275 | nWorkers); |
| 3907 | const uint64 sessionStartHashes = gParticipantHashes.load(); | 4276 | const uint64 sessionStartHashes = gParticipantHashes.load(); |
| @@ -3912,8 +4281,17 @@ connected: | |||
| 3912 | std::string jobId; | 4281 | std::string jobId; |
| 3913 | unsigned char header[80] = {}; | 4282 | unsigned char header[80] = {}; |
| 3914 | uint256 target; | 4283 | uint256 target; |
| 4284 | // Cached answer to "which block is this job built on", for the | ||
| 4285 | // mining.notify handler below -- see the comment there. Session-local, so | ||
| 4286 | // a reconnect re-resolves rather than trusting an answer obtained before | ||
| 4287 | // a possible change of pool. | ||
| 4288 | uint256 hashPrevResolved; // prevhash the cached answer describes | ||
| 4289 | bool fPrevResolved = false; // whether that answer exists yet | ||
| 4290 | int nPrevHeight = -1; // its height, or -1 == unknown to us | ||
| 4291 | int nBestHeightResolved = -1; // nBestHeight when we last resolved | ||
| 3915 | double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool | 4292 | double currentDifficulty = 1.0; // updated by mining.set_difficulty from pool |
| 3916 | bool haveJob = false; | 4293 | bool haveJob = false; |
| 4294 | bool gotAnyJob = false; // any job at all this session -- the return value | ||
| 3917 | bool gotSubscribeAck = false; | 4295 | bool gotSubscribeAck = false; |
| 3918 | bool gotAuthorizeAck = false; | 4296 | bool gotAuthorizeAck = false; |
| 3919 | int64 authWaitStart = 0; | 4297 | int64 authWaitStart = 0; |
| @@ -4030,8 +4408,9 @@ connected: | |||
| 4030 | jobId = newJobId; | 4408 | jobId = newJobId; |
| 4031 | memcpy(header, hdrBytes.data(), 80); | 4409 | memcpy(header, hdrBytes.data(), 80); |
| 4032 | memcpy(&target, tgtBytes.data(), 32); | 4410 | memcpy(&target, tgtBytes.data(), 32); |
| 4033 | haveJob = true; | 4411 | haveJob = true; |
| 4034 | nNonce = 1; | 4412 | gotAnyJob = true; |
| 4413 | nNonce = 1; | ||
| 4035 | { | 4414 | { |
| 4036 | std::lock_guard<std::mutex> lk(jobShared.mtx); | 4415 | std::lock_guard<std::mutex> lk(jobShared.mtx); |
| 4037 | memcpy(jobShared.header, header, 80); | 4416 | memcpy(jobShared.header, header, 80); |
| @@ -4040,8 +4419,64 @@ connected: | |||
| 4040 | jobShared.jobSeq++; | 4419 | jobShared.jobSeq++; |
| 4041 | jobShared.haveJob = true; | 4420 | jobShared.haveJob = true; |
| 4042 | } | 4421 | } |
| 4043 | LogPrint("worker", "[pool->worker] new job %s received\n", | 4422 | // Log the chain position the pool is |
| 4044 | jobId.c_str()); | 4423 | // building on, so a stale or private |
| 4424 | // fork is auditable from worker logs. | ||
| 4425 | // The prevhash comes from the header we | ||
| 4426 | // actually hash (== p[2] of the notify); | ||
| 4427 | // a brief cs_main lookup is safe here -- | ||
| 4428 | // this thread holds no other lock. | ||
| 4429 | // | ||
| 4430 | // Resolving it is the expensive half, and | ||
| 4431 | // this is the socket-reading thread: block | ||
| 4432 | // validation can hold cs_main for a long | ||
| 4433 | // time, and every moment spent waiting on | ||
| 4434 | // it here is a moment set_difficulty and | ||
| 4435 | // share responses are not being handled. | ||
| 4436 | // Most notifies in a round are a new job | ||
| 4437 | // on the SAME tip, so the answer is cached | ||
| 4438 | // and re-resolved only when it could come | ||
| 4439 | // out differently: a prevhash we have not | ||
| 4440 | // resolved, or one we could NOT resolve | ||
| 4441 | // last time while the chain has since | ||
| 4442 | // moved (unknown -> known). A block's | ||
| 4443 | // height is fixed once mapBlockIndex knows | ||
| 4444 | // it, so a cached "known" never goes | ||
| 4445 | // stale. nBestHeight is read unlocked on | ||
| 4446 | // purpose -- it only decides whether to | ||
| 4447 | // repeat a lookup, never what gets | ||
| 4448 | // printed, so a stale read costs at most | ||
| 4449 | // one deferred or one extra lookup. | ||
| 4450 | // The line still prints in full for every | ||
| 4451 | // job, cached or not. | ||
| 4452 | uint256 hashPrev; | ||
| 4453 | memcpy(&hashPrev, header + 4, 32); | ||
| 4454 | if (LogAcceptsCategory("worker")) | ||
| 4455 | { | ||
| 4456 | int nBestHeightNow = nBestHeight; | ||
| 4457 | if (!fPrevResolved | ||
| 4458 | || hashPrev != hashPrevResolved | ||
| 4459 | || (nPrevHeight < 0 && nBestHeightNow != nBestHeightResolved)) | ||
| 4460 | { | ||
| 4461 | nPrevHeight = -1; | ||
| 4462 | CRITICAL_BLOCK(cs_main) | ||
| 4463 | { | ||
| 4464 | map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(hashPrev); | ||
| 4465 | if (mi != mapBlockIndex.end()) | ||
| 4466 | nPrevHeight = (*mi).second->nHeight; | ||
| 4467 | } | ||
| 4468 | hashPrevResolved = hashPrev; | ||
| 4469 | nBestHeightResolved = nBestHeightNow; | ||
| 4470 | fPrevResolved = true; | ||
| 4471 | } | ||
| 4472 | if (nPrevHeight >= 0) | ||
| 4473 | LogPrint("worker", "[pool->worker] new job %s received, prevhash %s (known, job height %d)\n", | ||
| 4474 | jobId.c_str(), hashPrev.GetHex().c_str(), | ||
| 4475 | nPrevHeight + 1); | ||
| 4476 | else | ||
| 4477 | LogPrint("worker", "[pool->worker] new job %s received, prevhash %s (unknown to our chain)\n", | ||
| 4478 | jobId.c_str(), hashPrev.GetHex().c_str()); | ||
| 4479 | } | ||
| 4045 | SetParticipantMiningStatus("hashing job " + jobId); | 4480 | SetParticipantMiningStatus("hashing job " + jobId); |
| 4046 | } | 4481 | } |
| 4047 | } | 4482 | } |
| @@ -4167,7 +4602,11 @@ connected: | |||
| 4167 | (unsigned long long)hashesSinceSample, | 4602 | (unsigned long long)hashesSinceSample, |
| 4168 | (unsigned long long)sessionSharesSent, | 4603 | (unsigned long long)sessionSharesSent, |
| 4169 | (unsigned long long)sessionSharesAccepted); | 4604 | (unsigned long long)sessionSharesAccepted); |
| 4170 | return true; | 4605 | // Not "we got here", which is true of a tunnel the pool accepted and then |
| 4606 | // dropped without ever sending a job -- exactly the case the caller's | ||
| 4607 | // backoff exists for. Reaching the end of the session is not evidence the | ||
| 4608 | // session was worth anything; a job is. | ||
| 4609 | return gotAnyJob; | ||
| 4171 | } | 4610 | } |
| 4172 | 4611 | ||
| 4173 | // --------------------------------------------------------------------------- | 4612 | // --------------------------------------------------------------------------- |
| @@ -4364,11 +4803,51 @@ bool BitcoinMiner(int nThreadId) | |||
| 4364 | // Participant mode: connect to external pool instead of mining solo/operator | 4803 | // Participant mode: connect to external pool instead of mining solo/operator |
| 4365 | if (nMineMode == MINE_PARTICIPANT) | 4804 | if (nMineMode == MINE_PARTICIPANT) |
| 4366 | { | 4805 | { |
| 4806 | // An attempt that never got work backs off (10s doubling to 120s) | ||
| 4807 | // instead of hammering the same path every flat 10s, and a streak | ||
| 4808 | // lasting minutes is said at printf level: the identical once-per-10s | ||
| 4809 | // LogPrint line is how a worker idled all night unnoticed (issue #17). | ||
| 4810 | // | ||
| 4811 | // Only a session the pool actually sent a job to resets either. The | ||
| 4812 | // weaker test -- "the session returned" -- was gameable: a pool that | ||
| 4813 | // completes the tunnel and drops without ever sending work leaves by | ||
| 4814 | // the same tidy cleanup path a healthy one does, which would pin the | ||
| 4815 | // retry at 10s forever and defeat the backoff in exactly the scenario | ||
| 4816 | // it exists for. | ||
| 4817 | int nRetrySecs = 10; | ||
| 4818 | int64 nFailingSince = 0; | ||
| 4367 | while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { | 4819 | while (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { |
| 4368 | PoolParticipantMiner(); | 4820 | bool fGotWork = PoolParticipantMiner(); |
| 4369 | if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { | 4821 | if (fGenerateBitcoins && nMineMode == MINE_PARTICIPANT && !fShutdown) { |
| 4370 | LogPrint("worker", "PoolParticipantMiner: reconnecting in 10s\n"); | 4822 | if (fGotWork) { |
| 4371 | for (int i = 0; i < 10 && !fShutdown; i++) Sleep(1000); | 4823 | nRetrySecs = 10; |
| 4824 | nFailingSince = 0; | ||
| 4825 | } else { | ||
| 4826 | if (nFailingSince == 0) | ||
| 4827 | nFailingSince = GetTime(); | ||
| 4828 | int64 nMins = (GetTime() - nFailingSince) / 60; | ||
| 4829 | if (nMins >= 1) | ||
| 4830 | printf("[worker] connection to pool %s has been failing" | ||
| 4831 | " for %lld minutes\n", | ||
| 4832 | strParticipantPool.c_str(), (long long)nMins); | ||
| 4833 | } | ||
| 4834 | LogPrint("worker", "PoolParticipantMiner: reconnecting in %ds\n", | ||
| 4835 | nRetrySecs); | ||
| 4836 | // Poll everything the enclosing while polls, not just | ||
| 4837 | // fShutdown. Raising the ceiling to 120s made a | ||
| 4838 | // shutdown-only predicate expensive: after the operator | ||
| 4839 | // clears Generate or switches to Relay, the session -- and | ||
| 4840 | // the vfThreadRunning[3] slot that gates restarting it -- | ||
| 4841 | // would stay held for up to two minutes. Measured 119s | ||
| 4842 | // before, 1s after. | ||
| 4843 | for (int i = 0; i < nRetrySecs && !fShutdown && fGenerateBitcoins | ||
| 4844 | && nMineMode == MINE_PARTICIPANT; i++) | ||
| 4845 | Sleep(1000); | ||
| 4846 | if (!fGotWork && nRetrySecs < 120) { | ||
| 4847 | nRetrySecs *= 2; | ||
| 4848 | if (nRetrySecs > 120) | ||
| 4849 | nRetrySecs = 120; | ||
| 4850 | } | ||
| 4372 | } | 4851 | } |
| 4373 | } | 4852 | } |
| 4374 | return true; | 4853 | return true; |
| @@ -4555,7 +5034,7 @@ bool BitcoinMiner(int nThreadId) | |||
| 4555 | //// debug print | 5034 | //// debug print |
| 4556 | if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); | 5035 | if (LogAcceptsCategory("net")) printf("BitcoinMiner:\n"); |
| 4557 | if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); | 5036 | if (LogAcceptsCategory("net")) printf("RandomX proof-of-work found \n powhash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str()); |
| 4558 | if (LogAcceptsCategory("net")) pblock->print(); | 5037 | if (LogAcceptsCategory("relay")) pblock->print(); |
| 4559 | 5038 | ||
| 4560 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); | 5039 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); |
| 4561 | CRITICAL_BLOCK(cs_main) | 5040 | CRITICAL_BLOCK(cs_main) |