diff options
Diffstat (limited to 'PLAN.md')
| -rw-r--r-- | PLAN.md | 186 |
1 files changed, 186 insertions, 0 deletions
| @@ -0,0 +1,186 @@ | |||
| 1 | # Minimal Nostr Go Library - Implementation Plan | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | |||
| 5 | Build a minimal Go library for Nostr split into two modules: | ||
| 6 | |||
| 7 | **Module 1: Core** (`nostr-go` root) - 1 external dep | ||
| 8 | - Types, signing, serialization | ||
| 9 | - `github.com/btcsuite/btcd/btcec/v2` - BIP-340 Schnorr signatures | ||
| 10 | |||
| 11 | **Module 2: Relay** (`nostr-go/relay`) - 1 additional dep | ||
| 12 | - WebSocket connection, pub/sub | ||
| 13 | - `github.com/coder/websocket` - WebSocket library | ||
| 14 | - Imports core module | ||
| 15 | |||
| 16 | Users who only need types/signing don't pull in websocket dependencies. | ||
| 17 | |||
| 18 | ## Package Structure | ||
| 19 | |||
| 20 | ``` | ||
| 21 | nostr-go/ | ||
| 22 | ├── go.mod # Core module | ||
| 23 | ├── event.go # Event struct, ID computation, serialization | ||
| 24 | ├── tags.go # Tag/Tags types and helpers | ||
| 25 | ├── kinds.go # Event kind constants | ||
| 26 | ├── filter.go # Filter struct and matching logic | ||
| 27 | ├── keys.go # Key generation, signing, verification | ||
| 28 | ├── bech32.go # Bech32 encoding/decoding (our impl, ~150 lines) | ||
| 29 | ├── nip19.go # npub/nsec/note/nprofile encode/decode | ||
| 30 | ├── envelope.go # Protocol messages (EVENT, REQ, OK, etc.) | ||
| 31 | ├── *_test.go | ||
| 32 | │ | ||
| 33 | └── relay/ | ||
| 34 | ├── go.mod # Relay module (imports core) | ||
| 35 | ├── relay.go # WebSocket connection primitives | ||
| 36 | ├── subscription.go # Subscription handling | ||
| 37 | └── *_test.go | ||
| 38 | ``` | ||
| 39 | |||
| 40 | ## Core Types | ||
| 41 | |||
| 42 | ### Event (event.go) | ||
| 43 | ```go | ||
| 44 | type Event struct { | ||
| 45 | ID string `json:"id"` // 64-char hex (SHA256) | ||
| 46 | PubKey string `json:"pubkey"` // 64-char hex (x-only pubkey) | ||
| 47 | CreatedAt int64 `json:"created_at"` | ||
| 48 | Kind int `json:"kind"` | ||
| 49 | Tags Tags `json:"tags"` | ||
| 50 | Content string `json:"content"` | ||
| 51 | Sig string `json:"sig"` // 128-char hex (Schnorr sig) | ||
| 52 | } | ||
| 53 | ``` | ||
| 54 | |||
| 55 | **Design note**: Starting with hex strings for simplicity. Can evaluate byte arrays (`[32]byte`, `[64]byte`) later if type safety becomes important. | ||
| 56 | |||
| 57 | Key methods: | ||
| 58 | - `Serialize() []byte` - Canonical JSON for ID computation: `[0,"pubkey",created_at,kind,tags,"content"]` | ||
| 59 | - `ComputeID() string` - SHA256 hash of serialized form | ||
| 60 | - `Sign(privKeyHex string) error` - Sign with Schnorr, sets PubKey/ID/Sig | ||
| 61 | - `Verify() bool` - Verify signature | ||
| 62 | |||
| 63 | ### Tags (tags.go) | ||
| 64 | ```go | ||
| 65 | type Tag []string | ||
| 66 | type Tags []Tag | ||
| 67 | ``` | ||
| 68 | Methods: `Key()`, `Value()`, `Find(key)`, `FindAll(key)`, `GetD()` | ||
| 69 | |||
| 70 | ### Filter (filter.go) | ||
| 71 | ```go | ||
| 72 | type Filter struct { | ||
| 73 | IDs []string `json:"ids,omitempty"` | ||
| 74 | Kinds []int `json:"kinds,omitempty"` | ||
| 75 | Authors []string `json:"authors,omitempty"` | ||
| 76 | Tags map[string][]string `json:"-"` // Custom marshal for #e, #p | ||
| 77 | Since *int64 `json:"since,omitempty"` | ||
| 78 | Until *int64 `json:"until,omitempty"` | ||
| 79 | Limit int `json:"limit,omitempty"` | ||
| 80 | } | ||
| 81 | ``` | ||
| 82 | Methods: `Matches(event) bool`, custom `MarshalJSON`/`UnmarshalJSON` for tag filters | ||
| 83 | |||
| 84 | ### Kinds (kinds.go) | ||
| 85 | Essential constants only: | ||
| 86 | ```go | ||
| 87 | const ( | ||
| 88 | KindMetadata = 0 | ||
| 89 | KindTextNote = 1 | ||
| 90 | KindContactList = 3 | ||
| 91 | KindEncryptedDM = 4 | ||
| 92 | KindDeletion = 5 | ||
| 93 | KindRepost = 6 | ||
| 94 | KindReaction = 7 | ||
| 95 | ) | ||
| 96 | ``` | ||
| 97 | Helpers: `IsRegular()`, `IsReplaceable()`, `IsEphemeral()`, `IsAddressable()` | ||
| 98 | |||
| 99 | ### Envelopes (envelope.go) | ||
| 100 | Protocol messages as types with `Label()` and `MarshalJSON()`: | ||
| 101 | - Client→Relay: `EventEnvelope`, `ReqEnvelope`, `CloseEnvelope` | ||
| 102 | - Relay→Client: `EventEnvelope`, `OKEnvelope`, `EOSEEnvelope`, `ClosedEnvelope`, `NoticeEnvelope` | ||
| 103 | - `ParseEnvelope(data []byte) (Envelope, error)` | ||
| 104 | |||
| 105 | ## Keys & Signing (keys.go) | ||
| 106 | |||
| 107 | Using `github.com/btcsuite/btcd/btcec/v2/schnorr`: | ||
| 108 | ```go | ||
| 109 | func GenerateKey() (string, error) | ||
| 110 | func GetPublicKey(privKeyHex string) (string, error) | ||
| 111 | func (e *Event) Sign(privKeyHex string) error | ||
| 112 | func (e *Event) Verify() bool | ||
| 113 | ``` | ||
| 114 | |||
| 115 | ## NIP-19 Encoding (nip19.go) | ||
| 116 | |||
| 117 | Bech32 encoding for human-readable identifiers: | ||
| 118 | ```go | ||
| 119 | func EncodePublicKey(pubKeyHex string) (string, error) // -> npub1... | ||
| 120 | func EncodeSecretKey(secKeyHex string) (string, error) // -> nsec1... | ||
| 121 | func EncodeNote(eventID string) (string, error) // -> note1... | ||
| 122 | |||
| 123 | func DecodePublicKey(npub string) (string, error) // npub1... -> hex | ||
| 124 | func DecodeSecretKey(nsec string) (string, error) // nsec1... -> hex | ||
| 125 | func DecodeNote(note string) (string, error) // note1... -> hex | ||
| 126 | |||
| 127 | // TLV-encoded types (nprofile, nevent, naddr) can be added later | ||
| 128 | ``` | ||
| 129 | |||
| 130 | ## WebSocket Primitives (relay.go) | ||
| 131 | |||
| 132 | Simple design - no complex goroutine orchestration: | ||
| 133 | ```go | ||
| 134 | type Relay struct { | ||
| 135 | URL string | ||
| 136 | conn *websocket.Conn | ||
| 137 | mu sync.Mutex | ||
| 138 | } | ||
| 139 | |||
| 140 | func Connect(ctx context.Context, url string) (*Relay, error) | ||
| 141 | func (r *Relay) Close() error | ||
| 142 | func (r *Relay) Send(ctx context.Context, env Envelope) error | ||
| 143 | func (r *Relay) Receive(ctx context.Context) (Envelope, error) | ||
| 144 | func (r *Relay) Publish(ctx context.Context, event *Event) error | ||
| 145 | func (r *Relay) Subscribe(ctx context.Context, id string, filters ...Filter) (*Subscription, error) | ||
| 146 | |||
| 147 | type Subscription struct { | ||
| 148 | ID string | ||
| 149 | Events chan *Event | ||
| 150 | EOSE chan struct{} | ||
| 151 | } | ||
| 152 | func (s *Subscription) Listen() error | ||
| 153 | func (s *Subscription) Close() error | ||
| 154 | ``` | ||
| 155 | |||
| 156 | ## Implementation Order | ||
| 157 | |||
| 158 | ### Phase 1: Core Module (nostr-go) | ||
| 159 | 1. **go.mod** - Module definition with btcec/v2 dependency | ||
| 160 | 2. **event.go, tags.go, kinds.go** - Core types, serialization, ID computation | ||
| 161 | 3. **keys.go** - Schnorr signing with btcec/v2 | ||
| 162 | 4. **bech32.go** - Bech32 encode/decode (~150 lines) | ||
| 163 | 5. **nip19.go** - npub/nsec/note encoding | ||
| 164 | 6. **filter.go** - Filter struct with custom JSON and matching | ||
| 165 | 7. **envelope.go** - All envelope types and ParseEnvelope | ||
| 166 | 8. **Core tests** | ||
| 167 | |||
| 168 | ### Phase 2: Relay Module (nostr-go/relay) | ||
| 169 | 1. **relay/go.mod** - Module definition with websocket dep, imports core | ||
| 170 | 2. **relay/relay.go** - WebSocket connection primitives | ||
| 171 | 3. **relay/subscription.go** - Subscription handling | ||
| 172 | 4. **Relay tests** | ||
| 173 | |||
| 174 | ## What's Omitted (v0.1) | ||
| 175 | |||
| 176 | - NIP-42 AUTH | ||
| 177 | - NIP-04 encrypted DMs | ||
| 178 | - Connection pooling / relay pool | ||
| 179 | - Automatic reconnection | ||
| 180 | - Advanced kinds (10000+) | ||
| 181 | |||
| 182 | ## Verification | ||
| 183 | |||
| 184 | 1. Unit tests for each module | ||
| 185 | 2. Integration test: connect to `wss://relay.damus.io`, publish event, subscribe | ||
| 186 | 3. Verify signature interop with existing Nostr clients/libraries | ||
