upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/nostr.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/nostr.cpp')
-rw-r--r--src/nostr.cpp518
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.
48static 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.
53static const unsigned int BTF_TARGET_CONN = 24;
49static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle 54static 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;
58static CCriticalSection cs_btfPeers; 63static 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.
61const char* pszNostrRelays[] = { 79const 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};
67const int nNostrRelays = ARRAYLEN(pszNostrRelays); 88const 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
341int GetDiscoveredPeerCount()
342{
343 int n = 0;
344 CRITICAL_BLOCK(cs_btfPeers) { n = (int)g_btfPeers.size(); }
345 return n;
346}
347
320void BtfGetPoolAnnouncements(std::vector<BtfPoolAnnouncement>& out) 348void 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.
662struct RelayConn
663{
664 CWebSocket ws;
665 CCriticalSection cs; // serializes use of this relay's socket
666 bool fConnected;
667 RelayConn() : fConnected(false) {}
668};
669static map<string, RelayConn*> g_relayPool;
670static CCriticalSection cs_relayPool;
671
672static 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.
692class CRelayLease
693{
694public:
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
722private:
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.
1018bool 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.
878static bool SeedFromRelay(CNostrKey& key, const string& relay) 1110static 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
1250volatile bool gAnnounceNow = false;
1251
1015void ThreadBtfPoolAnnouncer(void*) 1252void 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.
1045static map<string, int64> g_btfPeerBackoff; // addr -> next-try time 1292static map<string, int64> g_btfPeerBackoff; // addr -> next-try time
1046static map<string, int> g_btfPeerFails; // addr -> consecutive failures 1293static map<string, int> g_btfPeerFails; // addr -> consecutive failures
1294static set<string> g_btfPeerEverConnected; // addr -> has succeeded at least once
1047static CCriticalSection cs_btfPeerState; 1295static 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.
1303static const int64 BTF_BACKOFF_CAP_SECS = 15 * 60; // 15 min: initial ramp cap
1304static const int64 BTF_BACKOFF_CAP_SECS_DEAD = 6 * 60 * 60; // 6 hr: cap for chronic failures
1305static 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
1161void ThreadNostrSeed(void* parg) 1539void 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);