upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/net.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-29 16:01:43 -0300
committerGitHub <noreply@github.com>2026-07-29 16:01:43 -0300
commitab24958f1470cb62a4637126345fdf14c2ec0bd4 (patch)
treede3c1ca3888a531fef6240b14eca79655e226907 /src/net.cpp
parentf9d839e4e53ba5e4ca4829e52ef9470fd116d784 (diff)
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.
Diffstat (limited to 'src/net.cpp')
-rw-r--r--src/net.cpp93
1 files changed, 92 insertions, 1 deletions
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,
414 } 414 }
415} 415}
416 416
417//
418// Compiled-in bootstrap seeds.
419//
420// A node with no cache has nothing but the Nostr relays, and if those are down,
421// blocked or simply slow, a first run has no way into the network at all. Seeds
422// are the floor under that: a handful of long-lived peers baked into the binary.
423//
424// A seed entry is deliberately just an address and an encryption key -- no
425// meeting node. The rendezvous relay pairs on the service's public key, and a
426// `.btf` address *is* that key, so a client can find a seed by trying the known
427// relays in turn. Recording which relay a seed was on would rot the moment it
428// failed over, which is the exact failure this project spent a long time
429// chasing: advertising a rendezvous somebody is not registered at.
430//
431// Seeds are ordinary nodes with no special authority. They hand out signed,
432// self-certifying descriptors like any peer, so a hostile seed can stall a
433// bootstrap but cannot forge a peer or feed a false chain.
434//
435struct BtfSeed
436{
437 const char* btfAddr;
438 const char* encHex; // 64 hex chars, the peer's x25519 public key
439};
440
441static const BtfSeed pszBtfSeeds[] =
442{
443 // Dedicated bootstrap node, Almaty. Runs beside a rendezvous relay on the
444 // same host, holds no wallet balance and does not mine -- it exists only to
445 // answer a first dial. It is trusted for nothing: it serves the same signed
446 // descriptors any peer does.
447 { "ygffz37jczlmrkzicxok6chobauyratdexc7hfgwzjdabzvb2nkngqy.btf",
448 "de8284b9d4effa2132e7981b566c1a297c39c1857d9c128e8ae093ef511d3200" },
449};
450static const size_t nBtfSeeds = ARRAYLEN(pszBtfSeeds);
451
452// Extra seeds from the command line (/btfseed=ADDRESS:ENCHEX, repeatable).
453vector<pair<string, string> > vBtfExtraSeeds;
454
455static int TryBtfSeeds()
456{
457 vector<pair<string, string> > seeds;
458 for (size_t i = 0; i < nBtfSeeds; i++)
459 seeds.push_back(make_pair(string(pszBtfSeeds[i].btfAddr),
460 string(pszBtfSeeds[i].encHex)));
461 foreach(const PAIRTYPE(string, string)& s, vBtfExtraSeeds)
462 seeds.push_back(s);
463
464 if (seeds.empty())
465 return 0;
466
467 vector<string> relays = BtfAllRelays();
468 if (relays.empty())
469 return 0;
470
471 LogPrint("net", "btfseed: trying %zu seed(s) across %zu relay(s)\n",
472 seeds.size(), relays.size());
473
474 int nConnected = 0;
475 foreach(const PAIRTYPE(string, string)& s, seeds)
476 {
477 if (fShutdown) return nConnected;
478 unsigned char enc[32];
479 if (!HexToBytes(s.second, enc, 32))
480 {
481 LogPrint("net", "btfseed: %s has a malformed encryption key, skipped\n",
482 s.first.c_str());
483 continue;
484 }
485 // We do not know which relay this seed is registered at, so walk them.
486 foreach(const string& strRelay, relays)
487 {
488 if (fShutdown) return nConnected;
489 if (ConnectNodeBtfResolved(s.first, strRelay, enc))
490 {
491 LogPrint("net", "btfseed: reached %s via %s\n",
492 s.first.c_str(), strRelay.c_str());
493 nConnected++;
494 break; // found it; no need to try this seed's other relays
495 }
496 }
497 if (nConnected >= 4)
498 break; // enough of a foothold; the rest comes from discovery
499 }
500 LogPrint("net", "btfseed: %d seed(s) answered\n", nConnected);
501 return nConnected;
502}
503
417// Dial everything we reached last time, before the relays have said anything. 504// Dial everything we reached last time, before the relays have said anything.
418void ThreadReconnectCachedBtfPeers(void* parg) 505void ThreadReconnectCachedBtfPeers(void* parg)
419{ 506{
@@ -421,7 +508,11 @@ void ThreadReconnectCachedBtfPeers(void* parg)
421 LoadCachedBtfPeers(peers); 508 LoadCachedBtfPeers(peers);
422 if (peers.empty()) 509 if (peers.empty())
423 { 510 {
424 LogPrint("net", "btfpeers: no cache yet, waiting on relay discovery\n"); 511 // First run, or the cache aged out. Seeds are the only way in that does
512 // not depend on a relay answering.
513 LogPrint("net", "btfpeers: no cache yet, falling back to seeds\n");
514 if (TryBtfSeeds() == 0)
515 LogPrint("net", "btfpeers: no seed answered, waiting on relay discovery\n");
425 return; 516 return;
426 } 517 }
427 LogPrint("net", "btfpeers: trying %zu remembered peer(s) before discovery\n", 518 LogPrint("net", "btfpeers: trying %zu remembered peer(s) before discovery\n",