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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-05 10:17:45 -04:00
1 parent 30b4f34300
commit 5aa7d9ca15
2 files changed
+87 -2

No files matched your search

+49 -2
View File
@@ -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[] = [];
+38
View File
@@ -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);
}
});
});