Skip to content

fix: tolerate hung, refusing and forgetful relays - #198

Merged
TheCryptoDonkey merged 5 commits into
mainfrom
fix/bad-relay-tolerance
Sep 28, 2026
Merged

TheCryptoDonkey merged 5 commits into
mainfrom
fix/bad-relay-tolerance

Conversation

@TheCryptoDonkey

Copy link
Copy Markdown
Member

Evidence-led audit of how the web/desktop app copes with bad relays (hung relay.primal.net, refusing nos.lol, relays that OK chat and keep none). Every 'before' was measured on main and each failing test was checked to fail there.

  • History no longer waits on a hung relay: every subscription reports loaded at the 8 s deadline (was 23 s; each resend restarted nostr-tools' wait). The hung relay keeps being retried in the background.
  • One shared backoff per relay (1 s doubling to 8 s, cleared only by a real answer): a refusing relay went from 101 dials in two minutes to 16 (browser: 46 in ~40 s to at most 12).
  • A publish that raced a closing socket was logged as a refusal and never retried; now a lost connection.
  • Unreachable relay reads "Connection failed", not "Publish timed out".
  • "Does not keep chat": when a readable relay OKs a stored kind, the app asks for it back 3 s later; only a real empty answer counts. Shown in relay settings.
  • Per-subscription seen-id set capped at 8,192 (grew for ever in standing rooms).

Already fine and now tested: forged/malformed/flooded events, publish counted on the first OK, outbox keeps unsent messages with Retry.

Tests: typecheck; relay unit tests; new test/bad-relays.spec.ts passes on chromium and firefox; related specs pass. Overlaps settled() in #196 and the upcoming room-archive branch in src/relay-pool.ts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk

…ct storms

A relay that accepts the socket and never answers held a subscription's
EOSE for about 23 seconds, because each resend restarted nostr-tools' own
8-second wait. The reader is now told history has loaded at that 8-second
deadline, whatever a hung relay does.

A relay that refused every connection was dialled about 100 times in two
minutes of heartbeats, since each publish's retries and the readers they
rebound dialled on their own schedules. Dials now share a per-relay
backoff (1s doubling to 8s) that only an answer clears, not an open socket;
a hung relay drops from 19 dials to 11 and one that drops each socket as
it opens from 34 to 18.

A publish that raced a socket closing (SendingOnClosedConnection) was
taken for the relay refusing it and never retried. It is now a lost
connection like any other. A relay that could not be dialled reads
"Connection failed" in its health rather than "Publish timed out".

Each subscription's seen-id set is bounded; every presence heartbeat is a
new id, so a long-lived room grew it without limit.

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
A relay that says OK to a kind 1460 write and never returns it looks
healthy by every other measure. The first time a readable relay accepts a
regular (stored) kind, the pool asks it for that event by id three seconds
later. Only a real EOSE without the event counts: a timeout, a refusal or
a dropped socket proves nothing and the kind is tried again on a later
write. Write-only relays are never read from. The finding is carried in
RelayHealth.unreturned and relay settings shows "Does not keep chat:
accepted a message, then did not return it".

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
A hung relay, one that keeps no chat and one that refuses every
connection, each beside the local test relay. On the previous pool the
refusing relay was dialled 46 times in about 40 seconds; the spec allows
12.

Claude-Session: https://claude.ai/code/session_01CG4pPCsd8pdySNvBpt8fTk
@TheCryptoDonkey
TheCryptoDonkey merged commit 6186347 into main Sep 28, 2026
8 of 10 checks passed
@TheCryptoDonkey
TheCryptoDonkey deleted the fix/bad-relay-tolerance branch September 28, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant