diff options
Diffstat (limited to 'PLAN.md')
| -rw-r--r-- | PLAN.md | 110 |
1 files changed, 92 insertions, 18 deletions
| @@ -34,24 +34,98 @@ These vectors must be committed to the repo and validated by the JS client in Ph | |||
| 34 | 34 | ||
| 35 | ## Phase 2: Relay | 35 | ## Phase 2: Relay |
| 36 | 36 | ||
| 37 | - WebSocket server (stdlib `net/http` + `golang.org/x/net/websocket` or `nhooyr.io/websocket`) | 37 | The relay lives in `relay/` as a separate Go module (`axon/relay`). It references the core package via a local `replace` directive during development. The muxstr project (a Nostr relay) provides several patterns we can lift directly: custom WebSocket framing, non-blocking fanout, SQLite setup, and filter query building. |
| 38 | - Challenge/Auth handshake on connect | 38 | |
| 39 | - Allowlist: authorized pubkeys in a flat config file or SQLite table | 39 | ### Module structure |
| 40 | - Ingest pipeline: | 40 | |
| 41 | 1. Unmarshal msgpack envelope | 41 | ``` |
| 42 | 2. Verify signature using Phase 1 core | 42 | relay/ |
| 43 | 3. Reject duplicates (id PRIMARY KEY) | 43 | go.mod — module "axon/relay"; replace axon => ../ |
| 44 | 4. Reject expired job requests (check `expires_at` tag) | 44 | go.sum |
| 45 | 5. Reject events exceeding 64KB content limit | 45 | main.go — entry point, config, wiring, graceful shutdown |
| 46 | 6. Write to index + store | 46 | config.go — YAML config: listen addr, db path, allowlist |
| 47 | 7. Fanout to matching subscribers | 47 | server.go — HTTP server, WebSocket upgrade, connection lifecycle |
| 48 | - SQLite storage via `database/sql` + `mattn/go-sqlite3` or `modernc.org/sqlite` (pure Go, no CGo) | 48 | handler.go — per-connection message dispatch |
| 49 | - Subscription management: filter matching, per-connection subscription map | 49 | websocket/ |
| 50 | - Ephemeral events (kind 3000–3999): fanout only, skip storage | 50 | websocket.go — raw WebSocket framing (lifted from muxstr, zero deps) |
| 51 | - Error responses with HTTP-borrowed codes | 51 | subscription/ |
| 52 | - WebSocket keepalive: ping every 30s, close after two missed | 52 | manager.go — in-memory subscriptions, filter matching, fanout |
| 53 | 53 | storage/ | |
| 54 | **Go dependencies:** sqlite driver, websocket library. Both should be pure Go where possible. | 54 | storage.go — SQLite init, schema, WAL config |
| 55 | events.go — store, query, dedup | ||
| 56 | ``` | ||
| 57 | |||
| 58 | ### Components | ||
| 59 | |||
| 60 | **WebSocket layer** (`websocket/websocket.go`) | ||
| 61 | - Lifted from muxstr with minimal changes — handles RFC 6455 framing, ping/pong, TLS | ||
| 62 | - No external WebSocket dependency | ||
| 63 | - Relay sends ping every 30s; connections missing two consecutive pings are closed | ||
| 64 | |||
| 65 | **Connection auth** (`handler.go`) | ||
| 66 | - On connect: relay sends `Challenge { nonce: 32 random bytes }` | ||
| 67 | - Client responds: `Auth { pubkey, sig }` where `sig = ed25519.Sign(privkey, SHA256(nonce || relay_url))` | ||
| 68 | - Relay verifies sig, checks pubkey against allowlist, responds `Ok` or `Error { 401 }` + close | ||
| 69 | - All subsequent messages rejected until auth completes | ||
| 70 | |||
| 71 | **Ingest pipeline** (`handler.go` → `storage/events.go`) | ||
| 72 | 1. Unmarshal msgpack `[msg_type, payload]` frame | ||
| 73 | 2. Dispatch by `msg_type` (uint16) | ||
| 74 | 3. For `Publish`: verify sig via axon core, reject if content > 64KB, reject duplicates | ||
| 75 | 4. For job requests (kind 5000–5999): check `expires_at` tag, reject if expired | ||
| 76 | 5. Ephemeral events (kind 3000–3999): fanout only, skip storage | ||
| 77 | 6. Persist to SQLite, fanout to matching subscribers | ||
| 78 | |||
| 79 | **Subscription manager** (`subscription/manager.go`) | ||
| 80 | - Lifted from muxstr: buffered channel per subscription (cap 100), non-blocking sends (drop-on-full) | ||
| 81 | - Per-subscription goroutine streams events to the client connection | ||
| 82 | - Filter matching: ids (prefix), authors (prefix), kinds, since/until, tag filters | ||
| 83 | - Multiple filters per subscription are OR'd | ||
| 84 | |||
| 85 | **Storage** (`storage/`) | ||
| 86 | - `modernc.org/sqlite` — pure Go, no CGo | ||
| 87 | - WAL mode, 40MB cache, memory-mapped I/O | ||
| 88 | - Schema: | ||
| 89 | |||
| 90 | ```sql | ||
| 91 | CREATE TABLE events ( | ||
| 92 | id BLOB PRIMARY KEY, | ||
| 93 | pubkey BLOB NOT NULL, | ||
| 94 | created_at INTEGER NOT NULL, | ||
| 95 | kind INTEGER NOT NULL, | ||
| 96 | envelope_bytes BLOB NOT NULL | ||
| 97 | ) STRICT; | ||
| 98 | |||
| 99 | CREATE TABLE tags ( | ||
| 100 | event_id BLOB NOT NULL REFERENCES events(id), | ||
| 101 | name TEXT NOT NULL, | ||
| 102 | value TEXT NOT NULL | ||
| 103 | ) STRICT; | ||
| 104 | |||
| 105 | CREATE INDEX idx_events_pubkey ON events(pubkey, created_at DESC); | ||
| 106 | CREATE INDEX idx_events_kind ON events(kind, created_at DESC); | ||
| 107 | CREATE INDEX idx_events_created_at ON events(created_at DESC); | ||
| 108 | CREATE INDEX idx_tags_name_value ON tags(name, value); | ||
| 109 | CREATE INDEX idx_tags_event_id ON tags(event_id); | ||
| 110 | ``` | ||
| 111 | |||
| 112 | **Config** (`config.go`) | ||
| 113 | ```yaml | ||
| 114 | addr: ":8080" | ||
| 115 | db: "axon.db" | ||
| 116 | relay_url: "ws://localhost:8080" | ||
| 117 | allowlist: | ||
| 118 | - "<hex pubkey>" | ||
| 119 | - "<hex pubkey>" | ||
| 120 | ``` | ||
| 121 | |||
| 122 | ### Dependencies | ||
| 123 | |||
| 124 | - `modernc.org/sqlite` — pure Go SQLite driver | ||
| 125 | - Custom WebSocket (no external lib) | ||
| 126 | - Core `axon` package via local replace | ||
| 127 | |||
| 128 | No other external dependencies. | ||
| 55 | 129 | ||
| 56 | --- | 130 | --- |
| 57 | 131 | ||
