upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/cvm_server.c
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 02:31:19 +0530
committerYour Name <you@example.com>2026-05-19 02:32:41 +0530
commit81f2dc52dc42d01c89dff45a5407ec40b8863052 (patch)
tree15018c2438639ca89dc6d33a5144c10d0b1c2af0 /main/cvm_server.c
parent75688d55b3c8d13c8c9a50da9668ec408f684cb3 (diff)
feat: local Nostr relay with relay selection, sync, and integration tests
Local Nostr relay (NIP-01) on port 4869 with LittleFS 4MB storage. All events published locally first, then synced to public relays via REQ-diff. Relay selection via NIP-11 HTTP probing with NIP-77 scoring and auto-failover. Components: - wisp_relay: 16-file local relay (ws_server, storage_engine, sub_manager, broadcaster, relay_validator, router, handlers, rate_limiter, nip11, deletion, flash_monitor, relay_types) - esp_littlefs: LittleFS VFS integration (git submodule) - negentropy: for future NIP-77 binary sync (git submodule) New source files: - local_relay.c/h: thin wrapper for relay init/start/publish - relay_selector.c/h: NIP-11 probe + scoring + auto-failover - sync_manager.c/h: REQ-diff sync (primary 30min, fallback 6h) Bug fixes: - config.c: use-after-free (cJSON_Delete before seed_relays/sync parsing) - local_relay: moved init to app_main for boot-time start (not gated on STA IP) Flash layout: 4MB LittleFS partition at 0x500000 for relay_store Test results (Board B, live hardware): - Smoke: ping + HTTP 4869 + NIP-11: PASS - NIP-11 info document: 10/11 PASS - WS pub/sub (connect, REQ/EOSE, EVENT/OK, CLOSE, concurrent): 6/6 PASS - Unit tests (relay_validator + relay_selector): 13/13 PASS Hardware test make targets in physical-router-test-automation/: - make relay-build, relay-flash-b, relay-test-smoke/nip11/pubsub/sync/full
Diffstat (limited to 'main/cvm_server.c')
-rw-r--r--main/cvm_server.c95
1 files changed, 10 insertions, 85 deletions
diff --git a/main/cvm_server.c b/main/cvm_server.c
index b93e176..dd04047 100644
--- a/main/cvm_server.c
+++ b/main/cvm_server.c
@@ -11,7 +11,6 @@
11#include "esp_tls.h" 11#include "esp_tls.h"
12#include "esp_crt_bundle.h" 12#include "esp_crt_bundle.h"
13#include "esp_random.h" 13#include "esp_random.h"
14#include "esp_timer.h"
15#include "freertos/FreeRTOS.h" 14#include "freertos/FreeRTOS.h"
16#include "freertos/task.h" 15#include "freertos/task.h"
17#include <string.h> 16#include <string.h>
@@ -31,8 +30,6 @@ static void publish_announcements_via_ws(esp_tls_t *tls);
31#define CVM_WS_BUF_SIZE 8192 30#define CVM_WS_BUF_SIZE 8192
32#define CVM_MAX_RESPONSE_SIZE 4096 31#define CVM_MAX_RESPONSE_SIZE 4096
33#define CVM_RECONNECT_DELAY_MS 5000 32#define CVM_RECONNECT_DELAY_MS 5000
34#define CVM_WS_READ_TIMEOUT_MS 60000
35#define CVM_WS_PING_INTERVAL_S 30
36 33
37static char *parse_ws_text_frame(const uint8_t *buf, int len) 34static char *parse_ws_text_frame(const uint8_t *buf, int len)
38{ 35{
@@ -151,7 +148,7 @@ static esp_err_t ws_connect(const char *relay_url, esp_tls_t **tls_out)
151 148
152 esp_tls_cfg_t tls_cfg = { 149 esp_tls_cfg_t tls_cfg = {
153 .crt_bundle_attach = esp_crt_bundle_attach, 150 .crt_bundle_attach = esp_crt_bundle_attach,
154 .timeout_ms = CVM_WS_READ_TIMEOUT_MS, 151 .timeout_ms = 15000,
155 }; 152 };
156 esp_tls_t *tls = esp_tls_init(); 153 esp_tls_t *tls = esp_tls_init();
157 if (!tls) return ESP_ERR_NO_MEM; 154 if (!tls) return ESP_ERR_NO_MEM;
@@ -326,54 +323,6 @@ static esp_err_t publish_event_to_relay(const char *relay_url, const char *event
326 return ESP_OK; 323 return ESP_OK;
327} 324}
328 325
329static esp_err_t publish_kind_25910_response_ws(esp_tls_t *tls,
330 const char *content_json,
331 const char *request_event_id)
332{
333 const tollgate_identity_t *id = identity_get();
334 if (!id || !id->initialized) return ESP_FAIL;
335
336 cJSON *tags = cJSON_CreateArray();
337 cJSON *e_tag = cJSON_CreateArray();
338 cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
339 cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_event_id));
340 cJSON_AddItemToArray(tags, e_tag);
341
342 char *tags_str = cJSON_PrintUnformatted(tags);
343 cJSON_Delete(tags);
344
345 nostr_event_t event;
346 nostr_event_init(&event, id->npub_hex, 25910, tags_str, content_json);
347 nostr_event_sign(&event, id->nsec);
348
349 char *event_json = malloc(8192);
350 if (!event_json) {
351 free(tags_str);
352 return ESP_ERR_NO_MEM;
353 }
354
355 esp_err_t ret = nostr_event_to_json(&event, event_json, 8192);
356 free(tags_str);
357 if (ret != ESP_OK) {
358 free(event_json);
359 return ret;
360 }
361
362 size_t msg_len = 10 + strlen(event_json) + 2;
363 char *msg = malloc(msg_len);
364 if (!msg) {
365 free(event_json);
366 return ESP_ERR_NO_MEM;
367 }
368 snprintf(msg, msg_len, "[\"EVENT\",%s]", event_json);
369 ESP_LOGD(TAG, "Sending WS response (%d bytes)", (int)strlen(msg));
370 int rc = ws_send_text(tls, msg);
371 ESP_LOGD(TAG, "WS send result: %d", rc);
372 free(msg);
373 free(event_json);
374 return ESP_OK;
375}
376
377static esp_err_t publish_kind_25910_response(const char *relay_url, 326static esp_err_t publish_kind_25910_response(const char *relay_url,
378 const char *content_json, 327 const char *content_json,
379 const char *request_event_id) 328 const char *request_event_id)
@@ -417,7 +366,7 @@ static bool is_owner_pubkey(const char *pubkey_hex)
417 return strcmp(id->npub_hex, pubkey_hex) == 0; 366 return strcmp(id->npub_hex, pubkey_hex) == 0;
418} 367}
419 368
420static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey, 369static void handle_mcp_message(const char *relay_url, const char *sender_pubkey,
421 const char *event_id, const char *content) 370 const char *event_id, const char *content)
422{ 371{
423 cJSON *msg = cJSON_Parse(content); 372 cJSON *msg = cJSON_Parse(content);
@@ -437,20 +386,14 @@ static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey,
437 if (strcmp(m, "initialize") == 0) { 386 if (strcmp(m, "initialize") == 0) {
438 ESP_LOGI(TAG, "MCP initialize from %s", sender_pubkey); 387 ESP_LOGI(TAG, "MCP initialize from %s", sender_pubkey);
439 char *resp = build_initialize_response(id_str, sender_pubkey); 388 char *resp = build_initialize_response(id_str, sender_pubkey);
440 if (tls) { 389 publish_kind_25910_response(relay_url, resp, event_id);
441 publish_kind_25910_response_ws(tls, resp, event_id);
442 } else {
443 ESP_LOGW(TAG, "No TLS for response");
444 }
445 free(resp); 390 free(resp);
446 } else if (strcmp(m, "notifications/initialized") == 0) { 391 } else if (strcmp(m, "notifications/initialized") == 0) {
447 ESP_LOGI(TAG, "Client initialized: %s", sender_pubkey); 392 ESP_LOGI(TAG, "Client initialized: %s", sender_pubkey);
448 } else if (strcmp(m, "tools/list") == 0) { 393 } else if (strcmp(m, "tools/list") == 0) {
449 ESP_LOGI(TAG, "tools/list from %s", sender_pubkey); 394 ESP_LOGI(TAG, "tools/list from %s", sender_pubkey);
450 char *resp = build_tools_list_response(id_str); 395 char *resp = build_tools_list_response(id_str);
451 if (tls) { 396 publish_kind_25910_response(relay_url, resp, event_id);
452 publish_kind_25910_response_ws(tls, resp, event_id);
453 }
454 free(resp); 397 free(resp);
455 } else if (strcmp(m, "tools/call") == 0) { 398 } else if (strcmp(m, "tools/call") == 0) {
456 cJSON *params = cJSON_GetObjectItem(msg, "params"); 399 cJSON *params = cJSON_GetObjectItem(msg, "params");
@@ -471,16 +414,12 @@ static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey,
471 414
472 mcp_response_t mcp_resp = mcp_dispatch(&req); 415 mcp_response_t mcp_resp = mcp_dispatch(&req);
473 char *resp = build_tool_call_response(id_str, &mcp_resp); 416 char *resp = build_tool_call_response(id_str, &mcp_resp);
474 if (tls) { 417 publish_kind_25910_response(relay_url, resp, event_id);
475 publish_kind_25910_response_ws(tls, resp, event_id);
476 }
477 free(resp); 418 free(resp);
478 } 419 }
479 } else if (strcmp(m, "ping") == 0) { 420 } else if (strcmp(m, "ping") == 0) {
480 char *resp = build_ping_response(id_str); 421 char *resp = build_ping_response(id_str);
481 if (tls) { 422 publish_kind_25910_response(relay_url, resp, event_id);
482 publish_kind_25910_response_ws(tls, resp, event_id);
483 }
484 free(resp); 423 free(resp);
485 } else { 424 } else {
486 ESP_LOGW(TAG, "Unknown MCP method: %s", m); 425 ESP_LOGW(TAG, "Unknown MCP method: %s", m);
@@ -494,7 +433,7 @@ static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey,
494 cJSON_Delete(msg); 433 cJSON_Delete(msg);
495} 434}
496 435
497static void process_relay_message(esp_tls_t *tls, const char *relay_url, const char *msg_str) 436static void process_relay_message(const char *relay_url, const char *msg_str)
498{ 437{
499 cJSON *arr = cJSON_Parse(msg_str); 438 cJSON *arr = cJSON_Parse(msg_str);
500 if (!arr || !cJSON_IsArray(arr)) { 439 if (!arr || !cJSON_IsArray(arr)) {
@@ -553,7 +492,7 @@ static void process_relay_message(esp_tls_t *tls, const char *relay_url, const c
553 return; 492 return;
554 } 493 }
555 494
556 handle_mcp_message(tls, pubkey->valuestring, event_id->valuestring, content->valuestring); 495 handle_mcp_message(relay_url, pubkey->valuestring, event_id->valuestring, content->valuestring);
557 cJSON_Delete(arr); 496 cJSON_Delete(arr);
558} 497}
559 498
@@ -566,9 +505,7 @@ static esp_err_t subscribe_to_relay(esp_tls_t *tls, const char *npub)
566 cJSON *kinds = cJSON_CreateArray(); 505 cJSON *kinds = cJSON_CreateArray();
567 cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); 506 cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910));
568 cJSON_AddItemToObject(filter, "kinds", kinds); 507 cJSON_AddItemToObject(filter, "kinds", kinds);
569 cJSON *p_tags = cJSON_CreateArray(); 508 cJSON_AddStringToObject(filter, "#p", npub);
570 cJSON_AddItemToArray(p_tags, cJSON_CreateString(npub));
571 cJSON_AddItemToObject(filter, "#p", p_tags);
572 cJSON_AddNumberToObject(filter, "limit", 100); 509 cJSON_AddNumberToObject(filter, "limit", 100);
573 cJSON_AddItemToArray(sub, filter); 510 cJSON_AddItemToArray(sub, filter);
574 511
@@ -616,8 +553,6 @@ static void cvm_relay_task(void *arg)
616 return; 553 return;
617 } 554 }
618 555
619 int64_t last_ping_time = 0;
620
621 while (g_running) { 556 while (g_running) {
622 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); 557 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1);
623 if (rlen < 0) { 558 if (rlen < 0) {
@@ -632,20 +567,10 @@ static void cvm_relay_task(void *arg)
632 char *text = parse_ws_text_frame(buf, rlen); 567 char *text = parse_ws_text_frame(buf, rlen);
633 if (text) { 568 if (text) {
634 if (strlen(text) > 0) { 569 if (strlen(text) > 0) {
635 process_relay_message(tls, relay_url, text); 570 process_relay_message(relay_url, text);
636 } 571 }
637 free(text); 572 free(text);
638 } 573 }
639 } else if ((buf[0] & 0x0F) == 0x09) {
640 uint8_t pong[2] = {0x8A, 0x00};
641 esp_tls_conn_write(tls, pong, 2);
642 }
643
644 int64_t now = (int64_t)esp_timer_get_time() / 1000000;
645 if (now - last_ping_time >= CVM_WS_PING_INTERVAL_S) {
646 uint8_t ping[2] = {0x89, 0x00};
647 esp_tls_conn_write(tls, ping, 2);
648 last_ping_time = now;
649 } 574 }
650 } 575 }
651 576