From ab24958f1470cb62a4637126345fdf14c2ec0bd4 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Wed, 29 Jul 2026 16:01:43 -0300 Subject: net: compiled-in bootstrap seeds, so a first run does not need a relay (#36) * net: compiled-in bootstrap seeds, so a first run does not need a relay A node with no cache has only the Nostr relays. If they are down, blocked or slow, a first run has no way into the network at all -- and that is the one moment a user has no patience for. Seeds are the floor under that. A seed entry is an address and an encryption key. Deliberately no meeting node: the rendezvous relay pairs on the service's public key, and a `.btf` address is that key, so a client can find a seed by trying the known relays in turn. Recording which relay a seed was on would rot the moment it failed over, which is exactly the failure this project spent weeks chasing -- advertising a rendezvous somebody is not registered at. Seeds carry no authority. They serve the same signed, self-certifying descriptors as any peer, so a hostile seed can stall a bootstrap but cannot forge a peer or feed a false chain. The compiled list is empty. Which nodes become permanent public bootstrap targets is a decision for whoever runs the network, not a default, and it is close to irreversible once binaries are distributed. Empty is valid: discovery falls back to Nostr exactly as before. /btfseed=ADDRESS:ENCHEX (repeatable) appends at runtime, for testing and for private networks. Tested against the live network, clean datadir, no cache, three seeds carrying no meeting node: btfpeers: no cache yet, falling back to seeds btfseed: trying 3 seed(s) across 3 relay(s) btfseed: reached q5jxyzn...btf via 90.156.222.107:8434 btfseed: 3 seed(s) answered * net: add the Almaty bootstrap node to the seed list Dedicated node, no wallet balance, no mining, running beside the rendezvous relay on the same host. It exists only to answer a first dial. Fresh datadir, no flags, compiled list only: btfpeers: no cache yet, falling back to seeds btfseed: trying 1 seed(s) across 3 relay(s) btfseed: reached ygffz37...btf via 90.156.222.107:8434 btfseed: 1 seed(s) answered btfpeers: kept 2 of 2 descriptor(s) 5.2 seconds from nothing to a peer, against 36s for a cold start that had to go through the relays. The seed then handed over two more peers by exchange, which the cache keeps for next time. --- src/net.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index 1d552cf..08a3348 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -414,6 +414,93 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, } } +// +// Compiled-in bootstrap seeds. +// +// A node with no cache has nothing but the Nostr relays, and if those are down, +// blocked or simply slow, a first run has no way into the network at all. Seeds +// are the floor under that: a handful of long-lived peers baked into the binary. +// +// A seed entry is deliberately just an address and an encryption key -- no +// meeting node. The rendezvous relay pairs on the service's public key, and a +// `.btf` address *is* that key, so a client can find a seed by trying the known +// relays in turn. Recording which relay a seed was on would rot the moment it +// failed over, which is the exact failure this project spent a long time +// chasing: advertising a rendezvous somebody is not registered at. +// +// Seeds are ordinary nodes with no special authority. They hand out signed, +// self-certifying descriptors like any peer, so a hostile seed can stall a +// bootstrap but cannot forge a peer or feed a false chain. +// +struct BtfSeed +{ + const char* btfAddr; + const char* encHex; // 64 hex chars, the peer's x25519 public key +}; + +static const BtfSeed pszBtfSeeds[] = +{ + // Dedicated bootstrap node, Almaty. Runs beside a rendezvous relay on the + // same host, holds no wallet balance and does not mine -- it exists only to + // answer a first dial. It is trusted for nothing: it serves the same signed + // descriptors any peer does. + { "ygffz37jczlmrkzicxok6chobauyratdexc7hfgwzjdabzvb2nkngqy.btf", + "de8284b9d4effa2132e7981b566c1a297c39c1857d9c128e8ae093ef511d3200" }, +}; +static const size_t nBtfSeeds = ARRAYLEN(pszBtfSeeds); + +// Extra seeds from the command line (/btfseed=ADDRESS:ENCHEX, repeatable). +vector > vBtfExtraSeeds; + +static int TryBtfSeeds() +{ + vector > seeds; + for (size_t i = 0; i < nBtfSeeds; i++) + seeds.push_back(make_pair(string(pszBtfSeeds[i].btfAddr), + string(pszBtfSeeds[i].encHex))); + foreach(const PAIRTYPE(string, string)& s, vBtfExtraSeeds) + seeds.push_back(s); + + if (seeds.empty()) + return 0; + + vector relays = BtfAllRelays(); + if (relays.empty()) + return 0; + + LogPrint("net", "btfseed: trying %zu seed(s) across %zu relay(s)\n", + seeds.size(), relays.size()); + + int nConnected = 0; + foreach(const PAIRTYPE(string, string)& s, seeds) + { + if (fShutdown) return nConnected; + unsigned char enc[32]; + if (!HexToBytes(s.second, enc, 32)) + { + LogPrint("net", "btfseed: %s has a malformed encryption key, skipped\n", + s.first.c_str()); + continue; + } + // We do not know which relay this seed is registered at, so walk them. + foreach(const string& strRelay, relays) + { + if (fShutdown) return nConnected; + if (ConnectNodeBtfResolved(s.first, strRelay, enc)) + { + LogPrint("net", "btfseed: reached %s via %s\n", + s.first.c_str(), strRelay.c_str()); + nConnected++; + break; // found it; no need to try this seed's other relays + } + } + if (nConnected >= 4) + break; // enough of a foothold; the rest comes from discovery + } + LogPrint("net", "btfseed: %d seed(s) answered\n", nConnected); + return nConnected; +} + // Dial everything we reached last time, before the relays have said anything. void ThreadReconnectCachedBtfPeers(void* parg) { @@ -421,7 +508,11 @@ void ThreadReconnectCachedBtfPeers(void* parg) LoadCachedBtfPeers(peers); if (peers.empty()) { - LogPrint("net", "btfpeers: no cache yet, waiting on relay discovery\n"); + // First run, or the cache aged out. Seeds are the only way in that does + // not depend on a relay answering. + LogPrint("net", "btfpeers: no cache yet, falling back to seeds\n"); + if (TryBtfSeeds() == 0) + LogPrint("net", "btfpeers: no seed answered, waiting on relay discovery\n"); return; } LogPrint("net", "btfpeers: trying %zu remembered peer(s) before discovery\n", -- cgit v1.2.3