diff options
| author | Bitflash <btf01@proton.me> | 2026-08-01 12:23:31 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 12:23:31 -0300 |
| commit | 24ed292f0524f1bc77c317b60d7ecf265596b3dd (patch) | |
| tree | acdbccfca6bc0725751af8e653c16b7797db8a3f | |
| parent | 64b9507846846e468ebba7e00fb59971edceca3b (diff) | |
diag: let the node say what is wrong with it (#81)
* Bump to 1.2.11
One change, and it is the reason a node on Windows kept collecting peers it
could not hear.
FD_SETSIZE was never defined in this tree, so it defaulted to 64 and select()
could watch the listen socket plus 63 peers -- everything past that stayed
connected and was never read again. Windows FD_SET stops adding once its array
is full and says nothing, so there was no error to notice. Because new
connections go on the end of vNodes, it was always the newest connection that
starved, including the ones dialed to fix connectivity.
Measured on two nodes: 277 and 90 CNode objects against 93 and 90 live
sockets, 316 and 355 sockets in a bound-but-never-connected state, and a status
bar reporting hundreds of peers for a node really talking to thirty.
1.2.11 raises FD_SETSIZE to 1024 on Windows, caps connections at 125, drops a
node stuck undeliverable after 60 seconds instead of holding it forever, stops
answering a failed select() with POSIX code that has no meaning on Windows, and
warns when there are more peers than select can watch.
* diag: let the node say what is wrong with it
Every hard problem in this project so far was found from outside the node --
sockets read over WMI, committed memory compared against a README, a grep over
somebody debug.log. In each case the node knew and had no way to say so.
One report, rendered identically for the GUI panel and the log: peers held vs
peers actually watched by select(), blocks received and how many arrived without
a parent, proof-of-work mode with the live miner thread count, and per peer the
age of the last message in each direction plus anything stuck unsent.
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | src/gui.cpp | 38 | ||||
| -rw-r--r-- | src/main.cpp | 7 | ||||
| -rw-r--r-- | src/net.cpp | 117 | ||||
| -rw-r--r-- | src/net.h | 19 |
5 files changed, 182 insertions, 1 deletions
| @@ -8,7 +8,7 @@ | |||
| 8 | ROOT := $(shell pwd) | 8 | ROOT := $(shell pwd) |
| 9 | NPROC := $(shell nproc 2>/dev/null || echo 2) | 9 | NPROC := $(shell nproc 2>/dev/null || echo 2) |
| 10 | SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") | 10 | SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") |
| 11 | VERSION := 1.2.10 | 11 | VERSION := 1.2.11 |
| 12 | 12 | ||
| 13 | # ---- Linux ---------------------------------------------------------------- | 13 | # ---- Linux ---------------------------------------------------------------- |
| 14 | 14 | ||
diff --git a/src/gui.cpp b/src/gui.cpp index 53cb6db..db6b69e 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -50,6 +50,7 @@ static int64 g_lastWalletRefresh = 0; | |||
| 50 | static bool g_showSend = false; | 50 | static bool g_showSend = false; |
| 51 | static bool g_showOptions = false; | 51 | static bool g_showOptions = false; |
| 52 | static bool g_showAbout = false; | 52 | static bool g_showAbout = false; |
| 53 | static bool g_showDiagnostics = false; | ||
| 53 | static bool g_needRefresh = true; | 54 | static bool g_needRefresh = true; |
| 54 | 55 | ||
| 55 | static char g_sendAddr[128] = {}; | 56 | static char g_sendAddr[128] = {}; |
| @@ -310,6 +311,7 @@ static void DrawMainWindow() | |||
| 310 | ImGui::EndMenu(); | 311 | ImGui::EndMenu(); |
| 311 | } | 312 | } |
| 312 | if (ImGui::MenuItem("Options")) g_showOptions = true; | 313 | if (ImGui::MenuItem("Options")) g_showOptions = true; |
| 314 | if (ImGui::MenuItem("Diagnostics")) g_showDiagnostics = true; | ||
| 313 | if (ImGui::MenuItem("About")) g_showAbout = true; | 315 | if (ImGui::MenuItem("About")) g_showAbout = true; |
| 314 | ImGui::EndMenuBar(); | 316 | ImGui::EndMenuBar(); |
| 315 | } | 317 | } |
| @@ -815,6 +817,41 @@ static void DrawOptionsDialog() | |||
| 815 | // --------------------------------------------------------------------------- | 817 | // --------------------------------------------------------------------------- |
| 816 | // About dialog | 818 | // About dialog |
| 817 | // --------------------------------------------------------------------------- | 819 | // --------------------------------------------------------------------------- |
| 820 | // What the node knows about itself, in the words it writes to the log. Same | ||
| 821 | // text in both places on purpose: what a user pastes into an issue is then | ||
| 822 | // exactly what a developer has already read a hundred times. | ||
| 823 | static void DrawDiagnosticsDialog() | ||
| 824 | { | ||
| 825 | if (!g_showDiagnostics) return; | ||
| 826 | ImGui::SetNextWindowSize(ImVec2(720.0f, 460.0f), ImGuiCond_FirstUseEver); | ||
| 827 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), | ||
| 828 | ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); | ||
| 829 | if (ImGui::Begin("Diagnostics", &g_showDiagnostics, ImGuiWindowFlags_NoCollapse)) | ||
| 830 | { | ||
| 831 | // Rebuilt at most once a second: it walks every peer and takes their | ||
| 832 | // send locks, which is not something to do at frame rate. | ||
| 833 | static std::string strReport; | ||
| 834 | static int64 nLastBuilt = 0; | ||
| 835 | if (GetTime() != nLastBuilt) | ||
| 836 | { | ||
| 837 | nLastBuilt = GetTime(); | ||
| 838 | strReport = GetDiagnosticsText(); | ||
| 839 | } | ||
| 840 | |||
| 841 | if (ImGui::Button("Copy to clipboard", ImVec2(160.0f, 0.0f))) | ||
| 842 | ImGui::SetClipboardText(strReport.c_str()); | ||
| 843 | ImGui::SameLine(); | ||
| 844 | ImGui::TextDisabled("The same report goes to debug.log every ten minutes."); | ||
| 845 | ImGui::Separator(); | ||
| 846 | |||
| 847 | ImGui::BeginChild("##diagtext", ImVec2(0.0f, 0.0f), false, | ||
| 848 | ImGuiWindowFlags_HorizontalScrollbar); | ||
| 849 | ImGui::TextUnformatted(strReport.c_str()); | ||
| 850 | ImGui::EndChild(); | ||
| 851 | } | ||
| 852 | ImGui::End(); | ||
| 853 | } | ||
| 854 | |||
| 818 | static void DrawAboutDialog() | 855 | static void DrawAboutDialog() |
| 819 | { | 856 | { |
| 820 | if (!g_showAbout) return; | 857 | if (!g_showAbout) return; |
| @@ -921,6 +958,7 @@ int RunGUI(int argc, char* argv[]) | |||
| 921 | DrawMainWindow(); | 958 | DrawMainWindow(); |
| 922 | DrawSendDialog(); | 959 | DrawSendDialog(); |
| 923 | DrawOptionsDialog(); | 960 | DrawOptionsDialog(); |
| 961 | DrawDiagnosticsDialog(); | ||
| 924 | DrawAboutDialog(); | 962 | DrawAboutDialog(); |
| 925 | DrawStatusBar(); | 963 | DrawStatusBar(); |
| 926 | 964 | ||
diff --git a/src/main.cpp b/src/main.cpp index 5610de9..1a4c9ea 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -1494,6 +1494,11 @@ bool CBlock::AcceptBlock() | |||
| 1494 | 1494 | ||
| 1495 | bool ProcessBlock(CNode* pfrom, CBlock* pblock) | 1495 | bool ProcessBlock(CNode* pfrom, CBlock* pblock) |
| 1496 | { | 1496 | { |
| 1497 | // Counted before any check, so the ratio below is against everything that | ||
| 1498 | // arrived rather than everything that was any good. | ||
| 1499 | if (pfrom) | ||
| 1500 | nBlocksReceived++; | ||
| 1501 | |||
| 1497 | // Check for duplicate | 1502 | // Check for duplicate |
| 1498 | uint256 hash = pblock->GetHash(); | 1503 | uint256 hash = pblock->GetHash(); |
| 1499 | if (mapBlockIndex.count(hash)) | 1504 | if (mapBlockIndex.count(hash)) |
| @@ -1512,6 +1517,8 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock) | |||
| 1512 | if (!mapBlockIndex.count(pblock->hashPrevBlock)) | 1517 | if (!mapBlockIndex.count(pblock->hashPrevBlock)) |
| 1513 | { | 1518 | { |
| 1514 | printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0,14).c_str()); | 1519 | printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0,14).c_str()); |
| 1520 | if (pfrom) | ||
| 1521 | nBlocksWithoutParent++; | ||
| 1515 | mapOrphanBlocks.insert(make_pair(hash, pblock)); | 1522 | mapOrphanBlocks.insert(make_pair(hash, pblock)); |
| 1516 | mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock)); | 1523 | mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock)); |
| 1517 | 1524 | ||
diff --git a/src/net.cpp b/src/net.cpp index 5ef8316..19893ee 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -46,6 +46,96 @@ CCriticalSection cs_mapRelay; | |||
| 46 | map<CInv, int64> mapAlreadyAskedFor; | 46 | map<CInv, int64> mapAlreadyAskedFor; |
| 47 | string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) | 47 | string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) |
| 48 | 48 | ||
| 49 | int nPeersWatched = 0; | ||
| 50 | int64 nBlocksReceived = 0; | ||
| 51 | int64 nBlocksWithoutParent = 0; | ||
| 52 | static int64 nNodeStartTime = 0; | ||
| 53 | |||
| 54 | // Defined further down, next to the counter it reads. The miner keeps its own | ||
| 55 | // live count -- threads bump it on the way in and on the way out -- which is | ||
| 56 | // the honest number here: how many are hashing now, not how many were asked to. | ||
| 57 | static int MinersRunningCount(); | ||
| 58 | |||
| 59 | // Seconds rendered the way a person reads them off a screen. | ||
| 60 | static string FormatAge(int64 nSeconds) | ||
| 61 | { | ||
| 62 | if (nSeconds < 0) return "never"; | ||
| 63 | if (nSeconds < 60) return strprintf("%llds", (long long)nSeconds); | ||
| 64 | if (nSeconds < 3600) | ||
| 65 | return strprintf("%lldm%02llds", (long long)(nSeconds / 60), (long long)(nSeconds % 60)); | ||
| 66 | return strprintf("%lldh%02lldm", (long long)(nSeconds / 3600), (long long)((nSeconds % 3600) / 60)); | ||
| 67 | } | ||
| 68 | |||
| 69 | string GetDiagnosticsText() | ||
| 70 | { | ||
| 71 | int64 nNow = GetTime(); | ||
| 72 | string str; | ||
| 73 | |||
| 74 | int nInbound = 0, nHeld = 0; | ||
| 75 | vector<CNode*> vCopy; | ||
| 76 | CRITICAL_BLOCK(cs_vNodes) | ||
| 77 | { | ||
| 78 | vCopy = vNodes; | ||
| 79 | nHeld = (int)vNodes.size(); | ||
| 80 | foreach(CNode* pnode, vNodes) | ||
| 81 | if (pnode->fInbound) | ||
| 82 | nInbound++; | ||
| 83 | } | ||
| 84 | |||
| 85 | int nMedian = GetPeerMedianHeight(); | ||
| 86 | |||
| 87 | str += "Bitflash node diagnostics\n"; | ||
| 88 | str += strprintf(" uptime %s\n", | ||
| 89 | FormatAge(nNodeStartTime ? nNow - nNodeStartTime : -1).c_str()); | ||
| 90 | if (nMedian < 0) | ||
| 91 | str += strprintf(" height %d (no peer has said where it is)\n", nBestHeight); | ||
| 92 | else | ||
| 93 | str += strprintf(" height %d (peers report %d, %s)\n", | ||
| 94 | nBestHeight, nMedian, | ||
| 95 | nBestHeight >= nMedian ? "level or ahead" | ||
| 96 | : strprintf("behind by %d", nMedian - nBestHeight).c_str()); | ||
| 97 | str += strprintf(" peers held %d (%d inbound, %d outbound)\n", | ||
| 98 | nHeld, nInbound, nHeld - nInbound); | ||
| 99 | |||
| 100 | // The number that would have made the deafness obvious. Anything held but | ||
| 101 | // not watched is a socket this node will never read again. | ||
| 102 | str += strprintf(" peers watched %d of %d that select() can hold%s\n", | ||
| 103 | nPeersWatched, (int)FD_SETSIZE - 1, | ||
| 104 | nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); | ||
| 105 | |||
| 106 | if (nBlocksReceived > 0) | ||
| 107 | str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", | ||
| 108 | (long long)nBlocksReceived, (long long)nBlocksWithoutParent, | ||
| 109 | 100.0 * nBlocksWithoutParent / nBlocksReceived); | ||
| 110 | else | ||
| 111 | str += " blocks received 0\n"; | ||
| 112 | |||
| 113 | int nMining = MinersRunningCount(); | ||
| 114 | str += strprintf(" proof of work %s mode%s\n", | ||
| 115 | RandomXFastReady() ? "fast (2 GB dataset)" : "light (256 MB cache)", | ||
| 116 | nMining > 0 | ||
| 117 | ? strprintf(", mining on %d thread(s), about %d MB", | ||
| 118 | nMining, | ||
| 119 | (RandomXFastReady() ? 2080 : 256) + 2 * nMining).c_str() | ||
| 120 | : ", not mining"); | ||
| 121 | |||
| 122 | str += "\n peer dir height last recv last send unsent\n"; | ||
| 123 | foreach(CNode* pnode, vCopy) | ||
| 124 | { | ||
| 125 | int nSendSize = 0; | ||
| 126 | TRY_CRITICAL_BLOCK(pnode->cs_vSend) | ||
| 127 | nSendSize = (int)pnode->vSend.size(); | ||
| 128 | str += strprintf(" %-28s %-4s %6d %10s %10s %7d\n", | ||
| 129 | pnode->addr.ToString().substr(0, 28).c_str(), | ||
| 130 | pnode->fInbound ? "in" : "out", | ||
| 131 | pnode->nStartingHeight, | ||
| 132 | FormatAge(pnode->nLastRecv ? nNow - pnode->nLastRecv : -1).c_str(), | ||
| 133 | FormatAge(pnode->nLastSend ? nNow - pnode->nLastSend : -1).c_str(), | ||
| 134 | nSendSize); | ||
| 135 | } | ||
| 136 | return str; | ||
| 137 | } | ||
| 138 | |||
| 49 | 139 | ||
| 50 | bool GetMyExternalIP(unsigned int& ipRet) | 140 | bool GetMyExternalIP(unsigned int& ipRet) |
| 51 | { | 141 | { |
| @@ -1058,6 +1148,24 @@ void ThreadSocketHandler2(void* parg) | |||
| 1058 | (int)vNodes.size() - (int)FD_SETSIZE + 1); | 1148 | (int)vNodes.size() - (int)FD_SETSIZE + 1); |
| 1059 | } | 1149 | } |
| 1060 | } | 1150 | } |
| 1151 | nPeersWatched = (int)nWatched - 1; // less the listen socket | ||
| 1152 | } | ||
| 1153 | |||
| 1154 | // Say the state of the node out loud now and then, so a log pulled off | ||
| 1155 | // a machine three weeks later still answers "was it hearing anybody". | ||
| 1156 | { | ||
| 1157 | static int64 nLastReport = 0; | ||
| 1158 | // Wound forward on the first pass so the first report lands about a | ||
| 1159 | // minute in, once peers have had time to connect. A report written | ||
| 1160 | // at second zero says nothing, and waiting ten minutes for the | ||
| 1161 | // first one is ten minutes of a log that cannot answer anything. | ||
| 1162 | if (nLastReport == 0) | ||
| 1163 | nLastReport = GetTime() - 9 * 60; | ||
| 1164 | if (GetTime() - nLastReport > 10 * 60) | ||
| 1165 | { | ||
| 1166 | nLastReport = GetTime(); | ||
| 1167 | printf("%s\n", GetDiagnosticsText().c_str()); | ||
| 1168 | } | ||
| 1061 | } | 1169 | } |
| 1062 | 1170 | ||
| 1063 | vfThreadRunning[0] = false; | 1171 | vfThreadRunning[0] = false; |
| @@ -1432,6 +1540,14 @@ void ThreadMessageHandler2(void* parg) | |||
| 1432 | static CCriticalSection cs_nMinersRunning; | 1540 | static CCriticalSection cs_nMinersRunning; |
| 1433 | static int nMinersRunning = 0; | 1541 | static int nMinersRunning = 0; |
| 1434 | 1542 | ||
| 1543 | static int MinersRunningCount() | ||
| 1544 | { | ||
| 1545 | int n = 0; | ||
| 1546 | CRITICAL_BLOCK(cs_nMinersRunning) | ||
| 1547 | n = nMinersRunning; | ||
| 1548 | return n; | ||
| 1549 | } | ||
| 1550 | |||
| 1435 | void ThreadBitcoinMiner(void* parg) | 1551 | void ThreadBitcoinMiner(void* parg) |
| 1436 | { | 1552 | { |
| 1437 | int nThreadId = (int)(intptr_t)parg; | 1553 | int nThreadId = (int)(intptr_t)parg; |
| @@ -1490,6 +1606,7 @@ int StartMinerThreads() | |||
| 1490 | bool StartNode(string& strError) | 1606 | bool StartNode(string& strError) |
| 1491 | { | 1607 | { |
| 1492 | strError = ""; | 1608 | strError = ""; |
| 1609 | nNodeStartTime = GetTime(); | ||
| 1493 | 1610 | ||
| 1494 | // Sockets startup | 1611 | // Sockets startup |
| 1495 | WSADATA wsadata; | 1612 | WSADATA wsadata; |
| @@ -102,6 +102,25 @@ static const int BTF_PING_INTERVAL_SECS = 10 * 60; | |||
| 102 | static const int BTF_BEHIND_GRACE_SECS = 5 * 60; | 102 | static const int BTF_BEHIND_GRACE_SECS = 5 * 60; |
| 103 | static const int BTF_BEHIND_WARN_INTERVAL_SECS = 5 * 60; | 103 | static const int BTF_BEHIND_WARN_INTERVAL_SECS = 5 * 60; |
| 104 | 104 | ||
| 105 | // --- Self-diagnosis ------------------------------------------------------- | ||
| 106 | // | ||
| 107 | // A node that has stopped working looks exactly like a node with nothing to | ||
| 108 | // do. That has cost this project real time more than once: a node deaf to | ||
| 109 | // most of its peers, a miner allocating one RandomX dataset per thread, a | ||
| 110 | // third of arriving blocks missing their parent -- each was found from | ||
| 111 | // outside, by reading the process's sockets or its memory from another | ||
| 112 | // machine, because the node itself had no way to say so. | ||
| 113 | // | ||
| 114 | // These counters exist so it can. Cheap to keep, and they name the failures | ||
| 115 | // that actually happened rather than the ones that sound impressive. | ||
| 116 | extern int nPeersWatched; // peers in the last select() set | ||
| 117 | extern int64 nBlocksReceived; // blocks handed to ProcessBlock | ||
| 118 | extern int64 nBlocksWithoutParent; // ...of those, how many arrived orphaned | ||
| 119 | |||
| 120 | // One report, rendered the same way for the GUI panel and the log, so what a | ||
| 121 | // user pastes into an issue is what a developer already knows how to read. | ||
| 122 | string GetDiagnosticsText(); | ||
| 123 | |||
| 105 | // Defined in main.cpp. Declared here because CNode announces it in the version | 124 | // Defined in main.cpp. Declared here because CNode announces it in the version |
| 106 | // message, and net.h is included before main.h. | 125 | // message, and net.h is included before main.h. |
| 107 | extern int nBestHeight; | 126 | extern int nBestHeight; |