diff options
| author | Your Name <you@example.com> | 2026-05-22 03:48:46 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-22 03:48:46 +0530 |
| commit | ed06f7eeab28c1fa6242c4ea6c3b9c933661fc06 (patch) | |
| tree | 96230dd069102688318f73b60b8322255ea30cad | |
| parent | 7009d2e0ac346376733863966374eac296e0471e (diff) | |
feat(cvm): fix CVM MCP roundtrip - task creation, subscription, relay selection
- Move CVM server start before mint_health_start (avoids RAM fragmentation)
- Fix NIP-01 subscription: #p tag must be array not string
- Fix read loop: tolerate TLS timeouts, don't disconnect
- Reduce TLS timeout from 15s to 5s for faster event delivery
- Add WS frame logging for debugging
- Sign test events with board's nsec (--sec flag) for owner auth
- Change default CVM relay to nos.lol (forwards ephemeral kind 25910)
- MCP get_config and get_balance roundtrips confirmed working
- Unit tests pass (16/16)
| -rw-r--r-- | PLAN_remaining_tests.md | 60 | ||||
| -rw-r--r-- | main/config.c | 2 | ||||
| -rw-r--r-- | main/cvm_server.c | 64 | ||||
| -rw-r--r-- | main/tollgate_main.c | 18 | ||||
| -rw-r--r-- | tests/integration/test-cvm-roundtrip.mjs | 6 |
5 files changed, 112 insertions, 38 deletions
diff --git a/PLAN_remaining_tests.md b/PLAN_remaining_tests.md new file mode 100644 index 0000000..e61735b --- /dev/null +++ b/PLAN_remaining_tests.md | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | # PLAN_remaining_tests.md — Remaining Integration Tests | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | |||
| 5 | All high-priority wallet work is complete (receive, send, swap, persistence, burst test). This plan covers the remaining integration test validation and fixes. | ||
| 6 | |||
| 7 | ## Phase A: Fix Stale Mint URLs — TODO | ||
| 8 | |||
| 9 | Three test files still reference `testnut.cashu.space` (dead mint): | ||
| 10 | |||
| 11 | - [ ] `tests/integration/test-session-expiry.mjs` (lines 26-27) | ||
| 12 | - [ ] `tests/integration/test-reset-auth.mjs` (lines 27-28) | ||
| 13 | - [ ] `tests/integration/test-dns-firewall.mjs` (lines 26-27) | ||
| 14 | |||
| 15 | Replace with `testnut-nutshell.mints.orangesync.tech`. | ||
| 16 | |||
| 17 | ## Phase B: Single-Board Integration Tests — TODO | ||
| 18 | |||
| 19 | Board A at `10.185.47.1`, port `/dev/ttyACM0`. | ||
| 20 | |||
| 21 | - [ ] `test-reset-auth.mjs` — payment + reset + re-auth flow | ||
| 22 | - [ ] `test-session-expiry.mjs` — session time-based expiry | ||
| 23 | - [ ] `test-dns-firewall.mjs` — DNS hijack + firewall per-client | ||
| 24 | - [ ] `test-local-relay.mjs` — WS pub/sub on port 4869 | ||
| 25 | - [ ] `test-relay-nip11.mjs` — NIP-11 info document | ||
| 26 | - [ ] `test-market.mjs` — GET /market endpoint | ||
| 27 | |||
| 28 | ## Phase C: Cross-Board Test — TODO | ||
| 29 | |||
| 30 | - [ ] Write Board C SPIFFS config (nsec `71bf3f4d...`) | ||
| 31 | - [ ] Verify Board C API responds | ||
| 32 | - [ ] Run `test-cross-board.mjs` against Board C | ||
| 33 | |||
| 34 | Board C: `10.74.63.1`, SSID `TollGate-4A2510`, port `/dev/ttyACM2`. | ||
| 35 | |||
| 36 | ## Phase D: CVM Round-Trip Fix — TODO | ||
| 37 | |||
| 38 | 9/11 tests pass. MCP kind 25910 requests go unanswered. | ||
| 39 | |||
| 40 | - [ ] Check serial logs for `cvm_relay` task creation failure | ||
| 41 | - [ ] Add error logging to `cvm_server_start` for `xTaskCreate` failure | ||
| 42 | - [ ] If task creation fails: try `xTaskCreatePinnedToCore(..., 1)` or reduce stack to 12KB | ||
| 43 | - [ ] Re-run `test-cvm-roundtrip.mjs` | ||
| 44 | |||
| 45 | **Root cause hypothesis:** `cvm_relay_task` (16KB stack) fails to create due to internal RAM fragmentation, same as old TLS worker. | ||
| 46 | |||
| 47 | ## Phase E: Two-Board Price Discovery — DEFERRED | ||
| 48 | |||
| 49 | Requires both boards on same network. Complex network setup. Defer until user requests. | ||
| 50 | |||
| 51 | ## Completed This Session | ||
| 52 | |||
| 53 | - [x] Wallet receive via health task queue (5/5 burst) | ||
| 54 | - [x] Wallet send (valid cashuA tokens) | ||
| 55 | - [x] Wallet swap (proof consolidation) | ||
| 56 | - [x] NVS persistence across reboots | ||
| 57 | - [x] Fix 4 API integration tests (19/19) | ||
| 58 | - [x] Fix E2E mint URL pattern (14/14 Playwright) | ||
| 59 | - [x] Flash Board C | ||
| 60 | - [x] CVM round-trip (9/11, 2 MCP response failures) | ||
diff --git a/main/config.c b/main/config.c index e3cbf46..2edb6da 100644 --- a/main/config.c +++ b/main/config.c | |||
| @@ -36,7 +36,7 @@ esp_err_t tollgate_config_init(void) | |||
| 36 | g_config.payout.recipient_count = 0; | 36 | g_config.payout.recipient_count = 0; |
| 37 | g_config.payout.mint_count = 0; | 37 | g_config.payout.mint_count = 0; |
| 38 | g_config.cvm_enabled = true; | 38 | g_config.cvm_enabled = true; |
| 39 | strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); | 39 | strncpy(g_config.cvm_relays, "wss://nos.lol", sizeof(g_config.cvm_relays) - 1); |
| 40 | strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1); | 40 | strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1); |
| 41 | g_config.display_enabled = true; | 41 | g_config.display_enabled = true; |
| 42 | g_config.nostr_sync_interval_s = 1800; | 42 | g_config.nostr_sync_interval_s = 1800; |
diff --git a/main/cvm_server.c b/main/cvm_server.c index f3a5ab8..1ac5cb6 100644 --- a/main/cvm_server.c +++ b/main/cvm_server.c | |||
| @@ -12,6 +12,7 @@ | |||
| 12 | #include "esp_tls.h" | 12 | #include "esp_tls.h" |
| 13 | #include "esp_crt_bundle.h" | 13 | #include "esp_crt_bundle.h" |
| 14 | #include "esp_random.h" | 14 | #include "esp_random.h" |
| 15 | #include "esp_heap_caps.h" | ||
| 15 | #include "freertos/FreeRTOS.h" | 16 | #include "freertos/FreeRTOS.h" |
| 16 | #include "freertos/task.h" | 17 | #include "freertos/task.h" |
| 17 | #include <string.h> | 18 | #include <string.h> |
| @@ -150,7 +151,7 @@ static esp_err_t ws_connect(const char *relay_url, esp_tls_t **tls_out) | |||
| 150 | 151 | ||
| 151 | esp_tls_cfg_t tls_cfg = { | 152 | esp_tls_cfg_t tls_cfg = { |
| 152 | .crt_bundle_attach = esp_crt_bundle_attach, | 153 | .crt_bundle_attach = esp_crt_bundle_attach, |
| 153 | .timeout_ms = 15000, | 154 | .timeout_ms = 5000, |
| 154 | }; | 155 | }; |
| 155 | esp_tls_t *tls = esp_tls_init(); | 156 | esp_tls_t *tls = esp_tls_init(); |
| 156 | if (!tls) return ESP_ERR_NO_MEM; | 157 | if (!tls) return ESP_ERR_NO_MEM; |
| @@ -507,7 +508,9 @@ static esp_err_t subscribe_to_relay(esp_tls_t *tls, const char *npub) | |||
| 507 | cJSON *kinds = cJSON_CreateArray(); | 508 | cJSON *kinds = cJSON_CreateArray(); |
| 508 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); | 509 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); |
| 509 | cJSON_AddItemToObject(filter, "kinds", kinds); | 510 | cJSON_AddItemToObject(filter, "kinds", kinds); |
| 510 | cJSON_AddStringToObject(filter, "#p", npub); | 511 | cJSON *p_tag = cJSON_CreateArray(); |
| 512 | cJSON_AddItemToArray(p_tag, cJSON_CreateString(npub)); | ||
| 513 | cJSON_AddItemToObject(filter, "#p", p_tag); | ||
| 511 | cJSON_AddNumberToObject(filter, "limit", 100); | 514 | cJSON_AddNumberToObject(filter, "limit", 100); |
| 512 | cJSON_AddItemToArray(sub, filter); | 515 | cJSON_AddItemToArray(sub, filter); |
| 513 | 516 | ||
| @@ -556,33 +559,33 @@ static void cvm_relay_task(void *arg) | |||
| 556 | } | 559 | } |
| 557 | 560 | ||
| 558 | int64_t last_ping_time = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; | 561 | int64_t last_ping_time = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; |
| 559 | int consecutive_timeouts = 0; | ||
| 560 | while (g_running) { | 562 | while (g_running) { |
| 561 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); | 563 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); |
| 562 | if (rlen < 0) { | ||
| 563 | ESP_LOGW(TAG, "Read error on %s (rlen=%d)", relay_url, rlen); | ||
| 564 | break; | ||
| 565 | } | ||
| 566 | if (rlen == 0) { | 564 | if (rlen == 0) { |
| 565 | ESP_LOGW(TAG, "Connection closed by relay"); | ||
| 567 | break; | 566 | break; |
| 568 | } else { | 567 | } |
| 569 | consecutive_timeouts = 0; | 568 | if (rlen < 0) { |
| 570 | if ((buf[0] & 0x0F) == 0x01) { | 569 | vTaskDelay(pdMS_TO_TICKS(100)); |
| 571 | char *text = parse_ws_text_frame(buf, rlen); | 570 | continue; |
| 572 | if (text) { | 571 | } |
| 573 | if (strlen(text) > 0) { | 572 | |
| 574 | process_relay_message(relay_url, text); | 573 | ESP_LOGI(TAG, "WS frame received: %d bytes, opcode=0x%02x", rlen, buf[0] & 0x0F); |
| 575 | } | 574 | |
| 576 | free(text); | 575 | if ((buf[0] & 0x0F) == 0x01) { |
| 576 | char *text = parse_ws_text_frame(buf, rlen); | ||
| 577 | if (text) { | ||
| 578 | if (strlen(text) > 0) { | ||
| 579 | process_relay_message(relay_url, text); | ||
| 577 | } | 580 | } |
| 578 | } else if ((buf[0] & 0x0F) == 0x09) { | 581 | free(text); |
| 579 | ESP_LOGD(TAG, "Relay ping received, sending pong"); | ||
| 580 | uint8_t pong[2] = {0x8A, 0x00}; | ||
| 581 | esp_tls_conn_write(tls, pong, 2); | ||
| 582 | } else if ((buf[0] & 0x0F) == 0x08) { | ||
| 583 | ESP_LOGW(TAG, "Relay sent close frame"); | ||
| 584 | break; | ||
| 585 | } | 582 | } |
| 583 | } else if ((buf[0] & 0x0F) == 0x09) { | ||
| 584 | uint8_t pong[2] = {0x8A, 0x00}; | ||
| 585 | esp_tls_conn_write(tls, pong, 2); | ||
| 586 | } else if ((buf[0] & 0x0F) == 0x08) { | ||
| 587 | ESP_LOGW(TAG, "Relay sent close frame"); | ||
| 588 | break; | ||
| 586 | } | 589 | } |
| 587 | 590 | ||
| 588 | int64_t now = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; | 591 | int64_t now = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000; |
| @@ -590,7 +593,6 @@ static void cvm_relay_task(void *arg) | |||
| 590 | uint8_t ping[2] = {0x89, 0x00}; | 593 | uint8_t ping[2] = {0x89, 0x00}; |
| 591 | esp_tls_conn_write(tls, ping, 2); | 594 | esp_tls_conn_write(tls, ping, 2); |
| 592 | last_ping_time = now; | 595 | last_ping_time = now; |
| 593 | ESP_LOGD(TAG, "Sent WS keepalive ping"); | ||
| 594 | } | 596 | } |
| 595 | } | 597 | } |
| 596 | 598 | ||
| @@ -726,8 +728,20 @@ void cvm_server_start(void) | |||
| 726 | const tollgate_config_t *cfg = tollgate_config_get(); | 728 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 727 | const char *relay = (cfg->cvm_relays[0]) ? cfg->cvm_relays : "wss://relay.primal.net"; | 729 | const char *relay = (cfg->cvm_relays[0]) ? cfg->cvm_relays : "wss://relay.primal.net"; |
| 728 | 730 | ||
| 731 | ESP_LOGI(TAG, "Starting CVM relay task (free internal: %u, largest: %u)", | ||
| 732 | heap_caps_get_free_size(MALLOC_CAP_INTERNAL), | ||
| 733 | heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); | ||
| 734 | |||
| 729 | char *relay_copy = strdup(relay); | 735 | char *relay_copy = strdup(relay); |
| 730 | xTaskCreate(cvm_relay_task, "cvm_relay", 16384, relay_copy, 5, &g_task); | 736 | BaseType_t ret = xTaskCreatePinnedToCore(cvm_relay_task, "cvm_relay", 16384, relay_copy, 5, &g_task, 1); |
| 737 | if (ret != pdPASS) { | ||
| 738 | ESP_LOGE(TAG, "Failed to create cvm_relay task (ret=%d, free internal: %u)", | ||
| 739 | ret, heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); | ||
| 740 | g_running = false; | ||
| 741 | free(relay_copy); | ||
| 742 | return; | ||
| 743 | } | ||
| 744 | ESP_LOGI(TAG, "CVM relay task created on core %d", xPortGetCoreID()); | ||
| 731 | } | 745 | } |
| 732 | 746 | ||
| 733 | void cvm_server_stop(void) | 747 | void cvm_server_stop(void) |
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 60503eb..2d4fa22 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -237,6 +237,12 @@ static void start_services(void) | |||
| 237 | session_manager_init(); | 237 | session_manager_init(); |
| 238 | 238 | ||
| 239 | const tollgate_config_t *cfg = tollgate_config_get(); | 239 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 240 | |||
| 241 | if (cfg->cvm_enabled) { | ||
| 242 | cvm_server_init(); | ||
| 243 | cvm_server_start(); | ||
| 244 | } | ||
| 245 | |||
| 240 | mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count); | 246 | mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count); |
| 241 | mint_health_start(); | 247 | mint_health_start(); |
| 242 | 248 | ||
| @@ -268,19 +274,13 @@ static void start_services(void) | |||
| 268 | 274 | ||
| 269 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); | 275 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); |
| 270 | 276 | ||
| 271 | const tollgate_config_t *cfg2 = tollgate_config_get(); | 277 | if (cfg->mining_enabled) { |
| 272 | if (cfg2->cvm_enabled) { | ||
| 273 | cvm_server_init(); | ||
| 274 | cvm_server_start(); | ||
| 275 | } | ||
| 276 | |||
| 277 | if (cfg2->mining_enabled) { | ||
| 278 | ESP_LOGI(TAG, "Mining subsystem enabled, initializing..."); | 278 | ESP_LOGI(TAG, "Mining subsystem enabled, initializing..."); |
| 279 | mining_payment_init(); | 279 | mining_payment_init(); |
| 280 | stratum_client_init(); | 280 | stratum_client_init(); |
| 281 | stratum_proxy_init(cfg2->mining_port); | 281 | stratum_proxy_init(cfg->mining_port); |
| 282 | 282 | ||
| 283 | if (cfg2->mining_payout_mode != MINING_PAYOUT_UPSTREAM) { | 283 | if (cfg->mining_payout_mode != MINING_PAYOUT_UPSTREAM) { |
| 284 | stratum_client_start(); | 284 | stratum_client_start(); |
| 285 | } | 285 | } |
| 286 | 286 | ||
diff --git a/tests/integration/test-cvm-roundtrip.mjs b/tests/integration/test-cvm-roundtrip.mjs index 821cfe7..87caf1a 100644 --- a/tests/integration/test-cvm-roundtrip.mjs +++ b/tests/integration/test-cvm-roundtrip.mjs | |||
| @@ -2,7 +2,7 @@ import { execSync } from 'child_process'; | |||
| 2 | import WebSocket from 'ws'; | 2 | import WebSocket from 'ws'; |
| 3 | 3 | ||
| 4 | const IP = process.env.TOLLGATE_IP || '10.192.45.1'; | 4 | const IP = process.env.TOLLGATE_IP || '10.192.45.1'; |
| 5 | const CVM_RELAY = process.env.CVM_RELAY || 'wss://relay.primal.net'; | 5 | const CVM_RELAY = process.env.CVM_RELAY || 'wss://nos.lol'; |
| 6 | const NSEC = process.env.CVM_NSEC || 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'; | 6 | const NSEC = process.env.CVM_NSEC || 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'; |
| 7 | 7 | ||
| 8 | let passed = 0, failed = 0; | 8 | let passed = 0, failed = 0; |
| @@ -104,7 +104,7 @@ async function runTests() { | |||
| 104 | params: { name: 'get_config', arguments: {} } | 104 | params: { name: 'get_config', arguments: {} } |
| 105 | }); | 105 | }); |
| 106 | 106 | ||
| 107 | const eventOut = nak(`event --kind 25910 --tag p=${npub} --content '${content.replace(/'/g, "'\\''")}' ${CVM_RELAY}`, 8000); | 107 | const eventOut = nak(`event --sec ${NSEC} --kind 25910 --tag p=${npub} --content '${content.replace(/'/g, "'\\''")}' ${CVM_RELAY}`, 8000); |
| 108 | const published = eventOut.includes('Success') || eventOut.includes('"id"'); | 108 | const published = eventOut.includes('Success') || eventOut.includes('"id"'); |
| 109 | assert(published, `Published kind 25910 get_config to ${CVM_RELAY}`); | 109 | assert(published, `Published kind 25910 get_config to ${CVM_RELAY}`); |
| 110 | 110 | ||
| @@ -149,7 +149,7 @@ async function runTests() { | |||
| 149 | params: { name: 'get_balance', arguments: {} } | 149 | params: { name: 'get_balance', arguments: {} } |
| 150 | }); | 150 | }); |
| 151 | 151 | ||
| 152 | const eventOut = nak(`event --kind 25910 --tag p=${npub} --content '${content.replace(/'/g, "'\\''")}' ${CVM_RELAY}`, 8000); | 152 | const eventOut = nak(`event --sec ${NSEC} --kind 25910 --tag p=${npub} --content '${content.replace(/'/g, "'\\''")}' ${CVM_RELAY}`, 8000); |
| 153 | const published = eventOut.includes('Success') || eventOut.includes('"id"'); | 153 | const published = eventOut.includes('Success') || eventOut.includes('"id"'); |
| 154 | assert(published, `Published kind 25910 get_balance to ${CVM_RELAY}`); | 154 | assert(published, `Published kind 25910 get_balance to ${CVM_RELAY}`); |
| 155 | 155 | ||