diff options
Diffstat (limited to 'src/nostr.cpp')
| -rw-r--r-- | src/nostr.cpp | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp index 59817f5..44aec56 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp | |||
| @@ -31,6 +31,13 @@ static const int BTF_DESC_KIND = 38501; | |||
| 31 | // the abandoned test .btf descriptors lingering on the relays become invisible. | 31 | // the abandoned test .btf descriptors lingering on the relays become invisible. |
| 32 | static const char* BTF_DESC_DTAG = "btf-descriptor-2"; | 32 | static const char* BTF_DESC_DTAG = "btf-descriptor-2"; |
| 33 | 33 | ||
| 34 | // Relay auto-discovery: a relay operator announces their relay as an addressable | ||
| 35 | // event (kind 38502) so every node learns it automatically -- no manual | ||
| 36 | // seed-list edit, no maintainer approval. Volunteer relays just work. | ||
| 37 | static const int BTF_RELAY_KIND = 38502; | ||
| 38 | static const char* BTF_RELAY_DTAG = "btf-relay-2"; | ||
| 39 | static const size_t BTF_MAX_DISCOVERED_RELAYS = 50; // anti-spam cap | ||
| 40 | |||
| 34 | // Nostr network/scope for Bitflash. Events are replaceable (NIP-78). | 41 | // Nostr network/scope for Bitflash. Events are replaceable (NIP-78). |
| 35 | static const int NOSTR_KIND = 30078; | 42 | static const int NOSTR_KIND = 30078; |
| 36 | static const char* NOSTR_DTAG = "bitflash-mainnet-2"; | 43 | static const char* NOSTR_DTAG = "bitflash-mainnet-2"; |
| @@ -82,6 +89,48 @@ void BtfSetActiveRelay(const string& relay) | |||
| 82 | strBtfActiveRelay = relay; | 89 | strBtfActiveRelay = relay; |
| 83 | } | 90 | } |
| 84 | 91 | ||
| 92 | // A relay this node advertises on Nostr for others to discover (/announcerelay). | ||
| 93 | string strBtfAnnounceRelay; | ||
| 94 | // Relays learned from other operators' Nostr announcements. | ||
| 95 | static set<string> g_discoveredRelays; | ||
| 96 | static CCriticalSection cs_discoveredRelays; | ||
| 97 | |||
| 98 | // Basic sanity: "host:port" with a numeric 1..65535 port and a plausible host. | ||
| 99 | static bool LooksLikeRelay(const string& r) | ||
| 100 | { | ||
| 101 | size_t colon = r.rfind(':'); | ||
| 102 | if (colon == string::npos || colon == 0 || colon + 1 >= r.size()) | ||
| 103 | return false; | ||
| 104 | if (r.size() > 100) | ||
| 105 | return false; | ||
| 106 | for (size_t i = colon + 1; i < r.size(); i++) | ||
| 107 | if (!isdigit((unsigned char)r[i])) | ||
| 108 | return false; | ||
| 109 | int port = atoi(r.c_str() + colon + 1); | ||
| 110 | if (port <= 0 || port > 65535) | ||
| 111 | return false; | ||
| 112 | // host: allow letters, digits, dot, dash, colon (IPv6 not supported yet) | ||
| 113 | for (size_t i = 0; i < colon; i++) | ||
| 114 | { | ||
| 115 | char c = r[i]; | ||
| 116 | if (!(isalnum((unsigned char)c) || c == '.' || c == '-')) | ||
| 117 | return false; | ||
| 118 | } | ||
| 119 | return true; | ||
| 120 | } | ||
| 121 | |||
| 122 | // Every relay to try: curated seeds first (reliable bootstrap), then the ones | ||
| 123 | // discovered from Nostr announcements (volunteer relays). | ||
| 124 | vector<string> BtfAllRelays() | ||
| 125 | { | ||
| 126 | vector<string> all = vBtfMeetingRelays; | ||
| 127 | CRITICAL_BLOCK(cs_discoveredRelays) | ||
| 128 | foreach(const string& r, g_discoveredRelays) | ||
| 129 | if (find(all.begin(), all.end(), r) == all.end()) | ||
| 130 | all.push_back(r); | ||
| 131 | return all; | ||
| 132 | } | ||
| 133 | |||
| 85 | 134 | ||
| 86 | // | 135 | // |
| 87 | // Utilities | 136 | // Utilities |
| @@ -560,6 +609,21 @@ static void PublishDescriptor(CWebSocket& ws, CNostrKey& key) | |||
| 560 | } | 609 | } |
| 561 | } | 610 | } |
| 562 | 611 | ||
| 612 | // Announce our relay (from /announcerelay=host:port) so every node discovers it | ||
| 613 | // automatically -- volunteer relays join the network with no maintainer step. | ||
| 614 | static void PublishRelayAnnouncement(CWebSocket& ws, CNostrKey& key) | ||
| 615 | { | ||
| 616 | if (strBtfAnnounceRelay.empty()) | ||
| 617 | return; | ||
| 618 | json tags = json::array({ json::array({ "d", BTF_RELAY_DTAG }) }); | ||
| 619 | json ev; | ||
| 620 | if (BuildSignedEvent(key, BTF_RELAY_KIND, tags, strBtfAnnounceRelay, ev)) | ||
| 621 | { | ||
| 622 | json pub = json::array({ "EVENT", ev }); | ||
| 623 | ws.SendText(pub.dump()); | ||
| 624 | } | ||
| 625 | } | ||
| 626 | |||
| 563 | // Resolve a `.btf` address via a relay: fetch the descriptor published by that | 627 | // Resolve a `.btf` address via a relay: fetch the descriptor published by that |
| 564 | // address's key and verify it self-certifies under the decoded pubkey. | 628 | // address's key and verify it self-certifies under the decoded pubkey. |
| 565 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 629 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, |
| @@ -654,6 +718,9 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 654 | // Publish our self-certifying .btf descriptor (rendezvous discovery) | 718 | // Publish our self-certifying .btf descriptor (rendezvous discovery) |
| 655 | PublishDescriptor(ws, key); | 719 | PublishDescriptor(ws, key); |
| 656 | 720 | ||
| 721 | // If we run a relay, announce it so others discover it automatically | ||
| 722 | PublishRelayAnnouncement(ws, key); | ||
| 723 | |||
| 657 | // Subscribe to receive the announcements of other nodes | 724 | // Subscribe to receive the announcements of other nodes |
| 658 | json filter = json::object(); | 725 | json filter = json::object(); |
| 659 | filter["kinds"] = json::array({ NOSTR_KIND }); | 726 | filter["kinds"] = json::array({ NOSTR_KIND }); |
| @@ -736,6 +803,49 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay) | |||
| 736 | } | 803 | } |
| 737 | } | 804 | } |
| 738 | } | 805 | } |
| 806 | |||
| 807 | // Discover volunteer relays announced by other operators (kind 38502). Nodes | ||
| 808 | // add these to their failover list automatically, so anyone can strengthen | ||
| 809 | // the network without a maintainer editing the seed list. Bad/dead relays | ||
| 810 | // just fail to pair and get skipped -- and a relay is trustless anyway (it | ||
| 811 | // only forwards echan ciphertext and can't read or MITM the traffic). | ||
| 812 | ws.SendText(json::array({ "CLOSE", "btf-disc" }).dump()); | ||
| 813 | { | ||
| 814 | json rfilter = json::object(); | ||
| 815 | rfilter["kinds"] = json::array({ BTF_RELAY_KIND }); | ||
| 816 | rfilter["#d"] = json::array({ BTF_RELAY_DTAG }); | ||
| 817 | rfilter["limit"] = 200; | ||
| 818 | json rreq = json::array({ "REQ", "btf-relays", rfilter }); | ||
| 819 | if (ws.SendText(rreq.dump())) | ||
| 820 | { | ||
| 821 | for (;;) | ||
| 822 | { | ||
| 823 | string msg; | ||
| 824 | if (!ws.RecvText(msg)) | ||
| 825 | break; | ||
| 826 | json j; | ||
| 827 | try { j = json::parse(msg); } catch (...) { continue; } | ||
| 828 | if (!j.is_array() || j.empty()) continue; | ||
| 829 | string type = j[0].get<string>(); | ||
| 830 | if (type == "EVENT" && j.size() >= 3) | ||
| 831 | { | ||
| 832 | const json& ev = j[2]; | ||
| 833 | if (!ev.contains("content") || !ev.contains("kind")) continue; | ||
| 834 | if (ev["kind"].get<int>() != BTF_RELAY_KIND) continue; | ||
| 835 | string r = ev["content"].get<string>(); | ||
| 836 | if (!LooksLikeRelay(r)) continue; | ||
| 837 | bool fNew = false; | ||
| 838 | CRITICAL_BLOCK(cs_discoveredRelays) | ||
| 839 | if (g_discoveredRelays.size() < BTF_MAX_DISCOVERED_RELAYS) | ||
| 840 | fNew = g_discoveredRelays.insert(r).second; | ||
| 841 | if (fNew) | ||
| 842 | printf("Nostr: discovered relay %s\n", r.c_str()); | ||
| 843 | } | ||
| 844 | else if (type == "EOSE") | ||
| 845 | break; | ||
| 846 | } | ||
| 847 | } | ||
| 848 | } | ||
| 739 | return true; | 849 | return true; |
| 740 | } | 850 | } |
| 741 | 851 | ||