diff options
Diffstat (limited to 'PLAN.md')
| -rw-r--r-- | PLAN.md | 204 |
1 files changed, 180 insertions, 24 deletions
| @@ -473,21 +473,48 @@ uint64_t bytes_consumed; | |||
| 473 | | 51 | NAPT byte counting | Integration | Counters match actual traffic | 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 | | 474 | | 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO | |
| 475 | 475 | ||
| 476 | ### Phase 7: ContextVM Server (MCP over Nostr) — COMPLETE | 476 | ### Phase 7: ContextVM Server (MCP over Nostr) — REWRITE IN PROGRESS |
| 477 | 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. | 478 | **Goal:** Full ContextVM protocol implementation — ESP32 acts as an MCP server discoverable on the Nostr network via CEP-6 public announcements, communicating via kind 25910 ephemeral events. |
| 479 | 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` | 480 | **Protocol:** ContextVM transports MCP JSON-RPC 2.0 messages over Nostr. Server is identified by its npub (derived from nsec). Clients discover the server via kind 11316 announcements, then communicate via kind 25910 ephemeral events. |
| 481 | 481 | ||
| 482 | #### Architecture | 482 | #### Architecture |
| 483 | 483 | ||
| 484 | ContextVM uses MCP (JSON-RPC 2.0) over NIP-44 encrypted Nostr DMs: | 484 | ``` |
| 485 | 1. ESP32 subscribes to Nostr relays for DMs addressed to its npub | 485 | Client (nak/ContextVM SDK) |
| 486 | 2. Incoming DMs are NIP-44 decrypted, parsed as MCP JSON-RPC requests | 486 | → publishes kind 25910 to relay ({"method":"tools/call","params":{"name":"get_config"}}) |
| 487 | 3. Dispatched to registered tool handlers | 487 | → ESP32 cvm_server reads from persistent WebSocket subscription |
| 488 | 4. Responses sent back via NIP-44 encrypted DM | 488 | → parses MCP JSON-RPC from event content |
| 489 | → dispatches to mcp_handler.c | ||
| 490 | → publishes kind 25910 response back to relay | ||
| 491 | → client receives response via subscription | ||
| 492 | ``` | ||
| 493 | |||
| 494 | #### ContextVM Event Kinds Used | ||
| 495 | |||
| 496 | | Kind | Purpose | CEP | | ||
| 497 | |------|---------|-----| | ||
| 498 | | 25910 | MCP request/response transport (ephemeral) | Draft spec | | ||
| 499 | | 11316 | Server announcement (replaceable) | CEP-6 | | ||
| 500 | | 11317 | Tools list announcement (replaceable) | CEP-6 | | ||
| 501 | | 10002 | Relay list (replaceable) | CEP-17 (NIP-65) | | ||
| 502 | |||
| 503 | #### MCP Protocol Flow | ||
| 489 | 504 | ||
| 490 | #### MCP Tools Exposed | 505 | 1. ESP32 publishes kind 11316 (server announcement) + kind 11317 (tools list) + kind 10002 (relay list) on startup |
| 506 | 2. ESP32 opens persistent WebSocket to relays, subscribes to `{"kinds":[25910],"#p":["<npub>"]}` | ||
| 507 | 3. Client sends kind 25910 `initialize` request | ||
| 508 | 4. ESP32 responds with kind 25910 `initialize` result (capabilities, serverInfo) | ||
| 509 | 5. Client sends `notifications/initialized` | ||
| 510 | 6. Client calls `tools/list` or `tools/call` | ||
| 511 | 7. ESP32 dispatches to `mcp_handler.c`, returns result | ||
| 512 | |||
| 513 | #### Encryption | ||
| 514 | |||
| 515 | Phase 7a ships with **plaintext** kind 25910 events. Encryption (CEP-4: NIP-44 gift wrap) is deferred to Phase 7b. The `support_encryption` tag is NOT included in announcements until Phase 7b. | ||
| 516 | |||
| 517 | #### MCP Tools Exposed (10 total) | ||
| 491 | 518 | ||
| 492 | | Tool | Input | Output | | 519 | | Tool | Input | Output | |
| 493 | |------|-------|--------| | 520 | |------|-------|--------| |
| @@ -497,32 +524,89 @@ ContextVM uses MCP (JSON-RPC 2.0) over NIP-44 encrypted Nostr DMs: | |||
| 497 | | `get_sessions` | — | Array of active sessions | | 524 | | `get_sessions` | — | Array of active sessions | |
| 498 | | `get_usage` | — | Upstream usage if client active | | 525 | | `get_usage` | — | Upstream usage if client active | |
| 499 | | `set_payout` | `{recipients: [...]}` | Success/error | | 526 | | `set_payout` | `{recipients: [...]}` | Success/error | |
| 500 | | `set_metric` | `{"bytes" or "milliseconds"}` | Success/error | | 527 | | `set_metric` | `{"metric": "bytes" or "milliseconds"}` | Success/error | |
| 501 | | `set_price` | `{price_per_step: N}` | Success/error | | 528 | | `set_price` | `{"price_per_step": N}` | Success/error | |
| 502 | | `wallet_send` | `{amount_sats: N}` | `{token: "cashuA..."}` | | 529 | | `wallet_send` | `{"amount": N}` | `{token: "cashuA..."}` | |
| 503 | | `wallet_melt` | `{bolt11: "ln..."}` | `{preimage: "..."}` | | 530 | | `wallet_melt` | `{"bolt11": "ln..."}` | `{preimage: "..."}` | |
| 504 | 531 | ||
| 505 | #### Auth | 532 | #### Auth |
| 506 | 533 | ||
| 507 | Only accept commands from owner npub (derived from nsec in config.json). | 534 | Only accept kind 25910 requests from owner npub (derived from nsec in config.json). Non-owner requests are silently dropped. |
| 508 | 535 | ||
| 509 | #### Dependencies | 536 | #### Dependencies |
| 510 | 537 | ||
| 511 | - XChaCha20-Poly1305 (from mbedtls or libsodium) | 538 | - WebSocket persistent connection (extends `wifistr.c` TLS + WS pattern) |
| 512 | - Base64url encoding (already in cashu code) | 539 | - secp256k1 Schnorr signing (existing `nostr_event.c`) |
| 513 | - WebSocket listener (extends existing wifistr infrastructure) | 540 | - cJSON (existing) |
| 514 | - NIP-44 v2 encryption/decryption | 541 | - mbedtls TLS (existing) |
| 542 | - NIP-04 encryption (existing `nip04.c`) — for future encrypted mode | ||
| 543 | |||
| 544 | #### Files | ||
| 545 | |||
| 546 | | File | Status | Purpose | | ||
| 547 | |------|--------|---------| | ||
| 548 | | `main/cvm_server.c` | Rewrite | WS listener, MCP handlers, CEP-6 announcements | | ||
| 549 | | `main/cvm_server.h` | Update | New public API | | ||
| 550 | | `main/mcp_handler.c` | Extend | 6 new tools | | ||
| 551 | | `main/mcp_handler.h` | Update | New tool enums + handlers | | ||
| 552 | | `main/config.c` | Minor | Default `cvm_enabled = true` | | ||
| 553 | | `tests/unit/test_cvm_server.c` | New | CVM unit tests | | ||
| 554 | | `tests/unit/test_mcp_handler.c` | Extend | 6 new tool tests | | ||
| 555 | | `tests/integration/test-cvm.mjs` | New | CVM integration test via nak | | ||
| 556 | | `Makefile` | Update | `cvm-*` targets | | ||
| 515 | 557 | ||
| 516 | #### Test Cases | 558 | #### Test Cases |
| 517 | 559 | ||
| 518 | | # | Test | Method | Pass Criteria | Status | | 560 | | # | Test | Method | Pass Criteria | Status | |
| 519 | |---|------|--------|---------------|--------| | 561 | |---|------|--------|---------------|--------| |
| 520 | | 53 | NIP-44 encrypt/decrypt | Unit test | Roundtrip matches | TODO | | 562 | | 53 | MCP JSON-RPC parse from kind 25910 | Unit test | Correct dispatch to tool handler | PASS | |
| 521 | | 54 | MCP JSON-RPC parse | Unit test | Correct dispatch | TODO | | 563 | | 54 | Kind 11316 announcement construction | Unit test | Valid event with correct tags/capabilities | PASS | |
| 522 | | 55 | Config change via DM | Integration | ESP32 applies new config | TODO | | 564 | | 55 | Kind 11317 tools list construction | Unit test | All 10 tools listed with schemas | PASS | |
| 523 | | 56 | Balance query via CVM | Integration | Returns correct balance | TODO | | 565 | | 56 | Kind 10002 relay list construction | Unit test | Correct `r` tags | PASS | |
| 524 | 566 | | 57 | Auth rejection for non-owner | Unit test | Non-owner events dropped | PASS | | |
| 525 | ## Total: 56 Tests across 7 phases | 567 | | 58 | MCP initialize response | Unit test | Correct capabilities + serverInfo | PASS | |
| 568 | | 59 | New tool: get_sessions | Unit test | Returns session array | PASS | | ||
| 569 | | 60 | New tool: get_usage | Unit test | Returns usage stats | PASS | | ||
| 570 | | 61 | New tool: set_payout | Unit test | Updates payout config | PASS | | ||
| 571 | | 62 | New tool: set_metric | Unit test | Updates metric field | PASS | | ||
| 572 | | 63 | New tool: set_price | Unit test | Updates price_per_step | PASS | | ||
| 573 | | 64 | New tool: wallet_melt | Unit test | Calls nucula_wallet_melt | PASS | | ||
| 574 | | 65 | Kind 11316 on relay | Integration | Announcement found on relay | PASS* | | ||
| 575 | | 66 | MCP initialize roundtrip | Integration | Response received via nak | PASS | | ||
| 576 | | 67 | get_config via CVM | Integration | Returns valid JSON config | PASS | | ||
| 577 | | 68 | get_balance via CVM | Integration | Returns balance + proofs | PASS | | ||
| 578 | | 69 | set_price via CVM | Integration | Price updated on device | PASS | | ||
| 579 | | 70 | Kind 11317 on relay | Integration | Tools list found on relay | PASS | | ||
| 580 | | 71 | Kind 10002 on relay | Integration | Relay list found on relay | PASS | | ||
| 581 | | 72 | API reachability from host | Integration | HTTP 200 from board AP | PASS | | ||
| 582 | | 73 | CVM event publish from host | Integration | Kind 25910 published to relay | PASS | | ||
| 583 | | 74 | tools/list via CVM | Integration | All 10 tools listed | PASS | | ||
| 584 | | 75 | get_sessions via CVM | Integration | Returns session array | TODO | | ||
| 585 | | 76 | get_usage via CVM | Integration | Returns usage stats | TODO | | ||
| 586 | | 77 | Non-owner rejection (live) | Integration | Unauthorized event ignored | TODO | | ||
| 587 | | 78 | Relay reconnect resilience | Integration | Board reconnects after disconnect | PASS | | ||
| 588 | |||
| 589 | ## Total: 85 Tests across 8 phases | ||
| 590 | |||
| 591 | ## Merge Readiness Checklist | ||
| 592 | |||
| 593 | ### Code Quality | ||
| 594 | - [ ] Fix relay disconnect cycle (rlen=-26880 every ~15s, WS read has no timeout) | ||
| 595 | - [ ] Clean up debug logging (Sending WS response, WS send result → DEBUG level) | ||
| 596 | - [ ] Document Board A hardware WiFi issue in AGENTS.md | ||
| 597 | |||
| 598 | ### Integration Testing (needs Board B + relay.primal.net) | ||
| 599 | - [ ] tools/list response via kind 25910 | ||
| 600 | - [ ] tools/call set_price via kind 25910 | ||
| 601 | - [ ] tools/call get_sessions via kind 25910 | ||
| 602 | - [ ] tools/call get_usage via kind 25910 | ||
| 603 | - [ ] Non-owner auth rejection via live relay | ||
| 604 | - [ ] Verify board npub on contextvm.org/servers | ||
| 605 | |||
| 606 | ### Pre-merge | ||
| 607 | - [ ] `make test-unit` — all 282 unit tests pass | ||
| 608 | - [ ] Rebase feature/cvm-integration onto master (1 commit behind) | ||
| 609 | - [ ] Verify no conflicts with feature branches (display-fix, multi-mint, price-discovery) | ||
| 526 | 610 | ||
| 527 | ## Post-Phase 7: Bug Fixes & Architecture Improvements | 611 | ## Post-Phase 7: Bug Fixes & Architecture Improvements |
| 528 | 612 | ||
| @@ -591,6 +675,78 @@ int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) { | |||
| 591 | 675 | ||
| 592 | **Rationale:** The mint's `cashu_check_proof_states()` already catches double-spends over HTTP. `nucula_wallet_receive()` → `swap()` registers proofs as spent and replaces them. After a successful receive, the old token is useless. Local tracking adds no security, wastes 6.5KB RAM, and is lost on reboot anyway. | 676 | **Rationale:** The mint's `cashu_check_proof_states()` already catches double-spends over HTTP. `nucula_wallet_receive()` → `swap()` registers proofs as spent and replaces them. After a successful receive, the old token is useless. Local tracking adds no security, wastes 6.5KB RAM, and is lost on reboot anyway. |
| 593 | 677 | ||
| 678 | ### Phase 8: TFT Display (JC3248W535 / AXS15231B) — IN PROGRESS | ||
| 679 | |||
| 680 | **Goal:** Add TFT display support to the JC3248W535 board for QR code rendering + status text. Display cycles between a Wi-Fi QR code (so customers can connect) and a portal URL QR code (for direct portal access). | ||
| 681 | |||
| 682 | **Hardware:** JC3248W535 board — ESP32-S3, AXS15231B 320x480 QSPI TFT, capacitive touch | ||
| 683 | **Pin mapping:** CS=45, CLK=47, D0=21, D1=48, D2=40, D3=39, BL=1, Touch SDA=4, Touch SCL=8 | ||
| 684 | |||
| 685 | #### Components Created | ||
| 686 | |||
| 687 | | Component | Path | Purpose | | ||
| 688 | |-----------|------|---------| | ||
| 689 | | `components/qrcode/` | `qrcoded.c/h` + CMakeLists.txt | QR code generation (ported from NSD, MIT license) | | ||
| 690 | | `components/axs15231b/` | `axs15231b.c/h` + CMakeLists.txt | AXS15231B QSPI display driver | | ||
| 691 | | `main/display.c/h` | Display abstraction | FreeRTOS display task, state machine, QR cycling | | ||
| 692 | | `main/font.c/h` | 8x8 bitmap font | ASCII 32-127 for status text rendering | | ||
| 693 | |||
| 694 | #### Display States | ||
| 695 | |||
| 696 | | State | Screen | QR Content | | ||
| 697 | |-------|--------|------------| | ||
| 698 | | `DISPLAY_BOOT` | "TollGate starting..." | None | | ||
| 699 | | `DISPLAY_READY` | QR code + SSID label | Cycles: Wi-Fi QR ↔ Portal URL QR every 5s | | ||
| 700 | | `DISPLAY_PAYMENT_RECEIVED` | Green "Paid! Access granted" | None (2s, then READY) | | ||
| 701 | | `DISPLAY_ERROR` | Red "No upstream" | None | | ||
| 702 | |||
| 703 | #### Wi-Fi QR Code Format | ||
| 704 | |||
| 705 | Uses the standardized ZXing `WIFI:` URI scheme — natively recognized by Android and iOS camera apps: | ||
| 706 | |||
| 707 | ``` | ||
| 708 | WIFI:S:<escaped_ssid>;T:nopass;; | ||
| 709 | ``` | ||
| 710 | |||
| 711 | **Special character escaping**: `;`, `:`, `\`, `,`, `"` are backslash-escaped in the SSID field per spec. | ||
| 712 | |||
| 713 | **Example:** | ||
| 714 | ``` | ||
| 715 | SSID: TollGate-C0E9CA → WIFI:S:TollGate-C0E9CA;T:nopass;; | ||
| 716 | SSID: My;WiFi:Test → WIFI:S:My\;WiFi\:Test;T:nopass;; | ||
| 717 | ``` | ||
| 718 | |||
| 719 | When scanned, the phone **automatically connects** to the TollGate AP — then the captive portal takes over for payment. | ||
| 720 | |||
| 721 | #### QR Cycling | ||
| 722 | |||
| 723 | The display alternates between two QR modes every 5 seconds: | ||
| 724 | 1. **Wi-Fi QR** — `WIFI:S:...;T:nopass;;` — auto-connects phone to AP | ||
| 725 | 2. **Portal URL QR** — `http://10.x.x.1/` — direct link to captive portal | ||
| 726 | |||
| 727 | Label text below QR changes to indicate current mode: "Scan to connect" vs "Portal URL". | ||
| 728 | |||
| 729 | #### Display Driver Architecture | ||
| 730 | |||
| 731 | - **Interface**: Single-line SPI (MOSI on D0/GPIO21) — simpler than QSPI, reliable for V1 | ||
| 732 | - **Framebuffer**: 307,200 bytes (480x320x2 RGB565) in PSRAM via `heap_caps_malloc` | ||
| 733 | - **Flush**: 10 chunks of 32KB via `spi_device_polling_transmit` | ||
| 734 | - **Rotation**: Landscape (MADCTL=0x60, MX|MV) | ||
| 735 | - **Backlight**: GPIO1 active-high | ||
| 736 | |||
| 737 | #### Test Cases | ||
| 738 | |||
| 739 | | # | Test | Method | Pass Criteria | Status | | ||
| 740 | |---|------|--------|---------------|--------| | ||
| 741 | | 70 | Wi-Fi QR scannable | Android camera scan | Phone connects to AP | TODO | | ||
| 742 | | 71 | Portal URL QR scannable | Android camera scan | Browser opens portal | TODO | | ||
| 743 | | 72 | QR cycling | Watch display | Mode changes every 5s | TODO | | ||
| 744 | | 73 | Boot screen | Visual | "TollGate starting..." shown | TODO | | ||
| 745 | | 74 | Payment screen | Trigger payment | Green "Paid!" for 2s | TODO | | ||
| 746 | | 75 | Error screen | Disconnect upstream | Red "No upstream" | TODO | | ||
| 747 | | 76 | Special char escape | Unit test | `\;:,"` correctly escaped | TODO | | ||
| 748 | | 77 | QR generation | Unit test | Valid QR matrix for various string lengths | TODO | | ||
| 749 | |||
| 594 | ## Testing Infrastructure | 750 | ## Testing Infrastructure |
| 595 | 751 | ||
| 596 | ### Three-Layer Test Architecture | 752 | ### Three-Layer Test Architecture |