diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 838 |
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 | |||
| 25 | using 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). | ||
| 29 | static 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. | ||
| 32 | static const char* BTF_DESC_DTAG = "btf-descriptor-2"; | ||
| 33 | |||
| 34 | // Nostr network/scope for Bitflash. Events are replaceable (NIP-78). | ||
| 35 | static const int NOSTR_KIND = 30078; | ||
| 36 | static 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. | ||
| 40 | static const unsigned int BTF_TARGET_CONN = 8; | ||
| 41 | static const int BTF_DIALS_PER_PASS = 4; | ||
| 42 | |||
| 43 | // Other nodes' .btf addresses learned from their descriptors on the relays. | ||
| 44 | static set<string> g_btfPeers; | ||
| 45 | static CCriticalSection cs_btfPeers; | ||
| 46 | |||
| 47 | // Public discovery relays. Tunable. | ||
| 48 | const char* pszNostrRelays[] = { | ||
| 49 | "wss://relay.damus.io", | ||
| 50 | "wss://nos.lol", | ||
| 51 | "wss://relay.nostr.band", | ||
| 52 | "wss://relay.snort.social", | ||
| 53 | }; | ||
| 54 | const 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. | ||
| 58 | string strBtfMeetingRelay = "90.156.222.107:8434"; | ||
| 59 | |||
| 60 | |||
| 61 | // | ||
| 62 | // Utilities | ||
| 63 | // | ||
| 64 | static 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 | |||
| 77 | static 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 | |||
| 91 | static 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 | // | ||
| 112 | class CNostrKey | ||
| 113 | { | ||
| 114 | public: | ||
| 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. | ||
| 209 | static CNostrKey g_nostrKey; | ||
| 210 | static CCriticalSection cs_nostrKey; | ||
| 211 | |||
| 212 | static bool EnsureNostrKey() | ||
| 213 | { | ||
| 214 | CRITICAL_BLOCK(cs_nostrKey) | ||
| 215 | if (!g_nostrKey.fValid) | ||
| 216 | g_nostrKey.Init(); | ||
| 217 | return g_nostrKey.fValid; | ||
| 218 | } | ||
| 219 | |||
| 220 | bool 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 | |||
| 229 | std::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. | ||
| 238 | static 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 | // | ||
| 268 | class CWebSocket | ||
| 269 | { | ||
| 270 | public: | ||
| 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 | |||
| 468 | private: | ||
| 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. | ||
| 497 | static 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). | ||
| 518 | static 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. | ||
| 539 | static 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. | ||
| 577 | bool 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'. | ||
| 606 | static 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. | ||
| 721 | static map<string, int64> g_btfPeerBackoff; // addr -> next-try time | ||
| 722 | static 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. | ||
| 727 | static 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 | |||
| 776 | void 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 | } | ||