diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 518 |
1 files changed, 450 insertions, 68 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp index c2ccc4c..009edd1 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp | |||
| @@ -45,7 +45,12 @@ static const int64 BTF_POOL_MAX_AGE = 3 * 60; // seconds | |||
| 45 | 45 | ||
| 46 | // Anonymous auto-discovery: how many peers to keep connected via .btf rendezvous | 46 | // Anonymous auto-discovery: how many peers to keep connected via .btf rendezvous |
| 47 | // before we stop dialing more, and how many new dials to attempt per cycle. | 47 | // before we stop dialing more, and how many new dials to attempt per cycle. |
| 48 | static const unsigned int BTF_TARGET_CONN = 8; | 48 | // Target outbound peer count. With ~100 known .btf peers on the network, |
| 49 | // an Erdos-Renyi random graph stays connected with overwhelming probability | ||
| 50 | // once average degree exceeds ln(N) (~4.6 here); 24 is ~5x past that | ||
| 51 | // threshold, so this is plenty *provided* selection isn't fully correlated | ||
| 52 | // across nodes -- see the diversity slice in ConnectDiscoveredBtfPeers. | ||
| 53 | static const unsigned int BTF_TARGET_CONN = 24; | ||
| 49 | static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle | 54 | static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle |
| 50 | 55 | ||
| 51 | // Nostr descriptor freshness cutoff: skip peers whose descriptor hasn't been | 56 | // Nostr descriptor freshness cutoff: skip peers whose descriptor hasn't been |
| @@ -58,11 +63,27 @@ static map<string, int64> g_btfPeers; | |||
| 58 | static CCriticalSection cs_btfPeers; | 63 | static CCriticalSection cs_btfPeers; |
| 59 | 64 | ||
| 60 | // Public discovery relays. Tunable. | 65 | // Public discovery relays. Tunable. |
| 66 | // | ||
| 67 | // Picked for: free/public (no NIP-42 auth or payment wall), track record of | ||
| 68 | // high uptime, and *infrastructure* diversity, not just different hostnames. | ||
| 69 | // relay.damus.io, relay.snort.social and relay.primal.net are all fronted by | ||
| 70 | // Cloudflare -- different operators, same upstream, so a Cloudflare-side | ||
| 71 | // incident (or a Cloudflare IP range getting rate-limited/blocked on some | ||
| 72 | // network) can take out several of them at once even though they look like | ||
| 73 | // independent relays. Kept only one Cloudflare-fronted relay (damus.io, the | ||
| 74 | // most established) and filled the rest with relays on different hosts | ||
| 75 | // (Hetzner, various independents) so no single upstream provider is a | ||
| 76 | // single point of failure for peer discovery. 7 relays lands inside the | ||
| 77 | // commonly recommended 5-10 range for redundancy without keeping an | ||
| 78 | // excessive number of persistent sockets open per node. | ||
| 61 | const char* pszNostrRelays[] = { | 79 | const char* pszNostrRelays[] = { |
| 62 | "wss://relay.damus.io", | 80 | "wss://relay.damus.io", |
| 63 | "wss://nos.lol", | 81 | "wss://nos.lol", |
| 64 | "wss://relay.nostr.band", | 82 | "wss://relay.nostr.band", |
| 65 | "wss://relay.snort.social", | 83 | "wss://nostr21.com", |
| 84 | "wss://offchain.pub", | ||
| 85 | "wss://nostr.mom", | ||
| 86 | "wss://relay.primal.net", | ||
| 66 | }; | 87 | }; |
| 67 | const int nNostrRelays = ARRAYLEN(pszNostrRelays); | 88 | const int nNostrRelays = ARRAYLEN(pszNostrRelays); |
| 68 | 89 | ||
| @@ -317,6 +338,13 @@ std::string BtfLocalAddress() | |||
| 317 | return g_nostrKey.BtfAddress(); | 338 | return g_nostrKey.BtfAddress(); |
| 318 | } | 339 | } |
| 319 | 340 | ||
| 341 | int GetDiscoveredPeerCount() | ||
| 342 | { | ||
| 343 | int n = 0; | ||
| 344 | CRITICAL_BLOCK(cs_btfPeers) { n = (int)g_btfPeers.size(); } | ||
| 345 | return n; | ||
| 346 | } | ||
| 347 | |||
| 320 | void BtfGetPoolAnnouncements(std::vector<BtfPoolAnnouncement>& out) | 348 | void BtfGetPoolAnnouncements(std::vector<BtfPoolAnnouncement>& out) |
| 321 | { | 349 | { |
| 322 | out.clear(); | 350 | out.clear(); |
| @@ -425,38 +453,55 @@ public: | |||
| 425 | size_t colon = host.find(':'); | 453 | size_t colon = host.find(':'); |
| 426 | if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } | 454 | if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } |
| 427 | 455 | ||
| 428 | // Resolve and connect (TCP) | 456 | // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA |
| 429 | struct addrinfo hints, *res = NULL; | 457 | // records back and try them in the order the resolver prefers, |
| 458 | // rather than hard-failing whenever the IPv4 path to a relay is | ||
| 459 | // filtered/rate-limited (increasingly common for CDN-fronted | ||
| 460 | // relays) while a working IPv6 route exists right there in the | ||
| 461 | // same getaddrinfo() result. | ||
| 462 | struct addrinfo hints, *res = NULL, *rp = NULL; | ||
| 430 | memset(&hints, 0, sizeof(hints)); | 463 | memset(&hints, 0, sizeof(hints)); |
| 431 | hints.ai_family = AF_INET; | 464 | hints.ai_family = AF_UNSPEC; |
| 432 | hints.ai_socktype = SOCK_STREAM; | 465 | hints.ai_socktype = SOCK_STREAM; |
| 433 | char portstr[16]; sprintf(portstr, "%d", port); | 466 | char portstr[16]; sprintf(portstr, "%d", port); |
| 434 | if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) | 467 | if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res) |
| 435 | return error("Nostr: getaddrinfo %s failed", host.c_str()); | 468 | return error("Nostr: getaddrinfo %s failed", host.c_str()); |
| 436 | hSocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol); | ||
| 437 | if (hSocket == INVALID_SOCKET) { freeaddrinfo(res); return false; } | ||
| 438 | 469 | ||
| 439 | // Socket receive/send timeout: Windows takes a DWORD of milliseconds; | 470 | bool fConnected = false; |
| 440 | // POSIX takes a struct timeval. | 471 | for (rp = res; rp != NULL; rp = rp->ai_next) |
| 472 | { | ||
| 473 | hSocket = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); | ||
| 474 | if (hSocket == INVALID_SOCKET) | ||
| 475 | continue; | ||
| 476 | |||
| 477 | // Socket receive/send timeout: Windows takes a DWORD of | ||
| 478 | // milliseconds; POSIX takes a struct timeval. | ||
| 441 | #ifdef _WIN32 | 479 | #ifdef _WIN32 |
| 442 | DWORD tv = timeoutSec * 1000; | 480 | DWORD tv = timeoutSec * 1000; |
| 443 | setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); | 481 | setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); |
| 444 | setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); | 482 | setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); |
| 445 | #else | 483 | #else |
| 446 | struct timeval tv; | 484 | struct timeval tv; |
| 447 | tv.tv_sec = timeoutSec; | 485 | tv.tv_sec = timeoutSec; |
| 448 | tv.tv_usec = 0; | 486 | tv.tv_usec = 0; |
| 449 | setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); | 487 | setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); |
| 450 | setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); | 488 | setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv)); |
| 451 | #endif | 489 | #endif |
| 452 | 490 | ||
| 453 | if (connect(hSocket, res->ai_addr, (int)res->ai_addrlen) != 0) | 491 | if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0) |
| 454 | { | 492 | { |
| 455 | freeaddrinfo(res); | 493 | fConnected = true; |
| 456 | return error("Nostr: connect %s:%d failed", host.c_str(), port); | 494 | break; |
| 495 | } | ||
| 496 | |||
| 497 | closesocket(hSocket); | ||
| 498 | hSocket = INVALID_SOCKET; | ||
| 457 | } | 499 | } |
| 458 | freeaddrinfo(res); | 500 | freeaddrinfo(res); |
| 459 | 501 | ||
| 502 | if (!fConnected) | ||
| 503 | return error("Nostr: connect %s:%d failed", host.c_str(), port); | ||
| 504 | |||
| 460 | // TLS | 505 | // TLS |
| 461 | if (fTls) | 506 | if (fTls) |
| 462 | { | 507 | { |
| @@ -598,6 +643,88 @@ private: | |||
| 598 | }; | 643 | }; |
| 599 | 644 | ||
| 600 | 645 | ||
| 646 | // Persistent relay connections. Public relays (relay.damus.io in particular) | ||
| 647 | // rate-limit new connections per source IP fairly aggressively. Before this, | ||
| 648 | // every single operation -- self-test, per-pass seeding, batch descriptor | ||
| 649 | // resolve, pool announce, pool query -- opened its own fresh CWebSocket, | ||
| 650 | // meaning a single node could fire a dozen-plus brand-new TCP+TLS+WS | ||
| 651 | // handshakes at the same 4 relays within any given minute (ThreadNostrSeed's | ||
| 652 | // own loop plus the separate ThreadBtfPoolAnnouncer thread, each reconnecting | ||
| 653 | // from scratch every cycle). That's exactly what was showing up as constant | ||
| 654 | // "handshake refused by relay.damus.io" and "connect ... failed" -- not | ||
| 655 | // necessarily dead relays, just this node getting rate-limited by its own | ||
| 656 | // reconnect churn. | ||
| 657 | // | ||
| 658 | // Now every relay-touching call reuses one long-lived socket per relay via | ||
| 659 | // this pool, reconnecting only when a socket actually drops. That cuts fresh | ||
| 660 | // connections from several per minute down to essentially one per relay for | ||
| 661 | // the life of the process. | ||
| 662 | struct RelayConn | ||
| 663 | { | ||
| 664 | CWebSocket ws; | ||
| 665 | CCriticalSection cs; // serializes use of this relay's socket | ||
| 666 | bool fConnected; | ||
| 667 | RelayConn() : fConnected(false) {} | ||
| 668 | }; | ||
| 669 | static map<string, RelayConn*> g_relayPool; | ||
| 670 | static CCriticalSection cs_relayPool; | ||
| 671 | |||
| 672 | static RelayConn* GetRelayConn(const string& relay) | ||
| 673 | { | ||
| 674 | CRITICAL_BLOCK(cs_relayPool) | ||
| 675 | { | ||
| 676 | map<string, RelayConn*>::iterator it = g_relayPool.find(relay); | ||
| 677 | if (it != g_relayPool.end()) | ||
| 678 | return it->second; | ||
| 679 | RelayConn* rc = new RelayConn(); | ||
| 680 | g_relayPool[relay] = rc; | ||
| 681 | return rc; | ||
| 682 | } | ||
| 683 | return NULL; // unreachable | ||
| 684 | } | ||
| 685 | |||
| 686 | // RAII lease on a relay's pooled connection: connects (if not already | ||
| 687 | // connected) on construction, and on destruction either keeps the socket | ||
| 688 | // alive for the next caller (if the exchange succeeded) or closes it so the | ||
| 689 | // next caller reconnects instead of reusing a socket that may be wedged. | ||
| 690 | // Caller must set fOk = true once its exchange with the relay completed | ||
| 691 | // cleanly. | ||
| 692 | class CRelayLease | ||
| 693 | { | ||
| 694 | public: | ||
| 695 | RelayConn* rc; | ||
| 696 | bool fOk; | ||
| 697 | bool fJustConnected; // true if this lease had to (re)connect, false if reusing a live socket | ||
| 698 | |||
| 699 | CRelayLease(const string& relay, int timeoutSec) : rc(NULL), fOk(false), fJustConnected(false) | ||
| 700 | { | ||
| 701 | rc = GetRelayConn(relay); | ||
| 702 | rc->cs.Enter(); | ||
| 703 | if (!rc->fConnected) | ||
| 704 | { | ||
| 705 | rc->ws.Close(); | ||
| 706 | rc->fConnected = rc->ws.Connect(relay, timeoutSec); | ||
| 707 | fJustConnected = rc->fConnected; | ||
| 708 | } | ||
| 709 | } | ||
| 710 | ~CRelayLease() | ||
| 711 | { | ||
| 712 | if (!fOk) | ||
| 713 | { | ||
| 714 | rc->ws.Close(); | ||
| 715 | rc->fConnected = false; | ||
| 716 | } | ||
| 717 | rc->cs.Leave(); | ||
| 718 | } | ||
| 719 | bool Connected() const { return rc->fConnected; } | ||
| 720 | CWebSocket& Ws() { return rc->ws; } | ||
| 721 | |||
| 722 | private: | ||
| 723 | CRelayLease(const CRelayLease&); | ||
| 724 | CRelayLease& operator=(const CRelayLease&); | ||
| 725 | }; | ||
| 726 | |||
| 727 | |||
| 601 | // Publish this node's self-certifying `.btf` descriptor: "reach me (this key) | 728 | // Publish this node's self-certifying `.btf` descriptor: "reach me (this key) |
| 602 | // via <meeting_node>, encrypting to my x25519 key <enc>". Signed by the node | 729 | // via <meeting_node>, encrypting to my x25519 key <enc>". Signed by the node |
| 603 | // key so only the address's owner can publish it (no hijacking). | 730 | // key so only the address's owner can publish it (no hijacking). |
| @@ -673,11 +800,11 @@ bool BtfPublishPoolAnnouncement(const BtfPoolAnnouncement& ann) | |||
| 673 | { | 800 | { |
| 674 | try | 801 | try |
| 675 | { | 802 | { |
| 676 | CWebSocket ws; | 803 | CRelayLease lease(pszNostrRelays[i], 10); |
| 677 | if (!ws.Connect(pszNostrRelays[i], 10)) | 804 | if (!lease.Connected()) |
| 678 | continue; | 805 | continue; |
| 679 | if (PublishPoolAnnouncementOnRelay(ws, g_nostrKey, ann)) | 806 | if (PublishPoolAnnouncementOnRelay(lease.Ws(), g_nostrKey, ann)) |
| 680 | fPublished = true; | 807 | fPublished = lease.fOk = true; |
| 681 | } | 808 | } |
| 682 | CATCH_PRINT_EXCEPTION("BtfPublishPoolAnnouncement") | 809 | CATCH_PRINT_EXCEPTION("BtfPublishPoolAnnouncement") |
| 683 | } | 810 | } |
| @@ -699,9 +826,10 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen | |||
| 699 | { | 826 | { |
| 700 | try | 827 | try |
| 701 | { | 828 | { |
| 702 | CWebSocket ws; | 829 | CRelayLease lease(pszNostrRelays[i], 10); |
| 703 | if (!ws.Connect(pszNostrRelays[i], 10)) | 830 | if (!lease.Connected()) |
| 704 | continue; | 831 | continue; |
| 832 | CWebSocket& ws = lease.Ws(); | ||
| 705 | 833 | ||
| 706 | json filter = json::object(); | 834 | json filter = json::object(); |
| 707 | filter["authors"] = json::array({ HexEncode(pk, 32) }); | 835 | filter["authors"] = json::array({ HexEncode(pk, 32) }); |
| @@ -756,6 +884,7 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen | |||
| 756 | else if (type == "EOSE") | 884 | else if (type == "EOSE") |
| 757 | break; | 885 | break; |
| 758 | } | 886 | } |
| 887 | lease.fOk = true; | ||
| 759 | } | 888 | } |
| 760 | CATCH_PRINT_EXCEPTION("BtfQueryPoolAnnouncement") | 889 | CATCH_PRINT_EXCEPTION("BtfQueryPoolAnnouncement") |
| 761 | } | 890 | } |
| @@ -855,12 +984,16 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | |||
| 855 | { | 984 | { |
| 856 | try | 985 | try |
| 857 | { | 986 | { |
| 858 | CWebSocket ws; | 987 | CRelayLease lease(pszNostrRelays[i], 10); |
| 859 | if (!ws.Connect(pszNostrRelays[i], 10)) | 988 | if (!lease.Connected()) |
| 860 | continue; | 989 | continue; |
| 861 | btf::Descriptor d; | 990 | btf::Descriptor d; |
| 862 | if (!ResolveDescriptor(ws, g_nostrKey.ctx, btfAddr, d)) | 991 | if (!ResolveDescriptor(lease.Ws(), g_nostrKey.ctx, btfAddr, d)) |
| 992 | { | ||
| 993 | lease.fOk = true; // exchange completed, just no match | ||
| 863 | continue; | 994 | continue; |
| 995 | } | ||
| 996 | lease.fOk = true; | ||
| 864 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") | 997 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") |
| 865 | continue; // descriptor predates the owner's relay config | 998 | continue; // descriptor predates the owner's relay config |
| 866 | if (!HexDecode(d.enc, enc_pub, 32)) | 999 | if (!HexDecode(d.enc, enc_pub, 32)) |
| @@ -873,14 +1006,115 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | |||
| 873 | return false; | 1006 | return false; |
| 874 | } | 1007 | } |
| 875 | 1008 | ||
| 1009 | // Resolve MANY .btf addresses in one pass. A single relay connection carries | ||
| 1010 | // a REQ with every still-unresolved address's pubkey in "authors", instead of | ||
| 1011 | // opening one relay connection per address. This matters because callers like | ||
| 1012 | // ConnectDiscoveredBtfPeers dial many candidates in parallel; without | ||
| 1013 | // batching, each of those dial threads independently walks the same handful | ||
| 1014 | // of public relays on its own, so a pass of N candidates can fire up to | ||
| 1015 | // N * nNostrRelays near-simultaneous connections at the same few relays -- | ||
| 1016 | // enough to get rate-limited or have handshakes refused, which then looks | ||
| 1017 | // like "dead peers" even though the descriptors are perfectly resolvable. | ||
| 1018 | bool BtfResolveMany(const std::vector<std::string>& btfAddrs, | ||
| 1019 | std::map<std::string, BtfResolvedPeer>& out) | ||
| 1020 | { | ||
| 1021 | if (!EnsureNostrKey() || btfAddrs.empty()) | ||
| 1022 | return false; | ||
| 1023 | |||
| 1024 | map<string, string> hexToAddr; // pubkey hex -> .btf addr, for matching events back | ||
| 1025 | set<string> pending; | ||
| 1026 | for (const string& a : btfAddrs) | ||
| 1027 | { | ||
| 1028 | unsigned char pk[32]; | ||
| 1029 | if (!btf::ParseAddress(a, pk)) | ||
| 1030 | continue; | ||
| 1031 | hexToAddr[HexEncode(pk, 32)] = a; | ||
| 1032 | pending.insert(a); | ||
| 1033 | } | ||
| 1034 | if (pending.empty()) | ||
| 1035 | return false; | ||
| 1036 | |||
| 1037 | for (int i = 0; i < nNostrRelays && !fShutdown && !pending.empty(); i++) | ||
| 1038 | { | ||
| 1039 | try | ||
| 1040 | { | ||
| 1041 | CRelayLease lease(pszNostrRelays[i], 10); | ||
| 1042 | if (!lease.Connected()) | ||
| 1043 | continue; | ||
| 1044 | CWebSocket& ws = lease.Ws(); | ||
| 1045 | |||
| 1046 | json authors = json::array(); | ||
| 1047 | for (auto& kv : hexToAddr) | ||
| 1048 | if (pending.count(kv.second)) | ||
| 1049 | authors.push_back(kv.first); | ||
| 1050 | if (authors.empty()) | ||
| 1051 | { | ||
| 1052 | lease.fOk = true; | ||
| 1053 | break; | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | json filter = json::object(); | ||
| 1057 | filter["authors"] = authors; | ||
| 1058 | filter["kinds"] = json::array({ BTF_DESC_KIND }); | ||
| 1059 | filter["limit"] = (int)authors.size(); | ||
| 1060 | json req = json::array({ "REQ", "btf-resolve-many", filter }); | ||
| 1061 | if (!ws.SendText(req.dump())) | ||
| 1062 | continue; | ||
| 1063 | |||
| 1064 | for (;;) | ||
| 1065 | { | ||
| 1066 | string msg; | ||
| 1067 | if (!ws.RecvText(msg)) | ||
| 1068 | break; | ||
| 1069 | json j; | ||
| 1070 | try { j = json::parse(msg); } catch (...) { continue; } | ||
| 1071 | if (!j.is_array() || j.empty()) continue; | ||
| 1072 | string t = j[0].get<string>(); | ||
| 1073 | if (t == "EVENT" && j.size() >= 3) | ||
| 1074 | { | ||
| 1075 | const json& ev = j[2]; | ||
| 1076 | if (!ev.contains("pubkey") || !ev.contains("content")) | ||
| 1077 | continue; | ||
| 1078 | string hexpk = ev["pubkey"].get<string>(); | ||
| 1079 | auto ai = hexToAddr.find(hexpk); | ||
| 1080 | if (ai == hexToAddr.end() || !pending.count(ai->second)) | ||
| 1081 | continue; // not one we're waiting on (or already resolved) | ||
| 1082 | |||
| 1083 | unsigned char pk[32]; | ||
| 1084 | if (!HexDecode(hexpk, pk, 32)) | ||
| 1085 | continue; | ||
| 1086 | btf::Descriptor d; | ||
| 1087 | if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d)) | ||
| 1088 | continue; | ||
| 1089 | if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") | ||
| 1090 | continue; | ||
| 1091 | BtfResolvedPeer rp; | ||
| 1092 | if (!HexDecode(d.enc, rp.enc_pub, 32)) | ||
| 1093 | continue; | ||
| 1094 | rp.meetingHostPort = d.meeting_node; | ||
| 1095 | out[ai->second] = rp; | ||
| 1096 | pending.erase(ai->second); | ||
| 1097 | } | ||
| 1098 | else if (t == "EOSE") | ||
| 1099 | break; | ||
| 1100 | } | ||
| 1101 | lease.fOk = true; | ||
| 1102 | } | ||
| 1103 | CATCH_PRINT_EXCEPTION("BtfResolveMany") | ||
| 1104 | } | ||
| 1105 | return !out.empty(); | ||
| 1106 | } | ||
| 1107 | |||
| 876 | 1108 | ||
| 877 | // Connect to a relay: publish our .btf descriptor and collect relay data. | 1109 | // Connect to a relay: publish our .btf descriptor and collect relay data. |
| 878 | static bool SeedFromRelay(CNostrKey& key, const string& relay) | 1110 | static bool SeedFromRelay(CNostrKey& key, const string& relay) |
| 879 | { | 1111 | { |
| 880 | CWebSocket ws; | 1112 | CRelayLease lease(relay, 10); |
| 881 | if (!ws.Connect(relay, 10)) | 1113 | if (!lease.Connected()) |
| 882 | return false; | 1114 | return false; |
| 883 | printf("Nostr: connected to %s\n", relay.c_str()); | 1115 | CWebSocket& ws = lease.Ws(); |
| 1116 | if (lease.fJustConnected) | ||
| 1117 | LogPrint("nostr", "Nostr: connected to %s\n", relay.c_str()); | ||
| 884 | 1118 | ||
| 885 | // Publish our self-certifying .btf descriptor (rendezvous discovery) | 1119 | // Publish our self-certifying .btf descriptor (rendezvous discovery) |
| 886 | PublishDescriptor(ws, key); | 1120 | PublishDescriptor(ws, key); |
| @@ -930,7 +1164,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 930 | else if (createdAt > it->second) it->second = createdAt; // refresh | 1164 | else if (createdAt > it->second) it->second = createdAt; // refresh |
| 931 | } | 1165 | } |
| 932 | if (fNew && fDebug) | 1166 | if (fNew && fDebug) |
| 933 | printf("Nostr: discovered .btf peer %s\n", peerAddr.c_str()); | 1167 | LogPrint("nostr", "Nostr: discovered .btf peer %s\n", peerAddr.c_str()); |
| 934 | if (++nPeers > 500) break; | 1168 | if (++nPeers > 500) break; |
| 935 | } | 1169 | } |
| 936 | else if (type == "EOSE") | 1170 | else if (type == "EOSE") |
| @@ -974,7 +1208,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 974 | if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS) | 1208 | if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS) |
| 975 | fNew = g_discoveredRelays.insert(r).second; | 1209 | fNew = g_discoveredRelays.insert(r).second; |
| 976 | if (fNew && fDebug) | 1210 | if (fNew && fDebug) |
| 977 | printf("Nostr: discovered relay %s\n", r.c_str()); | 1211 | LogPrint("nostr", "Nostr: discovered relay %s\n", r.c_str()); |
| 978 | } | 1212 | } |
| 979 | else if (type == "EOSE") | 1213 | else if (type == "EOSE") |
| 980 | break; | 1214 | break; |
| @@ -1009,12 +1243,15 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 1009 | } | 1243 | } |
| 1010 | } | 1244 | } |
| 1011 | } | 1245 | } |
| 1246 | lease.fOk = true; | ||
| 1012 | return true; | 1247 | return true; |
| 1013 | } | 1248 | } |
| 1014 | 1249 | ||
| 1250 | volatile bool gAnnounceNow = false; | ||
| 1251 | |||
| 1015 | void ThreadBtfPoolAnnouncer(void*) | 1252 | void ThreadBtfPoolAnnouncer(void*) |
| 1016 | { | 1253 | { |
| 1017 | printf("ThreadBtfPoolAnnouncer started\n"); | 1254 | LogPrint("pool", "ThreadBtfPoolAnnouncer started\n"); |
| 1018 | while (!fShutdown) | 1255 | while (!fShutdown) |
| 1019 | { | 1256 | { |
| 1020 | if (nMineMode == MINE_OPERATOR && gPoolRunning) | 1257 | if (nMineMode == MINE_OPERATOR && gPoolRunning) |
| @@ -1025,15 +1262,17 @@ void ThreadBtfPoolAnnouncer(void*) | |||
| 1025 | ann.dashboardUrl = strPoolDashboardUrl; | 1262 | ann.dashboardUrl = strPoolDashboardUrl; |
| 1026 | ann.feePercent = dPoolFeePercent; | 1263 | ann.feePercent = dPoolFeePercent; |
| 1027 | uint64 roundShares = 0; | 1264 | uint64 roundShares = 0; |
| 1028 | GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares); | 1265 | double totalHashRate = 0.0; |
| 1029 | ann.hashRate = 0.0; | 1266 | GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate); |
| 1267 | ann.hashRate = totalHashRate; | ||
| 1030 | ann.createdAt = GetTime(); | 1268 | ann.createdAt = GetTime(); |
| 1031 | if (!ann.btfAddress.empty() && !ann.poolName.empty()) | 1269 | if (!ann.btfAddress.empty() && !ann.poolName.empty()) |
| 1032 | BtfPublishPoolAnnouncement(ann); | 1270 | BtfPublishPoolAnnouncement(ann); |
| 1033 | } | 1271 | } |
| 1034 | 1272 | ||
| 1035 | for (int i = 0; i < 60 && !fShutdown; i++) | 1273 | for (int i = 0; i < 60 && !fShutdown && !gAnnounceNow; i++) |
| 1036 | Sleep(1000); | 1274 | Sleep(1000); |
| 1275 | gAnnounceNow = false; | ||
| 1037 | } | 1276 | } |
| 1038 | } | 1277 | } |
| 1039 | 1278 | ||
| @@ -1042,10 +1281,29 @@ void ThreadBtfPoolAnnouncer(void*) | |||
| 1042 | // dead/stale descriptors (e.g. abandoned test identities that linger on the | 1281 | // dead/stale descriptors (e.g. abandoned test identities that linger on the |
| 1043 | // relays) from consuming the whole per-pass dial budget and starving the live | 1282 | // relays) from consuming the whole per-pass dial budget and starving the live |
| 1044 | // peers. Backoff grows on repeated failures, capped. | 1283 | // peers. Backoff grows on repeated failures, capped. |
| 1284 | // | ||
| 1285 | // g_btfPeerFails also doubles as the priority signal for dial ordering: a peer | ||
| 1286 | // we've connected to before (present in g_btfPeerEverConnected) is known-good | ||
| 1287 | // and goes first; a peer we've never tried goes next; a peer with a growing | ||
| 1288 | // fail count is known-bad and goes last, worst offenders furthest back. This | ||
| 1289 | // replaces the old behavior where the whole candidate list was shuffled right | ||
| 1290 | // after being sorted by freshness, which made the freshness sort a no-op and | ||
| 1291 | // gave dead peers equal footing with everyone else. | ||
| 1045 | static map<string, int64> g_btfPeerBackoff; // addr -> next-try time | 1292 | static map<string, int64> g_btfPeerBackoff; // addr -> next-try time |
| 1046 | static map<string, int> g_btfPeerFails; // addr -> consecutive failures | 1293 | static map<string, int> g_btfPeerFails; // addr -> consecutive failures |
| 1294 | static set<string> g_btfPeerEverConnected; // addr -> has succeeded at least once | ||
| 1047 | static CCriticalSection cs_btfPeerState; | 1295 | static CCriticalSection cs_btfPeerState; |
| 1048 | 1296 | ||
| 1297 | // Backoff cap for repeatedly-dead peers. The old 15-minute cap meant a | ||
| 1298 | // descriptor that has failed many times in a row cycles back into the dial | ||
| 1299 | // pool just as often as a peer we've never tried, wasting dial budget on | ||
| 1300 | // known-dead addresses every pass. Once a peer has failed enough times to | ||
| 1301 | // be "chronic", stretch the cap much further so it consumes budget far less | ||
| 1302 | // often while still getting retried eventually in case it comes back online. | ||
| 1303 | static const int64 BTF_BACKOFF_CAP_SECS = 15 * 60; // 15 min: initial ramp cap | ||
| 1304 | static const int64 BTF_BACKOFF_CAP_SECS_DEAD = 6 * 60 * 60; // 6 hr: cap for chronic failures | ||
| 1305 | static const int BTF_CHRONIC_FAIL_THRESHOLD = 6; // fails before "chronic" cap kicks in | ||
| 1306 | |||
| 1049 | // Dial the .btf peers discovered on the relays, up to a target connection count. | 1307 | // Dial the .btf peers discovered on the relays, up to a target connection count. |
| 1050 | // Connections are attempted in parallel so a single slow/dead relay doesn't | 1308 | // Connections are attempted in parallel so a single slow/dead relay doesn't |
| 1051 | // stall the entire pass. Each candidate gets its own thread; all threads start | 1309 | // stall the entire pass. Each candidate gets its own thread; all threads start |
| @@ -1060,23 +1318,18 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1060 | for (auto& kv : g_btfPeers) | 1318 | for (auto& kv : g_btfPeers) |
| 1061 | peersWithAge.push_back({kv.second, kv.first}); | 1319 | peersWithAge.push_back({kv.second, kv.first}); |
| 1062 | } | 1320 | } |
| 1063 | // Sort newest descriptor first — recently-active nodes are much more | ||
| 1064 | // likely to be online right now. Stale ones stay in the list as fallback. | ||
| 1065 | sort(peersWithAge.begin(), peersWithAge.end(), | ||
| 1066 | [](const pair<int64,string>& a, const pair<int64,string>& b){ return a.first > b.first; }); | ||
| 1067 | |||
| 1068 | int64 cutoff = GetTime() - BTF_PEER_MAX_AGE; | 1321 | int64 cutoff = GetTime() - BTF_PEER_MAX_AGE; |
| 1069 | int nFresh = 0; | 1322 | int nFresh = 0; |
| 1070 | vector<string> peers; | 1323 | vector<pair<int64,string>> fresh; // (createdAt, addr), newest first |
| 1071 | for (auto& p : peersWithAge) | 1324 | for (auto& p : peersWithAge) |
| 1072 | { | 1325 | { |
| 1073 | if (p.first < cutoff) | 1326 | if (p.first < cutoff) |
| 1074 | continue; | 1327 | continue; |
| 1075 | peers.push_back(p.second); | 1328 | fresh.push_back(p); |
| 1076 | nFresh++; | 1329 | nFresh++; |
| 1077 | } | 1330 | } |
| 1078 | if (!peers.empty()) | 1331 | if (!fresh.empty()) |
| 1079 | printf("Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal); | 1332 | LogPrint("nostr", "Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal); |
| 1080 | 1333 | ||
| 1081 | // Drop stale peers so we stop retrying dead descriptors until they refresh. | 1334 | // Drop stale peers so we stop retrying dead descriptors until they refresh. |
| 1082 | { | 1335 | { |
| @@ -1092,42 +1345,164 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1092 | } | 1345 | } |
| 1093 | } | 1346 | } |
| 1094 | 1347 | ||
| 1095 | // Shuffle so we don't always retry the same dead peers at the front. | 1348 | // Sort newest descriptor first -- recently-active nodes are much more |
| 1096 | for (size_t i = peers.size(); i > 1; i--) | 1349 | // likely to be online right now. |
| 1097 | swap(peers[i - 1], peers[(size_t)GetRand(i)]); | 1350 | sort(fresh.begin(), fresh.end(), |
| 1351 | [](const pair<int64,string>& a, const pair<int64,string>& b){ return a.first > b.first; }); | ||
| 1352 | |||
| 1353 | // Reorder into tiers: known-good (succeeded before) first, untried next, | ||
| 1354 | // known-bad (has a fail count) last -- worst offenders furthest back. | ||
| 1355 | // Freshness order is preserved *within* each tier, so recency still | ||
| 1356 | // matters, but a peer we know is dead no longer gets equal footing with | ||
| 1357 | // a peer we've never tried or one that's worked before. | ||
| 1358 | vector<string> tierGood, tierUntried, tierBad; | ||
| 1359 | CRITICAL_BLOCK(cs_btfPeerState) | ||
| 1360 | { | ||
| 1361 | for (auto& p : fresh) | ||
| 1362 | { | ||
| 1363 | const string& addr = p.second; | ||
| 1364 | if (g_btfPeerEverConnected.count(addr)) | ||
| 1365 | tierGood.push_back(addr); | ||
| 1366 | else if (g_btfPeerFails.find(addr) == g_btfPeerFails.end()) | ||
| 1367 | tierUntried.push_back(addr); | ||
| 1368 | else | ||
| 1369 | tierBad.push_back(addr); | ||
| 1370 | } | ||
| 1371 | // Within known-bad, put fewer-fails peers ahead of chronic offenders. | ||
| 1372 | sort(tierBad.begin(), tierBad.end(), | ||
| 1373 | [](const string& a, const string& b){ return g_btfPeerFails[a] < g_btfPeerFails[b]; }); | ||
| 1374 | } | ||
| 1375 | |||
| 1376 | vector<string> peers; | ||
| 1377 | peers.reserve(fresh.size()); | ||
| 1378 | peers.insert(peers.end(), tierGood.begin(), tierGood.end()); | ||
| 1379 | peers.insert(peers.end(), tierUntried.begin(), tierUntried.end()); | ||
| 1380 | peers.insert(peers.end(), tierBad.begin(), tierBad.end()); | ||
| 1098 | 1381 | ||
| 1099 | // Pick candidates for this pass (skip those in backoff, cap at budget). | 1382 | // Pick candidates for this pass (skip those in backoff, cap at budget). |
| 1383 | // | ||
| 1384 | // The tiered order above is deliberately biased toward peers we know | ||
| 1385 | // are good -- that's the right default, since dialing untested/bad | ||
| 1386 | // peers wastes attempts. But if every node on the network applies the | ||
| 1387 | // same bias, everyone converges on the same small set of "known good" | ||
| 1388 | // peers as neighbors: that correlates peer sets across the network, | ||
| 1389 | // which is exactly what standard random-graph connectivity results | ||
| 1390 | // assume you avoid. If that shared hub set has a bad day (e.g. one | ||
| 1391 | // relay outage), many nodes lose the same edges at once instead of | ||
| 1392 | // losing independent, uncorrelated edges. | ||
| 1393 | // | ||
| 1394 | // So: fill most of the pass from the reliable/tiered order, but reserve | ||
| 1395 | // a slice for peers picked at random from the *entire* fresh pool | ||
| 1396 | // (not just the front of the sorted list). Same pattern Bitcoin's own | ||
| 1397 | // peer manager and Kademlia-style DHTs use -- sticky reliable peers | ||
| 1398 | // plus a random sample, not one or the other. | ||
| 1100 | int64 now = GetTime(); | 1399 | int64 now = GetTime(); |
| 1400 | int nBudget = (int)BTF_DIALS_PER_PASS; | ||
| 1401 | { | ||
| 1402 | int nRoom = (int)BTF_TARGET_CONN - (int)vNodes.size(); | ||
| 1403 | if (nRoom < nBudget) nBudget = nRoom; | ||
| 1404 | } | ||
| 1405 | if (nBudget < 0) nBudget = 0; | ||
| 1406 | int nRandomSlots = (nBudget > 0) ? max(1, nBudget * 3 / 10) : 0; | ||
| 1407 | int nSortedSlots = nBudget - nRandomSlots; | ||
| 1408 | |||
| 1101 | vector<string> candidates; | 1409 | vector<string> candidates; |
| 1410 | set<string> setChosen; | ||
| 1102 | CRITICAL_BLOCK(cs_btfPeerState) | 1411 | CRITICAL_BLOCK(cs_btfPeerState) |
| 1103 | { | 1412 | { |
| 1413 | // Reliability slice: walk the tiered (good -> untried -> bad) order. | ||
| 1104 | for (const string& addr : peers) | 1414 | for (const string& addr : peers) |
| 1105 | { | 1415 | { |
| 1106 | if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break; | 1416 | if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break; |
| 1107 | if ((int)candidates.size() >= BTF_DIALS_PER_PASS) break; | 1417 | if ((int)candidates.size() >= nSortedSlots) break; |
| 1108 | map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); | 1418 | map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); |
| 1109 | if (bi != g_btfPeerBackoff.end() && now < bi->second) | 1419 | if (bi != g_btfPeerBackoff.end() && now < bi->second) |
| 1110 | continue; | 1420 | continue; |
| 1111 | candidates.push_back(addr); | 1421 | candidates.push_back(addr); |
| 1422 | setChosen.insert(addr); | ||
| 1423 | } | ||
| 1424 | |||
| 1425 | // Diversity slice: random sample from the full fresh pool, so the | ||
| 1426 | // dial isn't limited to whatever the tiered sort put up front. | ||
| 1427 | vector<string> pool; | ||
| 1428 | for (auto& p : fresh) | ||
| 1429 | { | ||
| 1430 | const string& addr = p.second; | ||
| 1431 | if (setChosen.count(addr)) | ||
| 1432 | continue; | ||
| 1433 | map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); | ||
| 1434 | if (bi != g_btfPeerBackoff.end() && now < bi->second) | ||
| 1435 | continue; | ||
| 1436 | pool.push_back(addr); | ||
| 1437 | } | ||
| 1438 | int nWant = min(nRandomSlots, (int)pool.size()); | ||
| 1439 | for (int i = 0; i < nWant; i++) | ||
| 1440 | { | ||
| 1441 | if ((int)vNodes.size() + (int)candidates.size() >= (int)BTF_TARGET_CONN) break; | ||
| 1442 | if ((int)candidates.size() >= nBudget) break; | ||
| 1443 | size_t j = i + (size_t)GetRand((uint64)(pool.size() - i)); | ||
| 1444 | std::swap(pool[i], pool[j]); | ||
| 1445 | candidates.push_back(pool[i]); | ||
| 1446 | setChosen.insert(pool[i]); | ||
| 1112 | } | 1447 | } |
| 1113 | } | 1448 | } |
| 1114 | 1449 | ||
| 1115 | if (candidates.empty()) return; | 1450 | if (candidates.empty()) return; |
| 1116 | 1451 | ||
| 1117 | // Launch one thread per candidate — all connect attempts run in parallel. | 1452 | // Resolve every candidate's descriptor in one batched pass (a handful of |
| 1453 | // relay round-trips total, one REQ per relay covering all candidates) | ||
| 1454 | // instead of each dial thread independently walking the relay list on | ||
| 1455 | // its own. That per-thread approach meant a 12-candidate pass could fire | ||
| 1456 | // up to 12 * nNostrRelays near-simultaneous connections at the same | ||
| 1457 | // handful of public relays -- enough to trip rate limits or get | ||
| 1458 | // handshakes refused, which then looks exactly like "dead peers" even | ||
| 1459 | // though the descriptors were perfectly resolvable. | ||
| 1460 | map<string, BtfResolvedPeer> resolved; | ||
| 1461 | BtfResolveMany(candidates, resolved); | ||
| 1462 | |||
| 1463 | // Candidates whose descriptor didn't resolve this pass are recorded as a | ||
| 1464 | // failure right away (no point spawning a tunnel thread with nothing to | ||
| 1465 | // dial), same bookkeeping as a failed connect attempt. | ||
| 1466 | vector<string> toDial; | ||
| 1467 | CRITICAL_BLOCK(cs_btfPeerState) | ||
| 1468 | { | ||
| 1469 | for (const string& addr : candidates) | ||
| 1470 | { | ||
| 1471 | if (resolved.count(addr)) | ||
| 1472 | { | ||
| 1473 | toDial.push_back(addr); | ||
| 1474 | continue; | ||
| 1475 | } | ||
| 1476 | int n = ++g_btfPeerFails[addr]; | ||
| 1477 | int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD | ||
| 1478 | : BTF_BACKOFF_CAP_SECS; | ||
| 1479 | int64 secs = 30; | ||
| 1480 | for (int k = 1; k < n && secs < cap; k++) secs *= 2; | ||
| 1481 | if (secs > cap) secs = cap; | ||
| 1482 | g_btfPeerBackoff[addr] = GetTime() + secs; | ||
| 1483 | } | ||
| 1484 | } | ||
| 1485 | if (toDial.empty()) return; | ||
| 1486 | |||
| 1487 | // Launch one thread per resolved candidate -- all connect attempts run in | ||
| 1488 | // parallel, but purely against each peer's own rendezvous meeting node, | ||
| 1489 | // not the shared Nostr relays, so there's no thundering-herd effect here. | ||
| 1118 | // Each thread writes its result back via shared state guarded by cs_btfPeerState. | 1490 | // Each thread writes its result back via shared state guarded by cs_btfPeerState. |
| 1119 | struct DialCtx { string addr; }; | 1491 | struct DialCtx { string addr; BtfResolvedPeer rp; }; |
| 1120 | vector<DialCtx*> ctxs; | 1492 | vector<DialCtx*> ctxs; |
| 1121 | for (const string& addr : candidates) | 1493 | for (const string& addr : toDial) |
| 1122 | { | 1494 | { |
| 1123 | DialCtx* ctx = new DialCtx{addr}; | 1495 | DialCtx* ctx = new DialCtx{addr, resolved[addr]}; |
| 1124 | ctxs.push_back(ctx); | 1496 | ctxs.push_back(ctx); |
| 1125 | _beginthread([](void* arg) { | 1497 | _beginthread([](void* arg) { |
| 1126 | DialCtx* ctx = (DialCtx*)arg; | 1498 | DialCtx* ctx = (DialCtx*)arg; |
| 1127 | string addr = ctx->addr; | 1499 | string addr = ctx->addr; |
| 1500 | string meeting = ctx->rp.meetingHostPort; | ||
| 1501 | unsigned char enc_pub[32]; | ||
| 1502 | memcpy(enc_pub, ctx->rp.enc_pub, 32); | ||
| 1128 | delete ctx; | 1503 | delete ctx; |
| 1129 | 1504 | ||
| 1130 | CNode* pnode = ConnectNodeBtf(addr); | 1505 | CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); |
| 1131 | CRITICAL_BLOCK(cs_btfPeerState) | 1506 | CRITICAL_BLOCK(cs_btfPeerState) |
| 1132 | { | 1507 | { |
| 1133 | if (pnode) | 1508 | if (pnode) |
| @@ -1138,13 +1513,16 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1138 | pnode->Release(); | 1513 | pnode->Release(); |
| 1139 | g_btfPeerBackoff.erase(addr); | 1514 | g_btfPeerBackoff.erase(addr); |
| 1140 | g_btfPeerFails.erase(addr); | 1515 | g_btfPeerFails.erase(addr); |
| 1516 | g_btfPeerEverConnected.insert(addr); | ||
| 1141 | } | 1517 | } |
| 1142 | else | 1518 | else |
| 1143 | { | 1519 | { |
| 1144 | int n = ++g_btfPeerFails[addr]; | 1520 | int n = ++g_btfPeerFails[addr]; |
| 1521 | int64 cap = (n >= BTF_CHRONIC_FAIL_THRESHOLD) ? BTF_BACKOFF_CAP_SECS_DEAD | ||
| 1522 | : BTF_BACKOFF_CAP_SECS; | ||
| 1145 | int64 secs = 30; | 1523 | int64 secs = 30; |
| 1146 | for (int k = 1; k < n && secs < 15*60; k++) secs *= 2; | 1524 | for (int k = 1; k < n && secs < cap; k++) secs *= 2; |
| 1147 | if (secs > 15*60) secs = 15*60; | 1525 | if (secs > cap) secs = cap; |
| 1148 | g_btfPeerBackoff[addr] = GetTime() + secs; | 1526 | g_btfPeerBackoff[addr] = GetTime() + secs; |
| 1149 | } | 1527 | } |
| 1150 | } | 1528 | } |
| @@ -1160,16 +1538,16 @@ static void ConnectDiscoveredBtfPeers() | |||
| 1160 | 1538 | ||
| 1161 | void ThreadNostrSeed(void* parg) | 1539 | void ThreadNostrSeed(void* parg) |
| 1162 | { | 1540 | { |
| 1163 | printf("ThreadNostrSeed started\n"); | 1541 | LogPrint("nostr", "ThreadNostrSeed started\n"); |
| 1164 | 1542 | ||
| 1165 | if (!EnsureNostrKey()) | 1543 | if (!EnsureNostrKey()) |
| 1166 | { | 1544 | { |
| 1167 | printf("Nostr: failed to initialize secp256k1 key\n"); | 1545 | LogPrint("nostr", "Nostr: failed to initialize secp256k1 key\n"); |
| 1168 | return; | 1546 | return; |
| 1169 | } | 1547 | } |
| 1170 | CNostrKey& key = g_nostrKey; | 1548 | CNostrKey& key = g_nostrKey; |
| 1171 | printf("Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); | 1549 | LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); |
| 1172 | printf("Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); | 1550 | LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); |
| 1173 | 1551 | ||
| 1174 | // One-time self-test of the rendezvous: publish our descriptor to a relay, | 1552 | // One-time self-test of the rendezvous: publish our descriptor to a relay, |
| 1175 | // then resolve our OWN .btf address back and verify it self-certifies. Proves | 1553 | // then resolve our OWN .btf address back and verify it self-certifies. Proves |
| @@ -1178,17 +1556,20 @@ void ThreadNostrSeed(void* parg) | |||
| 1178 | { | 1556 | { |
| 1179 | try | 1557 | try |
| 1180 | { | 1558 | { |
| 1181 | CWebSocket ws; | 1559 | CRelayLease lease(pszNostrRelays[i], 10); |
| 1182 | if (!ws.Connect(pszNostrRelays[i], 10)) continue; | 1560 | if (!lease.Connected()) continue; |
| 1561 | CWebSocket& ws = lease.Ws(); | ||
| 1183 | PublishDescriptor(ws, key); | 1562 | PublishDescriptor(ws, key); |
| 1184 | Sleep(700); // let the relay store the replaceable event | 1563 | Sleep(700); // let the relay store the replaceable event |
| 1185 | btf::Descriptor d; | 1564 | btf::Descriptor d; |
| 1186 | if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) | 1565 | if (ResolveDescriptor(ws, key.ctx, key.BtfAddress(), d)) |
| 1187 | { | 1566 | { |
| 1188 | printf("Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", | 1567 | LogPrint("nostr", "Nostr: .btf self-resolve OK via %s (meeting_node=%s, enc=%s...)\n", |
| 1189 | pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); | 1568 | pszNostrRelays[i], d.meeting_node.c_str(), d.enc.substr(0, 16).c_str()); |
| 1569 | lease.fOk = true; | ||
| 1190 | break; | 1570 | break; |
| 1191 | } | 1571 | } |
| 1572 | lease.fOk = true; | ||
| 1192 | } | 1573 | } |
| 1193 | CATCH_PRINT_EXCEPTION("btf self-test") | 1574 | CATCH_PRINT_EXCEPTION("btf self-test") |
| 1194 | } | 1575 | } |
| @@ -1223,8 +1604,9 @@ void ThreadNostrSeed(void* parg) | |||
| 1223 | ann.dashboardUrl = strPoolDashboardUrl; | 1604 | ann.dashboardUrl = strPoolDashboardUrl; |
| 1224 | ann.feePercent = dPoolFeePercent; | 1605 | ann.feePercent = dPoolFeePercent; |
| 1225 | uint64 roundShares = 0; | 1606 | uint64 roundShares = 0; |
| 1226 | GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares); | 1607 | double totalHashRate = 0.0; |
| 1227 | ann.hashRate = 0.0; | 1608 | GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares, totalHashRate); |
| 1609 | ann.hashRate = totalHashRate; | ||
| 1228 | ann.createdAt = GetTime(); | 1610 | ann.createdAt = GetTime(); |
| 1229 | if (!ann.btfAddress.empty() && !ann.poolName.empty()) | 1611 | if (!ann.btfAddress.empty() && !ann.poolName.empty()) |
| 1230 | BtfPublishPoolAnnouncement(ann); | 1612 | BtfPublishPoolAnnouncement(ann); |