Files
coracle-rust/book/research/addresses.md
T
2026-04-17 16:43:56 -07:00

6.2 KiB

Research: Addresses

Topic Summary

Create an Address struct representing a nostr event address (kind:pubkey:d-tag). It should:

  • Be constructable from an event via from_event
  • Encode/decode to the naddr NIP-19 bech32 format (TLV-based)
  • Encode/decode to the kind:pubkey:identifier string format
  • Provide convenient property access (kind, pubkey, identifier)

The reader already understands kind ranges and addressable events from the kinds chapter.

Philosophy

From ref/building-nostr:

  • Events are content-addressable via their ID (hash), giving referential transparency. But replaceability broke this — a replaceable event's ID changes on every update, so you need a stable reference.
  • An "address" is kind:pubkey:identifier, pointing to all events sharing the same kind, pubkey, and d-tag.
  • The a tag carries this triple as its value, with an optional relay hint in position 2.
  • Design principle: "you should have a slight bias towards regular, non-replaceable events because they don't break referential transparency."
  • The address includes the author's pubkey, which enables outbox-model relay lookup — unlike a bare event ID.

Reference Implementation Analysis

applesauce

  • No custom Address struct — re-exports AddressPointer from nostr-tools: { kind, pubkey, identifier, relays? }
  • Also has AddressPointerWithoutD for replaceable (non-addressable) events
  • createReplaceableAddress(kind, pubkey, identifier?) concatenates with :
  • parseReplaceableAddress(str) splits on :, handles identifiers containing colons by joining remaining parts
  • getAddressPointerForEvent(event, relays?) extracts d-tag: event.tags.find(t => t[0] === "d")?.[1] ?? ""
  • Delegates naddr encoding to nostr-tools

ndk

  • No dedicated Address type — handles addresses on NDKEvent directly
  • tagAddress() returns "${kind}:${pubkey}:${dTag ?? ""}"
  • dTag getter/setter on NDKEvent
  • Auto-generates d-tag for param replaceable events if missing (random string)
  • filterFromId(id) handles three input forms: raw address string, naddr bech32, or event ID
  • Regex: NIP33_A_REGEX = /^(\d+):([0-9A-Fa-f]+)(?::(.*))?$/
  • Delegates nip19 to nostr-tools

nostr-gadgets

  • No dedicated Address type — addresses are plain kind:pubkey:d-tag strings
  • isATag(input) validates via regex: /^\d+:[0-9a-f]{64}:[^:]+$/
  • Imports AddressPointer from nostr-tools when needed
  • Inline parsing by splitting on :
  • d-tag defaults to empty string

nostrlib (Go)

  • Type: EntityPointer { PublicKey, Kind, Identifier, Relays []string }
  • AsTagReference(): fmt.Sprintf("%d:%s:%s", ep.Kind, ep.PublicKey.Hex(), ep.Identifier)
  • ParseAddrString(addr): splits on : (max 3 parts)
  • Tags.GetD() returns first d tag value or ""
  • MatchesEvent checks kind + pubkey + identifier
  • NIP-19 TLV: identifier=TLV0, relays=TLV1, author=TLV2(32 bytes), kind=TLV3(u32 BE)
  • No separate from_event — caller assembles EntityPointer

nostr-tools

  • AddressPointer = { identifier: string, pubkey: string, kind: number, relays?: string[] }
  • naddr TLV: 0=identifier(UTF-8), 1=relay(multiple), 2=pubkey(32 bytes), 3=kind(u32 BE)
  • Strict decode: TLV 0, 2, 3 required; relays default to []
  • a tag parsed as tag[1].split(':')

rust-nostr

  • Coordinate { kind: Kind, public_key: PublicKey, identifier: String }
  • Nip19Coordinate { coordinate: Coordinate, relays: Vec<RelayUrl> } (separate from core Coordinate)
  • from_kpi_format(str) parses kind:pubkey:d-tag via split on :
  • parse(str) tries kpi format, then bech32, then NIP-21 URI
  • Display outputs kind:pubkey:identifier
  • FromStr delegates to parse()
  • verify() validates: replaceable must have empty identifier, addressable must have non-empty
  • TLV encode: SPECIAL(0)=identifier, AUTHOR(2)=32-byte pubkey, KIND(3)=u32 BE, RELAY(1)=URL strings
  • TLV decode: while loop consuming bytes, first occurrence wins for each field type
  • Coordinate converts Into<Tag> and Into<Filter>
  • CoordinateBorrow<'a> for zero-copy operations

welshman

  • Address class with kind, pubkey, identifier, relays
  • Address.from(str, relays?) parses kind:pubkey:identifier via regex
  • Address.fromNaddr(naddr) decodes via nostr-tools
  • Address.fromEvent(event, relays?) extracts d-tag: event.tags.find(t => t[0] === "d")?.[1] || ""
  • toString() returns kind:pubkey:identifier
  • toNaddr() delegates to nostr-tools
  • getAddress(event) shorthand
  • Address.isAddress(str) regex test

Common Patterns

  1. Three fields: kind (u16), pubkey (32 bytes), identifier/d-tag (string). All implementations agree.
  2. String format: kind:pubkey:identifier — universal, colon-separated, kind is decimal.
  3. NIP-19 TLV: identifier=type 0, relay=type 1, author=type 2 (32 bytes), kind=type 3 (u32 big-endian).
  4. d-tag defaults to empty string, not None/null — important for replaceable events that have no d-tag.
  5. from_event pattern: extract d-tag from tags.find("d")?.value() ?? "", combine with event.kind and event.pubkey.
  6. Relay hints are separate: the core address is just the triple; relays are transport metadata. Most implementations keep them optional/separate.
  7. Parsing colon-split: most implementations just split on : and take 3 parts. Some handle identifiers containing colons by joining remaining parts.

Considerations for Our Implementation

  • Keep the core Address struct to the three fields (kind, pubkey, identifier). Relay hints can be added later as a wrapper or in the naddr encoding methods.
  • from_event should work on our existing Event type, using tags.value("d") which already exists.
  • NIP-19 TLV encoding is new infrastructure — the keys chapter only used simple bech32 (no TLV). This chapter introduces the TLV pattern that will be reused for nprofile and nevent later.
  • Display/FromStr should use the kind:pubkey:identifier format (the a tag value), since that's the most common string representation.
  • Unlike rust-nostr, we don't need to validate that addressable kinds have non-empty identifiers — an address is just a reference triple, and empty identifiers are valid for replaceable events.
  • The identifier field should handle colons in d-tags correctly during parsing.