diff options
Diffstat (limited to 'LOCAL_RELAY_PLAN.md')
| -rw-r--r-- | LOCAL_RELAY_PLAN.md | 294 |
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 | |||
| 5 | Integrate 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 | ``` | ||
| 141 | components/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 | |||
| 155 | components/negentropy/ # Git submodule → hoytech/negentropy/cpp/ | ||
| 156 | negentropy.h | ||
| 157 | encoding.h | ||
| 158 | types.h | ||
| 159 | storage/Vector.h | ||
| 160 | storage/base.h | ||
| 161 | |||
| 162 | main/ | ||
| 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 | |||
| 168 | tests/unit/ | ||
| 169 | test_relay_validator.c | ||
| 170 | test_negentropy_sync.c | ||
| 171 | |||
| 172 | tests/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 | ||