Skip to content

feat: NIP-66 relay liveness pre-filtering with safety guardrails#55

Open
alltheseas wants to merge 4 commits into
hzrd149:masterfrom
alltheseas:feat/nip66-relay-liveness
Open

feat: NIP-66 relay liveness pre-filtering with safety guardrails#55
alltheseas wants to merge 4 commits into
hzrd149:masterfrom
alltheseas:feat/nip66-relay-liveness

Conversation

@alltheseas

Copy link
Copy Markdown
Contributor

Depends on #54 — this branch is based on the Thompson sampling PR. Merge #54 first; these commits apply cleanly on top with zero conflicts.

Summary

  • Add removeDeadRelays helper — pure function that filters dead relays from ProfilePointer[] using NIP-66 monitor alive sets
  • Add classifyRelays helper — classifies relay URLs as alive/dead/unmonitored with .onion/.i2p preservation
  • Add ignoreDeadRelays RxJS operator — follows ignoreBlacklistedRelays pattern; Observable inputs wrapped with startWith(emptySet) so the pipeline never blocks
  • Wire into OutboxModel — new aliveRelays, livenessFilter, livenessId options; absent = no-op, zero behavior change for existing callers

Designed in response to community feedback on nostrability/nostrability#69. Outbox benchmarks show NIP-66 pre-filtering removes 53.8% dead relays, improves success rate by +41.7pp, and reduces wall-clock time by 45%.

NIP-66 PR #2240 compliance

Requirement How we comply
Absence of monitoring data MUST NOT prevent relay connections Empty aliveRelays = no-op (pass-through)
A monitor may publish erroneous 30166 events maxFilterRatio (default 0.8) per-user cap; minAliveSetSize (default 100) global guard
Clients SHOULD NOT trust a single source aliveRelays is opaque — client builds it from WoT-filtered monitors. Applesauce doesn't choose monitors

Safety guardrails

  • minAliveSetSize (default 100): skip filtering if alive set is suspiciously small (rogue/misconfigured monitor)
  • maxFilterRatio (default 0.8): if filtering would remove >80% of a user's relays, skip that user (protects against monitor claiming most relays are dead)
  • monitoredRelays option: only remove relays that monitors actually checked; unmonitored personal relays are preserved
  • .onion/.i2p preservation: monitors can't check these without Tor/I2P
  • Malformed URL preservation: don't silently drop what we can't parse

Commits

Commit Description
2fbc33b3 Pure helper: classifyRelays, removeDeadRelays, normalizeRelayUrl
40516f67 RxJS operator: ignoreDeadRelays with startWith(emptySet)
88bab56d OutboxModel integration: aliveRelays / livenessFilter / livenessId
1e9ec809 29 tests covering all edge cases and guardrails

Test plan

  • All 519 core tests pass (npx vitest run packages/core)
  • removeDeadRelays(users, new Set()) returns users unchanged (spec req 1)
  • removeDeadRelays(users, tinySet) where tinySet.size < 100 returns users unchanged
  • .onion relays preserved regardless of alive set
  • monitoredRelays option keeps unmonitored personal relays
  • maxFilterRatio edge cases: 4/5=0.8 filtered, 5/5=1.0 skipped, 1/1=1.0 skipped
  • Observable operator emits immediately via startWith, never blocks
  • Snapshot exports updated

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Mar 5, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: efbae92

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

alltheseas and others added 4 commits March 5, 2026 12:18
Pure helper for classifying and filtering dead relays using NIP-66
monitor data. Safety guardrails prevent rogue monitors from breaking
relay selection:

- Empty alive set = no-op (NIP-66 PR #2240 requirement 1)
- minAliveSetSize (default 100) skips tiny/suspect monitor data
- maxFilterRatio (default 0.8) per-user cap on removal
- monitoredRelays option preserves unmonitored personal relays
- .onion/.i2p and malformed URLs always preserved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follows the ignoreBlacklistedRelays pattern. Accepts a static Set or
Observable<Set> of alive relay URLs. Observable inputs are wrapped
with startWith(emptySet) so the pipeline emits immediately and never
blocks waiting for monitor data (NIP-66 PR #2240 requirement 1).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds aliveRelays, livenessFilter, and livenessId to OutboxModelOptions.
Pipeline: contacts → blacklist → mailboxes → ignoreDeadRelays → select.
Absent aliveRelays = no-op, existing behavior unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
29 tests covering:
- normalizeRelayUrl: trailing slash, hostname case, scheme upgrade
- classifyRelays: alive/dead/unmonitored, .onion/.i2p, malformed URLs
- removeDeadRelays: empty set no-op, minAliveSetSize, maxFilterRatio
  edge cases (0.8 threshold, all-dead skip, single-relay skip),
  monitoredRelays preservation
- ignoreDeadRelays operator: static Set, Observable with startWith

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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