1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
# 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:
- "<hex pubkey>"
- "<hex pubkey>"
```
### 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
|