diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 505 |
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; | |||
| 38 | static const char* BTF_RELAY_DTAG = "btf-relay-2"; | 38 | static const char* BTF_RELAY_DTAG = "btf-relay-2"; |
| 39 | static const size_t BTF_MAX_DISCOVERED_RELAYS = 50; // anti-spam cap | 39 | static 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. |
| 42 | static const int NOSTR_KIND = 30078; | 42 | static const int BTF_POOL_KIND = 38503; |
| 43 | static const char* NOSTR_DTAG = "bitflash-mainnet-2"; | 43 | static const char* BTF_POOL_DTAG = "btf-pool-2"; |
| 44 | static 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. |
| 47 | static const unsigned int BTF_TARGET_CONN = 8; | 48 | static const unsigned int BTF_TARGET_CONN = 8; |
| 48 | static const int BTF_DIALS_PER_PASS = 4; | 49 | static 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. | ||
| 53 | static 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. |
| 51 | static set<string> g_btfPeers; | 56 | // Maps address -> descriptor created_at timestamp (Unix seconds). |
| 52 | static CCriticalSection cs_btfPeers; | 57 | static map<string, int64> g_btfPeers; |
| 58 | static CCriticalSection cs_btfPeers; | ||
| 53 | 59 | ||
| 54 | // Public discovery relays. Tunable. | 60 | // Public discovery relays. Tunable. |
| 55 | const char* pszNostrRelays[] = { | 61 | const char* pszNostrRelays[] = { |
| @@ -95,6 +101,10 @@ string strBtfAnnounceRelay; | |||
| 95 | static set<string> g_discoveredRelays; | 101 | static set<string> g_discoveredRelays; |
| 96 | static CCriticalSection cs_discoveredRelays; | 102 | static CCriticalSection cs_discoveredRelays; |
| 97 | 103 | ||
| 104 | // Live pool announcements discovered from the relays. | ||
| 105 | static map<string, BtfPoolAnnouncement> g_discoveredPools; | ||
| 106 | static 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. |
| 99 | static bool LooksLikeRelay(const string& r) | 109 | static 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 | ||
| 320 | void 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. |
| 312 | static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags, | 343 | static 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. | ||
| 571 | static 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 | ||
| 639 | static 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 | |||
| 666 | bool 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 | |||
| 687 | bool 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 | |||
| 766 | static 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. |
| 629 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 811 | static 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. |
| 696 | static bool SeedFromRelay(CNostrKey& key, const string& relay) | 878 | static 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 | ||
| 1015 | void 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. |
| 857 | static map<string, int64> g_btfPeerBackoff; // addr -> next-try time | 1045 | static map<string, int64> g_btfPeerBackoff; // addr -> next-try time |
| 858 | static map<string, int> g_btfPeerFails; // addr -> consecutive failures | 1046 | static map<string, int> g_btfPeerFails; // addr -> consecutive failures |
| 1047 | static 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. | ||
| 863 | static void ConnectDiscoveredBtfPeers() | 1053 | static 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 | } |