Commit graph

2 commits

Author SHA1 Message Date
1c16a2a4b7 fix(transport): harden RelayPool — connect timeout, stop-race, cross-relay dedup
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
Folds three medium findings from the transport review into the RelayPool:

- RP-4: connectOnce used the static Relay.connect(), which silently DROPS a
  {timeout} option in nostr-tools 2.20.0, so a black-holed TCP connect (SYN
  accepted, never upgraded) stalled that relay's loop for the OS socket timeout
  (minutes) with no retry. Now constructs the Relay and calls the instance
  .connect({ timeout: 5000 }), which honours the timeout → prompt reject + backoff.

- RP-2: connectOnce didn't re-check `stopped` after the await. If stop() ran
  while a connect was in flight, the resolved socket re-armed subscriptions on a
  relay we meant to abandon, leaked the socket, and hung connectLoop (its promise
  never resolved because onclose never fired). Now drops the socket cleanly and
  resolves if stopped mid-connect.

- RP-1: no cross-relay event dedup — a kind:24133 request published to N relays
  (the normal NIP-46 pattern), or re-delivered after a reconnect, drove the
  daemon handler + recordSigning N times, making rate caps bind ~N× tighter
  (fails closed, not open). Added a bounded (4000-id, ≈LRU) pool-wide seen-set;
  onevent fires at most once per event id. Closes the CS-4 replay vector too.

Test: tests/relay-pool.test.ts asserts a duplicate event id is delivered once.
relay 3 / nip46 1 / admin 2 green; daemon bundles; tsc at baseline.

Refs: transport review RP-1/RP-2/RP-4, CS-4; #42
2026-06-27 12:27:36 +02:00
1409941d11 feat(transport): nostr-tools relay pool that re-subscribes on reconnect (#42)
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
First increment of the NDK -> nostr-tools transport swap (#42), the root fix
for #41 (bunker goes silently deaf after a relay flap).

NDK does not replay subscriptions on reconnect: a NDKRelaySubscription registers
`relay.once("ready", execute)` and never re-arms, so after a flap the socket
reconnects but the kind:24133 REQ is never re-sent. We chased that through
#4/#7/#20/#21 without closing it because it is structural in NDK.

`RelayPool` (src/daemon/lib/relay-pool.ts) owns the connect loop, modelled on
lightning.pub's RelayConnection and signet's relay-pool (both nostr-tools, both
bind resubscribe to reconnect). Every (re)connect re-subscribes the whole
registry, so subscription liveness can't drift from socket liveness. It also
exposes `healthy()` (connected AND registry subscribed on the wire) — the
session-liveness signal the old connectedRelays()-only watchdog couldn't make,
which is what let #20's reconnect mask the deaf state.

We disable nostr-tools' own `enableReconnect`: its auto-resubscribe is
version-fragile right now (regressed in 2.23.0 fb7de7f; the 455124e fix is
unreleased as of 2026-06-26), so the resubscribe is OUR code, not a function of
which nostr-tools version is installed.

Regression test (tests/relay-pool.test.ts + tests/helpers/mock-relay.ts): an
in-process mock relay flaps mid-session (down + back up on the same port) and we
assert a subsequent inbound kind:24133 is still delivered — the exact #41
scenario, and the test that was missing every prior round. Green; existing
lifecycle suite unchanged.

Next increments on this branch: port Backend (NIP-46) + AdminInterface (RPC)
onto the pool, wire run.ts, retire relay-reconnect.ts + the connection-only
watchdog.

Refs: #42, #41, #21, #20, #9
2026-06-26 23:42:36 +02:00