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:
Diffstat (limited to 'src/nostr.cpp')
-rw-r--r--src/nostr.cpp838
1 files changed, 838 insertions, 0 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
new file mode 100644
index 0000000..8e2a07d
--- /dev/null
+++ b/src/nostr.cpp
@@ -0,0 +1,838 @@
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#define SECP256K1_STATIC
11#include <secp256k1.h>
12#include <secp256k1_schnorrsig.h>
13#include <secp256k1_extrakeys.h>
14
15// util.h redefines snprintf as a macro (2009 MSVC compat); this breaks
16// nlohmann/json, which calls std::snprintf. Undo the macro just here.
17#ifdef snprintf
18#undef snprintf
19#endif
20#include <nlohmann/json.hpp>
21
22#include "btfaddr.h"
23#include "btfchan.h"
24
25using json = nlohmann::json;
26
27// Bitflash rendezvous descriptor: parameterized-replaceable Nostr event so each
28// node keeps exactly one current descriptor (keyed by author pubkey).
29static const int BTF_DESC_KIND = 38501;
30// "-2" scopes the 2026 stable relaunch: new nodes discover only each other, and
31// the abandoned test .btf descriptors lingering on the relays become invisible.
32static const char* BTF_DESC_DTAG = "btf-descriptor-2";
33
34// Nostr network/scope for Bitflash. Events are replaceable (NIP-78).
35static const int NOSTR_KIND = 30078;
36static const char* NOSTR_DTAG = "bitflash-mainnet-2";
37
38// Anonymous auto-discovery: how many peers to keep connected via .btf rendezvous
39// before we stop dialing more, and how many new dials to attempt per cycle.
40static const unsigned int BTF_TARGET_CONN = 8;
41static const int BTF_DIALS_PER_PASS = 4;
42
43// Other nodes' .btf addresses learned from their descriptors on the relays.
44static set<string> g_btfPeers;
45static CCriticalSection cs_btfPeers;
46
47// Public discovery relays. Tunable.
48const char* pszNostrRelays[] = {
49 "wss://relay.damus.io",
50 "wss://nos.lol",
51 "wss://relay.nostr.band",
52 "wss://relay.snort.social",
53};
54const int nNostrRelays = ARRAYLEN(pszNostrRelays);
55
56// Rendezvous meeting relay this node registers at (ThreadBtfAccept in net.cpp)
57// and advertises in its .btf descriptor. Seed default; /rvrelay overrides.
58string strBtfMeetingRelay = "90.156.222.107:8434";
59
60
61//
62// Utilities
63//
64static string HexEncode(const unsigned char* p, size_t n)
65{
66 static const char* h = "0123456789abcdef";
67 string s;
68 s.reserve(n * 2);
69 for (size_t i = 0; i < n; i++)
70 {
71 s += h[p[i] >> 4];
72 s += h[p[i] & 0xf];
73 }
74 return s;
75}
76
77static bool HexDecode(const string& s, unsigned char* out, size_t n)
78{
79 if (s.size() != n * 2)
80 return false;
81 for (size_t i = 0; i < n; i++)
82 {
83 unsigned int b;
84 if (sscanf(s.c_str() + i * 2, "%2x", &b) != 1)
85 return false;
86 out[i] = (unsigned char)b;
87 }
88 return true;
89}
90
91static string Base64Encode(const unsigned char* p, size_t n)
92{
93 static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
94 string s;
95 for (size_t i = 0; i < n; i += 3)
96 {
97 unsigned int b = p[i] << 16;
98 if (i + 1 < n) b |= p[i + 1] << 8;
99 if (i + 2 < n) b |= p[i + 2];
100 s += tbl[(b >> 18) & 0x3f];
101 s += tbl[(b >> 12) & 0x3f];
102 s += (i + 1 < n) ? tbl[(b >> 6) & 0x3f] : '=';
103 s += (i + 2 < n) ? tbl[b & 0x3f] : '=';
104 }
105 return s;
106}
107
108
109//
110// Node Nostr key -- secp256k1 x-only (BIP340), persisted in <appdir>/nostr.key
111//
112class CNostrKey
113{
114public:
115 secp256k1_context* ctx;
116 secp256k1_keypair keypair;
117 unsigned char seckey[32]; // node's secp256k1 secret (for descriptor signing)
118 unsigned char xonly[32]; // node's x-only pubkey (also its .btf identity)
119 unsigned char enc_pk[32]; // node's x25519 public key (echan `enc` field)
120 unsigned char enc_sk[32]; // node's x25519 secret key
121 bool fValid;
122
123 CNostrKey() : ctx(NULL), fValid(false) {}
124
125 ~CNostrKey()
126 {
127 if (ctx) secp256k1_context_destroy(ctx);
128 }
129
130 // Load or generate a 32-byte key persisted as hex in <appdir>/<name>.
131 static void LoadOrGenSecp(secp256k1_context* ctx, const string& file, unsigned char sk[32])
132 {
133 FILE* f = fopen(file.c_str(), "rb");
134 string strHex;
135 if (f) { char buf[65] = {0}; fread(buf, 1, 64, f); fclose(f); strHex = buf; }
136 if (!HexDecode(strHex, sk, 32) || !secp256k1_ec_seckey_verify(ctx, sk))
137 {
138 do { RAND_bytes(sk, sizeof(unsigned char) * 32); } while (!secp256k1_ec_seckey_verify(ctx, sk));
139 FILE* fw = fopen(file.c_str(), "wb");
140 if (fw) { string h = HexEncode(sk, 32); fwrite(h.c_str(), 1, h.size(), fw); fclose(fw); }
141 }
142 }
143
144 bool Init()
145 {
146 ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
147 if (!ctx)
148 return false;
149 unsigned char seed[32];
150 RAND_bytes(seed, sizeof(seed));
151 secp256k1_context_randomize(ctx, seed);
152
153 LoadOrGenSecp(ctx, GetAppDir() + "/nostr.key", seckey);
154
155 if (!secp256k1_keypair_create(ctx, &keypair, seckey))
156 return false;
157 secp256k1_xonly_pubkey xpub;
158 if (!secp256k1_keypair_xonly_pub(ctx, &xpub, NULL, &keypair))
159 return false;
160 secp256k1_xonly_pubkey_serialize(ctx, xonly, &xpub);
161
162 // x25519 encryption keypair for the end-to-end channel, persisted separately.
163 btf::ChanInit();
164 string strEncFile = GetAppDir() + "/btf_enc.key";
165 FILE* fe = fopen(strEncFile.c_str(), "rb");
166 bool fLoaded = false;
167 if (fe)
168 {
169 char buf[65] = {0};
170 if (fread(buf, 1, 64, fe) == 64 && HexDecode(string(buf), enc_sk, 32))
171 fLoaded = true;
172 fclose(fe);
173 }
174 if (fLoaded)
175 {
176 // Derive the public key from the stored secret (stable identity).
177 btf::ChanPublicFromSecret(enc_pk, enc_sk);
178 }
179 else
180 {
181 btf::ChanKeypair(enc_pk, enc_sk);
182 FILE* few = fopen(strEncFile.c_str(), "wb");
183 if (few) { string h = HexEncode(enc_sk, 32); fwrite(h.c_str(), 1, h.size(), few); fclose(few); }
184 }
185
186 fValid = true;
187 return true;
188 }
189
190 string PubKeyHex() const { return HexEncode(xonly, 32); }
191 string EncPubHex() const { return HexEncode(enc_pk, 32); }
192 string BtfAddress() const { return btf::Address(xonly); }
193
194 bool SignId(const unsigned char id[32], string& sigHexOut)
195 {
196 unsigned char sig[64];
197 unsigned char aux[32];
198 RAND_bytes(aux, sizeof(aux));
199 if (!secp256k1_schnorrsig_sign32(ctx, sig, id, &keypair, aux))
200 return false;
201 sigHexOut = HexEncode(sig, 64);
202 return true;
203 }
204};
205
206// The node's single Nostr/.btf identity, shared by the seeding thread here and
207// the rendezvous transport in net.cpp. Lazily initialized under a lock; the
208// secp256k1 context is immutable after Init, so concurrent use is safe.
209static CNostrKey g_nostrKey;
210static CCriticalSection cs_nostrKey;
211
212static bool EnsureNostrKey()
213{
214 CRITICAL_BLOCK(cs_nostrKey)
215 if (!g_nostrKey.fValid)
216 g_nostrKey.Init();
217 return g_nostrKey.fValid;
218}
219
220bool BtfGetIdentity(unsigned char pubkey[32], unsigned char enc_sk[32])
221{
222 if (!EnsureNostrKey())
223 return false;
224 memcpy(pubkey, g_nostrKey.xonly, 32);
225 memcpy(enc_sk, g_nostrKey.enc_sk, 32);
226 return true;
227}
228
229std::string BtfLocalAddress()
230{
231 if (!EnsureNostrKey())
232 return "";
233 return g_nostrKey.BtfAddress();
234}
235
236
237// Build and sign a Nostr event (NIP-01). Returns the event JSON.
238static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags,
239 const string& content, json& eventOut)
240{
241 int64 created = GetTime();
242 // Canonical serialization for the id: [0,pubkey,created_at,kind,tags,content]
243 json ser = json::array({ 0, key.PubKeyHex(), created, kind, tags, content });
244 string s = ser.dump();
245
246 unsigned char id[32];
247 SHA256((const unsigned char*)s.data(), s.size(), id);
248
249 string sigHex;
250 if (!key.SignId(id, sigHex))
251 return false;
252
253 eventOut = json::object();
254 eventOut["id"] = HexEncode(id, 32);
255 eventOut["pubkey"] = key.PubKeyHex();
256 eventOut["created_at"] = created;
257 eventOut["kind"] = kind;
258 eventOut["tags"] = tags;
259 eventOut["content"] = content;
260 eventOut["sig"] = sigHex;
261 return true;
262}
263
264
265//
266// Minimal WebSocket client over TLS (wss) or TCP (ws), blocking with timeout.
267//
268class CWebSocket
269{
270public:
271 SOCKET hSocket;
272 SSL_CTX* sslctx;
273 SSL* ssl;
274 bool fTls;
275 string recvbuf;
276
277 CWebSocket() : hSocket(INVALID_SOCKET), sslctx(NULL), ssl(NULL), fTls(false) {}
278 ~CWebSocket() { Close(); }
279
280 void Close()
281 {
282 if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = NULL; }
283 if (sslctx) { SSL_CTX_free(sslctx); sslctx = NULL; }
284 if (hSocket != INVALID_SOCKET) { closesocket(hSocket); hSocket = INVALID_SOCKET; }
285 }
286
287 int RawRead(char* buf, int len)
288 {
289 if (fTls) return SSL_read(ssl, buf, len);
290 return recv(hSocket, buf, len, 0);
291 }
292
293 bool RawWrite(const char* buf, int len)
294 {
295 int off = 0;
296 while (off < len)
297 {
298 int n = fTls ? SSL_write(ssl, buf + off, len - off)
299 : send(hSocket, buf + off, len - off, 0);
300 if (n <= 0) return false;
301 off += n;
302 }
303 return true;
304 }
305
306 // Connect and perform the WebSocket handshake. url = wss://host[:port]/path
307 bool Connect(const string& url, int timeoutSec)
308 {
309 string u = url;
310 int port = 443;
311 fTls = true;
312 if (u.compare(0, 6, "wss://") == 0) { u = u.substr(6); fTls = true; port = 443; }
313 else if (u.compare(0, 5, "ws://") == 0) { u = u.substr(5); fTls = false; port = 80; }
314 else return false;
315
316 string host, path = "/";
317 size_t slash = u.find('/');
318 if (slash != string::npos) { host = u.substr(0, slash); path = u.substr(slash); }
319 else host = u;
320 size_t colon = host.find(':');
321 if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); }
322
323 // Resolve and connect (TCP)
324 struct addrinfo hints, *res = NULL;
325 memset(&hints, 0, sizeof(hints));
326 hints.ai_family = AF_INET;
327 hints.ai_socktype = SOCK_STREAM;
328 char portstr[16]; sprintf(portstr, "%d", port);
329 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
330 return error("Nostr: getaddrinfo %s failed", host.c_str());
331 hSocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
332 if (hSocket == INVALID_SOCKET) { freeaddrinfo(res); return false; }
333
334 // Socket receive/send timeout: Windows takes a DWORD of milliseconds;
335 // POSIX takes a struct timeval.
336#ifdef _WIN32
337 DWORD tv = timeoutSec * 1000;
338 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
339 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
340#else
341 struct timeval tv;
342 tv.tv_sec = timeoutSec;
343 tv.tv_usec = 0;
344 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
345 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
346#endif
347
348 if (connect(hSocket, res->ai_addr, (int)res->ai_addrlen) != 0)
349 {
350 freeaddrinfo(res);
351 return error("Nostr: connect %s:%d failed", host.c_str(), port);
352 }
353 freeaddrinfo(res);
354
355 // TLS
356 if (fTls)
357 {
358 sslctx = SSL_CTX_new(TLS_client_method());
359 if (!sslctx) return false;
360 ssl = SSL_new(sslctx);
361 SSL_set_fd(ssl, (int)hSocket);
362 SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI
363 if (SSL_connect(ssl) != 1)
364 return error("Nostr: TLS handshake with %s failed", host.c_str());
365 }
366
367 // WebSocket handshake (HTTP Upgrade)
368 unsigned char keybytes[16];
369 RAND_bytes(keybytes, sizeof(keybytes));
370 string wskey = Base64Encode(keybytes, sizeof(keybytes));
371 string req =
372 "GET " + path + " HTTP/1.1\r\n"
373 "Host: " + host + "\r\n"
374 "Upgrade: websocket\r\n"
375 "Connection: Upgrade\r\n"
376 "Sec-WebSocket-Key: " + wskey + "\r\n"
377 "Sec-WebSocket-Version: 13\r\n\r\n";
378 if (!RawWrite(req.data(), (int)req.size()))
379 return false;
380
381 // Read the handshake response until \r\n\r\n
382 string resp;
383 char c;
384 while (resp.find("\r\n\r\n") == string::npos)
385 {
386 int n = RawRead(&c, 1);
387 if (n <= 0) return error("Nostr: no handshake response from %s", host.c_str());
388 resp += c;
389 if (resp.size() > 4096) break;
390 }
391 if (resp.find(" 101 ") == string::npos)
392 return error("Nostr: handshake refused by %s", host.c_str());
393 return true;
394 }
395
396 // Send a text message (masked frame, as required by the protocol)
397 bool SendText(const string& msg)
398 {
399 string frame;
400 frame += (char)0x81; // FIN + opcode texto
401 size_t len = msg.size();
402 if (len < 126)
403 frame += (char)(0x80 | len);
404 else if (len <= 0xffff)
405 {
406 frame += (char)(0x80 | 126);
407 frame += (char)((len >> 8) & 0xff);
408 frame += (char)(len & 0xff);
409 }
410 else
411 {
412 frame += (char)(0x80 | 127);
413 for (int i = 7; i >= 0; i--) frame += (char)((len >> (8 * i)) & 0xff);
414 }
415 unsigned char mask[4];
416 RAND_bytes(mask, 4);
417 frame.append((char*)mask, 4);
418 string masked = msg;
419 for (size_t i = 0; i < masked.size(); i++)
420 masked[i] ^= mask[i & 3];
421 frame += masked;
422 return RawWrite(frame.data(), (int)frame.size());
423 }
424
425 // Receive the next complete text message. Returns false on error/timeout.
426 bool RecvText(string& out)
427 {
428 out.clear();
429 for (;;)
430 {
431 unsigned char hdr[2];
432 if (!ReadN((char*)hdr, 2)) return false;
433 bool fin = hdr[0] & 0x80;
434 int opcode = hdr[0] & 0x0f;
435 bool masked = hdr[1] & 0x80;
436 uint64 len = hdr[1] & 0x7f;
437 if (len == 126)
438 {
439 unsigned char e[2];
440 if (!ReadN((char*)e, 2)) return false;
441 len = (e[0] << 8) | e[1];
442 }
443 else if (len == 127)
444 {
445 unsigned char e[8];
446 if (!ReadN((char*)e, 8)) return false;
447 len = 0;
448 for (int i = 0; i < 8; i++) len = (len << 8) | e[i];
449 }
450 unsigned char mask[4] = {0,0,0,0};
451 if (masked && !ReadN((char*)mask, 4)) return false;
452
453 string payload;
454 payload.resize((size_t)len);
455 if (len && !ReadN(&payload[0], (int)len)) return false;
456 if (masked)
457 for (size_t i = 0; i < payload.size(); i++)
458 payload[i] ^= mask[i & 3];
459
460 if (opcode == 0x8) return false; // close
461 if (opcode == 0x9) { SendPong(payload); continue; } // ping
462 if (opcode == 0xA) continue; // pong
463 out += payload; // text/continuation
464 if (fin) return true;
465 }
466 }
467
468private:
469 bool ReadN(char* buf, int n)
470 {
471 int off = 0;
472 while (off < n)
473 {
474 int r = RawRead(buf + off, n - off);
475 if (r <= 0) return false;
476 off += r;
477 }
478 return true;
479 }
480
481 void SendPong(const string& payload)
482 {
483 string frame;
484 frame += (char)0x8A;
485 frame += (char)(0x80 | (payload.size() & 0x7f));
486 unsigned char mask[4]; RAND_bytes(mask, 4);
487 frame.append((char*)mask, 4);
488 string m = payload;
489 for (size_t i = 0; i < m.size(); i++) m[i] ^= mask[i & 3];
490 frame += m;
491 RawWrite(frame.data(), (int)frame.size());
492 }
493};
494
495
496// Handle a received event: extract "ip:port" and register the address.
497static void HandlePeerEvent(const json& ev)
498{
499 if (!ev.contains("content")) return;
500 string content = ev["content"].get<string>();
501 // expected content: "ip:port"
502 CAddress addr(content.c_str());
503 if (addr.ip == 0 || addr.ip == INADDR_NONE)
504 return;
505 if (addr.ip == addrLocalHost.ip && addr.port == addrLocalHost.port)
506 return; // not ourselves (same ip:port)
507 // Peer received from the relay (before AddAddress's routability filter).
508 printf("Nostr: peer received from relay: %s\n", addr.ToStringIPPort().c_str());
509 CAddrDB addrdb;
510 if (AddAddress(addrdb, addr))
511 printf("Nostr: new peer added %s\n", addr.ToStringIPPort().c_str());
512}
513
514
515// Publish this node's self-certifying `.btf` descriptor: "reach me (this key)
516// via <meeting_node>, encrypting to my x25519 key <enc>". Signed by the node
517// key so only the address's owner can publish it (no hijacking).
518static void PublishDescriptor(CWebSocket& ws, CNostrKey& key)
519{
520 // meeting_node is the rendezvous relay this node's hidden service
521 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us.
522 string meeting_node = strBtfMeetingRelay.empty() ? "rendezvous-pending"
523 : strBtfMeetingRelay;
524 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(),
525 meeting_node, (uint64_t)GetTime());
526 if (desc.empty())
527 return;
528 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) });
529 json ev;
530 if (BuildSignedEvent(key, BTF_DESC_KIND, tags, desc, ev))
531 {
532 json pub = json::array({ "EVENT", ev });
533 ws.SendText(pub.dump());
534 }
535}
536
537// Resolve a `.btf` address via a relay: fetch the descriptor published by that
538// address's key and verify it self-certifies under the decoded pubkey.
539static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
540 btf::Descriptor& out)
541{
542 unsigned char pk[32];
543 if (!btf::ParseAddress(btfAddr, pk))
544 return false;
545 json filter = json::object();
546 filter["authors"] = json::array({ HexEncode(pk, 32) });
547 filter["kinds"] = json::array({ BTF_DESC_KIND });
548 filter["limit"] = 1;
549 json req = json::array({ "REQ", "btf-resolve", filter });
550 if (!ws.SendText(req.dump()))
551 return false;
552 for (;;)
553 {
554 string msg;
555 if (!ws.RecvText(msg))
556 break;
557 json j;
558 try { j = json::parse(msg); } catch (...) { continue; }
559 if (!j.is_array() || j.empty()) continue;
560 string t = j[0].get<string>();
561 if (t == "EVENT" && j.size() >= 3)
562 {
563 const json& ev = j[2];
564 if (ev.contains("content") &&
565 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
566 return true;
567 }
568 else if (t == "EOSE")
569 break;
570 }
571 return false;
572}
573
574// Resolve a `.btf` address into its rendezvous coordinates by querying the
575// public discovery relays for the owner's self-certified descriptor. Used by
576// ConnectNodeBtf in net.cpp.
577bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
578 unsigned char enc_pub[32])
579{
580 if (!EnsureNostrKey())
581 return false;
582 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
583 {
584 try
585 {
586 CWebSocket ws;
587 if (!ws.Connect(pszNostrRelays[i], 10))
588 continue;
589 btf::Descriptor d;
590 if (!ResolveDescriptor(ws, g_nostrKey.ctx, btfAddr, d))
591 continue;
592 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
593 continue; // descriptor predates the owner's relay config
594 if (!HexDecode(d.enc, enc_pub, 32))
595 continue;
596 meetingHostPort = d.meeting_node;
597 return true;
598 }
599 CATCH_PRINT_EXCEPTION("BtfResolve")
600 }
601 return false;
602}
603
604
605// Connect to a relay: publish our address and collect the others'.
606static bool SeedFromRelay(CNostrKey& key, const string& relay)
607{
608 CWebSocket ws;
609 if (!ws.Connect(relay, 10))
610 return false;
611 printf("Nostr: connected to %s\n", relay.c_str());
612
613 // Publish our announcement (replaceable event with our ip:port)
614 if (addrLocalHost.ip != 0)
615 {
616 json tags = json::array({
617 json::array({ "d", NOSTR_DTAG }),
618 json::array({ "t", NOSTR_DTAG })
619 });
620 json ev;
621 if (BuildSignedEvent(key, NOSTR_KIND, tags, addrLocalHost.ToStringIPPort(), ev))
622 {
623 json pub = json::array({ "EVENT", ev });
624 ws.SendText(pub.dump());
625 }
626 }
627
628 // Publish our self-certifying .btf descriptor (rendezvous discovery)
629 PublishDescriptor(ws, key);
630
631 // Subscribe to receive the announcements of other nodes
632 json filter = json::object();
633 filter["kinds"] = json::array({ NOSTR_KIND });
634 filter["#d"] = json::array({ NOSTR_DTAG });
635 filter["limit"] = 500;
636 json req = json::array({ "REQ", "bf-sub", filter });
637 if (!ws.SendText(req.dump()))
638 return false;
639
640 // Read responses until EOSE (end of stored events) or timeout
641 int nEvents = 0;
642 for (;;)
643 {
644 string msg;
645 if (!ws.RecvText(msg))
646 break; // timeout or closed connection
647 json j;
648 try { j = json::parse(msg); } catch (...) { continue; }
649 if (!j.is_array() || j.empty()) continue;
650 string type = j[0].get<string>();
651 if (type == "EVENT" && j.size() >= 3)
652 {
653 HandlePeerEvent(j[2]);
654 if (++nEvents > 1000) break;
655 }
656 else if (type == "EOSE")
657 {
658 // We received all stored events; close this relay.
659 break;
660 }
661 else if (type == "NOTICE" || type == "OK" || type == "CLOSED")
662 {
663 // informational
664 }
665 }
666 printf("Nostr: %s returned %d events\n", relay.c_str(), nEvents);
667
668 // Discover other nodes' .btf descriptors so we can auto-connect anonymously
669 // (no manual /connectbtf). Each node publishes one addressable descriptor
670 // (kind 38501, d-tag btf-descriptor); we learn every peer's .btf address by
671 // deriving it from the event author. Reachability is verified later, when
672 // the rendezvous tunnel boxes to that key's static x25519 key.
673 ws.SendText(json::array({ "CLOSE", "bf-sub" }).dump());
674 {
675 json dfilter = json::object();
676 dfilter["kinds"] = json::array({ BTF_DESC_KIND });
677 dfilter["#d"] = json::array({ BTF_DESC_DTAG });
678 dfilter["limit"] = 200;
679 json dreq = json::array({ "REQ", "btf-disc", dfilter });
680 int nPeers = 0;
681 if (ws.SendText(dreq.dump()))
682 {
683 for (;;)
684 {
685 string msg;
686 if (!ws.RecvText(msg))
687 break;
688 json j;
689 try { j = json::parse(msg); } catch (...) { continue; }
690 if (!j.is_array() || j.empty()) continue;
691 string type = j[0].get<string>();
692 if (type == "EVENT" && j.size() >= 3)
693 {
694 const json& ev = j[2];
695 if (!ev.contains("pubkey") || !ev.contains("kind")) continue;
696 if (ev["kind"].get<int>() != BTF_DESC_KIND) continue;
697 unsigned char pkb[32];
698 if (!HexDecode(ev["pubkey"].get<string>(), pkb, 32)) continue;
699 if (memcmp(pkb, key.xonly, 32) == 0) continue; // not ourselves
700 string peerAddr = btf::Address(pkb);
701 bool fNew = false;
702 CRITICAL_BLOCK(cs_btfPeers)
703 fNew = g_btfPeers.insert(peerAddr).second;
704 if (fNew)
705 printf("Nostr: discovered .btf peer %s\n", peerAddr.c_str());
706 if (++nPeers > 500) break;
707 }
708 else if (type == "EOSE")
709 break;
710 }
711 }
712 }
713 return true;
714}
715
716
717// Per-peer backoff: don't redial a .btf that just failed until this time. Keeps
718// dead/stale descriptors (e.g. abandoned test identities that linger on the
719// relays) from consuming the whole per-pass dial budget and starving the live
720// peers. Backoff grows on repeated failures, capped.
721static map<string, int64> g_btfPeerBackoff; // addr -> next-try time
722static map<string, int> g_btfPeerFails; // addr -> consecutive failures
723
724// Dial the .btf peers discovered on the relays, up to a target connection count.
725// Mirrors ThreadOpenConnections' ref handling: a kept connection holds one ref
726// (owned by fNetworkNode, released on disconnect); a redundant one is released.
727static void ConnectDiscoveredBtfPeers()
728{
729 vector<string> peers;
730 CRITICAL_BLOCK(cs_btfPeers)
731 peers.assign(g_btfPeers.begin(), g_btfPeers.end());
732
733 // Shuffle: the candidate set is sorted, so without this a handful of dead
734 // peers at the front would be retried every pass and the live peers further
735 // down the list would never be reached within the per-pass dial budget.
736 for (size_t i = peers.size(); i > 1; i--)
737 swap(peers[i - 1], peers[(size_t)GetRand(i)]);
738
739 int64 now = GetTime();
740 int nDials = 0;
741 foreach(const string& addr, peers)
742 {
743 if (fShutdown) break;
744 if (vNodes.size() >= BTF_TARGET_CONN) break;
745 if (nDials >= BTF_DIALS_PER_PASS) break;
746
747 // Skip peers still in backoff from a recent failure.
748 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr);
749 if (bi != g_btfPeerBackoff.end() && now < bi->second)
750 continue;
751
752 CNode* pnode = ConnectNodeBtf(addr);
753 if (pnode)
754 {
755 if (!pnode->fNetworkNode)
756 pnode->fNetworkNode = true; // keep the ref from ConnectNodeBtf
757 else
758 pnode->Release(); // already connected; drop the extra ref
759 g_btfPeerBackoff.erase(addr); // reachable again
760 g_btfPeerFails.erase(addr);
761 }
762 else
763 {
764 // Exponential-ish backoff: 1,2,4,... minutes, capped at 30.
765 int n = ++g_btfPeerFails[addr];
766 int64 mins = 1;
767 for (int k = 1; k < n && mins < 30; k++) mins *= 2;
768 if (mins > 30) mins = 30;
769 g_btfPeerBackoff[addr] = now + mins * 60;
770 nDials++; // only count real (failed) dial attempts against the budget
771 }
772 }
773}
774
775
776void ThreadNostrSeed(void* parg)
777{
778 printf("ThreadNostrSeed started\n");
779
780 if (!EnsureNostrKey())
781 {
782 printf("Nostr: failed to initialize secp256k1 key\n");
783 return;
784 }
785 CNostrKey& key = g_nostrKey;
786 printf("Nostr: node pubkey = %s\n", key.PubKeyHex().c_str());
787 printf("Nostr: node .btf address = %s\n", key.BtfAddress().c_str());
788
789 // One-time self-test of the rendezvous: publish our descriptor to a relay,
790 // then resolve our OWN .btf address back and verify it self-certifies. Proves
791 // the .btf publish/resolve/verify chain works against real public relays.
792 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
793 {
794 try
795 {
796 CWebSocket ws;
797 if (!ws.Connect(pszNostrRelays[i], 10)) continue;
798 PublishDescriptor(ws, key);
799 Sleep(700); // let the relay store the replaceable event
800 btf::Descriptor d;
801 if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d))
802 {
803 printf("Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n",
804 pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str());
805 break;
806 }
807 }
808 CATCH_PRINT_EXCEPTION("btf self-test")
809 }
810
811 // OpenSSL is already initialized by the rest of the app; TLS_client_method is enough.
812 loop
813 {
814 for (int i = 0; i < nNostrRelays; i++)
815 {
816 if (fShutdown) return;
817 try
818 {
819 SeedFromRelay(key, pszNostrRelays[i]);
820 }
821 CATCH_PRINT_EXCEPTION("SeedFromRelay")
822 }
823
824 // Auto-connect to discovered .btf peers over the rendezvous relay -- the
825 // anonymous, CGNAT-friendly replacement for the old IP-based connect. No
826 // manual /connectbtf needed: nodes find each other purely via Nostr.
827 try
828 {
829 ConnectDiscoveredBtfPeers();
830 }
831 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers")
832
833 // Re-announce/re-discover periodically (the replaceable event keeps the
834 // relay up to date and picks up nodes that joined after us)
835 for (int i = 0; i < 60 && !fShutdown; i++)
836 Sleep(1000);
837 }
838}