upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-22 03:48:46 +0530
committerYour Name <you@example.com>2026-05-22 03:48:46 +0530
commited06f7eeab28c1fa6242c4ea6c3b9c933661fc06 (patch)
tree96230dd069102688318f73b60b8322255ea30cad
parent7009d2e0ac346376733863966374eac296e0471e (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.md60
-rw-r--r--main/config.c2
-rw-r--r--main/cvm_server.c64
-rw-r--r--main/tollgate_main.c18
-rw-r--r--tests/integration/test-cvm-roundtrip.mjs6
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
5All 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
9Three 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
15Replace with `testnut-nutshell.mints.orangesync.tech`.
16
17## Phase B: Single-Board Integration Tests — TODO
18
19Board 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
34Board C: `10.74.63.1`, SSID `TollGate-4A2510`, port `/dev/ttyACM2`.
35
36## Phase D: CVM Round-Trip Fix — TODO
37
389/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
49Requires 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
733void cvm_server_stop(void) 747void 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';
2import WebSocket from 'ws'; 2import WebSocket from 'ws';
3 3
4const IP = process.env.TOLLGATE_IP || '10.192.45.1'; 4const IP = process.env.TOLLGATE_IP || '10.192.45.1';
5const CVM_RELAY = process.env.CVM_RELAY || 'wss://relay.primal.net'; 5const CVM_RELAY = process.env.CVM_RELAY || 'wss://nos.lol';
6const NSEC = process.env.CVM_NSEC || 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'; 6const NSEC = process.env.CVM_NSEC || 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
7 7
8let passed = 0, failed = 0; 8let 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