diff options
| author | Bitflash <btf01@proton.me> | 2026-07-30 18:22:51 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-30 18:22:51 -0300 |
| commit | 86f080cc6deb0464edf3913daa2e1131b5eed7c7 (patch) | |
| tree | 058f6aa96501191a76cd35ee1a5e7831ca3227ba /src | |
| parent | 773440fcc7752b85d99ee5b8500b64dbb5869603 (diff) | |
net: announce our height in the handshake, and say when we are behind (#56)
A node running this code cannot tell "in sync with everyone" from "not
hearing anyone". The version message carries no height -- 0.1.0 never had
the field and this fork inherited the gap -- so the only evidence of a
problem is a number on screen that stopped moving, which looks the same as
a network with nothing to report. It is the structural reason a broken node
here has always been indistinguishable from an idle one.
Append nBestHeight to the version message and keep what a peer announced in
CNode::nStartingHeight. The field goes last, so a node that predates it
stops reading after addr and treats the rest as trailing bytes, which cost
it one log line. Reading it is guarded by vRecv.empty() and never required:
peers that do not send it keep nStartingHeight at -1 and are left out of
the median. Every release so far sends the short form.
GetPeerMedianHeight() takes the median rather than the maximum so a single
peer claiming an absurd height cannot move it.
Then use it. After five minutes more than one block behind the peer median,
the node says so in the log -- not behind a debug category, because an
operator who already enabled -debug is not the one who needs telling -- and
the GUI carries the same sentence in its status area. "8 blocks behind for
27 minutes" is something a user can act on; a frozen counter is not.
Verified against two nodes on the live network: the one starting from an
empty datadir read its peer at 3797, reported 2636 then 1274 then 433
blocks behind as it caught up, and went quiet on its own once level. The
other logged "peer height = -1" for the older nodes it met on the real
network, which is the compatibility path doing its job.
Known limit: nStartingHeight is a snapshot from handshake time and goes
stale on a long-lived connection, so this catches a node that is receiving
something but not blocks, not one that has gone completely deaf. The
inactivity timeout from #55 covers that case and refreshes these heights
when it forces the reconnect.
Diffstat (limited to 'src')
| -rw-r--r-- | src/gui.cpp | 10 | ||||
| -rw-r--r-- | src/main.cpp | 8 | ||||
| -rw-r--r-- | src/net.cpp | 61 | ||||
| -rw-r--r-- | src/net.h | 42 |
4 files changed, 119 insertions, 2 deletions
diff --git a/src/gui.cpp b/src/gui.cpp index 825202b..28f5ffa 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -26,6 +26,7 @@ extern CAddress addrLocalHost; | |||
| 26 | 26 | ||
| 27 | void MainFrameRepaint(); | 27 | void MainFrameRepaint(); |
| 28 | int GetDiscoveredPeerCount(); | 28 | int GetDiscoveredPeerCount(); |
| 29 | int GetPeerMedianHeight(); | ||
| 29 | string DateTimeStr(int64 nTime); | 30 | string DateTimeStr(int64 nTime); |
| 30 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); | 31 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); |
| 31 | int64 GetBalance(); | 32 | int64 GetBalance(); |
| @@ -238,6 +239,15 @@ static void DrawStatusBar() | |||
| 238 | connected, discovered, nBestHeight); | 239 | connected, discovered, nBestHeight); |
| 239 | } | 240 | } |
| 240 | 241 | ||
| 242 | // A node that has stopped receiving looks exactly like one with nothing to | ||
| 243 | // do. Now that peers announce their height, say the difference out loud | ||
| 244 | // instead of leaving the user to guess from a number that stopped moving. | ||
| 245 | int peerHeight = GetPeerMedianHeight(); | ||
| 246 | if (peerHeight >= 0 && nBestHeight < peerHeight - 1) | ||
| 247 | ImGui::TextColored(ImVec4(0.95f, 0.65f, 0.20f, 1.0f), | ||
| 248 | "Behind the network by %d block(s) -- peers report height %d", | ||
| 249 | peerHeight - nBestHeight, peerHeight); | ||
| 250 | |||
| 241 | ImGui::End(); | 251 | ImGui::End(); |
| 242 | } | 252 | } |
| 243 | 253 | ||
diff --git a/src/main.cpp b/src/main.cpp index c77448d..15805ca 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -1884,6 +1884,12 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 1884 | if (pfrom->nVersion == 0) | 1884 | if (pfrom->nVersion == 0) |
| 1885 | return false; | 1885 | return false; |
| 1886 | 1886 | ||
| 1887 | // Optional: peers older than this field simply end the message here, | ||
| 1888 | // and nStartingHeight stays -1 for them. Never make this a requirement | ||
| 1889 | // -- every node released so far sends the short form. | ||
| 1890 | if (!vRecv.empty()) | ||
| 1891 | vRecv >> pfrom->nStartingHeight; | ||
| 1892 | |||
| 1887 | pfrom->vSend.SetVersion(min(pfrom->nVersion, VERSION)); | 1893 | pfrom->vSend.SetVersion(min(pfrom->nVersion, VERSION)); |
| 1888 | pfrom->vRecv.SetVersion(min(pfrom->nVersion, VERSION)); | 1894 | pfrom->vRecv.SetVersion(min(pfrom->nVersion, VERSION)); |
| 1889 | 1895 | ||
| @@ -1912,7 +1918,7 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 1912 | if (!vPexOut.empty()) | 1918 | if (!vPexOut.empty()) |
| 1913 | pfrom->PushMessage("btfpeers", vPexOut); | 1919 | pfrom->PushMessage("btfpeers", vPexOut); |
| 1914 | 1920 | ||
| 1915 | if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str()); | 1921 | if (LogAcceptsCategory("net")) printf("version addrMe = %s, peer height = %d\n", addrMe.ToString().c_str(), pfrom->nStartingHeight); |
| 1916 | } | 1922 | } |
| 1917 | 1923 | ||
| 1918 | 1924 | ||
diff --git a/src/net.cpp b/src/net.cpp index 81932d7..ac8df82 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -1225,6 +1225,65 @@ void ThreadMessageHandler(void* parg) | |||
| 1225 | } | 1225 | } |
| 1226 | } | 1226 | } |
| 1227 | 1227 | ||
| 1228 | int GetPeerMedianHeight() | ||
| 1229 | { | ||
| 1230 | vector<int> vHeights; | ||
| 1231 | CRITICAL_BLOCK(cs_vNodes) | ||
| 1232 | foreach(CNode* pnode, vNodes) | ||
| 1233 | if (pnode->nStartingHeight >= 0) | ||
| 1234 | vHeights.push_back(pnode->nStartingHeight); | ||
| 1235 | |||
| 1236 | if (vHeights.empty()) | ||
| 1237 | return -1; | ||
| 1238 | |||
| 1239 | sort(vHeights.begin(), vHeights.end()); | ||
| 1240 | return vHeights[vHeights.size() / 2]; | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | |||
| 1244 | // Say out loud when we are behind the network. | ||
| 1245 | // | ||
| 1246 | // This is the whole reason the height is in the handshake. Every serious bug | ||
| 1247 | // this node has had looked identical from the outside: running, threads alive, | ||
| 1248 | // nothing in the log, and no blocks arriving. A node that knows where everyone | ||
| 1249 | // else is can say so, and "8 blocks behind for 27 minutes" is a sentence a | ||
| 1250 | // user can act on. Deliberately not behind a debug category -- an operator who | ||
| 1251 | // already suspects trouble is not the one who needs telling. | ||
| 1252 | static void WarnIfBehind() | ||
| 1253 | { | ||
| 1254 | static int64 nLastWarned; | ||
| 1255 | static int64 nBehindSince; | ||
| 1256 | |||
| 1257 | int nPeers = GetPeerMedianHeight(); | ||
| 1258 | if (nPeers < 0 || nBestHeight < 0) | ||
| 1259 | return; | ||
| 1260 | |||
| 1261 | // One block of slack: somebody is always mid-relay. | ||
| 1262 | if (nBestHeight >= nPeers - 1) | ||
| 1263 | { | ||
| 1264 | nBehindSince = 0; | ||
| 1265 | return; | ||
| 1266 | } | ||
| 1267 | |||
| 1268 | int64 nNow = GetTime(); | ||
| 1269 | if (nBehindSince == 0) | ||
| 1270 | { | ||
| 1271 | nBehindSince = nNow; | ||
| 1272 | return; | ||
| 1273 | } | ||
| 1274 | |||
| 1275 | // Falling briefly behind is ordinary. Staying behind is not. | ||
| 1276 | if (nNow - nBehindSince < BTF_BEHIND_GRACE_SECS) | ||
| 1277 | return; | ||
| 1278 | if (nNow - nLastWarned < BTF_BEHIND_WARN_INTERVAL_SECS) | ||
| 1279 | return; | ||
| 1280 | |||
| 1281 | nLastWarned = nNow; | ||
| 1282 | printf("WARNING: %d blocks behind the network (height %d, peers report %d) for %d minutes\n", | ||
| 1283 | nPeers - nBestHeight, nBestHeight, nPeers, (int)((nNow - nBehindSince) / 60)); | ||
| 1284 | } | ||
| 1285 | |||
| 1286 | |||
| 1228 | void ThreadMessageHandler2(void* parg) | 1287 | void ThreadMessageHandler2(void* parg) |
| 1229 | { | 1288 | { |
| 1230 | printf("ThreadMessageHandler started\n"); | 1289 | printf("ThreadMessageHandler started\n"); |
| @@ -1250,6 +1309,8 @@ void ThreadMessageHandler2(void* parg) | |||
| 1250 | pnode->Release(); | 1309 | pnode->Release(); |
| 1251 | } | 1310 | } |
| 1252 | 1311 | ||
| 1312 | WarnIfBehind(); | ||
| 1313 | |||
| 1253 | // Wait and allow messages to bunch up | 1314 | // Wait and allow messages to bunch up |
| 1254 | vfThreadRunning[2] = false; | 1315 | vfThreadRunning[2] = false; |
| 1255 | Sleep(100); | 1316 | Sleep(100); |
| @@ -69,6 +69,21 @@ static const int BTF_HANDSHAKE_GRACE_SECS = 60; | |||
| 69 | // stay well under BTF_RECV_TIMEOUT_SECS. | 69 | // stay well under BTF_RECV_TIMEOUT_SECS. |
| 70 | static const int BTF_PING_INTERVAL_SECS = 10 * 60; | 70 | static const int BTF_PING_INTERVAL_SECS = 10 * 60; |
| 71 | 71 | ||
| 72 | // How long a node must stay behind the network before it says so, and how | ||
| 73 | // often it repeats itself. Five minutes is two and a half block intervals -- | ||
| 74 | // long enough that ordinary relay lag never trips it. | ||
| 75 | static const int BTF_BEHIND_GRACE_SECS = 5 * 60; | ||
| 76 | static const int BTF_BEHIND_WARN_INTERVAL_SECS = 5 * 60; | ||
| 77 | |||
| 78 | // Defined in main.cpp. Declared here because CNode announces it in the version | ||
| 79 | // message, and net.h is included before main.h. | ||
| 80 | extern int nBestHeight; | ||
| 81 | |||
| 82 | // Median height announced by the peers we are connected to, or -1 while nobody | ||
| 83 | // has told us anything. Median rather than maximum so one peer claiming an | ||
| 84 | // absurd height cannot move it. | ||
| 85 | int GetPeerMedianHeight(); | ||
| 86 | |||
| 72 | // Descriptors to hand a peer: ours first, then peers that answered us. | 87 | // Descriptors to hand a peer: ours first, then peers that answered us. |
| 73 | void BtfPexCollect(std::vector<std::string>& vDescOut); | 88 | void BtfPexCollect(std::vector<std::string>& vDescOut); |
| 74 | // Verify descriptors a peer sent and remember the good ones. Returns how many. | 89 | // Verify descriptors a peer sent and remember the good ones. Returns how many. |
| @@ -528,6 +543,11 @@ public: | |||
| 528 | int64 nLastRecv; | 543 | int64 nLastRecv; |
| 529 | int64 nLastSendEmpty; | 544 | int64 nLastSendEmpty; |
| 530 | 545 | ||
| 546 | // Height this peer announced in its version message, or -1 if it sent a | ||
| 547 | // version message that predates the field. Knowing how far along everyone | ||
| 548 | // else is, is the only way a node can tell "in sync" from "not hearing". | ||
| 549 | int nStartingHeight; | ||
| 550 | |||
| 531 | 551 | ||
| 532 | CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false) | 552 | CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false) |
| 533 | { | 553 | { |
| @@ -549,12 +569,16 @@ public: | |||
| 549 | nLastSend = 0; | 569 | nLastSend = 0; |
| 550 | nLastRecv = 0; | 570 | nLastRecv = 0; |
| 551 | nLastSendEmpty = GetTime(); | 571 | nLastSendEmpty = GetTime(); |
| 572 | nStartingHeight = -1; | ||
| 552 | vfSubscribe.assign(256, false); | 573 | vfSubscribe.assign(256, false); |
| 553 | 574 | ||
| 554 | // Push a version message | 575 | // Push a version message |
| 555 | /// when NTP implemented, change to just nTime = GetAdjustedTime() | 576 | /// when NTP implemented, change to just nTime = GetAdjustedTime() |
| 556 | int64 nTime = (fInbound ? GetAdjustedTime() : GetTime()); | 577 | int64 nTime = (fInbound ? GetAdjustedTime() : GetTime()); |
| 557 | PushMessage("version", VERSION, nLocalServices, nTime, addr); | 578 | // nBestHeight goes last so a node that predates it stops reading after |
| 579 | // addr and treats the rest as trailing bytes, which cost it a log line | ||
| 580 | // and nothing else. | ||
| 581 | PushMessage("version", VERSION, nLocalServices, nTime, addr, nBestHeight); | ||
| 558 | } | 582 | } |
| 559 | 583 | ||
| 560 | ~CNode() | 584 | ~CNode() |
| @@ -771,6 +795,22 @@ public: | |||
| 771 | } | 795 | } |
| 772 | } | 796 | } |
| 773 | 797 | ||
| 798 | template<typename T1, typename T2, typename T3, typename T4, typename T5> | ||
| 799 | void PushMessage(const char* pszCommand, const T1& a1, const T2& a2, const T3& a3, const T4& a4, const T5& a5) | ||
| 800 | { | ||
| 801 | try | ||
| 802 | { | ||
| 803 | BeginMessage(pszCommand); | ||
| 804 | vSend << a1 << a2 << a3 << a4 << a5; | ||
| 805 | EndMessage(); | ||
| 806 | } | ||
| 807 | catch (...) | ||
| 808 | { | ||
| 809 | AbortMessage(); | ||
| 810 | throw; | ||
| 811 | } | ||
| 812 | } | ||
| 813 | |||
| 774 | 814 | ||
| 775 | void PushRequest(const char* pszCommand, | 815 | void PushRequest(const char* pszCommand, |
| 776 | void (*fn)(void*, CDataStream&), void* param1) | 816 | void (*fn)(void*, CDataStream&), void* param1) |