feat: opt-in Namecoin (.bit) name whitelist via ElectrumX resolution#126
Draft
mstrofnone wants to merge 2 commits into
Draft
feat: opt-in Namecoin (.bit) name whitelist via ElectrumX resolution#126mstrofnone wants to merge 2 commits into
mstrofnone wants to merge 2 commits into
Conversation
Adds a new optional WHITELISTED_NAMECOIN_NAMES_FILE env. On startup, each .bit / d/ / id/ identifier in the file is resolved to its underlying Nostr pubkey via public Namecoin ElectrumX servers and merged into the existing in-memory whitelist (alongside any pubkeys loaded from WHITELISTED_NPUBS_FILE). - new pkg/nip05namecoin: parser, ElectrumX client (TCP/TCP+TLS, pinned-cert trust store), name-index script + scripthash, and the whitelist resolver helper. Unit tests cover the parser, script encoding, scripthash derivation (incl. the d/testls reference value), pinned-cert parse, and the resolver helper with a mock ElectrumX backend. - config.go: load names file via nip05namecoin.LoadNamesFile, then call nip05namecoin.ResolveNamesToPubkeys after the existing whitelist is built. Resolution failures log a warning and continue so an operator can bring their pod online later. - .env.example, README.md, whitelisted_namecoin_names.example.json: documentation and example config. Spec: nostr-protocol/nips#2349
The 520-byte per-name limit on Namecoin makes apex records (d/<name>)
crowded enough that real-world deployments delegate shared blocks
(like nostr.names) into a sibling name via an "import" key on the
JSON value. Without import-chain handling, a NIP-05 lookup against
the canonical demo target testls.bit silently fails: the resolver
sees the apex value, finds no nostr field, and returns null — never
consulting the imported sibling dd/testls that actually carries
the names block.
Adds pkg/nip05namecoin/import.go implementing expandImports per
ifa-0001 §"import":
- four shorthand forms accepted (bare string, single-arr,
pair-arr-with-selector, canonical array-of-arrays)
- importer-wins merge with JSON-null semantic suppression
- recursive descent into nested objects on merge
- selector walk on the imported value's map tree using
exact > "*" wildcard > "" default per label (DNS rightmost-first)
- default depth budget of 4 (spec minimum); deeper chains are
silently truncated and the importer's own fields still apply
- (name|selector) visited-set cycle protection scoped to one
top-level expansion
- lenient I/O: missing/malformed/panicking lookup is treated as
{} so transient ElectrumX hiccups can't nuke an otherwise
resolvable record
Wires the expansion into extractNostrFromValue, and refactors the
QueryIdentifier path so the same code can be exercised hermetically
in tests via a queryIdentifierWithLookup helper that takes an
in-memory lookup. Records without an import key short-circuit and
issue exactly one ElectrumX query (regression guard test included).
Adds 22 tests covering the 20 distinct behaviours pinned by the
reference suite: passthrough, all four shorthand forms, canonical
array, importer-wins, null suppression, depth-4 happy path,
budget-truncation, missing/malformed/panicking lookup, malformed
import value, cycle, multi-label DNS-order selector walk,
wildcard fallback, plus six integration tests through
queryIdentifierWithLookup (bare resolution across import, named
local-part across import, zero-extra-I/O guard, importer-wins
on nostr.names, failed-import-doesn't-break-local-names, and
import-target-lacks-nostr error surfacing).
Spec: https://github.com/namecoin/proposals/blob/master/ifa-0001.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Sovereign-relay operators increasingly want to publish (and accept) chain-anchored identities that don't depend on DNS. Namecoin (
.bit) names are owner-controlled, on-chain, and survive registrar/DNS-provider failure modes. Letting HAVEN whitelist by.bitname means operators can hand outme@example.bitto friends and family and have HAVEN figure out the underlying npub at startup, without anyone having to copy hex around.What
A small, opt-in addition: a new env var
WHITELISTED_NAMECOIN_NAMES_FILEpointing at a JSON array of Namecoin identifiers (me@me.bit,d/example,id/alice, …). At startup, each name is resolved to its underlying Nostr pubkey via public Namecoin ElectrumX servers and merged into the same in-memory whitelist already populated fromWHITELISTED_NPUBS_FILE.pkg/nip05namecoin/subpackage: identifier parser, ElectrumX TCP/TCP+TLS client with a pinned-cert trust store (no WebSocket — HAVEN runs server-side, raw TCP is always available), Namecoin name-index script + scripthash derivation, and the startup-resolver helper.config.goextension: aWhitelistedNamecoinNamesfield, agetNamesFromFilehelper that mirrorsgetNpubsFromFile, and a single call tonip05namecoin.ResolveNamesToPubkeysafter the existing whitelist is built..env.example,README.md, andwhitelisted_namecoin_names.example.jsonget documentation/example entries.No changes to any relay policy. Once resolution finishes, every existing access-control code path (
MustBeWhitelistedToPost,MustBeWhitelistedToQuery, etc.) keys off the sameWhitelistedPubKeysmap and Just Works.Blast radius
WHITELISTED_NAMECOIN_NAMES_FILEsee zero behavioural change — the resolver is never called.nostrfield on-chain is missing, HAVEN logs a warning and keeps starting. The operator can fix the upstream issue and restart at their leisure.nbd-wtf/go-nostr(already imported) and the Go standard library.Spec reference
nostr-protocol/nips#2349— Namecoin extension to NIP-05.rust-nostr/nostr#1367(Rust) and the Kotlin Amethyst port (used as the source of the pinned cert bundle and scripthash test vector).Trust model
pkg/nip05namecoin/servers.go. Operators who prefer to point HAVEN at a local Namecoin daemon's ElectrumX bridge or a private mirror can patch this list in a fork; we're happy to add a runtime override env var if maintainers want one.Live verification
The
d/testlsname-index scripthash that public Namecoin ElectrumX servers expose for the long-standing test name isb519574e96740a4b3627674a0708e71a73e654a95117fc828b8e177a0579ab42— there's a unit test (TestElectrumScriptHashDTestLS) asserting our script + scripthash code reproduces it, which is the same fingerprint Amethyst and the Rust reference implementation use.Maintenance status caveat
I noticed the README banner says HAVEN is in maintenance mode (bug fixes only). This PR is deliberately framed as a low-impact additive opt-in — no surface-area expansion in policies, no new mandatory dependencies, and zero behaviour change for existing operators. If you'd rather not take any feature work right now, I completely understand; happy to scope this down further (e.g. ship the package without the config wiring), park it as a fork, or close.
Testing
Unit tests cover: identifier parsing (incl. NIP-21
nostr:prefix, mixed case,@shape,d//id/shape, junk inputs),nostrvalue extraction (simple form, extendednames+relaysform,id/form, fallback-to-root, invalid pubkey rejection), the name-index script encoding, the scripthash for both empty input and thed/testlsreference value, OP_PUSHDATA1/2/4 round-trips, NAME_UPDATE script parsing, pinned cert PEM parse,LoadNamesFile(empty/parses/malformed/missing), andResolveNamesToPubkeyswith a mock resolver (success+failure mix, empty pubkey ignored, nil-target safety, no-names short-circuit).Network-touching integration tests are guarded behind
HAVEN_NAMECOIN_INTEGRATION(none shipped in this PR — happy to add some if you'd like).Update: import-chain support (ifa-0001 §"import")
A second commit on this branch adds support for the ifa-0001 §"import" chain. Real-world Namecoin records — including the long-running
testls.bitdemo target — push up against the 520-byte per-name limit and split theirnostr.namesblock into a sibling name (typicallydd/<name>) via an"import"key on the apex JSON value. Without this, NIP-05 lookups against those records silently fail: the resolver sees the apex value, finds nonostrfield, and returns null without ever consulting the imported sibling.pkg/nip05namecoin/import.goadds anexpandImportspass that runs once between "fetched the apex value" and "extract thenostrfield". Behaviour follows the spec:[name],[name, selector], canonical[[name, sel], ...]).maptree (exact > "*" > ""per label, DNS rightmost-first).(name|selector)visited-set cycle protection scoped to one top-level expansion.{}so transient ElectrumX hiccups don't nuke an otherwise resolvable record.Zero blast radius for non-import records. A regression guard test pins that records without an
"import"key issue exactly one ElectrumX query (the apex name) and never consult any sibling.Test count added: 22 (16 unit + 6 integration). Reference suite parity: all 20 distinct behaviours from the Kotlin reference (
NamecoinImportTest.kt) are covered; some closely-related cases are merged where natural for Go.