upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-11 20:52:09 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 04:37:36 +0000
commit3d7c30ebb6ac0c586635c693a40bc39180f539b9 (patch)
treefc4b0db498c03fdab8244fd4a2a3360db77aa0b9
parent090628b34d1aefc7029be37d7bc8512b5edb3ca5 (diff)
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)
-rw-r--r--src/main.cpp15
-rw-r--r--src/main.h1
-rw-r--r--src/main_gui.cpp3
-rw-r--r--src/net.cpp23
-rw-r--r--src/net.h8
-rw-r--r--src/rpc.cpp32
6 files changed, 82 insertions, 0 deletions
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;
88int nDropMessagesTest = 0; 88int nDropMessagesTest = 0;
89bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test) 89bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local test)
90bool fPoolNoAnnounce = false; // /noannounce: operator serves miners but never publishes pool announcements 90bool fPoolNoAnnounce = false; // /noannounce: operator serves miners but never publishes pool announcements
91bool fPoolMineBlind = false; // /poolmineblind: issue jobs even when no peer has reported a height
91 92
92// Settings 93// Settings
93int fGenerateBitcoins; 94int fGenerateBitcoins;
@@ -3172,7 +3173,21 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
3172 pfrom->AddInventoryKnown(inv); 3173 pfrom->AddInventoryKnown(inv);
3173 3174
3174 if (ProcessBlock(pfrom, pblock.release())) 3175 if (ProcessBlock(pfrom, pblock.release()))
3176 {
3175 mapAlreadyAskedFor.erase(inv); 3177 mapAlreadyAskedFor.erase(inv);
3178
3179 // A peer that just handed us a block is demonstrably at least at
3180 // that block's height. Without this, nStartingHeight is a snapshot
3181 // taken once during the version handshake and never touched again,
3182 // so on a long-lived connection it is the peer's height from
3183 // however many hours ago it happened to connect. The pool's
3184 // behind-the-network check compares against the median of those
3185 // numbers, so as connections age it drifts toward "everyone is
3186 // behind us" and quietly stops firing -- the opposite of its job.
3187 map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(inv.hash);
3188 if (mi != mapBlockIndex.end() && mi->second)
3189 pfrom->nStartingHeight = max(pfrom->nStartingHeight, mi->second->nHeight);
3190 }
3176 } 3191 }
3177 3192
3178 3193
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
93extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet) 93extern string strPayoutAddress; // participant: /payoutaddr override (mine without a wallet)
94extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect 94extern string strPayoutFile; // participant: /payoutfile address list, rotated per reconnect
95extern bool fPoolNoAnnounce; // /noannounce: suppress pool announcements 95extern bool fPoolNoAnnounce; // /noannounce: suppress pool announcements
96extern bool fPoolMineBlind; // /poolmineblind: mine on even when deaf to every peer
96extern int nHashThreads; // participant: /hashthreads override (0 = cores-1) 97extern int nHashThreads; // participant: /hashthreads override (0 = cores-1)
97extern string strPoolName; // operator: announced pool name 98extern string strPoolName; // operator: announced pool name
98extern string strPoolDashboardUrl; // operator: optional dashboard URL 99extern 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[])
253 if (arg(argc,argv,"/noannounce") || arg(argc,argv,"-noannounce")) 253 if (arg(argc,argv,"/noannounce") || arg(argc,argv,"-noannounce"))
254 fPoolNoAnnounce = true; 254 fPoolNoAnnounce = true;
255 255
256 if (arg(argc,argv,"/poolmineblind") || arg(argc,argv,"-poolmineblind"))
257 fPoolMineBlind = true;
258
256 string poolFee = argval2(argc, argv, "/poolfee", "-poolfee"); 259 string poolFee = argval2(argc, argv, "/poolfee", "-poolfee");
257 if (!poolFee.empty()) 260 if (!poolFee.empty())
258 dPoolFeePercent = atof(poolFee.c_str()); 261 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)
1700 } 1700 }
1701} 1701}
1702 1702
1703// When any peer last told us a height. A wedged node -- one that still has
1704// peers but has stopped hearing them -- is exactly a node where this stops
1705// advancing, and that is the one thing the height comparison itself cannot
1706// see: there is no height to compare.
1707static int64 nLastPeerHeightSeen = 0;
1708static int64 nPeerHeightFirstAsked = 0;
1709
1703int GetPeerMedianHeight() 1710int GetPeerMedianHeight()
1704{ 1711{
1705 vector<int> vHeights; 1712 vector<int> vHeights;
@@ -1711,10 +1718,26 @@ int GetPeerMedianHeight()
1711 if (vHeights.empty()) 1718 if (vHeights.empty())
1712 return -1; 1719 return -1;
1713 1720
1721 nLastPeerHeightSeen = GetTime();
1714 sort(vHeights.begin(), vHeights.end()); 1722 sort(vHeights.begin(), vHeights.end());
1715 return vHeights[vHeights.size() / 2]; 1723 return vHeights[vHeights.size() / 2];
1716} 1724}
1717 1725
1726// Seconds since any peer last reported a height.
1727//
1728// Before ever hearing one, measure from the first time somebody asked rather
1729// than from the epoch -- otherwise a node refuses jobs the instant it starts,
1730// before it has had a chance to complete a single handshake.
1731int64 GetPeerHeightAgeSecs()
1732{
1733 int64 nNow = GetTime();
1734 if (nPeerHeightFirstAsked == 0)
1735 nPeerHeightFirstAsked = nNow;
1736 if (nLastPeerHeightSeen == 0)
1737 return nNow - nPeerHeightFirstAsked;
1738 return nNow - nLastPeerHeightSeen;
1739}
1740
1718 1741
1719// Say out loud when we are behind the network. 1742// Say out loud when we are behind the network.
1720// 1743//
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;
120// this long regardless of what is still buffered. 120// this long regardless of what is still buffered.
121static const int64 DISCONNECT_DRAIN_SECS = 60; 121static const int64 DISCONNECT_DRAIN_SECS = 60;
122 122
123// How long a pool may go without hearing a height from ANY peer before it
124// stops issuing jobs. Long enough that ordinary peer churn and a quiet
125// handshake window do not trip it; short enough that a deaf pool cannot build
126// more than a few blocks of private fork before it is stopped. Heights
127// 3966-4085 -- 120 blocks, 5890.50 BTF -- were mined in exactly this state.
128static const int64 BTF_NO_PEER_HEIGHT_MAX_SECS = 20 * 60;
129
123// Send a ping after this long with nothing to say, so a peer running the 130// Send a ping after this long with nothing to say, so a peer running the
124// inactivity check above does not mistake a quiet node for a dead one. Must 131// inactivity check above does not mistake a quiet node for a dead one. Must
125// stay well under BTF_RECV_TIMEOUT_SECS. 132// stay well under BTF_RECV_TIMEOUT_SECS.
@@ -160,6 +167,7 @@ extern int nBestHeight;
160// has told us anything. Median rather than maximum so one peer claiming an 167// has told us anything. Median rather than maximum so one peer claiming an
161// absurd height cannot move it. 168// absurd height cannot move it.
162int GetPeerMedianHeight(); 169int GetPeerMedianHeight();
170int64 GetPeerHeightAgeSecs();
163 171
164// Lightweight .btf churn accounting. These counters are diagnostics only: they 172// Lightweight .btf churn accounting. These counters are diagnostics only: they
165// let an operator tell stale descriptors, relay pairing failures, and paired 173// 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()
180 return false; 180 return false;
181 } 181 }
182 182
183 // The comparison above is blind in the one case that matters most.
184 //
185 // When the node goes deaf -- peers still in vNodes, nothing arriving --
186 // no peer announces anything, GetPeerMedianHeight() returns -1, and the
187 // fallback ("nobody has told us anything, so mine our best chain") let
188 // the pool build a private fork for as long as the deafness lasted.
189 // That fallback is right for a node that has simply not been told yet
190 // and wrong for a node that has stopped being able to hear, and a
191 // height nobody sent cannot tell those apart. Time can.
192 //
193 // Deliberately not behind a debug category, and deliberately not a
194 // silent refusal: an operator whose pool has stopped issuing jobs needs
195 // the reason in the log. /poolmineblind is the escape hatch for a node
196 // that really is alone -- without one, a genuinely isolated pool that is
197 // the majority of the hashrate could never restart its own chain, which
198 // is the deadlock the tip-age check used to cause.
199 if (nPeerHeight < 0 && !fPoolMineBlind) {
200 int64 nAge = GetPeerHeightAgeSecs();
201 if (nAge >= BTF_NO_PEER_HEIGHT_MAX_SECS) {
202 static int64 nLastSaid = 0;
203 if (GetTime() - nLastSaid > 60) {
204 nLastSaid = GetTime();
205 printf("RebuildJob: no peer has reported a height for %d minutes "
206 "-- not issuing jobs (our height %d, %d peers connected). "
207 "This node is deaf; mining now would fork it. "
208 "Pass /poolmineblind to override.\n",
209 (int)(nAge / 60), nBestHeight, (int)vNodes.size());
210 }
211 return false;
212 }
213 }
214
183 CBlockIndex* pindexPrev = pindexBest; 215 CBlockIndex* pindexPrev = pindexBest;
184 unsigned int nBits = GetNextWorkRequired(pindexPrev); 216 unsigned int nBits = GetNextWorkRequired(pindexPrev);
185 217