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.cpp505
1 files changed, 385 insertions, 120 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 44aec56..c2ccc4c 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -38,18 +38,24 @@ static const int BTF_RELAY_KIND = 38502;
38static const char* BTF_RELAY_DTAG = "btf-relay-2"; 38static const char* BTF_RELAY_DTAG = "btf-relay-2";
39static const size_t BTF_MAX_DISCOVERED_RELAYS = 50; // anti-spam cap 39static const size_t BTF_MAX_DISCOVERED_RELAYS = 50; // anti-spam cap
40 40
41// Nostr network/scope for Bitflash. Events are replaceable (NIP-78). 41// Pool announcements carry the operator's .btf address, fee, and live stats.
42static const int NOSTR_KIND = 30078; 42static const int BTF_POOL_KIND = 38503;
43static const char* NOSTR_DTAG = "bitflash-mainnet-2"; 43static const char* BTF_POOL_DTAG = "btf-pool-2";
44static const int64 BTF_POOL_MAX_AGE = 3 * 60; // seconds
44 45
45// 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
46// 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.
47static const unsigned int BTF_TARGET_CONN = 8; 48static const unsigned int BTF_TARGET_CONN = 8;
48static const int BTF_DIALS_PER_PASS = 4; 49static const int BTF_DIALS_PER_PASS = 12; // try more peers per Nostr cycle
50
51// Nostr descriptor freshness cutoff: skip peers whose descriptor hasn't been
52// refreshed in this many seconds. Stale descriptors mean the node isn't running.
53static const int64 BTF_PEER_MAX_AGE = 3 * 60 * 60; // 3 hours
49 54
50// Other nodes' .btf addresses learned from their descriptors on the relays. 55// Other nodes' .btf addresses learned from their descriptors on the relays.
51static set<string> g_btfPeers; 56// Maps address -> descriptor created_at timestamp (Unix seconds).
52static CCriticalSection cs_btfPeers; 57static map<string, int64> g_btfPeers;
58static CCriticalSection cs_btfPeers;
53 59
54// Public discovery relays. Tunable. 60// Public discovery relays. Tunable.
55const char* pszNostrRelays[] = { 61const char* pszNostrRelays[] = {
@@ -95,6 +101,10 @@ string strBtfAnnounceRelay;
95static set<string> g_discoveredRelays; 101static set<string> g_discoveredRelays;
96static CCriticalSection cs_discoveredRelays; 102static CCriticalSection cs_discoveredRelays;
97 103
104// Live pool announcements discovered from the relays.
105static map<string, BtfPoolAnnouncement> g_discoveredPools;
106static CCriticalSection cs_discoveredPools;
107
98// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. 108// Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host.
99static bool LooksLikeRelay(const string& r) 109static bool LooksLikeRelay(const string& r)
100{ 110{
@@ -307,6 +317,27 @@ std::string BtfLocalAddress()
307 return g_nostrKey.BtfAddress(); 317 return g_nostrKey.BtfAddress();
308} 318}
309 319
320void BtfGetPoolAnnouncements(std::vector<BtfPoolAnnouncement>& out)
321{
322 out.clear();
323 int64 now = GetTime();
324 CRITICAL_BLOCK(cs_discoveredPools)
325 {
326 for (map<string, BtfPoolAnnouncement>::iterator it = g_discoveredPools.begin(); it != g_discoveredPools.end(); )
327 {
328 if (it->second.createdAt > 0 && now - it->second.createdAt > BTF_POOL_MAX_AGE)
329 {
330 it = g_discoveredPools.erase(it);
331 continue;
332 }
333 out.push_back(it->second);
334 ++it;
335 }
336 }
337 sort(out.begin(), out.end(), [](const BtfPoolAnnouncement& a, const BtfPoolAnnouncement& b) {
338 return a.createdAt > b.createdAt;
339 });
340}
310 341
311// Build and sign a Nostr event (NIP-01). Returns the event JSON. 342// Build and sign a Nostr event (NIP-01). Returns the event JSON.
312static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags, 343static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags,
@@ -567,25 +598,6 @@ private:
567}; 598};
568 599
569 600
570// Handle a received event: extract "ip:port" and register the address.
571static void HandlePeerEvent(const json& ev)
572{
573 if (!ev.contains("content")) return;
574 string content = ev["content"].get<string>();
575 // expected content: "ip:port"
576 CAddress addr(content.c_str());
577 if (addr.ip == 0 || addr.ip == INADDR_NONE)
578 return;
579 if (addr.ip == addrLocalHost.ip && addr.port == addrLocalHost.port)
580 return; // not ourselves (same ip:port)
581 // Peer received from the relay (before AddAddress's routability filter).
582 printf("Nostr: peer received from relay: %s\n", addr.ToStringIPPort().c_str());
583 CAddrDB addrdb;
584 if (AddAddress(addrdb, addr))
585 printf("Nostr: new peer added %s\n", addr.ToStringIPPort().c_str());
586}
587
588
589// Publish this node's self-certifying `.btf` descriptor: "reach me (this key) 601// Publish this node's self-certifying `.btf` descriptor: "reach me (this key)
590// via <meeting_node>, encrypting to my x25519 key <enc>". Signed by the node 602// via <meeting_node>, encrypting to my x25519 key <enc>". Signed by the node
591// key so only the address's owner can publish it (no hijacking). 603// key so only the address's owner can publish it (no hijacking).
@@ -624,6 +636,176 @@ static void PublishRelayAnnouncement(CWebSocket& ws, CNostrKey& key)
624 } 636 }
625} 637}
626 638
639static bool PublishPoolAnnouncementOnRelay(CWebSocket& ws, CNostrKey& key,
640 const BtfPoolAnnouncement& ann)
641{
642 if (ann.btfAddress.empty() || ann.poolName.empty())
643 return false;
644
645 json content = json::object();
646 content["btf"] = ann.btfAddress;
647 content["name"] = ann.poolName;
648 if (!ann.dashboardUrl.empty())
649 content["dashboard_url"] = ann.dashboardUrl;
650 content["fee_pct"] = ann.feePercent;
651 content["miners"] = ann.connectedMiners;
652 content["blocks"] = ann.blocksFound;
653 content["hashrate"] = ann.hashRate;
654
655 json tags = json::array({
656 json::array({ "d", BTF_POOL_DTAG }),
657 json::array({ "t", BTF_POOL_DTAG })
658 });
659 json ev;
660 if (!BuildSignedEvent(key, BTF_POOL_KIND, tags, content.dump(), ev))
661 return false;
662 json pub = json::array({ "EVENT", ev });
663 return ws.SendText(pub.dump());
664}
665
666bool BtfPublishPoolAnnouncement(const BtfPoolAnnouncement& ann)
667{
668 if (!EnsureNostrKey())
669 return false;
670
671 bool fPublished = false;
672 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
673 {
674 try
675 {
676 CWebSocket ws;
677 if (!ws.Connect(pszNostrRelays[i], 10))
678 continue;
679 if (PublishPoolAnnouncementOnRelay(ws, g_nostrKey, ann))
680 fPublished = true;
681 }
682 CATCH_PRINT_EXCEPTION("BtfPublishPoolAnnouncement")
683 }
684 return fPublished;
685}
686
687bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncement& out)
688{
689 unsigned char pk[32];
690 if (!btf::ParseAddress(poolBtfAddr, pk))
691 return false;
692
693 if (!EnsureNostrKey())
694 return false;
695
696 int64 bestCreatedAt = 0;
697 bool fFound = false;
698 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
699 {
700 try
701 {
702 CWebSocket ws;
703 if (!ws.Connect(pszNostrRelays[i], 10))
704 continue;
705
706 json filter = json::object();
707 filter["authors"] = json::array({ HexEncode(pk, 32) });
708 filter["kinds"] = json::array({ BTF_POOL_KIND });
709 filter["#d"] = json::array({ BTF_POOL_DTAG });
710 filter["limit"] = 1;
711 json req = json::array({ "REQ", "btf-pool-query", filter });
712 if (!ws.SendText(req.dump()))
713 continue;
714
715 for (;;)
716 {
717 string msg;
718 if (!ws.RecvText(msg))
719 break;
720 json j;
721 try { j = json::parse(msg); } catch (...) { continue; }
722 if (!j.is_array() || j.empty()) continue;
723 string type = j[0].get<string>();
724 if (type == "EVENT" && j.size() >= 3)
725 {
726 const json& ev = j[2];
727 if (!ev.contains("content") || !ev["content"].is_string())
728 continue;
729 BtfPoolAnnouncement ann;
730 try
731 {
732 json content = json::parse(ev["content"].get<string>());
733 if (content.contains("btf") && content["btf"].is_string())
734 ann.btfAddress = content["btf"].get<string>();
735 if (content.contains("name") && content["name"].is_string())
736 ann.poolName = content["name"].get<string>();
737 if (content.contains("dashboard_url") && content["dashboard_url"].is_string())
738 ann.dashboardUrl = content["dashboard_url"].get<string>();
739 ann.feePercent = content.value("fee_pct", content.value("fee", 0.0));
740 ann.connectedMiners = content.value("miners", 0);
741 ann.blocksFound = content.value("blocks", 0);
742 ann.hashRate = content.value("hashrate", 0.0);
743 ann.createdAt = ev.value("created_at", (int64)0);
744 }
745 catch (...)
746 {
747 continue;
748 }
749 if (ann.btfAddress == poolBtfAddr && ann.createdAt >= bestCreatedAt)
750 {
751 bestCreatedAt = ann.createdAt;
752 out = ann;
753 fFound = true;
754 }
755 }
756 else if (type == "EOSE")
757 break;
758 }
759 }
760 CATCH_PRINT_EXCEPTION("BtfQueryPoolAnnouncement")
761 }
762
763 return fFound;
764}
765
766static void HandlePoolAnnouncement(const json& ev)
767{
768 if (!ev.contains("content") || !ev["content"].is_string())
769 return;
770
771 BtfPoolAnnouncement ann;
772 try
773 {
774 json content = json::parse(ev["content"].get<string>());
775 if (content.contains("btf") && content["btf"].is_string())
776 ann.btfAddress = content["btf"].get<string>();
777 else if (content.contains("address") && content["address"].is_string())
778 ann.btfAddress = content["address"].get<string>();
779 if (content.contains("name") && content["name"].is_string())
780 ann.poolName = content["name"].get<string>();
781 else if (content.contains("pool_name") && content["pool_name"].is_string())
782 ann.poolName = content["pool_name"].get<string>();
783 if (content.contains("dashboard_url") && content["dashboard_url"].is_string())
784 ann.dashboardUrl = content["dashboard_url"].get<string>();
785 else if (content.contains("dashboard") && content["dashboard"].is_string())
786 ann.dashboardUrl = content["dashboard"].get<string>();
787 ann.feePercent = content.value("fee_pct", content.value("fee", 0.0));
788 ann.connectedMiners = content.value("miners", 0);
789 ann.blocksFound = content.value("blocks", 0);
790 ann.hashRate = content.value("hashrate", 0.0);
791 ann.createdAt = ev.value("created_at", (int64)0);
792 }
793 catch (...)
794 {
795 return;
796 }
797
798 if (ann.btfAddress.empty() || ann.poolName.empty())
799 return;
800
801 CRITICAL_BLOCK(cs_discoveredPools)
802 {
803 map<string, BtfPoolAnnouncement>::iterator it = g_discoveredPools.find(ann.btfAddress);
804 if (it == g_discoveredPools.end() || ann.createdAt >= it->second.createdAt)
805 g_discoveredPools[ann.btfAddress] = ann;
806 }
807}
808
627// Resolve a `.btf` address via a relay: fetch the descriptor published by that 809// Resolve a `.btf` address via a relay: fetch the descriptor published by that
628// address's key and verify it self-certifies under the decoded pubkey. 810// address's key and verify it self-certifies under the decoded pubkey.
629static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 811static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
@@ -692,7 +874,7 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
692} 874}
693 875
694 876
695// Connect to a relay: publish our address and collect the others'. 877// Connect to a relay: publish our .btf descriptor and collect relay data.
696static bool SeedFromRelay(CNostrKey& key, const string& relay) 878static bool SeedFromRelay(CNostrKey& key, const string& relay)
697{ 879{
698 CWebSocket ws; 880 CWebSocket ws;
@@ -700,64 +882,12 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
700 return false; 882 return false;
701 printf("Nostr: connected to %s\n", relay.c_str()); 883 printf("Nostr: connected to %s\n", relay.c_str());
702 884
703 // Publish our announcement (replaceable event with our ip:port)
704 if (addrLocalHost.ip != 0)
705 {
706 json tags = json::array({
707 json::array({ "d", NOSTR_DTAG }),
708 json::array({ "t", NOSTR_DTAG })
709 });
710 json ev;
711 if (BuildSignedEvent(key, NOSTR_KIND, tags, addrLocalHost.ToStringIPPort(), ev))
712 {
713 json pub = json::array({ "EVENT", ev });
714 ws.SendText(pub.dump());
715 }
716 }
717
718 // Publish our self-certifying .btf descriptor (rendezvous discovery) 885 // Publish our self-certifying .btf descriptor (rendezvous discovery)
719 PublishDescriptor(ws, key); 886 PublishDescriptor(ws, key);
720 887
721 // If we run a relay, announce it so others discover it automatically 888 // If we run a relay, announce it so others discover it automatically
722 PublishRelayAnnouncement(ws, key); 889 PublishRelayAnnouncement(ws, key);
723 890
724 // Subscribe to receive the announcements of other nodes
725 json filter = json::object();
726 filter["kinds"] = json::array({ NOSTR_KIND });
727 filter["#d"] = json::array({ NOSTR_DTAG });
728 filter["limit"] = 500;
729 json req = json::array({ "REQ", "bf-sub", filter });
730 if (!ws.SendText(req.dump()))
731 return false;
732
733 // Read responses until EOSE (end of stored events) or timeout
734 int nEvents = 0;
735 for (;;)
736 {
737 string msg;
738 if (!ws.RecvText(msg))
739 break; // timeout or closed connection
740 json j;
741 try { j = json::parse(msg); } catch (...) { continue; }
742 if (!j.is_array() || j.empty()) continue;
743 string type = j[0].get<string>();
744 if (type == "EVENT" && j.size() >= 3)
745 {
746 HandlePeerEvent(j[2]);
747 if (++nEvents > 1000) break;
748 }
749 else if (type == "EOSE")
750 {
751 // We received all stored events; close this relay.
752 break;
753 }
754 else if (type == "NOTICE" || type == "OK" || type == "CLOSED")
755 {
756 // informational
757 }
758 }
759 printf("Nostr: %s returned %d events\n", relay.c_str(), nEvents);
760
761 // Discover other nodes' .btf descriptors so we can auto-connect anonymously 891 // Discover other nodes' .btf descriptors so we can auto-connect anonymously
762 // (no manual /connectbtf). Each node publishes one addressable descriptor 892 // (no manual /connectbtf). Each node publishes one addressable descriptor
763 // (kind 38501, d-tag btf-descriptor); we learn every peer's .btf address by 893 // (kind 38501, d-tag btf-descriptor); we learn every peer's .btf address by
@@ -791,10 +921,15 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
791 if (!HexDecode(ev["pubkey"].get<string>(), pkb, 32)) continue; 921 if (!HexDecode(ev["pubkey"].get<string>(), pkb, 32)) continue;
792 if (memcmp(pkb, key.xonly, 32) == 0) continue; // not ourselves 922 if (memcmp(pkb, key.xonly, 32) == 0) continue; // not ourselves
793 string peerAddr = btf::Address(pkb); 923 string peerAddr = btf::Address(pkb);
924 int64 createdAt = ev.value("created_at", (int64)0);
794 bool fNew = false; 925 bool fNew = false;
795 CRITICAL_BLOCK(cs_btfPeers) 926 CRITICAL_BLOCK(cs_btfPeers)
796 fNew = g_btfPeers.insert(peerAddr).second; 927 {
797 if (fNew) 928 auto it = g_btfPeers.find(peerAddr);
929 if (it == g_btfPeers.end()) { g_btfPeers[peerAddr] = createdAt; fNew = true; }
930 else if (createdAt > it->second) it->second = createdAt; // refresh
931 }
932 if (fNew && fDebug)
798 printf("Nostr: discovered .btf peer %s\n", peerAddr.c_str()); 933 printf("Nostr: discovered .btf peer %s\n", peerAddr.c_str());
799 if (++nPeers > 500) break; 934 if (++nPeers > 500) break;
800 } 935 }
@@ -838,7 +973,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
838 CRITICAL_BLOCK(cs_discoveredRelays) 973 CRITICAL_BLOCK(cs_discoveredRelays)
839 if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS) 974 if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS)
840 fNew = g_discoveredRelays.insert(r).second; 975 fNew = g_discoveredRelays.insert(r).second;
841 if (fNew) 976 if (fNew && fDebug)
842 printf("Nostr: discovered relay %s\n", r.c_str()); 977 printf("Nostr: discovered relay %s\n", r.c_str());
843 } 978 }
844 else if (type == "EOSE") 979 else if (type == "EOSE")
@@ -846,9 +981,62 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
846 } 981 }
847 } 982 }
848 } 983 }
984
985 // Discover live pool announcements. These are expiring status beacons, not
986 // permanent directory entries, so the GUI can show only fresh pools.
987 ws.SendText(json::array({ "CLOSE", "btf-pools" }).dump());
988 {
989 json pfilter = json::object();
990 pfilter["kinds"] = json::array({ BTF_POOL_KIND });
991 pfilter["#d"] = json::array({ BTF_POOL_DTAG });
992 pfilter["limit"] = 200;
993 json preq = json::array({ "REQ", "btf-pools", pfilter });
994 if (ws.SendText(preq.dump()))
995 {
996 for (;;)
997 {
998 string msg;
999 if (!ws.RecvText(msg))
1000 break;
1001 json j;
1002 try { j = json::parse(msg); } catch (...) { continue; }
1003 if (!j.is_array() || j.empty()) continue;
1004 string type = j[0].get<string>();
1005 if (type == "EVENT" && j.size() >= 3)
1006 HandlePoolAnnouncement(j[2]);
1007 else if (type == "EOSE")
1008 break;
1009 }
1010 }
1011 }
849 return true; 1012 return true;
850} 1013}
851 1014
1015void ThreadBtfPoolAnnouncer(void*)
1016{
1017 printf("ThreadBtfPoolAnnouncer started\n");
1018 while (!fShutdown)
1019 {
1020 if (nMineMode == MINE_OPERATOR && gPoolRunning)
1021 {
1022 BtfPoolAnnouncement ann;
1023 ann.btfAddress = BtfLocalAddress();
1024 ann.poolName = strPoolName;
1025 ann.dashboardUrl = strPoolDashboardUrl;
1026 ann.feePercent = dPoolFeePercent;
1027 uint64 roundShares = 0;
1028 GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares);
1029 ann.hashRate = 0.0;
1030 ann.createdAt = GetTime();
1031 if (!ann.btfAddress.empty() && !ann.poolName.empty())
1032 BtfPublishPoolAnnouncement(ann);
1033 }
1034
1035 for (int i = 0; i < 60 && !fShutdown; i++)
1036 Sleep(1000);
1037 }
1038}
1039
852 1040
853// Per-peer backoff: don't redial a .btf that just failed until this time. Keeps 1041// Per-peer backoff: don't redial a .btf that just failed until this time. Keeps
854// dead/stale descriptors (e.g. abandoned test identities that linger on the 1042// dead/stale descriptors (e.g. abandoned test identities that linger on the
@@ -856,56 +1044,117 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
856// peers. Backoff grows on repeated failures, capped. 1044// peers. Backoff grows on repeated failures, capped.
857static map<string, int64> g_btfPeerBackoff; // addr -> next-try time 1045static map<string, int64> g_btfPeerBackoff; // addr -> next-try time
858static map<string, int> g_btfPeerFails; // addr -> consecutive failures 1046static map<string, int> g_btfPeerFails; // addr -> consecutive failures
1047static CCriticalSection cs_btfPeerState;
859 1048
860// Dial the .btf peers discovered on the relays, up to a target connection count. 1049// Dial the .btf peers discovered on the relays, up to a target connection count.
861// Mirrors ThreadOpenConnections' ref handling: a kept connection holds one ref 1050// Connections are attempted in parallel so a single slow/dead relay doesn't
862// (owned by fNetworkNode, released on disconnect); a redundant one is released. 1051// stall the entire pass. Each candidate gets its own thread; all threads start
1052// at once and we wait for them to finish before returning.
863static void ConnectDiscoveredBtfPeers() 1053static void ConnectDiscoveredBtfPeers()
864{ 1054{
865 vector<string> peers; 1055 vector<pair<int64,string>> peersWithAge;
1056 int nTotal = 0;
866 CRITICAL_BLOCK(cs_btfPeers) 1057 CRITICAL_BLOCK(cs_btfPeers)
867 peers.assign(g_btfPeers.begin(), g_btfPeers.end());
868
869 // Shuffle: the candidate set is sorted, so without this a handful of dead
870 // peers at the front would be retried every pass and the live peers further
871 // down the list would never be reached within the per-pass dial budget.
872 for (size_t i = peers.size(); i > 1; i--)
873 swap(peers[i - 1], peers[(size_t)GetRand(i)]);
874
875 int64 now = GetTime();
876 int nDials = 0;
877 foreach(const string& addr, peers)
878 { 1058 {
879 if (fShutdown) break; 1059 nTotal = g_btfPeers.size();
880 if (vNodes.size() >= BTF_TARGET_CONN) break; 1060 for (auto& kv : g_btfPeers)
881 if (nDials >= BTF_DIALS_PER_PASS) break; 1061 peersWithAge.push_back({kv.second, kv.first});
1062 }
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; });
882 1067
883 // Skip peers still in backoff from a recent failure. 1068 int64 cutoff = GetTime() - BTF_PEER_MAX_AGE;
884 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr); 1069 int nFresh = 0;
885 if (bi != g_btfPeerBackoff.end() && now < bi->second) 1070 vector<string> peers;
1071 for (auto& p : peersWithAge)
1072 {
1073 if (p.first < cutoff)
886 continue; 1074 continue;
1075 peers.push_back(p.second);
1076 nFresh++;
1077 }
1078 if (!peers.empty())
1079 printf("Nostr: %d fresh peers, %d stale (of %d total)\n", nFresh, nTotal-nFresh, nTotal);
887 1080
888 CNode* pnode = ConnectNodeBtf(addr); 1081 // Drop stale peers so we stop retrying dead descriptors until they refresh.
889 if (pnode) 1082 {
1083 CRITICAL_BLOCK(cs_btfPeers)
890 { 1084 {
891 if (!pnode->fNetworkNode) 1085 for (map<string, int64>::iterator it = g_btfPeers.begin(); it != g_btfPeers.end(); )
892 pnode->fNetworkNode = true; // keep the ref from ConnectNodeBtf 1086 {
893 else 1087 if (it->second < cutoff)
894 pnode->Release(); // already connected; drop the extra ref 1088 it = g_btfPeers.erase(it);
895 g_btfPeerBackoff.erase(addr); // reachable again 1089 else
896 g_btfPeerFails.erase(addr); 1090 ++it;
1091 }
897 } 1092 }
898 else 1093 }
1094
1095 // Shuffle so we don't always retry the same dead peers at the front.
1096 for (size_t i = peers.size(); i > 1; i--)
1097 swap(peers[i - 1], peers[(size_t)GetRand(i)]);
1098
1099 // Pick candidates for this pass (skip those in backoff, cap at budget).
1100 int64 now = GetTime();
1101 vector<string> candidates;
1102 CRITICAL_BLOCK(cs_btfPeerState)
1103 {
1104 for (const string& addr : peers)
899 { 1105 {
900 // Exponential-ish backoff: 1,2,4,... minutes, capped at 30. 1106 if ((int)vNodes.size() >= (int)BTF_TARGET_CONN) break;
901 int n = ++g_btfPeerFails[addr]; 1107 if ((int)candidates.size() >= BTF_DIALS_PER_PASS) break;
902 int64 mins = 1; 1108 map<string, int64>::iterator bi = g_btfPeerBackoff.find(addr);
903 for (int k = 1; k < n && mins < 30; k++) mins *= 2; 1109 if (bi != g_btfPeerBackoff.end() && now < bi->second)
904 if (mins > 30) mins = 30; 1110 continue;
905 g_btfPeerBackoff[addr] = now + mins * 60; 1111 candidates.push_back(addr);
906 nDials++; // only count real (failed) dial attempts against the budget
907 } 1112 }
908 } 1113 }
1114
1115 if (candidates.empty()) return;
1116
1117 // Launch one thread per candidate — all connect attempts run in parallel.
1118 // Each thread writes its result back via shared state guarded by cs_btfPeerState.
1119 struct DialCtx { string addr; };
1120 vector<DialCtx*> ctxs;
1121 for (const string& addr : candidates)
1122 {
1123 DialCtx* ctx = new DialCtx{addr};
1124 ctxs.push_back(ctx);
1125 _beginthread([](void* arg) {
1126 DialCtx* ctx = (DialCtx*)arg;
1127 string addr = ctx->addr;
1128 delete ctx;
1129
1130 CNode* pnode = ConnectNodeBtf(addr);
1131 CRITICAL_BLOCK(cs_btfPeerState)
1132 {
1133 if (pnode)
1134 {
1135 if (!pnode->fNetworkNode)
1136 pnode->fNetworkNode = true;
1137 else
1138 pnode->Release();
1139 g_btfPeerBackoff.erase(addr);
1140 g_btfPeerFails.erase(addr);
1141 }
1142 else
1143 {
1144 int n = ++g_btfPeerFails[addr];
1145 int64 secs = 30;
1146 for (int k = 1; k < n && secs < 15*60; k++) secs *= 2;
1147 if (secs > 15*60) secs = 15*60;
1148 g_btfPeerBackoff[addr] = GetTime() + secs;
1149 }
1150 }
1151 }, 0, ctx);
1152 }
1153
1154 // Wait long enough for all parallel dials to finish (each has a 10s socket
1155 // timeout, so 12s gives them all time to resolve).
1156 for (int i = 0; i < 12 && !fShutdown; i++)
1157 Sleep(1000);
909} 1158}
910 1159
911 1160
@@ -966,9 +1215,25 @@ void ThreadNostrSeed(void* parg)
966 } 1215 }
967 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers") 1216 CATCH_PRINT_EXCEPTION("ConnectDiscoveredBtfPeers")
968 1217
969 // Re-announce/re-discover periodically (the replaceable event keeps the 1218 if (nMineMode == MINE_OPERATOR && gPoolRunning)
970 // relay up to date and picks up nodes that joined after us) 1219 {
971 for (int i = 0; i < 60 && !fShutdown; i++) 1220 BtfPoolAnnouncement ann;
1221 ann.btfAddress = BtfLocalAddress();
1222 ann.poolName = strPoolName;
1223 ann.dashboardUrl = strPoolDashboardUrl;
1224 ann.feePercent = dPoolFeePercent;
1225 uint64 roundShares = 0;
1226 GetPoolOperatorStats(ann.connectedMiners, ann.blocksFound, roundShares);
1227 ann.hashRate = 0.0;
1228 ann.createdAt = GetTime();
1229 if (!ann.btfAddress.empty() && !ann.poolName.empty())
1230 BtfPublishPoolAnnouncement(ann);
1231 }
1232
1233 // Re-discover periodically. Sleep less aggressively when
1234 // we have no connections yet so we find peers faster on startup.
1235 int nSleepSecs = vNodes.empty() ? 20 : 60;
1236 for (int i = 0; i < nSleepSecs && !fShutdown; i++)
972 Sleep(1000); 1237 Sleep(1000);
973 } 1238 }
974} 1239}