diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/nostr.cpp | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff) | |
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we
build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped
line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git
merge-base returns no result at all -- so anyone cloning this repository
landed on a default branch that was neither current nor related to the
running node.
The 28 commits on main were audited before this merge: every one is already
present in the tree by content, including the two that are hardest to see
because they were rewritten afterwards -- the payout reorg-safety work
(PayoutBlockStillOnChain, since evolved to re-check on every retry rather
than once at maturity) and the nostr relay certificate verification
(SSL_set1_host, now with a Windows root-store path). Nothing on main is lost
by this merge.
Recorded as a merge rather than a force-push: main only ever moves forward,
its old history stays reachable as the first parent, and this is revertible.
The tree is taken wholesale from the merged branch, so main's content is now
exactly what was built and tested.
What this brings onto main:
- compactblock: BIP152-style compact block relay ported from upstream 1.2.23,
primitives and unit test only, with the RAND_bytes failure path closed and
the reconstruction allocation bounded.
- upstream/main 17741d9: isolated testnet parameters and genesis, optional
2 MB large pages for RandomX. Mainnet genesis verified byte-identical
through upstream's BuildGenesisBlock() refactor.
- deploy: every image built on bookworm so binaries run on Debian 12, and the
container build now honours the RandomX and libsecp256k1 pins it had been
silently ignoring since before 1.2.15.
Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus
constants unchanged against e4f898c apart from the pre-existing
MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
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 | } |