upleb.uk

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

summaryrefslogtreecommitdiff
path: root/PLAN.md
diff options
context:
space:
mode:
Diffstat (limited to 'PLAN.md')
-rw-r--r--PLAN.md293
1 files changed, 217 insertions, 76 deletions
diff --git a/PLAN.md b/PLAN.md
index 8ea827d..2a0ed2b 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -286,103 +286,243 @@ Publishes TollGate node to Nostr as kind 38787 (wifistr):
286| 37 | 5 consecutive payments | Loop | All authenticated | TODO | 286| 37 | 5 consecutive payments | Loop | All authenticated | TODO |
287| 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | 287| 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO |
288 288
289### Phase 4: Mesh Service Discovery + ESP32-to-OpenWRT Interop — NOT STARTED 289### Phase 4: ESP32 TollGate Client Detection + Auto-Payment — IN PROGRESS
290 290
291**Goal:** Two capabilities: (1) Pre-association price discovery between mesh nodes using Wi-Fi Vendor IE beacons, (2) ESP32-to-OpenWRT TollGate interoperability with Cashu tokens. 291**Goal:** ESP32 detects upstream TollGate when connected as STA, automatically pays for internet access using on-device wallet. Enables ESP32→OpenWRT (Scenario 4) and ESP32→ESP32 (Scenario 5) auto-payment.
292 292
293#### 4A: Pre-Association Service Discovery via Vendor IE Beacons 293**New files:** `main/tollgate_client.c`, `main/tollgate_client.h`
294 294
295**Problem:** In a tollgate mesh network, a client router needs to know an upstream gateway's price before investing in Wi-Fi connection setup/teardown. Standard 802.11u ANQP is not supported by ESP-IDF. 295#### Architecture
296 296
297**Solution: Vendor-Specific Information Elements in Beacon/Probe Response frames** 297The ESP32 already runs `WIFI_MODE_APSTA` — STA connects to upstream WiFi. When STA gets an IP, the client module:
2981. Extracts gateway IP from DHCP info
2992. HTTP GET `http://{gw}:2121/` — check for TollGate (kind=10021)
3003. Parse price/mint/metric from advertisement tags
3014. Check wallet balance ≥ price
3025. `nucula_wallet_send(price_sats)` → cashuA V3 token
3036. POST token to `http://{gw}:2121/`
3047. Parse kind=1022 response — session granted
3058. Monitor: periodic GET `/usage`, auto-renew at 20% remaining
298 306
299ESP-IDF provides `esp_wifi_set_vendor_ie()` to inject custom data into 802.11 management frames. This allows passive price discovery during normal Wi-Fi scanning — no connection required. 307#### Client State Machine
300 308
301``` 309```
302┌─────────────────────────────────────────────────────────────┐ 310IDLE → [STA got IP] → DETECTING → [kind=10021 found] → NEEDS_PAY
303│ Layer 2 (Pre-Association) │ 311 ↓ [no TollGate] ↓ [wallet has funds]
304│ │ 312 NO_TOLLGATE PAYING → [kind=1022] → PAID
305│ Gateway AP broadcasts price in every Beacon (~100ms) │ 313 ↓ [expiry near]
306│ Client STA scans, reads price from beacon before connect │ 314 RENEWING → PAID
307│ │
308│ ┌─────────────┐ ┌─────────────┐ │
309│ │ Gateway AP │ Beacon ──────────► │ Client STA │ │
310│ │ │ (with price IE) │ │ │
311│ │ Vendor IE: │ │ Scan result │ │
312│ │ OUI:TG │ │ includes │ │
313│ │ price/sats │ │ price data │ │
314│ │ step_ms │ └──────┬──────┘ │
315│ │ mint_url │ │ │
316│ └─────────────┘ Decision: connect? │
317│ │ │
318└──────────────────────────────────────────────┼──────────────┘
319
320 ┌────────────────▼──────────────┐
321 │ Layer 3+ (Connected) │
322 │ POST / with Cashu token │
323 └───────────────────────────────┘
324``` 315```
325 316
326**Beacon IE Payload Format (Vendor-Specific, Element ID 0xDD):** 317#### Design Decisions
318- **Blocking**: upstream payment must succeed before local services start
319- **1 step per payment** (21 sats / 60s) — minimal, renew frequently
320- **No budget cap** — keep paying as long as wallet has balance
321- **Renew at 20% remaining** — re-pay when 80% of session consumed
322- **Wallet init synchronous** — must complete before client can create tokens
327 323
324#### Config Addition
325
326```json
327{
328 "client_enabled": true,
329 "client_steps_to_buy": 1,
330 "client_renewal_threshold_pct": 20,
331 "client_retry_interval_ms": 30000
332}
333```
334
335#### Integration with `tollgate_main.c`
336
337| Event | Action |
338|-------|--------|
339| `IP_EVENT_STA_GOT_IP` | Wallet init (sync) → `tollgate_client_on_sta_connected()` → start local services |
340| `WIFI_EVENT_STA_DISCONNECTED` | `tollgate_client_on_sta_disconnected()` — reset state |
341| Main loop (every 1s) | `tollgate_client_tick()` — check usage, renew if needed |
342
343#### Test Cases
344
345| # | Test | Method | Pass Criteria | Status |
346|---|------|--------|---------------|--------|
347| 39 | Client detection (kind=10021) | Unit test parse | Correct price/mint/metric extracted | TODO |
348| 40 | Client payment flow | Mock HTTP | Token POSTed, kind=1022 parsed | TODO |
349| 41 | Session renewal | Mock usage < 20% | Re-payment triggered | TODO |
350| 42 | ESP32→OpenWRT auto-pay | Integration | NAT works after payment | TODO |
351| 43 | ESP32→ESP32 auto-pay | Cross-board | Board B pays Board A | TODO |
352
353#### Vendor IE Beacon (Pre-Association Discovery) — DEFERRED
354
355Pre-association price discovery via Wi-Fi Vendor IE beacons (OUI `0x54:0x47`) is deferred to a future phase. The client currently uses HTTP-based discovery after connection.
356
357### Phase 5: Lightning Auto-Payout — NOT STARTED
358
359**Goal:** When wallet balance exceeds a configurable threshold, automatically pay out to Lightning addresses via LNURL-pay + Cashu NUT-05 melt.
360
361**New files:** `main/lnurl_pay.c`, `main/lnurl_pay.h`, `main/lightning_payout.c`, `main/lightning_payout.h`
362**Modified files:** `components/nucula_lib/nucula_wallet.h`, `components/nucula_lib/nucula_wallet.cpp`
363
364#### Architecture
365
366Mirrors the Go implementation in `tollgate-module-basic-go/src/merchant/` and `src/lightning/`:
367
368```
369Every 60s (per mint):
370 balance = nucula_wallet_balance()
371 balance >= min_payout_amount? No → skip
372 Yes:
373 payout_pool = balance - min_balance
374 For each recipient (factor):
375 share = payout_pool * factor
376 bolt11 = lnurl_get_invoice(lightning_address, share)
377 nucula_wallet_melt(bolt11, share + fee_tolerance%)
378```
379
380#### LNURL-pay Protocol (`lnurl_pay.c/h`)
381
382Pure HTTP implementation (2 GETs):
3831. `GET https://{domain}/.well-known/lnurlp/{username}` → parse callback URL, min/max amounts
3842. `GET {callback}?amount={millisats}` → extract BOLT11 invoice from response
385
386#### nucula Bridge Extension
387
388Add to `nucula_wallet.h`:
389```c
390esp_err_t nucula_wallet_melt(const char *bolt11_invoice, uint64_t max_fee_sats);
391```
392
393Wraps `Wallet::request_melt_quote()` + `Wallet::melt_tokens()` (NUT-05).
394
395#### Config Addition
396
397```json
398{
399 "payout": {
400 "enabled": true,
401 "min_payout_amount": 128,
402 "min_balance": 64,
403 "fee_tolerance_pct": 10,
404 "check_interval_s": 60,
405 "recipients": [
406 {"lightning_address": "user@domain.com", "factor": 0.79},
407 {"lightning_address": "dev@domain.com", "factor": 0.21}
408 ]
409 }
410}
411```
412
413#### Test Cases
414
415| # | Test | Method | Pass Criteria | Status |
416|---|------|--------|---------------|--------|
417| 44 | LNURL-pay flow | Unit test HTTP parse | Correct BOLT11 extracted | TODO |
418| 45 | Payout threshold | Unit test | Skip when below, trigger when above | TODO |
419| 46 | Multi-recipient split | Unit test | Factors sum to 1.0 | TODO |
420| 47 | Melt with fee tolerance | Integration | Invoice paid, change received | TODO |
421| 48 | Full payout cycle | E2E | Wallet drains to min_balance | TODO |
422
423### Phase 6: Bytes-Based Billing — NOT STARTED
424
425**Goal:** Support both time-based (milliseconds) and data-based (bytes) billing metrics. Mirrors the Go implementation's dual-metric system.
426
427#### lwIP NAPT Byte Counting (Managed Component)
428
429**New component:** `components/lwip_napt_stats/` — patched copy of ESP-IDF's `ip4_napt.c` with per-entry byte counters.
430
431Patch adds to `struct ip_napt_entry`:
432```c
433uint64_t bytes_up; // bytes uploaded (client → internet)
434uint64_t bytes_down; // bytes downloaded (internet → client)
328``` 435```
329┌──────────┬────────┬─────────────┬──────────────┬──────────────────┐ 436
330│ element_id│ length │ vendor_oui │ oui_type │ payload │ 437Increment in `ip_napt_forward()` (upload) and `ip_napt_recv()` (download).
331│ (0xDD) │ │ (3 bytes) │ (1 byte) │ (variable) │ 438
332├──────────┼────────┼─────────────┼──────────────┼──────────────────┤ 439New public API:
333│ 0xDD │ N │ "TG" │ 0x01 (price) │ See below │ 440```c
334│ │ │ 0x54:0x47 │ │ │ 441void ip_napt_get_client_bytes(uint32_t client_ip, uint64_t *bytes_up, uint64_t *bytes_down);
335└──────────┴────────┴─────────────┴──────────────┴──────────────────┘
336
337Price Payload (oui_type 0x01):
338┌─────────────┬─────────────┬──────────────┬───────────────┬────────────┐
339│ version (1B)│ price (2B) │ step_ms (2B) │ fee_ppk (2B) │ hop_count │
340│ = 0x01 │ sat/step │ ms/step │ or 0 │ (1B) │
341├─────────────┼─────────────┼──────────────┼───────────────┼────────────┤
342│ 0x01 │ uint16_le │ uint16_le │ uint16_le │ uint8 │
343└─────────────┴─────────────┴──────────────┴───────────────┴────────────┘
344Total payload: 9 bytes (fits easily in beacon, typical budget ~200 bytes)
345``` 442```
346 443
347**Implementation:** 444~30 line patch. Lives in the project repo as a managed component, survives ESP-IDF updates.
348 445
349**AP Side (Gateway — `beacon_price.c/h`):** 446#### Session Changes
350- `beacon_price_start()` — calls `esp_wifi_set_vendor_ie(true, WIFI_VND_IE_TYPE_BEACON, WIFI_VND_IE_ID_0, &ie_data)` and also for `WIFI_VND_IE_TYPE_PROBE_RESP`
351- `beacon_price_update(uint16_t price_sat, uint16_t step_ms, uint16_t fee_ppk, uint8_t hop_count)` — dynamically updates the IE in-place (no reconnect, no user kick; next beacon frame carries new price)
352- Price derived from `tollgate_config_t` fields (`price_per_step`, `step_size_ms`)
353- Can be called on-the-fly when market conditions change (e.g., upstream price changes)
354 447
355**STA Side (Client — `beacon_scan.c/h`):** 448`session_t` gains dual-metric support:
356- `beacon_scan_prices(wifi_ap_record_t *aps, int count, tollgate_price_t *prices, int *price_count)` — given scan results, extract price IEs 449```c
357- Uses `esp_wifi_set_vendor_ie_cb()` to register a callback that fires during scan 450uint64_t allotment_bytes;
358- Or parses `vendor_ie_data_t` from scan results if available in `wifi_ap_record_t` 451uint64_t bytes_consumed;
359- Returns array of `{bssid, ssid, price_sat, step_ms, fee_ppk, hop_count}` 452```
360- Client selects cheapest/upstream gateway from scan results before connecting
361 453
362**Integration with existing config:** 454`session_is_expired()` dispatches on metric type:
363- OUI: `0x54, 0x47` ("TG" in ASCII) — unique to TollGate 455- `"milliseconds"`: elapsed time ≥ allotment_ms
364- oui_type: `0x01` = price advertisement, `0x02` = mesh routing (future) 456- `"bytes"`: bytes_consumed ≥ allotment_bytes
365- `hop_count`: indicates network depth (0 = directly connected to internet, 1 = one hop away)
366- Price updates are rate-limited to once per 5 seconds to avoid beacon churn
367 457
368**GL-MT3000 (OpenWrt) Compatibility:** 458#### Config Addition
369- OpenWrt supports vendor IEs via `hostapd_cli -i wlan0 set vendor_elements <hex>` + `hostapd_cli -i wlan0 update_beacon`
370- Client scans via `iw dev wlan0 scan` show vendor elements
371- Requires stock OpenWrt 24 firmware (not GL.iNet default) for mac80211 driver access
372- Same OUI/payload format ensures ESP32 ↔ OpenWrt interop
373 459
374**Key Benefits:** 460```json
375- Zero connection overhead for price discovery 461{
376- Works during normal passive/active scanning (no extra frames) 462 "metric": "milliseconds",
377- Prices update live without disconnecting clients 463 "step_size_bytes": 22020096
378- Supports multi-hop mesh routing via `hop_count` 464}
379- Compatible with both ESP32 and Linux (OpenWrt) platforms 465```
380 466
381#### 4B: ESP32-to-OpenWRT TollGate Interop 467#### Test Cases
382 468
383**Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. 469| # | Test | Method | Pass Criteria | Status |
470|---|------|--------|---------------|--------|
471| 49 | Byte allotment calc | Unit test | Correct bytes per step | TODO |
472| 50 | Byte session expiry | Unit test | Expired when consumed ≥ allotment | TODO |
473| 51 | NAPT byte counting | Integration | Counters match actual traffic | TODO |
474| 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO |
475
476### Phase 7: ContextVM Server (MCP over Nostr) — NOT STARTED
477
478**Goal:** Remote configuration of ESP32 TollGate via ContextVM — services communicate over Nostr using public keys as addresses. Exposes configuration as MCP tools accessible by both humans and AI agents.
479
480**New files:** `main/cvm_server.c`, `main/cvm_server.h`, `main/nip44.c`, `main/nip44.h`, `main/mcp_handler.c`, `main/mcp_handler.h`
481
482#### Architecture
483
484ContextVM uses MCP (JSON-RPC 2.0) over NIP-44 encrypted Nostr DMs:
4851. ESP32 subscribes to Nostr relays for DMs addressed to its npub
4862. Incoming DMs are NIP-44 decrypted, parsed as MCP JSON-RPC requests
4873. Dispatched to registered tool handlers
4884. Responses sent back via NIP-44 encrypted DM
489
490#### MCP Tools Exposed
491
492| Tool | Input | Output |
493|------|-------|--------|
494| `get_config` | — | Full config JSON |
495| `set_config` | `{key: value}` | Success/error |
496| `get_balance` | — | `{balance, proof_count}` |
497| `get_sessions` | — | Array of active sessions |
498| `get_usage` | — | Upstream usage if client active |
499| `set_payout` | `{recipients: [...]}` | Success/error |
500| `set_metric` | `{"bytes" or "milliseconds"}` | Success/error |
501| `set_price` | `{price_per_step: N}` | Success/error |
502| `wallet_send` | `{amount_sats: N}` | `{token: "cashuA..."}` |
503| `wallet_melt` | `{bolt11: "ln..."}` | `{preimage: "..."}` |
504
505#### Auth
506
507Only accept commands from owner npub (derived from nsec in config.json).
508
509#### Dependencies
510
511- XChaCha20-Poly1305 (from mbedtls or libsodium)
512- Base64url encoding (already in cashu code)
513- WebSocket listener (extends existing wifistr infrastructure)
514- NIP-44 v2 encryption/decryption
515
516#### Test Cases
517
518| # | Test | Method | Pass Criteria | Status |
519|---|------|--------|---------------|--------|
520| 53 | NIP-44 encrypt/decrypt | Unit test | Roundtrip matches | TODO |
521| 54 | MCP JSON-RPC parse | Unit test | Correct dispatch | TODO |
522| 55 | Config change via DM | Integration | ESP32 applies new config | TODO |
523| 56 | Balance query via CVM | Integration | Returns correct balance | TODO |
384 524
385## Total: 38 + 20 Tests across 4 phases 525## Total: 56 Tests across 7 phases
386 526
387## Testing Infrastructure 527## Testing Infrastructure
388 528
@@ -407,6 +547,7 @@ Host-compiled C tests that verify pure-logic functions with known input/output v
407| `test_nostr_event.c` | `nostr_event.c` | NIP-01 event ID (SHA-256 of canonical JSON), Schnorr signature generation + verification, JSON serialization | 547| `test_nostr_event.c` | `nostr_event.c` | NIP-01 event ID (SHA-256 of canonical JSON), Schnorr signature generation + verification, JSON serialization |
408| `test_cashu.c` | `cashu.c` | `cashu_decode_token()`, `cashu_calculate_allotment_ms()`, `cashu_is_mint_accepted()` | 548| `test_cashu.c` | `cashu.c` | `cashu_decode_token()`, `cashu_calculate_allotment_ms()`, `cashu_is_mint_accepted()` |
409| `test_session.c` | `session.c` | Session lifecycle: create/find/extend/expire/revoke, spent-secret dedup | 549| `test_session.c` | `session.c` | Session lifecycle: create/find/extend/expire/revoke, spent-secret dedup |
550| `test_tollgate_client.c` | `tollgate_client.c` | Discovery parsing, payment flow, renewal logic, state machine |
410 551
411**Run:** `make test-unit` 552**Run:** `make test-unit`
412 553