# Minimal Nostr Go Library - Implementation Plan ## Overview Build a minimal Go library for Nostr split into two modules: **Module 1: Core** (`nostr-go` root) - 1 external dep - Types, signing, serialization - `github.com/btcsuite/btcd/btcec/v2` - BIP-340 Schnorr signatures **Module 2: Relay** (`nostr-go/relay`) - 1 additional dep - WebSocket connection, pub/sub - `github.com/coder/websocket` - WebSocket library - Imports core module Users who only need types/signing don't pull in websocket dependencies. ## Package Structure ``` nostr-go/ ├── go.mod # Core module ├── event.go # Event struct, ID computation, serialization ├── tags.go # Tag/Tags types and helpers ├── kinds.go # Event kind constants ├── filter.go # Filter struct and matching logic ├── keys.go # Key generation, signing, verification ├── bech32.go # Bech32 encoding/decoding (our impl, ~150 lines) ├── nip19.go # npub/nsec/note/nprofile encode/decode ├── envelope.go # Protocol messages (EVENT, REQ, OK, etc.) ├── *_test.go │ └── relay/ ├── go.mod # Relay module (imports core) ├── relay.go # WebSocket connection primitives ├── subscription.go # Subscription handling └── *_test.go ``` ## Core Types ### Event (event.go) ```go type Event struct { ID string `json:"id"` // 64-char hex (SHA256) PubKey string `json:"pubkey"` // 64-char hex (x-only pubkey) CreatedAt int64 `json:"created_at"` Kind int `json:"kind"` Tags Tags `json:"tags"` Content string `json:"content"` Sig string `json:"sig"` // 128-char hex (Schnorr sig) } ``` **Design note**: Starting with hex strings for simplicity. Can evaluate byte arrays (`[32]byte`, `[64]byte`) later if type safety becomes important. Key methods: - `Serialize() []byte` - Canonical JSON for ID computation: `[0,"pubkey",created_at,kind,tags,"content"]` - `ComputeID() string` - SHA256 hash of serialized form - `Sign(privKeyHex string) error` - Sign with Schnorr, sets PubKey/ID/Sig - `Verify() bool` - Verify signature ### Tags (tags.go) ```go type Tag []string type Tags []Tag ``` Methods: `Key()`, `Value()`, `Find(key)`, `FindAll(key)`, `GetD()` ### Filter (filter.go) ```go type Filter struct { IDs []string `json:"ids,omitempty"` Kinds []int `json:"kinds,omitempty"` Authors []string `json:"authors,omitempty"` Tags map[string][]string `json:"-"` // Custom marshal for #e, #p Since *int64 `json:"since,omitempty"` Until *int64 `json:"until,omitempty"` Limit int `json:"limit,omitempty"` } ``` Methods: `Matches(event) bool`, custom `MarshalJSON`/`UnmarshalJSON` for tag filters ### Kinds (kinds.go) Essential constants only: ```go const ( KindMetadata = 0 KindTextNote = 1 KindContactList = 3 KindEncryptedDM = 4 KindDeletion = 5 KindRepost = 6 KindReaction = 7 ) ``` Helpers: `IsRegular()`, `IsReplaceable()`, `IsEphemeral()`, `IsAddressable()` ### Envelopes (envelope.go) Protocol messages as types with `Label()` and `MarshalJSON()`: - Client→Relay: `EventEnvelope`, `ReqEnvelope`, `CloseEnvelope` - Relay→Client: `EventEnvelope`, `OKEnvelope`, `EOSEEnvelope`, `ClosedEnvelope`, `NoticeEnvelope` - `ParseEnvelope(data []byte) (Envelope, error)` ## Keys & Signing (keys.go) Using `github.com/btcsuite/btcd/btcec/v2/schnorr`: ```go func GenerateKey() (string, error) func GetPublicKey(privKeyHex string) (string, error) func (e *Event) Sign(privKeyHex string) error func (e *Event) Verify() bool ``` ## NIP-19 Encoding (nip19.go) Bech32 encoding for human-readable identifiers: ```go func EncodePublicKey(pubKeyHex string) (string, error) // -> npub1... func EncodeSecretKey(secKeyHex string) (string, error) // -> nsec1... func EncodeNote(eventID string) (string, error) // -> note1... func DecodePublicKey(npub string) (string, error) // npub1... -> hex func DecodeSecretKey(nsec string) (string, error) // nsec1... -> hex func DecodeNote(note string) (string, error) // note1... -> hex // TLV-encoded types (nprofile, nevent, naddr) can be added later ``` ## WebSocket Primitives (relay.go) Simple design - no complex goroutine orchestration: ```go type Relay struct { URL string conn *websocket.Conn mu sync.Mutex } func Connect(ctx context.Context, url string) (*Relay, error) func (r *Relay) Close() error func (r *Relay) Send(ctx context.Context, env Envelope) error func (r *Relay) Receive(ctx context.Context) (Envelope, error) func (r *Relay) Publish(ctx context.Context, event *Event) error func (r *Relay) Subscribe(ctx context.Context, id string, filters ...Filter) (*Subscription, error) type Subscription struct { ID string Events chan *Event EOSE chan struct{} } func (s *Subscription) Listen() error func (s *Subscription) Close() error ``` ## Implementation Order ### Phase 1: Core Module (nostr-go) 1. **go.mod** - Module definition with btcec/v2 dependency 2. **event.go, tags.go, kinds.go** - Core types, serialization, ID computation 3. **keys.go** - Schnorr signing with btcec/v2 4. **bech32.go** - Bech32 encode/decode (~150 lines) 5. **nip19.go** - npub/nsec/note encoding 6. **filter.go** - Filter struct with custom JSON and matching 7. **envelope.go** - All envelope types and ParseEnvelope 8. **Core tests** ### Phase 2: Relay Module (nostr-go/relay) 1. **relay/go.mod** - Module definition with websocket dep, imports core 2. **relay/relay.go** - WebSocket connection primitives 3. **relay/subscription.go** - Subscription handling 4. **Relay tests** ## What's Omitted (v0.1) - NIP-42 AUTH - NIP-04 encrypted DMs - Connection pooling / relay pool - Automatic reconnection - Advanced kinds (10000+) ## Verification 1. Unit tests for each module 2. Integration test: connect to `wss://relay.damus.io`, publish event, subscribe 3. Verify signature interop with existing Nostr clients/libraries