upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr.cpp
diff options
context:
space:
mode:
authorbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
committerbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
commit5ce650382352c1503f40abf08129ada69402af55 (patch)
tree6a18198901f1e2c4933f1ac53c9365eec512bf63 /src/nostr.cpp
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'src/nostr.cpp')
-rw-r--r--src/nostr.cpp1775
1 files changed, 1775 insertions, 0 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
new file mode 100644
index 0000000..1fd60ad
--- /dev/null
+++ b/src/nostr.cpp
@@ -0,0 +1,1775 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Peer discovery over Nostr relays. See nostr.h.
5
6#include "headers.h"
7#include <openssl/ssl.h>
8#include <openssl/err.h>
9// We link libsecp256k1 statically (without this the header assumes DLL import).
10// Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from
11// warning while leaving this file correct if built without the flag.
12#ifndef SECP256K1_STATIC
13#define SECP256K1_STATIC
14#endif
15#include <secp256k1.h>
16#include <secp256k1_schnorrsig.h>
17#include <secp256k1_extrakeys.h>
18
19// util.h redefines snprintf as a macro (2009 MSVC compat); this breaks
20// nlohmann/json, which calls std::snprintf. Undo the macro just here.
21#ifdef snprintf
22#undef snprintf
23#endif
24#include <nlohmann/json.hpp>
25
26#include "btfaddr.h"
27#include "btfchan.h"
28
29using json = nlohmann::json;
30
31// Bitflash rendezvous descriptor: parameterized-replaceable Nostr event so each
32// node keeps exactly one current descriptor (keyed by author pubkey).
33static const int BTF_DESC_KIND = 38501;
34// "-2" scopes the 2026 stable relaunch: new nodes discover only each other, and
35// the abandoned test .btf descriptors lingering on the relays become invisible.
36static const char* BTF_DESC_DTAG = "btf-descriptor-2";
37
38// Relay auto-discovery: a relay operator announces their relay as an addressable
39// event (kind 38502) so every node learns it automatically -- no manual
40// seed-list edit, no maintainer approval. Volunteer relays just work.
41static const int BTF_RELAY_KIND = 38502;
42static const char* BTF_RELAY_DTAG = "btf-relay-2";
43static const size_t BTF_MAX_DISCOVERED_RELAYS = 50; // anti-spam cap
44
45// Pool announcements carry the operator's .btf address, fee, and live stats.
46static const int BTF_POOL_KIND = 38503;
47static const char* BTF_POOL_DTAG = "btf-pool-2";
48static const int64 BTF_POOL_MAX_AGE = 3 * 60; // seconds
49
50// Anonymous auto-discovery: how many peers to keep connected via .btf rendezvous
51// before we stop dialing more, and how many new dials to attempt per cycle.
52// Target outbound peer count. With ~100 known .btf peers on the network,
53// an Erdos-Renyi random graph stays connected with overwhelming probability
54// once average degree exceeds ln(N) (~4.6 here); 24 is ~5x past that
55// threshold, so this is plenty *provided* selection isn't fully correlated
56// across nodes -- see the diversity slice in ConnectDiscoveredBtfPeers.
57static const unsigned int BTF_TARGET_CONN = 24;
58static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle
59
60// Nostr descriptor freshness cutoff: skip peers whose descriptor hasn't been
61// refreshed in this many seconds. Stale descriptors mean the node isn't running.
62static const int64 BTF_PEER_MAX_AGE = 3 * 60 * 60; // 3 hours
63
64// Other nodes' .btf addresses learned from their descriptors on the relays.
65// Maps address -> descriptor created_at timestamp (Unix seconds).
66static map<string, int64> g_btfPeers;
67static CCriticalSection cs_btfPeers;
68
69// Public discovery relays. Tunable.
70//
71// Picked for: free/public (no NIP-42 auth or payment wall), track record of
72// high uptime, and *infrastructure* diversity, not just different hostnames.
73// relay.damus.io, relay.snort.social and relay.primal.net are all fronted by
74// Cloudflare -- different operators, same upstream, so a Cloudflare-side
75// incident (or a Cloudflare IP range getting rate-limited/blocked on some
76// network) can take out several of them at once even though they look like
77// independent relays. Kept only one Cloudflare-fronted relay (damus.io, the
78// most established) and filled the rest with relays on different hosts
79// (Hetzner, various independents) so no single upstream provider is a
80// single point of failure for peer discovery. 7 relays lands inside the
81// commonly recommended 5-10 range for redundancy without keeping an
82// excessive number of persistent sockets open per node.
83const char* pszNostrRelays[] = {
84 "wss://relay.damus.io",
85 "wss://nos.lol",
86 // Was relay.nostr.band, which went down and stayed down (2026-07-31:
87 // resolves, refuses TCP on 443, from several networks). Replaced with a
88 // relay on a different upstream again -- 206.245.132.32, neither
89 // Cloudflare nor Hetzner, which the rest of this list already leans on.
90 "wss://nostr.bitcoiner.social",
91 "wss://nostr21.com",
92 "wss://offchain.pub",
93 "wss://nostr.mom",
94 "wss://relay.primal.net",
95};
96const int nNostrRelays = ARRAYLEN(pszNostrRelays);
97
98// Rendezvous meeting relay this node registers at (ThreadBtfAccept in net.cpp)
99// and advertises in its .btf descriptor. Seed default; /rvrelay overrides.
100// Seed rendezvous relays. More entries = more resilience: the node fails over
101// between them (see ThreadBtfAccept), so DDoSing one relay IP can't take the
102// network down. /rvrelay overrides this list with a single entry.
103vector<string> vBtfMeetingRelays = {
104 "92.246.128.180:8434", // Sao Paulo, BR
105 "31.44.4.249:8434", // New Jersey, US
106 "90.156.222.107:8434", // Almaty, KZ
107};
108string strBtfActiveRelay;
109static CCriticalSection cs_activeRelay;
110
111// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from
112// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it
113// used to fall back to vBtfMeetingRelays[0], and since ThreadNostrSeed and
114// ThreadBtfAccept start in parallel (net.cpp), that fallback is what got
115// published at boot -- a descriptor telling peers to dial a relay we were not
116// registered at. When that first seed was down, every peer that read the
117// descriptor burned a dial on a dead rendezvous.
118string BtfActiveRelay()
119{
120 CRITICAL_BLOCK(cs_activeRelay)
121 return strBtfActiveRelay;
122 return "";
123}
124
125void BtfSetActiveRelay(const string& relay)
126{
127 CRITICAL_BLOCK(cs_activeRelay)
128 strBtfActiveRelay = relay;
129}
130
131// A relay this node advertises on Nostr for others to discover (/announcerelay).
132string strBtfAnnounceRelay;
133// Relays learned from other operators' Nostr announcements.
134static set<string> g_discoveredRelays;
135static CCriticalSection cs_discoveredRelays;
136
137// Live pool announcements discovered from the relays.
138static map<string, BtfPoolAnnouncement> g_discoveredPools;
139static CCriticalSection cs_discoveredPools;
140
141// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host.
142static bool LooksLikeRelay(const string& r)
143{
144 size_t colon = r.rfind(':');
145 if (colon == string::npos || colon == 0 || colon + 1 >= r.size())
146 return false;
147 if (r.size() > 100)
148 return false;
149 for (size_t i = colon + 1; i < r.size(); i++)
150 if (!isdigit((unsigned char)r[i]))
151 return false;
152 int port = atoi(r.c_str() + colon + 1);
153 if (port <= 0 || port > 65535)
154 return false;
155 // host: allow letters, digits, dot, dash, colon (IPv6 not supported yet)
156 for (size_t i = 0; i < colon; i++)
157 {
158 char c = r[i];
159 if (!(isalnum((unsigned char)c) || c == '.' || c == '-'))
160 return false;
161 }
162 return true;
163}
164
165// Every relay to try: curated seeds first (reliable bootstrap), then the ones
166// discovered from Nostr announcements (volunteer relays).
167vector<string> BtfAllRelays()
168{
169 vector<string> all = vBtfMeetingRelays;
170 CRITICAL_BLOCK(cs_discoveredRelays)
171 foreach(const string& r, g_discoveredRelays)
172 if (find(all.begin(), all.end(), r) == all.end())
173 all.push_back(r);
174 return all;
175}
176
177
178//
179// Utilities
180//
181static string HexEncode(const unsigned char* p, size_t n)
182{
183 static const char* h = "0123456789abcdef";
184 string s;
185 s.reserve(n * 2);
186 for (size_t i = 0; i < n; i++)
187 {
188 s += h[p[i] >> 4];
189 s += h[p[i] & 0xf];
190 }
191 return s;
192}
193
194static bool HexDecode(const string& s, unsigned char* out, size_t n)
195{
196 if (s.size() != n * 2)
197 return false;
198 for (size_t i = 0; i < n; i++)
199 {
200 unsigned int b;
201 if (sscanf(s.c_str() + i * 2, "%2x", &b) != 1)
202 return false;
203 out[i] = (unsigned char)b;
204 }
205 return true;
206}
207
208static string Base64Encode(const unsigned char* p, size_t n)
209{
210 static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
211 string s;
212 for (size_t i = 0; i < n; i += 3)
213 {
214 unsigned int b = p[i] << 16;
215 if (i + 1 < n) b |= p[i + 1] << 8;
216 if (i + 2 < n) b |= p[i + 2];
217 s += tbl[(b >> 18) & 0x3f];
218 s += tbl[(b >> 12) & 0x3f];
219 s += (i + 1 < n) ? tbl[(b >> 6) & 0x3f] : '=';
220 s += (i + 2 < n) ? tbl[b & 0x3f] : '=';
221 }
222 return s;
223}
224
225
226//
227// Node Nostr key -- secp256k1 x-only (BIP340), persisted in <appdir>/nostr.key
228//
229class CNostrKey
230{
231public:
232 secp256k1_context* ctx;
233 secp256k1_keypair keypair;
234 unsigned char seckey[32]; // node's secp256k1 secret (for descriptor signing)
235 unsigned char xonly[32]; // node's x-only pubkey (also its .btf identity)
236 unsigned char enc_pk[32]; // node's x25519 public key (echan `enc` field)
237 unsigned char enc_sk[32]; // node's x25519 secret key
238 bool fValid;
239
240 CNostrKey() : ctx(NULL), fValid(false) {}
241
242 ~CNostrKey()
243 {
244 if (ctx) secp256k1_context_destroy(ctx);
245 }
246
247 // Load or generate a 32-byte key persisted as hex in <appdir>/<name>.
248 static void LoadOrGenSecp(secp256k1_context* ctx, const string& file, unsigned char sk[32])
249 {
250 FILE* f = fopen(file.c_str(), "rb");
251 string strHex;
252 if (f) { char buf[65] = {0}; fread(buf, 1, 64, f); fclose(f); strHex = buf; }
253 if (!HexDecode(strHex, sk, 32) || !secp256k1_ec_seckey_verify(ctx, sk))
254 {
255 do { RAND_bytes(sk, sizeof(unsigned char) * 32); } while (!secp256k1_ec_seckey_verify(ctx, sk));
256 FILE* fw = fopen(file.c_str(), "wb");
257 if (fw) { string h = HexEncode(sk, 32); fwrite(h.c_str(), 1, h.size(), fw); fclose(fw); }
258 }
259 }
260
261 bool Init()
262 {
263 ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
264 if (!ctx)
265 return false;
266 unsigned char seed[32];
267 RAND_bytes(seed, sizeof(seed));
268 secp256k1_context_randomize(ctx, seed);
269
270 LoadOrGenSecp(ctx, GetAppDir() + "/nostr.key", seckey);
271
272 if (!secp256k1_keypair_create(ctx, &keypair, seckey))
273 return false;
274 secp256k1_xonly_pubkey xpub;
275 if (!secp256k1_keypair_xonly_pub(ctx, &xpub, NULL, &keypair))
276 return false;
277 secp256k1_xonly_pubkey_serialize(ctx, xonly, &xpub);
278
279 // x25519 encryption keypair for the end-to-end channel, persisted separately.
280 btf::ChanInit();
281 string strEncFile = GetAppDir() + "/btf_enc.key";
282 FILE* fe = fopen(strEncFile.c_str(), "rb");
283 bool fLoaded = false;
284 if (fe)
285 {
286 char buf[65] = {0};
287 if (fread(buf, 1, 64, fe) == 64 && HexDecode(string(buf), enc_sk, 32))
288 fLoaded = true;
289 fclose(fe);
290 }
291 if (fLoaded)
292 {
293 // Derive the public key from the stored secret (stable identity).
294 btf::ChanPublicFromSecret(enc_pk, enc_sk);
295 }
296 else
297 {
298 btf::ChanKeypair(enc_pk, enc_sk);
299 FILE* few = fopen(strEncFile.c_str(), "wb");
300 if (few) { string h = HexEncode(enc_sk, 32); fwrite(h.c_str(), 1, h.size(), few); fclose(few); }
301 }
302
303 fValid = true;
304 return true;
305 }
306
307 string PubKeyHex() const { return HexEncode(xonly, 32); }
308 string EncPubHex() const { return HexEncode(enc_pk, 32); }
309 string BtfAddress() const { return btf::Address(xonly); }
310
311 bool SignId(const unsigned char id[32], string& sigHexOut)
312 {
313 unsigned char sig[64];
314 unsigned char aux[32];
315 RAND_bytes(aux, sizeof(aux));
316 if (!secp256k1_schnorrsig_sign32(ctx, sig, id, &keypair, aux))
317 return false;
318 sigHexOut = HexEncode(sig, 64);
319 return true;
320 }
321};
322
323// The node's single Nostr/.btf identity, shared by the seeding thread here and
324// the rendezvous transport in net.cpp. Lazily initialized under a lock; the
325// secp256k1 context is immutable after Init, so concurrent use is safe.
326static CNostrKey g_nostrKey;
327static CCriticalSection cs_nostrKey;
328
329static bool EnsureNostrKey()
330{
331 CRITICAL_BLOCK(cs_nostrKey)
332 if (!g_nostrKey.fValid)
333 g_nostrKey.Init();
334 return g_nostrKey.fValid;
335}
336
337bool BtfGetIdentity(unsigned char pubkey[32], unsigned char enc_sk[32])
338{
339 if (!EnsureNostrKey())
340 return false;
341 memcpy(pubkey, g_nostrKey.xonly, 32);
342 memcpy(enc_sk, g_nostrKey.enc_sk, 32);
343 return true;
344}
345
346std::string BtfLocalAddress()
347{
348 if (!EnsureNostrKey())
349 return "";
350 return g_nostrKey.BtfAddress();
351}
352
353// This node's own descriptor, signed and self-certifying -- the same blob
354// PublishDescriptor puts on Nostr, but handed straight to a peer instead.
355//
356// Reads strBtfActiveRelay directly rather than going through BtfActiveRelay():
357// only a rendezvous we actually registered at is worth announcing. Handing out
358// a meeting node we are not reachable at would make every peer that receives
359// it burn a dial, and over peer exchange that mistake travels further than it
360// does over Nostr.
361std::string BtfLocalDescriptor()
362{
363 if (!EnsureNostrKey())
364 return "";
365 string meeting;
366 CRITICAL_BLOCK(cs_activeRelay)
367 meeting = strBtfActiveRelay;
368 if (meeting.empty())
369 return "";
370 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey,
371 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime());
372}
373
374// The shared secp256k1 context, so net.cpp can verify descriptors that arrive
375// from peers. NULL if the identity could not be loaded.
376void* BtfSecpContext()
377{
378 if (!EnsureNostrKey())
379 return NULL;
380 return (void*)g_nostrKey.ctx;
381}
382
383int GetDiscoveredPeerCount()
384{
385 int n = 0;
386 CRITICAL_BLOCK(cs_btfPeers) { n = (int)g_btfPeers.size(); }
387 return n;
388}
389
390void BtfGetPoolAnnouncements(std::vector<BtfPoolAnnouncement>& out)
391{
392 out.clear();
393 int64 now = GetTime();
394 CRITICAL_BLOCK(cs_discoveredPools)
395 {
396 for (map<string, BtfPoolAnnouncement>::iterator it = g_discoveredPools.begin(); it != g_discoveredPools.end(); )
397 {
398 if (it->second.createdAt > 0 && now - it->second.createdAt > BTF_POOL_MAX_AGE)
399 {
400 it = g_discoveredPools.erase(it);
401 continue;
402 }
403 out.push_back(it->second);
404 ++it;
405 }
406 }
407 sort(out.begin(), out.end(), [](const BtfPoolAnnouncement& a, const BtfPoolAnnouncement& b) {
408 return a.createdAt > b.createdAt;
409 });
410}
411
412// Build and sign a Nostr event (NIP-01). Returns the event JSON.
413static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags,
414 const string& content, json& eventOut)
415{
416 int64 created = GetTime();
417 // Canonical serialization for the id: [0,pubkey,created_at,kind,tags,content]
418 json ser = json::array({ 0, key.PubKeyHex(), created, kind, tags, content });
419 string s = ser.dump();
420
421 unsigned char id[32];
422 SHA256((const unsigned char*)s.data(), s.size(), id);
423
424 string sigHex;
425 if (!key.SignId(id, sigHex))
426 return false;
427
428 eventOut = json::object();
429 eventOut["id"] = HexEncode(id, 32);
430 eventOut["pubkey"] = key.PubKeyHex();
431 eventOut["created_at"] = created;
432 eventOut["kind"] = kind;
433 eventOut["tags"] = tags;
434 eventOut["content"] = content;
435 eventOut["sig"] = sigHex;
436 return true;
437}
438
439
440//
441// Minimal WebSocket client over TLS (wss) or TCP (ws), blocking with timeout.
442//
443// Ceilings on what a relay may make this node allocate. Nostr events are
444// small -- relays themselves usually refuse anything over a few hundred KB --
445// so these are generous, and they bound an otherwise attacker-chosen size.
446static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame
447static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled
448
449class CWebSocket
450{
451public:
452 SOCKET hSocket;
453 SSL_CTX* sslctx;
454 SSL* ssl;
455 bool fTls;
456 string recvbuf;
457
458 CWebSocket() : hSocket(INVALID_SOCKET), sslctx(NULL), ssl(NULL), fTls(false) {}
459 ~CWebSocket() { Close(); }
460
461 void Close()
462 {
463 if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = NULL; }
464 if (sslctx) { SSL_CTX_free(sslctx); sslctx = NULL; }
465 if (hSocket != INVALID_SOCKET) { BtfCloseSocket(hSocket); hSocket = INVALID_SOCKET; }
466 }
467
468 int RawRead(char* buf, int len)
469 {
470 if (fTls) return SSL_read(ssl, buf, len);
471 return recv(hSocket, buf, len, 0);
472 }
473
474 bool RawWrite(const char* buf, int len)
475 {
476 int off = 0;
477 while (off < len)
478 {
479 int n = fTls ? SSL_write(ssl, buf + off, len - off)
480 : send(hSocket, buf + off, len - off, 0);
481 if (n <= 0) return false;
482 off += n;
483 }
484 return true;
485 }
486
487 // Connect and perform the WebSocket handshake. url = wss://host[:port]/path
488 bool Connect(const string& url, int timeoutSec)
489 {
490 string u = url;
491 int port = 443;
492 fTls = true;
493 if (u.compare(0, 6, "wss://") == 0) { u = u.substr(6); fTls = true; port = 443; }
494 else if (u.compare(0, 5, "ws://") == 0) { u = u.substr(5); fTls = false; port = 80; }
495 else return false;
496
497 string host, path = "/";
498 size_t slash = u.find('/');
499 if (slash != string::npos) { host = u.substr(0, slash); path = u.substr(slash); }
500 else host = u;
501 size_t colon = host.find(':');
502 if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); }
503
504 // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA
505 // records back and try them in the order the resolver prefers,
506 // rather than hard-failing whenever the IPv4 path to a relay is
507 // filtered/rate-limited (increasingly common for CDN-fronted
508 // relays) while a working IPv6 route exists right there in the
509 // same getaddrinfo() result.
510 struct addrinfo hints, *res = NULL, *rp = NULL;
511 memset(&hints, 0, sizeof(hints));
512 hints.ai_family = AF_UNSPEC;
513 hints.ai_socktype = SOCK_STREAM;
514 char portstr[16]; sprintf(portstr, "%d", port);
515 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
516 return error("Nostr: getaddrinfo %s failed", host.c_str());
517
518 bool fConnected = false;
519 for (rp = res; rp != NULL; rp = rp->ai_next)
520 {
521 hSocket = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_NOSTR);
522 if (hSocket == INVALID_SOCKET)
523 continue;
524
525 // Socket receive/send timeout: Windows takes a DWORD of
526 // milliseconds; POSIX takes a struct timeval.
527#ifdef _WIN32
528 DWORD tv = timeoutSec * 1000;
529 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
530 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
531#else
532 struct timeval tv;
533 tv.tv_sec = timeoutSec;
534 tv.tv_usec = 0;
535 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
536 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
537#endif
538
539 if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
540 {
541 fConnected = true;
542 break;
543 }
544
545 BtfCloseSocket(hSocket);
546 hSocket = INVALID_SOCKET;
547 }
548 freeaddrinfo(res);
549
550 if (!fConnected)
551 return error("Nostr: connect %s:%d failed", host.c_str(), port);
552
553 // TLS
554 if (fTls)
555 {
556 sslctx = SSL_CTX_new(TLS_client_method());
557 if (!sslctx) return false;
558 ssl = SSL_new(sslctx);
559 SSL_set_fd(ssl, (int)hSocket);
560 SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI
561 if (SSL_connect(ssl) != 1)
562 return error("Nostr: TLS handshake with %s failed", host.c_str());
563 }
564
565 // WebSocket handshake (HTTP Upgrade)
566 unsigned char keybytes[16];
567 RAND_bytes(keybytes, sizeof(keybytes));
568 string wskey = Base64Encode(keybytes, sizeof(keybytes));
569 string req =
570 "GET " + path + " HTTP/1.1\r\n"
571 "Host: " + host + "\r\n"
572 "Upgrade: websocket\r\n"
573 "Connection: Upgrade\r\n"
574 "Sec-WebSocket-Key: " + wskey + "\r\n"
575 "Sec-WebSocket-Version: 13\r\n\r\n";
576 if (!RawWrite(req.data(), (int)req.size()))
577 return false;
578
579 // Read the handshake response until \r\n\r\n
580 string resp;
581 char c;
582 while (resp.find("\r\n\r\n") == string::npos)
583 {
584 int n = RawRead(&c, 1);
585 if (n <= 0) return error("Nostr: no handshake response from %s", host.c_str());
586 resp += c;
587 // Bail out instead of breaking: breaking left the socket parked in
588 // the middle of the header block, and since the status line is read
589 // first the " 101 " check below still passed -- so the caller got a
590 // "connected" socket whose next bytes were leftover HTTP, which the
591 // frame parser then read as garbage frames.
592 if (resp.size() > 8192)
593 return error("Nostr: handshake headers too large from %s", host.c_str());
594 }
595 if (resp.find(" 101 ") == string::npos)
596 return error("Nostr: handshake refused by %s", host.c_str());
597 return true;
598 }
599
600 // Send a text message (masked frame, as required by the protocol)
601 bool SendText(const string& msg)
602 {
603 string frame;
604 frame += (char)0x81; // FIN + opcode texto
605 size_t len = msg.size();
606 if (len < 126)
607 frame += (char)(0x80 | len);
608 else if (len <= 0xffff)
609 {
610 frame += (char)(0x80 | 126);
611 frame += (char)((len >> 8) & 0xff);
612 frame += (char)(len & 0xff);
613 }
614 else
615 {
616 frame += (char)(0x80 | 127);
617 for (int i = 7; i >= 0; i--) frame += (char)((len >> (8 * i)) & 0xff);
618 }
619 unsigned char mask[4];
620 RAND_bytes(mask, 4);
621 frame.append((char*)mask, 4);
622 string masked = msg;
623 for (size_t i = 0; i < masked.size(); i++)
624 masked[i] ^= mask[i & 3];
625 frame += masked;
626 return RawWrite(frame.data(), (int)frame.size());
627 }
628
629 // Receive the next complete text message. Returns false on error/timeout.
630 bool RecvText(string& out)
631 {
632 out.clear();
633 for (;;)
634 {
635 unsigned char hdr[2];
636 if (!ReadN((char*)hdr, 2)) return false;
637 bool fin = hdr[0] & 0x80;
638 int opcode = hdr[0] & 0x0f;
639 bool masked = hdr[1] & 0x80;
640 uint64 len = hdr[1] & 0x7f;
641 if (len == 126)
642 {
643 unsigned char e[2];
644 if (!ReadN((char*)e, 2)) return false;
645 len = (e[0] << 8) | e[1];
646 }
647 else if (len == 127)
648 {
649 unsigned char e[8];
650 if (!ReadN((char*)e, 8)) return false;
651 len = 0;
652 for (int i = 0; i < 8; i++) len = (len << 8) | e[i];
653 }
654 // The length is whatever the relay says it is, and these are public
655 // relays the node does not control. Unbounded, a single frame header
656 // claiming 2^40 bytes makes the node allocate until it dies -- and
657 // reassembly across continuation frames grows without limit even
658 // when each frame is small. Cap both, and refuse anything that will
659 // not survive the int narrowing ReadN takes.
660 if (len > MAX_WS_FRAME) return false;
661
662 unsigned char mask[4] = {0,0,0,0};
663 if (masked && !ReadN((char*)mask, 4)) return false;
664
665 string payload;
666 payload.resize((size_t)len);
667 if (len && !ReadN(&payload[0], (int)len)) return false;
668 if (masked)
669 for (size_t i = 0; i < payload.size(); i++)
670 payload[i] ^= mask[i & 3];
671
672 if (opcode == 0x8) return false; // close
673 if (opcode == 0x9) { SendPong(payload); continue; } // ping
674 if (opcode == 0xA) continue; // pong
675 if (out.size() + payload.size() > MAX_WS_MESSAGE) return false;
676 out += payload; // text/continuation
677 if (fin) return true;
678 }
679 }
680
681private:
682 bool ReadN(char* buf, int n)
683 {
684 int off = 0;
685 while (off < n)
686 {
687 int r = RawRead(buf + off, n - off);
688 if (r <= 0) return false;
689 off += r;
690 }
691 return true;
692 }
693
694 void SendPong(const string& payload)
695 {
696 string frame;
697 frame += (char)0x8A;
698 frame += (char)(0x80 | (payload.size() & 0x7f));
699 unsigned char mask[4]; RAND_bytes(mask, 4);
700 frame.append((char*)mask, 4);
701 string m = payload;
702 for (size_t i = 0; i < m.size(); i++) m[i] ^= mask[i & 3];
703 frame += m;
704 RawWrite(frame.data(), (int)frame.size());
705 }
706};
707
708
709// Persistent relay connections. Public relays (relay.damus.io in particular)
710// rate-limit new connections per source IP fairly aggressively. Before this,
711// every single operation -- self-test, per-pass seeding, batch descriptor
712// resolve, pool announce, pool query -- opened its own fresh CWebSocket,
713// meaning a single node could fire a dozen-plus brand-new TCP+TLS+WS
714// handshakes at the same 4 relays within any given minute (ThreadNostrSeed's
715// own loop plus the separate ThreadBtfPoolAnnouncer thread, each reconnecting
716// from scratch every cycle). That's exactly what was showing up as constant
717// "handshake refused by relay.damus.io" and "connect ... failed" -- not
718// necessarily dead relays, just this node getting rate-limited by its own
719// reconnect churn.
720//
721// Now every relay-touching call reuses one long-lived socket per relay via
722// this pool, reconnecting only when a socket actually drops. That cuts fresh
723// connections from several per minute down to essentially one per relay for
724// the life of the process.
725struct RelayConn
726{
727 CWebSocket ws;
728 CCriticalSection cs; // serializes use of this relay's socket
729 bool fConnected;
730 RelayConn() : fConnected(false) {}
731};
732static map<string, RelayConn*> g_relayPool;
733static CCriticalSection cs_relayPool;
734
735static RelayConn* GetRelayConn(const string& relay)
736{
737 CRITICAL_BLOCK(cs_relayPool)
738 {
739 map<string, RelayConn*>::iterator it = g_relayPool.find(relay);
740 if (it != g_relayPool.end())
741 return it->second;
742 RelayConn* rc = new RelayConn();
743 g_relayPool[relay] = rc;
744 return rc;
745 }
746 return NULL; // unreachable
747}
748
749// RAII lease on a relay's pooled connection: connects (if not already
750// connected) on construction, and on destruction either keeps the socket
751// alive for the next caller (if the exchange succeeded) or closes it so the
752// next caller reconnects instead of reusing a socket that may be wedged.
753// Caller must set fOk = true once its exchange with the relay completed
754// cleanly.
755class CRelayLease
756{
757public:
758 RelayConn* rc;
759 bool fOk;
760 bool fJustConnected; // true if this lease had to (re)connect, false if reusing a live socket
761
762 CRelayLease(const string& relay, int timeoutSec) : rc(NULL), fOk(false), fJustConnected(false)
763 {
764 rc = GetRelayConn(relay);
765 rc->cs.Enter();
766 if (!rc->fConnected)
767 {
768 rc->ws.Close();
769 rc->fConnected = rc->ws.Connect(relay, timeoutSec);
770 fJustConnected = rc->fConnected;
771 }
772 }
773 ~CRelayLease()
774 {
775 if (!fOk)
776 {
777 rc->ws.Close();
778 rc->fConnected = false;
779 }
780 rc->cs.Leave();
781 }
782 bool Connected() const { return rc->fConnected; }
783 CWebSocket& Ws() { return rc->ws; }
784
785private:
786 CRelayLease(const CRelayLease&);
787 CRelayLease& operator=(const CRelayLease&);
788};
789
790
791// Publish this node's self-certifying `.btf` descriptor: "reach me (this key)
792// via <meeting_node>, encrypting to my x25519 key <enc>". Signed by the node
793// key so only the address's owner can publish it (no hijacking).
794static void PublishDescriptor(CWebSocket& ws, CNostrKey& key)
795{
796 // meeting_node is the rendezvous relay this node's hidden service
797 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us.
798 string meeting_node = BtfActiveRelay();
799 if (meeting_node.empty())
800 {
801 // Not registered anywhere yet. Publishing now would advertise a meeting
802 // node we can't be reached at, and peers would waste dials on it. The
803 // ThreadNostrSeed loop republishes on every pass, so skipping costs
804 // nothing but the delay until ThreadBtfAccept registers.
805 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n");
806 return;
807 }
808 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(),
809 meeting_node, (uint64_t)GetTime());
810 if (desc.empty())
811 return;
812 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) });
813 json ev;
814 if (BuildSignedEvent(key, BTF_DESC_KIND, tags, desc, ev))
815 {
816 json pub = json::array({ "EVENT", ev });
817 ws.SendText(pub.dump());
818 }
819}
820
821// Announce our relay (from /announcerelay=host:port) so every node discovers it
822// automatically -- volunteer relays join the network with no maintainer step.
823static void PublishRelayAnnouncement(CWebSocket& ws, CNostrKey& key)
824{
825 if (strBtfAnnounceRelay.empty())
826 return;
827 json tags = json::array({ json::array({ "d", BTF_RELAY_DTAG }) });
828 json ev;
829 if (BuildSignedEvent(key, BTF_RELAY_KIND, tags, strBtfAnnounceRelay, ev))
830 {
831 json pub = json::array({ "EVENT", ev });
832 ws.SendText(pub.dump());
833 }
834}
835
836static bool PublishPoolAnnouncementOnRelay(CWebSocket& ws, CNostrKey& key,
837 const BtfPoolAnnouncement& ann)
838{
839 if (ann.btfAddress.empty() || ann.poolName.empty())
840 return false;
841
842 json content = json::object();
843 content["btf"] = ann.btfAddress;
844 content["name"] = ann.poolName;
845 if (!ann.dashboardUrl.empty())
846 content["dashboard_url"] = ann.dashboardUrl;
847 content["fee_pct"] = ann.feePercent;
848 content["miners"] = ann.connectedMiners;
849 content["blocks"] = ann.blocksFound;
850 content["hashrate"] = ann.hashRate;
851
852 json tags = json::array({
853 json::array({ "d", BTF_POOL_DTAG }),
854 json::array({ "t", BTF_POOL_DTAG })
855 });
856 json ev;
857 if (!BuildSignedEvent(key, BTF_POOL_KIND, tags, content.dump(), ev))
858 return false;
859 json pub = json::array({ "EVENT", ev });
860 return ws.SendText(pub.dump());
861}
862
863bool BtfPublishPoolAnnouncement(const BtfPoolAnnouncement& ann)
864{
865 if (!EnsureNostrKey())
866 return false;
867
868 bool fPublished = false;
869 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
870 {
871 try
872 {
873 CRelayLease lease(pszNostrRelays[i], 10);
874 if (!lease.Connected())
875 continue;
876 if (PublishPoolAnnouncementOnRelay(lease.Ws(), g_nostrKey, ann))
877 fPublished = lease.fOk = true;
878 }
879 CATCH_PRINT_EXCEPTION("BtfPublishPoolAnnouncement")
880 }
881 return fPublished;
882}
883
884bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncement& out)
885{
886 unsigned char pk[32];
887 if (!btf::ParseAddress(poolBtfAddr, pk))
888 return false;
889
890 if (!EnsureNostrKey())
891 return false;
892
893 int64 bestCreatedAt = 0;
894 bool fFound = false;
895 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
896 {
897 try
898 {
899 CRelayLease lease(pszNostrRelays[i], 10);
900 if (!lease.Connected())
901 continue;
902 CWebSocket& ws = lease.Ws();
903
904 json filter = json::object();
905 filter["authors"] = json::array({ HexEncode(pk, 32) });
906 filter["kinds"] = json::array({ BTF_POOL_KIND });
907 filter["#d"] = json::array({ BTF_POOL_DTAG });
908 filter["limit"] = 1;
909 json req = json::array({ "REQ", "btf-pool-query", filter });
910 if (!ws.SendText(req.dump()))
911 continue;
912
913 for (;;)
914 {
915 string msg;
916 if (!ws.RecvText(msg))
917 break;
918 json j;
919 try { j = json::parse(msg); } catch (...) { continue; }
920 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
921 string type = j[0].get<string>();
922 if (type == "EVENT" && j.size() >= 3)
923 {
924 const json& ev = j[2];
925 if (!ev.contains("content") || !ev["content"].is_string())
926 continue;
927 BtfPoolAnnouncement ann;
928 try
929 {
930 json content = json::parse(ev["content"].get<string>());
931 if (content.contains("btf") && content["btf"].is_string())
932 ann.btfAddress = content["btf"].get<string>();
933 if (content.contains("name") && content["name"].is_string())
934 ann.poolName = content["name"].get<string>();
935 if (content.contains("dashboard_url") && content["dashboard_url"].is_string())
936 ann.dashboardUrl = content["dashboard_url"].get<string>();
937 ann.feePercent = content.value("fee_pct", content.value("fee", 0.0));
938 ann.connectedMiners = content.value("miners", 0);
939 ann.blocksFound = content.value("blocks", 0);
940 ann.hashRate = content.value("hashrate", 0.0);
941 ann.createdAt = ev.value("created_at", (int64)0);
942 }
943 catch (...)
944 {
945 continue;
946 }
947 if (ann.btfAddress == poolBtfAddr && ann.createdAt >= bestCreatedAt)
948 {
949 bestCreatedAt = ann.createdAt;
950 out = ann;
951 fFound = true;
952 }
953 }
954 else if (type == "EOSE")
955 break;
956 }
957 lease.fOk = true;
958 }
959 CATCH_PRINT_EXCEPTION("BtfQueryPoolAnnouncement")
960 }
961
962 return fFound;
963}
964
965static void HandlePoolAnnouncement(const json& ev)
966{
967 if (!ev.contains("content") || !ev["content"].is_string())
968 return;
969
970 BtfPoolAnnouncement ann;
971 try
972 {
973 json content = json::parse(ev["content"].get<string>());
974 if (content.contains("btf") && content["btf"].is_string())
975 ann.btfAddress = content["btf"].get<string>();
976 else if (content.contains("address") && content["address"].is_string())
977 ann.btfAddress = content["address"].get<string>();
978 if (content.contains("name") && content["name"].is_string())
979 ann.poolName = content["name"].get<string>();
980 else if (content.contains("pool_name") && content["pool_name"].is_string())
981 ann.poolName = content["pool_name"].get<string>();
982 if (content.contains("dashboard_url") && content["dashboard_url"].is_string())
983 ann.dashboardUrl = content["dashboard_url"].get<string>();
984 else if (content.contains("dashboard") && content["dashboard"].is_string())
985 ann.dashboardUrl = content["dashboard"].get<string>();
986 ann.feePercent = content.value("fee_pct", content.value("fee", 0.0));
987 ann.connectedMiners = content.value("miners", 0);
988 ann.blocksFound = content.value("blocks", 0);
989 ann.hashRate = content.value("hashrate", 0.0);
990 ann.createdAt = ev.value("created_at", (int64)0);
991 }
992 catch (...)
993 {
994 return;
995 }
996
997 if (ann.btfAddress.empty() || ann.poolName.empty())
998 return;
999
1000 CRITICAL_BLOCK(cs_discoveredPools)
1001 {
1002 map<string, BtfPoolAnnouncement>::iterator it = g_discoveredPools.find(ann.btfAddress);
1003 if (it == g_discoveredPools.end() || ann.createdAt >= it->second.createdAt)
1004 g_discoveredPools[ann.btfAddress] = ann;
1005 }
1006}
1007
1008// Resolve a `.btf` address via a relay: fetch the descriptor published by that
1009// address's key and verify it self-certifies under the decoded pubkey.
1010static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1011 btf::Descriptor& out)
1012{
1013 unsigned char pk[32];
1014 if (!btf::ParseAddress(btfAddr, pk))
1015 return false;
1016 json filter = json::object();
1017 filter["authors"] = json::array({ HexEncode(pk, 32) });
1018 filter["kinds"] = json::array({ BTF_DESC_KIND });
1019 filter["limit"] = 1;
1020 json req = json::array({ "REQ", "btf-resolve", filter });
1021 if (!ws.SendText(req.dump()))
1022 return false;
1023 for (;;)
1024 {
1025 string msg;
1026 if (!ws.RecvText(msg))
1027 break;
1028 json j;
1029 try { j = json::parse(msg); } catch (...) { continue; }
1030 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1031 string t = j[0].get<string>();
1032 if (t == "EVENT" && j.size() >= 3)
1033 {
1034 const json& ev = j[2];
1035 if (ev.contains("content") &&
1036 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1037 return true;
1038 }
1039 else if (t == "EOSE")
1040 break;
1041 }
1042 return false;
1043}
1044
1045// Resolve a `.btf` address into its rendezvous coordinates by querying the
1046// public discovery relays for the owner's self-certified descriptor. Used by
1047// ConnectNodeBtf in net.cpp.
1048bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1049 unsigned char enc_pub[32])
1050{
1051 if (!EnsureNostrKey())
1052 return false;
1053 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
1054 {
1055 try
1056 {
1057 CRelayLease lease(pszNostrRelays[i], 10);
1058 if (!lease.Connected())
1059 continue;
1060 btf::Descriptor d;
1061 if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d))
1062 {
1063 lease.fOk = true; // exchange completed, just no match
1064 continue;
1065 }
1066 lease.fOk = true;
1067 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1068 continue; // descriptor predates the owner's relay config
1069 if (!HexDecode(d.enc, enc_pub, 32))
1070 continue;
1071 meetingHostPort = d.meeting_node;
1072 return true;
1073 }
1074 CATCH_PRINT_EXCEPTION("BtfResolve")
1075 }
1076 return false;
1077}
1078
1079// Resolve MANY .btf addresses in one pass. A single relay connection carries
1080// a REQ with every still-unresolved address's pubkey in "authors", instead of
1081// opening one relay connection per address. This matters because callers like
1082// ConnectDiscoveredBtfPeers dial many candidates in parallel; without
1083// batching, each of those dial threads independently walks the same handful
1084// of public relays on its own, so a pass of N candidates can fire up to
1085// N * nNostrRelays near-simultaneous connections at the same few relays --
1086// enough to get rate-limited or have handshakes refused, which then looks
1087// like "dead peers" even though the descriptors are perfectly resolvable.
1088bool BtfResolveMany(const std::vector<std::string>& btfAddrs,
1089 std::map<std::string, BtfResolvedPeer>& out)
1090{
1091 if (!EnsureNostrKey() || btfAddrs.empty())
1092 return false;
1093
1094 map<string, string> hexToAddr; // pubkey hex -> .btf addr, for matching events back
1095 set<string> pending;
1096 for (const string& a : btfAddrs)
1097 {
1098 unsigned char pk[32];
1099 if (!btf::ParseAddress(a, pk))
1100 continue;
1101 hexToAddr[HexEncode(pk, 32)] = a;
1102 pending.insert(a);
1103 }
1104 if (pending.empty())
1105 return false;
1106
1107 for (int i = 0; i < nNostrRelays && !fShutdown && !pending.empty(); i++)
1108 {
1109 try
1110 {
1111 CRelayLease lease(pszNostrRelays[i], 10);
1112 if (!lease.Connected())
1113 continue;
1114 CWebSocket& ws = lease.Ws();
1115
1116 json authors = json::array();
1117 for (auto& kv : hexToAddr)
1118 if (pending.count(kv.second))
1119 authors.push_back(kv.first);
1120 if (authors.empty())
1121 {
1122 lease.fOk = true;
1123 break;
1124 }
1125
1126 json filter = json::object();
1127 filter["authors"] = authors;
1128 filter["kinds"] = json::array({ BTF_DESC_KIND });
1129 filter["limit"] = (int)authors.size();
1130 json req = json::array({ "REQ", "btf-resolve-many", filter });
1131 if (!ws.SendText(req.dump()))
1132 continue;
1133
1134 for (;;)
1135 {
1136 string msg;
1137 if (!ws.RecvText(msg))
1138 break;
1139 json j;
1140 try { j = json::parse(msg); } catch (...) { continue; }
1141 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1142 string t = j[0].get<string>();
1143 if (t == "EVENT" && j.size() >= 3)
1144 {
1145 const json& ev = j[2];
1146 if (!ev.contains("pubkey") || !ev.contains("content"))
1147 continue;
1148 string hexpk = ev["pubkey"].get<string>();
1149 auto ai = hexToAddr.find(hexpk);
1150 if (ai == hexToAddr.end() || !pending.count(ai->second))
1151 continue; // not one we're waiting on (or already resolved)
1152
1153 unsigned char pk[32];
1154 if (!HexDecode(hexpk, pk, 32))
1155 continue;
1156 btf::Descriptor d;
1157 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d))
1158 continue;
1159 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1160 continue;
1161 BtfResolvedPeer rp;
1162 if (!HexDecode(d.enc, rp.enc_pub, 32))
1163 continue;
1164 rp.meetingHostPort = d.meeting_node;
1165 out[ai->second] = rp;
1166 pending.erase(ai->second);
1167 }
1168 else if (t == "EOSE")
1169 break;
1170 }
1171 lease.fOk = true;
1172 }
1173 CATCH_PRINT_EXCEPTION("BtfResolveMany")
1174 }
1175 return !out.empty();
1176}
1177
1178
1179// ---- per-relay backoff -----------------------------------------------------
1180//
1181// A relay that is down costs the whole discovery loop the full connect timeout,
1182// every round, forever. relay.nostr.band went down and produced 1092 identical
1183// error lines on the seed while adding ten seconds to every pass -- and the
1184// loop is what discovers peers, so a dead relay was slowing down the live ones.
1185//
1186// Back off per relay instead: 30s after the first failure, doubling to a
1187// fifteen-minute ceiling, cleared the moment it answers again. This changes
1188// only when an attempt happens, never whether discovery is correct.
1189struct CRelayBackoff
1190{
1191 int64 nNextTry;
1192 int nFails;
1193 CRelayBackoff() : nNextTry(0), nFails(0) {}
1194};
1195static map<string, CRelayBackoff> mapRelayBackoff;
1196static CCriticalSection cs_relayBackoff;
1197
1198static const int64 RELAY_BACKOFF_BASE_SECS = 30;
1199static const int64 RELAY_BACKOFF_MAX_SECS = 15 * 60;
1200
1201static bool RelayDueForRetry(const string& relay)
1202{
1203 CRITICAL_BLOCK(cs_relayBackoff)
1204 {
1205 map<string, CRelayBackoff>::iterator it = mapRelayBackoff.find(relay);
1206 if (it == mapRelayBackoff.end())
1207 return true;
1208 return GetTime() >= it->second.nNextTry;
1209 }
1210 return true;
1211}
1212
1213static void RelayNoteFailure(const string& relay)
1214{
1215 CRITICAL_BLOCK(cs_relayBackoff)
1216 {
1217 CRelayBackoff& b = mapRelayBackoff[relay];
1218 b.nFails++;
1219 int64 nWait = RELAY_BACKOFF_BASE_SECS;
1220 for (int i = 1; i < b.nFails && nWait < RELAY_BACKOFF_MAX_SECS; i++)
1221 nWait *= 2;
1222 if (nWait > RELAY_BACKOFF_MAX_SECS)
1223 nWait = RELAY_BACKOFF_MAX_SECS;
1224 b.nNextTry = GetTime() + nWait;
1225 // Only on the way in, so a relay that stays down says this once per
1226 // backoff step rather than once per pass.
1227 LogPrint("nostr", "Nostr: %s unreachable (%d in a row), not retrying for %ds\n",
1228 relay.c_str(), b.nFails, (int)nWait);
1229 }
1230}
1231
1232static void RelayNoteSuccess(const string& relay)
1233{
1234 CRITICAL_BLOCK(cs_relayBackoff)
1235 {
1236 map<string, CRelayBackoff>::iterator it = mapRelayBackoff.find(relay);
1237 if (it != mapRelayBackoff.end())
1238 {
1239 if (it->second.nFails > 0)
1240 LogPrint("nostr", "Nostr: %s answering again after %d failure(s)\n",
1241 relay.c_str(), it->second.nFails);
1242 mapRelayBackoff.erase(it);
1243 }
1244 }
1245}
1246
1247// Connect to a relay: publish our .btf descriptor and collect relay data.
1248static bool SeedFromRelay(CNostrKey& key, const string& relay)
1249{
1250 CRelayLease lease(relay, 10);
1251 if (!lease.Connected())
1252 return false;
1253 CWebSocket& ws = lease.Ws();
1254 if (lease.fJustConnected)
1255 LogPrint("nostr", "Nostr: connected to %s\n", relay.c_str());
1256
1257 // Publish our self-certifying .btf descriptor (rendezvous discovery)
1258 PublishDescriptor(ws, key);
1259
1260 // If we run a relay, announce it so others discover it automatically
1261 PublishRelayAnnouncement(ws, key);
1262
1263 // Discover other nodes' .btf descriptors so we can auto-connect anonymously
1264 // (no manual /connectbtf). Each node publishes one addressable descriptor
1265 // (kind 38501, d-tag btf-descriptor); we learn every peer's .btf address by
1266 // deriving it from the event author. Reachability is verified later, when
1267 // the rendezvous tunnel boxes to that key's static x25519 key.
1268 ws.SendText(json::array({ "CLOSE", "bf-sub" }).dump());
1269 {
1270 json dfilter = json::object();
1271 dfilter["kinds"] = json::array({ BTF_DESC_KIND });
1272 dfilter["#d"] = json::array({ BTF_DESC_DTAG });
1273 dfilter["limit"] = 200;
1274 json dreq = json::array({ "REQ", "btf-disc", dfilter });
1275 int nPeers = 0;
1276 if (ws.SendText(dreq.dump()))
1277 {
1278 for (;;)
1279 {
1280 string msg;
1281 if (!ws.RecvText(msg))
1282 break;
1283 json j;
1284 try { j = json::parse(msg); } catch (...) { continue; }
1285 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1286 string type = j[0].get<string>();
1287 if (type == "EVENT" && j.size() >= 3)
1288 {
1289 const json& ev = j[2];
1290 if (!ev.contains("pubkey") || !ev.contains("kind")) continue;
1291 if (ev["kind"].get<int>() != BTF_DESC_KIND) continue;
1292 unsigned char pkb[32];
1293 if (!HexDecode(ev["pubkey"].get<string>(), pkb, 32)) continue;
1294 if (memcmp(pkb, key.xonly, 32) == 0) continue; // not ourselves
1295 string peerAddr = btf::Address(pkb);
1296 int64 createdAt = ev.value("created_at", (int64)0);
1297 bool fNew = false;
1298 CRITICAL_BLOCK(cs_btfPeers)
1299 {
1300 auto it = g_btfPeers.find(peerAddr);
1301 if (it == g_btfPeers.end()) { g_btfPeers[peerAddr] = createdAt; fNew = true; }
1302 else if (createdAt > it->second) it->second = createdAt; // refresh
1303 }
1304 if (fNew && fDebug)
1305 LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str());
1306 if (++nPeers > 500) break;
1307 }
1308 else if (type == "EOSE")
1309 break;
1310 }
1311 }
1312 }
1313
1314 // Discover volunteer relays announced by other operators (kind 38502). Nodes
1315 // add these to their failover list automatically, so anyone can strengthen
1316 // the network without a maintainer editing the seed list. Bad/dead relays
1317 // just fail to pair and get skipped -- and a relay is trustless anyway (it
1318 // only forwards echan ciphertext and can't read or MITM the traffic).
1319 ws.SendText(json::array({ "CLOSE", "btf-disc" }).dump());
1320 {
1321 json rfilter = json::object();
1322 rfilter["kinds"] = json::array({ BTF_RELAY_KIND });
1323 rfilter["#d"] = json::array({ BTF_RELAY_DTAG });
1324 rfilter["limit"] = 200;
1325 json rreq = json::array({ "REQ", "btf-relays", rfilter });
1326 if (ws.SendText(rreq.dump()))
1327 {
1328 for (;;)
1329 {
1330 string msg;
1331 if (!ws.RecvText(msg))
1332 break;
1333 json j;
1334 try { j = json::parse(msg); } catch (...) { continue; }
1335 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1336 string type = j[0].get<string>();
1337 if (type == "EVENT" && j.size() >= 3)
1338 {
1339 const json& ev = j[2];
1340 if (!ev.contains("content") || !ev.contains("kind")) continue;
1341 if (ev["kind"].get<int>() != BTF_RELAY_KIND) continue;
1342 string r = ev["content"].get<string>();
1343 if (!LooksLikeRelay(r)) continue;
1344 bool fNew = false;
1345 CRITICAL_BLOCK(cs_discoveredRelays)
1346 if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS)
1347 fNew = g_discoveredRelays.insert(r).second;
1348 if (fNew && fDebug)
1349 LogPrint("nostr", "Nostr: discovered relay %s\n", r.c_str());
1350 }
1351 else if (type == "EOSE")
1352 break;
1353 }
1354 }
1355 }
1356
1357 // Discover live pool announcements. These are expiring status beacons, not
1358 // permanent directory entries, so the GUI can show only fresh pools.
1359 ws.SendText(json::array({ "CLOSE", "btf-pools" }).dump());
1360 {
1361 json pfilter = json::object();
1362 pfilter["kinds"] = json::array({ BTF_POOL_KIND });
1363 pfilter["#d"] = json::array({ BTF_POOL_DTAG });
1364 pfilter["limit"] = 200;
1365 json preq = json::array({ "REQ", "btf-pools", pfilter });
1366 if (ws.SendText(preq.dump()))
1367 {
1368 for (;;)
1369 {
1370 string msg;
1371 if (!ws.RecvText(msg))
1372 break;
1373 json j;
1374 try { j = json::parse(msg); } catch (...) { continue; }
1375 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1376 string type = j[0].get<string>();
1377 if (type == "EVENT" && j.size() >= 3)
1378 HandlePoolAnnouncement(j[2]);
1379 else if (type == "EOSE")
1380 break;
1381 }
1382 }
1383 }
1384 lease.fOk = true;
1385 return true;
1386}
1387
1388volatile bool gAnnounceNow = false;
1389
1390void ThreadBtfPoolAnnouncer(void*)
1391{
1392 LogPrint("pool", "ThreadBtfPoolAnnouncer started\n");
1393 while (!fShutdown)
1394 {
1395 if (nMineMode == MINE_OPERATOR && gPoolRunning)
1396 {
1397 BtfPoolAnnouncement ann;
1398 ann.btfAddress = BtfLocalAddress();
1399 ann.poolName = strPoolName;
1400 ann.dashboardUrl = strPoolDashboardUrl;
1401 ann.feePercent = dPoolFeePercent;
1402 uint64 roundShares = 0;
1403 double totalHashRate = 0.0;
1404 GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate);
1405 ann.hashRate = totalHashRate;
1406 ann.createdAt = GetTime();
1407 if (!ann.btfAddress.empty() && !ann.poolName.empty())
1408 BtfPublishPoolAnnouncement(ann);
1409 }
1410
1411 for (int i = 0; i < 60 && !fShutdown && !gAnnounceNow; i++)
1412 Sleep(1000);
1413 gAnnounceNow = false;
1414 }
1415}
1416
1417
1418// Per-peer backoff: don't redial a .btf that just failed until this time. Keeps
1419// dead/stale descriptors (e.g. abandoned test identities that linger on the
1420// relays) from consuming the whole per-pass dial budget and starving the live
1421// peers. Backoff grows on repeated failures, capped.
1422//
1423// g_btfPeerFails also doubles as the priority signal for dial ordering: a peer
1424// we've connected to before (present in g_btfPeerEverConnected) is known-good
1425// and goes first; a peer we've never tried goes next; a peer with a growing
1426// fail count is known-bad and goes last, worst offenders furthest back. This
1427// replaces the old behavior where the whole candidate list was shuffled right
1428// after being sorted by freshness, which made the freshness sort a no-op and
1429// gave dead peers equal footing with everyone else.
1430static map<string, int64> g_btfPeerBackoff; // addr -> next-try time
1431static map<string, int> g_btfPeerFails; // addr -> consecutive failures
1432static set<string> g_btfPeerEverConnected; // addr -> has succeeded at least once
1433static CCriticalSection cs_btfPeerState;
1434
1435// Backoff cap for repeatedly-dead peers. The old 15-minute cap meant a
1436// descriptor that has failed many times in a row cycles back into the dial
1437// pool just as often as a peer we've never tried, wasting dial budget on
1438// known-dead addresses every pass. Once a peer has failed enough times to
1439// be "chronic", stretch the cap much further so it consumes budget far less
1440// often while still getting retried eventually in case it comes back online.
1441static const int64 BTF_BACKOFF_CAP_SECS = 15 * 60; // 15 min: initial ramp cap
1442static const int64 BTF_BACKOFF_CAP_SECS_DEAD = 6 * 60 * 60; // 6 hr: cap for chronic failures
1443static const int BTF_CHRONIC_FAIL_THRESHOLD = 6; // fails before "chronic" cap kicks in
1444
1445// Dial the .btf peers discovered on the relays, up to a target connection count.
1446// Connections are attempted in parallel so a single slow/dead relay doesn't
1447// stall the entire pass. Each candidate gets its own thread; all threads start
1448// at once and we wait for them to finish before returning.
1449static void ConnectDiscoveredBtfPeers()
1450{
1451 vector<pair<int64,string>> peersWithAge;
1452 int nTotal = 0;
1453 CRITICAL_BLOCK(cs_btfPeers)
1454 {
1455 nTotal = g_btfPeers.size();
1456 for (auto& kv : g_btfPeers)
1457 peersWithAge.push_back({kv.second, kv.first});
1458 }
1459 int64 cutoff = GetTime() - BTF_PEER_MAX_AGE;
1460 int nFresh = 0;
1461 vector<pair<int64,string>> fresh; // (createdAt, addr), newest first
1462 for (auto& p : peersWithAge)
1463 {
1464 if (p.first < cutoff)
1465 continue;
1466 fresh.push_back(p);
1467 nFresh++;
1468 }
1469 if (!fresh.empty())
1470 LogPrint("nostr", "Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal);
1471
1472 // Drop stale peers so we stop retrying dead descriptors until they refresh.
1473 {
1474 CRITICAL_BLOCK(cs_btfPeers)
1475 {
1476 for (map<string, int64>::iterator it = g_btfPeers.begin(); it != g_btfPeers.end(); )
1477 {
1478 if (it->second < cutoff)
1479 it = g_btfPeers.erase(it);
1480 else
1481 ++it;
1482 }
1483 }
1484 }
1485
1486 // Sort newest descriptor first -- recently-active nodes are much more
1487 // likely to be online right now.
1488 sort(fresh.begin(), fresh.end(),
1489 [](const pair<int64,string>& a, const pair<int64,string>& b){ return a.first > b.first; });
1490
1491 // Reorder into tiers: known-good (succeeded before) first, untried next,
1492 // known-bad (has a fail count) last -- worst offenders furthest back.
1493 // Freshness order is preserved *within* each tier, so recency still
1494 // matters, but a peer we know is dead no longer gets equal footing with
1495 // a peer we've never tried or one that's worked before.
1496 vector<string> tierGood, tierUntried, tierBad;
1497 CRITICAL_BLOCK(cs_btfPeerState)
1498 {
1499 for (auto& p : fresh)
1500 {
1501 const string& addr = p.second;
1502 if (g_btfPeerEverConnected.count(addr))
1503 tierGood.push_back(addr);
1504 else if (g_btfPeerFails.find(addr) == g_btfPeerFails.end())
1505 tierUntried.push_back(addr);
1506 else
1507 tierBad.push_back(addr);
1508 }
1509 // Within known-bad, put fewer-fails peers ahead of chronic offenders.
1510 sort(tierBad.begin(), tierBad.end(),
1511 [](const string& a, const string& b){ return g_btfPeerFails[a] < g_btfPeerFails[b]; });
1512 }
1513
1514 vector<string> peers;
1515 peers.reserve(fresh.size());
1516 peers.insert(peers.end(), tierGood.begin(), tierGood.end());
1517 peers.insert(peers.end(), tierUntried.begin(), tierUntried.end());
1518 peers.insert(peers.end(), tierBad.begin(), tierBad.end());
1519
1520 // Pick candidates for this pass (skip those in backoff, cap at budget).
1521 //
1522 // The tiered order above is deliberately biased toward peers we know
1523 // are good -- that's the right default, since dialing untested/bad
1524 // peers wastes attempts. But if every node on the network applies the
1525 // same bias, everyone converges on the same small set of "known good"
1526 // peers as neighbors: that correlates peer sets across the network,
1527 // which is exactly what standard random-graph connectivity results
1528 // assume you avoid. If that shared hub set has a bad day (e.g. one
1529 // relay outage), many nodes lose the same edges at once instead of
1530 // losing independent, uncorrelated edges.
1531 //
1532 // So: fill most of the pass from the reliable/tiered order, but reserve
1533 // a slice for peers picked at random from the *entire* fresh pool
1534 // (not just the front of the sorted list). Same pattern Bitcoin's own
1535 // peer manager and Kademlia-style DHTs use -- sticky reliable peers
1536 // plus a random sample, not one or the other.
1537 int64 now = GetTime();
1538 int nBudget = (int)BTF_DIALS_PER_PASS;
1539 {
1540 int nRoom = (int)BTF_TARGET_CONN - (int)vNodes.size();
1541 if (nRoom < nBudget) nBudget = nRoom;
1542 }
1543 if (nBudget < 0) nBudget = 0;
1544 int nRandomSlots = (nBudget > 0) ? max(1, nBudget * 3 / 10) : 0;
1545 int nSortedSlots = nBudget - nRandomSlots;
1546
1547 vector<string> candidates;
1548 set<string> setChosen;
1549 CRITICAL_BLOCK(cs_btfPeerState)
1550 {
1551 // Reliability slice: walk the tiered (good -> untried -> bad) order.
1552 for (const string& addr : peers)
1553 {
1554 if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break;
1555 if ((int)candidates.size() >= nSortedSlots) break;
1556 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr);
1557 if (bi != g_btfPeerBackoff.end() && now < bi->second)
1558 continue;
1559 candidates.push_back(addr);
1560 setChosen.insert(addr);
1561 }
1562
1563 // Diversity slice: random sample from the full fresh pool, so the
1564 // dial isn't limited to whatever the tiered sort put up front.
1565 vector<string> pool;
1566 for (auto& p : fresh)
1567 {
1568 const string& addr = p.second;
1569 if (setChosen.count(addr))
1570 continue;
1571 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr);
1572 if (bi != g_btfPeerBackoff.end() && now < bi->second)
1573 continue;
1574 pool.push_back(addr);
1575 }
1576 int nWant = min(nRandomSlots, (int)pool.size());
1577 for (int i = 0; i < nWant; i++)
1578 {
1579 if ((int)vNodes.size() + (int)candidates.size() >= (int)BTF_TARGET_CONN) break;
1580 if ((int)candidates.size() >= nBudget) break;
1581 size_t j = i + (size_t)GetRand((uint64)(pool.size() - i));
1582 std::swap(pool[i], pool[j]);
1583 candidates.push_back(pool[i]);
1584 setChosen.insert(pool[i]);
1585 }
1586 }
1587
1588 if (candidates.empty()) return;
1589
1590 // Resolve every candidate's descriptor in one batched pass (a handful of
1591 // relay round-trips total, one REQ per relay covering all candidates)
1592 // instead of each dial thread independently walking the relay list on
1593 // its own. That per-thread approach meant a 12-candidate pass could fire
1594 // up to 12 * nNostrRelays near-simultaneous connections at the same
1595 // handful of public relays -- enough to trip rate limits or get
1596 // handshakes refused, which then looks exactly like "dead peers" even
1597 // though the descriptors were perfectly resolvable.
1598 map<string, BtfResolvedPeer> resolved;
1599 for (const string& addr : candidates)
1600 BtfChurnNoteResolveAttempt();
1601 BtfResolveMany(candidates, resolved);
1602
1603 // Candidates whose descriptor didn't resolve this pass are recorded as a
1604 // failure right away (no point spawning a tunnel thread with nothing to
1605 // dial), same bookkeeping as a failed connect attempt.
1606 vector<string> toDial;
1607 CRITICAL_BLOCK(cs_btfPeerState)
1608 {
1609 for (const string& addr : candidates)
1610 {
1611 if (resolved.count(addr))
1612 {
1613 BtfChurnNoteResolveResult(true);
1614 toDial.push_back(addr);
1615 continue;
1616 }
1617 BtfChurnNoteResolveResult(false);
1618 int n = ++g_btfPeerFails[addr];
1619 int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD
1620 : BTF_BACKOFF_CAP_SECS;
1621 int64 secs = 30;
1622 for (int k = 1; k < n && secs < cap; k++) secs *= 2;
1623 if (secs > cap) secs = cap;
1624 g_btfPeerBackoff[addr] = GetTime() + secs;
1625 }
1626 }
1627 if (toDial.empty()) return;
1628
1629 // Launch one thread per resolved candidate -- all connect attempts run in
1630 // parallel, but purely against each peer's own rendezvous meeting node,
1631 // not the shared Nostr relays, so there's no thundering-herd effect here.
1632 // Each thread writes its result back via shared state guarded by cs_btfPeerState.
1633 struct DialCtx { string addr; BtfResolvedPeer rp; };
1634 vector<DialCtx*> ctxs;
1635 for (const string& addr : toDial)
1636 {
1637 DialCtx* ctx = new DialCtx{addr, resolved[addr]};
1638 ctxs.push_back(ctx);
1639 _beginthread([](void* arg) {
1640 DialCtx* ctx = (DialCtx*)arg;
1641 string addr = ctx->addr;
1642 string meeting = ctx->rp.meetingHostPort;
1643 unsigned char enc_pub[32];
1644 memcpy(enc_pub, ctx->rp.enc_pub, 32);
1645 delete ctx;
1646
1647 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub);
1648 CRITICAL_BLOCK(cs_btfPeerState)
1649 {
1650 if (pnode)
1651 {
1652 if (!pnode->fNetworkNode)
1653 pnode->fNetworkNode = true;
1654 else
1655 pnode->Release();
1656 g_btfPeerBackoff.erase(addr);
1657 g_btfPeerFails.erase(addr);
1658 g_btfPeerEverConnected.insert(addr);
1659 }
1660 else
1661 {
1662 int n = ++g_btfPeerFails[addr];
1663 int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD
1664 : BTF_BACKOFF_CAP_SECS;
1665 int64 secs = 30;
1666 for (int k = 1; k < n && secs < cap; k++) secs *= 2;
1667 if (secs > cap) secs = cap;
1668 g_btfPeerBackoff[addr] = GetTime() + secs;
1669 }
1670 }
1671 }, 0, ctx);
1672 }
1673
1674 // Wait long enough for all parallel dials to finish (each has a 10s socket
1675 // timeout, so 12s gives them all time to resolve).
1676 for (int i = 0; i < 12 && !fShutdown; i++)
1677 Sleep(1000);
1678}
1679
1680
1681void ThreadNostrSeed(void* parg)
1682{
1683 LogPrint("nostr", "ThreadNostrSeed started\n");
1684
1685 if (!EnsureNostrKey())
1686 {
1687 LogPrint("nostr", "Nostr: failed to initialize secp256k1 key\n");
1688 return;
1689 }
1690 CNostrKey& key = g_nostrKey;
1691 LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str());
1692 LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str());
1693
1694 // The self-test below publishes a real descriptor, and PublishDescriptor now
1695 // refuses to publish before ThreadBtfAccept has registered a rendezvous. Give
1696 // registration a bounded head start so the test measures the publish/resolve
1697 // chain instead of losing the race against it.
1698 for (int i = 0; i < 60 && !fShutdown && BtfActiveRelay().empty(); i++)
1699 Sleep(1000);
1700
1701 // One-time self-test of the rendezvous: publish our descriptor to a relay,
1702 // then resolve our OWN .btf address back and verify it self-certifies. Proves
1703 // the .btf publish/resolve/verify chain works against real public relays.
1704 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
1705 {
1706 try
1707 {
1708 CRelayLease lease(pszNostrRelays[i], 10);
1709 if (!lease.Connected()) continue;
1710 CWebSocket& ws = lease.Ws();
1711 PublishDescriptor(ws, key);
1712 Sleep(700); // let the relay store the replaceable event
1713 btf::Descriptor d;
1714 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d))
1715 {
1716 LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n",
1717 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str());
1718 lease.fOk = true;
1719 break;
1720 }
1721 lease.fOk = true;
1722 }
1723 CATCH_PRINT_EXCEPTION("btf self-test")
1724 }
1725
1726 // OpenSSL is already initialized by the rest of the app; TLS_client_method is enough.
1727 loop
1728 {
1729 for (int i = 0; i < nNostrRelays; i++)
1730 {
1731 if (fShutdown) return;
1732 if (!RelayDueForRetry(pszNostrRelays[i]))
1733 continue;
1734 try
1735 {
1736 if (SeedFromRelay(key, pszNostrRelays[i]))
1737 RelayNoteSuccess(pszNostrRelays[i]);
1738 else
1739 RelayNoteFailure(pszNostrRelays[i]);
1740 }
1741 CATCH_PRINT_EXCEPTION("SeedFromRelay")
1742 }
1743
1744 // Auto-connect to discovered .btf peers over the rendezvous relay -- the
1745 // anonymous, CGNAT-friendly replacement for the old IP-based connect. No
1746 // manual /connectbtf needed: nodes find each other purely via Nostr.
1747 try
1748 {
1749 ConnectDiscoveredBtfPeers();
1750 }
1751 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers")
1752
1753 if (nMineMode == MINE_OPERATOR && gPoolRunning)
1754 {
1755 BtfPoolAnnouncement ann;
1756 ann.btfAddress = BtfLocalAddress();
1757 ann.poolName = strPoolName;
1758 ann.dashboardUrl = strPoolDashboardUrl;
1759 ann.feePercent = dPoolFeePercent;
1760 uint64 roundShares = 0;
1761 double totalHashRate = 0.0;
1762 GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate);
1763 ann.hashRate = totalHashRate;
1764 ann.createdAt = GetTime();
1765 if (!ann.btfAddress.empty() && !ann.poolName.empty())
1766 BtfPublishPoolAnnouncement(ann);
1767 }
1768
1769 // Re-discover periodically. Sleep less aggressively when
1770 // we have no connections yet so we find peers faster on startup.
1771 int nSleepSecs = vNodes.empty() ? 20 : 60;
1772 for (int i = 0; i < nSleepSecs && !fShutdown; i++)
1773 Sleep(1000);
1774 }
1775}