# Axon — Implementation Plan ## Dependency Philosophy Minimize external dependencies at every layer. The protocol is designed so that the Go implementation requires nothing beyond the standard library and `golang.org/x/crypto`. The JS implementation requires only `@noble/curves` and `@msgpack/msgpack` — both small, audited, zero-dependency libraries. No frameworks, no ORMs, no generated code. --- ## Phase 1: Core Package **Everything depends on this. Do not move to Phase 2 until the signing spec is fully verified.** - `Event` struct, `Tag`, kind constants matching the registry in PROTOCOL.md - Canonical signing payload construction — byte-exact per the spec - Tag sorting (`canonical_tags` hash) - Ed25519 sign and verify (`crypto/ed25519`) - X25519 key conversion from Ed25519 keypair (`golang.org/x/crypto/curve25519`) - ChaCha20-Poly1305 encrypt/decrypt for DMs (`golang.org/x/crypto/chacha20poly1305`) - MessagePack encode/decode for events **Test vectors — write these before any implementation:** Publish a set of known-input → known-output pairs for: - `canonical_tags` hash given a specific tag list - `canonical_payload` bytes given a specific event - Event `id` (SHA256 of payload) - Signature verification These vectors must be committed to the repo and validated by the JS client in Phase 4. They are the ground truth for cross-implementation correctness. A disagreement on a test vector means the spec is ambiguous — fix the spec before fixing the code. **Go dependencies:** `golang.org/x/crypto` only. --- ## Phase 2: Relay 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. ### Module structure ``` relay/ go.mod — module "axon/relay"; replace axon => ../ go.sum main.go — entry point, config, wiring, graceful shutdown config.go — YAML config: listen addr, db path, allowlist server.go — HTTP server, WebSocket upgrade, connection lifecycle handler.go — per-connection message dispatch websocket/ websocket.go — raw WebSocket framing (lifted from muxstr, zero deps) subscription/ manager.go — in-memory subscriptions, filter matching, fanout storage/ storage.go — SQLite init, schema, WAL config events.go — store, query, dedup ``` ### Components **WebSocket layer** (`websocket/websocket.go`) - Lifted from muxstr with minimal changes — handles RFC 6455 framing, ping/pong, TLS - No external WebSocket dependency - Relay sends ping every 30s; connections missing two consecutive pings are closed **Connection auth** (`handler.go`) - On connect: relay sends `Challenge { nonce: 32 random bytes }` - Client responds: `Auth { pubkey, sig }` where `sig = ed25519.Sign(privkey, SHA256(nonce || relay_url))` - Relay verifies sig, checks pubkey against allowlist, responds `Ok` or `Error { 401 }` + close - All subsequent messages rejected until auth completes **Ingest pipeline** (`handler.go` → `storage/events.go`) 1. Unmarshal msgpack `[msg_type, payload]` frame 2. Dispatch by `msg_type` (uint16) 3. For `Publish`: verify sig via axon core, reject if content > 64KB, reject duplicates 4. For job requests (kind 5000–5999): check `expires_at` tag, reject if expired 5. Ephemeral events (kind 3000–3999): fanout only, skip storage 6. Persist to SQLite, fanout to matching subscribers **Subscription manager** (`subscription/manager.go`) - Lifted from muxstr: buffered channel per subscription (cap 100), non-blocking sends (drop-on-full) - Per-subscription goroutine streams events to the client connection - Filter matching: ids (prefix), authors (prefix), kinds, since/until, tag filters - Multiple filters per subscription are OR'd **Storage** (`storage/`) - `modernc.org/sqlite` — pure Go, no CGo - WAL mode, 40MB cache, memory-mapped I/O - Schema: ```sql CREATE TABLE events ( id BLOB PRIMARY KEY, pubkey BLOB NOT NULL, created_at INTEGER NOT NULL, kind INTEGER NOT NULL, envelope_bytes BLOB NOT NULL ) STRICT; CREATE TABLE tags ( event_id BLOB NOT NULL REFERENCES events(id), name TEXT NOT NULL, value TEXT NOT NULL ) STRICT; CREATE INDEX idx_events_pubkey ON events(pubkey, created_at DESC); CREATE INDEX idx_events_kind ON events(kind, created_at DESC); CREATE INDEX idx_events_created_at ON events(created_at DESC); CREATE INDEX idx_tags_name_value ON tags(name, value); CREATE INDEX idx_tags_event_id ON tags(event_id); ``` **Config** (`config.go`) ```yaml addr: ":8080" db: "axon.db" relay_url: "ws://localhost:8080" allowlist: - "" - "" ``` ### Dependencies - `modernc.org/sqlite` — pure Go SQLite driver - Custom WebSocket (no external lib) - Core `axon` package via local replace No other external dependencies. --- ## Phase 3: Go Client - Connect to relay + complete auth handshake - Publish events - Subscribe with filter, receive event stream - Unsubscribe - Reconnect with backoff The Go client is the primary tool for integration testing the relay. All relay behavior should be exercised via the client before moving to Phase 4. **Go dependencies:** none beyond what Phase 1 and Phase 2 already use. --- ## Phase 4: JS Client - Connect + auth (Ed25519 sign the challenge) - Publish events (msgpack encode, Ed25519 sign) - Subscribe / unsubscribe - DM encrypt/decrypt (X25519 ECDH + ChaCha20-Poly1305) - Validate Phase 1 test vectors — JS and Go must agree on every vector before this phase is considered complete **JS dependencies:** `@noble/curves` (Ed25519, X25519), `@msgpack/msgpack`. No others. --- ## Phase 5: Integration - End-to-end: publish from JS client, receive in Go client (and vice versa) - Cross-language signature verification: Go signs, JS verifies; JS signs, Go verifies - DM round-trip: encrypt in JS, decrypt in Go - Job protocol flow: request → feedback → result across two separate processes At the end of Phase 5 you have a working protocol implementation ready to back an agent system. --- ## What Comes After - Agent discovery via `KindProfile` (kind 0) — capability advertisement - Relay clustering / replication (out of scope for v1) - Retention and event expiry policies