diff options
| -rw-r--r-- | docs/TOLLGATE_CORE_DESIGN.md | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/docs/TOLLGATE_CORE_DESIGN.md b/docs/TOLLGATE_CORE_DESIGN.md new file mode 100644 index 0000000..fe31c91 --- /dev/null +++ b/docs/TOLLGATE_CORE_DESIGN.md | |||
| @@ -0,0 +1,380 @@ | |||
| 1 | # TollGate Core Component: Architecture Design | ||
| 2 | |||
| 3 | ## Goal | ||
| 4 | |||
| 5 | Maintain all TollGate business logic in `esp32-tollgate` as a reusable ESP-IDF | ||
| 6 | component (`tollgate_core`), and consume it in `esp-miner` (BitAxe) via the | ||
| 7 | **IDF Component Manager**. No code duplication, no manual sync. | ||
| 8 | |||
| 9 | ## Current State (Pre-Refactoring) | ||
| 10 | |||
| 11 | All TollGate modules live flat in `esp32-tollgate/main/`: | ||
| 12 | |||
| 13 | ``` | ||
| 14 | esp32-tollgate/main/ | ||
| 15 | cashu.c / cashu.h | ||
| 16 | dns_server.c / dns_server.h | ||
| 17 | firewall.c / firewall.h | ||
| 18 | session.c / session.h | ||
| 19 | tollgate_api.c / tollgate_api.h | ||
| 20 | tollgate_client.c / tollgate_client.h | ||
| 21 | config.c / config.h | ||
| 22 | ... | ||
| 23 | ``` | ||
| 24 | |||
| 25 | The ESP-Miner port (`esp-miner/main/tollgate_*.c`) is a manual copy with edits: | ||
| 26 | stripped prefixes (`cashu_` → `tollgate_cashu_`), NVS config instead of | ||
| 27 | `config.h` singleton, removed wallet integration, moved cross-module wiring. | ||
| 28 | |||
| 29 | ### Shared Code by Module | ||
| 30 | |||
| 31 | | Module | Shared % | Key Differences | | ||
| 32 | |--------|----------|-----------------| | ||
| 33 | | cashu | 73% | Config access, mint check parameterized | | ||
| 34 | | dns_server | 74% | Minor logic reorder, logging stripped | | ||
| 35 | | firewall | 94% | Cross-module DNS notification moved | | ||
| 36 | | session | 79% | Bytes metric stripped, DNS notification added | | ||
| 37 | | tollgate_api vs tollgate.c | 13% | Full rewrite (HTTP server vs library API) | | ||
| 38 | | tollgate_client | 0% | No ESP-Miner equivalent | | ||
| 39 | |||
| 40 | ## Target Architecture | ||
| 41 | |||
| 42 | ### Directory Layout (in `esp32-tollgate`) | ||
| 43 | |||
| 44 | ``` | ||
| 45 | esp32-tollgate/ | ||
| 46 | components/ | ||
| 47 | tollgate_core/ ← shared ESP-IDF component | ||
| 48 | CMakeLists.txt | ||
| 49 | idf_component.yml ← component metadata for IDF Component Manager | ||
| 50 | include/ | ||
| 51 | tollgate_core.h ← public API | ||
| 52 | tollgate_platform.h ← platform interface (config/state callbacks) | ||
| 53 | src/ | ||
| 54 | tollgate_core_cashu.c ← from main/cashu.c | ||
| 55 | tollgate_core_cashu.h | ||
| 56 | tollgate_core_dns.c ← from main/dns_server.c | ||
| 57 | tollgate_core_dns.h | ||
| 58 | tollgate_core_firewall.c ← from main/firewall.c | ||
| 59 | tollgate_core_firewall.h | ||
| 60 | tollgate_core_session.c ← from main/session.c | ||
| 61 | tollgate_core_session.h | ||
| 62 | nucula_lib/ ← stays as-is (git submodule + wrapper) | ||
| 63 | CMakeLists.txt | ||
| 64 | nucula_wallet.cpp / .h | ||
| 65 | main/ | ||
| 66 | tollgate_platform.c ← standalone impl of tollgate_platform.h | ||
| 67 | tollgate_api.c / .h ← standalone HTTP server (unchanged) | ||
| 68 | tollgate_client.c / .h ← standalone client mode (unchanged) | ||
| 69 | config.c / config.h ← standalone config (unchanged) | ||
| 70 | ... | ||
| 71 | ``` | ||
| 72 | |||
| 73 | ### How ESP-Miner Consumes It | ||
| 74 | |||
| 75 | In `esp-miner/main/idf_component.yml`: | ||
| 76 | |||
| 77 | ```yaml | ||
| 78 | dependencies: | ||
| 79 | tollgate/core: | ||
| 80 | git: https://github.com/<user>/esp32-tollgate.git | ||
| 81 | path: components/tollgate_core | ||
| 82 | ``` | ||
| 83 | |||
| 84 | ESP-Miner provides only: | ||
| 85 | |||
| 86 | ``` | ||
| 87 | esp-miner/main/ | ||
| 88 | tollgate_platform.c ← implements tollgate_platform.h (NVS config) | ||
| 89 | tollgate.c / .h ← ESP-Miner orchestrator (owner detection, WiFi events) | ||
| 90 | tollgate_page.html ← captive portal payment UI | ||
| 91 | lwip_tollgate_hooks.h ← LWIP hook (stays in esp-miner) | ||
| 92 | http_server.c ← modified to call tollgate_core API | ||
| 93 | ``` | ||
| 94 | |||
| 95 | ### Why IDF Component Manager (not submodule) | ||
| 96 | |||
| 97 | | Aspect | IDF Component Manager | Git Submodule | | ||
| 98 | |--------|----------------------|---------------| | ||
| 99 | | What's downloaded | Only `components/tollgate_core/` | Entire `esp32-tollgate` repo | | ||
| 100 | | Update mechanism | Modify version in yml, rebuild | Manual `git submodule update` | | ||
| 101 | | Transitive deps | Automatic (nucula_lib resolved) | Must manage manually | | ||
| 102 | | CI/CD | Automatic on `idf.py build` | Needs `--recursive` clone | | ||
| 103 | | Offline after first build | Yes (cached in managed_components) | Yes | | ||
| 104 | | Contributor friction | Low (automatic) | Moderate (forgot --recursive) | | ||
| 105 | |||
| 106 | ESP-Miner never reaches into tollgate_core's source tree. It calls a clean API | ||
| 107 | and provides a platform implementation. This is exactly the "packaged API | ||
| 108 | consumption" pattern the Component Manager is designed for. | ||
| 109 | |||
| 110 | ### Why Git Submodule for nucula (not Component Manager) | ||
| 111 | |||
| 112 | nucula is consumed differently — it's a **raw source integration**: | ||
| 113 | |||
| 114 | ```cmake | ||
| 115 | # nucula_lib/CMakeLists.txt reaches INTO the submodule and cherry-picks files: | ||
| 116 | set(NUCULA_SRC ${CMAKE_CURRENT_SOURCE_DIR}/../../nucula_src/main) | ||
| 117 | idf_component_register( | ||
| 118 | SRCS "nucula_wallet.cpp" | ||
| 119 | "${NUCULA_SRC}/crypto.c" # cherry-picked | ||
| 120 | "${NUCULA_SRC}/wallet.cpp" # cherry-picked | ||
| 121 | "${NUCULA_SRC}/cashu_json.cpp" # cherry-picked (6 of ~20 files) | ||
| 122 | "${NUCULA_SRC}/nut10.cpp" | ||
| 123 | "${NUCULA_SRC}/hex.c" | ||
| 124 | "${NUCULA_SRC}/http.c" | ||
| 125 | ... | ||
| 126 | ) | ||
| 127 | ``` | ||
| 128 | |||
| 129 | The Component Manager downloads packaged components — you get everything or | ||
| 130 | nothing. You can't say "give me this component but only compile these 6 files | ||
| 131 | from it." A git submodule gives you the raw source tree on disk, which is what | ||
| 132 | cherry-picking requires. | ||
| 133 | |||
| 134 | **Principle:** Need to reach into source tree and pick files? → Submodule. | ||
| 135 | Only need a clean API? → Component Manager. | ||
| 136 | |||
| 137 | ### The Platform Interface | ||
| 138 | |||
| 139 | ```c | ||
| 140 | // components/tollgate_core/include/tollgate_platform.h | ||
| 141 | |||
| 142 | #ifndef TOLLGATE_PLATFORM_H | ||
| 143 | #define TOLLGATE_PLATFORM_H | ||
| 144 | |||
| 145 | #include <stdint.h> | ||
| 146 | #include <stdbool.h> | ||
| 147 | |||
| 148 | typedef struct { | ||
| 149 | // Config access (each project implements its own storage) | ||
| 150 | uint16_t (*get_price_sats)(void); | ||
| 151 | int32_t (*get_step_ms)(void); | ||
| 152 | const char * (*get_mint_url)(void); | ||
| 153 | const char * (*get_metric)(void); // "milliseconds" or "bytes" | ||
| 154 | int32_t (*get_step_bytes)(void); | ||
| 155 | |||
| 156 | // Time source | ||
| 157 | int64_t (*get_time_ms)(void); | ||
| 158 | |||
| 159 | // Wallet integration: called after proofs verified, before session create | ||
| 160 | // Return true to proceed, false to reject payment | ||
| 161 | // Can be NULL (accepts payment without spending proofs — double-spend risk) | ||
| 162 | bool (*spend_proofs)(const char *raw_token_json); | ||
| 163 | } tollgate_platform_t; | ||
| 164 | |||
| 165 | #endif | ||
| 166 | ``` | ||
| 167 | |||
| 168 | **Standalone implementation** (`main/tollgate_platform.c`): | ||
| 169 | - Reads from `tollgate_config_get()` singleton (SPIFFS-backed) | ||
| 170 | - `spend_proofs` calls `nucula_wallet_receive()` to swap proofs at the mint | ||
| 171 | |||
| 172 | **ESP-Miner implementation** (`main/tollgate_platform.c`): | ||
| 173 | - Reads from `nvs_config_get_*()` (NVS flash) | ||
| 174 | - `spend_proofs` is initially NULL (Phase 1: accept without spending) | ||
| 175 | - Later: calls nucula_wallet_receive when wallet component is integrated | ||
| 176 | |||
| 177 | ### Wallet Integration: The Double-Spend Problem | ||
| 178 | |||
| 179 | The `spend_proofs` hook exists because of a real security gap: | ||
| 180 | |||
| 181 | ``` | ||
| 182 | Client sends Cashu token | ||
| 183 | │ | ||
| 184 | ▼ | ||
| 185 | cashu_decode_token() ← extract proofs | ||
| 186 | │ | ||
| 187 | ▼ | ||
| 188 | cashu_check_proof_states() ← HTTP POST to mint /v1/checkstate: "unspent?" | ||
| 189 | │ | ||
| 190 | ▼ | ||
| 191 | spend_proofs() ← THE CRITICAL STEP | ||
| 192 | │ standalone: nucula_wallet_receive() → swap at mint | ||
| 193 | │ esp-miner: NULL → skipped (double-spend window) | ||
| 194 | ▼ | ||
| 195 | session_create() ← grant client access | ||
| 196 | ``` | ||
| 197 | |||
| 198 | Without `spend_proofs`, a client can replay the same token on multiple devices. | ||
| 199 | Both check "unspent?" → both say yes → both grant access. The swap step marks | ||
| 200 | proofs as spent at the mint, closing the window. | ||
| 201 | |||
| 202 | ESP-Miner accepts this risk initially. When `spend_proofs` is NULL, the | ||
| 203 | component logs a warning. Phase 2 of ESP-Miner integration adds nucula and | ||
| 204 | implements the hook. | ||
| 205 | |||
| 206 | ### Cross-Module Wiring (Internal to tollgate_core) | ||
| 207 | |||
| 208 | The `session → firewall → dns_server` notification chain stays internal: | ||
| 209 | |||
| 210 | ``` | ||
| 211 | tollgate_core_session_create() | ||
| 212 | → tollgate_core_firewall_grant(ip) | ||
| 213 | → tollgate_core_dns_set_authenticated(ip, true) | ||
| 214 | |||
| 215 | tollgate_core_session_revoke() | ||
| 216 | → tollgate_core_firewall_revoke(ip) | ||
| 217 | → tollgate_core_dns_set_authenticated(ip, false) | ||
| 218 | ``` | ||
| 219 | |||
| 220 | Consumers never see this. They call `tollgate_core_process_payment()` and | ||
| 221 | `tollgate_core_tick()`. The internal wiring is an implementation detail. | ||
| 222 | |||
| 223 | ### Full Dependency Graph | ||
| 224 | |||
| 225 | ``` | ||
| 226 | esp-miner | ||
| 227 | └── IDF Component Manager → tollgate_core (API-level boundary) | ||
| 228 | ├── CMakeLists.txt REQUIRES: nucula_lib | ||
| 229 | └── Platform: esp-miner provides tollgate_platform_t (NVS-backed) | ||
| 230 | |||
| 231 | esp32-tollgate (standalone) | ||
| 232 | └── tollgate_core (local component, same repo) | ||
| 233 | ├── CMakeLists.txt REQUIRES: nucula_lib | ||
| 234 | └── Platform: main/tollgate_platform.c (config singleton-backed) | ||
| 235 | |||
| 236 | nucula_lib (local component in esp32-tollgate) | ||
| 237 | └── cherry-picks source files from nucula_src/ (git submodule → zeugmaster/nucula) | ||
| 238 | ``` | ||
| 239 | |||
| 240 | ### Dependency Chain for IDF Component Manager | ||
| 241 | |||
| 242 | When `esp-miner` declares: | ||
| 243 | |||
| 244 | ```yaml | ||
| 245 | dependencies: | ||
| 246 | tollgate/core: | ||
| 247 | git: https://github.com/<user>/esp32-tollgate.git | ||
| 248 | path: components/tollgate_core | ||
| 249 | ``` | ||
| 250 | |||
| 251 | The Component Manager: | ||
| 252 | 1. Clones `esp32-tollgate` (or fetches the component archive) | ||
| 253 | 2. Reads `tollgate_core/idf_component.yml` → finds dependency on `nucula_lib` | ||
| 254 | 3. Since `nucula_lib` is a sibling component in the same repo, resolves it | ||
| 255 | from the same clone | ||
| 256 | 4. Downloads into `managed_components/` | ||
| 257 | 5. `nucula_lib` depends on `secp256k1` (local component) and `nucula_src` | ||
| 258 | (submodule) — these must be available within the cloned repo | ||
| 259 | |||
| 260 | **Note:** The git submodule within `nucula_src` needs verification. The IDF | ||
| 261 | Component Manager may or may not initialize submodules within a git-sourced | ||
| 262 | dependency. This needs testing. If it doesn't, `nucula_lib` may need to bundle | ||
| 263 | the required nucula source files directly instead of referencing a submodule. | ||
| 264 | |||
| 265 | ## Blocking Dependencies | ||
| 266 | |||
| 267 | This refactoring **must not proceed** until these branches land on master: | ||
| 268 | |||
| 269 | | Branch | Blocking Files | Status | | ||
| 270 | |--------|---------------|--------| | ||
| 271 | | `feature/multi-mint-support` | `cashu.c`, `tollgate_api.c`, `main/CMakeLists.txt`, `nucula_wallet.cpp/h`, `captive_portal.c`, `mint_health.c/h`, `config.c/h` | **In progress** | | ||
| 272 | | `feature/price-discovery` | `tollgate_api.c`, `tollgate_client.c`, `main/CMakeLists.txt`, `config.c/h`, `beacon_price.c/h`, `market.c/h` | **In progress** | | ||
| 273 | | `feature/cvm-integration` | Same commit as master — no new changes | **Merged already** | | ||
| 274 | |||
| 275 | **Specific conflicts if we refactor now:** | ||
| 276 | - Moving `cashu.c` → `tollgate_core_cashu.c` while multi-mint modifies `cashu.c` | ||
| 277 | - Moving `dns_server.c` while price-discovery may touch it | ||
| 278 | - Modifying `main/CMakeLists.txt` (remove SRCS) while all branches modify it | ||
| 279 | - Modifying `tollgate_api.c` call sites while multi-mint and price-discovery modify it | ||
| 280 | |||
| 281 | ## Refactoring Plan (After Blocking PRs Merge) | ||
| 282 | |||
| 283 | ### Phase 0: Prerequisites | ||
| 284 | |||
| 285 | - [ ] All blocking PRs merged to master | ||
| 286 | - [ ] This branch rebased onto latest master | ||
| 287 | - [ ] Full build passes on master | ||
| 288 | |||
| 289 | ### Phase 1: Create Component Skeleton | ||
| 290 | |||
| 291 | - [ ] Create `components/tollgate_core/` directory structure | ||
| 292 | - [ ] Create `components/tollgate_core/include/tollgate_core.h` (public API) | ||
| 293 | - [ ] Create `components/tollgate_core/include/tollgate_platform.h` (platform interface) | ||
| 294 | - [ ] Create `components/tollgate_core/idf_component.yml` (component metadata) | ||
| 295 | - [ ] Create `components/tollgate_core/CMakeLists.txt` (register component, REQUIRES nucula_lib) | ||
| 296 | - [ ] Verify empty component builds without errors | ||
| 297 | |||
| 298 | ### Phase 2: Move Core Modules (one at a time, build after each) | ||
| 299 | |||
| 300 | - [ ] Copy `main/cashu.c/h` → `components/tollgate_core/src/tollgate_core_cashu.c/h` | ||
| 301 | - [ ] Rename functions: `cashu_*` → `tollgate_core_cashu_*` | ||
| 302 | - [ ] Replace `tollgate_config_get()` calls with platform callbacks | ||
| 303 | - [ ] Remove direct `config.h` include | ||
| 304 | - [ ] Build and verify | ||
| 305 | - [ ] Copy `main/dns_server.c/h` → `components/tollgate_core/src/tollgate_core_dns.c/h` | ||
| 306 | - [ ] Rename functions: `dns_server_*` → `tollgate_core_dns_*` | ||
| 307 | - [ ] No platform dependencies (pure LWIP) — should be clean copy | ||
| 308 | - [ ] Build and verify | ||
| 309 | - [ ] Copy `main/firewall.c/h` → `components/tollgate_core/src/tollgate_core_firewall.c/h` | ||
| 310 | - [ ] Rename functions: `firewall_*` → `tollgate_core_firewall_*` | ||
| 311 | - [ ] Internalize `dns_set_authenticated` calls (keep within component) | ||
| 312 | - [ ] Remove `dns_server.h` external dependency | ||
| 313 | - [ ] Build and verify | ||
| 314 | - [ ] Copy `main/session.c/h` → `components/tollgate_core/src/tollgate_core_session.c/h` | ||
| 315 | - [ ] Rename functions: `session_*` → `tollgate_core_session_*` | ||
| 316 | - [ ] Replace `config.h` calls with platform callbacks for metric check | ||
| 317 | - [ ] Internalize firewall notification (already calls firewall directly) | ||
| 318 | - [ ] Support both time and bytes metrics (portable, not stripped) | ||
| 319 | - [ ] Build and verify | ||
| 320 | |||
| 321 | ### Phase 3: Wire Component API | ||
| 322 | |||
| 323 | - [ ] Implement `tollgate_core_init(const tollgate_platform_t *platform)` — stores platform, inits all sub-modules | ||
| 324 | - [ ] Implement `tollgate_core_process_payment(ip, token)` — decode → verify → spend → create session | ||
| 325 | - [ ] Implement `tollgate_core_client_connected(mac, ip)` — owner detection + firewall check | ||
| 326 | - [ ] Implement `tollgate_core_client_disconnected(mac)` — session cleanup + owner reassign | ||
| 327 | - [ ] Implement `tollgate_core_tick()` — session expiry check | ||
| 328 | - [ ] Implement `tollgate_core_get_status_json()` — JSON status | ||
| 329 | - [ ] Implement `tollgate_core_get_config_json()` — JSON config (via platform) | ||
| 330 | - [ ] Build and verify standalone | ||
| 331 | |||
| 332 | ### Phase 4: Standalone Platform Implementation | ||
| 333 | |||
| 334 | - [ ] Create `main/tollgate_platform.c` implementing `tollgate_platform_t` | ||
| 335 | - [ ] `get_price_sats` → `tollgate_config_get()->price_per_step` | ||
| 336 | - [ ] `get_step_ms` → `tollgate_config_get()->step_size` | ||
| 337 | - [ ] `get_mint_url` → `tollgate_config_get()->mint_url` | ||
| 338 | - [ ] `get_metric` → `tollgate_config_get()->metric` | ||
| 339 | - [ ] `get_step_bytes` → `tollgate_config_get()->step_bytes` | ||
| 340 | - [ ] `get_time_ms` → `xTaskGetTickCount() * portTICK_PERIOD_MS` | ||
| 341 | - [ ] `spend_proofs` → `nucula_wallet_receive()` | ||
| 342 | - [ ] Update `main/tollgate_api.c` to call `tollgate_core_*` instead of direct module calls | ||
| 343 | - [ ] Update `main/tollgate_main.c` init sequence | ||
| 344 | - [ ] Remove old `main/cashu.c`, `main/dns_server.c`, `main/firewall.c`, `main/session.c` | ||
| 345 | - [ ] Update `main/CMakeLists.txt` (remove old SRCS, add `tollgate_platform.c`) | ||
| 346 | - [ ] Full standalone build + test | ||
| 347 | |||
| 348 | ### Phase 5: ESP-Miner Integration | ||
| 349 | |||
| 350 | - [ ] Update `esp-miner/main/idf_component.yml` to add tollgate_core dependency | ||
| 351 | - [ ] Create `esp-miner/main/tollgate_platform.c` implementing `tollgate_platform_t` | ||
| 352 | - [ ] Config reads from NVS (`nvs_config_get_*`) | ||
| 353 | - [ ] `spend_proofs` = NULL initially (Phase 1: accept without spending) | ||
| 354 | - [ ] Update `esp-miner/main/tollgate.c` to call `tollgate_core_*` API | ||
| 355 | - [ ] Remove `esp-miner/main/tollgate_cashu.c`, `tollgate_dns.c`, `tollgate_firewall.c`, `tollgate_session.c` | ||
| 356 | - [ ] Update `esp-miner/main/CMakeLists.txt` (remove old SRCS) | ||
| 357 | - [ ] Full ESP-Miner build + test | ||
| 358 | |||
| 359 | ### Phase 6: Verify Component Manager Flow | ||
| 360 | |||
| 361 | - [ ] Remove local `managed_components/` if present | ||
| 362 | - [ ] Run `idf.py reconfigure` in esp-miner — verify Component Manager downloads tollgate_core | ||
| 363 | - [ ] Run `idf.py build` — verify transitive dependency resolution (nucula_lib + nucula_src) | ||
| 364 | - [ ] Test that submodule within nucula_src is properly initialized by Component Manager | ||
| 365 | - [ ] If submodule init fails: bundle nucula source files directly in nucula_lib instead | ||
| 366 | |||
| 367 | ### Phase 7: Documentation and Cleanup | ||
| 368 | |||
| 369 | - [ ] Update `esp-miner/main/idf_component.yml` with correct git URL | ||
| 370 | - [ ] Update `esp-miner/TOLLGATE_PR_PLAN.md` to reflect component-based architecture | ||
| 371 | - [ ] Add `docs/` to `tollgate_core` with integration guide for new consumers | ||
| 372 | - [ ] Update `esp-miner/TOLLGATE_CHECKLIST.md` | ||
| 373 | - [ ] Verify both projects build clean from scratch | ||
| 374 | |||
| 375 | ## Open Questions | ||
| 376 | |||
| 377 | - [ ] Does the IDF Component Manager initialize git submodules within git-sourced dependencies? | ||
| 378 | - [ ] Should tollgate_core publish to the ESP Component Registry (public) or stay git-only? | ||
| 379 | - [ ] What versioning scheme for tollgate_core? (semver tags in esp32-tollgate?) | ||
| 380 | - [ ] Should `tollgate_client.c` (client mode) eventually move into tollgate_core? | ||