diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/headless.cpp | 3 | ||||
| -rw-r--r-- | src/net.cpp | 15 | ||||
| -rw-r--r-- | src/nostr.cpp | 110 | ||||
| -rw-r--r-- | src/nostr.h | 6 | ||||
| -rw-r--r-- | src/ui.cpp | 4 |
5 files changed, 135 insertions, 3 deletions
diff --git a/src/headless.cpp b/src/headless.cpp index 646476f..cdb8ac9 100644 --- a/src/headless.cpp +++ b/src/headless.cpp | |||
| @@ -80,6 +80,7 @@ int main(int argc, char* argv[]) | |||
| 80 | " -datadir=<dir> data directory (default ~/.bitflash)\n" | 80 | " -datadir=<dir> data directory (default ~/.bitflash)\n" |
| 81 | " -port=<n> P2P listen port (default 8433)\n" | 81 | " -port=<n> P2P listen port (default 8433)\n" |
| 82 | " -rvrelay=host:port rendezvous meeting relay (has a seed default)\n" | 82 | " -rvrelay=host:port rendezvous meeting relay (has a seed default)\n" |
| 83 | " -announcerelay=h:p announce a relay you run so nodes discover it\n" | ||
| 83 | " -connectbtf=<a>.btf keep a connection to a specific .btf peer\n" | 84 | " -connectbtf=<a>.btf keep a connection to a specific .btf peer\n" |
| 84 | " -solomine mine without waiting for a peer (testing)\n" | 85 | " -solomine mine without waiting for a peer (testing)\n" |
| 85 | " -debug verbose logging\n", | 86 | " -debug verbose logging\n", |
| @@ -96,6 +97,8 @@ int main(int argc, char* argv[]) | |||
| 96 | addrProxy = CAddress(mapArgs["/proxy"].c_str()); | 97 | addrProxy = CAddress(mapArgs["/proxy"].c_str()); |
| 97 | if (mapArgs.count("/rvrelay")) | 98 | if (mapArgs.count("/rvrelay")) |
| 98 | vBtfMeetingRelays = { mapArgs["/rvrelay"] }; | 99 | vBtfMeetingRelays = { mapArgs["/rvrelay"] }; |
| 100 | if (mapArgs.count("/announcerelay")) | ||
| 101 | strBtfAnnounceRelay = mapArgs["/announcerelay"]; | ||
| 99 | if (mapArgs.count("/connectbtf")) | 102 | if (mapArgs.count("/connectbtf")) |
| 100 | strBtfConnect = mapArgs["/connectbtf"]; | 103 | strBtfConnect = mapArgs["/connectbtf"]; |
| 101 | if (mapArgs.count("/debug")) | 104 | if (mapArgs.count("/debug")) |
diff --git a/src/net.cpp b/src/net.cpp index 659de05..31afaf5 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -442,18 +442,27 @@ void ThreadBtfAccept(void* parg) | |||
| 442 | Sleep(5000); | 442 | Sleep(5000); |
| 443 | } | 443 | } |
| 444 | size_t iRelay = 0; | 444 | size_t iRelay = 0; |
| 445 | bool fPicked = false; | ||
| 445 | loop | 446 | loop |
| 446 | { | 447 | { |
| 447 | if (fShutdown) | 448 | if (fShutdown) |
| 448 | return; | 449 | return; |
| 449 | if (vBtfMeetingRelays.empty()) | 450 | // Curated seeds + relays discovered from Nostr announcements. |
| 451 | vector<string> relays = BtfAllRelays(); | ||
| 452 | if (relays.empty()) | ||
| 450 | { | 453 | { |
| 451 | Sleep(10000); // no meeting relay configured | 454 | Sleep(10000); // no meeting relay configured |
| 452 | continue; | 455 | continue; |
| 453 | } | 456 | } |
| 457 | // Random start spreads nodes across all relays (incl. volunteer ones). | ||
| 458 | if (!fPicked) | ||
| 459 | { | ||
| 460 | iRelay = (size_t)GetRand(relays.size()); | ||
| 461 | fPicked = true; | ||
| 462 | } | ||
| 454 | // Stick to the current relay while it works; on failure, fail over to | 463 | // Stick to the current relay while it works; on failure, fail over to |
| 455 | // the next seed so a DDoS'd/blocked relay IP can't keep us offline. | 464 | // the next one so a DDoS'd/blocked relay IP can't keep us offline. |
| 456 | string strMeeting = vBtfMeetingRelays[iRelay % vBtfMeetingRelays.size()]; | 465 | string strMeeting = relays[iRelay % relays.size()]; |
| 457 | size_t colon = strMeeting.rfind(':'); | 466 | size_t colon = strMeeting.rfind(':'); |
| 458 | if (colon == string::npos) | 467 | if (colon == string::npos) |
| 459 | { | 468 | { |
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 | ||
diff --git a/src/nostr.h b/src/nostr.h index 41e59df..b878db9 100644 --- a/src/nostr.h +++ b/src/nostr.h | |||
| @@ -32,6 +32,12 @@ std::string BtfActiveRelay(); | |||
| 32 | // Record the relay this node just registered its service at (thread-safe). | 32 | // Record the relay this node just registered its service at (thread-safe). |
| 33 | void BtfSetActiveRelay(const std::string& relay); | 33 | void BtfSetActiveRelay(const std::string& relay); |
| 34 | 34 | ||
| 35 | // If set (via /announcerelay=host:port), this node announces that relay on | ||
| 36 | // Nostr so other nodes discover it automatically -- no manual seed-list edit. | ||
| 37 | extern std::string strBtfAnnounceRelay; | ||
| 38 | // Every relay to try: curated seeds + relays discovered from Nostr announcements. | ||
| 39 | std::vector<std::string> BtfAllRelays(); | ||
| 40 | |||
| 35 | // Copy this node's .btf identity (lazily loading or generating it): the x-only | 41 | // Copy this node's .btf identity (lazily loading or generating it): the x-only |
| 36 | // pubkey (= the .btf address) and the x25519 secret for the end-to-end channel. | 42 | // pubkey (= the .btf address) and the x25519 secret for the end-to-end channel. |
| 37 | bool BtfGetIdentity(unsigned char pubkey[32], unsigned char enc_sk[32]); | 43 | bool BtfGetIdentity(unsigned char pubkey[32], unsigned char enc_sk[32]); |
| @@ -2974,6 +2974,10 @@ bool CMyApp::OnInit2() | |||
| 2974 | if (mapArgs.count("/rvrelay")) | 2974 | if (mapArgs.count("/rvrelay")) |
| 2975 | vBtfMeetingRelays = { mapArgs["/rvrelay"] }; | 2975 | vBtfMeetingRelays = { mapArgs["/rvrelay"] }; |
| 2976 | 2976 | ||
| 2977 | // Announce a relay you run so other nodes discover it automatically. | ||
| 2978 | if (mapArgs.count("/announcerelay")) | ||
| 2979 | strBtfAnnounceRelay = mapArgs["/announcerelay"]; | ||
| 2980 | |||
| 2977 | // Keep an anonymous outbound connection to a specific .btf peer. | 2981 | // Keep an anonymous outbound connection to a specific .btf peer. |
| 2978 | if (mapArgs.count("/connectbtf")) | 2982 | if (mapArgs.count("/connectbtf")) |
| 2979 | strBtfConnect = mapArgs["/connectbtf"]; | 2983 | strBtfConnect = mapArgs["/connectbtf"]; |