diff options
| author | Your Name <you@example.com> | 2026-05-16 15:32:55 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-16 15:32:55 +0530 |
| commit | 133e40c82afb4d7659758b1fa57925ac57af4621 (patch) | |
| tree | 130f43e668f42f8fcc8ef55808ba89b6db40f615 /PLAN.md | |
| parent | 8f0aeb7d7b8216f1fc906cf855e5be9e90ecc0a8 (diff) | |
Phase 3: on-device Cashu wallet with mbedTLS secp256k1 + SPIFFS persistence + PSRAM
- wallet.c/h: secp256k1 ECP primitives (hash_to_curve, scalar_mul, point_add)
- wallet_persist.c/h: SPIFFS persistence with threshold-based write protection
- Fee accounting for swap (input_fee_ppk from /v1/keysets)
- Keyset fetch via /v1/keysets (586 bytes vs 21KB for /v1/keys)
- Wallet API: GET /wallet, POST /wallet/swap, POST /wallet/send
- Payment proofs auto-stored to wallet + persisted on SPIFFS
- PSRAM enabled for large allocations (ESP32-S3 has 8MB)
- Wallet init deferred to dedicated task (avoids sys_evt stack overflow)
- Cashu proof ID buffer size fixed (66 hex chars, not 16)
- HTTP client: added fetch_headers() call for proper response handling
- persist_threshold_sats config parameter (default: 1 sat)
Diffstat (limited to 'PLAN.md')
| -rw-r--r-- | PLAN.md | 96 |
1 files changed, 73 insertions, 23 deletions
| @@ -2,12 +2,12 @@ | |||
| 2 | 2 | ||
| 3 | ## Overview | 3 | ## Overview |
| 4 | 4 | ||
| 5 | Build a TollGate firmware for two ESP32 devices, following the [TollGate protocol spec](https://github.com/OpenTollGate/tollgate) (TIP-01, TIP-02, HTTP-01/02/03). The implementation uses ESP-IDF (C/C++) and integrates the nucula Cashu wallet. | 5 | Build a TollGate firmware for two ESP32 devices, following the [TollGate protocol spec](https://github.com/OpenTollGate/tollgate) (TIP-01, TIP-02, HTTP-01/02/03). The implementation uses ESP-IDF (C/C++) with an on-device Cashu wallet using mbedTLS secp256k1. |
| 6 | 6 | ||
| 7 | ## Architecture Decision: C/C++ (ESP-IDF) | 7 | ## Architecture Decision: C/C++ (ESP-IDF) |
| 8 | 8 | ||
| 9 | - Existing working captive portal is in C (ESP-IDF) | 9 | - Existing working captive portal is in C (ESP-IDF) |
| 10 | - Nucula Cashu wallet is in C/C++ (ESP-IDF) | 10 | - On-device Cashu wallet uses mbedTLS secp256k1 (hardware RNG, software ECP) |
| 11 | - ESP-IDF is already installed at `~/esp/esp-idf` | 11 | - ESP-IDF is already installed at `~/esp/esp-idf` |
| 12 | - No Rust/ESP32 toolchain installed | 12 | - No Rust/ESP32 toolchain installed |
| 13 | 13 | ||
| @@ -16,11 +16,12 @@ Build a TollGate firmware for two ESP32 devices, following the [TollGate protoco | |||
| 16 | | Layer | Technology | | 16 | | Layer | Technology | |
| 17 | |-------|-----------| | 17 | |-------|-----------| |
| 18 | | Framework | ESP-IDF v5.4.1 (C/C++) | | 18 | | Framework | ESP-IDF v5.4.1 (C/C++) | |
| 19 | | Cashu wallet | nucula `Wallet` class (Phase 3) | | 19 | | Cashu wallet | Custom mbedTLS secp256k1 wallet (hash_to_curve, blind signing, swap, send) | |
| 20 | | HTTP server | `esp_http_server` (port 80 captive portal, port 2121 TollGate API) | | 20 | | HTTP server | `esp_http_server` (port 80 captive portal, port 2121 TollGate API + wallet) | |
| 21 | | DNS | Custom UDP task (hijack unauthenticated, forward authenticated) | | 21 | | DNS | Custom UDP task (hijack unauthenticated, forward authenticated) | |
| 22 | | NAT | lwIP NAPT | | 22 | | NAT | lwIP NAPT | |
| 23 | | Testing | Playwright + curl + pyserial | | 23 | | Persistence | SPIFFS (960K partition) with threshold-based write protection | |
| 24 | | Testing | Playwright + curl + nutshell CLI | | ||
| 24 | | Build | Makefile | | 25 | | Build | Makefile | |
| 25 | 26 | ||
| 26 | ## Four-Phase Plan | 27 | ## Four-Phase Plan |
| @@ -52,7 +53,7 @@ Build a TollGate firmware for two ESP32 devices, following the [TollGate protoco | |||
| 52 | | 13 | Reset auth | GET /reset_authentication | 200 | PASS | | 53 | | 13 | Reset auth | GET /reset_authentication | 200 | PASS | |
| 53 | | 14 | Internet blocked after reset | ping 8.8.8.8 | Fails | PASS | | 54 | | 14 | Internet blocked after reset | ping 8.8.8.8 | Fails | PASS | |
| 54 | 55 | ||
| 55 | ### Phase 2: E-Cash Payments — IN PROGRESS | 56 | ### Phase 2: E-Cash Payments — COMPLETE |
| 56 | 57 | ||
| 57 | **Goal:** Replace free access with Cashu payment. ESP32 parses token, checks proof state via mint API, grants time-based session. | 58 | **Goal:** Replace free access with Cashu payment. ESP32 parses token, checks proof state via mint API, grants time-based session. |
| 58 | 59 | ||
| @@ -79,29 +80,78 @@ Build a TollGate firmware for two ESP32 devices, following the [TollGate protoco | |||
| 79 | | 26 | Client isolation | Only payer gets internet | Non-payer blocked | Phase 3 | | 80 | | 26 | Client isolation | Only payer gets internet | Non-payer blocked | Phase 3 | |
| 80 | | 27 | Full e2e: portal→pay→browse | Playwright | Complete flow | Phase 3 | | 81 | | 27 | Full e2e: portal→pay→browse | Playwright | Complete flow | Phase 3 | |
| 81 | 82 | ||
| 82 | **Captive Portal Fix:** Added DoT reject server on port 853 (TCP RST forces DNS-over-TLS fallback to plain DNS), DNS hijack returns NXDOMAIN for all non-A query types, explicit 302 redirect handlers for all captive detection URIs. Needs verification with actual GrapheneOS phone. | 83 | **Captive Portal Detection:** DoT reject server on port 853, NXDOMAIN for non-A queries, 302 redirects for captive URIs. Verified working on GrapheneOS (commit `236b61d`). |
| 83 | 84 | ||
| 84 | ### Phase 3: nucula Wallet + ESP32-to-ESP32 Payments — NOT STARTED | 85 | ### Phase 3: On-Device Wallet + ESP32-to-ESP32 Payments — IN PROGRESS |
| 85 | 86 | ||
| 86 | **Goal:** Integrate nucula's full Cashu wallet. ESP32 holds balance, can be a reseller. ESP32-to-ESP32 direct payments. | 87 | **Goal:** On-device Cashu wallet using mbedTLS secp256k1. ESP32 holds balance, can swap proofs, create tokens for P2P payments. Proof persistence via SPIFFS with threshold-based write protection. |
| 87 | 88 | ||
| 88 | **11 Additional Test Cases:** | 89 | #### Wallet Architecture |
| 89 | | # | Test | Method | Pass Criteria | | 90 | |
| 90 | |---|------|--------|---------------| | 91 | - **Crypto**: mbedTLS secp256k1 (software ECP, hardware RNG via `esp_fill_random`) |
| 91 | | 28 | Wallet boot | Serial | Keysets loaded | | 92 | - **Blind signing**: `hash_to_curve()` (SHA256 try-and-increment), `scalar_mul()`, `point_add()` |
| 92 | | 29 | Receive via wallet | POST :2121/ | Balance incremented | | 93 | - **Unblinding**: `C = C_ + (order - r) * G` — avoids needing mint's public key K, avoids point negation |
| 93 | | 30 | Balance persists | Reboot | Same balance | | 94 | - **Proof storage**: In-memory array (50 max), persisted to SPIFFS JSON |
| 94 | | 31 | Payout routine | Wait + serial | Tokens melted to LN | | 95 | - **Persistence**: SPIFFS `/spiffs/wallet.json`, only written when `balance >= persist_threshold_sats` |
| 95 | | 32 | Reseller discover | Serial | Upstream TollGate found | | 96 | - **Keyset fetch**: GET /v1/keys from mint on boot |
| 96 | | 33 | Reseller pay | Serial + API | Token POSTed upstream | | 97 | - **Swap**: POST /v1/swap — reissues proofs with new secrets |
| 97 | | 34 | Multi-hop internet | Ping from laptop | laptop→A→B→internet | | 98 | - **Token creation**: Encode proofs as `cashuA` base64url token |
| 98 | | 35 | P2PK receive | Post P2PK token | Auto-signed, accepted | | 99 | |
| 99 | | 36 | DLEQ verified | Post token with DLEQ | Verified, accepted | | 100 | #### Wallet Endpoints (on :2121) |
| 100 | | 37 | 5 consecutive payments | Loop | All authenticated | | 101 | |
| 101 | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | | 102 | | Method | Path | Description | |
| 103 | |--------|------|-------------| | ||
| 104 | | GET | /wallet | Balance, proof count, keyset count | | ||
| 105 | | POST | /wallet/swap | Swap all proofs for fresh ones via mint | | ||
| 106 | | POST | /wallet/send | Create cashuA token for given amount (body = sat count) | | ||
| 107 | |||
| 108 | #### Payment Integration | ||
| 109 | |||
| 110 | Received payment proofs are automatically added to wallet after session creation in `tollgate_api.c`. | ||
| 111 | |||
| 112 | #### Persistence Threshold | ||
| 113 | |||
| 114 | Config parameter `persist_threshold_sats` (default: 1) controls when wallet state is written to flash: | ||
| 115 | - `balance >= persist_threshold_sats` → write wallet.json | ||
| 116 | - `balance < threshold` → skip write (or delete existing file) | ||
| 117 | - Rationale: flash has finite write cycles (~100K erase per sector); only persist when e-cash value justifies the wear cost | ||
| 118 | - SPIFFS wear-leveling spreads writes across the 960K partition | ||
| 119 | |||
| 120 | #### Test Cases | ||
| 121 | |||
| 122 | | # | Test | Method | Pass Criteria | Status | | ||
| 123 | |---|------|--------|---------------|--------| | ||
| 124 | | 28 | Wallet boot | Serial | Keysets loaded | TODO | | ||
| 125 | | 29 | Receive via wallet | POST :2121/ | Balance incremented | TODO | | ||
| 126 | | 30 | Wallet swap | POST /wallet/swap | Same balance, new proofs | TODO | | ||
| 127 | | 31 | Wallet send | POST /wallet/send | Valid cashuA token returned | TODO | | ||
| 128 | | 32 | Persistence survives reboot | Reboot + GET /wallet | Same balance | TODO | | ||
| 129 | | 33 | Cross-board payment | B sends → A receives | A balance increases | TODO | | ||
| 130 | | 34 | Two clients pay independently | Two POSTs | Both authenticated | TODO | | ||
| 131 | | 35 | Client isolation | Only payer gets internet | Non-payer blocked | TODO | | ||
| 132 | | 36 | Full e2e: portal→pay→browse | Playwright | Complete flow | TODO | | ||
| 133 | | 37 | 5 consecutive payments | Loop | All authenticated | TODO | | ||
| 134 | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | | ||
| 102 | 135 | ||
| 103 | ### Phase 4: ESP32-to-OpenWRT TollGate Interop — NOT STARTED | 136 | ### Phase 4: ESP32-to-OpenWRT TollGate Interop — NOT STARTED |
| 104 | 137 | ||
| 105 | **Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. | 138 | **Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. |
| 106 | 139 | ||
| 107 | ## Total: 38 Tests across 4 phases | 140 | ## Total: 38 Tests across 4 phases |
| 141 | |||
| 142 | ## Key Technical Notes | ||
| 143 | |||
| 144 | ### mbedTLS 3.x Compatibility | ||
| 145 | - `mbedtls_ecp_point` is opaque — cannot access `.X`, `.Y`, `.Z` directly | ||
| 146 | - Use `mbedtls_ecp_muladd`, `mbedtls_ecp_mul`, `mbedtls_ecp_point_read/write_binary` | ||
| 147 | - No point negation needed with `C = C_ + (order - r) * G` unblinding approach | ||
| 148 | |||
| 149 | ### Board Configuration | ||
| 150 | - Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-377C`, AP IP `10.55.85.1` | ||
| 151 | - Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50`, unique SSID/IP derived from MAC | ||
| 152 | - Both boards run identical firmware, unique config derived at boot from factory MAC | ||
| 153 | |||
| 154 | ### Test Mint | ||
| 155 | - `testnut.cashu.space` — auto-pays lightning invoices for testing | ||
| 156 | - `cashu -h https://testnut.cashu.space invoice <amount>` → auto-paid | ||
| 157 | - `cashu -h https://testnut.cashu.space send --legacy <amount>` → generates cashuA token | ||