From e236d013e973fd00cbdbed4bea1e5f373c0d1236 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Tue, 11 Aug 2026 20:52:09 +0000 Subject: pool: refuse jobs when no peer has reported a height for 20 minutes The peer-height gate replaced a tip-age check that was wrong in both directions. It is a strict improvement and it is still blind in the one case that matters most. When the node goes deaf -- peers still in vNodes, nothing arriving -- no peer announces anything, GetPeerMedianHeight() returns -1, and the fallback mines our best chain. That fallback is right for a node that has not been told yet and wrong for a node that has stopped being able to hear, and a height nobody sent cannot tell those apart. A wedged node is exactly a node with no peer height information, so the gate was silent for the whole of heights 3966-4085: 120 blocks, 5890.50 BTF voided. Time can tell them apart. GetPeerHeightAgeSecs() measures how long since any peer reported a height, counting from first use rather than the epoch so a starting node does not refuse jobs before its first handshake. Past 20 minutes the pool stops issuing jobs and says so unconditionally in the log. /poolmineblind overrides it: without an escape hatch a genuinely isolated pool that is 90% of the hashrate could never restart its own chain, which is the deadlock the tip-age check used to cause. Also stop nStartingHeight from going stale. It was a snapshot taken once during the version handshake and never touched again, so on a long-lived connection the median is built from heights the peers had hours ago, and the behind-the-network check drifts toward 'everyone is behind us' and quietly stops firing. A peer that hands us a block is demonstrably at least at that block's height. (cherry picked from commit 0800c18faf703b4acde1b131d0b742d37a86d92b) --- src/main.cpp | 15 +++++++++++++++ src/main.h | 1 + src/main_gui.cpp | 3 +++ src/net.cpp | 23 +++++++++++++++++++++++ src/net.h | 8 ++++++++ src/rpc.cpp | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index 382a008..5d0b70a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -88,6 +88,7 @@ string strSetDataDir; int nDropMessagesTest = 0; bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test) bool fPoolNoAnnounce = false; // /noannounce: operator serves miners but never publishes pool announcements +bool fPoolMineBlind = false; // /poolmineblind: issue jobs even when no peer has reported a height // Settings int fGenerateBitcoins; @@ -3172,7 +3173,21 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) pfrom->AddInventoryKnown(inv); if (ProcessBlock(pfrom, pblock.release())) + { mapAlreadyAskedFor.erase(inv); + + // A peer that just handed us a block is demonstrably at least at + // that block's height. Without this, nStartingHeight is a snapshot + // taken once during the version handshake and never touched again, + // so on a long-lived connection it is the peer's height from + // however many hours ago it happened to connect. The pool's + // behind-the-network check compares against the median of those + // numbers, so as connections age it drifts toward "everyone is + // behind us" and quietly stops firing -- the opposite of its job. + map::iterator mi = mapBlockIndex.find(inv.hash); + if (mi != mapBlockIndex.end() && mi->second) + pfrom->nStartingHeight = max(pfrom->nStartingHeight, mi->second->nHeight); + } } diff --git a/src/main.h b/src/main.h index 6b1ff0a..f5409e9 100644 --- a/src/main.h +++ b/src/main.h @@ -93,6 +93,7 @@ extern string strParticipantPool; // participant: pool .btf address extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet) extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect extern bool fPoolNoAnnounce; // /noannounce: suppress pool announcements +extern bool fPoolMineBlind; // /poolmineblind: mine on even when deaf to every peer extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) extern string strPoolName; // operator: announced pool name extern string strPoolDashboardUrl; // operator: optional dashboard URL diff --git a/src/main_gui.cpp b/src/main_gui.cpp index b1fe0af..c20ae46 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -253,6 +253,9 @@ static void ParseStartupArguments(int argc, char* argv[]) if (arg(argc,argv,"/noannounce") || arg(argc,argv,"-noannounce")) fPoolNoAnnounce = true; + if (arg(argc,argv,"/poolmineblind") || arg(argc,argv,"-poolmineblind")) + fPoolMineBlind = true; + string poolFee = argval2(argc, argv, "/poolfee", "-poolfee"); if (!poolFee.empty()) dPoolFeePercent = atof(poolFee.c_str()); diff --git a/src/net.cpp b/src/net.cpp index 5cf2f8c..964097b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1700,6 +1700,13 @@ void ThreadMessageHandler(void* parg) } } +// When any peer last told us a height. A wedged node -- one that still has +// peers but has stopped hearing them -- is exactly a node where this stops +// advancing, and that is the one thing the height comparison itself cannot +// see: there is no height to compare. +static int64 nLastPeerHeightSeen = 0; +static int64 nPeerHeightFirstAsked = 0; + int GetPeerMedianHeight() { vector vHeights; @@ -1711,10 +1718,26 @@ int GetPeerMedianHeight() if (vHeights.empty()) return -1; + nLastPeerHeightSeen = GetTime(); sort(vHeights.begin(), vHeights.end()); return vHeights[vHeights.size() / 2]; } +// Seconds since any peer last reported a height. +// +// Before ever hearing one, measure from the first time somebody asked rather +// than from the epoch -- otherwise a node refuses jobs the instant it starts, +// before it has had a chance to complete a single handshake. +int64 GetPeerHeightAgeSecs() +{ + int64 nNow = GetTime(); + if (nPeerHeightFirstAsked == 0) + nPeerHeightFirstAsked = nNow; + if (nLastPeerHeightSeen == 0) + return nNow - nPeerHeightFirstAsked; + return nNow - nLastPeerHeightSeen; +} + // Say out loud when we are behind the network. // diff --git a/src/net.h b/src/net.h index bcd0b2a..e4cfa69 100644 --- a/src/net.h +++ b/src/net.h @@ -120,6 +120,13 @@ static const unsigned int MAX_CONNECTIONS = 125; // this long regardless of what is still buffered. static const int64 DISCONNECT_DRAIN_SECS = 60; +// How long a pool may go without hearing a height from ANY peer before it +// stops issuing jobs. Long enough that ordinary peer churn and a quiet +// handshake window do not trip it; short enough that a deaf pool cannot build +// more than a few blocks of private fork before it is stopped. Heights +// 3966-4085 -- 120 blocks, 5890.50 BTF -- were mined in exactly this state. +static const int64 BTF_NO_PEER_HEIGHT_MAX_SECS = 20 * 60; + // Send a ping after this long with nothing to say, so a peer running the // inactivity check above does not mistake a quiet node for a dead one. Must // stay well under BTF_RECV_TIMEOUT_SECS. @@ -160,6 +167,7 @@ extern int nBestHeight; // has told us anything. Median rather than maximum so one peer claiming an // absurd height cannot move it. int GetPeerMedianHeight(); +int64 GetPeerHeightAgeSecs(); // Lightweight .btf churn accounting. These counters are diagnostics only: they // let an operator tell stale descriptors, relay pairing failures, and paired diff --git a/src/rpc.cpp b/src/rpc.cpp index 8a0d112..da22b1d 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -180,6 +180,38 @@ static bool RebuildJob() return false; } + // The comparison above is blind in the one case that matters most. + // + // When the node goes deaf -- peers still in vNodes, nothing arriving -- + // no peer announces anything, GetPeerMedianHeight() returns -1, and the + // fallback ("nobody has told us anything, so mine our best chain") let + // the pool build a private fork for as long as the deafness lasted. + // That fallback is right for a node that has simply not been told yet + // and wrong for a node that has stopped being able to hear, and a + // height nobody sent cannot tell those apart. Time can. + // + // Deliberately not behind a debug category, and deliberately not a + // silent refusal: an operator whose pool has stopped issuing jobs needs + // the reason in the log. /poolmineblind is the escape hatch for a node + // that really is alone -- without one, a genuinely isolated pool that is + // the majority of the hashrate could never restart its own chain, which + // is the deadlock the tip-age check used to cause. + if (nPeerHeight < 0 && !fPoolMineBlind) { + int64 nAge = GetPeerHeightAgeSecs(); + if (nAge >= BTF_NO_PEER_HEIGHT_MAX_SECS) { + static int64 nLastSaid = 0; + if (GetTime() - nLastSaid > 60) { + nLastSaid = GetTime(); + printf("RebuildJob: no peer has reported a height for %d minutes " + "-- not issuing jobs (our height %d, %d peers connected). " + "This node is deaf; mining now would fork it. " + "Pass /poolmineblind to override.\n", + (int)(nAge / 60), nBestHeight, (int)vNodes.size()); + } + return false; + } + } + CBlockIndex* pindexPrev = pindexBest; unsigned int nBits = GetNextWorkRequired(pindexPrev); -- cgit v1.2.3