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

104 lines
6.2 KiB
Markdown

# 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.