upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-30 15:41:26 -0300
committerGitHub <noreply@github.com>2026-07-30 15:41:26 -0300
commit10f248876aaa77e840dcb0d9585aba2fde712130 (patch)
tree0591b8a78a47345e97720090c231b2a31f7bce49 /src
parent88a1e9973e34c972e965fe5dd16cd1f891885bba (diff)
net: notice a peer that has gone quiet, and stay audible to peers that check (#55)
A connection whose path dies silently -- NAT drops an idle mapping, a relay restarts, a route changes -- never delivers FIN. Nothing becomes readable, no error is raised, and the node simply stops hearing from that peer while the socket looks perfectly healthy. #51 bounded this for the rendezvous registration; the connection that comes out of the rendezvous had the same hole, and so did every ordinary peer. Stamp nLastRecv and nLastSend on the socket path and act on them: - nothing at all within 60s of connecting, disconnect. In testing this fired twice per run against dead .btf descriptors handed out by the relay, which is where the ghosts have been coming from. - queued to send for 10 minutes with nothing going out, disconnect. - nothing received for 30 minutes, disconnect. A healthy peer relays a block inventory about every two minutes, so this is fifteen intervals of silence. Bitcoin used ninety minutes against ten-minute blocks. Disconnecting is the whole cure: reconnection already works, and a peer we cannot hear is worth what no peer is worth. A node with nothing to relay would now look dead to a peer applying the same rule, so send "ping" after ten minutes of having said nothing. It carries no payload and expects no answer -- arriving is the entire content, because the receive path has already stamped nLastRecv by the time it is parsed. Requiring an answer would have read every node predating this change as dead, since those ignore unknown commands rather than replying. Verified on the live network: synced 0 to 3790 with five peers and no spurious disconnects, then again with the interval cut to 20s, which put 54 pings on the wire with no peer dropping us and no exceptions.
Diffstat (limited to 'src')
-rw-r--r--src/main.cpp23
-rw-r--r--src/net.cpp43
-rw-r--r--src/net.h32
3 files changed, 97 insertions, 1 deletions
diff --git a/src/main.cpp b/src/main.cpp
index d4fc6d0..f4e6ce2 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -2190,6 +2190,18 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
2190 } 2190 }
2191 2191
2192 2192
2193 else if (strCommand == "ping")
2194 {
2195 // Nothing to do and nothing to answer. Arriving at all is the entire
2196 // content of the message: the receive path has already stamped
2197 // nLastRecv, which is what the sender wanted us to notice.
2198 //
2199 // Deliberately no reply. A node old enough not to know "ping" ignores
2200 // it as an unknown command and would never answer one, so a scheme
2201 // that required an answer would read every such peer as dead.
2202 }
2203
2204
2193 else 2205 else
2194 { 2206 {
2195 // Ignore unknown commands for extensibility 2207 // Ignore unknown commands for extensibility
@@ -2222,6 +2234,17 @@ bool SendMessages(CNode* pto)
2222 2234
2223 2235
2224 // 2236 //
2237 // Message: keep-alive ping
2238 //
2239 // Only when we have nothing else queued -- any real message serves the
2240 // same purpose. A peer with nothing to relay is otherwise silent, and
2241 // silence is exactly what the inactivity check disconnects on.
2242 //
2243 if (pto->nLastSend && GetTime() - pto->nLastSend > BTF_PING_INTERVAL_SECS && pto->vSend.empty())
2244 pto->PushMessage("ping");
2245
2246
2247 //
2225 // Message: inventory 2248 // Message: inventory
2226 // 2249 //
2227 vector<CInv> vInventoryToSend; 2250 vector<CInv> vInventoryToSend;
diff --git a/src/net.cpp b/src/net.cpp
index 9ee056f..81932d7 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -1092,7 +1092,11 @@ void ThreadSocketHandler2(void* parg)
1092 vRecv.resize(nPos + nBufSize); 1092 vRecv.resize(nPos + nBufSize);
1093 int nBytes = recv(hSocket, &vRecv[nPos], nBufSize, 0); 1093 int nBytes = recv(hSocket, &vRecv[nPos], nBufSize, 0);
1094 vRecv.resize(nPos + max(nBytes, 0)); 1094 vRecv.resize(nPos + max(nBytes, 0));
1095 if (nBytes == 0) 1095 if (nBytes > 0)
1096 {
1097 pnode->nLastRecv = GetTime();
1098 }
1099 else if (nBytes == 0)
1096 { 1100 {
1097 // socket closed gracefully 1101 // socket closed gracefully
1098 if (!pnode->fDisconnect) 1102 if (!pnode->fDisconnect)
@@ -1127,6 +1131,7 @@ void ThreadSocketHandler2(void* parg)
1127 if (nBytes > 0) 1131 if (nBytes > 0)
1128 { 1132 {
1129 vSend.erase(vSend.begin(), vSend.begin() + nBytes); 1133 vSend.erase(vSend.begin(), vSend.begin() + nBytes);
1134 pnode->nLastSend = GetTime();
1130 } 1135 }
1131 else if (nBytes == 0) 1136 else if (nBytes == 0)
1132 { 1137 {
@@ -1140,6 +1145,42 @@ void ThreadSocketHandler2(void* parg)
1140 pnode->vSend.clear(); 1145 pnode->vSend.clear();
1141 } 1146 }
1142 } 1147 }
1148 if (vSend.empty())
1149 pnode->nLastSendEmpty = GetTime();
1150 }
1151 }
1152
1153 //
1154 // Inactivity
1155 //
1156 // Dropping the connection is the whole cure: everything above this
1157 // layer already knows how to reconnect, and a peer we cannot hear
1158 // is worth exactly as much as no peer at all. Give a new
1159 // connection a grace period first, or we would cut off peers that
1160 // are still completing the rendezvous handshake.
1161 //
1162 if (GetTime() - pnode->nTimeConnected > BTF_HANDSHAKE_GRACE_SECS)
1163 {
1164 if (pnode->nLastRecv == 0 || pnode->nLastSend == 0)
1165 {
1166 LogPrint("net", "socket no message in first %d seconds, recv=%d send=%d\n",
1167 BTF_HANDSHAKE_GRACE_SECS, pnode->nLastRecv != 0, pnode->nLastSend != 0);
1168 pnode->fDisconnect = true;
1169 }
1170 else if (GetTime() - pnode->nLastSend > BTF_SEND_STALL_SECS &&
1171 GetTime() - pnode->nLastSendEmpty > BTF_SEND_STALL_SECS)
1172 {
1173 // We have had something queued to send for this long and
1174 // none of it has gone out: the socket accepts no more.
1175 LogPrint("net", "socket not sending\n");
1176 pnode->fDisconnect = true;
1177 }
1178 else if (GetTime() - pnode->nLastRecv > BTF_RECV_TIMEOUT_SECS)
1179 {
1180 // The deaf case. Nothing has arrived for many block
1181 // intervals while the socket still looks perfectly fine.
1182 LogPrint("net", "socket inactivity timeout\n");
1183 pnode->fDisconnect = true;
1143 } 1184 }
1144 } 1185 }
1145 } 1186 }
diff --git a/src/net.h b/src/net.h
index 11c023e..449cff5 100644
--- a/src/net.h
+++ b/src/net.h
@@ -51,6 +51,24 @@ static const int64 PEX_MIN_INTERVAL = 60;
51// traffic is nothing: one per relay per five minutes. 51// traffic is nothing: one per relay per five minutes.
52static const int BTF_RENDEZVOUS_WAIT_SECS = 300; 52static const int BTF_RENDEZVOUS_WAIT_SECS = 300;
53 53
54// Liveness of an established peer connection. BTF_RENDEZVOUS_WAIT_SECS bounds
55// the wait for a dial to arrive; these bound the connection that comes out of
56// it, which has the same failure mode once it goes quiet.
57//
58// A healthy peer relays a block inventory roughly every nTargetSpacing (two
59// minutes), so silence measured in block intervals is the honest signal.
60// Bitcoin used ninety minutes against ten-minute blocks -- nine intervals --
61// and the same nine intervals here is eighteen minutes; thirty leaves margin
62// for a slow stretch without letting a dead path last an hour.
63static const int BTF_RECV_TIMEOUT_SECS = 30 * 60;
64static const int BTF_SEND_STALL_SECS = 10 * 60;
65static const int BTF_HANDSHAKE_GRACE_SECS = 60;
66
67// Send a ping after this long with nothing to say, so a peer running the
68// inactivity check above does not mistake a quiet node for a dead one. Must
69// stay well under BTF_RECV_TIMEOUT_SECS.
70static const int BTF_PING_INTERVAL_SECS = 10 * 60;
71
54// Descriptors to hand a peer: ours first, then peers that answered us. 72// Descriptors to hand a peer: ours first, then peers that answered us.
55void BtfPexCollect(std::vector<std::string>& vDescOut); 73void BtfPexCollect(std::vector<std::string>& vDescOut);
56// Verify descriptors a peer sent and remember the good ones. Returns how many. 74// Verify descriptors a peer sent and remember the good ones. Returns how many.
@@ -500,6 +518,16 @@ public:
500 // Last "btfpeers" we accepted from this node, to rate-limit the exchange. 518 // Last "btfpeers" we accepted from this node, to rate-limit the exchange.
501 int64 nLastPexRecv; 519 int64 nLastPexRecv;
502 520
521 // Liveness. A peer whose network path dies silently -- NAT drops an idle
522 // mapping, a relay restarts, a route changes -- never sends FIN, so the
523 // socket stays readable-never and the node simply stops hearing from it
524 // with no error anywhere. These stamps are the only way to tell that
525 // apart from a peer that merely has nothing to say.
526 int64 nTimeConnected;
527 int64 nLastSend;
528 int64 nLastRecv;
529 int64 nLastSendEmpty;
530
503 531
504 CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false) 532 CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false)
505 { 533 {
@@ -517,6 +545,10 @@ public:
517 nRefCount = 0; 545 nRefCount = 0;
518 nReleaseTime = 0; 546 nReleaseTime = 0;
519 nLastPexRecv = 0; 547 nLastPexRecv = 0;
548 nTimeConnected = GetTime();
549 nLastSend = 0;
550 nLastRecv = 0;
551 nLastSendEmpty = GetTime();
520 vfSubscribe.assign(256, false); 552 vfSubscribe.assign(256, false);
521 553
522 // Push a version message 554 // Push a version message