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); + } + }); +});