diff options
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 34 |
1 files changed, 28 insertions, 6 deletions
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | ## Project Overview | 3 | ## Project Overview |
| 4 | 4 | ||
| 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, wifistr service discovery, and ContextVM (MCP over Nostr) server. Runs on three ESP32-S3 boards. | 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, wifistr service discovery, ContextVM (MCP over Nostr) server, and **local Nostr relay** with relay selection and sync. Runs on three ESP32-S3 boards. |
| 6 | 6 | ||
| 7 | ## Technology Stack | 7 | ## Technology Stack |
| 8 | 8 | ||
| @@ -12,6 +12,9 @@ TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, | |||
| 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP | 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP |
| 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket | 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket |
| 14 | - **ContextVM:** MCP over Nostr (kind 25910), CEP-6 announcements, 10 MCP tools | 14 | - **ContextVM:** MCP over Nostr (kind 25910), CEP-6 announcements, 10 MCP tools |
| 15 | - **Local relay:** wisp-esp32 (adapted), NIP-01 server on port 4869, LittleFS 4MB storage | ||
| 16 | - **Relay selection:** NIP-11 HTTP probing, latency + NIP-77 scoring, auto-failover | ||
| 17 | - **Sync:** REQ-diff with primary (30min) and fallback (6h) relays | ||
| 15 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E | 18 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E |
| 16 | 19 | ||
| 17 | ## Board Configuration | 20 | ## Board Configuration |
| @@ -42,7 +45,8 @@ nvs_flash_init() | |||
| 42 | → wifi_configure_ap() // uses derived SSID | 45 | → wifi_configure_ap() // uses derived SSID |
| 43 | → esp_wifi_start() | 46 | → esp_wifi_start() |
| 44 | → [on STA got IP] start_services(): | 47 | → [on STA got IP] start_services(): |
| 45 | sntp_init, firewall_init, session_init, wallet_init, dns_server, captive_portal, api, wifistr_publish, cvm_server_start | 48 | sntp_init, firewall_init, session_init, wallet_init, dns_server, captive_portal, api, |
| 49 | local_relay_init+start, relay_selector_init+probe, sync_manager_start, wifistr_publish, cvm_server_start | ||
| 46 | ``` | 50 | ``` |
| 47 | 51 | ||
| 48 | ## Key Files | 52 | ## Key Files |
| @@ -53,7 +57,7 @@ nvs_flash_init() | |||
| 53 | - `identity.c/h` — HMAC-SHA512 derivation from nsec, npub/MAC/SSID/IP | 57 | - `identity.c/h` — HMAC-SHA512 derivation from nsec, npub/MAC/SSID/IP |
| 54 | - `nostr_event.c/h` — NIP-01 event serialization + BIP-340 Schnorr signing | 58 | - `nostr_event.c/h` — NIP-01 event serialization + BIP-340 Schnorr signing |
| 55 | - `geohash.c/h` — lat/lon to geohash encoding | 59 | - `geohash.c/h` — lat/lon to geohash encoding |
| 56 | - `wifistr.c/h` — kind 38787 event builder + WebSocket relay publish | 60 | - `wifistr.c/h` — kind 38787 event builder + local-first publish (local relay then public) |
| 57 | - `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset | 61 | - `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset |
| 58 | - `dns_server.c/h` — DNS hijack/forward per-client, DoT reject | 62 | - `dns_server.c/h` — DNS hijack/forward per-client, DoT reject |
| 59 | - `firewall.c/h` — per-client NAT filter via LWIP_HOOK_IP4_CANFORWARD, MAC resolution | 63 | - `firewall.c/h` — per-client NAT filter via LWIP_HOOK_IP4_CANFORWARD, MAC resolution |
| @@ -62,10 +66,18 @@ nvs_flash_init() | |||
| 62 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints | 66 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints |
| 63 | - `cvm_server.c/h` — ContextVM: persistent WS relay listener, kind 25910 subscription, MCP protocol handlers, CEP-6 announcements | 67 | - `cvm_server.c/h` — ContextVM: persistent WS relay listener, kind 25910 subscription, MCP protocol handlers, CEP-6 announcements |
| 64 | - `mcp_handler.c/h` — 10 MCP tool handlers (get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt) | 68 | - `mcp_handler.c/h` — 10 MCP tool handlers (get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt) |
| 69 | - `local_relay.c/h` — Thin wrapper: inits wisp_relay storage/sub/rate-limiter on port 4869, publishes events to LittleFS + broadcasts to WS subscribers | ||
| 70 | - `relay_selector.c/h` — NIP-11 HTTP probing of seed relays, latency + NIP-77 scoring, auto-failover after 3 disconnects, 6h re-probe cycle | ||
| 71 | - `sync_manager.c/h` — REQ-diff sync: primary every 30min, fallback every 6h, reconciles local events vs remote, dedicated FreeRTOS task | ||
| 65 | 72 | ||
| 66 | ### Components | 73 | ### Components |
| 67 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) | 74 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) |
| 68 | - `secp256k1/` — symlink to nucula_src/components/secp256k1/ | 75 | - `secp256k1/` — symlink to nucula_src/components/secp256k1/ |
| 76 | - `wisp_relay/` — Local Nostr relay (NIP-01): ws_server, storage_engine (LittleFS), sub_manager, broadcaster, router, handlers, relay_validator (Schnorr+SHA256), rate_limiter, nip11, deletion, flash_monitor | ||
| 77 | - `esp_littlefs/` — LittleFS VFS integration for relay storage partition (git submodule) | ||
| 78 | - `negentropy/` — Negentropy set-reconciliation library (git submodule, for future NIP-77) | ||
| 79 | - `axs15231b/` — QSPI TFT display driver (JC3248W535) | ||
| 80 | - `qrcode/` — QR code generator | ||
| 69 | 81 | ||
| 70 | ### Config Format (config.json on SPIFFS) | 82 | ### Config Format (config.json on SPIFFS) |
| 71 | ```json | 83 | ```json |
| @@ -79,6 +91,14 @@ nvs_flash_init() | |||
| 79 | "nostr_geohash": "u281w0dfz", | 91 | "nostr_geohash": "u281w0dfz", |
| 80 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], | 92 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], |
| 81 | "nostr_publish_interval_s": 21600, | 93 | "nostr_publish_interval_s": 21600, |
| 94 | "nostr_seed_relays": [ | ||
| 95 | "wss://relay.orangesync.tech", | ||
| 96 | "wss://relay.damus.io", | ||
| 97 | "wss://nos.lol", | ||
| 98 | "wss://relay.nostr.band" | ||
| 99 | ], | ||
| 100 | "nostr_sync_interval_s": 1800, | ||
| 101 | "nostr_fallback_sync_interval_s": 21600, | ||
| 82 | "cvm_enabled": true | 102 | "cvm_enabled": true |
| 83 | } | 103 | } |
| 84 | ``` | 104 | ``` |
| @@ -186,7 +206,9 @@ make flash-b # flash to Board B | |||
| 186 | 206 | ||
| 187 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices | 207 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices |
| 188 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events | 208 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events |
| 209 | - **Seed relays:** `relay.orangesync.tech` (NIP-77), `relay.damus.io`, `nos.lol`, `relay.nostr.band` — for relay selection and sync | ||
| 189 | - **CVM relay:** `relay.primal.net` — for ContextVM kind 25910 events and CEP-6 announcements | 210 | - **CVM relay:** `relay.primal.net` — for ContextVM kind 25910 events and CEP-6 announcements |
| 211 | - **Local relay:** Port 4869, LittleFS 4MB partition at 0x500000, max 5000 events, 21-day TTL | ||
| 190 | - **Nutshell CLI:** `cashu` command for token generation | 212 | - **Nutshell CLI:** `cashu` command for token generation |
| 191 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands | 213 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands |
| 192 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` | 214 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` |
| @@ -200,12 +222,12 @@ make flash-b # flash to Board B | |||
| 200 | - `sudo` password: `c03rad0r123` | 222 | - `sudo` password: `c03rad0r123` |
| 201 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale | 223 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale |
| 202 | - NVS stores wallet proofs — erasing NVS clears wallet balance | 224 | - NVS stores wallet proofs — erasing NVS clears wallet balance |
| 225 | - **Relay storage** LittleFS at offset `0x500000`, size `0x400000` (4MB) — auto-formatted on first boot | ||
| 203 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests | 226 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests |
| 204 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests | 227 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests |
| 228 | - relay_validator.c does Schnorr verify + SHA-256 event ID — test with `test_relay_validator` | ||
| 229 | - relay_selector scoring: NIP-77 bonus (1000pts) + latency + failure penalty (100pts each) — test with `test_relay_selector` | ||
| 205 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch | 230 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch |
| 206 | - **WiFi country code:** Must set `esp_wifi_set_country_code("DE")` before `esp_wifi_start()` — defaults to CN which causes auth failures on EU APs | 231 | - **WiFi country code:** Must set `esp_wifi_set_country_code("DE")` before `esp_wifi_start()` — defaults to CN which causes auth failures on EU APs |
| 207 | - **Board A WiFi is broken** — hardware issue confirmed: `WIFI_REASON_AUTH_EXPIRED` on all APs in all modes (APSTA, STA-only, factory MAC). Board B with identical firmware connects instantly. Do not waste time debugging Board A WiFi. | ||
| 208 | - Default nsec: `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2` | 232 | - Default nsec: `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2` |
| 209 | - Board A nsec: `9af47906b45aca5e238390f3d03c8274e154198e81aa2095065627d1e61ca968` | 233 | - Board A nsec: `9af47906b45aca5e238390f3d03c8274e154198e81aa2095065627d1e61ca968` |
| 210 | - CVM relay: `relay.primal.net` — relay disconnects every ~15s by default, now has 60s timeout + WS ping/pong keepalive | ||
| 211 | - MCP responses sent via existing WS connection (not new TLS) — ESP32 can't handle multiple simultaneous TLS sessions | ||