upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-08-11 21:52:45 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-08-19 22:48:27 +0000
commiteb2e32e14b67d0eaad17cfa7086d8d7ebaf7f00f (patch)
tree453af5925d5722cb7b91aefc45feff8491bacbf7
parente236d013e973fd00cbdbed4bea1e5f373c0d1236 (diff)
net: stop closing a descriptor number that now belongs to somebody else
THE WEDGE. CNode::Disconnect() closed hSocket and left the member holding the stale number. ~CNode's only guard is 'hSocket != INVALID_SOCKET' and nothing in the tree ever assigned that sentinel, so the destructor closed the same number a second time. The two closes are five minutes apart by design: ThreadSocketHandler2 disconnects a node, sets nReleaseTime to GetTime() + 5*60, and parks it in vNodesDisconnected; GetRefCount() reports it busy until that expires, so 'delete pnode' -- and the second close -- cannot run before then. Linux hands out the lowest free descriptor. Measured on the pool: 202 open descriptors with a high-water mark of 216, so a freed number is reissued within seconds -- to a Nostr relay TLS socket, a rendezvous socket, a tunnel end, or a miner connection. Five minutes later the destructor closes it. The victim gets no error and no log line. It simply stops. That is the whole signature, including the part that never made sense: .btf block delivery and Nostr descriptor lookup share no lock, no queue and no thread, so they should not fail together. They share the file descriptor table, and a stray close is drawn from all of it. It also explains why #78 did not help and may have hurt. #78 drops stuck nodes -- it fired 41 times in one window -- and every drop is another Disconnect(), which is another delayed close armed against an innocent socket. Second bug, same symptom, different subsystem: the pooled relay sockets. ResolveDescriptor returned the instant a descriptor verified, leaving the relay's EOSE unread on a socket that goes straight back into g_relayPool, so the next caller read our leftovers as its own answer -- permanently one message out of step, and no code path anywhere inspected the subscription id that would have caught it. Worse, BtfResolve set lease.fOk = true on every failure path including a dead RecvText, labelling it 'exchange completed, just no match'. fOk = false is the only thing that ever closes a pooled socket or clears fConnected, so a relay connection that died was pinned in the pool, reported healthy, and reused for the life of the process. That is 'no Nostr descriptor found' for a descriptor that nak can see, cleared only by a restart. Now: read to EOSE before returning, match the subscription id, and keep a pooled socket only when the relay actually finished the conversation. (cherry picked from commit ed4640c306cc46dbdbd92cb6dc55e7528651269c)
-rw-r--r--src/nostr.cpp56
1 files changed, 45 insertions, 11 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
index b3e4a7c..3f43302 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -1057,9 +1057,14 @@ static void HandlePoolAnnouncement(const json& ev)
1057 1057
1058// Resolve a `.btf` address via a relay: fetch the descriptor published by that 1058// Resolve a `.btf` address via a relay: fetch the descriptor published by that
1059// address's key and verify it self-certifies under the decoded pubkey. 1059// address's key and verify it self-certifies under the decoded pubkey.
1060// fExchangeOk reports whether the relay actually finished the conversation
1061// (an EOSE arrived) as opposed to the socket dying mid-read. The caller pools
1062// this socket for reuse, and only a completed exchange leaves it in a state
1063// the next caller can trust.
1060static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1064static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1061 btf::Descriptor& out) 1065 btf::Descriptor& out, bool& fExchangeOk)
1062{ 1066{
1067 fExchangeOk = false;
1063 unsigned char pk[32]; 1068 unsigned char pk[32];
1064 if (!btf::ParseAddress(btfAddr, pk)) 1069 if (!btf::ParseAddress(btfAddr, pk))
1065 return false; 1070 return false;
@@ -1070,26 +1075,42 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1070 json req = json::array({ "REQ", "btf-resolve", filter }); 1075 json req = json::array({ "REQ", "btf-resolve", filter });
1071 if (!ws.SendText(req.dump())) 1076 if (!ws.SendText(req.dump()))
1072 return false; 1077 return false;
1078 bool fFound = false;
1073 for (;;) 1079 for (;;)
1074 { 1080 {
1075 string msg; 1081 string msg;
1076 if (!ws.RecvText(msg)) 1082 if (!ws.RecvText(msg))
1077 break; 1083 return fFound; // socket died mid-exchange; fExchangeOk stays false
1078 json j; 1084 json j;
1079 try { j = json::parse(msg); } catch (...) { continue; } 1085 try { j = json::parse(msg); } catch (...) { continue; }
1080 if (!j.is_array() || j.empty() || !j[0].is_string()) continue; 1086 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1081 string t = j[0].get<string>(); 1087 string t = j[0].get<string>();
1088 // Every relay message names the subscription it belongs to. This socket
1089 // is pooled and reused, so a message left over from an earlier exchange
1090 // is the ordinary case here, not an exotic one -- and an unmatched EOSE
1091 // ends this query before it has begun, which is indistinguishable from
1092 // "that descriptor does not exist". Check the id.
1093 if (j.size() >= 2 && j[1].is_string() && j[1].get<string>() != "btf-resolve")
1094 continue;
1082 if (t == "EVENT" && j.size() >= 3) 1095 if (t == "EVENT" && j.size() >= 3)
1083 { 1096 {
1084 const json& ev = j[2]; 1097 const json& ev = j[2];
1085 if (ev.contains("content") && 1098 if (!fFound && ev.contains("content") &&
1086 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1099 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1087 return true; 1100 fFound = true;
1101 // Deliberately keep reading to EOSE rather than returning here.
1102 // Returning early leaves the rest of the relay's answer queued on a
1103 // socket that goes straight back into the pool, and the next caller
1104 // reads our leftovers as its own -- one message out of step, for
1105 // the life of the process.
1088 } 1106 }
1089 else if (t == "EOSE") 1107 else if (t == "EOSE")
1108 {
1109 fExchangeOk = true; // relay said "that is all": socket is clean
1090 break; 1110 break;
1111 }
1091 } 1112 }
1092 return false; 1113 return fFound;
1093} 1114}
1094 1115
1095// Resolve a `.btf` address into its rendezvous coordinates by querying the 1116// Resolve a `.btf` address into its rendezvous coordinates by querying the
@@ -1108,12 +1129,22 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1108 if (!lease.Connected()) 1129 if (!lease.Connected())
1109 continue; 1130 continue;
1110 btf::Descriptor d; 1131 btf::Descriptor d;
1111 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) 1132 bool fExchangeOk = false;
1133 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d, fExchangeOk))
1112 { 1134 {
1113 lease.fOk = true; // exchange completed, just no match 1135 // Only keep the pooled socket if the relay actually finished
1136 // the conversation. This used to say "exchange completed, just
1137 // no match" on every failure -- including a dead socket -- so
1138 // ~CRelayLease never closed it and fConnected was never
1139 // cleared. A relay connection that died was then pinned in the
1140 // pool, reported healthy, and reused for every later lookup,
1141 // which is why "no Nostr descriptor found" persisted for a
1142 // descriptor that was demonstrably published, and why only a
1143 // restart cleared it.
1144 lease.fOk = fExchangeOk;
1114 continue; 1145 continue;
1115 } 1146 }
1116 lease.fOk = true; 1147 lease.fOk = fExchangeOk;
1117 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1148 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1118 continue; // descriptor predates the owner's relay config 1149 continue; // descriptor predates the owner's relay config
1119 if (!HexDecode(d.enc, enc_pub, 32)) 1150 if (!HexDecode(d.enc, enc_pub, 32))
@@ -1761,14 +1792,17 @@ void ThreadNostrSeed(void* parg)
1761 PublishDescriptor(ws, key); 1792 PublishDescriptor(ws, key);
1762 Sleep(700); // let the relay store the replaceable event 1793 Sleep(700); // let the relay store the replaceable event
1763 btf::Descriptor d; 1794 btf::Descriptor d;
1764 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) 1795 bool fExchangeOk = false;
1796 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d, fExchangeOk))
1765 { 1797 {
1766 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", 1798 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n",
1767 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); 1799 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str());
1768 lease.fOk = true; 1800 lease.fOk = fExchangeOk;
1769 break; 1801 break;
1770 } 1802 }
1771 lease.fOk = true; 1803 // Same rule as BtfResolve: a socket only goes back in the pool if
1804 // the relay finished the conversation on it.
1805 lease.fOk = fExchangeOk;
1772 } 1806 }
1773 CATCH_PRINT_EXCEPTION("btf self-test") 1807 CATCH_PRINT_EXCEPTION("btf self-test")
1774 } 1808 }