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-maintainer <noreply@bitflash.invalid>2026-08-23 04:31:18 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-23 04:37:36 +0000
commit6d4c3f153cf44cefead39b21191dfb33491bfa5a (patch)
tree767e6e9f5281a7da648acf0ac70a1f18175dad11 /src
parentc4c19bd894c8fc30c558e560e326aab538b23170 (diff)
net: start every peer's stall clock stopped, not at whatever was in memory
CNode's constructor never wrote nMsgWaitSince or nMsgWaitSize, so every peer began life with indeterminate values in the pair ProcessMessages uses to decide whether that peer has stopped sending, and the first partial message it read was measured against them. The guard restarts the clock only when nMsgWaitSince is 0 or the buffer has grown since the last look. Garbage in nMsgWaitSince is neither, so the very first look fell through to the timeout branch. A peer that merely split a message across two TCP segments -- header in one, body in the next, which is ordinary on any real link -- could be dropped as unreadable on first contact, before it had sent anything wrong and often before its version message. It was never reproducible either: what happened depended on what had last used that heap. The log reported the stall as roughly the age of the epoch: peer 127.0.0.1:8433 announced a 100 byte 'block' and has sent nothing more for 1786593923 s (0 of 100 payload bytes) -- dropping, this peer is unreadable Every node is running this. Introduced in 7f6e5ad, which added the pair to the class and to ProcessMessages but not to the constructor; the constructor went on initialising nIncompleteMessageStart, nIncompleteMessageSize and strIncompleteMessageCommand, the upstream members our implementation supersedes. 5490d66 removed their last reader from ProcessMessages, and nothing has read or written them since, so they go, with BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS -- decoy state in a networking struct is how this happened, and the merge ahead of us would carry it further. The initialiser list now follows declaration order, so a member missing from it is visible. The net-message selftest was asserting on those dead members too, which is why it could not have caught this: it read state no code path writes, and its stale-message case passed only when the garbage in nMsgWaitSince happened to look old. Rewired onto nMsgWaitSince / nMsgWaitSize / BTF_MSG_WAIT_MAX_SECS, and given the two cases it never covered -- a new peer's clock must start stopped, and a peer that is still delivering payload must not be dropped however old its clock is. Measured on the build host, 20 runs each: before, net-message failed 20/20 ("a fresh incomplete message does not disconnect immediately" failed 17/20 by itself). After, 20/20 pass. Rewired selftest against a reverted constructor fix: fails 20/20, deterministically, where the old suite caught it by luck or not at all.
Diffstat (limited to 'src')
-rw-r--r--src/net.h16
-rw-r--r--src/selftest.cpp37
2 files changed, 38 insertions, 15 deletions
diff --git a/src/net.h b/src/net.h
index 8b0245a..3c8c409 100644
--- a/src/net.h
+++ b/src/net.h
@@ -89,10 +89,9 @@ static const int BTF_HANDSHAKE_GRACE_SECS = 60;
89 89
90// A serialized object cannot be larger than MAX_SIZE, so no valid network 90// A serialized object cannot be larger than MAX_SIZE, so no valid network
91// message needs the old 256 MB header allowance. Keep the wire cap aligned with 91// message needs the old 256 MB header allowance. Keep the wire cap aligned with
92// what the deserializer can accept and bound how long one peer may sit on a 92// what the deserializer can accept. How long a peer may sit on a partial
93// partial payload after a complete header has arrived. 93// payload after a complete header has arrived is BTF_MSG_WAIT_MAX_SECS, below.
94static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE; 94static const unsigned int MAX_PROTOCOL_MESSAGE_SIZE = MAX_SIZE;
95static const int BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS = 2 * 60;
96 95
97// Ceiling on simultaneous connections. 96// Ceiling on simultaneous connections.
98// 97//
@@ -656,9 +655,6 @@ public:
656 int64 nLastSend; 655 int64 nLastSend;
657 int64 nLastRecv; 656 int64 nLastRecv;
658 int64 nLastSendEmpty; 657 int64 nLastSendEmpty;
659 int64 nIncompleteMessageStart;
660 unsigned int nIncompleteMessageSize;
661 string strIncompleteMessageCommand;
662 string strBtfAddr; 658 string strBtfAddr;
663 string strBtfMeeting; 659 string strBtfMeeting;
664 660
@@ -699,11 +695,13 @@ public:
699 nLastSend = 0; 695 nLastSend = 0;
700 nLastRecv = 0; 696 nLastRecv = 0;
701 nLastSendEmpty = GetTime(); 697 nLastSendEmpty = GetTime();
702 nIncompleteMessageStart = 0;
703 nIncompleteMessageSize = 0;
704 strIncompleteMessageCommand.clear();
705 strBtfAddr.clear(); 698 strBtfAddr.clear();
706 strBtfMeeting.clear(); 699 strBtfMeeting.clear();
700 // Not waiting on anything yet. Left indeterminate, the first partial
701 // message this peer sends is measured against whatever was in this
702 // memory, and ProcessMessages drops it as unreadable on first contact.
703 nMsgWaitSince = 0;
704 nMsgWaitSize = 0;
707 nStartingHeight = -1; 705 nStartingHeight = -1;
708 706
709 // Push a version message 707 // Push a version message
diff --git a/src/selftest.cpp b/src/selftest.cpp
index ff0289b..d12c96b 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -1105,6 +1105,13 @@ static int RunNetMessageSelfTest()
1105 int nFail = 0; 1105 int nFail = 0;
1106 try 1106 try
1107 { 1107 {
1108 // A peer that has not yet been kept waiting must not look like one that
1109 // has been waiting since the epoch. This is the whole bug: the stall
1110 // clock lives in CNode, so only the constructor can start it at zero.
1111 CNode opened(INVALID_SOCKET, CAddress("127.0.0.1"));
1112 nFail += Check(opened.nMsgWaitSince == 0 && opened.nMsgWaitSize == 0,
1113 "a new peer starts with its stall clock stopped") ? 0 : 1;
1114
1108 CNode complete(INVALID_SOCKET, CAddress("127.0.0.1")); 1115 CNode complete(INVALID_SOCKET, CAddress("127.0.0.1"));
1109 complete.nVersion = VERSION; 1116 complete.nVersion = VERSION;
1110 complete.vRecv << CMessageHeader("ping", 0); 1117 complete.vRecv << CMessageHeader("ping", 0);
@@ -1119,20 +1126,38 @@ static int RunNetMessageSelfTest()
1119 nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1; 1126 nFail += Check(ProcessMessages(&partial), "an incomplete message returns cleanly") ? 0 : 1;
1120 nFail += Check(partial.vRecv.size() == nPartialBefore, 1127 nFail += Check(partial.vRecv.size() == nPartialBefore,
1121 "an incomplete message keeps one header in the buffer") ? 0 : 1; 1128 "an incomplete message keeps one header in the buffer") ? 0 : 1;
1122 nFail += Check(partial.nIncompleteMessageStart != 0, 1129 nFail += Check(partial.nMsgWaitSince != 0,
1123 "an incomplete message starts a timeout clock") ? 0 : 1; 1130 "an incomplete message starts the stall clock") ? 0 : 1;
1131 nFail += Check(partial.nMsgWaitSize == 0,
1132 "the stall clock records how much payload had arrived") ? 0 : 1;
1124 nFail += Check(!partial.fDisconnect, 1133 nFail += Check(!partial.fDisconnect,
1125 "a fresh incomplete message does not disconnect immediately") ? 0 : 1; 1134 "a fresh incomplete message does not disconnect immediately") ? 0 : 1;
1126 1135
1136 // Still arriving, just slowly: more payload is here than when the clock
1137 // was started, so the clock restarts however old it was. Timing the
1138 // wait instead of the stall would drop this peer.
1139 CNode slow(INVALID_SOCKET, CAddress("127.0.0.1"));
1140 slow.nVersion = VERSION;
1141 slow.vRecv << CMessageHeader("block", 100);
1142 slow.vRecv.insert(slow.vRecv.end(), 50, '\0');
1143 slow.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1;
1144 slow.nMsgWaitSize = 10;
1145 ProcessMessages(&slow);
1146 nFail += Check(!slow.fDisconnect,
1147 "a peer still sending payload is not dropped") ? 0 : 1;
1148 nFail += Check(slow.nMsgWaitSize == 50,
1149 "progress restarts the stall clock") ? 0 : 1;
1150
1151 // Nothing more has arrived since the clock started, and the clock is
1152 // older than the limit.
1127 CNode stale(INVALID_SOCKET, CAddress("127.0.0.1")); 1153 CNode stale(INVALID_SOCKET, CAddress("127.0.0.1"));
1128 stale.nVersion = VERSION; 1154 stale.nVersion = VERSION;
1129 stale.vRecv << CMessageHeader("block", 100); 1155 stale.vRecv << CMessageHeader("block", 100);
1130 stale.nIncompleteMessageStart = GetTime() - BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS - 1; 1156 stale.nMsgWaitSince = GetTime() - BTF_MSG_WAIT_MAX_SECS - 1;
1131 stale.nIncompleteMessageSize = 100; 1157 stale.nMsgWaitSize = 0;
1132 stale.strIncompleteMessageCommand = "block";
1133 ProcessMessages(&stale); 1158 ProcessMessages(&stale);
1134 nFail += Check(stale.fDisconnect, 1159 nFail += Check(stale.fDisconnect,
1135 "a stale incomplete message disconnects the peer") ? 0 : 1; 1160 "a stalled incomplete message disconnects the peer") ? 0 : 1;
1136 1161
1137 CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1")); 1162 CNode oversized(INVALID_SOCKET, CAddress("127.0.0.1"));
1138 oversized.nVersion = VERSION; 1163 oversized.nVersion = VERSION;