diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 256 |
1 files changed, 177 insertions, 79 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp index 3f43302..f44dc08 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp | |||
| @@ -7,6 +7,10 @@ | |||
| 7 | #include <openssl/ssl.h> | 7 | #include <openssl/ssl.h> |
| 8 | #include <openssl/err.h> | 8 | #include <openssl/err.h> |
| 9 | #include <openssl/x509.h> | 9 | #include <openssl/x509.h> |
| 10 | #include <openssl/x509v3.h> | ||
| 11 | #ifdef _WIN32 | ||
| 12 | #include <wincrypt.h> | ||
| 13 | #endif | ||
| 10 | // We link libsecp256k1 statically (without this the header assumes DLL import). | 14 | // We link libsecp256k1 statically (without this the header assumes DLL import). |
| 11 | // Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from | 15 | // Both makefiles also pass -DSECP256K1_STATIC; the guard keeps that from |
| 12 | // warning while leaving this file correct if built without the flag. | 16 | // warning while leaving this file correct if built without the flag. |
| @@ -26,6 +30,7 @@ | |||
| 26 | 30 | ||
| 27 | #include "btfaddr.h" | 31 | #include "btfaddr.h" |
| 28 | #include "btfchan.h" | 32 | #include "btfchan.h" |
| 33 | #include "proxy.h" | ||
| 29 | 34 | ||
| 30 | using json = nlohmann::json; | 35 | using json = nlohmann::json; |
| 31 | 36 | ||
| @@ -127,6 +132,8 @@ vector<string> vBtfMeetingRelays = { | |||
| 127 | bool fBtfRelayPinned = false; | 132 | bool fBtfRelayPinned = false; |
| 128 | string strBtfActiveRelay; | 133 | string strBtfActiveRelay; |
| 129 | static CCriticalSection cs_activeRelay; | 134 | static CCriticalSection cs_activeRelay; |
| 135 | static string strBtfOnionEndpoint; | ||
| 136 | static CCriticalSection cs_onionEndpoint; | ||
| 130 | 137 | ||
| 131 | // Only the rendezvous we actually registered at (BtfSetActiveRelay, called from | 138 | // Only the rendezvous we actually registered at (BtfSetActiveRelay, called from |
| 132 | // ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it | 139 | // ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it |
| @@ -144,10 +151,43 @@ string BtfActiveRelay() | |||
| 144 | 151 | ||
| 145 | void BtfSetActiveRelay(const string& relay) | 152 | void BtfSetActiveRelay(const string& relay) |
| 146 | { | 153 | { |
| 154 | // With /rvrelay set, the descriptor must never name anything but the pin. | ||
| 155 | // No caller can reach here with another relay today -- both registration | ||
| 156 | // paths draw their list from BtfAllRelays(), which returns the pin alone -- | ||
| 157 | // but the descriptor is the one thing a miner obeys, so the invariant is | ||
| 158 | // enforced at the sink instead of being retrusted to every future caller. | ||
| 159 | if (fBtfRelayPinned && | ||
| 160 | find(vBtfMeetingRelays.begin(), vBtfMeetingRelays.end(), relay) == vBtfMeetingRelays.end()) | ||
| 161 | { | ||
| 162 | LogPrint("pool", "refusing to advertise meeting relay %s: /rvrelay pins %s\n", | ||
| 163 | relay.c_str(), | ||
| 164 | vBtfMeetingRelays.empty() ? "an empty list" : vBtfMeetingRelays[0].c_str()); | ||
| 165 | return; | ||
| 166 | } | ||
| 147 | CRITICAL_BLOCK(cs_activeRelay) | 167 | CRITICAL_BLOCK(cs_activeRelay) |
| 148 | strBtfActiveRelay = relay; | 168 | strBtfActiveRelay = relay; |
| 149 | } | 169 | } |
| 150 | 170 | ||
| 171 | bool BtfSetLocalOnionEndpoint(const string& endpoint, string& errOut) | ||
| 172 | { | ||
| 173 | string norm; | ||
| 174 | if (!btf::NormalizeOnionEndpoint(endpoint, norm)) | ||
| 175 | { | ||
| 176 | errOut = "expected v3 onion endpoint HOST.onion:PORT"; | ||
| 177 | return false; | ||
| 178 | } | ||
| 179 | CRITICAL_BLOCK(cs_onionEndpoint) | ||
| 180 | strBtfOnionEndpoint = norm; | ||
| 181 | return true; | ||
| 182 | } | ||
| 183 | |||
| 184 | string BtfLocalOnionEndpoint() | ||
| 185 | { | ||
| 186 | CRITICAL_BLOCK(cs_onionEndpoint) | ||
| 187 | return strBtfOnionEndpoint; | ||
| 188 | return ""; | ||
| 189 | } | ||
| 190 | |||
| 151 | // A relay this node advertises on Nostr for others to discover (/announcerelay). | 191 | // A relay this node advertises on Nostr for others to discover (/announcerelay). |
| 152 | string strBtfAnnounceRelay; | 192 | string strBtfAnnounceRelay; |
| 153 | // Relays learned from other operators' Nostr announcements. | 193 | // Relays learned from other operators' Nostr announcements. |
| @@ -196,6 +236,18 @@ vector<string> BtfAllRelays() | |||
| 196 | return all; | 236 | return all; |
| 197 | } | 237 | } |
| 198 | 238 | ||
| 239 | // Relays learned from Nostr, whether or not a pin is in force. Reporting only: | ||
| 240 | // it exists so the pool can name what /rvrelay is excluding. Anything choosing | ||
| 241 | // a relay to register or dial at must keep using BtfAllRelays(). | ||
| 242 | vector<string> BtfDiscoveredRelays() | ||
| 243 | { | ||
| 244 | vector<string> out; | ||
| 245 | CRITICAL_BLOCK(cs_discoveredRelays) | ||
| 246 | foreach(const string& r, g_discoveredRelays) | ||
| 247 | out.push_back(r); | ||
| 248 | return out; | ||
| 249 | } | ||
| 250 | |||
| 199 | 251 | ||
| 200 | // | 252 | // |
| 201 | // Utilities | 253 | // Utilities |
| @@ -387,10 +439,16 @@ std::string BtfLocalDescriptor() | |||
| 387 | string meeting; | 439 | string meeting; |
| 388 | CRITICAL_BLOCK(cs_activeRelay) | 440 | CRITICAL_BLOCK(cs_activeRelay) |
| 389 | meeting = strBtfActiveRelay; | 441 | meeting = strBtfActiveRelay; |
| 442 | string onion = BtfLocalOnionEndpoint(); | ||
| 390 | if (meeting.empty()) | 443 | if (meeting.empty()) |
| 391 | return ""; | 444 | { |
| 445 | if (onion.empty()) | ||
| 446 | return ""; | ||
| 447 | meeting = "rendezvous-pending"; | ||
| 448 | } | ||
| 392 | return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, | 449 | return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, |
| 393 | g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime()); | 450 | g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime(), |
| 451 | onion); | ||
| 394 | } | 452 | } |
| 395 | 453 | ||
| 396 | // The shared secp256k1 context, so net.cpp can verify descriptors that arrive | 454 | // The shared secp256k1 context, so net.cpp can verify descriptors that arrive |
| @@ -468,6 +526,41 @@ static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags, | |||
| 468 | static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame | 526 | static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame |
| 469 | static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled | 527 | static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled |
| 470 | 528 | ||
| 529 | static int BtfConnectedNodeCount() | ||
| 530 | { | ||
| 531 | int nNodes = 0; | ||
| 532 | CRITICAL_BLOCK(cs_vNodes) | ||
| 533 | { | ||
| 534 | nNodes = (int)vNodes.size(); | ||
| 535 | } | ||
| 536 | return nNodes; | ||
| 537 | } | ||
| 538 | |||
| 539 | #ifdef _WIN32 | ||
| 540 | static bool BtfLoadWindowsRootCertificates(SSL_CTX* sslctx) | ||
| 541 | { | ||
| 542 | HCERTSTORE hStore = CertOpenSystemStoreA(0, "ROOT"); | ||
| 543 | if (!hStore) | ||
| 544 | return false; | ||
| 545 | |||
| 546 | bool fLoaded = false; | ||
| 547 | X509_STORE* x509store = SSL_CTX_get_cert_store(sslctx); | ||
| 548 | PCCERT_CONTEXT pCert = NULL; | ||
| 549 | while ((pCert = CertEnumCertificatesInStore(hStore, pCert)) != NULL) | ||
| 550 | { | ||
| 551 | const unsigned char* p = pCert->pbCertEncoded; | ||
| 552 | X509* x509 = d2i_X509(NULL, &p, pCert->cbCertEncoded); | ||
| 553 | if (!x509) | ||
| 554 | continue; | ||
| 555 | if (X509_STORE_add_cert(x509store, x509) == 1) | ||
| 556 | fLoaded = true; | ||
| 557 | X509_free(x509); | ||
| 558 | } | ||
| 559 | CertCloseStore(hStore, 0); | ||
| 560 | return fLoaded; | ||
| 561 | } | ||
| 562 | #endif | ||
| 563 | |||
| 471 | class CWebSocket | 564 | class CWebSocket |
| 472 | { | 565 | { |
| 473 | public: | 566 | public: |
| @@ -523,53 +616,8 @@ public: | |||
| 523 | size_t colon = host.find(':'); | 616 | size_t colon = host.find(':'); |
| 524 | if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } | 617 | if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } |
| 525 | 618 | ||
| 526 | // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA | 619 | hSocket = BtfConnectSocket(host, (unsigned short)port, SOCK_NOSTR, timeoutSec); |
| 527 | // records back and try them in the order the resolver prefers, | 620 | if (hSocket == INVALID_SOCKET) |
| 528 | // rather than hard-failing whenever the IPv4 path to a relay is | ||
| 529 | // filtered/rate-limited (increasingly common for CDN-fronted | ||
| 530 | // relays) while a working IPv6 route exists right there in the | ||
| 531 | // same getaddrinfo() result. | ||
| 532 | struct addrinfo hints, *res = NULL, *rp = NULL; | ||
| 533 | memset(&hints, 0, sizeof(hints)); | ||
| 534 | hints.ai_family = AF_UNSPEC; | ||
| 535 | hints.ai_socktype = SOCK_STREAM; | ||
| 536 | char portstr[16]; sprintf(portstr, "%d", port); | ||
| 537 | if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) | ||
| 538 | return error("Nostr: getaddrinfo %s failed", host.c_str()); | ||
| 539 | |||
| 540 | bool fConnected = false; | ||
| 541 | for (rp = res; rp != NULL; rp = rp->ai_next) | ||
| 542 | { | ||
| 543 | hSocket = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_NOSTR); | ||
| 544 | if (hSocket == INVALID_SOCKET) | ||
| 545 | continue; | ||
| 546 | |||
| 547 | // Socket receive/send timeout: Windows takes a DWORD of | ||
| 548 | // milliseconds; POSIX takes a struct timeval. | ||
| 549 | #ifdef _WIN32 | ||
| 550 | DWORD tv = timeoutSec * 1000; | ||
| 551 | setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); | ||
| 552 | setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); | ||
| 553 | #else | ||
| 554 | struct timeval tv; | ||
| 555 | tv.tv_sec = timeoutSec; | ||
| 556 | tv.tv_usec = 0; | ||
| 557 | setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); | ||
| 558 | setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); | ||
| 559 | #endif | ||
| 560 | |||
| 561 | if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0) | ||
| 562 | { | ||
| 563 | fConnected = true; | ||
| 564 | break; | ||
| 565 | } | ||
| 566 | |||
| 567 | BtfCloseSocket(hSocket); | ||
| 568 | hSocket = INVALID_SOCKET; | ||
| 569 | } | ||
| 570 | freeaddrinfo(res); | ||
| 571 | |||
| 572 | if (!fConnected) | ||
| 573 | return error("Nostr: connect %s:%d failed", host.c_str(), port); | 621 | return error("Nostr: connect %s:%d failed", host.c_str(), port); |
| 574 | 622 | ||
| 575 | // TLS | 623 | // TLS |
| @@ -585,22 +633,35 @@ public: | |||
| 585 | // so a MITM cannot forge them, but it could drop the rendezvous | 633 | // so a MITM cannot forge them, but it could drop the rendezvous |
| 586 | // registration (leaving the pool unreachable to miners) or answer | 634 | // registration (leaving the pool unreachable to miners) or answer |
| 587 | // subscriptions with whatever it liked. | 635 | // subscriptions with whatever it liked. |
| 588 | if (!SSL_CTX_set_default_verify_paths(sslctx)) | ||
| 589 | return error("Nostr: no system CA store available, refusing %s", host.c_str()); | ||
| 590 | SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); | 636 | SSL_CTX_set_verify(sslctx, SSL_VERIFY_PEER, NULL); |
| 637 | |||
| 638 | // OpenSSL's default verify paths are usually empty on Windows, so | ||
| 639 | // the system ROOT store is loaded too -- without it verification | ||
| 640 | // fails closed there and every relay becomes unreachable. | ||
| 641 | bool fTrustLoaded = (SSL_CTX_set_default_verify_paths(sslctx) == 1); | ||
| 642 | #ifdef _WIN32 | ||
| 643 | fTrustLoaded = BtfLoadWindowsRootCertificates(sslctx) || fTrustLoaded; | ||
| 644 | #endif | ||
| 645 | if (!fTrustLoaded) | ||
| 646 | return error("Nostr: failed to load TLS trust store"); | ||
| 647 | |||
| 648 | // Floor the protocol version. Verification says who the peer is, | ||
| 649 | // not how weak a cipher suite we will agree to; without this a | ||
| 650 | // relay offering nothing better still gets TLS 1.0. | ||
| 591 | SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION); | 651 | SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION); |
| 592 | 652 | ||
| 593 | ssl = SSL_new(sslctx); | 653 | ssl = SSL_new(sslctx); |
| 594 | if (!ssl) return false; | 654 | if (!ssl) return false; |
| 595 | SSL_set_fd(ssl, (int)hSocket); | 655 | SSL_set_fd(ssl, (int)hSocket); |
| 596 | SSL_set_tlsext_host_name(ssl, host.c_str()); // SNI | 656 | if (SSL_set_tlsext_host_name(ssl, host.c_str()) != 1) // SNI |
| 657 | return error("Nostr: failed to set TLS SNI for %s", host.c_str()); | ||
| 597 | 658 | ||
| 598 | // Verifying the chain is not enough on its own: without this the | 659 | // Verifying the chain is not enough on its own: without this the |
| 599 | // relay could present any certificate a trusted CA ever issued, | 660 | // relay could present any certificate a trusted CA ever issued, |
| 600 | // for any name. SSL_set1_host binds the check to the host we asked | 661 | // for any name. SSL_set1_host binds the check to the host we asked |
| 601 | // for. | 662 | // for. |
| 602 | if (!SSL_set1_host(ssl, host.c_str())) | 663 | if (SSL_set1_host(ssl, host.c_str()) != 1) |
| 603 | return error("Nostr: cannot pin verification to %s", host.c_str()); | 664 | return error("Nostr: failed to set TLS hostname for %s", host.c_str()); |
| 604 | 665 | ||
| 605 | if (SSL_connect(ssl) != 1) | 666 | if (SSL_connect(ssl) != 1) |
| 606 | { | 667 | { |
| @@ -846,17 +907,21 @@ static void PublishDescriptor(CWebSocket& ws, CNostrKey& key) | |||
| 846 | // meeting_node is the rendezvous relay this node's hidden service | 907 | // meeting_node is the rendezvous relay this node's hidden service |
| 847 | // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us. | 908 | // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us. |
| 848 | string meeting_node = BtfActiveRelay(); | 909 | string meeting_node = BtfActiveRelay(); |
| 910 | string onion = BtfLocalOnionEndpoint(); | ||
| 849 | if (meeting_node.empty()) | 911 | if (meeting_node.empty()) |
| 850 | { | 912 | { |
| 851 | // Not registered anywhere yet. Publishing now would advertise a meeting | 913 | if (onion.empty()) |
| 852 | // node we can't be reached at, and peers would waste dials on it. The | 914 | { |
| 853 | // ThreadNostrSeed loop republishes on every pass, so skipping costs | 915 | // Not registered anywhere yet, and no direct onion endpoint exists. |
| 854 | // nothing but the delay until ThreadBtfAccept registers. | 916 | // Publishing now would advertise a meeting node we can't be reached at. |
| 855 | LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); | 917 | LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); |
| 856 | return; | 918 | return; |
| 919 | } | ||
| 920 | meeting_node = "rendezvous-pending"; | ||
| 857 | } | 921 | } |
| 858 | string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(), | 922 | string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(), |
| 859 | meeting_node, (uint64_t)GetTime()); | 923 | meeting_node, (uint64_t)GetTime(), |
| 924 | onion); | ||
| 860 | if (desc.empty()) | 925 | if (desc.empty()) |
| 861 | return; | 926 | return; |
| 862 | json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) }); | 927 | json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) }); |
| @@ -1062,7 +1127,8 @@ static void HandlePoolAnnouncement(const json& ev) | |||
| 1062 | // this socket for reuse, and only a completed exchange leaves it in a state | 1127 | // this socket for reuse, and only a completed exchange leaves it in a state |
| 1063 | // the next caller can trust. | 1128 | // the next caller can trust. |
| 1064 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1129 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, |
| 1065 | btf::Descriptor& out, bool& fExchangeOk) | 1130 | btf::Descriptor& out, bool& fExchangeOk, |
| 1131 | string* rawOut = NULL) | ||
| 1066 | { | 1132 | { |
| 1067 | fExchangeOk = false; | 1133 | fExchangeOk = false; |
| 1068 | unsigned char pk[32]; | 1134 | unsigned char pk[32]; |
| @@ -1071,6 +1137,7 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | |||
| 1071 | json filter = json::object(); | 1137 | json filter = json::object(); |
| 1072 | filter["authors"] = json::array({ HexEncode(pk, 32) }); | 1138 | filter["authors"] = json::array({ HexEncode(pk, 32) }); |
| 1073 | filter["kinds"] = json::array({ BTF_DESC_KIND }); | 1139 | filter["kinds"] = json::array({ BTF_DESC_KIND }); |
| 1140 | filter["#d"] = json::array({ BTF_DESC_DTAG }); | ||
| 1074 | filter["limit"] = 1; | 1141 | filter["limit"] = 1; |
| 1075 | json req = json::array({ "REQ", "btf-resolve", filter }); | 1142 | json req = json::array({ "REQ", "btf-resolve", filter }); |
| 1076 | if (!ws.SendText(req.dump())) | 1143 | if (!ws.SendText(req.dump())) |
| @@ -1097,7 +1164,11 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | |||
| 1097 | const json& ev = j[2]; | 1164 | const json& ev = j[2]; |
| 1098 | if (!fFound && ev.contains("content") && | 1165 | if (!fFound && ev.contains("content") && |
| 1099 | btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) | 1166 | btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) |
| 1167 | { | ||
| 1100 | fFound = true; | 1168 | fFound = true; |
| 1169 | if (rawOut) | ||
| 1170 | *rawOut = ev["content"].get<string>(); | ||
| 1171 | } | ||
| 1101 | // Deliberately keep reading to EOSE rather than returning here. | 1172 | // Deliberately keep reading to EOSE rather than returning here. |
| 1102 | // Returning early leaves the rest of the relay's answer queued on a | 1173 | // Returning early leaves the rest of the relay's answer queued on a |
| 1103 | // socket that goes straight back into the pool, and the next caller | 1174 | // socket that goes straight back into the pool, and the next caller |
| @@ -1117,8 +1188,10 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | |||
| 1117 | // public discovery relays for the owner's self-certified descriptor. Used by | 1188 | // public discovery relays for the owner's self-certified descriptor. Used by |
| 1118 | // ConnectNodeBtf in net.cpp. | 1189 | // ConnectNodeBtf in net.cpp. |
| 1119 | bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | 1190 | bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, |
| 1120 | unsigned char enc_pub[32]) | 1191 | unsigned char enc_pub[32], std::string* onionHostPort) |
| 1121 | { | 1192 | { |
| 1193 | if (onionHostPort) | ||
| 1194 | onionHostPort->clear(); | ||
| 1122 | if (!EnsureNostrKey()) | 1195 | if (!EnsureNostrKey()) |
| 1123 | return false; | 1196 | return false; |
| 1124 | for (int i = 0; i < nNostrRelays && !fShutdown; i++) | 1197 | for (int i = 0; i < nNostrRelays && !fShutdown; i++) |
| @@ -1146,10 +1219,15 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | |||
| 1146 | } | 1219 | } |
| 1147 | lease.fOk = fExchangeOk; | 1220 | lease.fOk = fExchangeOk; |
| 1148 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") | 1221 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") |
| 1149 | continue; // descriptor predates the owner's relay config | 1222 | { |
| 1223 | if (!onionHostPort || d.onion.empty()) | ||
| 1224 | continue; // caller needs rendezvous, or descriptor has no direct path | ||
| 1225 | } | ||
| 1150 | if (!HexDecode(d.enc, enc_pub, 32)) | 1226 | if (!HexDecode(d.enc, enc_pub, 32)) |
| 1151 | continue; | 1227 | continue; |
| 1152 | meetingHostPort = d.meeting_node; | 1228 | meetingHostPort = d.meeting_node; |
| 1229 | if (onionHostPort) | ||
| 1230 | *onionHostPort = d.onion; | ||
| 1153 | return true; | 1231 | return true; |
| 1154 | } | 1232 | } |
| 1155 | CATCH_PRINT_EXCEPTION("BtfResolve") | 1233 | CATCH_PRINT_EXCEPTION("BtfResolve") |
| @@ -1207,6 +1285,7 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs, | |||
| 1207 | json filter = json::object(); | 1285 | json filter = json::object(); |
| 1208 | filter["authors"] = authors; | 1286 | filter["authors"] = authors; |
| 1209 | filter["kinds"] = json::array({ BTF_DESC_KIND }); | 1287 | filter["kinds"] = json::array({ BTF_DESC_KIND }); |
| 1288 | filter["#d"] = json::array({ BTF_DESC_DTAG }); | ||
| 1210 | filter["limit"] = (int)authors.size(); | 1289 | filter["limit"] = (int)authors.size(); |
| 1211 | json req = json::array({ "REQ", "btf-resolve-many", filter }); | 1290 | json req = json::array({ "REQ", "btf-resolve-many", filter }); |
| 1212 | if (!ws.SendText(req.dump())) | 1291 | if (!ws.SendText(req.dump())) |
| @@ -1237,12 +1316,15 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs, | |||
| 1237 | btf::Descriptor d; | 1316 | btf::Descriptor d; |
| 1238 | if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d)) | 1317 | if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d)) |
| 1239 | continue; | 1318 | continue; |
| 1240 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") | 1319 | if ((d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") && |
| 1320 | d.onion.empty()) | ||
| 1241 | continue; | 1321 | continue; |
| 1242 | BtfResolvedPeer rp; | 1322 | BtfResolvedPeer rp; |
| 1243 | if (!HexDecode(d.enc, rp.enc_pub, 32)) | 1323 | if (!HexDecode(d.enc, rp.enc_pub, 32)) |
| 1244 | continue; | 1324 | continue; |
| 1245 | rp.meetingHostPort = d.meeting_node; | 1325 | rp.meetingHostPort = d.meeting_node; |
| 1326 | rp.onionHostPort = d.onion; | ||
| 1327 | rp.descriptor = ev["content"].get<string>(); | ||
| 1246 | out[ai->second] = rp; | 1328 | out[ai->second] = rp; |
| 1247 | pending.erase(ai->second); | 1329 | pending.erase(ai->second); |
| 1248 | } | 1330 | } |
| @@ -1383,7 +1465,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 1383 | else if (createdAt > it->second) it->second = createdAt; // refresh | 1465 | else if (createdAt > it->second) it->second = createdAt; // refresh |
| 1384 | } | 1466 | } |
| 1385 | if (fNew && fDebug) | 1467 | if (fNew && fDebug) |
| 1386 | LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); | 1468 | LogPrint("nostrdiscovery", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); |
| 1387 | if (++nPeers > 500) break; | 1469 | if (++nPeers > 500) break; |
| 1388 | } | 1470 | } |
| 1389 | else if (type == "EOSE") | 1471 | else if (type == "EOSE") |
| @@ -1617,8 +1699,9 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1617 | // plus a random sample, not one or the other. | 1699 | // plus a random sample, not one or the other. |
| 1618 | int64 now = GetTime(); | 1700 | int64 now = GetTime(); |
| 1619 | int nBudget = (int)BTF_DIALS_PER_PASS; | 1701 | int nBudget = (int)BTF_DIALS_PER_PASS; |
| 1702 | int nNodeCount = BtfConnectedNodeCount(); | ||
| 1620 | { | 1703 | { |
| 1621 | int nRoom = (int)BTF_TARGET_CONN - (int)vNodes.size(); | 1704 | int nRoom = (int)BTF_TARGET_CONN - nNodeCount; |
| 1622 | if (nRoom < nBudget) nBudget = nRoom; | 1705 | if (nRoom < nBudget) nBudget = nRoom; |
| 1623 | } | 1706 | } |
| 1624 | if (nBudget < 0) nBudget = 0; | 1707 | if (nBudget < 0) nBudget = 0; |
| @@ -1632,7 +1715,6 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1632 | // Reliability slice: walk the tiered (good -> untried -> bad) order. | 1715 | // Reliability slice: walk the tiered (good -> untried -> bad) order. |
| 1633 | for (const string& addr : peers) | 1716 | for (const string& addr : peers) |
| 1634 | { | 1717 | { |
| 1635 | if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break; | ||
| 1636 | if ((int)candidates.size() >= nSortedSlots) break; | 1718 | if ((int)candidates.size() >= nSortedSlots) break; |
| 1637 | map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); | 1719 | map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); |
| 1638 | if (bi != g_btfPeerBackoff.end() && now < bi->second) | 1720 | if (bi != g_btfPeerBackoff.end() && now < bi->second) |
| @@ -1657,7 +1739,7 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1657 | int nWant = min(nRandomSlots, (int)pool.size()); | 1739 | int nWant = min(nRandomSlots, (int)pool.size()); |
| 1658 | for (int i = 0; i < nWant; i++) | 1740 | for (int i = 0; i < nWant; i++) |
| 1659 | { | 1741 | { |
| 1660 | if ((int)vNodes.size() + (int)candidates.size() >= (int)BTF_TARGET_CONN) break; | 1742 | if (nNodeCount + (int)candidates.size() >= (int)BTF_TARGET_CONN) break; |
| 1661 | if ((int)candidates.size() >= nBudget) break; | 1743 | if ((int)candidates.size() >= nBudget) break; |
| 1662 | size_t j = i + (size_t)GetRand((uint64)(pool.size() - i)); | 1744 | size_t j = i + (size_t)GetRand((uint64)(pool.size() - i)); |
| 1663 | std::swap(pool[i], pool[j]); | 1745 | std::swap(pool[i], pool[j]); |
| @@ -1712,20 +1794,20 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1712 | // not the shared Nostr relays, so there's no thundering-herd effect here. | 1794 | // not the shared Nostr relays, so there's no thundering-herd effect here. |
| 1713 | // Each thread writes its result back via shared state guarded by cs_btfPeerState. | 1795 | // Each thread writes its result back via shared state guarded by cs_btfPeerState. |
| 1714 | struct DialCtx { string addr; BtfResolvedPeer rp; }; | 1796 | struct DialCtx { string addr; BtfResolvedPeer rp; }; |
| 1715 | vector<DialCtx*> ctxs; | ||
| 1716 | for (const string& addr : toDial) | 1797 | for (const string& addr : toDial) |
| 1717 | { | 1798 | { |
| 1718 | DialCtx* ctx = new DialCtx{addr, resolved[addr]}; | 1799 | DialCtx* ctx = new DialCtx{addr, resolved[addr]}; |
| 1719 | ctxs.push_back(ctx); | 1800 | uintptr_t tid = _beginthread([](void* arg) { |
| 1720 | _beginthread([](void* arg) { | ||
| 1721 | DialCtx* ctx = (DialCtx*)arg; | 1801 | DialCtx* ctx = (DialCtx*)arg; |
| 1722 | string addr = ctx->addr; | 1802 | string addr = ctx->addr; |
| 1723 | string meeting = ctx->rp.meetingHostPort; | 1803 | string meeting = ctx->rp.meetingHostPort; |
| 1804 | string onion = ctx->rp.onionHostPort; | ||
| 1805 | string desc = ctx->rp.descriptor; | ||
| 1724 | unsigned char enc_pub[32]; | 1806 | unsigned char enc_pub[32]; |
| 1725 | memcpy(enc_pub, ctx->rp.enc_pub, 32); | 1807 | memcpy(enc_pub, ctx->rp.enc_pub, 32); |
| 1726 | delete ctx; | 1808 | delete ctx; |
| 1727 | 1809 | ||
| 1728 | CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); | 1810 | CNode* pnode = ConnectNodeBtfResolved(addr, meeting, onion, enc_pub, desc); |
| 1729 | CRITICAL_BLOCK(cs_btfPeerState) | 1811 | CRITICAL_BLOCK(cs_btfPeerState) |
| 1730 | { | 1812 | { |
| 1731 | if (pnode) | 1813 | if (pnode) |
| @@ -1750,6 +1832,21 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1750 | } | 1832 | } |
| 1751 | } | 1833 | } |
| 1752 | }, 0, ctx); | 1834 | }, 0, ctx); |
| 1835 | if (tid == (uintptr_t)-1) | ||
| 1836 | { | ||
| 1837 | LogPrint("nostr", "Nostr: failed to create .btf dial thread for %s\n", addr.c_str()); | ||
| 1838 | CRITICAL_BLOCK(cs_btfPeerState) | ||
| 1839 | { | ||
| 1840 | int n = ++g_btfPeerFails[addr]; | ||
| 1841 | int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD | ||
| 1842 | : BTF_BACKOFF_CAP_SECS; | ||
| 1843 | int64 secs = 30; | ||
| 1844 | for (int k = 1; k < n && secs < cap; k++) secs *= 2; | ||
| 1845 | if (secs > cap) secs = cap; | ||
| 1846 | g_btfPeerBackoff[addr] = GetTime() + secs; | ||
| 1847 | } | ||
| 1848 | delete ctx; | ||
| 1849 | } | ||
| 1753 | } | 1850 | } |
| 1754 | 1851 | ||
| 1755 | // Wait long enough for all parallel dials to finish (each has a 10s socket | 1852 | // Wait long enough for all parallel dials to finish (each has a 10s socket |
| @@ -1772,11 +1869,12 @@ void ThreadNostrSeed(void* parg) | |||
| 1772 | LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); | 1869 | LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); |
| 1773 | LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); | 1870 | LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); |
| 1774 | 1871 | ||
| 1775 | // The self-test below publishes a real descriptor, and PublishDescriptor now | 1872 | // The self-test below publishes a real descriptor. Rendezvous-only nodes |
| 1776 | // refuses to publish before ThreadBtfAccept has registered a rendezvous. Give | 1873 | // need ThreadBtfAccept to register first; Tor nodes can publish as soon as |
| 1777 | // registration a bounded head start so the test measures the publish/resolve | 1874 | // their signed onion endpoint exists, with rendezvous marked pending. |
| 1778 | // chain instead of losing the race against it. | 1875 | for (int i = 0; i < 60 && !fShutdown && |
| 1779 | for (int i = 0; i < 60 && !fShutdown && BtfActiveRelay().empty(); i++) | 1876 | BtfActiveRelay().empty() && |
| 1877 | BtfLocalOnionEndpoint().empty(); i++) | ||
| 1780 | Sleep(1000); | 1878 | Sleep(1000); |
| 1781 | 1879 | ||
| 1782 | // One-time self-test of the rendezvous: publish our descriptor to a relay, | 1880 | // One-time self-test of the rendezvous: publish our descriptor to a relay, |
| @@ -1852,7 +1950,7 @@ void ThreadNostrSeed(void* parg) | |||
| 1852 | 1950 | ||
| 1853 | // Re-discover periodically. Sleep less aggressively when | 1951 | // Re-discover periodically. Sleep less aggressively when |
| 1854 | // we have no connections yet so we find peers faster on startup. | 1952 | // we have no connections yet so we find peers faster on startup. |
| 1855 | int nSleepSecs = vNodes.empty() ? 20 : 60; | 1953 | int nSleepSecs = (BtfConnectedNodeCount() == 0) ? 20 : 60; |
| 1856 | for (int i = 0; i < nSleepSecs && !fShutdown; i++) | 1954 | for (int i = 0; i < nSleepSecs && !fShutdown; i++) |
| 1857 | Sleep(1000); | 1955 | Sleep(1000); |
| 1858 | } | 1956 | } |