From 5aa7d9ca153d0e9dbdb8c5fd379c29d8be0abdc9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:17:45 -0400 Subject: [PATCH] Relay lists: drop a relay that cannot accept writes, widen for margin Measured every candidate on 2026-08-05 against its own NIP-11 document and over a real socket, three runs each. nostr.wine advertises payment_required AND restricted_writes, so it can never accept a free user's capsule -- yet it held one of five slots in BUILTIN_VAULT_RELAYS, the list capsules are PUBLISHED to. relay.damus.io and relay.nostr.band were unreachable 3/3 from the machine under test. That left two usable relays against publishAndVerify's floor of two acknowledgements and two readbacks: zero margin, so a single hiccup fails registration or a password rotation outright. Vault list is now eight relays, each verified to accept anonymous writes (no payment_required, no auth_required, no restricted_writes). damus stays for reach -- its failure here may be datacenter-IP filtering rather than downtime -- but quorum no longer depends on it. Discovery gains primal; relay.nostr.band stays there, where a search relay belongs, rather than in a write path. Pinned by hermetic tests: no WRITE_HOSTILE_RELAYS entry may appear in the vault list, the list must keep margin over the floor, and both lists must be duplicate-free wss URLs. Reachability is a property of the user's network and is deliberately NOT asserted; write policy is a property of the relay and is. Mutation-verified: putting nostr.wine back turns the pin red. Co-Authored-By: Claude Fable 5 --- packages/core/src/nostr/bootstrap.ts | 51 +++++++++++++++++++++++++-- packages/core/src/nostr/nostr.test.ts | 38 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/packages/core/src/nostr/bootstrap.ts b/packages/core/src/nostr/bootstrap.ts index e0ca999..be1d685 100644 --- a/packages/core/src/nostr/bootstrap.ts +++ b/packages/core/src/nostr/bootstrap.ts @@ -23,9 +23,56 @@ export const MAINTAINER_PUBLIC_KEY_HEX: string | null = null; // comfortably -- a 3-relay list has zero margin the moment one relay is // unreachable, which is exactly what "add more vault relays" (the // registration failure message) is telling operators to fix. -export const BUILTIN_VAULT_RELAYS: readonly string[] = ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band", "wss://nostr.wine", "wss://relay.snort.social"]; +/** + * Relays that CANNOT accept an ordinary user's capsule, by their own policy. + * + * Measured 2026-08-05 from each relay's NIP-11 document, not from reputation: + * `nostr.wine` advertises `payment_required: true` and + * `restricted_writes: true`, so a free user's write is refused there no matter + * how healthy the socket looks. It sat in the vault list below occupying one + * of five write slots it could never fill. + * + * A relay here may still be perfectly good for READING. This list exists so + * the test suite can refuse to let one back into a write path. + */ +export const WRITE_HOSTILE_RELAYS: readonly string[] = ["wss://nostr.wine", "wss://nostr21.com", "wss://nostr.land"]; -export const BUILTIN_DISCOVERY_RELAYS: readonly string[] = ["wss://purplepag.es", "wss://relay.nostr.band", "wss://nostr-pub.wellorder.net"]; +/** + * Vault relays: where capsules are PUBLISHED, so every entry must accept + * anonymous writes. + * + * Widened 2026-08-05 after measuring the old five. Only two could actually + * take a write from a fresh client: nostr.wine refuses by policy (above), and + * relay.damus.io and relay.nostr.band were unreachable across three runs from + * the machine under test. Two usable relays against a floor of two + * acknowledgements is zero margin -- one hiccup and registration or a + * password rotation fails outright. + * + * damus stays: it is the most widely used relay on the network and the + * failure may be datacenter-IP filtering rather than downtime. The point of + * widening is that quorum no longer DEPENDS on it. + */ +export const BUILTIN_VAULT_RELAYS: readonly string[] = [ + "wss://nos.lol", + "wss://offchain.pub", + "wss://nostr.bitcoiner.social", + "wss://relay.mostr.pub", + "wss://nostr-pub.wellorder.net", + "wss://relay.primal.net", + "wss://relay.snort.social", + "wss://relay.damus.io", +]; + +/** + * Discovery relays: read-only lookups (profiles, relay lists), so a + * search-oriented or write-restricted relay is appropriate here. + */ +export const BUILTIN_DISCOVERY_RELAYS: readonly string[] = [ + "wss://purplepag.es", + "wss://relay.nostr.band", + "wss://nostr-pub.wellorder.net", + "wss://relay.primal.net", +]; /** Well-known static HTTPS fallback URLs for the signed relay-list document (ยง19.1). */ export const BOOTSTRAP_HTTPS_FALLBACK_URLS: readonly string[] = []; diff --git a/packages/core/src/nostr/nostr.test.ts b/packages/core/src/nostr/nostr.test.ts index 9298a96..20a3599 100644 --- a/packages/core/src/nostr/nostr.test.ts +++ b/packages/core/src/nostr/nostr.test.ts @@ -16,7 +16,10 @@ import { import { RelayPool } from "./pool.js"; import { MockRelay } from "../test-support/mockRelay.js"; import { + BUILTIN_DISCOVERY_RELAYS, + BUILTIN_VAULT_RELAYS, MAINTAINER_PUBLIC_KEY_HEX, + WRITE_HOSTILE_RELAYS, mergeRelayLists, parseAndVerifyBootstrapList, type BootstrapRelayList, @@ -427,3 +430,38 @@ describe("RelayPool quorum", () => { pool.closeAll(); }); }); + +/* + * The built-in relay lists are a shipped configuration that decides whether a + * user can create an account at all, so they get the same treatment as any + * other invariant: pinned, with the reasoning attached. + * + * Hermetic on purpose -- no relay is contacted here. Reachability is a + * property of the network the user is on and cannot be asserted in CI; + * WRITE POLICY is a property of the relay and can. + */ +describe("built-in relay lists", () => { + it("keeps write-hostile relays out of the vault (publish) list", () => { + // nostr.wine advertises payment_required + restricted_writes in its own + // NIP-11 document, so it can never accept a free user's capsule. It sat + // in this list occupying one of five write slots it could not fill. + for (const hostile of WRITE_HOSTILE_RELAYS) { + expect(BUILTIN_VAULT_RELAYS).not.toContain(hostile); + } + }); + + it("leaves enough margin over the publish floor to lose relays", () => { + // publishAndVerify enforces minAcks/minReadbacks = 2 regardless of list + // size. Two usable relays against a floor of two is zero margin: one + // unreachable relay fails registration outright. Require room to lose + // several and still clear the floor. + expect(BUILTIN_VAULT_RELAYS.length).toBeGreaterThanOrEqual(6); + }); + + it("ships no duplicates and only secure websocket URLs", () => { + for (const list of [BUILTIN_VAULT_RELAYS, BUILTIN_DISCOVERY_RELAYS]) { + expect(new Set(list).size).toBe(list.length); + for (const url of list) expect(url.startsWith("wss://")).toBe(true); + } + }); +});