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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
|
# Test Coverage + Phase 4/5 + Interop Plan
Ensure full hardware + Playwright test coverage at all times. Restructure main/ to delegate
to tollgate_core. Fix NerdQAxePlus symlink. Add ESP32 ↔ OpenWRT interop tests.
## End State
```
esp32-tollgate (master)
main/ = thin shell calling tollgate_core API
components/tollgate_core/ = THE reusable core (cashu, dns, firewall, session, mining, stratum)
.env = board nsecs + upstream WiFi secrets (gitignored)
esp-miner-nerdqaxeplus (develop)
components/tollgate_core/ = IDF Component Manager override_path → esp32-tollgate
physical-router-test-automation/
esp32/Makefile = ESP32 board tests (lock-protected)
esp32/boards.env = all 3 boards with derived SSID/IP/MAC
mint-health/ = OpenWRT router tests (lock-protected)
Interop targets = ESP32 ↔ Go router bidirectional payment tests
```
## Mutex Rules (MANDATORY)
| Target type | Lock mechanism | Required locks |
|-------------|---------------|----------------|
| ESP32 flash/test | `make -C esp32 lock-{a,b,c}` | Per-board lock file in locks/ |
| OpenWRT router test | `make lock` | hardware.lock |
| Interop (ESP32+router) | Both | Board lock AND hardware.lock |
| Unit tests | None | None (no hardware) |
| Build | None | None (no hardware) |
All hardware access MUST go through lock-protected Makefile targets.
No direct esptool.py, curl, or nmcli against hardware without locks.
## Mints
| Mint | URL | Purpose | Status |
|------|-----|---------|--------|
| Testnut | `https://testnut.cashu.space` | ESP32 boards, auto-pays invoices | UP |
| ~~Testnut no-fee~~ | `https://nofee.testnut.cashu.space` | ~~Alpha router, feeless swaps~~ | **DOWN** (TLS timeout) |
| Orangesync compat | `https://testnut-compat.mints.orangesync.tech` | All routers, mint-token tool default | UP |
| Minibits | `https://mint.minibits.cash/Bitcoin` | Alpha router (secondary) | Unknown |
---
## Phase 0: Fix Stale Infrastructure
- [x] **0.1** Generate random nsec for Board C, store all 3 nsecs in esp32-tollgate/.env
- [x] **0.2** Derive Board C identity (SSID, IP, MACs) from nsec via HMAC-SHA512
- [x] **0.3** Update physical-router-test-automation/esp32/boards.env:
- FIRMWARE_DIR → /home/c03rad0r/esp32-tollgate
- ARCH_FIRMWARE_DIR → /home/c03rad0r/esp32-tollgate
- RELAY_FIRMWARE_DIR → /home/c03rad0r/esp32-tollgate
- Add BOARD_C_SSID, BOARD_C_IP, BOARD_C_UUID
- Update ARCH_IP/ARCH_SSID to Board C's derived values
- [x] **0.4** Update SPIFFS generation to read nsec from .env per board
- [x] **0.5** Fix NerdQAxePlus broken symlink → idf_component.yml with override_path
- [x] **0.6** Remove hardcoded include from NerdQAxePlus main/CMakeLists.txt
- [x] **0.7** Verify NerdQAxePlus builds: BOARD=NERDAXE TOLLGATE=1 idf.py build
- [x] **0.8** Verify esp32-tollgate builds: idf.py build
- [x] **0.9** Verify unit tests pass: make test-unit
- [ ] **0.10** Commit + push
## Phase 1: Baseline Hardware Tests
### Board A (TollGate-B96D80 at 10.185.47.1)
- [x] **1.1** make -C esp32 lock-a PHASE="baseline"
- [x] **1.2** Firmware fix: replaced `xTaskCreate` with `esp_timer` for `start_services()` (commit `09c9a64`)
- [x] **1.3** Captive portal moved to `start_ap_services()` — portal starts on AP_START, not STA GOT IP
- [x] **1.4** Added `/debug` HTTP endpoint (portal_running, start_services_called, sta_got_ip, free_heap)
- [x] **1.5** Smoke test: **6/6 PASS** (SSID visible, portal loads, grant, internet, reset, blocked)
- [x] **1.6** Multi-mint API test: Discovery (kind 10021) OK, Mints endpoint OK, Wallet OK
- [x] **1.7** Unit tests: **407/407 PASS** (22 test suites)
- [x] **1.8** ESP32 E2E tests: 3/14 pass (Board A WiFi became unstable during E2E run)
- [x] **1.9** make -C esp32 unlock-a
**Known issue:** Board A mint health reports `reachable: false` for testnut.cashu.space. STA gets IP but DNS doesn't resolve mint URLs. Wallet funding blocked by `"payment-error-mint-not-accepted"`.
### Board B (TollGate-C0E9CA at 10.192.45.1)
- [x] **1.10** Diagnosed as **hardware issue** — USB CDC ACM stub upload fails at ~64% of app binary. Bootloader/partition table always flash fine. Board B skipped for Phase 1.
### Board C (TollGate-4A2510 at 10.74.63.1)
- [x] **1.11** make -C esp32 lock-c PHASE="baseline-provision"
- [x] **1.12** SPIFFS generated with Board C nsec (71bf3f4d...)
- [x] **1.13** make -C esp32 provision-c — flash + SPIFFS + boot + verify: **SUCCESS**
- [x] **1.14** Debug: all services started, sta_got_ip=true, 8MB heap free
- [x] **1.15** Discovery API: kind 10021 with correct pricing (21 sats/step)
- [x] **1.16** Mints: testnut.cashu.space registered, reachable=false (same STA DNS issue)
- [x] **1.17** Portal HTML loads correctly with server-side template substitution
- [x] **1.18** **ESP32 E2E: 14/14 PASS** (all captive portal tests in 15s)
- [x] **1.19** make -C esp32 unlock-c
### OpenWRT Router Alpha (10.47.41.1)
- [x] **1.20** Fixed mint URL: replaced down `nofee.testnut.cashu.space` with `testnut-compat.mints.orangesync.tech`
- [x] **1.21** Rebuilt `/tmp/mint-token` Go binary from updated source
- [x] **1.22** r-smoke-degraded: **PASSED** (all 9 phases: setup, fund, full merchant, baseline, block, degraded, offline ops, unblock, restore)
- [x] **1.23** r-fix-dns: DNS restored, merchant mode confirmed
- [x] **1.24** r-test-captive-portal-happy: **4/4 desktop-portal tests PASS** (API, cashu input, lightning input, mint buttons)
- [x] **1.25** Portal port fix: tests updated from port 2050 → 80 (nodogsplash)
### Unit Tests (no hardware)
- [x] **1.26** make test-unit: **407/407 PASS** across 22 test suites:
- test_geohash (11), test_identity (24), test_nostr_event (23), test_cashu (14),
- test_session (16), test_tollgate_client (30), test_lnurl_pay (7),
- test_lightning_payout (11), test_mcp_handler (60), test_nip04 (15),
- test_cvm_server (61), test_display (22), test_negentropy_adapter (109),
- test_beacon_price (28), test_market (17), test_mint_health (14),
- test_mining_payment (23), test_stratum_proxy (21), test_session_payment_method (12),
- test_tollgate_client_mining (20), test_firewall_sandbox (16)
### Summary
| Category | Result |
|----------|--------|
| Unit tests | **407/407 PASS** |
| Board A smoke | **6/6 PASS** |
| Board A E2E | **3/14 PASS** (WiFi instability) |
| Board B | **SKIPPED** (USB hardware issue) |
| Board C provision | **SUCCESS** |
| Board C E2E | **14/14 PASS** |
| Router Alpha smoke-degraded | **PASSED** |
| Router Alpha captive portal | **4/4 PASS** |
| Board wallet funding | **BLOCKED** (STA DNS issue) |
## Phase 2: Restructure main/ → tollgate_core
Consolidate 6 duplicated modules. Order: session → firewall → cashu → dns_server → stratum_proxy → mining_payment.
### 2A: session module
- [ ] **2A.1** Diff main/session.c vs tollgate_core/src/tollgate_core_session.c
- [ ] **2A.2** Merge unique main/ functionality into tollgate_core
- [ ] **2A.3** Update tollgate_core.h public API if needed
- [ ] **2A.4** Replace main/session.c with thin wrapper (or remove)
- [ ] **2A.5** Update main/CMakeLists.txt
- [ ] **2A.6** idf.py build
- [ ] **2A.7** make test-unit
- [ ] **2A.8** Quick hardware verify (lock-a, flash-a, arch-test-api-quick, unlock-a)
- [ ] **2A.9** Commit + push
### 2B: firewall module
- [ ] **2B.1** Diff main/firewall.c vs tollgate_core/src/tollgate_core_firewall.c
- [ ] **2B.2** Merge unique main/ functionality into tollgate_core
- [ ] **2B.3** Keep lwip_tollgate_hooks.h in main/ (build-system concern)
- [ ] **2B.4** main/firewall.c becomes thin wrapper calling tollgate_core_fw_* functions
- [ ] **2B.5** Verify LWIP hook still calls tollgate_core_ip4_canforward_filter
- [ ] **2B.6** Update main/CMakeLists.txt
- [ ] **2B.7** idf.py build
- [ ] **2B.8** make test-unit
- [ ] **2B.9** Quick hardware verify
- [ ] **2B.10** Commit + push
### 2C: cashu module
- [ ] **2C.1** Diff main/cashu.c vs tollgate_core/src/tollgate_core_cashu.c
- [ ] **2C.2** Merge unique main/ functionality into tollgate_core
- [ ] **2C.3** Replace main/cashu.c with thin wrapper
- [ ] **2C.4** Update main/CMakeLists.txt
- [ ] **2C.5** idf.py build
- [ ] **2C.6** make test-unit
- [ ] **2C.7** Quick hardware verify
- [ ] **2C.8** Commit + push
### 2D: dns_server module
- [ ] **2D.1** Diff main/dns_server.c vs tollgate_core/src/tollgate_core_dns.c
- [ ] **2D.2** Merge unique main/ functionality into tollgate_core
- [ ] **2D.3** Replace main/dns_server.c with thin wrapper
- [ ] **2D.4** Update main/CMakeLists.txt
- [ ] **2D.5** idf.py build
- [ ] **2D.6** make test-unit
- [ ] **2D.7** Quick hardware verify
- [ ] **2D.8** Commit + push
### 2E: stratum_proxy module
- [ ] **2E.1** Diff main/stratum_proxy.c vs tollgate_core/src/tollgate_core_stratum_proxy.c
- [ ] **2E.2** Merge unique main/ functionality into tollgate_core
- [ ] **2E.3** Replace main/stratum_proxy.c with thin wrapper
- [ ] **2E.4** Update main/CMakeLists.txt
- [ ] **2E.5** idf.py build
- [ ] **2E.6** make test-unit
- [ ] **2E.7** Quick hardware verify
- [ ] **2E.8** Commit + push
### 2F: mining_payment module
- [ ] **2F.1** Diff main/mining_payment.c vs tollgate_core/src/tollgate_core_mining.c
- [ ] **2F.2** Merge unique main/ functionality into tollgate_core
- [ ] **2F.3** Replace main/mining_payment.c with thin wrapper
- [ ] **2F.4** Update main/CMakeLists.txt
- [ ] **2F.5** idf.py build
- [ ] **2F.6** make test-unit
- [ ] **2F.7** Quick hardware verify
- [ ] **2F.8** Commit + push
### 2G: Post-restructure verification
- [ ] **2G.1** Full hardware test (repeat Phase 1)
- [ ] **2G.2** make test-unit — all suites pass
- [ ] **2G.3** Commit + push
## Phase 3: NerdQAxePlus Component Manager Verification
- [ ] **3.1** BOARD=NERDAXE TOLLGATE=1 idf.py build (from esp-miner-nerdqaxeplus)
- [ ] **3.2** Verify tollgate_nerdqaxe_init() links correctly
- [ ] **3.3** Commit + push (if any changes)
## Phase 4: ESP32 ↔ OpenWRT Interop Tests
### Setup infrastructure
- [ ] **4.1** Create esp32/interop-test.sh orchestration script
- [ ] **4.2** Add interop Make targets to esp32/Makefile (all require board lock + hardware.lock)
- [ ] **4.3** Add interop Make targets to top-level Makefile (forwarding)
- [ ] **4.4** Create tests/protocol/esp32-router-interop.spec.mjs (Playwright)
### Direction 1: ESP32 buys from Go router
- [ ] **4.5** interop-esp32-buy-from-alpha: Board A STA → Alpha AP → discover → pay → internet
- [ ] **4.6** interop-esp32-buy-from-beta: Board A STA → Beta AP → discover → pay → internet
### Direction 2: Go router buys from ESP32
- [ ] **4.7** interop-alpha-buy-from-a: Alpha STA → Board A AP → discover → pay → internet
- [ ] **4.8** interop-beta-buy-from-a: Beta STA → Board A AP → discover → pay → internet
### Bidirectional
- [ ] **4.9** interop-bidirectional-a-alpha: Both directions in sequence
- [ ] **4.10** Commit + push
## Phase 5: Post-Restructure Full Test Suite
- [ ] **5.1** make test-unit — all suites pass
- [ ] **5.2** ESP32 full suite (all 3 boards)
- [ ] **5.3** OpenWRT full suite (both routers)
- [ ] **5.4** Playwright E2E (ESP32 captive portal + Go captive portal)
- [ ] **5.5** Interop tests (all directions)
- [ ] **5.6** Final commit + push
---
## LWIP Hook Integration Pattern (for Bitaxe/esp-miner repos)
The LWIP hook provides per-client (per-MAC) NAT access control. Other repos integrate it via:
1. Add to root CMakeLists.txt:
```cmake
idf_component_get_property(lwip lwip COMPONENT_LIB)
target_compile_options(${lwip} PRIVATE "-I${PROJECT_DIR}/main")
target_compile_definitions(${lwip} PRIVATE "-DESP_IDF_LWIP_HOOK_FILENAME=\"lwip_tollgate_hooks.h\"")
```
2. Copy lwip_tollgate_hooks.h to main/ (calls tollgate_core_ip4_canforward_filter)
3. Add tollgate_core as component dependency (override_path or git)
4. Call from platform code:
- tollgate_core_fw_init(ap_ip)
- tollgate_core_fw_grant(client_ip)
- tollgate_core_fw_revoke(client_ip)
- tollgate_core_fw_is_allowed(client_ip)
- tollgate_core_ip4_canforward_filter(p, addr) — called by LWIP hook
## Identity Derivation
All identifiers (SSID, MAC, IP) are derived from nsec via HMAC-SHA512:
```
nsec → secp256k1 → npub (Nostr public key)
nsec → HMAC-SHA512("sta-mac", 0) → STA MAC (locally administered)
nsec → HMAC-SHA512("ap-mac", 0) → AP MAC (locally administered)
AP MAC → SSID = "TollGate-{MAC[3]}{MAC[4]}{MAC[5]}"
AP MAC → IP = 10.{MAC[3]}.{(MAC[4]^MAC[5])%200+10}.1
```
Rotating nsec automatically rotates all identifiers.
## Board Configuration
| Board | nsec source | SSID | AP IP | Port |
|-------|-----------|------|-------|------|
| A | .env BOARD_A_NSEC | TollGate-B96D80 | 10.185.47.1 | /dev/ttyACM0 |
| B | .env BOARD_B_NSEC | TollGate-C0E9CA | 10.192.45.1 | /dev/ttyACM1 |
| C | .env BOARD_C_NSEC | TollGate-4A2510 | 10.74.63.1 | /dev/ttyACM2 |
## OpenWRT Routers
| Router | IP (LAN) | SSID (open) | API | Mint |
|--------|----------|-------------|-----|------|
| Alpha | 10.47.41.1 | TollGate-EVXZ-2.4GHz | :2121 | nofee.testnut, minibits |
| Beta | 192.168.244.1 | TollGate-24A6-2.4GHz | :2121 | testnut-compat.orangesync |
|