Add addresses chapter
This commit is contained in:
@@ -133,6 +133,13 @@ impl PublicKey {
|
||||
.map_err(|_| KeyError::InvalidKey)?;
|
||||
Ok(PublicKey(inner))
|
||||
}
|
||||
|
||||
/// Parse from raw bytes (exactly 32).
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, KeyError> {
|
||||
let inner = secp256k1::XOnlyPublicKey::from_slice(bytes)
|
||||
.map_err(|_| KeyError::InvalidKey)?;
|
||||
Ok(PublicKey(inner))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+3
-3
@@ -267,6 +267,6 @@ define a struct, implement `Kind`, and the trait provides the rest.
|
||||
|
||||
## What's next
|
||||
|
||||
The next chapter looks more closely at kind ranges — the system that
|
||||
ties storage behavior to kind numbers — and what that means for relay
|
||||
queries and event addressing.
|
||||
The next chapter introduces addresses — the stable `kind:pubkey:identifier`
|
||||
references that let you point at a replaceable or addressable event without
|
||||
tying yourself to a particular version's id.
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
# Addresses
|
||||
|
||||
An event's id is a hash of its contents. Change the content, sign again, and
|
||||
you get a different id. That's fine for regular events — they're immutable,
|
||||
so the id is a permanent name. But replaceable and addressable events are
|
||||
designed to be overwritten. A user's profile (kind 0) updates every time they
|
||||
change their display name. An article (kind 30023) updates every time the
|
||||
author edits a paragraph. The old id is gone; a new one takes its place.
|
||||
|
||||
You need a way to point at the *slot* rather than the *version*. That's what
|
||||
an address is: the triple `kind:pubkey:identifier` that stays the same no
|
||||
matter how many times the event is replaced. For a replaceable event (kind
|
||||
0, 3, or 10000–19999), the identifier is empty — the relay keeps one event
|
||||
per author and kind. For an addressable event (kind 30000–39999), the
|
||||
identifier is the `d` tag's value, which partitions the kind further so
|
||||
that a single author can maintain many independent slots under the same kind.
|
||||
|
||||
The `a` tag carries this triple as its value, and the `naddr` bech32 encoding
|
||||
wraps it for sharing. This chapter builds the `Address` type that handles
|
||||
both.
|
||||
|
||||
## The module
|
||||
|
||||
```rust {file=coracle-lib/src/lib.rs}
|
||||
pub mod addresses;
|
||||
```
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
//! Event addresses: stable references to replaceable and addressable events.
|
||||
//!
|
||||
//! An [`Address`] is the triple `(kind, pubkey, identifier)` that names the
|
||||
//! slot a mutable event occupies, independent of any particular version.
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bech32::{Bech32, Hrp};
|
||||
|
||||
use crate::events::Event;
|
||||
use crate::keys::{KeyError, PublicKey};
|
||||
use crate::kinds;
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
Address operations can fail when parsing a string or decoding bech32. A
|
||||
single error type covers all the ways.
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
/// Errors that can occur when parsing or encoding an address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AddressError {
|
||||
/// The string could not be parsed as `kind:pubkey:identifier`.
|
||||
InvalidFormat,
|
||||
/// The bech32 string was malformed.
|
||||
InvalidBech32,
|
||||
/// The bech32 prefix was not `naddr`.
|
||||
WrongPrefix { found: String },
|
||||
/// The public key bytes were not a valid secp256k1 point.
|
||||
InvalidKey,
|
||||
/// A required TLV field was missing.
|
||||
FieldMissing(&'static str),
|
||||
/// The TLV data was malformed.
|
||||
TlvError,
|
||||
}
|
||||
|
||||
impl fmt::Display for AddressError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AddressError::InvalidFormat => write!(f, "invalid address format"),
|
||||
AddressError::InvalidBech32 => write!(f, "invalid bech32 encoding"),
|
||||
AddressError::WrongPrefix { found } => {
|
||||
write!(f, "wrong bech32 prefix: expected naddr, found {found}")
|
||||
}
|
||||
AddressError::InvalidKey => write!(f, "invalid public key"),
|
||||
AddressError::FieldMissing(name) => {
|
||||
write!(f, "missing required field: {name}")
|
||||
}
|
||||
AddressError::TlvError => write!(f, "malformed TLV data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AddressError {}
|
||||
|
||||
impl From<KeyError> for AddressError {
|
||||
fn from(_: KeyError) -> Self {
|
||||
AddressError::InvalidKey
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The `Address` struct
|
||||
|
||||
An address has three fields that identify the slot — `kind`, `pubkey`, and
|
||||
`identifier` — plus a `hints` field that carries relay URLs where the event
|
||||
might be found. The hints are transport metadata: they travel inside `naddr`
|
||||
bech32 strings and `a` tags but are not part of the logical identity. Two
|
||||
addresses that differ only in their hints point at the same event.
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
/// A stable reference to a replaceable or addressable event.
|
||||
///
|
||||
/// The triple `(kind, pubkey, identifier)` names the slot a mutable event
|
||||
/// occupies. Two events with the same address are versions of the same
|
||||
/// logical object — the relay keeps whichever has the later `created_at`.
|
||||
///
|
||||
/// `hints` carries relay URLs where the event might be found. These are
|
||||
/// transport metadata used when encoding to `naddr` or `a` tags, but are
|
||||
/// not part of the address identity — `PartialEq` and `Hash` ignore them.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Address {
|
||||
/// The event kind.
|
||||
pub kind: u16,
|
||||
/// The author's public key.
|
||||
pub pubkey: PublicKey,
|
||||
/// The `d` tag value. Empty for plain replaceable events.
|
||||
pub identifier: String,
|
||||
/// Relay URLs where this event might be found.
|
||||
pub hints: Vec<String>,
|
||||
}
|
||||
|
||||
impl PartialEq for Address {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.kind == other.kind
|
||||
&& self.pubkey == other.pubkey
|
||||
&& self.identifier == other.identifier
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Address {}
|
||||
|
||||
impl std::hash::Hash for Address {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.kind.hash(state);
|
||||
self.pubkey.hash(state);
|
||||
self.identifier.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Create a new address from its components.
|
||||
pub fn new(kind: u16, pubkey: PublicKey, identifier: impl Into<String>) -> Self {
|
||||
Address {
|
||||
kind,
|
||||
pubkey,
|
||||
identifier: identifier.into(),
|
||||
hints: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach relay hints to this address.
|
||||
pub fn hints<I, S>(mut self, relays: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
self.hints = relays.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`PartialEq` and `Hash` are hand-implemented to exclude `hints`. Two
|
||||
addresses that name the same slot are equal regardless of which relays
|
||||
they suggest — the hints are advice, not identity.
|
||||
|
||||
### Building from an event
|
||||
|
||||
The most common way to get an address is from an event you already have.
|
||||
For addressable events, the identifier comes from the `d` tag (defaulting
|
||||
to the empty string if absent). For plain replaceable events, the
|
||||
identifier is always empty — even if a `d` tag happens to be present, it
|
||||
has no meaning for replacement semantics, so we ignore it. If the kind is
|
||||
neither replaceable nor addressable, there is no address — the event is
|
||||
immutable and identified by its id alone.
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
impl Address {
|
||||
/// Extract the address from an event, if the event's kind is
|
||||
/// replaceable or addressable.
|
||||
///
|
||||
/// Returns `None` for regular and ephemeral events. The returned
|
||||
/// address has no relay hints — call `.hints(...)` to attach them.
|
||||
pub fn from_event(event: &Event) -> Option<Self> {
|
||||
let identifier = if kinds::is_addressable(event.kind) {
|
||||
event.tags.value("d").unwrap_or("").to_string()
|
||||
} else if kinds::is_replaceable(event.kind) {
|
||||
String::new()
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(Address {
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey,
|
||||
identifier,
|
||||
hints: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## String format
|
||||
|
||||
The canonical string form of an address is `kind:pubkey:identifier` — the
|
||||
same value that goes inside an `a` tag. `Display` writes it, `FromStr`
|
||||
parses it.
|
||||
|
||||
Parsing splits on `:` and takes the first segment as the kind, the second
|
||||
as the pubkey hex, and everything after the second colon as the identifier.
|
||||
Joining the remainder handles identifiers that themselves contain colons,
|
||||
which is unusual but legal.
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
impl fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.kind, self.pubkey, self.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Try naddr bech32 first.
|
||||
if s.starts_with("naddr1") {
|
||||
return Self::from_naddr(s);
|
||||
}
|
||||
|
||||
let mut parts = s.splitn(3, ':');
|
||||
let kind: u16 = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.ok_or(AddressError::InvalidFormat)?;
|
||||
let pubkey = parts
|
||||
.next()
|
||||
.ok_or(AddressError::InvalidFormat)
|
||||
.and_then(|s| PublicKey::from_hex(s).map_err(|_| AddressError::InvalidFormat))?;
|
||||
let identifier = parts.next().unwrap_or("").to_string();
|
||||
|
||||
Ok(Address {
|
||||
kind,
|
||||
pubkey,
|
||||
identifier,
|
||||
hints: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`FromStr` also accepts `naddr1…` strings, so callers can parse either
|
||||
format with a single `.parse()` call — the same convenience we used for
|
||||
public keys accepting both hex and `npub`.
|
||||
|
||||
## NIP-19 `naddr` encoding
|
||||
|
||||
The simple bech32 encodings we built in the keys chapter — `npub` and `nsec`
|
||||
— wrap a fixed 32-byte payload. An address carries variable-length data: the
|
||||
identifier can be any length, and there may be zero or more relay hints. NIP-19
|
||||
handles this with a **TLV** (type-length-value) scheme inside the bech32
|
||||
payload.
|
||||
|
||||
Each TLV entry is:
|
||||
|
||||
| Byte | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Type tag: what this entry represents |
|
||||
| 1 | Length: how many bytes follow |
|
||||
| 2..2+len | Value |
|
||||
|
||||
Four type tags are defined:
|
||||
|
||||
| Type | Name | Value |
|
||||
|------|------|-------|
|
||||
| 0 | special | The identifier (`d` tag), UTF-8 bytes |
|
||||
| 1 | relay | A relay URL, UTF-8 bytes |
|
||||
| 2 | author | The pubkey, 32 raw bytes |
|
||||
| 3 | kind | The kind number, 4-byte big-endian u32 |
|
||||
|
||||
An `naddr` payload concatenates one entry for the identifier, one for
|
||||
the author, one for the kind, and zero or more for relay hints. The
|
||||
order of these entries is identifier first by convention, but decoders
|
||||
should not rely on ordering.
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
const NADDR_HRP: Hrp = Hrp::parse_unchecked("naddr");
|
||||
|
||||
const TLV_SPECIAL: u8 = 0;
|
||||
const TLV_RELAY: u8 = 1;
|
||||
const TLV_AUTHOR: u8 = 2;
|
||||
const TLV_KIND: u8 = 3;
|
||||
```
|
||||
|
||||
### Encoding
|
||||
|
||||
```rust {file=coracle-lib/src/addresses.rs}
|
||||
impl Address {
|
||||
/// Encode as an `naddr1…` bech32 string per NIP-19.
|
||||
///
|
||||
/// Any relay hints on this address are embedded in the TLV payload.
|
||||
pub fn to_naddr(&self) -> String {
|
||||
let id_bytes = self.identifier.as_bytes();
|
||||
let relay_len: usize = self.hints.iter().map(|r| 2 + r.len()).sum();
|
||||
let capacity = (2 + id_bytes.len()) + (2 + 32) + (2 + 4) + relay_len;
|
||||
let mut buf = Vec::with_capacity(capacity);
|
||||
|
||||
// Identifier (TLV type 0)
|
||||
buf.push(TLV_SPECIAL);
|
||||
buf.push(id_bytes.len() as u8);
|
||||
buf.extend_from_slice(id_bytes);
|
||||
|
||||
// Relays (TLV type 1, one entry per relay)
|
||||
for relay in &self.hints {
|
||||
buf.push(TLV_RELAY);
|
||||
buf.push(relay.len() as u8);
|
||||
buf.extend_from_slice(relay.as_bytes());
|
||||
}
|
||||
|
||||
// Author (TLV type 2)
|
||||
buf.push(TLV_AUTHOR);
|
||||
buf.push(32);
|
||||
buf.extend_from_slice(&self.pubkey.as_bytes());
|
||||
|
||||
// Kind (TLV type 3, 4-byte big-endian u32)
|
||||
buf.push(TLV_KIND);
|
||||
buf.push(4);
|
||||
buf.extend_from_slice(&(self.kind as u32).to_be_bytes());
|
||||
|
||||
bech32::encode::<Bech32>(NADDR_HRP, &buf)
|
||||
.expect("naddr encoding cannot fail for well-formed TLV")
|
||||
}
|
||||
|
||||
/// Decode an `naddr1…` bech32 string into an address.
|
||||
///
|
||||
/// Any relay hints embedded in the payload are stored in the
|
||||
/// returned address's `hints` field.
|
||||
pub fn from_naddr(s: &str) -> Result<Self, AddressError> {
|
||||
let (hrp, data) = bech32::decode(s).map_err(|_| AddressError::InvalidBech32)?;
|
||||
if hrp != NADDR_HRP {
|
||||
return Err(AddressError::WrongPrefix {
|
||||
found: hrp.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut identifier: Option<String> = None;
|
||||
let mut pubkey: Option<PublicKey> = None;
|
||||
let mut kind: Option<u16> = None;
|
||||
let mut hints: Vec<String> = Vec::new();
|
||||
|
||||
let mut pos = 0;
|
||||
while pos < data.len() {
|
||||
let t = *data.get(pos).ok_or(AddressError::TlvError)?;
|
||||
let l = *data.get(pos + 1).ok_or(AddressError::TlvError)? as usize;
|
||||
let value = data
|
||||
.get(pos + 2..pos + 2 + l)
|
||||
.ok_or(AddressError::TlvError)?;
|
||||
pos += 2 + l;
|
||||
|
||||
match t {
|
||||
TLV_SPECIAL => {
|
||||
if identifier.is_none() {
|
||||
identifier = Some(
|
||||
String::from_utf8(value.to_vec())
|
||||
.map_err(|_| AddressError::TlvError)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
TLV_RELAY => {
|
||||
if let Ok(url) = String::from_utf8(value.to_vec()) {
|
||||
hints.push(url);
|
||||
}
|
||||
}
|
||||
TLV_AUTHOR => {
|
||||
if pubkey.is_none() {
|
||||
pubkey = Some(PublicKey::from_bytes(value)?);
|
||||
}
|
||||
}
|
||||
TLV_KIND => {
|
||||
if kind.is_none() {
|
||||
let bytes: [u8; 4] =
|
||||
value.try_into().map_err(|_| AddressError::TlvError)?;
|
||||
kind = Some(u32::from_be_bytes(bytes) as u16);
|
||||
}
|
||||
}
|
||||
_ => {} // Unknown TLV types are ignored for forward compatibility.
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Address {
|
||||
kind: kind.ok_or(AddressError::FieldMissing("kind"))?,
|
||||
pubkey: pubkey.ok_or(AddressError::FieldMissing("pubkey"))?,
|
||||
identifier: identifier.ok_or(AddressError::FieldMissing("identifier"))?,
|
||||
hints,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The decoder takes first-occurrence-wins for each field type, which matches
|
||||
what every other implementation does. Unknown TLV types are silently
|
||||
skipped — future NIPs may add new ones, and rejecting them would break
|
||||
forward compatibility.
|
||||
|
||||
## What's next
|
||||
|
||||
We now have a way to name mutable events that survives content changes.
|
||||
The next chapter introduces proof of work — the optional CPU cost a client
|
||||
can burn into an event id to signal effort and reduce spam.
|
||||
+37
-38
@@ -9,56 +9,55 @@
|
||||
- [Tags](04-tags.md)
|
||||
- [Events](05-events.md)
|
||||
- [Kinds](06-kinds.md)
|
||||
- [Kind Ranges](07-kind-ranges.md)
|
||||
- [Addresses](08-addresses.md)
|
||||
- [Proof of Work](09-proof-of-work.md)
|
||||
- [Filters](10-filters.md)
|
||||
- [Addresses](07-addresses.md)
|
||||
- [Proof of Work](08-proof-of-work.md)
|
||||
- [Filters](09-filters.md)
|
||||
|
||||
## Domain
|
||||
|
||||
- [Relay Selections](11-relay-selections.md)
|
||||
- [Relay Metadata](12-relay-metadata.md)
|
||||
- [Relay Membership](13-relay-membership.md)
|
||||
- [Profiles](14-profiles.md)
|
||||
- [Follows](15-follows.md)
|
||||
- [Microblogging](16-microblogging.md)
|
||||
- [Reactions](17-reactions.md)
|
||||
- [Reports](18-reports.md)
|
||||
- [Emojis](19-emojis.md)
|
||||
- [Zaps](20-zaps.md)
|
||||
- [Rooms](21-rooms.md)
|
||||
- [Relay Selections](10-relay-selections.md)
|
||||
- [Relay Metadata](11-relay-metadata.md)
|
||||
- [Relay Membership](12-relay-membership.md)
|
||||
- [Profiles](13-profiles.md)
|
||||
- [Follows](14-follows.md)
|
||||
- [Microblogging](15-microblogging.md)
|
||||
- [Reactions](16-reactions.md)
|
||||
- [Reports](17-reports.md)
|
||||
- [Emojis](18-emojis.md)
|
||||
- [Zaps](19-zaps.md)
|
||||
- [Rooms](20-rooms.md)
|
||||
|
||||
## Networking
|
||||
|
||||
- [Relay Connections](22-relay-connections.md)
|
||||
- [Relay Authentication](23-relay-authentication.md)
|
||||
- [Relay Policies](24-relay-policies.md)
|
||||
- [Server Authentication](25-server-authentication.md)
|
||||
- [Relay Management API](26-relay-management-api.md)
|
||||
- [Blossom Media Storage](27-blossom-media-storage.md)
|
||||
- [Relay Connections](21-relay-connections.md)
|
||||
- [Relay Authentication](22-relay-authentication.md)
|
||||
- [Relay Policies](23-relay-policies.md)
|
||||
- [Server Authentication](24-server-authentication.md)
|
||||
- [Relay Management API](25-relay-management-api.md)
|
||||
- [Blossom Media Storage](26-blossom-media-storage.md)
|
||||
|
||||
## Signers
|
||||
|
||||
- [Signer Interface](28-signer-interface.md)
|
||||
- [Secret Signers](29-secret-signers.md)
|
||||
- [Remote Signers](30-remote-signers.md)
|
||||
- [Android Signers](31-android-signers.md)
|
||||
- [Browser Signers](32-browser-signers.md)
|
||||
- [Signer Interface](27-signer-interface.md)
|
||||
- [Secret Signers](28-secret-signers.md)
|
||||
- [Remote Signers](29-remote-signers.md)
|
||||
- [Android Signers](30-android-signers.md)
|
||||
- [Browser Signers](31-browser-signers.md)
|
||||
|
||||
## Content
|
||||
|
||||
- [Entities](33-entities.md)
|
||||
- [Relays](34-relays.md)
|
||||
- [Rooms](35-rooms.md)
|
||||
- [Links](36-links.md)
|
||||
- [Lightning](37-lightning.md)
|
||||
- [Cashu](38-cashu.md)
|
||||
- [Emojis](39-emojis.md)
|
||||
- [Topics](40-topics.md)
|
||||
- [Code](41-code.md)
|
||||
- [Entities](32-entities.md)
|
||||
- [Relays](33-relays.md)
|
||||
- [Rooms](34-rooms.md)
|
||||
- [Links](35-links.md)
|
||||
- [Lightning](36-lightning.md)
|
||||
- [Cashu](37-cashu.md)
|
||||
- [Emojis](38-emojis.md)
|
||||
- [Topics](39-topics.md)
|
||||
- [Code](40-code.md)
|
||||
|
||||
## Storage
|
||||
|
||||
- [Event Repository](42-event-repository.md)
|
||||
- [In Memory Backend](43-in-memory-backend.md)
|
||||
- [Sqlite Backend](44-sqlite-backend.md)
|
||||
- [Event Repository](41-event-repository.md)
|
||||
- [In Memory Backend](42-in-memory-backend.md)
|
||||
- [Sqlite Backend](43-sqlite-backend.md)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Plan: Addresses
|
||||
|
||||
## Overview
|
||||
|
||||
This chapter introduces the `Address` struct — a stable reference to a replaceable or
|
||||
addressable event. Where an event ID is a hash of a specific version, an address is the
|
||||
triple `(kind, pubkey, identifier)` that names the *slot* a mutable event occupies.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Reader has already covered: keys (PublicKey, bech32), tags (Tags, d-tag access via
|
||||
`tags.value("d")`), events (Event struct), kinds (kind ranges, is_addressable,
|
||||
is_replaceable).
|
||||
|
||||
## Structure
|
||||
|
||||
### 1. Motivation (prose only)
|
||||
- Event IDs are hashes of content — they change when the content changes
|
||||
- For replaceable/addressable events, you need a stable reference
|
||||
- An address is `kind:pubkey:identifier` — it names the slot, not the version
|
||||
|
||||
### 2. The module
|
||||
- Register `pub mod addresses;` in lib.rs
|
||||
- Import dependencies: keys::PublicKey, tags::Tag, events::Event, kinds, bech32, std::fmt, std::str::FromStr
|
||||
|
||||
### 3. Error type
|
||||
- `AddressError` enum: InvalidFormat, InvalidBech32, WrongPrefix, InvalidKey, FieldMissing(&'static str), TlvError
|
||||
|
||||
### 4. The `Address` struct
|
||||
- Fields: `kind: u16`, `pubkey: PublicKey`, `identifier: String`
|
||||
- Constructor: `Address::new(kind, pubkey, identifier)`
|
||||
- `from_event(event) -> Option<Self>` — returns None for non-replaceable/non-addressable events. Uses `tags.value("d").unwrap_or("").to_string()` for identifier.
|
||||
- `to_tag(relay_hint: Option<&str>) -> Tag` — produces an `a` tag
|
||||
|
||||
### 5. Display / FromStr (kind:pubkey:identifier format)
|
||||
- `Display`: `write!(f, "{}:{}:{}", self.kind, self.pubkey, self.identifier)`
|
||||
- `FromStr`: split on `:`, take first two segments, join remainder as identifier (handles colons in d-tags)
|
||||
|
||||
### 6. NIP-19 naddr encoding
|
||||
- Introduce TLV format: type(1 byte) + length(1 byte) + value(N bytes)
|
||||
- Constants: SPECIAL=0, RELAY=1, AUTHOR=2, KIND=3
|
||||
- `to_naddr() -> String` — encode as bech32 with "naddr" prefix
|
||||
- `from_naddr(s: &str) -> Result<(Self, Vec<String>), AddressError>` — returns address + relay hints
|
||||
- TLV decode: while loop consuming bytes
|
||||
- SPECIAL=identifier (UTF-8), RELAY=URL string, AUTHOR=32-byte pubkey, KIND=u32 big-endian
|
||||
|
||||
### 7. What's next
|
||||
- Brief pointer to proof of work or filters chapter
|
||||
|
||||
## Code placement
|
||||
- All code in `coracle-lib/src/addresses.rs`
|
||||
- Module registered in `coracle-lib/src/lib.rs`
|
||||
|
||||
## Tests needed
|
||||
- `Address::new` basic construction
|
||||
- `from_event` for addressable event (returns Some)
|
||||
- `from_event` for regular event (returns None)
|
||||
- `from_event` for replaceable event (returns Some, empty identifier)
|
||||
- Display round-trip through FromStr
|
||||
- to_naddr / from_naddr round-trip
|
||||
- FromStr with identifier containing colons
|
||||
- to_tag produces correct `a` tag format
|
||||
@@ -0,0 +1,103 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user