upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/LOCAL_RELAY_PLAN.md
diff options
context:
space:
mode:
Diffstat (limited to 'LOCAL_RELAY_PLAN.md')
-rw-r--r--LOCAL_RELAY_PLAN.md294
1 files changed, 294 insertions, 0 deletions
diff --git a/LOCAL_RELAY_PLAN.md b/LOCAL_RELAY_PLAN.md
new file mode 100644
index 0000000..38c3a29
--- /dev/null
+++ b/LOCAL_RELAY_PLAN.md
@@ -0,0 +1,294 @@
1# Local Nostr Relay + Negentropy Sync — Implementation Plan
2
3## Overview
4
5Integrate a local Nostr relay (wisp-esp32) into the TollGate firmware so that all Nostr events are published locally first, then efficiently synced to public relays using negentropy (NIP-77). This reduces WebSocket connection churn, enables true offline operation, and provides smart relay selection via NIP-11 probing.
6
7## Architecture
8
9```
10┌──────────────────────────────────────────────────────────────┐
11│ TollGate ESP32-S3 │
12│ │
13│ ┌─────────────┐ ┌──────────────────────────────────┐ │
14│ │ Publishers │ │ Local Relay :4869 │ │
15│ │ │ │ (LittleFS 4MB, 5000 events) │ │
16│ │ wifistr │────►│ │ │
17│ │ CEP-6 anns │────►│ │ │
18│ │ CVM resp │─┬──►│ │ │
19│ │ relay list │─┤ └──────────┬───────────────────────┘ │
20│ └─────────────┘ │ │ │
21│ │ ┌──────────┴──────────────────────┐ │
22│ │ │ Relay Selector (NIP-11 probes) │ │
23│ │ │ Seeds: orangesync, damus, │ │
24│ │ │ nos.lol, nostr.band │ │
25│ │ │ Re-probe: every 6h │ │
26│ │ │ Auto-failover: 3 disconnects │ │
27│ │ └──────────┬──────────────────────┘ │
28│ │ │ │
29│ │ ┌──────────┴──────────────────────┐ │
30│ │ │ Selected Relays │ │
31│ │ │ │ │
32│ │ │ primary (best NIP-77): │ │
33│ │ │ → CVM persistent WS │ │
34│ │ │ → Negentropy sync (30min) │ │
35│ │ │ │ │
36│ │ │ fallbacks (top 2 others): │ │
37│ │ │ → REQ-diff sync (6h) │ │
38│ │ └─────────────────────────────────┘ │
39│ │ │
40│ ┌──────────────┴──┐ │
41│ │ CVM Server │ Persistent WS to primary relay │
42│ │ (subscribe + │ Real-time receive requests, │
43│ │ respond) │ send responses immediately │
44│ └─────────────────┘ Also stores in local relay │
45│ │
46└──────────────────────────────────────────────────────────────┘
47```
48
49## Design Decisions
50
51### 1. Local-First Publishing (not dual-publish)
52
53**Decision**: All events (wifistr, CEP-6, CVM responses, relay lists) are published to the local relay first. Only CVM responses are also sent in real-time via the persistent CVM WebSocket.
54
55**Reasoning**: Maintaining simultaneous WebSocket connections to N public relays is expensive on ESP32 — each connection consumes a TCP socket, TLS state, and RAM. By publishing locally and syncing later, we reduce the steady-state connection count from 1 persistent + N temporary to 1 persistent + 1 periodic. The local relay always accepts events, even when offline.
56
57**Trade-off**: wifistr service discovery events and CEP-6 announcements appear on public relays with a delay (up to 30 minutes). This is acceptable because these are not time-critical — they're informational and update infrequently (every 6h).
58
59### 2. Negentropy (NIP-77) for Sync
60
61**Decision**: Use the negentropy set-reconciliation protocol for syncing local events to public relays, with REQ-diff as a fallback for relays that don't support NIP-77.
62
63**Reasoning**: Negentropy uses Merkle-tree-based fingerprint comparison to determine exactly which events are missing, transferring only the delta. A typical sync round-trip when nothing is missing is ~100 bytes. This is orders of magnitude more efficient than downloading all events via REQ and comparing locally. The negentropy C++ library is only ~660 lines of header-only code with no external dependencies — feasible for ESP32 with C++ compilation support.
64
65**Trade-off**: Negentropy only works with strfry-based relays (NIP-77). Most major public relays (damus, nos.lol) don't support it. We mitigate this by maintaining REQ-diff as a fallback and prioritizing relays that support NIP-77.
66
67### 3. NIP-11 HTTP Probing for Relay Selection
68
69**Decision**: On boot and every 6 hours, probe seed relays via NIP-11 HTTP requests (not WebSocket). Rank by latency and NIP-77 support. Auto-failover after 3 consecutive disconnects.
70
71**Reasoning**: ESP32 WebSocket connections are expensive. We need to ensure every persistent connection goes to the best available relay. NIP-11 is a simple HTTP GET with `Accept: application/nostr+json` — no WebSocket, no subscription, minimal overhead. From the response we get: liveness, latency, supported NIPs, and connection limits. This lets us select the best relay for each role without maintaining any connections.
72
73**Trade-off**: NIP-11 probes add ~5 seconds of HTTP traffic every 6 hours. Negligible. Relay rankings may not reflect real-time conditions perfectly, but re-probing every 6h is a good balance.
74
75### 4. Approach C: Rewrite Wisp Validator for TollGate Crypto
76
77**Decision**: Port only the relay "core" (ws_server, storage_engine, sub_manager, broadcaster) from wisp-esp32. Rewrite the validator and router to use TollGate's existing secp256k1 + mbedtls instead of wisp's libnostr-c/noscrypt dependencies.
78
79**Reasoning**: TollGate already has secp256k1 linked (via nucula) and mbedtls (via ESP-IDF). Adding libnostr-c + noscrypt + secp256k1-frost would introduce symbol conflicts and bloated flash usage. The validator only needs two crypto operations: SHA-256 event ID computation and Schnorr signature verification — both already available in TollGate's existing libraries.
80
81**Trade-off**: More upfront work than just including libnostr-c, but cleaner build, no symbol conflicts, and smaller binary.
82
83### 5. 4MB LittleFS Relay Storage Partition
84
85**Decision**: Allocate a 4MB LittleFS partition at offset 0x500000 for relay event storage (up to 5000 events with 21-day TTL).
86
87**Reasoning**: TollGate has 16MB flash with 11MB currently unused. 4MB matches wisp's tested capacity. LittleFS is better suited than SPIFFS for the relay's write pattern (many small sequential writes). The SPIFFS partition for config.json remains unchanged.
88
89**Trade-off**: Uses 4MB of the 11MB free space. Still leaves 7MB for future use.
90
91### 6. Client-Accessible Relay
92
93**Decision**: The local relay listens on port 4869 and is accessible to authenticated WiFi clients.
94
95**Reasoning**: Connected clients (who have paid for access) can subscribe to events on the local relay. This enables local CVM tool calls, local service discovery, and mesh scenarios without internet. The firewall ensures only authenticated clients can reach it.
96
97**Trade-off**: Slightly increased attack surface, but limited to authenticated clients and the relay has rate limiting.
98
99### 7. Minimal Seed Relay List (4 relays)
100
101**Decision**: Hardcode 4 seed relays: `relay.orangesync.tech`, `relay.damus.io`, `nos.lol`, `relay.nostr.band`.
102
103**Reasoning**: Fewer relays means fewer NIP-11 probes and simpler selection logic. Orangesync is the user's strfry relay (NIP-77). Damus and nos.lol are major reliable relays. Nostr.band is a strfry backup with NIP-77. Users can override in config.json.
104
105**Trade-off**: Less diversity in relay selection. If all 4 are degraded simultaneously, the device has no alternatives. Mitigated by allowing user-configured relay lists.
106
107### 8. Negentropy as Git Submodule
108
109**Decision**: Include `hoytech/negentropy` as a git submodule, referencing the `cpp/` directory.
110
111**Reasoning**: Matches how nucula and secp256k1 are already included. Easy to update upstream. The library is MIT licensed, header-only C++ with no external dependencies.
112
113**Trade-off**: Requires `git submodule update --init` on clone. Standard for this project.
114
115## Connection Budget
116
117| Connection | Type | Frequency | Duration |
118|-----------|------|-----------|----------|
119| CVM subscribe/respond | Persistent WS | Always | Continuous |
120| Negentropy sync | Temporary WS | Every 30min | ~2-5s |
121| REQ-diff fallback sync | Temporary WS | Every 6h | ~5-10s |
122| NIP-11 probes | Plain HTTPS | Every 6h | ~5s total |
123| Local relay (loopback) | HTTP/WS | Always | No network cost |
124
125**Steady state: 1 persistent WS + brief periodic temporary connections.**
126
127## Flash Layout
128
129| Partition | Offset | Size | Purpose |
130|-----------|--------|------|---------|
131| nvs | 0x9000 | 24KB | Wallet proofs, settings |
132| phy_init | 0xf000 | 4KB | PHY calibration |
133| factory | 0x10000 | ~4MB | Application firmware |
134| storage (SPIFFS) | 0x410000 | 960KB | config.json |
135| **relay_store (LittleFS)** | **0x500000** | **4MB** | **Relay event storage** |
136| Free | 0x900000 | 7MB | Future use |
137
138## New Files
139
140```
141components/wisp_relay/ # From wisp-esp32 (adapted)
142 CMakeLists.txt
143 ws_server.c/h
144 storage_engine.c/h # Adapted: partition label, LittleFS
145 sub_manager.c/h
146 broadcaster.c/h
147 rate_limiter.c/h
148 nip11.c/h # Customized for TollGate
149 deletion.c/h
150 flash_monitor.c/h
151 router.c/h # REWRITTEN for TollGate cJSON
152 relay_validator.c/h # REWRITTEN for TollGate secp256k1+mbedtls
153 relay_core.h
154
155components/negentropy/ # Git submodule → hoytech/negentropy/cpp/
156 negentropy.h
157 encoding.h
158 types.h
159 storage/Vector.h
160 storage/base.h
161
162main/
163 local_relay.c/h # Thin wrapper for publishing to local relay
164 relay_selector.c/h # NIP-11 HTTP probe + ranking + auto-failover
165 sync_manager.c/h # Negentropy + REQ-diff sync engine
166 negentropy_storage.c/h # Adapter: wisp storage → negentropy storage API
167
168tests/unit/
169 test_relay_validator.c
170 test_negentropy_sync.c
171
172tests/integration/
173 test_local_relay.mjs
174 test_sync.mjs
175```
176
177## Modified Files
178
179| File | Change |
180|------|--------|
181| `partitions.csv` | Add 4MB LittleFS partition at 0x500000 |
182| `sdkconfig.defaults` | Enable HTTPD WS support, bump LWIP sockets to 20 |
183| `CMakeLists.txt` | Add negentropy submodule, wisp_relay component |
184| `.gitmodules` | Add negentropy submodule |
185| `config.c/h` | Add seed_relays, sync_interval fields |
186| `tollgate_main.c` | Init local relay, selector, sync manager in start_services() |
187| `wifistr.c` | Publish to local relay only |
188| `cvm_server.c` | Use dynamic relay from selector; store responses locally |
189
190## Config Additions
191
192```json
193{
194 "nostr_seed_relays": [
195 "wss://relay.orangesync.tech",
196 "wss://relay.damus.io",
197 "wss://nos.lol",
198 "wss://relay.nostr.band"
199 ],
200 "nostr_sync_interval_s": 1800,
201 "nostr_fallback_sync_interval_s": 21600
202}
203```
204
205---
206
207## Implementation Checklist
208
209### Phase 0: Branch & Infrastructure
210- [ ] Create `feature/local-relay` branch
211- [ ] Create git worktree at `../esp32-tollgate-relay`
212- [ ] Add `hoytech/negentropy` git submodule
213
214### Phase 1: Partition & Build System
215- [ ] Update `partitions.csv` with 4MB LittleFS relay partition
216- [ ] Update `sdkconfig.defaults`: `CONFIG_HTTPD_WS_SUPPORT=y`, `CONFIG_LWIP_MAX_SOCKETS=20`
217- [ ] Update `CMakeLists.txt` with negentropy + wisp_relay components
218- [ ] Verify build compiles (may not link yet)
219
220### Phase 2: Port Wisp Relay Core
221- [ ] Create `components/wisp_relay/` directory
222- [ ] Port `ws_server.c/h` (minimal adaptation, port 4869)
223- [ ] Port `storage_engine.c/h` (partition label → `"relay_store"`, LittleFS)
224- [ ] Port `sub_manager.c/h` (as-is)
225- [ ] Port `broadcaster.c/h` (as-is)
226- [ ] Port `rate_limiter.c/h` (as-is)
227- [ ] Port `nip11.c/h` (customize NIP-11 JSON for TollGate)
228- [ ] Port `deletion.c/h` (as-is)
229- [ ] Port `flash_monitor.c/h` (as-is)
230- [ ] Port `relay_core.h` (adapt types)
231- [ ] Create `components/wisp_relay/CMakeLists.txt`
232- [ ] Verify relay component compiles
233
234### Phase 3: Rewrite Validator & Router
235- [ ] Write `relay_validator.c/h` using TollGate's secp256k1 + mbedtls
236 - [ ] Event ID verification (SHA-256 of serialized event)
237 - [ ] Schnorr signature verification (secp256k1_schnorrsig_verify)
238 - [ ] Event age check, expiration check
239- [ ] Write `router.c/h` using TollGate's cJSON (not libnostr-c)
240 - [ ] Parse CLIENT messages (EVENT, REQ, CLOSE)
241 - [ ] Dispatch to handlers
242 - [ ] Serialize relay messages (OK, EVENT, EOSE, NOTICE, CLOSED)
243- [ ] Write `handlers.c` (event handler, REQ handler, CLOSE handler)
244
245### Phase 4: Local-First Publishing
246- [ ] Create `main/local_relay.c/h` — thin wrapper
247- [ ] Modify `wifistr.c` — publish to local relay only
248- [ ] Modify `cvm_server.c` — store CEP-6 announcements locally
249- [ ] Modify `tollgate_main.c` — init local relay in `start_services()`
250
251### Phase 5: Relay Selector (NIP-11)
252- [ ] Create `main/relay_selector.c/h`
253- [ ] Implement NIP-11 HTTP probe (esp_http_client + Accept header)
254- [ ] Implement relay scoring (latency + NIP-77 bonus)
255- [ ] Implement selection: primary (best NIP-77) + fallbacks
256- [ ] Implement auto-failover (3 disconnects → re-probe + switch)
257- [ ] Implement periodic re-probe (every 6h)
258- [ ] Add seed relay config to `config.c/h`
259
260### Phase 6: CVM Dynamic Relay
261- [ ] Modify `cvm_server.c` to connect to `primary_relay` from selector
262- [ ] Keep real-time publish via persistent WS for CVM responses
263- [ ] Also store CVM responses in local relay
264- [ ] Handle relay switch (disconnect old, connect new)
265
266### Phase 7: Negentropy Sync Manager
267- [ ] Create `main/negentropy_storage.c/h` — adapter from wisp storage to negentropy API
268- [ ] Create `main/sync_manager.c/h`
269- [ ] Implement negentropy sync with primary relay (every 30min)
270 - [ ] Build negentropy set from local relay events
271 - [ ] Open WS to primary relay
272 - [ ] Run negentropy protocol (NEG_OPEN/NEG_MSG)
273 - [ ] Re-publish missing events
274- [ ] Implement REQ-diff fallback with fallback relays (every 6h)
275 - [ ] REQ own events from public relay
276 - [ ] Diff event IDs against local relay
277 - [ ] Re-publish missing events
278- [ ] Implement on-reconnect immediate sync trigger
279
280### Phase 8: Tests
281- [ ] Unit test: relay validator (event ID + Schnorr verify with known vectors)
282- [ ] Unit test: negentropy sync logic (mock ID sets)
283- [ ] Unit test: relay selector scoring
284- [ ] Integration test: local relay (publish + subscribe via `nak`)
285- [ ] Integration test: negentropy sync (local → orangesync)
286- [ ] Integration test: REQ-diff fallback
287- [ ] Integration test: CVM through local relay
288- [ ] E2E test: CVM tool call via relay
289
290### Phase 9: Documentation
291- [ ] Update AGENTS.md with local relay info
292- [ ] Update config format documentation
293- [ ] Add relay selection documentation
294- [ ] Update test instructions