{"id":"74489630b23af68c650ec0ab7302797bdc66d590aa9568c05b595d9cf02a2545","pubkey":"43185edecb675892824b1a37a57f3e407fbde2eda7201a3829b8cf4ba7c5b4f0","created_at":1782681136,"kind":30817,"tags":[["title","PR #2349: NIP-05 verification via Namecoin (.bit)"],["alt","Custom NIP: PR #2349: NIP-05 verification via Namecoin (.bit)"],["l","recreated-github-pr"],["L","org.nostr-protocol.nips"],["k","0"],["d","pr-2349-nip05-namecoin"]],"content":"> **Recreation note.** This is a copy of pull request [nostr-protocol/nips#2349](https://github.com/nostr-protocol/nips/pull/2349), preserved as a custom NIP because my GitHub account (`mstrofnone`) is currently suspended. The narrative below is the PR body; the spec text follows verbatim from the proposed `XX.md`.\n\n---\n\n## PR body\n\n\nNIP-05 verification via Namecoin (`.bit`)\n=========================================\n\nAdds a new NIP specifying how clients verify, and publishers publish, a\n[NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md)\nidentifier whose right-hand side is anchored in the Namecoin blockchain\nrather than DNS + HTTPS. Strict complement to NIP-05; both can coexist\non the same identity.\n\nThis is the narrowest possible slice of the broader RFC track in\n[#2330](https://github.com/nostr-protocol/nips/issues/2330) — NIP-05\nverification only. Relay discovery via Namecoin, TLSA pinning of `.bit`\nrelay WebSockets, and service-attestation kinds are deliberately\n**out of scope** and tracked separately (see §\"Out of scope\" at the\nbottom of the spec).\n\n## Why this NIP\n\n`.bit` is a top-level name that can't be taken down by a registrar, a\nDNS root authority, or a public CA compromise. The wire format inside\nthe existing ifa-0001 Domain Name Object adds **zero** new top-level\nNamecoin record keys — everything lives inside a new `nostr` object\nthat ifa-0001 consumers ignore. Adoption is therefore drop-in for any\nclient that already implements NIP-05: same `kind:0` field, same\nlocal-part / pubkey shape; only the right-hand-side resolver changes.\n\n## Implementation evidence\n\nEight wire-compatible reference implementations across six platforms\nhave shipped this NIP against the live Namecoin chain, with\nbyte-for-byte agreement on the same on-chain records:\n\n| Platform | Implementation | Status |\n| --- | --- | --- |\n| Android / iOS / Desktop (Kotlin/KMP) | [vitorpamplona/amethyst#1734](https://github.com/vitorpamplona/amethyst/pull/1734) + follow-ups | merged |\n| iOS (Swift) | [nostur-com/nostur-ios-public#60](https://github.com/nostur-com/nostur-ios-public/pull/60) | merged |\n| TypeScript / Node (isomorphic) | [nbd-wtf/nostr-tools#533](https://github.com/nbd-wtf/nostr-tools/pull/533) | merged |\n| Flutter / Dart (`nostr` on pub.dev) | [ethicnology/dart-nostr#44](https://github.com/ethicnology/dart-nostr/pull/44) | merged |\n| Web (browser-resident `wss://` ElectrumX) | [CodyTseng/jumble#774](https://github.com/CodyTseng/jumble/pull/774) | merged |\n| Web | [hzrd149/nostrudel#352](https://github.com/hzrd149/nostrudel/pull/352) | merged |\n| strfry NIP-05 sidecar (Rust + Node) | [mstrofnone/strfry-nip05-namecoin](https://github.com/mstrofnone/strfry-nip05-namecoin) | shipped |\n| Go (drop-in for `nostrlib`) | [mstrofnone/nostrlib-nip05-namecoin](https://github.com/mstrofnone/nostrlib-nip05-namecoin) per [fiatjaf/nak#123](https://github.com/fiatjaf/nak/pull/123) | shipped |\n\nLive reference deployment at `testls.bit` / `relay.testls.bit` is\nexercised by the test suites of every implementation above.\n\n## Scope discipline\n\nPer the explicit \"form preference\" question in the\n[RFC issue](https://github.com/nostr-protocol/nips/issues/2330), this\nPR follows the one-NIP-per-PR option, starting with NIP-05 verification\nbecause it has the deepest deployment evidence. The N0 (\"Namecoin\nrecord container\") draft has been folded directly into this document\nas a §\"Namecoin record container\" section, since a standalone\ncontainer NIP isn't useful without a consuming NIP.\n\nThe companion drafts (relay discovery / TLSA / service attestations)\nlive at https://github.com/mstrofnone/nips and are intentionally not\nproposed here. Happy to open separate PRs for each, after this one\nlands, if maintainers want to upstream them.\n\n## Notes for review\n\n- No new event kinds.\n- No new tags.\n- No changes to existing NIPs.\n- The `nostr` key inside the ifa-0001 Domain Name Object is currently\n  unallocated; ifa-0001 §\"Item Suppression Rules\" requires existing\n  consumers to ignore unknown keys, so there is no upgrade hazard for\n  the Namecoin ecosystem either.\n- Numbering: assume merge order picks the next free `XX.md` slot;\n  happy to rename to whatever the maintainer prefers.\n\nCC: none — opening cold so the broadest set of reviewers can chime in.\n\n\n---\n\n## Proposed NIP text\n\nNIP-XX\n======\n\nNIP-05 verification via Namecoin\n--------------------------------\n\n`draft` `optional`\n\nThis NIP specifies how a Nostr client verifies, and a publisher\npublishes, a [NIP-05](05.md) identifier whose right-hand side is\nanchored in the [Namecoin](https://www.namecoin.org/) blockchain\nrather than in DNS + HTTPS. It is a complement to, not a replacement\nfor, NIP-05. A client MAY implement this NIP in addition to NIP-05 and\nremain fully interoperable with publishers who use only DNS-based\nNIP-05.\n\nIt builds on the existing Namecoin Domain Name Object schema defined\nby [ifa-0001][ifa-0001]; no new top-level Namecoin record key is\nintroduced.\n\n[ifa-0001]: https://github.com/namecoin/proposals/blob/master/ifa-0001.md\n\n## Abstract\n\nA `kind:0` event MAY contain `\"nip05\": \"<identifier>\"` where\n`<identifier>` resolves through Namecoin instead of through DNS.\nVerification proceeds by looking up the relevant Namecoin name,\nreading a `nostr` item from the merged Domain Name Object, and\nchecking that the value at the corresponding local-part matches the\nevent's `pubkey`.\n\n## Identifier grammar\n\nThe following identifier shapes MUST be recognised:\n\n| Form | Example | Resolution |\n|---|---|---|\n| `<localpart>@<name>.bit` | `alice@example.bit` | `d/example`, look up `nostr.names.alice` |\n| `<name>.bit` | `example.bit` | `d/example`, look up `nostr.names._` |\n| `_@<name>.bit` | `_@example.bit` | Equivalent to `<name>.bit` |\n| `d/<name>` | `d/example` | `d/example`, look up `nostr.names._` |\n| `id/<name>` | `id/alice` | `id/alice`, look up `nostr.names._` |\n| `<localpart>@<sub>.<name>.bit` | `alice@relay.example.bit` | `d/example`, walk `map.relay`, look up `nostr.names.alice` |\n| `<sub>.<name>.bit` | `relay.example.bit` | `d/example`, walk `map.relay`, look up `nostr.names._` |\n\nMulti-label subdomains are resolved through the ifa-0001 `map` item\n(see §\"Subdomains\"). The walked node provides the `nostr` item;\nancestors do **not** contribute. Multi-label hosts are always\nresolved via the parent registered name; implementations MUST NOT\nsynthesise `d/<sub>.<name>` and query it as a fallback.\n\nThe `<name>` part MUST match the ifa-0001 regex\n`^(xn--)?[a-z0-9]+(-[a-z0-9]+)*$` and SHALL be matched\ncase-insensitively (i.e. `EXAMPLE.BIT` resolves to `d/example`). The\n`<localpart>` is matched against `nostr.names` keys case-sensitively\nto mirror NIP-05.\n\nThe bare `@<name>.bit` form (e.g. `@example.bit`) MAY be additionally\nrecognised by clients that render Namecoin identifiers in note\ncontent, and SHOULD resolve identically to `_@<name>.bit`.\n\n## Namecoin record container\n\nThe Namecoin Domain Name Object MAY contain an item with key `nostr`.\nIts value SHALL be a JSON object holding mappings from local-part to\nhex pubkey and, optionally, per-pubkey relay hints:\n\n```jsonc\n{\n  \"nostr\": {\n    \"names\":  { \"_\": \"<hex>\", \"alice\": \"<hex>\", \"bob\": \"<hex>\" },\n    \"relays\": { \"<hex>\": [ \"wss://r1/\", \"wss://r2/\" ] }\n  }\n}\n```\n\nImplementations MUST ignore unknown keys inside the `nostr` object so\nthat this NIP can be extended in the future. An object with no\nrecognised keys is equivalent to the absence of the item.\n\n`nostr` is a previously-unallocated key in the ifa-0001 schema.\nExisting ifa-0001 consumers ignore unknown keys per\n[ifa-0001 §\"Item Suppression Rules\"](https://github.com/namecoin/proposals/blob/master/ifa-0001.md#item-suppression-rules),\nso adding a `nostr` item to a record does not affect DNS, TLS, or Tor\nclients of the same name.\n\n### Encoding constraints\n\n- Hex pubkeys MUST be lowercase 64-character hex strings (32 bytes).\n  Implementations MUST reject any other shape (e.g. `npub1...`,\n  uppercase hex, prefixed `0x...`).\n- Relay URLs MUST be valid `ws://` or `wss://` URLs as parsed by\n  [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986). Implementations\n  MUST drop entries that fail parsing or use other schemes.\n\n### Reserved local-parts\n\nThe local-part `_` is RESERVED for the root identity associated with\nthe name itself, matching NIP-05's `_` convention. Implementations MUST\ntreat `_@<name>.bit` as a request for `nostr.names._`.\n\nThe local-part `*` is RESERVED for future wildcard semantics and MUST\nNOT be used by publishers. Implementations MUST NOT silently substitute\n`*` for a missing local-part lookup.\n\n## Subdomains\n\nMulti-label hosts (`alice.example.bit`, `relay.example.bit`,\n`a.b.example.bit`) are realised through ifa-0001's\n[`map`](https://github.com/namecoin/proposals/blob/master/ifa-0001.md#map)\nitem — they are **not** registered as separate Namecoin names. The\n`d/` namespace is single-label.\n\nWalking a subdomain to read `nostr`:\n\n1. Split the host on dots, drop the trailing `.bit`, and reverse so\n   labels are processed in DNS order (rightmost-most-significant\n   first).\n2. From the parent record's root, descend `value.map.<label>` for each\n   remaining label.\n3. At each step, prefer an exact label match. Fall back to\n   `value.map[\"*\"]` (wildcard). Honour the `\"\"` empty-key default per\n   ifa-0001.\n4. Stop at the deepest matching node and read `nostr` **only from\n   that node**.\n\nImplementations MUST NOT inherit `nostr` from ancestor nodes when\nwalking. A subdomain only gets what its walked node declares (or what\ncomes through the wildcard / empty-key rules). Otherwise a parent\nname's owner could silently authorise a Nostr identity on a subdomain\nthey did not control.\n\n## `import`\n\nThe Namecoin `import` item from\n[ifa-0001 §\"import\"](https://github.com/namecoin/proposals/blob/master/ifa-0001.md#import)\nis fully supported and is the canonical way to split large records\nacross the 520-byte per-name limit. Implementations:\n\n- MUST resolve `import` before reading `nostr`.\n- MUST honour importer-precedence: the importing object's items win\n  over the imported items, including `null` (which suppresses the\n  imported value).\n- MUST resolve at least 4 levels of recursion (the spec minimum).\n  Deeper chains MAY be silently truncated.\n- MUST break cycles via a per-chain visited set.\n- SHOULD treat a failed `import` lookup (network error, missing name,\n  malformed JSON) as `{}` rather than failing the importing record. A\n  failed import MUST NOT cause the importing record's own fields to be\n  ignored.\n- MUST accept all four canonical shapes (string, single-element array,\n  pair-array with subdomain selector, full array-of-arrays) per\n  ifa-0001 §\"import\".\n\n## Wire format\n\nAfter resolving `import` and walking `map`, the merged Domain Name\nObject is read for `nostr.names`. `names` SHALL be a JSON object whose\nkeys are local-parts and whose values are 32-byte hex pubkeys per\n§\"Encoding constraints\".\n\nVerification succeeds iff:\n\n1. The name exists, is not expired (see §\"Expiry\"), and parses as\n   valid ifa-0001 JSON.\n2. After import-merge and subdomain walk,\n   `nostr.names[<localpart>]` exists.\n3. Its value, lowercased, equals the event's `pubkey` field.\n\nIf any of those conditions fail, the implementation MUST treat the\nNIP-05 as unverified. It MUST NOT silently fall back to a DNS lookup\nof `<name>.bit` — `.bit` is not a valid public TLD in DNS and any\nmatch found via DNS is necessarily out-of-band.\n\n### Opcode handling\n\nImplementations that extract the name value directly from the\nscriptPubKey of a Namecoin transaction output (rather than asking the\nupstream resolver for the parsed value) MUST recognise **both**\nname-operation opcodes:\n\n| Opcode | Value | Name | When emitted |\n|---|---|---|---|\n| `OP_NAME_FIRSTUPDATE` | `0x52` (`OP_2`) | First update | The first transaction in a name's lifetime, immediately after the `name_new` commitment matures. |\n| `OP_NAME_UPDATE`      | `0x53` (`OP_3`) | Subsequent update | Every later `NAME_UPDATE` transaction (renewals, value changes). |\n\nA scriptPubKey emitting a name operation always begins with one of\nthese two opcodes. A resolver that only matches `0x53` silently\nreturns no value for **every Namecoin identity that has never been\nre-updated** — i.e. any identity registered once and never renewed.\nThese cases are common in practice, especially for short-lived\nexperiments and fresh registrations.\n\nThe two opcodes carry slightly different stack shapes:\n\n- `OP_NAME_UPDATE` (`0x53`):\n  `OP_NAME_UPDATE <push(name)> <push(value)> OP_2DROP OP_DROP <address_script>`\n- `OP_NAME_FIRSTUPDATE` (`0x52`):\n  `OP_NAME_FIRSTUPDATE <push(name)> <push(rand)> <push(value)> OP_2DROP OP_2DROP <address_script>`\n\nThe FIRSTUPDATE form carries an additional 8-byte `rand` salt push\nbetween the name and the value. Resolvers MUST skip the `rand` push\nwhen reading the value, not return it as the value.\n\n### Root local-part fallback\n\nFor root lookups (no local-part, or local-part `_`), if `nostr.names._`\nis absent but `names` is non-empty, an implementation MAY return the\nfirst available entry in `names` to accommodate publishers who only\npopulate per-user identities. This MUST NOT apply to non-root lookups:\n`alice@example.bit` MUST NOT match `nostr.names.bob`.\n\n### Per-pubkey relays (optional)\n\n`nostr.relays` MAY be present and is structurally identical to NIP-05's\n`relays` map. When present, the listed relays are treated as\noutbox-style hints for the corresponding pubkey. The\n[NIP-65](65.md) (`kind:10002`) relay list still takes precedence when\nboth are available.\n\n## Lookup transport\n\nThis NIP does not mandate a specific transport for retrieving the\nNamecoin record. Implementations MAY use any of:\n\n- [ElectrumX-NMC][electrumx-nmc] over **TCP+TLS** (the standard Electrum\n  protocol port, typically `:50002`).\n- [ElectrumX-NMC][electrumx-nmc] over **WebSocket** (`ws://` or\n  `wss://`, typically `:50003` plaintext and `:50004` TLS). This is\n  the only viable transport for browser-resident clients, which\n  cannot open raw TCP sockets. See §\"Browser / WebSocket transport\".\n- A local Namecoin Core RPC (`name_show d/<name>`).\n- A trusted forward proxy that exposes one of the above.\n\nThe transport MUST NOT compromise on authenticity: the resolver MUST\neither pin or fully validate the upstream certificate chain, or use\nSPV/header verification, or run against a local node. A plaintext HTTP\nproxy that allows tampering MUST NOT be used.\n\nThe transport itself is interchangeable — the same JSON-RPC method set\n(`server.version`, `blockchain.scripthash.get_history`,\n`blockchain.transaction.get`, `blockchain.headers.subscribe`) works\nover all of them, and a single resolver implementation MAY pick its\ntransport per environment without changing its caller-facing API.\n\n[electrumx-nmc]: https://github.com/namecoin/electrumx\n\n### Bidirectional JSON-RPC\n\nElectrumX is a **bidirectional** JSON-RPC transport. The server is\npermitted to initiate its own method calls toward the client,\ninterleaved with — and often before — responses to the client's\noutstanding requests. Server-initiated calls observed in the wild\ninclude `server.banner`, `blockchain.headers.subscribe`,\n`blockchain.relayfee`, and `blockchain.estimatefee`. Many of these\narrive before the client's first `server.version` response.\n\nClients MUST NOT assume that the next inbound frame on a connection is\nthe response to the most recently sent request. Concretely:\n\n- Every outbound JSON-RPC request MUST carry an `id` field.\n- The client MUST match every inbound response by its `id` against\n  the set of outstanding outbound requests.\n- Inbound frames with no `id`, or with an `id` that does not match an\n  outstanding request, MUST be either dispatched to a separate\n  server-initiated-request handler or silently ignored. They MUST NOT\n  be consumed as the response to a request that happens to be waiting.\n\nA naive \"read the next message and treat it as my response\" handler\nwill return `null` / `undefined` / a corrupted result whenever the\nserver chooses to push first. The failure is silent: the resolver\nappears to say \"name not found\" or \"no nostr field\" even though the\nname is present on chain.\n\nThe canonical receive-loop shape, in pseudocode:\n\n```\noutstanding[id] = pendingPromise\nws.send({ jsonrpc: \"2.0\", method, params, id })\n// ...\non_message(frame):\n    msg = parse(frame)\n    if msg.id == undefined or msg.id not in outstanding:\n        // server-initiated push, ignore or route\n        return\n    outstanding[msg.id].resolve(msg.result)\n    delete outstanding[msg.id]\n```\n\nThe same requirement applies to the TCP+TLS, WebSocket-plaintext, and\nWebSocket-TLS transports; the framing differs (`\\n`-delimited vs. one\nframe per message) but the JSON-RPC semantics are identical.\n\n### ElectrumX server set\n\nWhen ElectrumX is used, the resolver SHOULD:\n\n1. Negotiate `server.version` first.\n2. Construct the canonical name index script for `d/<name>` or\n   `id/<name>`, take its SHA-256 (reversed, hex), and call\n   `blockchain.scripthash.get_history`.\n3. Fetch the latest transaction via `blockchain.transaction.get`,\n   parse the `NAME_UPDATE` script, and extract the value.\n4. Call `blockchain.headers.subscribe` once per session for the\n   current block height, used by §\"Expiry\".\n\nThe set of ElectrumX servers SHOULD be **caller-configurable**. This\nNIP does **not** mandate a default server list. Implementations that\nship defaults SHOULD:\n\n- Treat user-configured custom server lists as authoritative, i.e. use\n  them exclusively when configured, so privacy-conscious users can\n  pick their own observers.\n- Expose the server list as a runtime parameter (a function returning\n  a list, evaluated per request) rather than a hard-coded constant,\n  so toggling Tor or switching ElectrumX servers takes effect\n  immediately.\n- Prefer a `.onion` ElectrumX server when the user has Tor enabled for\n  NIP-05 lookups.\n\nLibrary authors integrating this NIP into a generic Nostr SDK SHOULD\naccept the server list (and ideally the transport itself, e.g. a\nWebSocket factory) from the caller, so the same library works in a\nbrowser, on a desktop with a local node, on a mobile app with a\npinned-cert TCP path, and behind a corporate proxy.\n\n### Failover and local-primary deployments\n\nResolvers SHOULD support an **ordered list** of ElectrumX servers and\ndegrade gracefully when an entry is unreachable. The recommended\npattern is a circuit breaker:\n\n1. Try hosts in list order, picking the first that is currently\n   marked healthy (or, if all are open, force-probe the next in line).\n2. On success, mark the host healthy and reset its failure counter.\n3. On a connect/handshake error or timeout, increment the host's\n   failure counter; once a small threshold (e.g. 3 failures inside a\n   60 s window) is reached, mark the host *open* (failing fast) for a\n   cooldown (e.g. 30 s) before allowing another probe.\n4. *Definitive* errors from the resolver path (name expired, name\n   missing, malformed value) MUST propagate immediately and MUST NOT\n   record a host failure — they are facts about the chain, not about\n   the host.\n\nServer-side and gated-relay deployments SHOULD run a **local**\nElectrumX-NMC alongside their relay or service and list it *first* in\nthe resolver's server list, with one or more public servers as\nfallback only. A single public ElectrumX would otherwise be a single\npoint of failure exactly like DNS + Web PKI was; the whole point of\nthis NIP is to remove that single point of failure for clients,\noperators should not reintroduce it server-side. Local lookups also\nnever leak per-event author metadata to a third party and remove the\nper-lookup network latency that otherwise dominates write-policy\ndecision time on busy relays.\n\nClient deployments (mobile, desktop, browser) MAY still use public\nservers as their only source: the privacy and availability trade-offs\nthere are a user-controlled setting.\n\n### Browser / WebSocket transport\n\nBrowser clients resolve `.bit` identifiers entirely client-side\nwithout any backend proxy. The transport differences from the TCP+TLS\npath are minor and standardised below.\n\nServer URL form:\n\n- `wss://<host>[:<port>][/path]` for TLS WebSocket. Port `:50004` is\n  the de facto standard for ElectrumX-NMC's WebSocket-TLS endpoint.\n- `ws://<host>[:<port>][/path]` for plaintext. Port `:50003` is the de\n  facto standard. Plaintext WebSocket MUST NOT be used over an\n  untrusted network — its only legitimate uses are local development\n  and explicit `localhost` setups.\n\nMixed-content rules:\n\n- A page served from `https://` MUST default to `wss://` ElectrumX\n  endpoints. Browsers will block `ws://` from a secure origin.\n- A page served from `http://` MAY use `ws://` endpoints; it remains\n  the implementation's responsibility to satisfy the\n  §\"Lookup transport\" authenticity rule, typically by pinning the\n  TLS endpoint anyway.\n- Implementations that auto-select a server SHOULD pick the\n  scheme-matching subset of their configured server list based on the\n  current page scheme, rather than failing closed.\n\nFraming and methods:\n\n- Each WebSocket message frame carries exactly one JSON-RPC request or\n  response object, terminated by a single `\\n`. This is the same\n  newline-delimited framing used by ElectrumX over TCP — the\n  WebSocket transport does not change the wire JSON.\n- The same four method calls listed above apply unchanged.\n\nCryptographic primitives:\n\n- Browser implementations SHOULD use\n  [Web Crypto](https://www.w3.org/TR/WebCryptoAPI/) `subtle.digest`\n  for SHA-256 (scripthash derivation) rather than bundling a JS hash.\n  It returns an `ArrayBuffer`; the Electrum scripthash is the\n  **reversed** byte order of the digest, rendered as lowercase hex.\n\nServer authenticity in the browser:\n\n- Browsers cannot expose the validated certificate to JavaScript, so\n  a browser-resident resolver cannot pin a SHA-256 fingerprint the\n  way a TCP+TLS resolver can. The browser MUST instead rely on the\n  user agent's TLS validation. This in turn means a browser\n  implementation SHOULD only auto-select WebSocket-TLS servers whose\n  certificate chains validate against the public CA trust store.\n  Self-signed `.bit` ElectrumX servers remain reachable from native\n  clients per the TCP+TLS path.\n\n### Per-server concurrency\n\nImplementations that talk to more than one ElectrumX server in parallel\nMUST NOT serialize all lookups behind a single mutex; a slow or stuck\nserver would otherwise block requests to healthy ones. A per-server\nmutex (or a connection pool keyed by server) is RECOMMENDED.\n\n## Expiry\n\nNamecoin names expire 36,000 blocks (~250 days) after their last\n`NAME_UPDATE` transaction. An implementation MUST treat an expired\nname as if it did not exist. Specifically:\n\n1. Compute `currentHeight` from `blockchain.headers.subscribe` (or\n   the equivalent on the chosen transport).\n2. Compute `nameHeight` from the latest `NAME_UPDATE` transaction's\n   confirmation height. Unconfirmed transactions (height ≤ 0) are\n   treated as active.\n3. If `currentHeight - nameHeight ≥ 36000`, treat the lookup as a\n   failure (`null` / `NotFound`).\n4. Otherwise, populate an `expiresIn` field (block count remaining)\n   for downstream UI use.\n\nImplementations SHOULD surface \"expired\" as a distinct UI state so\nthat users can prompt the publisher to renew, rather than silently\ntreating the identity as never-published.\n\n## Caching\n\nImplementations SHOULD cache positive lookup results with a TTL on\nthe order of an hour, and negative results with a shorter TTL (e.g.\n5 minutes) to bound poisoning windows.\n\nA cache entry MUST be invalidated when:\n\n- The cached name is observed to be expired.\n- The user explicitly refreshes a profile.\n- The user changes the configured ElectrumX server set.\n\n## Race-condition guidance\n\nNIP-05 lookups are typically driven by user typing. The result for an\nin-progress query (`d/test`) MUST NOT overwrite the result of a later\nquery (`d/testls`) just because it returned faster. Implementations\nSHOULD use a \"latest wins\" combinator (Kotlin `mapLatest`, RxJS\n`switchMap`, etc.) and explicitly re-throw / propagate cancellation\nexceptions before any generic `catch`.\n\n## Tor\n\nWhen the user's Tor preference for NIP-05 verification is enabled,\nthe resolver SHOULD:\n\n- Route ElectrumX TCP via the configured SOCKS5 proxy.\n- Prefer `.onion` ElectrumX servers when available.\n- Fall back to clearnet servers via Tor (rather than direct) so the\n  resolver still works when no `.onion` server is reachable.\n\nThis MUST NOT exempt the resolver from the §\"Lookup transport\"\nauthenticity rule. Self-signed `.onion` ElectrumX servers MUST still\nhave their TLS certificate fingerprint pinned.\n\n## Signer-side enforcement (optional)\n\nThis NIP defines a reader-side verification path: a client receives an\nevent and checks the `nip05` claim against the Namecoin record. A\nsigner MAY additionally enforce the same check **before** signing a\n`kind:0` event whose `nip05` value ends in `.bit`, refusing or warning\non a mismatch between the claimed identifier and the key being used\nto sign.\n\nSigner-side enforcement is OPTIONAL and orthogonal to reader-side\nverification — a signer that does not enforce produces events that\nreader-side verifiers can still verify correctly. Implementations\nthat choose to enforce SHOULD:\n\n1. Trigger the check only when the `kind:0` event's `nip05` value\n   matches one of the identifier shapes in §\"Identifier grammar\" with\n   a `.bit` right-hand side.\n2. Resolve the Namecoin record using the same transport rules in\n   §\"Lookup transport\".\n3. **Fail open on network errors.** A signer that hard-blocks signing\n   whenever ElectrumX is unreachable would be unusable on flaky\n   networks; the signer SHOULD distinguish between \"name does not\n   match\" (refuse / warn) and \"could not check\" (proceed with a\n   user-visible warning).\n4. Allow the user to override per-event, and to disable the check\n   globally.\n\nThis pattern complements reader-side verification by catching\nmisconfigurations and impersonation attempts at the source, before an\nevent reaches any relay.\n\n## Publishing\n\nA publisher with a Namecoin name and a Nostr keypair publishes a\n`kind:0` event whose `nip05` field uses any of the identifier shapes\nabove. The publisher's `kind:10002` (NIP-65) relay list SHOULD be\npublished in advance of, or together with, the `kind:0` so that\nclients verifying the NIP-05 can also discover the publisher's relays.\n\nIf the publisher uses a relay that itself enforces this NIP on every\nevent, the publishing order MUST be:\n\n1. `kind:0` with the `.bit` `nip05` value.\n2. `kind:10002` (NIP-65 relay list).\n3. The actual content events.\n\nThis ensures the gating relay accepts the content event when it\nsubsequently re-verifies the author.\n\n## Security considerations\n\n- A Namecoin name's owner controls the value, but not what consumers\n  cache. To bound poisoning windows, implementations SHOULD use TTLs\n  on the order of an hour and SHOULD invalidate cache entries when\n  their owning name expires.\n- A subdomain trap (`d/<sub>.<parent>` directly registered by a\n  squatter) MUST NOT be queried as a fallback when walking `map`.\n  Implementations MUST always go through the registered parent name,\n  never through a synthesised single-label form.\n- An ElectrumX server can lie about a non-existent name (denial of\n  service) or about a stale value (replay across blocks). Pinning\n  multiple servers and using one with SPV verification is\n  RECOMMENDED.\n- Local-part `_` falling back to \"first available entry\"\n  (§\"Root local-part fallback\") is a usability concession.\n  Implementations MAY make it configurable; security-critical\n  deployments SHOULD disable it and require an explicit `_` entry.\n- A NIP-05 identifier published in a Nostr event is signed by the\n  Nostr key. The Nostr key is therefore the security anchor; a\n  Namecoin record that points to a different pubkey is a verification\n  failure, not a hijack of the Nostr identity.\n- Records using `import` can pull fields from any name in the chain.\n  Implementations SHOULD apply the same caching and TTL policy to the\n  imported names as to the importing one.\n\n## Backwards compatibility\n\nThis NIP is strictly additive to NIP-05:\n\n- A client that does not implement this NIP sees a `.bit` `nip05`\n  value as an unverifiable NIP-05 (since `.bit` is not a valid public\n  DNS TLD, the well-known fetch will fail). No event signature\n  verification or display is affected.\n- A publisher who does not use this NIP is unaffected; the `nostr`\n  item is a new optional key in the ifa-0001 schema.\n- The two paths can coexist on the same identity. A publisher MAY\n  publish both an `alice@example.com` (DNS) and an `alice@example.bit`\n  (Namecoin) identity that resolve to the same pubkey, and a client\n  that implements both NIPs MAY accept either.\n\n## Reference implementations\n\nThis NIP has shipping implementations across six runtimes — Kotlin,\nSwift, Dart, TypeScript, vanilla JavaScript, and Haskell — spanning\nAndroid, iOS, web, desktop, and relay-side. All exercise the same\nlive Namecoin chain via overlapping ElectrumX server sets and have\nbeen verified byte-for-byte against the same on-chain records.\n\n### Reader-side (clients)\n\n| Runtime | Project | Surface | PR / repo |\n|---|---|---|---|\n| Kotlin | Amethyst | Android / KMP iOS / Desktop | [vitorpamplona/amethyst #1734](https://github.com/vitorpamplona/amethyst/pull/1734), [#1771](https://github.com/vitorpamplona/amethyst/pull/1771), [#1786](https://github.com/vitorpamplona/amethyst/pull/1786), [#1937](https://github.com/vitorpamplona/amethyst/pull/1937) |\n| Swift | Nostur | iOS | [nostur-com/nostur-ios-public #60](https://github.com/nostur-com/nostur-ios-public/pull/60) |\n| Swift | nos | iOS (Planetary) | [planetary-social/nos #1779](https://github.com/planetary-social/nos/pull/1779) |\n| Dart | dart-nostr | Flutter library | [ethicnology/dart-nostr #44](https://github.com/ethicnology/dart-nostr/pull/44) (merged) |\n| Dart | 0xchat | Flutter, Android+iOS+Web | [0xchat-app/0xchat-app-main #65](https://github.com/0xchat-app/0xchat-app-main/pull/65) |\n| Dart | nostrmo | Flutter cross-platform | [haorendashu/nostrmo #33](https://github.com/haorendashu/nostrmo/pull/33) |\n| TypeScript | nostr-tools | Library (isomorphic) | [nbd-wtf/nostr-tools #533](https://github.com/nbd-wtf/nostr-tools/pull/533) |\n| TypeScript | noStrudel | Web client | [hzrd149/nostrudel #352](https://github.com/hzrd149/nostrudel/pull/352) |\n| TypeScript | Jumble | Web client (browser `wss://`) | [CodyTseng/jumble #774](https://github.com/CodyTseng/jumble/pull/774) |\n| TypeScript | nostter | SvelteKit web | [SnowCait/nostter #2128](https://github.com/SnowCait/nostter/pull/2128) |\n| TypeScript | lumilumi | SvelteKit web | [TsukemonoGit/lumilumi #1037](https://github.com/TsukemonoGit/lumilumi/pull/1037) |\n| TypeScript | nosotros | Web client | [cesardeazevedo/nosotros #205](https://github.com/cesardeazevedo/nosotros/pull/205) |\n| TypeScript | ants | Search / Next.js | [dergigi/ants #281](https://github.com/dergigi/ants/pull/281) |\n| Vanilla JS | alphaama | Web (no build step) | [eskema/alphaama #9](https://github.com/eskema/alphaama/pull/9) |\n| Haskell | futr | Desktop (Qt5) | [futrnostr/futr #162](https://github.com/futrnostr/futr/pull/162) |\n\n### Signer-side\n\n| Runtime | Project | Surface | PR / repo |\n|---|---|---|---|\n| Dart | Aegis | Flutter cross-platform signer (NIP-46 / NIP-07 / NIP-55) | [ZharlieW/Aegis #14](https://github.com/ZharlieW/Aegis/pull/14) |\n\n### Server-side / library\n\n- **strfry NIP-05 sidecar** (relay-side, Rust + Node):\n  https://github.com/mstrofnone/strfry-nip05-namecoin.\n- **nostrlib** (Go): drop-in shape at\n  https://github.com/mstrofnone/nostrlib-nip05-namecoin, per\n  [`nak#123`](https://github.com/fiatjaf/nak/pull/123) discussion.\n\n### Live reference deployment\n\n`testls.bit` / `relay.testls.bit` (a public `.bit` ElectrumX server\nplus a `.bit`-gated relay) is exercised by the test suites of every\nimplementation above. The associated identity\n`mstrofnone@testls.bit` is the canonical end-to-end test target for\nFIRSTUPDATE-shaped records (see §\"Opcode handling\").\n\n## Out of scope for this NIP\n\n- **Relay discovery via Namecoin** (subdomain walking through `map`,\n  Tor routing through ifa-0002 `tor`). Tracked separately and\n  intended as a follow-up NIP; the `nostr.relays` field defined in\n  §\"Per-pubkey relays\" is the only relay-related field specified\n  here.\n- **TLSA pinning of `.bit` relay WebSockets**. Belongs with relay\n  discovery; tracked separately.\n- **Service attestations** (binding higher-kind events to a Namecoin\n  name at a specific block height). Builds on this NIP but does not\n  affect it.\n\nSee https://github.com/nostr-protocol/nips/issues/2330 for the\nbroader track and links to the companion drafts.\n","sig":"e6f53a0b89097993e18b48e9aca4a81b33c7b0e218260cbedd25e678cdb0af3b526027c4cc59c4c613957adc7317bdcff81d4291187daedab6a2208732d17738"}