diff options
| author | Bitflash <btf01@proton.me> | 2026-07-29 16:01:43 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-29 16:01:43 -0300 |
| commit | ab24958f1470cb62a4637126345fdf14c2ec0bd4 (patch) | |
| tree | de3c1ca3888a531fef6240b14eca79655e226907 /src/net.cpp | |
| parent | f9d839e4e53ba5e4ca4829e52ef9470fd116d784 (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.cpp | 93 |
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 | // | ||
| 435 | struct BtfSeed | ||
| 436 | { | ||
| 437 | const char* btfAddr; | ||
| 438 | const char* encHex; // 64 hex chars, the peer's x25519 public key | ||
| 439 | }; | ||
| 440 | |||
| 441 | static 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 | }; | ||
| 450 | static const size_t nBtfSeeds = ARRAYLEN(pszBtfSeeds); | ||
| 451 | |||
| 452 | // Extra seeds from the command line (/btfseed=ADDRESS:ENCHEX, repeatable). | ||
| 453 | vector<pair<string, string> > vBtfExtraSeeds; | ||
| 454 | |||
| 455 | static 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. |
| 418 | void ThreadReconnectCachedBtfPeers(void* parg) | 505 | void 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", |