diff options
| author | Bitflash <btf01@proton.me> | 2026-07-31 17:40:58 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-31 17:40:58 -0300 |
| commit | d5bcd347cd8d156e3a6d4526fd2470741194a64a (patch) | |
| tree | 41e0820126fb7a0f1daebc8130054c64a00b9faf | |
| parent | 795e51612bd5b17dbbf8f0cbea64ee4c874b5a33 (diff) | |
net: a node could only ever watch 63 peers (#78)
| -rw-r--r-- | bitflash-node-1.2.10-x86_64 | bin | 0 -> 2602056 bytes | |||
| -rw-r--r-- | bitflash-node-1.2.9-x86_64 | bin | 0 -> 2593864 bytes | |||
| -rw-r--r-- | src/makefile.mingw | 7 | ||||
| -rw-r--r-- | src/net.cpp | 96 | ||||
| -rw-r--r-- | src/net.h | 26 |
5 files changed, 120 insertions, 9 deletions
diff --git a/bitflash-node-1.2.10-x86_64 b/bitflash-node-1.2.10-x86_64 new file mode 100644 index 0000000..a279608 --- /dev/null +++ b/bitflash-node-1.2.10-x86_64 | |||
| Binary files differ | |||
diff --git a/bitflash-node-1.2.9-x86_64 b/bitflash-node-1.2.9-x86_64 new file mode 100644 index 0000000..561bc22 --- /dev/null +++ b/bitflash-node-1.2.9-x86_64 | |||
| Binary files differ | |||
diff --git a/src/makefile.mingw b/src/makefile.mingw index 01f7702..ae533ef 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw | |||
| @@ -14,8 +14,13 @@ WARNFLAGS = -Wformat -Werror=format \ | |||
| 14 | -Wno-deprecated-declarations -Wno-write-strings \ | 14 | -Wno-deprecated-declarations -Wno-write-strings \ |
| 15 | -Wno-narrowing -Wno-unused-result | 15 | -Wno-narrowing -Wno-unused-result |
| 16 | 16 | ||
| 17 | # FD_SETSIZE has to be defined before winsock2.h and it has to be the same | ||
| 18 | # number in every translation unit -- fd_set carries an array of that length, | ||
| 19 | # so disagreeing about it means disagreeing about sizeof(fd_set). That is why | ||
| 20 | # it lives here as a -D and not as a #define in a header. Windows defaults to | ||
| 21 | # 64, which capped this node at 63 watchable peers no matter how many it had. | ||
| 17 | CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ | 22 | CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ |
| 18 | -DWIN32 -DNOPCH \ | 23 | -DWIN32 -DNOPCH -DFD_SETSIZE=1024 \ |
| 19 | -DSECP256K1_STATIC \ | 24 | -DSECP256K1_STATIC \ |
| 20 | -I$(DEPS)/include -I$(RANDOMX)/src \ | 25 | -I$(DEPS)/include -I$(RANDOMX)/src \ |
| 21 | -I$(DEPS)/include/nlohmann \ | 26 | -I$(DEPS)/include/nlohmann \ |
diff --git a/src/net.cpp b/src/net.cpp index 37c081f..5ef8316 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -954,8 +954,27 @@ void ThreadSocketHandler2(void* parg) | |||
| 954 | vector<CNode*> vNodesCopy = vNodes; | 954 | vector<CNode*> vNodesCopy = vNodes; |
| 955 | foreach(CNode* pnode, vNodesCopy) | 955 | foreach(CNode* pnode, vNodesCopy) |
| 956 | { | 956 | { |
| 957 | if (pnode->ReadyToDisconnect() && pnode->vRecv.empty() && pnode->vSend.empty()) | 957 | // Wait for the buffers to drain before letting a node go, so a |
| 958 | // last message still gets out -- but not forever. vSend never | ||
| 959 | // drains through a socket whose far end is gone, and a node | ||
| 960 | // stuck that way stayed in vNodes for the life of the process, | ||
| 961 | // holding its socket and counting as a peer. | ||
| 962 | if (!pnode->ReadyToDisconnect()) | ||
| 963 | pnode->nDisconnectSince = 0; | ||
| 964 | else if (pnode->nDisconnectSince == 0) | ||
| 965 | pnode->nDisconnectSince = GetTime(); | ||
| 966 | |||
| 967 | bool fDrained = pnode->vRecv.empty() && pnode->vSend.empty(); | ||
| 968 | bool fStuck = pnode->nDisconnectSince != 0 && | ||
| 969 | GetTime() - pnode->nDisconnectSince > DISCONNECT_DRAIN_SECS; | ||
| 970 | |||
| 971 | if (pnode->ReadyToDisconnect() && (fDrained || fStuck)) | ||
| 958 | { | 972 | { |
| 973 | if (!fDrained) | ||
| 974 | printf("dropping stuck node %s after %d s with %d bytes unsent\n", | ||
| 975 | pnode->addr.ToString().c_str(), | ||
| 976 | (int)(GetTime() - pnode->nDisconnectSince), | ||
| 977 | (int)pnode->vSend.size()); | ||
| 959 | // remove from vNodes | 978 | // remove from vNodes |
| 960 | vNodes.erase(remove(vNodes.begin(), vNodes.end(), pnode), vNodes.end()); | 979 | vNodes.erase(remove(vNodes.begin(), vNodes.end(), pnode), vNodes.end()); |
| 961 | pnode->Disconnect(); | 980 | pnode->Disconnect(); |
| @@ -1010,16 +1029,35 @@ void ThreadSocketHandler2(void* parg) | |||
| 1010 | SOCKET hSocketMax = 0; | 1029 | SOCKET hSocketMax = 0; |
| 1011 | FD_SET(hListenSocket, &fdsetRecv); | 1030 | FD_SET(hListenSocket, &fdsetRecv); |
| 1012 | hSocketMax = max(hSocketMax, hListenSocket); | 1031 | hSocketMax = max(hSocketMax, hListenSocket); |
| 1032 | // FD_SET drops silently once the set is full, so count what goes in | ||
| 1033 | // and say so. A peer that never makes it into the set is never read, | ||
| 1034 | // and that is indistinguishable from a peer with nothing to say -- | ||
| 1035 | // the failure this whole path used to hide. | ||
| 1036 | unsigned int nWatched = 1; // the listen socket | ||
| 1013 | CRITICAL_BLOCK(cs_vNodes) | 1037 | CRITICAL_BLOCK(cs_vNodes) |
| 1014 | { | 1038 | { |
| 1015 | foreach(CNode* pnode, vNodes) | 1039 | foreach(CNode* pnode, vNodes) |
| 1016 | { | 1040 | { |
| 1041 | if (nWatched >= FD_SETSIZE) | ||
| 1042 | break; | ||
| 1017 | FD_SET(pnode->hSocket, &fdsetRecv); | 1043 | FD_SET(pnode->hSocket, &fdsetRecv); |
| 1044 | nWatched++; | ||
| 1018 | hSocketMax = max(hSocketMax, pnode->hSocket); | 1045 | hSocketMax = max(hSocketMax, pnode->hSocket); |
| 1019 | TRY_CRITICAL_BLOCK(pnode->cs_vSend) | 1046 | TRY_CRITICAL_BLOCK(pnode->cs_vSend) |
| 1020 | if (!pnode->vSend.empty()) | 1047 | if (!pnode->vSend.empty()) |
| 1021 | FD_SET(pnode->hSocket, &fdsetSend); | 1048 | FD_SET(pnode->hSocket, &fdsetSend); |
| 1022 | } | 1049 | } |
| 1050 | if (vNodes.size() + 1 > (size_t)FD_SETSIZE) | ||
| 1051 | { | ||
| 1052 | static int64 nLastWarned = 0; | ||
| 1053 | if (GetTime() - nLastWarned > 60) | ||
| 1054 | { | ||
| 1055 | nLastWarned = GetTime(); | ||
| 1056 | printf("WARNING: %d peers but select() can only watch %d -- %d are not being read\n", | ||
| 1057 | (int)vNodes.size(), (int)FD_SETSIZE - 1, | ||
| 1058 | (int)vNodes.size() - (int)FD_SETSIZE + 1); | ||
| 1059 | } | ||
| 1060 | } | ||
| 1023 | } | 1061 | } |
| 1024 | 1062 | ||
| 1025 | vfThreadRunning[0] = false; | 1063 | vfThreadRunning[0] = false; |
| @@ -1028,12 +1066,37 @@ void ThreadSocketHandler2(void* parg) | |||
| 1028 | CheckForShutdown(0); | 1066 | CheckForShutdown(0); |
| 1029 | if (nSelect == SOCKET_ERROR) | 1067 | if (nSelect == SOCKET_ERROR) |
| 1030 | { | 1068 | { |
| 1069 | // This used to answer a failed select() by marking every | ||
| 1070 | // descriptor from 0 to hSocketMax ready, which is code written for | ||
| 1071 | // POSIX, where a descriptor is a small integer. A Windows SOCKET is | ||
| 1072 | // a kernel handle in the thousands, so the loop iterated over | ||
| 1073 | // numbers that are not sockets and told the rest of the function | ||
| 1074 | // that every peer had data. One bad handle in the set makes | ||
| 1075 | // select() fail on every pass, so a single closed socket put the | ||
| 1076 | // whole thread into that state permanently. | ||
| 1077 | // | ||
| 1078 | // Find the bad handle instead and let it be dropped. | ||
| 1031 | int nErr = WSAGetLastError(); | 1079 | int nErr = WSAGetLastError(); |
| 1032 | LogPrint("net", "select failed: %d\n", nErr); | 1080 | LogPrint("net", "select failed: %d\n", nErr); |
| 1033 | for (int i = 0; i <= hSocketMax; i++) | 1081 | FD_ZERO(&fdsetRecv); |
| 1082 | FD_ZERO(&fdsetSend); | ||
| 1083 | CRITICAL_BLOCK(cs_vNodes) | ||
| 1034 | { | 1084 | { |
| 1035 | FD_SET(i, &fdsetRecv); | 1085 | foreach(CNode* pnode, vNodes) |
| 1036 | FD_SET(i, &fdsetSend); | 1086 | { |
| 1087 | int nType = 0; | ||
| 1088 | #ifdef _WIN32 | ||
| 1089 | int nTypeLen = sizeof(nType); | ||
| 1090 | #else | ||
| 1091 | socklen_t nTypeLen = sizeof(nType); | ||
| 1092 | #endif | ||
| 1093 | if (getsockopt(pnode->hSocket, SOL_SOCKET, SO_TYPE, (char*)&nType, &nTypeLen) != 0) | ||
| 1094 | { | ||
| 1095 | printf("dropping node %s: its socket is no longer valid\n", | ||
| 1096 | pnode->addr.ToString().c_str()); | ||
| 1097 | pnode->fDisconnect = true; | ||
| 1098 | } | ||
| 1099 | } | ||
| 1037 | } | 1100 | } |
| 1038 | Sleep(timeout.tv_usec/1000); | 1101 | Sleep(timeout.tv_usec/1000); |
| 1039 | } | 1102 | } |
| @@ -1068,11 +1131,28 @@ void ThreadSocketHandler2(void* parg) | |||
| 1068 | } | 1131 | } |
| 1069 | else | 1132 | else |
| 1070 | { | 1133 | { |
| 1071 | LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); | 1134 | // Refuse rather than accept a connection this node cannot |
| 1072 | CNode* pnode = new CNode(hSocket, addr, true); | 1135 | // watch. Without this the listen socket kept taking peers -- |
| 1073 | pnode->AddRef(); | 1136 | // it is always in the select set -- while everything past the |
| 1137 | // limit sat open and unread, which fed on itself: the deafer | ||
| 1138 | // the node got, the more connections it collected. | ||
| 1139 | unsigned int nNodes = 0; | ||
| 1074 | CRITICAL_BLOCK(cs_vNodes) | 1140 | CRITICAL_BLOCK(cs_vNodes) |
| 1075 | vNodes.push_back(pnode); | 1141 | nNodes = (unsigned int)vNodes.size(); |
| 1142 | if (nNodes >= MAX_CONNECTIONS) | ||
| 1143 | { | ||
| 1144 | LogPrint("net", "refusing connection from %s, already at %u\n", | ||
| 1145 | addr.ToString().c_str(), nNodes); | ||
| 1146 | closesocket(hSocket); | ||
| 1147 | } | ||
| 1148 | else | ||
| 1149 | { | ||
| 1150 | LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); | ||
| 1151 | CNode* pnode = new CNode(hSocket, addr, true); | ||
| 1152 | pnode->AddRef(); | ||
| 1153 | CRITICAL_BLOCK(cs_vNodes) | ||
| 1154 | vNodes.push_back(pnode); | ||
| 1155 | } | ||
| 1076 | } | 1156 | } |
| 1077 | } | 1157 | } |
| 1078 | 1158 | ||
| @@ -67,6 +67,30 @@ static const int BTF_RECV_TIMEOUT_SECS = 30 * 60; | |||
| 67 | static const int BTF_SEND_STALL_SECS = 10 * 60; | 67 | static const int BTF_SEND_STALL_SECS = 10 * 60; |
| 68 | static const int BTF_HANDSHAKE_GRACE_SECS = 60; | 68 | static const int BTF_HANDSHAKE_GRACE_SECS = 60; |
| 69 | 69 | ||
| 70 | // Ceiling on simultaneous connections. | ||
| 71 | // | ||
| 72 | // ThreadSocketHandler watches every peer through one select(), and select() | ||
| 73 | // cannot watch more descriptors than FD_SETSIZE. On Windows FD_SET simply | ||
| 74 | // stops adding once the set is full -- no error, no log -- so every socket | ||
| 75 | // past the limit stays open and is never read again. Nothing capped vNodes, | ||
| 76 | // so a node accepted connections it had no way to service, and because new | ||
| 77 | // entries go on the end of vNodes it was always the newest connection that | ||
| 78 | // starved, including the ones dialed to fix connectivity. | ||
| 79 | // | ||
| 80 | // Measured on two nodes before this ceiling existed: 277 and 90 CNode objects | ||
| 81 | // against 93 and 90 live sockets, and a status bar reporting hundreds of | ||
| 82 | // peers for a node that was really talking to thirty. | ||
| 83 | // | ||
| 84 | // 125 is Bitcoin's number and sits far below FD_SETSIZE on both platforms. | ||
| 85 | static const unsigned int MAX_CONNECTIONS = 125; | ||
| 86 | |||
| 87 | // A node marked for disconnect is normally held until its buffers drain, so | ||
| 88 | // a last message still goes out. But vSend cannot drain through a socket | ||
| 89 | // whose far end is gone, and that made such a node immortal: it stayed in | ||
| 90 | // vNodes, kept its socket open, and kept being counted. Force it out after | ||
| 91 | // this long regardless of what is still buffered. | ||
| 92 | static const int64 DISCONNECT_DRAIN_SECS = 60; | ||
| 93 | |||
| 70 | // Send a ping after this long with nothing to say, so a peer running the | 94 | // Send a ping after this long with nothing to say, so a peer running the |
| 71 | // inactivity check above does not mistake a quiet node for a dead one. Must | 95 | // inactivity check above does not mistake a quiet node for a dead one. Must |
| 72 | // stay well under BTF_RECV_TIMEOUT_SECS. | 96 | // stay well under BTF_RECV_TIMEOUT_SECS. |
| @@ -520,6 +544,7 @@ protected: | |||
| 520 | int nRefCount; | 544 | int nRefCount; |
| 521 | public: | 545 | public: |
| 522 | int64 nReleaseTime; | 546 | int64 nReleaseTime; |
| 547 | int64 nDisconnectSince; | ||
| 523 | map<uint256, CRequestTracker> mapRequests; | 548 | map<uint256, CRequestTracker> mapRequests; |
| 524 | CCriticalSection cs_mapRequests; | 549 | CCriticalSection cs_mapRequests; |
| 525 | 550 | ||
| @@ -572,6 +597,7 @@ public: | |||
| 572 | fDisconnect = false; | 597 | fDisconnect = false; |
| 573 | nRefCount = 0; | 598 | nRefCount = 0; |
| 574 | nReleaseTime = 0; | 599 | nReleaseTime = 0; |
| 600 | nDisconnectSince = 0; | ||
| 575 | nLastPexRecv = 0; | 601 | nLastPexRecv = 0; |
| 576 | pindexLastGetBlocksBegin = NULL; | 602 | pindexLastGetBlocksBegin = NULL; |
| 577 | hashLastGetBlocksEnd = 0; | 603 | hashLastGetBlocksEnd = 0; |