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
174
175
176
177
178
179
|
# TollGate ESP32 — Progress Checklist
## Phase 0: Bootstrap — COMPLETE
- [x] Create project directory and git repo
- [x] Create .env, .env.example, .gitignore
- [x] Persist PLAN.md and CHECKLIST.md
- [x] Create ESP-IDF project skeleton (CMakeLists, partitions.csv, sdkconfig.defaults)
- [x] Create Makefile with detect/build/flash/test targets
- [x] Run `make detect-all` — identified both boards as ESP32-S3 (16MB flash)
- [x] Fix ESP-IDF v5.4.1 installation (was deeply corrupted, re-cloned)
## Phase 1: Captive Portal + Firewall — COMPLETE (commit `a7d0a67`)
- [x] Implement tollgate_main.c (WiFi AP+STA, event loop)
- [x] Implement config.c/h (SPIFFS JSON config loading)
- [x] Implement dns_server.c/h (DNS hijack/forward per-client)
- [x] Implement captive_portal.c/h (HTTP :80, portal HTML)
- [x] Implement firewall.c/h (NAPT on/off per auth state)
- [x] Set up test infrastructure (Node.js tests, helpers, Playwright)
- [x] Fix WiFi init order bug
- [x] Fix DNS hijack test (nslookup exits 1 for AAAA)
- [x] Fix ping tests (use `-I wlp59s0`)
- [x] Tests 1-14: ALL PASSING
## Phase 2: E-Cash Payments — COMPLETE
### Code Written
- [x] Implement cashu.c/h (Cashu token parse, base64url, checkstate, mint validation)
- [x] Implement session.c/h (time-based allotment, expiry, secret tracking, MAC tracking)
- [x] Implement tollgate_api.c/h (:2121 server, GET/POST /, /usage, /whoami)
- [x] Update captive portal HTML with payment form (Cashu token textarea + "Pay & Connect")
- [x] Wire into tollgate_main.c (session_init, api_start, session_tick loop)
- [x] Per-MAC access tracking: `firewall_get_mac_for_ip()` using `esp_wifi_ap_get_sta_list_with_ip()` + ARP fallback
- [x] Two httpd instances: port 80 (captive portal) and port 2121 (TollGate API)
### Bug Fixes
- [x] Stack overflow: httpd stack_size increased to 32768 (TLS+mbedTLS needs ~20KB)
- [x] Heap allocations: cashu_token_t, cashu_proof_state_t, json_buf, post_body all heap-allocated
- [x] TLS to mint: `esp_crt_bundle_attach` + `esp-tls` in CMakeLists.txt REQUIRES
- [x] HTTP client: `open/write/fetch_headers/read` pattern (not `perform`)
- [x] Token decode: dynamic `json_buf` sizing `malloc((b64_len * 3) / 4 + 4)`, strip trailing `\n`/`\r`
- [x] POST body recv: loop `httpd_req_recv` until all `content_len` bytes read
- [x] `secret_count` bug: capped at `MIN(proof_count, 5)` before `session_create`
- [x] `config.c` default mint URL fixed to `testnut.cashu.space`
- [x] Makefile: nutshell wallet targets (wallet-setup, wallet-info, mint-token, send-token)
- [x] `tests/phase2.mjs`: `/whoami` test checks `includes('mac=')`
### Tests Passing
- [x] Tests 15-24: ALL PASSING
## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — COMPLETE
### nucula Wallet Integration
- [x] Add nucula as git submodule (`nucula_src/`)
- [x] Create `components/secp256k1/` (symlink to nucula's libsecp256k1)
- [x] Create `components/nucula_lib/` (C++ bridge + C API)
- [x] C bridge: `nucula_wallet.h` (init, receive, send, swap_all, balance, proofs_json)
- [x] All wallet operations tested on Board A: pay, swap, send, persistence
### Nostr Identity Derivation (identity.c/h)
- [x] HMAC-SHA512 derivation via mbedtls, npub via secp256k1
- [x] Derive STA/AP MAC, SSID, AP IP from nsec
- [x] Set MACs via `esp_wifi_set_mac()` in boot sequence
- [x] 24/24 unit tests passing
### Nostr Event Signing (nostr_event.c/h)
- [x] NIP-01 canonical JSON, SHA-256 ID, Schnorr signature
- [x] 23/23 unit tests passing
### Geohash Encoding (geohash.c/h)
- [x] Standard base-32 geohash encoding
- [x] 11/11 unit tests passing
### Wifistr Service Discovery (wifistr.c/h)
- [x] kind 38787 event builder + WebSocket relay publish
- [x] Publish on boot + periodic timer (6h default)
- [x] Verified published to relay.damus.io and nos.lol
## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE (commit `78dd599`)
- [x] tollgate_client.c/h — detection, payment, monitoring, state machine
- [x] Config fields: client_enabled, client_steps_to_buy, etc.
- [x] Integration into tollgate_main.c
- [x] 30/30 unit tests passing
## Phase 5: Lightning Auto-Payout — COMPLETE (commit `cb4bd7d`)
- [x] lnurl_pay.c/h — LNURL-pay HTTP flow
- [x] lightning_payout.c/h — periodic balance check, threshold, multi-recipient split, melt
- [x] nucula_wallet_melt() bridge for NUT-05
- [x] Config: payout.enabled, recipients, mints, fee_tolerance, etc.
- [x] 7/7 lnurl_pay + 11/11 lightning_payout = 18 unit tests passing
## Phase 6: Bytes-Based Billing — COMPLETE (commit `edd125d`)
- [x] Dual-metric session support (milliseconds + bytes)
- [x] session_create_bytes(), session_add_bytes()
- [x] Config: metric, step_size_bytes
- [x] Discovery endpoint advertises correct metric
- [x] Unit tests: bytes session lifecycle, mixed metrics
## Phase 7: MCP Handler + NIP-04 + CVM Server — COMPLETE (commit `fdf662f`)
- [x] mcp_handler.c/h — 4 tools (get_config, set_config, get_balance, wallet_send), 25 unit tests
- [x] nip04.c/h — AES-256-CBC + ECDH with 0x02 compressed pubkey prefix, 15 unit tests
- [x] cvm_server.c/h — Nostr DM listener skeleton with FreeRTOS task
- [x] Fixed NIP-04 IV bug: mbedtls_aes_crypt_cbc modifies IV in-place
- [x] Fixed missing esp_random.h include in nip04.c
- [x] 156 total unit tests passing across 10 test binaries
## Bug Fixes — COMPLETE (commit `3342c8e`)
- [x] reset_auth_handler now calls session_revoke_all() before firewall_revoke_all()
- [x] Port 80 /usage shows real session data (remaining/total) instead of "0/0"
- [x] Config metric defaults to "milliseconds" (ESP32 can't track per-client bytes from NAT)
- [x] Fixed sys_evt stack overflow: deferred start_services() to dedicated 32KB task
## Playwright Interop Tests — COMPLETE (commit `4fb44e7`)
- [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop)
- [x] 7 screenshots generated
- [x] Double-spend rejection verified on live hardware
---
## TODO — In Progress
### Per-Client NAT Filtering (Multi-Client Fix)
- [ ] Create `main/lwip_tollgate_hooks.h` — LWIP_HOOK_IP4_CANFORWARD definition
- [ ] Update `CMakeLists.txt` — inject hook header into lwIP compilation
- [ ] Add `tollgate_ip4_canforward_filter()` to `firewall.c` — filter forwarded packets by source IP
- [ ] Change firewall strategy: NAT always ON, per-client filter in lwIP forwarding path
- [ ] Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()` from firewall.c
- [ ] Update `stop_services()` in tollgate_main.c — remove `firewall_disable_nat()` call
- [ ] Add unit test for filter function
- [ ] Build, flash, test on Board A
- [ ] Verify multi-client isolation: expire one client while other is active
### Spent-Secret Cleanup
- [ ] Remove `s_spent_secrets[]` and `session_is_secret_spent()` from `session.c`
- [ ] Remove `spent_secrets` field from `session_t` struct in `session.h`
- [ ] Remove `spent_secrets` params from `session_create()` and `session_create_bytes()`
- [ ] Remove local spent-secret check in `tollgate_api.c` (lines 227-239)
- [ ] Remove `secrets[]` array construction in `tollgate_api.c`
- [ ] Update `tests/unit/test_session.c` — remove secret-tracking tests
- [ ] Run `make test-unit` — all tests pass
### Integration Tests (tests/integration/)
- [ ] Create `tests/integration/` directory
- [ ] Move existing tests (api.mjs, network.mjs, smoke.mjs, phase2.mjs) into integration/
- [ ] Write `test-reset-auth.mjs` — verify sessions cleared after reset
- [ ] Write `test-session-lifecycle.mjs` — pay → verify usage → wait expiry → verify blocked (65s)
- [ ] Write `test-dns-firewall.mjs` — DNS hijack before auth, forward after auth
- [ ] Update Makefile targets for new paths
- [ ] All integration tests passing
### Test Reorganization
- [ ] Fix all hardcoded IPs → `process.env.TOLLGATE_IP`
- [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/`
- [ ] Move `tests/interop-happy-path.spec.mjs` → `tests/e2e/` or `tests/integration/`
- [ ] Move `tests/playwright.config.mjs` → `tests/e2e/`
### Playwright Video Recording Fix
- [ ] Per-test context isolation (not shared serial context)
- [ ] Verify `.webm` files generated in test-results/
### OpenWRT Interop
- [ ] Investigate `nofee.testnut.cashu.space` API compatibility issues
- [ ] Fix cashu CLI v0.19.2 Pydantic validation failures with missing `active` field
### Board B
- [ ] Flash Board B with current firmware (different nsec)
- [ ] Cross-board payment test: Board B → Board A
- [ ] ESP32→ESP32 auto-payment (Scenario 5)
---
## Reminders
- **Commit + push every time a test passes that previously didn't pass**
- Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-C0E9CA`, AP IP `10.192.45.1`
- Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50`
- OpenWRT Router: SSH `root@10.47.41.1`, port 2121
- `source ~/esp/esp-idf/export.sh` before `idf.py`
- Latest commit: `3342c8e`
- 156 unit tests + 18 Playwright tests — all passing
- sudo password: `c03rad0r123`
- Token generation: `cashu -h https://testnut.cashu.space send --legacy 21`
- See `AGENTS.md` for full testing rules
|