diff options
| author | Bitflash <btf01@proton.me> | 2026-07-29 11:52:12 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-29 11:52:12 -0300 |
| commit | 96a8e42bbc313f96d54377ae5886da84298e61a3 (patch) | |
| tree | 36bcd61016ee03c90140b407d4e5863a282e78cf | |
| parent | 7f6fdbab6c90360ab3b2d391a7b5aedc6f9721ac (diff) | |
Remember .btf peers that answered, and dial them first on start (#25)
* net: remember .btf peers that answered, and dial them first on start
Discovery depends entirely on the Nostr relays. A node that has been up
for hours knows exactly which peers respond, and throws all of it away on
exit; the next start reaches for the relays again and walks the same
descriptor list, of which roughly 96% is dead. That is what makes a
restart take minutes before the first tunnel opens -- measured today at
about seven.
Every peer that completes a tunnel is now written to btfpeers.json with
its meeting node and encryption key, which is everything needed to redial
it. On start a thread tries those before the relays have answered
anything, stopping once eight are connected. Entries older than a week
are dropped, and the file holds at most fifty.
Cache only: nothing here changes how a first-ever run bootstraps, and a
missing or corrupt file just falls through to relay discovery.
* net: exchange signed .btf descriptors between peers
Discovery has depended entirely on the Nostr relays. Two connected nodes never
told each other who else existed, so a relay outage left a running network
unable to grow, and a node that found one stale peer had no way to learn there
was anything better. The old `addr` gossip went out with the IRC seed.
Peers now trade signed descriptors over a new "btfpeers" message: our own
first, then peers that actually answered us. The receiver checks each Schnorr
signature against the key the `.btf` address decodes to, so the sender is
trusted for nothing -- forging an entry needs somebody else's secret key. That
leaves flooding with self-generated keys, which the per-message caps and the
"peers that answered" ordering are there to blunt.
Unknown commands are ignored (main.cpp), so nodes without this just drop the
message; nothing here is a protocol break.
The cache from the peer-cache change now keeps each peer's descriptor verbatim
alongside the parsed fields, because that signature is what makes a peer
relayable. Entries without one -- resolved through Nostr, or written before
this -- stay dialable, just not passed on. A dial never clears a descriptor
already on file.
* main_gui: implement /port, which net.cpp already documented
net.cpp has said nListenPort is "tunable via /port" since it was written, but
nothing ever parsed the option. Two nodes could not share a machine, which is
exactly what testing peer exchange needs. Sets addrLocalHost.port too, so we
do not advertise a port we never bound.
| -rw-r--r-- | src/main.cpp | 31 | ||||
| -rw-r--r-- | src/main_gui.cpp | 18 | ||||
| -rw-r--r-- | src/net.cpp | 280 | ||||
| -rw-r--r-- | src/net.h | 19 | ||||
| -rw-r--r-- | src/nostr.cpp | 30 | ||||
| -rw-r--r-- | src/nostr.h | 8 |
6 files changed, 386 insertions, 0 deletions
diff --git a/src/main.cpp b/src/main.cpp index a75bf54..bb317d2 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -1877,6 +1877,14 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 1877 | pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0)); | 1877 | pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), uint256(0)); |
| 1878 | } | 1878 | } |
| 1879 | 1879 | ||
| 1880 | // Introduce ourselves and hand over the .btf peers that answered us. | ||
| 1881 | // Everything in here is self-certifying, so this is safe to send to a | ||
| 1882 | // node we know nothing about, and safe for it to act on. | ||
| 1883 | vector<string> vPexOut; | ||
| 1884 | BtfPexCollect(vPexOut); | ||
| 1885 | if (!vPexOut.empty()) | ||
| 1886 | pfrom->PushMessage("btfpeers", vPexOut); | ||
| 1887 | |||
| 1880 | if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str()); | 1888 | if (LogAcceptsCategory("net")) printf("version addrMe = %s\n", addrMe.ToString().c_str()); |
| 1881 | } | 1889 | } |
| 1882 | 1890 | ||
| @@ -1888,6 +1896,29 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 1888 | } | 1896 | } |
| 1889 | 1897 | ||
| 1890 | 1898 | ||
| 1899 | else if (strCommand == "btfpeers") | ||
| 1900 | { | ||
| 1901 | // Peer exchange. Nothing here is taken on trust: BtfPexAccept checks | ||
| 1902 | // every descriptor's signature against the key its .btf address | ||
| 1903 | // decodes to, and drops the rest. | ||
| 1904 | int64 nNow = GetTime(); | ||
| 1905 | if (nNow - pfrom->nLastPexRecv < PEX_MIN_INTERVAL) | ||
| 1906 | { | ||
| 1907 | if (LogAcceptsCategory("net")) | ||
| 1908 | printf("btfpeers: ignoring, %s is sending them too fast\n", | ||
| 1909 | pfrom->addr.ToString().c_str()); | ||
| 1910 | return true; | ||
| 1911 | } | ||
| 1912 | pfrom->nLastPexRecv = nNow; | ||
| 1913 | |||
| 1914 | vector<string> vDesc; | ||
| 1915 | vRecv >> vDesc; | ||
| 1916 | int nKept = BtfPexAccept(vDesc); | ||
| 1917 | LogPrint("net", "btfpeers: kept %d of %zu descriptor(s) from %s\n", | ||
| 1918 | nKept, vDesc.size(), pfrom->addr.ToString().c_str()); | ||
| 1919 | } | ||
| 1920 | |||
| 1921 | |||
| 1891 | else if (strCommand == "inv") | 1922 | else if (strCommand == "inv") |
| 1892 | { | 1923 | { |
| 1893 | vector<CInv> vInv; | 1924 | vector<CInv> vInv; |
diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 842b0d5..edc772d 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp | |||
| @@ -58,6 +58,7 @@ static void PrintUsage() | |||
| 58 | printf(" /connectbtf=PEER_BTF_ADDRESS\n"); | 58 | printf(" /connectbtf=PEER_BTF_ADDRESS\n"); |
| 59 | printf(" /rvrelay=HOST:PORT\n"); | 59 | printf(" /rvrelay=HOST:PORT\n"); |
| 60 | printf(" /announcerelay=HOST:PORT\n"); | 60 | printf(" /announcerelay=HOST:PORT\n"); |
| 61 | printf(" /port=N (P2P listen port, default 8433)\n"); | ||
| 61 | printf("\n"); | 62 | printf("\n"); |
| 62 | printf("Each option also accepts '-' instead of '/'.\n"); | 63 | printf("Each option also accepts '-' instead of '/'.\n"); |
| 63 | } | 64 | } |
| @@ -113,6 +114,23 @@ static void ParseStartupArguments(int argc, char* argv[]) | |||
| 113 | string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); | 114 | string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); |
| 114 | if (!announceRelay.empty()) | 115 | if (!announceRelay.empty()) |
| 115 | strBtfAnnounceRelay = announceRelay; | 116 | strBtfAnnounceRelay = announceRelay; |
| 117 | |||
| 118 | // net.cpp has claimed nListenPort was "tunable via /port" since it was | ||
| 119 | // written, but nothing ever read the option. Without it a second node | ||
| 120 | // cannot start on a machine that already runs one -- which is exactly what | ||
| 121 | // testing peer exchange needs. | ||
| 122 | string strPort = argval2(argc, argv, "/port", "-port"); | ||
| 123 | if (!strPort.empty()) | ||
| 124 | { | ||
| 125 | int nPort = atoi(strPort.c_str()); | ||
| 126 | if (nPort <= 0 || nPort > 65535) | ||
| 127 | fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str()); | ||
| 128 | else | ||
| 129 | { | ||
| 130 | nListenPort = htons((unsigned short)nPort); | ||
| 131 | addrLocalHost.port = nListenPort; // or we advertise a port we never bound | ||
| 132 | } | ||
| 133 | } | ||
| 116 | } | 134 | } |
| 117 | 135 | ||
| 118 | int main(int argc, char* argv[]) | 136 | int main(int argc, char* argv[]) |
diff --git a/src/net.cpp b/src/net.cpp index ab07f94..dfeb4a2 100644 --- a/src/net.cpp +++ b/src/net.cpp | |||
| @@ -2,6 +2,13 @@ | |||
| 2 | // Distributed under the MIT/X11 software license, see the accompanying | 2 | // Distributed under the MIT/X11 software license, see the accompanying |
| 3 | // file license.txt or http://www.opensource.org/licenses/mit-license.php. | 3 | // file license.txt or http://www.opensource.org/licenses/mit-license.php. |
| 4 | 4 | ||
| 5 | #pragma push_macro("snprintf") | ||
| 6 | #undef snprintf | ||
| 7 | #include <nlohmann/json.hpp> | ||
| 8 | #pragma pop_macro("snprintf") | ||
| 9 | #ifdef snprintf | ||
| 10 | #undef snprintf | ||
| 11 | #endif | ||
| 5 | #include "headers.h" | 12 | #include "headers.h" |
| 6 | #ifdef _WIN32 | 13 | #ifdef _WIN32 |
| 7 | #include <winsock2.h> | 14 | #include <winsock2.h> |
| @@ -10,6 +17,7 @@ | |||
| 10 | #include "btfaddr.h" | 17 | #include "btfaddr.h" |
| 11 | #include "btftunnel.h" | 18 | #include "btftunnel.h" |
| 12 | 19 | ||
| 20 | void ThreadReconnectCachedBtfPeers(void* parg); | ||
| 13 | void ThreadMessageHandler2(void* parg); | 21 | void ThreadMessageHandler2(void* parg); |
| 14 | void ThreadSocketHandler2(void* parg); | 22 | void ThreadSocketHandler2(void* parg); |
| 15 | 23 | ||
| @@ -259,6 +267,269 @@ static CAddress BtfMarkerAddr(const unsigned char b5[5]) | |||
| 259 | // ConnectNodeBtfResolved (which takes an already-resolved descriptor, so it | 267 | // ConnectNodeBtfResolved (which takes an already-resolved descriptor, so it |
| 260 | // makes no Nostr relay calls at all -- see nostr.cpp's BtfResolveMany for why | 268 | // makes no Nostr relay calls at all -- see nostr.cpp's BtfResolveMany for why |
| 261 | // that matters when dialing many candidates in parallel). | 269 | // that matters when dialing many candidates in parallel). |
| 270 | // | ||
| 271 | // Peers we have actually reached, remembered across restarts. | ||
| 272 | // | ||
| 273 | // Discovery is otherwise entirely dependent on the Nostr relays: a node that | ||
| 274 | // has been running for hours and knows exactly which peers answer throws all | ||
| 275 | // of it away on exit, and the next start walks the same ~96%-dead descriptor | ||
| 276 | // list again. Reconnecting from this file skips the relays completely -- | ||
| 277 | // address, meeting node and encryption key are all we need. | ||
| 278 | // | ||
| 279 | static const size_t MAX_CACHED_BTF_PEERS = 50; | ||
| 280 | static const int64 CACHED_BTF_PEER_TTL = 7 * 24 * 60 * 60; // a week | ||
| 281 | |||
| 282 | struct CachedBtfPeer | ||
| 283 | { | ||
| 284 | string btfAddr; | ||
| 285 | string meeting; | ||
| 286 | string encHex; | ||
| 287 | int64 lastSeen; | ||
| 288 | // The peer's own signed descriptor, as it announced itself. Kept verbatim | ||
| 289 | // because that signature is what lets us hand this peer on to somebody | ||
| 290 | // else: the receiver checks it against the key the address decodes to and | ||
| 291 | // never has to take our word for anything. Empty for entries learned | ||
| 292 | // before peer exchange existed, or resolved through Nostr -- those are | ||
| 293 | // still dialable, just not relayable. | ||
| 294 | string desc; | ||
| 295 | }; | ||
| 296 | |||
| 297 | static string BtfPeerCachePath() { return GetAppDir() + "/btfpeers.json"; } | ||
| 298 | |||
| 299 | static string BytesToHex(const unsigned char* p, size_t n) | ||
| 300 | { | ||
| 301 | static const char* h = "0123456789abcdef"; | ||
| 302 | string s; | ||
| 303 | s.reserve(n * 2); | ||
| 304 | for (size_t i = 0; i < n; i++) { s += h[p[i] >> 4]; s += h[p[i] & 0xf]; } | ||
| 305 | return s; | ||
| 306 | } | ||
| 307 | |||
| 308 | static bool HexToBytes(const string& s, unsigned char* out, size_t n) | ||
| 309 | { | ||
| 310 | if (s.size() != n * 2) return false; | ||
| 311 | for (size_t i = 0; i < n; i++) | ||
| 312 | { | ||
| 313 | unsigned int b; | ||
| 314 | if (sscanf(s.c_str() + i * 2, "%2x", &b) != 1) return false; | ||
| 315 | out[i] = (unsigned char)b; | ||
| 316 | } | ||
| 317 | return true; | ||
| 318 | } | ||
| 319 | |||
| 320 | void LoadCachedBtfPeers(vector<CachedBtfPeer>& out) | ||
| 321 | { | ||
| 322 | out.clear(); | ||
| 323 | FILE* f = fopen(BtfPeerCachePath().c_str(), "r"); | ||
| 324 | if (!f) return; | ||
| 325 | string body; | ||
| 326 | char buf[4096]; | ||
| 327 | size_t r; | ||
| 328 | while ((r = fread(buf, 1, sizeof(buf), f)) > 0) body.append(buf, r); | ||
| 329 | fclose(f); | ||
| 330 | try | ||
| 331 | { | ||
| 332 | nlohmann::json arr = nlohmann::json::parse(body); | ||
| 333 | if (!arr.is_array()) return; | ||
| 334 | int64 nNow = GetTime(); | ||
| 335 | for (const auto& o : arr) | ||
| 336 | { | ||
| 337 | if (!o.is_object()) continue; | ||
| 338 | if (!o.contains("btf") || !o["btf"].is_string()) continue; | ||
| 339 | if (!o.contains("meeting") || !o["meeting"].is_string()) continue; | ||
| 340 | if (!o.contains("enc") || !o["enc"].is_string()) continue; | ||
| 341 | CachedBtfPeer p; | ||
| 342 | p.btfAddr = o["btf"].get<string>(); | ||
| 343 | p.meeting = o["meeting"].get<string>(); | ||
| 344 | p.encHex = o["enc"].get<string>(); | ||
| 345 | p.lastSeen = o.value("seen", (int64)0); | ||
| 346 | if (o.contains("desc") && o["desc"].is_string()) | ||
| 347 | p.desc = o["desc"].get<string>(); | ||
| 348 | if (nNow - p.lastSeen > CACHED_BTF_PEER_TTL) continue; // long gone | ||
| 349 | out.push_back(p); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | catch (...) | ||
| 353 | { | ||
| 354 | LogPrint("net", "btfpeers: cache unreadable, starting empty\n"); | ||
| 355 | out.clear(); | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | static CCriticalSection cs_btfPeerCache; | ||
| 360 | |||
| 361 | // strDesc is the peer's own signed descriptor when we have it (it announced | ||
| 362 | // itself over peer exchange), "" when we only resolved it through Nostr. An | ||
| 363 | // empty one never clears a descriptor already on file: dialing a peer we first | ||
| 364 | // learned about by exchange must not cost us the ability to pass it on. | ||
| 365 | static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, | ||
| 366 | const unsigned char enc_pub[32], | ||
| 367 | const string& strDesc = string()) | ||
| 368 | { | ||
| 369 | CRITICAL_BLOCK(cs_btfPeerCache) | ||
| 370 | { | ||
| 371 | vector<CachedBtfPeer> peers; | ||
| 372 | LoadCachedBtfPeers(peers); | ||
| 373 | |||
| 374 | string strKeepDesc = strDesc; | ||
| 375 | for (size_t i = 0; i < peers.size(); i++) | ||
| 376 | if (peers[i].btfAddr == strBtfAddr) | ||
| 377 | { | ||
| 378 | if (strKeepDesc.empty()) | ||
| 379 | strKeepDesc = peers[i].desc; | ||
| 380 | peers.erase(peers.begin() + i); | ||
| 381 | break; | ||
| 382 | } | ||
| 383 | |||
| 384 | CachedBtfPeer p; | ||
| 385 | p.btfAddr = strBtfAddr; | ||
| 386 | p.meeting = strMeeting; | ||
| 387 | p.encHex = BytesToHex(enc_pub, 32); | ||
| 388 | p.lastSeen = GetTime(); | ||
| 389 | p.desc = strKeepDesc; | ||
| 390 | peers.insert(peers.begin(), p); // most recent first | ||
| 391 | |||
| 392 | if (peers.size() > MAX_CACHED_BTF_PEERS) | ||
| 393 | peers.resize(MAX_CACHED_BTF_PEERS); | ||
| 394 | |||
| 395 | nlohmann::json arr = nlohmann::json::array(); | ||
| 396 | foreach(const CachedBtfPeer& q, peers) | ||
| 397 | { | ||
| 398 | nlohmann::json o; | ||
| 399 | o["btf"] = q.btfAddr; | ||
| 400 | o["meeting"] = q.meeting; | ||
| 401 | o["enc"] = q.encHex; | ||
| 402 | o["seen"] = q.lastSeen; | ||
| 403 | if (!q.desc.empty()) | ||
| 404 | o["desc"] = q.desc; | ||
| 405 | arr.push_back(o); | ||
| 406 | } | ||
| 407 | FILE* f = fopen(BtfPeerCachePath().c_str(), "w"); | ||
| 408 | if (f) | ||
| 409 | { | ||
| 410 | string s = arr.dump(2); | ||
| 411 | fwrite(s.c_str(), 1, s.size(), f); | ||
| 412 | fclose(f); | ||
| 413 | } | ||
| 414 | } | ||
| 415 | } | ||
| 416 | |||
| 417 | // Dial everything we reached last time, before the relays have said anything. | ||
| 418 | void ThreadReconnectCachedBtfPeers(void* parg) | ||
| 419 | { | ||
| 420 | vector<CachedBtfPeer> peers; | ||
| 421 | LoadCachedBtfPeers(peers); | ||
| 422 | if (peers.empty()) | ||
| 423 | { | ||
| 424 | LogPrint("net", "btfpeers: no cache yet, waiting on relay discovery\n"); | ||
| 425 | return; | ||
| 426 | } | ||
| 427 | LogPrint("net", "btfpeers: trying %zu remembered peer(s) before discovery\n", | ||
| 428 | peers.size()); | ||
| 429 | |||
| 430 | int nConnected = 0; | ||
| 431 | foreach(const CachedBtfPeer& p, peers) | ||
| 432 | { | ||
| 433 | if (fShutdown) return; | ||
| 434 | unsigned char enc[32]; | ||
| 435 | if (!HexToBytes(p.encHex, enc, 32)) continue; | ||
| 436 | if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, enc)) | ||
| 437 | { | ||
| 438 | nConnected++; | ||
| 439 | if (nConnected >= 8) break; // enough to bootstrap; the rest can wait | ||
| 440 | } | ||
| 441 | } | ||
| 442 | LogPrint("net", "btfpeers: %d of %zu remembered peer(s) answered\n", | ||
| 443 | nConnected, peers.size()); | ||
| 444 | } | ||
| 445 | |||
| 446 | // | ||
| 447 | // .btf peer exchange | ||
| 448 | // | ||
| 449 | // Until now discovery depended entirely on the Nostr relays: two connected | ||
| 450 | // nodes never told each other who else existed, so a relay outage left a | ||
| 451 | // running network unable to grow, and a node that found one stale peer had no | ||
| 452 | // way to learn there was anything better. | ||
| 453 | // | ||
| 454 | // What travels here is each peer's own signed descriptor. The receiver checks | ||
| 455 | // the Schnorr signature against the key the `.btf` address decodes to, so the | ||
| 456 | // sender is trusted for nothing -- forging an entry would need somebody else's | ||
| 457 | // secret key. That leaves flooding: valid descriptors for keys the sender | ||
| 458 | // generated itself. The caps below bound one message, and the cache prefers | ||
| 459 | // peers that actually answered, so a flood costs a Sybil more than it costs us. | ||
| 460 | // | ||
| 461 | |||
| 462 | void BtfPexCollect(vector<string>& vDescOut) | ||
| 463 | { | ||
| 464 | vDescOut.clear(); | ||
| 465 | |||
| 466 | // Ours first -- the node on the other end may know nobody but us. | ||
| 467 | string strMine = BtfLocalDescriptor(); | ||
| 468 | if (!strMine.empty()) | ||
| 469 | vDescOut.push_back(strMine); | ||
| 470 | |||
| 471 | vector<CachedBtfPeer> peers; | ||
| 472 | CRITICAL_BLOCK(cs_btfPeerCache) | ||
| 473 | LoadCachedBtfPeers(peers); | ||
| 474 | |||
| 475 | // Most recently seen first: these answered us, they are not names copied | ||
| 476 | // off a relay listing. | ||
| 477 | foreach(const CachedBtfPeer& p, peers) | ||
| 478 | { | ||
| 479 | if (vDescOut.size() >= MAX_PEX_DESCRIPTORS) break; | ||
| 480 | if (p.desc.empty()) continue; // nothing provable to pass on | ||
| 481 | if (p.desc.size() > MAX_PEX_DESCRIPTOR_BYTES) continue; | ||
| 482 | vDescOut.push_back(p.desc); | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | int BtfPexAccept(const vector<string>& vDesc) | ||
| 487 | { | ||
| 488 | void* ctx = BtfSecpContext(); | ||
| 489 | if (!ctx) | ||
| 490 | return 0; | ||
| 491 | |||
| 492 | string strSelf = BtfLocalAddress(); | ||
| 493 | int nKept = 0; | ||
| 494 | unsigned int nSeen = 0; | ||
| 495 | |||
| 496 | foreach(const string& strDesc, vDesc) | ||
| 497 | { | ||
| 498 | if (++nSeen > MAX_PEX_DESCRIPTORS) break; | ||
| 499 | if (strDesc.empty() || strDesc.size() > MAX_PEX_DESCRIPTOR_BYTES) continue; | ||
| 500 | |||
| 501 | // The descriptor names the key it belongs to, and taking that key from | ||
| 502 | // the blob is safe precisely because the signature must verify under | ||
| 503 | // it: nobody can produce a valid descriptor for a key they don't hold. | ||
| 504 | unsigned char pubkey[32]; | ||
| 505 | try | ||
| 506 | { | ||
| 507 | nlohmann::json v = nlohmann::json::parse(strDesc); | ||
| 508 | if (!v.is_object() || !v.contains("pubkey") || !v["pubkey"].is_string()) | ||
| 509 | continue; | ||
| 510 | if (!HexToBytes(v["pubkey"].get<string>(), pubkey, 32)) | ||
| 511 | continue; | ||
| 512 | } | ||
| 513 | catch (...) { continue; } // malformed JSON from an untrusted peer | ||
| 514 | |||
| 515 | btf::Descriptor d; | ||
| 516 | if (!btf::VerifyDescriptor(ctx, strDesc, pubkey, d)) | ||
| 517 | continue; | ||
| 518 | |||
| 519 | string strAddr = btf::Address(pubkey); | ||
| 520 | if (strAddr.empty() || strAddr == strSelf) | ||
| 521 | continue; // ourselves, nothing to learn | ||
| 522 | |||
| 523 | unsigned char enc[32]; | ||
| 524 | if (!HexToBytes(d.enc, enc, 32)) | ||
| 525 | continue; | ||
| 526 | |||
| 527 | RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc); | ||
| 528 | nKept++; | ||
| 529 | } | ||
| 530 | return nKept; | ||
| 531 | } | ||
| 532 | |||
| 262 | static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], | 533 | static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], |
| 263 | const string& strMeeting, const unsigned char enc_pub[32]) | 534 | const string& strMeeting, const unsigned char enc_pub[32]) |
| 264 | { | 535 | { |
| @@ -289,6 +560,9 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p | |||
| 289 | if (fDebug) | 560 | if (fDebug) |
| 290 | LogPrint("net", "connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); | 561 | LogPrint("net", "connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); |
| 291 | 562 | ||
| 563 | // This one answered -- worth trying first next time we start. | ||
| 564 | RememberBtfPeer(strBtfAddr, strMeeting, enc_pub); | ||
| 565 | |||
| 292 | // Add node | 566 | // Add node |
| 293 | pnode = new CNode(hSocket, addr, false); | 567 | pnode = new CNode(hSocket, addr, false); |
| 294 | pnode->AddRef(); | 568 | pnode->AddRef(); |
| @@ -951,6 +1225,12 @@ bool StartNode(string& strError) | |||
| 951 | } | 1225 | } |
| 952 | }, 0, NULL); | 1226 | }, 0, NULL); |
| 953 | 1227 | ||
| 1228 | // Peers that answered last time, dialled straight away. The relays are the | ||
| 1229 | // only way to find anyone otherwise, and reaching them plus walking their | ||
| 1230 | // descriptor list is what makes a restart take minutes. | ||
| 1231 | if (_beginthread(ThreadReconnectCachedBtfPeers, 0, NULL) == -1) | ||
| 1232 | printf("Error: _beginthread(ThreadReconnectCachedBtfPeers) failed\n"); | ||
| 1233 | |||
| 954 | // Peer discovery over Nostr relays (replaces the old IRC seed) | 1234 | // Peer discovery over Nostr relays (replaces the old IRC seed) |
| 955 | if (_beginthread(ThreadNostrSeed, 0, NULL) == -1) | 1235 | if (_beginthread(ThreadNostrSeed, 0, NULL) == -1) |
| 956 | printf("Error: _beginthread(ThreadNostrSeed) failed\n"); | 1236 | printf("Error: _beginthread(ThreadNostrSeed) failed\n"); |
| @@ -29,6 +29,21 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting | |||
| 29 | void ThreadBtfAccept(void* parg); | 29 | void ThreadBtfAccept(void* parg); |
| 30 | void ThreadBtfConnect(void* parg); | 30 | void ThreadBtfConnect(void* parg); |
| 31 | extern string strBtfConnect; | 31 | extern string strBtfConnect; |
| 32 | |||
| 33 | // --- .btf peer exchange --------------------------------------------------- | ||
| 34 | // Peers gossip signed descriptors so discovery survives a relay outage. Both | ||
| 35 | // caps apply to a single "btfpeers" message; the generic header limit is 256 MB, | ||
| 36 | // which is no protection at all for something a stranger can send unprompted. | ||
| 37 | static const unsigned int MAX_PEX_DESCRIPTORS = 20; | ||
| 38 | static const unsigned int MAX_PEX_DESCRIPTOR_BYTES = 1024; | ||
| 39 | // Ignore a peer's exchange more often than this. One announcement per side per | ||
| 40 | // connection is the intended traffic; anything faster is someone else's idea. | ||
| 41 | static const int64 PEX_MIN_INTERVAL = 60; | ||
| 42 | |||
| 43 | // Descriptors to hand a peer: ours first, then peers that answered us. | ||
| 44 | void BtfPexCollect(std::vector<std::string>& vDescOut); | ||
| 45 | // Verify descriptors a peer sent and remember the good ones. Returns how many. | ||
| 46 | int BtfPexAccept(const std::vector<std::string>& vDesc); | ||
| 32 | void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1); | 47 | void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1); |
| 33 | bool AnySubscribed(unsigned int nChannel); | 48 | bool AnySubscribed(unsigned int nChannel); |
| 34 | void ThreadBitcoinMiner(void* parg); | 49 | void ThreadBitcoinMiner(void* parg); |
| @@ -467,6 +482,9 @@ public: | |||
| 467 | // publish and subscription | 482 | // publish and subscription |
| 468 | vector<char> vfSubscribe; | 483 | vector<char> vfSubscribe; |
| 469 | 484 | ||
| 485 | // Last "btfpeers" we accepted from this node, to rate-limit the exchange. | ||
| 486 | int64 nLastPexRecv; | ||
| 487 | |||
| 470 | 488 | ||
| 471 | CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false) | 489 | CNode(SOCKET hSocketIn, CAddress addrIn, bool fInboundIn=false) |
| 472 | { | 490 | { |
| @@ -483,6 +501,7 @@ public: | |||
| 483 | fDisconnect = false; | 501 | fDisconnect = false; |
| 484 | nRefCount = 0; | 502 | nRefCount = 0; |
| 485 | nReleaseTime = 0; | 503 | nReleaseTime = 0; |
| 504 | nLastPexRecv = 0; | ||
| 486 | vfSubscribe.assign(256, false); | 505 | vfSubscribe.assign(256, false); |
| 487 | 506 | ||
| 488 | // Push a version message | 507 | // Push a version message |
diff --git a/src/nostr.cpp b/src/nostr.cpp index ec02c2e..482a835 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp | |||
| @@ -342,6 +342,36 @@ std::string BtfLocalAddress() | |||
| 342 | return g_nostrKey.BtfAddress(); | 342 | return g_nostrKey.BtfAddress(); |
| 343 | } | 343 | } |
| 344 | 344 | ||
| 345 | // This node's own descriptor, signed and self-certifying -- the same blob | ||
| 346 | // PublishDescriptor puts on Nostr, but handed straight to a peer instead. | ||
| 347 | // | ||
| 348 | // Reads strBtfActiveRelay directly rather than going through BtfActiveRelay(): | ||
| 349 | // only a rendezvous we actually registered at is worth announcing. Handing out | ||
| 350 | // a meeting node we are not reachable at would make every peer that receives | ||
| 351 | // it burn a dial, and over peer exchange that mistake travels further than it | ||
| 352 | // does over Nostr. | ||
| 353 | std::string BtfLocalDescriptor() | ||
| 354 | { | ||
| 355 | if (!EnsureNostrKey()) | ||
| 356 | return ""; | ||
| 357 | string meeting; | ||
| 358 | CRITICAL_BLOCK(cs_activeRelay) | ||
| 359 | meeting = strBtfActiveRelay; | ||
| 360 | if (meeting.empty()) | ||
| 361 | return ""; | ||
| 362 | return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, | ||
| 363 | g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime()); | ||
| 364 | } | ||
| 365 | |||
| 366 | // The shared secp256k1 context, so net.cpp can verify descriptors that arrive | ||
| 367 | // from peers. NULL if the identity could not be loaded. | ||
| 368 | void* BtfSecpContext() | ||
| 369 | { | ||
| 370 | if (!EnsureNostrKey()) | ||
| 371 | return NULL; | ||
| 372 | return (void*)g_nostrKey.ctx; | ||
| 373 | } | ||
| 374 | |||
| 345 | int GetDiscoveredPeerCount() | 375 | int GetDiscoveredPeerCount() |
| 346 | { | 376 | { |
| 347 | int n = 0; | 377 | int n = 0; |
diff --git a/src/nostr.h b/src/nostr.h index 04149e3..b5eb5e3 100644 --- a/src/nostr.h +++ b/src/nostr.h | |||
| @@ -82,6 +82,14 @@ bool BtfGetIdentity(unsigned char pubkey[32], unsigned char enc_sk[32]); | |||
| 82 | // This node's printable .btf address, or "" if the identity failed to load. | 82 | // This node's printable .btf address, or "" if the identity failed to load. |
| 83 | std::string BtfLocalAddress(); | 83 | std::string BtfLocalAddress(); |
| 84 | 84 | ||
| 85 | // This node's own signed descriptor, ready to hand to a peer over the wire. | ||
| 86 | // "" until ThreadBtfAccept has registered at a rendezvous. | ||
| 87 | std::string BtfLocalDescriptor(); | ||
| 88 | |||
| 89 | // The shared secp256k1 context, for btf::VerifyDescriptor (which takes it as | ||
| 90 | // void* to keep headers free of secp). NULL if the identity failed to load. | ||
| 91 | void* BtfSecpContext(); | ||
| 92 | |||
| 85 | // Resolve a .btf address via the public discovery relays: fetch the owner's | 93 | // Resolve a .btf address via the public discovery relays: fetch the owner's |
| 86 | // self-certified descriptor and return its meeting node ("host:port") and | 94 | // self-certified descriptor and return its meeting node ("host:port") and |
| 87 | // x25519 public key for the end-to-end channel. | 95 | // x25519 public key for the end-to-end channel. |