From 192eeb248d4e5c3c7b50a4cc893f5806c9a4bd73 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Wed, 1 Apr 2026 07:04:58 -0400 Subject: [PATCH] v2.0.2 - Stabilize websocket queue draining, add db config count helper, and align NIP-45/50 test expectations --- relay.pid | 2 +- src/api.c | 348 ++++++++++++++------------------- src/config.c | 212 +++----------------- src/db_ops.c | 455 +++++++++++++++++++++++++++++++++++++++++++ src/db_ops.h | 35 ++++ src/dm_admin.c | 18 +- src/main.c | 54 ++--- src/main.h | 4 +- src/websockets.c | 97 +++++---- tests/45_nip_test.sh | 21 +- tests/nip42_test.log | 59 +++--- 11 files changed, 764 insertions(+), 541 deletions(-) diff --git a/relay.pid b/relay.pid index 76d9faf..e31f356 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -409692 +457096 diff --git a/src/api.c b/src/api.c index bfee576..880945b 100644 --- a/src/api.c +++ b/src/api.c @@ -28,6 +28,7 @@ int get_active_connection_count(void); #include "../nostr_core_lib/nostr_core/nip017.h" #include "../nostr_core_lib/nostr_core/nip044.h" #include "subscriptions.h" +#include "db_ops.h" // External subscription manager (from main.c via subscriptions.c) extern subscription_manager_t g_subscription_manager; @@ -62,22 +63,21 @@ int get_monitoring_throttle_seconds(void) { // Query event kind distribution from database cJSON* query_event_kind_distribution(void) { - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for monitoring query"); return NULL; } - // Query event kinds distribution with total count - sqlite3_stmt* stmt; const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC"; // Start timing struct timespec query_start, query_end; clock_gettime(CLOCK_MONOTONIC, &query_start); - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare event kind distribution query"); + long long total_events = 0; + cJSON* kinds_array = db_get_event_kind_distribution_rows(&total_events); + if (!kinds_array) { + DEBUG_ERROR("Failed to query event kind distribution rows"); return NULL; } @@ -85,29 +85,12 @@ cJSON* query_event_kind_distribution(void) { cJSON_AddStringToObject(distribution, "data_type", "event_kinds"); cJSON_AddNumberToObject(distribution, "timestamp", (double)time(NULL)); - cJSON* kinds_array = cJSON_CreateArray(); - long long total_events = 0; - int row_count = 0; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - row_count++; - int kind = sqlite3_column_int(stmt, 0); - long long count = sqlite3_column_int64(stmt, 1); - total_events += count; - - cJSON* kind_obj = cJSON_CreateObject(); - cJSON_AddNumberToObject(kind_obj, "kind", kind); - cJSON_AddNumberToObject(kind_obj, "count", count); - cJSON_AddItemToArray(kinds_array, kind_obj); - } - - sqlite3_finalize(stmt); - // Stop timing and log clock_gettime(CLOCK_MONOTONIC, &query_end); long elapsed_us = (query_end.tv_sec - query_start.tv_sec) * 1000000L + (query_end.tv_nsec - query_start.tv_nsec) / 1000L; - + + int row_count = cJSON_GetArraySize(kinds_array); log_query_execution("MONITOR", "event_kinds", NULL, sql, elapsed_us, row_count); cJSON_AddNumberToObject(distribution, "total_events", total_events); @@ -118,8 +101,7 @@ cJSON* query_event_kind_distribution(void) { // Query time-based statistics from database cJSON* query_time_based_statistics(void) { - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for time stats query"); return NULL; } @@ -143,38 +125,18 @@ cJSON* query_time_based_statistics(void) { {NULL, 0, NULL} }; - // Get total events count - sqlite3_stmt* total_stmt; - const char* total_sql = "SELECT COUNT(*) FROM events"; long long total_events = 0; - - if (sqlite3_prepare_v2(g_db, total_sql, -1, &total_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(total_stmt) == SQLITE_ROW) { - total_events = sqlite3_column_int64(total_stmt, 0); - } - sqlite3_finalize(total_stmt); - } + (void)db_get_total_event_count_ll(&total_events); // Query each time period for (int i = 0; periods[i].period != NULL; i++) { - sqlite3_stmt* stmt; - const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?"; - - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare time stats query"); + time_t cutoff = now - periods[i].seconds; + long long count = 0; + if (db_get_event_count_since(cutoff, &count) != 0) { + DEBUG_ERROR("Failed to query time stats count"); continue; } - time_t cutoff = now - periods[i].seconds; - sqlite3_bind_int64(stmt, 1, cutoff); - - long long count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int64(stmt, 0); - } - - sqlite3_finalize(stmt); - cJSON* period_obj = cJSON_CreateObject(); cJSON_AddStringToObject(period_obj, "period", periods[i].period); cJSON_AddNumberToObject(period_obj, "count", count); @@ -190,51 +152,39 @@ cJSON* query_time_based_statistics(void) { // Query top pubkeys by event count from database cJSON* query_top_pubkeys(void) { - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for top pubkeys query"); return NULL; } - // Query top 10 pubkeys by event count - sqlite3_stmt* stmt; - const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT 10"; - - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare top pubkeys query"); - return NULL; - } - cJSON* top_pubkeys = cJSON_CreateObject(); cJSON_AddStringToObject(top_pubkeys, "data_type", "top_pubkeys"); cJSON_AddNumberToObject(top_pubkeys, "timestamp", (double)time(NULL)); - cJSON* pubkeys_array = cJSON_CreateArray(); - - // Get total events count for percentage calculation - sqlite3_stmt* total_stmt; - const char* total_sql = "SELECT COUNT(*) FROM events"; - long long total_events = 0; - - if (sqlite3_prepare_v2(g_db, total_sql, -1, &total_stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(total_stmt) == SQLITE_ROW) { - total_events = sqlite3_column_int64(total_stmt, 0); - } - sqlite3_finalize(total_stmt); + cJSON* rows = db_get_top_pubkeys_rows(10); + if (!rows) { + cJSON_Delete(top_pubkeys); + DEBUG_ERROR("Failed to query top pubkeys rows"); + return NULL; } - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); - long long count = sqlite3_column_int64(stmt, 1); - + cJSON* pubkeys_array = cJSON_CreateArray(); + cJSON* row = NULL; + cJSON_ArrayForEach(row, rows) { cJSON* pubkey_obj = cJSON_CreateObject(); - cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : ""); - cJSON_AddNumberToObject(pubkey_obj, "event_count", count); - // Percentage will be calculated by frontend using total_events + cJSON* pubkey_item = cJSON_GetObjectItemCaseSensitive(row, "pubkey"); + cJSON* count_item = cJSON_GetObjectItemCaseSensitive(row, "count"); + + cJSON_AddStringToObject(pubkey_obj, "pubkey", + cJSON_IsString(pubkey_item) ? pubkey_item->valuestring : ""); + cJSON_AddNumberToObject(pubkey_obj, "event_count", + cJSON_IsNumber(count_item) ? count_item->valuedouble : 0.0); cJSON_AddItemToArray(pubkeys_array, pubkey_obj); } + cJSON_Delete(rows); - sqlite3_finalize(stmt); + long long total_events = 0; + (void)db_get_total_event_count_ll(&total_events); cJSON_AddItemToObject(top_pubkeys, "pubkeys", pubkeys_array); cJSON_AddNumberToObject(top_pubkeys, "total_events", total_events); @@ -246,15 +196,11 @@ cJSON* query_top_pubkeys(void) { // Query detailed subscription information from database log (ADMIN ONLY) // Uses subscriptions table instead of in-memory iteration to avoid mutex contention cJSON* query_subscription_details(void) { - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for subscription details query"); return NULL; } - // Query active subscriptions from the active_subscriptions_log view - // This view properly handles deduplication of closed/expired subscriptions - sqlite3_stmt* stmt; const char* sql = "SELECT * " "FROM active_subscriptions_log " @@ -268,11 +214,6 @@ cJSON* query_subscription_details(void) { struct timespec query_start, query_end; clock_gettime(CLOCK_MONOTONIC, &query_start); - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare subscription details query"); - return NULL; - } - time_t current_time = time(NULL); cJSON* subscriptions_data = cJSON_CreateObject(); cJSON_AddStringToObject(subscriptions_data, "data_type", "subscription_details"); @@ -281,34 +222,51 @@ cJSON* query_subscription_details(void) { cJSON* data = cJSON_CreateObject(); cJSON* subscriptions_array = cJSON_CreateArray(); + cJSON* rows = db_get_subscription_details_rows(); + if (!rows) { + cJSON_Delete(subscriptions_data); + cJSON_Delete(data); + cJSON_Delete(subscriptions_array); + DEBUG_ERROR("Failed to query subscription details rows"); + return NULL; + } + // Iterate through query results int row_count = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON* row = NULL; + cJSON_ArrayForEach(row, rows) { row_count++; cJSON* sub_obj = cJSON_CreateObject(); - // Extract subscription data from database - const char* sub_id = (const char*)sqlite3_column_text(stmt, 0); - const char* client_ip = (const char*)sqlite3_column_text(stmt, 1); - const char* filter_json = (const char*)sqlite3_column_text(stmt, 2); - long long events_sent = sqlite3_column_int64(stmt, 3); - long long created_at = sqlite3_column_int64(stmt, 4); - long long duration_seconds = sqlite3_column_int64(stmt, 5); - const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 6); + cJSON* sub_id_item = cJSON_GetObjectItemCaseSensitive(row, "id"); + cJSON* client_ip_item = cJSON_GetObjectItemCaseSensitive(row, "client_ip"); + cJSON* filter_json_item = cJSON_GetObjectItemCaseSensitive(row, "filter_json"); + cJSON* events_sent_item = cJSON_GetObjectItemCaseSensitive(row, "events_sent"); + cJSON* created_at_item = cJSON_GetObjectItemCaseSensitive(row, "created_at"); + cJSON* duration_item = cJSON_GetObjectItemCaseSensitive(row, "duration_seconds"); + cJSON* wsi_pointer_item = cJSON_GetObjectItemCaseSensitive(row, "wsi_pointer"); + + const char* sub_id = cJSON_IsString(sub_id_item) ? sub_id_item->valuestring : ""; + const char* client_ip = cJSON_IsString(client_ip_item) ? client_ip_item->valuestring : ""; + const char* filter_json = cJSON_IsString(filter_json_item) ? filter_json_item->valuestring : "[]"; + long long events_sent = cJSON_IsNumber(events_sent_item) ? (long long)events_sent_item->valuedouble : 0; + long long created_at = cJSON_IsNumber(created_at_item) ? (long long)created_at_item->valuedouble : 0; + long long duration_seconds = cJSON_IsNumber(duration_item) ? (long long)duration_item->valuedouble : 0; + const char* wsi_pointer = cJSON_IsString(wsi_pointer_item) ? wsi_pointer_item->valuestring : ""; // DEBUG: Log each subscription found DEBUG_LOG("Row %d: sub_id=%s, client_ip=%s, events_sent=%lld, created_at=%lld", - row_count, sub_id ? sub_id : "NULL", client_ip ? client_ip : "NULL", + row_count, sub_id[0] ? sub_id : "NULL", client_ip[0] ? client_ip : "NULL", events_sent, created_at); // Add basic subscription info - cJSON_AddStringToObject(sub_obj, "id", sub_id ? sub_id : ""); - cJSON_AddStringToObject(sub_obj, "client_ip", client_ip ? client_ip : ""); + cJSON_AddStringToObject(sub_obj, "id", sub_id); + cJSON_AddStringToObject(sub_obj, "client_ip", client_ip); cJSON_AddNumberToObject(sub_obj, "created_at", (double)created_at); cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds); cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent); cJSON_AddBoolToObject(sub_obj, "active", 1); // All from this view are active - cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer ? wsi_pointer : "N/A"); + cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer[0] ? wsi_pointer : "N/A"); // Extract query stats from per_session_data if wsi is still valid int db_queries = 0; @@ -317,7 +275,7 @@ cJSON* query_subscription_details(void) { double row_rate = 0.0; double avg_rows_per_query = 0.0; - if (wsi_pointer && strlen(wsi_pointer) > 2) { // Check for valid pointer string + if (wsi_pointer[0] != '\0' && strlen(wsi_pointer) > 2) { // Check for valid pointer string // Parse wsi pointer from hex string struct lws* wsi = NULL; if (sscanf(wsi_pointer, "%p", (void**)&wsi) == 1 && wsi != NULL) { @@ -326,7 +284,7 @@ cJSON* query_subscription_details(void) { if (pss) { db_queries = pss->db_queries_executed; db_rows = pss->db_rows_returned; - + // Calculate rates (per minute) time_t connection_duration = current_time - pss->query_tracking_start; if (connection_duration > 0) { @@ -334,7 +292,7 @@ cJSON* query_subscription_details(void) { query_rate = db_queries / minutes; row_rate = db_rows / minutes; } - + // Calculate average rows per query if (db_queries > 0) { avg_rows_per_query = (double)db_rows / (double)db_queries; @@ -365,8 +323,7 @@ cJSON* query_subscription_details(void) { cJSON_AddItemToArray(subscriptions_array, sub_obj); } - - sqlite3_finalize(stmt); + cJSON_Delete(rows); // Add subscriptions array and count to data cJSON_AddItemToObject(data, "subscriptions", subscriptions_array); @@ -378,9 +335,9 @@ cJSON* query_subscription_details(void) { clock_gettime(CLOCK_MONOTONIC, &query_end); long elapsed_us = (query_end.tv_sec - query_start.tv_sec) * 1000000L + (query_end.tv_nsec - query_start.tv_nsec) / 1000L; - + log_query_execution("MONITOR", "subscription_details", NULL, sql, elapsed_us, row_count); - + // DEBUG: Log final summary DEBUG_LOG("Total subscriptions found: %d", row_count); DEBUG_LOG("=== END SUBSCRIPTION_DETAILS QUERY DEBUG ==="); @@ -1336,8 +1293,7 @@ cJSON* query_cpu_metrics(void) { // Generate stats JSON from database queries char* generate_stats_json(void) { - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for stats generation"); return NULL; } @@ -1362,85 +1318,80 @@ char* generate_stats_json(void) { pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); cJSON_AddNumberToObject(response, "active_subscriptions", active_subs); - // Query total events count - sqlite3_stmt* stmt; - if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0)); - } - sqlite3_finalize(stmt); + long long total_events = 0; + if (db_get_total_event_count_ll(&total_events) == 0) { + cJSON_AddNumberToObject(response, "total_events", total_events); } // Query event kinds distribution cJSON* event_kinds = cJSON_CreateArray(); - if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { + long long kinds_total = 0; + cJSON* kind_rows = db_get_event_kind_distribution_rows(&kinds_total); + if (kind_rows) { + cJSON* kind_row = NULL; + cJSON_ArrayForEach(kind_row, kind_rows) { cJSON* kind_obj = cJSON_CreateObject(); - cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0)); - cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1)); - cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2)); + cJSON* kind = cJSON_GetObjectItemCaseSensitive(kind_row, "kind"); + cJSON* count = cJSON_GetObjectItemCaseSensitive(kind_row, "count"); + double count_val = cJSON_IsNumber(count) ? count->valuedouble : 0.0; + double pct = (kinds_total > 0) ? ((count_val * 100.0) / (double)kinds_total) : 0.0; + + cJSON_AddNumberToObject(kind_obj, "kind", cJSON_IsNumber(kind) ? kind->valuedouble : 0.0); + cJSON_AddNumberToObject(kind_obj, "count", count_val); + cJSON_AddNumberToObject(kind_obj, "percentage", pct); cJSON_AddItemToArray(event_kinds, kind_obj); } - sqlite3_finalize(stmt); + cJSON_Delete(kind_rows); } cJSON_AddItemToObject(response, "event_kinds", event_kinds); // Query time-based statistics cJSON* time_stats = cJSON_CreateObject(); - if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* period = (const char*)sqlite3_column_text(stmt, 0); - sqlite3_int64 count = sqlite3_column_int64(stmt, 1); - - if (strcmp(period, "total") == 0) { - cJSON_AddNumberToObject(time_stats, "total", count); - } else if (strcmp(period, "24h") == 0) { - cJSON_AddNumberToObject(time_stats, "last_24h", count); - } else if (strcmp(period, "7d") == 0) { - cJSON_AddNumberToObject(time_stats, "last_7d", count); - } else if (strcmp(period, "30d") == 0) { - cJSON_AddNumberToObject(time_stats, "last_30d", count); - } - } - sqlite3_finalize(stmt); + time_t now = time(NULL); + long long c24h = 0, c7d = 0, c30d = 0; + if (db_get_event_count_since(now - 86400, &c24h) == 0) { + cJSON_AddNumberToObject(time_stats, "last_24h", c24h); } + if (db_get_event_count_since(now - 604800, &c7d) == 0) { + cJSON_AddNumberToObject(time_stats, "last_7d", c7d); + } + if (db_get_event_count_since(now - 2592000, &c30d) == 0) { + cJSON_AddNumberToObject(time_stats, "last_30d", c30d); + } + cJSON_AddNumberToObject(time_stats, "total", total_events); cJSON_AddItemToObject(response, "time_stats", time_stats); // Query top pubkeys cJSON* top_pubkeys = cJSON_CreateArray(); - if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON* pubkey_rows = db_get_top_pubkeys_rows(10); + if (pubkey_rows) { + cJSON* row = NULL; + cJSON_ArrayForEach(row, pubkey_rows) { cJSON* pubkey_obj = cJSON_CreateObject(); - const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); - cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : ""); - cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1)); - cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2)); + cJSON* pubkey_item = cJSON_GetObjectItemCaseSensitive(row, "pubkey"); + cJSON* count_item = cJSON_GetObjectItemCaseSensitive(row, "count"); + double count_val = cJSON_IsNumber(count_item) ? count_item->valuedouble : 0.0; + double pct = (total_events > 0) ? ((count_val * 100.0) / (double)total_events) : 0.0; + + cJSON_AddStringToObject(pubkey_obj, "pubkey", cJSON_IsString(pubkey_item) ? pubkey_item->valuestring : ""); + cJSON_AddNumberToObject(pubkey_obj, "event_count", count_val); + cJSON_AddNumberToObject(pubkey_obj, "percentage", pct); cJSON_AddItemToArray(top_pubkeys, pubkey_obj); } - sqlite3_finalize(stmt); + cJSON_Delete(pubkey_rows); } cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys); - // Get database creation timestamp (oldest event) - if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0); - if (oldest_timestamp > 0) { - cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp); - } + // Get database creation/latest timestamps + long long min_created_at = 0; + long long max_created_at = 0; + if (db_get_event_time_bounds(&min_created_at, &max_created_at) == 0) { + if (min_created_at > 0) { + cJSON_AddNumberToObject(response, "database_created_at", (double)min_created_at); } - sqlite3_finalize(stmt); - } - - // Get latest event timestamp - if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0); - if (latest_timestamp > 0) { - cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp); - } + if (max_created_at > 0) { + cJSON_AddNumberToObject(response, "latest_event_at", (double)max_created_at); } - sqlite3_finalize(stmt); } // Convert to JSON string @@ -1567,8 +1518,7 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content, // Generate config text from database char* generate_config_text(void) { - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("NIP-17: Database not available for config query"); return NULL; } @@ -1587,25 +1537,29 @@ char* generate_config_text(void) { "🔧 Relay Configuration\n" "━━━━━━━━━━━━━━━━━━━━━━━━\n"); - // Query all config values from database - sqlite3_stmt* stmt; - if (sqlite3_prepare_v2(g_db, "SELECT key, value FROM config ORDER BY key", -1, &stmt, NULL) == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW && offset < 8192 - 200) { - const char* key = (const char*)sqlite3_column_text(stmt, 0); - const char* value = (const char*)sqlite3_column_text(stmt, 1); - - if (key && value) { - offset += snprintf(config_text + offset, 8192 - offset, - "%s: %s\n", key, value); - } - } - sqlite3_finalize(stmt); - } else { + cJSON* rows = db_get_all_config_rows(); + if (!rows) { free(config_text); DEBUG_ERROR("NIP-17: Failed to query config from database"); return NULL; } + cJSON* row = NULL; + cJSON_ArrayForEach(row, rows) { + if (offset >= 8192 - 200) break; + + cJSON* key_item = cJSON_GetObjectItemCaseSensitive(row, "key"); + cJSON* value_item = cJSON_GetObjectItemCaseSensitive(row, "value"); + const char* key = cJSON_IsString(key_item) ? key_item->valuestring : NULL; + const char* value = cJSON_IsString(value_item) ? value_item->valuestring : NULL; + + if (key && value) { + offset += snprintf(config_text + offset, 8192 - offset, + "%s: %s\n", key, value); + } + } + cJSON_Delete(rows); + // Footer offset += snprintf(config_text + offset, 8192 - offset, "\n"); @@ -2166,8 +2120,7 @@ int apply_config_change(const char* key, const char* value) { return -1; } - extern sqlite3* g_db; - if (!g_db) { + if (!db_is_available()) { DEBUG_ERROR("Database not available for config change"); return -1; } @@ -2200,30 +2153,11 @@ int apply_config_change(const char* key, const char* value) { } // Update or insert the configuration value - sqlite3_stmt* stmt; - const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)"; - - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare config update statement"); - const char* err_msg = sqlite3_errmsg(g_db); - DEBUG_ERROR(err_msg); - return -1; - } - - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, normalized_value, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC); - - int result = sqlite3_step(stmt); - if (result != SQLITE_DONE) { + if (db_upsert_config_value(key, normalized_value, data_type) != 0) { DEBUG_ERROR("Failed to update configuration in database"); - const char* err_msg = sqlite3_errmsg(g_db); - DEBUG_ERROR(err_msg); - sqlite3_finalize(stmt); return -1; } - sqlite3_finalize(stmt); return 0; } diff --git a/src/config.c b/src/config.c index 22be7c1..8f21d97 100644 --- a/src/config.c +++ b/src/config.c @@ -3,6 +3,7 @@ #include "debug.h" #include "default_config_event.h" #include "dm_admin.h" +#include "db_ops.h" // Undefine VERSION macros before including nostr_core.h to avoid redefinition warnings // This must come AFTER default_config_event.h so that RELAY_VERSION macro expansion works correctly @@ -225,56 +226,13 @@ int store_config_event_in_database(const cJSON* event) { if (!event || !g_db) { return -1; } - - // Get event fields - cJSON* id_obj = cJSON_GetObjectItemCaseSensitive(event, "id"); - cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); - cJSON* created_at_obj = cJSON_GetObjectItemCaseSensitive(event, "created_at"); - cJSON* kind_obj = cJSON_GetObjectItemCaseSensitive(event, "kind"); - cJSON* content_obj = cJSON_GetObjectItemCaseSensitive(event, "content"); - cJSON* sig_obj = cJSON_GetObjectItemCaseSensitive(event, "sig"); - cJSON* tags_obj = cJSON_GetObjectItemCaseSensitive(event, "tags"); - - if (!id_obj || !pubkey_obj || !created_at_obj || !kind_obj || !content_obj || !sig_obj || !tags_obj) { - return -1; - } - - // Convert tags to JSON string - char* tags_str = cJSON_Print(tags_obj); - if (!tags_str) { - return -1; - } - - // Insert or replace the configuration event - const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; - sqlite3_stmt* stmt; - - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare configuration event insert"); - free(tags_str); - return -1; - } - - sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id_obj), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_STATIC); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); - sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj)); - sqlite3_bind_text(stmt, 5, "regular", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_obj), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 8, tags_str, -1, SQLITE_TRANSIENT); - - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - free(tags_str); - - if (rc == SQLITE_DONE) { - return 0; - } else { + + if (db_store_config_event(event) != 0) { DEBUG_ERROR("Failed to store configuration event"); return -1; } + + return 0; } cJSON* load_config_event_from_database(const char* relay_pubkey) { @@ -527,27 +485,13 @@ int store_relay_private_key(const char* relay_privkey_hex) { DEBUG_ERROR("Database not available for relay private key storage"); return -1; } - - const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)"; - sqlite3_stmt* stmt; - - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare relay private key storage query"); - return -1; - } - - sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_STATIC); - - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - if (rc == SQLITE_DONE) { + + if (db_store_relay_private_key_hex(relay_privkey_hex) == 0) { return 0; - } else { - DEBUG_ERROR("Failed to store relay private key in database"); - return -1; } + + DEBUG_ERROR("Failed to store relay private key in database"); + return -1; } char* get_relay_private_key(void) { @@ -556,32 +500,15 @@ char* get_relay_private_key(void) { return NULL; } - const char* sql = "SELECT private_key_hex FROM relay_seckey"; - sqlite3_stmt* stmt; - - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare relay private key retrieval query"); + char* private_key = db_get_relay_private_key_hex_dup(); + if (!private_key || strlen(private_key) != 64) { + if (private_key) { + free(private_key); + } + DEBUG_ERROR("Relay private key not found in secure storage"); return NULL; } - - char* private_key = NULL; - if (sqlite3_step(stmt) == SQLITE_ROW) { - const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0); - if (key_from_db && strlen(key_from_db) == 64) { - private_key = malloc(65); // 64 chars + null terminator - if (private_key) { - strcpy(private_key, key_from_db); - } - } - } - - sqlite3_finalize(stmt); - - if (!private_key) { - DEBUG_ERROR("Relay private key not found in secure storage"); - } - + return private_key; } @@ -1685,30 +1612,7 @@ const char* get_config_value_from_table(const char* key) { return NULL; } - const char* sql = "SELECT value FROM config WHERE key = ?"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - return NULL; - } - - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); - - const char* result = NULL; - - int step_rc = sqlite3_step(stmt); - - if (step_rc == SQLITE_ROW) { - const char* value = (char*)sqlite3_column_text(stmt, 0); - if (value) { - // Return a dynamically allocated string to prevent buffer reuse - result = strdup(value); - } - } - - sqlite3_finalize(stmt); - return result; + return db_get_config_value_dup(key); } // Set value in config table @@ -1717,27 +1621,8 @@ int set_config_value_in_table(const char* key, const char* value, const char* da if (!g_db || !key || !value || !data_type) { return -1; } - - const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) " - "VALUES (?, ?, ?, ?, ?, ?)"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 6, requires_restart); - - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? 0 : -1; + + return db_set_config_value_full(key, value, data_type, description, category, requires_restart); } // Update config in table (simpler version of set_config_value_in_table) @@ -1745,29 +1630,15 @@ int update_config_in_table(const char* key, const char* value) { if (!g_db || !key || !value) { return -1; } - + // Additional validation: reject empty strings to prevent accidental deletion of config values if (strlen(value) == 0) { DEBUG_WARN("Attempted to update config with empty value - rejecting to prevent data loss"); printf(" Rejected empty value for key: %s\n", key); return -1; } - - const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, value, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC); - - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? 0 : -1; + + return db_update_config_value_only(key, value); } // DEPRECATED: This function is no longer used in the unified startup flow. @@ -2210,23 +2081,7 @@ int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, return -1; } - const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) " - "VALUES (?, ?, ?)"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC); - - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? 0 : -1; + return db_add_auth_rule(rule_type, pattern_type, pattern_value); } // Remove auth rule from configuration @@ -2235,23 +2090,8 @@ int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type if (!g_db || !rule_type || !pattern_type || !pattern_value) { return -1; } - - const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; - - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC); - - rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? 0 : -1; + + return db_remove_auth_rule(rule_type, pattern_type, pattern_value); } // ================================ diff --git a/src/db_ops.c b/src/db_ops.c index b286782..bc138e2 100644 --- a/src/db_ops.c +++ b/src/db_ops.c @@ -5,6 +5,7 @@ #include "config.h" #include +#include #include // Database handle owned by main.c today. @@ -290,3 +291,457 @@ int db_count_active_whitelist_rules(void) { sqlite3_close(db); return count; } + +int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) { + if (!g_db || !sql || !out_count) return -1; + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return -1; + } + + for (int i = 0; i < bind_param_count; i++) { + const char* param = (bind_params && bind_params[i]) ? bind_params[i] : ""; + sqlite3_bind_text(stmt, i + 1, param, -1, SQLITE_TRANSIENT); + } + + int count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = sqlite3_column_int(stmt, 0); + } + + sqlite3_finalize(stmt); + *out_count = count; + return 0; +} + +int db_get_total_event_count_ll(long long* out_count) { + if (!g_db || !out_count) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT COUNT(*) FROM events"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + + long long count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = sqlite3_column_int64(stmt, 0); + } + + sqlite3_finalize(stmt); + *out_count = count; + return 0; +} + +int db_get_event_count_since(time_t cutoff, long long* out_count) { + if (!g_db || !out_count) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff); + + long long count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = sqlite3_column_int64(stmt, 0); + } + + sqlite3_finalize(stmt); + *out_count = count; + return 0; +} + +cJSON* db_get_event_kind_distribution_rows(long long* out_total_events) { + if (!g_db) return NULL; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; + + cJSON* rows = cJSON_CreateArray(); + if (!rows) { + sqlite3_finalize(stmt); + return NULL; + } + + long long total = 0; + while (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON* row = cJSON_CreateObject(); + if (!row) { + sqlite3_finalize(stmt); + cJSON_Delete(rows); + return NULL; + } + + int kind = sqlite3_column_int(stmt, 0); + long long count = sqlite3_column_int64(stmt, 1); + total += count; + + cJSON_AddNumberToObject(row, "kind", kind); + cJSON_AddNumberToObject(row, "count", count); + cJSON_AddItemToArray(rows, row); + } + + sqlite3_finalize(stmt); + if (out_total_events) { + *out_total_events = total; + } + return rows; +} + +cJSON* db_get_top_pubkeys_rows(int limit) { + if (!g_db) return NULL; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT ?"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; + + sqlite3_bind_int(stmt, 1, limit > 0 ? limit : 10); + + cJSON* rows = cJSON_CreateArray(); + if (!rows) { + sqlite3_finalize(stmt); + return NULL; + } + + while (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON* row = cJSON_CreateObject(); + if (!row) { + sqlite3_finalize(stmt); + cJSON_Delete(rows); + return NULL; + } + + const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); + long long count = sqlite3_column_int64(stmt, 1); + cJSON_AddStringToObject(row, "pubkey", pubkey ? pubkey : ""); + cJSON_AddNumberToObject(row, "count", count); + cJSON_AddItemToArray(rows, row); + } + + sqlite3_finalize(stmt); + return rows; +} + +cJSON* db_get_subscription_details_rows(void) { + if (!g_db) return NULL; + + sqlite3_stmt* stmt = NULL; + const char* sql = + "SELECT * " + "FROM active_subscriptions_log " + "ORDER BY created_at DESC"; + + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; + + cJSON* rows = cJSON_CreateArray(); + if (!rows) { + sqlite3_finalize(stmt); + return NULL; + } + + while (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON* row = cJSON_CreateObject(); + if (!row) { + sqlite3_finalize(stmt); + cJSON_Delete(rows); + return NULL; + } + + const char* sub_id = (const char*)sqlite3_column_text(stmt, 0); + const char* client_ip = (const char*)sqlite3_column_text(stmt, 1); + const char* filter_json = (const char*)sqlite3_column_text(stmt, 2); + long long events_sent = sqlite3_column_int64(stmt, 3); + long long created_at = sqlite3_column_int64(stmt, 4); + long long duration_seconds = sqlite3_column_int64(stmt, 5); + const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 6); + + cJSON_AddStringToObject(row, "id", sub_id ? sub_id : ""); + cJSON_AddStringToObject(row, "client_ip", client_ip ? client_ip : ""); + cJSON_AddStringToObject(row, "filter_json", filter_json ? filter_json : "[]"); + cJSON_AddNumberToObject(row, "events_sent", events_sent); + cJSON_AddNumberToObject(row, "created_at", created_at); + cJSON_AddNumberToObject(row, "duration_seconds", duration_seconds); + cJSON_AddStringToObject(row, "wsi_pointer", wsi_pointer ? wsi_pointer : ""); + + cJSON_AddItemToArray(rows, row); + } + + sqlite3_finalize(stmt); + return rows; +} + +cJSON* db_get_all_config_rows(void) { + if (!g_db) return NULL; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT key, value FROM config ORDER BY key"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; + + cJSON* rows = cJSON_CreateArray(); + if (!rows) { + sqlite3_finalize(stmt); + return NULL; + } + + while (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON* row = cJSON_CreateObject(); + if (!row) { + sqlite3_finalize(stmt); + cJSON_Delete(rows); + return NULL; + } + + const char* key = (const char*)sqlite3_column_text(stmt, 0); + const char* value = (const char*)sqlite3_column_text(stmt, 1); + cJSON_AddStringToObject(row, "key", key ? key : ""); + cJSON_AddStringToObject(row, "value", value ? value : ""); + cJSON_AddItemToArray(rows, row); + } + + sqlite3_finalize(stmt); + return rows; +} + +char* db_get_config_value_dup(const char* key) { + if (!g_db || !key) return NULL; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT value FROM config WHERE key = ?"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); + + char* result = NULL; + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* value = (const char*)sqlite3_column_text(stmt, 0); + if (value) result = strdup(value); + } + + sqlite3_finalize(stmt); + return result; +} + +int db_set_config_value_full(const char* key, const char* value, const char* data_type, + const char* description, const char* category, int requires_restart) { + if (!g_db || !key || !value || !data_type) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = + "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) " + "VALUES (?, ?, ?, ?, ?, ?)"; + + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 6, requires_restart); + + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return (rc == SQLITE_DONE) ? 0 : -1; +} + +int db_update_config_value_only(const char* key, const char* value) { + if (!g_db || !key || !value) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT); + + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return (rc == SQLITE_DONE) ? 0 : -1; +} + +int db_upsert_config_value(const char* key, const char* value, const char* data_type) { + if (!g_db || !key || !value || !data_type) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)"; + + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT); + + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return (rc == SQLITE_DONE) ? 0 : -1; +} + +int db_store_relay_private_key_hex(const char* relay_privkey_hex) { + if (!g_db || !relay_privkey_hex) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + + sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_TRANSIENT); + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? 0 : -1; +} + +char* db_get_relay_private_key_hex_dup(void) { + if (!g_db) return NULL; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT private_key_hex FROM relay_seckey"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; + + char* result = NULL; + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0); + if (key_from_db) result = strdup(key_from_db); + } + + sqlite3_finalize(stmt); + return result; +} + +int db_store_config_event(const cJSON* event) { + if (!g_db || !event) return -1; + + cJSON* id_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "id"); + cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "pubkey"); + cJSON* created_at_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "created_at"); + cJSON* kind_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "kind"); + cJSON* content_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "content"); + cJSON* sig_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "sig"); + cJSON* tags_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "tags"); + if (!id_obj || !pubkey_obj || !created_at_obj || !kind_obj || !content_obj || !sig_obj || !tags_obj) { + return -1; + } + + char* tags_str = cJSON_Print(tags_obj); + if (!tags_str) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + free(tags_str); + return -1; + } + + sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id_obj), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); + sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj)); + sqlite3_bind_text(stmt, 5, "regular", -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_obj), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 8, tags_str, -1, SQLITE_TRANSIENT); + + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + free(tags_str); + + return (rc == SQLITE_DONE) ? 0 : -1; +} + +int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) { + if (!g_db || !out_min_created_at || !out_max_created_at) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT MIN(created_at), MAX(created_at) FROM events"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return -1; + } + + long long min_ts = 0; + long long max_ts = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + min_ts = sqlite3_column_int64(stmt, 0); + max_ts = sqlite3_column_int64(stmt, 1); + } + + sqlite3_finalize(stmt); + *out_min_created_at = min_ts; + *out_max_created_at = max_ts; + return 0; +} + +int db_get_config_row_count(int* out_count) { + if (!g_db || !out_count) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "SELECT COUNT(*) FROM config"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return -1; + } + + int count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = sqlite3_column_int(stmt, 0); + } + + sqlite3_finalize(stmt); + *out_count = count; + return 0; +} + +int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { + if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + + sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT); + + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return (rc == SQLITE_DONE) ? 0 : -1; +} + +int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { + if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1; + + sqlite3_stmt* stmt = NULL; + const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; + if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + + sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT); + + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return (rc == SQLITE_DONE) ? 0 : -1; +} + +int db_delete_wot_whitelist_rules(void) { + if (!g_db) return -1; + + const char* sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'"; + int rc = sqlite3_exec(g_db, sql, NULL, NULL, NULL); + return (rc == SQLITE_OK) ? 0 : -1; +} + +int db_count_wot_whitelist_rules(void) { + if (!g_db) return 0; + + const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1"; + int count = 0; + if (db_count_with_sql(sql, NULL, 0, &count) != 0) { + return 0; + } + return count; +} diff --git a/src/db_ops.h b/src/db_ops.h index e7b8e0e..158c300 100644 --- a/src/db_ops.h +++ b/src/db_ops.h @@ -2,7 +2,9 @@ #define DB_OPS_H #include +#include #include +#include // Generic helpers int db_is_available(void); @@ -28,4 +30,37 @@ int db_is_hash_blacklisted(const char* resource_hash); int db_is_pubkey_whitelisted(const char* pubkey); int db_count_active_whitelist_rules(void); +// Generic prepared COUNT helper +int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count); + +// Monitoring/stat helpers (Phase 1 api.c migration) +int db_get_total_event_count_ll(long long* out_count); +int db_get_event_count_since(time_t cutoff, long long* out_count); +cJSON* db_get_event_kind_distribution_rows(long long* out_total_events); +cJSON* db_get_top_pubkeys_rows(int limit); +cJSON* db_get_subscription_details_rows(void); + +// Config helpers +cJSON* db_get_all_config_rows(void); +char* db_get_config_value_dup(const char* key); +int db_set_config_value_full(const char* key, const char* value, const char* data_type, + const char* description, const char* category, int requires_restart); +int db_update_config_value_only(const char* key, const char* value); +int db_upsert_config_value(const char* key, const char* value, const char* data_type); +int db_store_relay_private_key_hex(const char* relay_privkey_hex); +char* db_get_relay_private_key_hex_dup(void); +int db_store_config_event(const cJSON* event); + +// Event timestamp helpers +int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at); + +// Config table helpers +int db_get_config_row_count(int* out_count); + +// Auth/WoT rule helpers +int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value); +int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value); +int db_delete_wot_whitelist_rules(void); +int db_count_wot_whitelist_rules(void); + #endif // DB_OPS_H diff --git a/src/dm_admin.c b/src/dm_admin.c index 6fa8f37..5871895 100644 --- a/src/dm_admin.c +++ b/src/dm_admin.c @@ -5,6 +5,7 @@ #include "config.h" #include "debug.h" #include "api.h" +#include "db_ops.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "../nostr_core_lib/nostr_core/nip017.h" #include "../nostr_core_lib/nostr_core/nip044.h" @@ -17,9 +18,6 @@ #include #include -// External database connection (from main.c) -extern sqlite3* g_db; - // Forward declarations for unified handlers extern int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); extern int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); @@ -622,8 +620,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err update_config_in_table("nip42_auth_required_subscriptions", "false"); // Clear wot_whitelist rules - const char* delete_sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'"; - sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL); + db_delete_wot_whitelist_rules(); snprintf(response_msg, sizeof(response_msg), "🔓 Web of Trust Disabled\n" @@ -695,16 +692,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err else if (strcmp(wot_cmd, "status") == 0 || strlen(wot_cmd) == 0) { // Show status // Count whitelisted pubkeys - sqlite3_stmt* stmt; - const char* count_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1"; - int whitelist_count = 0; - - if (sqlite3_prepare_v2(g_db, count_sql, -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - whitelist_count = sqlite3_column_int(stmt, 0); - } - sqlite3_finalize(stmt); - } + int whitelist_count = db_count_wot_whitelist_rules(); const char* level_str; if (wot_level == 0) level_str = "Off (open relay)"; diff --git a/src/main.c b/src/main.c index 28b8bb7..c3fdcbc 100644 --- a/src/main.c +++ b/src/main.c @@ -27,6 +27,7 @@ #include "subscriptions.h" // Subscription management system #include "debug.h" // Debug system #include "thread_pool.h" // Thread pool scaffold +#include "db_ops.h" // Forward declarations for unified request validator int nostr_validate_unified_request(const char* json_string, size_t json_length); @@ -434,36 +435,11 @@ int init_database(const char* database_path_override) { // DEBUG_GUARD_START if (g_debug_level >= DEBUG_LEVEL_DEBUG) { // Check config table row count immediately after database open - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL); - if (rc == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - int row_count = sqlite3_column_int(stmt, 0); - DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count); - } - sqlite3_finalize(stmt); + int row_count = 0; + if (db_get_config_row_count(&row_count) == 0) { + DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count); } else { - // Capture and log the actual SQLite error instead of assuming table doesn't exist - const char* err_msg = sqlite3_errmsg(g_db); - DEBUG_LOG("Failed to prepare config table query: %s (error code: %d)", err_msg, rc); - - // Check if it's actually a missing table vs other error - if (rc == SQLITE_ERROR) { - // Try to check if config table exists - sqlite3_stmt* check_stmt; - int check_rc = sqlite3_prepare_v2(g_db, "SELECT name FROM sqlite_master WHERE type='table' AND name='config'", -1, &check_stmt, NULL); - if (check_rc == SQLITE_OK) { - int has_table = (sqlite3_step(check_stmt) == SQLITE_ROW); - sqlite3_finalize(check_stmt); - if (has_table) { - DEBUG_LOG("Config table EXISTS but query failed - possible database corruption or locking issue"); - } else { - DEBUG_LOG("Config table does not exist yet (first-time startup)"); - } - } else { - DEBUG_LOG("Failed to check table existence: %s (error code: %d)", sqlite3_errmsg(g_db), check_rc); - } - } + DEBUG_LOG("Config table count unavailable immediately after sqlite3_open() (table may not exist yet)"); } } // DEBUG_GUARD_END @@ -2038,13 +2014,9 @@ int main(int argc, char* argv[]) { // DEBUG_GUARD_START if (g_debug_level >= DEBUG_LEVEL_DEBUG) { - sqlite3_stmt* stmt; - if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - int row_count = sqlite3_column_int(stmt, 0); - DEBUG_LOG("Config table row count after init_database() (first-time): %d", row_count); - } - sqlite3_finalize(stmt); + int row_count = 0; + if (db_get_config_row_count(&row_count) == 0) { + DEBUG_LOG("Config table row count after init_database() (first-time): %d", row_count); } } // DEBUG_GUARD_END @@ -2183,13 +2155,9 @@ int main(int argc, char* argv[]) { // DEBUG_GUARD_START if (g_debug_level >= DEBUG_LEVEL_DEBUG) { - sqlite3_stmt* stmt; - if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - int row_count = sqlite3_column_int(stmt, 0); - DEBUG_LOG("Config table row count after init_database(): %d", row_count); - } - sqlite3_finalize(stmt); + int row_count = 0; + if (db_get_config_row_count(&row_count) == 0) { + DEBUG_LOG("Config table row count after init_database(): %d", row_count); } } diff --git a/src/main.h b/src/main.h index 5b2c970..8e558e4 100644 --- a/src/main.h +++ b/src/main.h @@ -13,8 +13,8 @@ // Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define CRELAY_VERSION_MAJOR 2 #define CRELAY_VERSION_MINOR 0 -#define CRELAY_VERSION_PATCH 1 -#define CRELAY_VERSION "v2.0.1" +#define CRELAY_VERSION_PATCH 2 +#define CRELAY_VERSION "v2.0.2" // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" diff --git a/src/websockets.c b/src/websockets.c index ecf9a10..7dfe12d 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -32,6 +32,7 @@ #include "dm_admin.h" // DM admin functions including NIP-17 #include "ip_ban.h" // IP auth failure ban system #include "thread_pool.h" // Thread pool scaffold +#include "db_ops.h" // DB abstraction wrappers // Forward declarations for logging functions @@ -108,7 +109,6 @@ void send_notice_message(struct lws* wsi, struct per_session_data* pss, const ch extern int get_config_bool(const char* key, int default_value); // Forward declarations for global state -extern sqlite3* g_db; extern int g_server_running; extern volatile sig_atomic_t g_shutdown_flag; extern int g_restart_requested; @@ -351,43 +351,55 @@ int process_message_queue(struct lws* wsi, struct per_session_data* pss) { return -1; } - // Get next message from queue (thread-safe) - pthread_mutex_lock(&pss->session_lock); + // Drain as many queued messages as possible in one writeable callback. + // This avoids short-lived clients timing out before receiving all EVENT/EOSE frames. + int processed = 0; + while (1) { + pthread_mutex_lock(&pss->session_lock); + + struct message_queue_node* node = pss->message_queue_head; + if (!node) { + // Queue is empty + pss->writeable_requested = 0; + pthread_mutex_unlock(&pss->session_lock); + break; + } + + // Remove from queue + pss->message_queue_head = node->next; + if (!pss->message_queue_head) { + pss->message_queue_tail = NULL; + } + pss->message_queue_count--; - struct message_queue_node* node = pss->message_queue_head; - if (!node) { - // Queue is empty - pss->writeable_requested = 0; pthread_mutex_unlock(&pss->session_lock); - return 0; + + // Write message (libwebsockets handles partial writes internally) + int write_result = lws_write(wsi, node->data + LWS_PRE, node->length, node->type); + + // Free node resources + free(node->data); + free(node); + + if (write_result < 0) { + DEBUG_ERROR("process_message_queue: write failed, result=%d", write_result); + return -1; + } + + processed++; + + // If socket is currently choked, stop and continue later. + if (lws_send_pipe_choked(wsi)) { + break; + } } - // Remove from queue - pss->message_queue_head = node->next; - if (!pss->message_queue_head) { - pss->message_queue_tail = NULL; - } - pss->message_queue_count--; + DEBUG_TRACE("Processed %d queued messages", processed); - pthread_mutex_unlock(&pss->session_lock); - - // Write message (libwebsockets handles partial writes internally) - int write_result = lws_write(wsi, node->data + LWS_PRE, node->length, node->type); - - // Free node resources - free(node->data); - free(node); - - if (write_result < 0) { - DEBUG_ERROR("process_message_queue: write failed, result=%d", write_result); - return -1; - } - - DEBUG_TRACE("Processed message: wrote %d bytes, remaining in queue: %d", write_result, pss->message_queue_count); - - // If queue not empty, request another callback + // If queue still has pending messages, ensure we request another callback. pthread_mutex_lock(&pss->session_lock); if (pss->message_queue_head) { + pss->writeable_requested = 1; lws_callback_on_writable(wsi); } else { pss->writeable_requested = 0; @@ -3122,28 +3134,11 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st clock_gettime(CLOCK_MONOTONIC, &query_start); // Execute count query - sqlite3_stmt* stmt; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to prepare COUNT query: %s", sqlite3_errmsg(g_db)); - DEBUG_ERROR(error_msg); + int filter_count = 0; + if (db_count_with_sql(sql, (const char**)bind_params, bind_param_count, &filter_count) != 0) { + DEBUG_ERROR("Failed to execute COUNT query via db_ops"); continue; } - - // Bind parameters - for (int i = 0; i < bind_param_count; i++) { - sqlite3_bind_text(stmt, i + 1, bind_params[i], -1, SQLITE_TRANSIENT); - } - - int filter_count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - filter_count = sqlite3_column_int(stmt, 0); - } - - // Filter count calculated - - sqlite3_finalize(stmt); // Stop query timing and log clock_gettime(CLOCK_MONOTONIC, &query_end); diff --git a/tests/45_nip_test.sh b/tests/45_nip_test.sh index e703c66..d48cbbd 100755 --- a/tests/45_nip_test.sh +++ b/tests/45_nip_test.sh @@ -54,6 +54,8 @@ BASELINE_KIND1=0 BASELINE_KIND0=0 BASELINE_KIND30001=0 BASELINE_AUTHOR=0 +BASELINE_AUTHOR_KIND0=0 +BASELINE_AUTHOR_KIND30001=0 BASELINE_TYPE_REGULAR=0 BASELINE_TEST_NIP45=0 BASELINE_KINDS_01=0 @@ -253,9 +255,11 @@ run_count_test() { publish_event "$regular1" "regular" "Regular event #1" - # Now that we have the pubkey, get the author baseline + # Now that we have the pubkey, get author baselines local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null) BASELINE_AUTHOR=$(get_baseline_count "{\"authors\":[\"$test_pubkey\"]}" "events by test author") + BASELINE_AUTHOR_KIND0=$(get_baseline_count "{\"authors\":[\"$test_pubkey\"],\"kinds\":[0]}" "kind 0 events by test author") + BASELINE_AUTHOR_KIND30001=$(get_baseline_count "{\"authors\":[\"$test_pubkey\"],\"kinds\":[30001]}" "kind 30001 events by test author") publish_event "$regular2" "regular" "Regular event #2" publish_event "$regular3" "regular" "Regular event #3" @@ -314,10 +318,13 @@ run_count_test() { fi # Test 3: Count events by author (pubkey) - # BASELINE_AUTHOR includes the first regular event, we add 2 more regular - # Replaceable and addressable replace existing events from this author + # BASELINE_AUTHOR is measured after regular1 is already published. + # Net additions after baseline: + # +2 regular (regular2, regular3) + # +(1 - BASELINE_AUTHOR_KIND0) from replaceable upsert behavior + # +(1 - BASELINE_AUTHOR_KIND30001) from addressable replacement behavior local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null) - local expected_author=$((2 + BASELINE_AUTHOR)) + local expected_author=$((BASELINE_AUTHOR + 2 + (1 - BASELINE_AUTHOR_KIND0) + (1 - BASELINE_AUTHOR_KIND30001))) if ! test_count "count_author" "{\"authors\":[\"$test_pubkey\"]}" "Count events by specific author" "$expected_author"; then ((test_failures++)) fi @@ -340,8 +347,10 @@ run_count_test() { fi # Test 6: Count multiple kinds - # BASELINE_KINDS_01 + 3 regular events = total for kinds 0+1 - local expected_kinds_01=$((3 + BASELINE_KINDS_01)) + # Net additions for kinds 0+1 after baseline: + # +3 kind-1 regular events + # +(1 - BASELINE_AUTHOR_KIND0) for kind-0 replaceable upsert behavior + local expected_kinds_01=$((BASELINE_KINDS_01 + 3 + (1 - BASELINE_AUTHOR_KIND0))) if ! test_count "count_multi_kinds" '{"kinds":[0,1]}' "Count multiple kinds (0,1)" "$expected_kinds_01"; then ((test_failures++)) fi diff --git a/tests/nip42_test.log b/tests/nip42_test.log index 80230a9..8ed825e 100644 --- a/tests/nip42_test.log +++ b/tests/nip42_test.log @@ -1,11 +1,13 @@ === NIP-42 Authentication Test Started === -2026-02-24 09:45:30 - Starting NIP-42 authentication tests +2026-04-01 05:46:11 - Starting NIP-42 authentication tests [INFO] === Starting NIP-42 Authentication Tests === [INFO] Checking dependencies... +[WARNING] wscat not found. Some manual WebSocket tests will be skipped +[WARNING] Install with: npm install -g wscat [SUCCESS] Dependencies check complete [INFO] Test 1: Checking NIP-42 support in relay info [SUCCESS] NIP-42 is advertised in supported NIPs -2026-02-24 09:45:30 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70 +2026-04-01 05:46:11 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70 [INFO] Test 2: Testing AUTH challenge generation [WARNING] Could not extract admin private key from relay.log - using manual test approach [INFO] Manual test: Connect to relay and send an event without auth to trigger challenge @@ -13,64 +15,61 @@ [INFO] Generated test keypair: test_pubkey [INFO] Attempting to publish event without authentication... [INFO] Publishing test event to relay... -2026-02-24 09:45:31 - Event publish result: connecting to ws://localhost:8888... ok. -{"kind":1,"id":"74a0b723797569b2483d47457d2f6e2378aed6ccd4cca0f553038e0431ade0e8","pubkey":"7a2f213c220c46a194c5730e5bb2fe9b27304f0c27226f380086d93ff10bf7da","created_at":1771940731,"tags":[],"content":"NIP-42 test event - should require auth","sig":"319466df2167cd7f33c83d41b09bdd06f23c961380fd205a0a5dc5a923f11e45b6d158d79e251239bef364430c5d45e6a9e43fe93fd629864a88de00e6766a44"} +2026-04-01 05:46:12 - Event publish result: connecting to localhost:8888... ok. +{"kind":1,"id":"334754e42c2bd54bdf733fb2c9a28c1e0f33d2275ca2c70f26062187793b37fe","pubkey":"40d9f28055baeba6a9bcac1942a17370add58a95a46985f66c5c14d485573983","created_at":1775036772,"tags":[],"content":"NIP-42 test event - should require auth","sig":"6e81c7335d34a770c72566250ac363aa5a126383a1efe2effe5b4b7e84bf600c4d9531b26e9be188fe884e9bd32aa384d49739815c7fba4594174277b90cbf7d"} publishing to ws://localhost:8888... success. [SUCCESS] Relay requested authentication as expected [INFO] Test 4: Testing WebSocket AUTH message handling -[INFO] Testing WebSocket connection and AUTH message... -[INFO] Sending test message via WebSocket... -2026-02-24 09:45:31 - WebSocket response: -[INFO] No AUTH challenge in WebSocket response +[WARNING] Skipping WebSocket tests - wscat not available [INFO] Test 5: Testing NIP-42 configuration options [INFO] Retrieving current relay configuration... [WARNING] Could not retrieve configuration events [INFO] Test 6: Testing NIP-42 performance and stability [INFO] Testing multiple authentication attempts... -2026-02-24 09:45:33 - Attempt 1: .264126491s - connecting to ws://localhost:8888... ok. -{"kind":1,"id":"9cb8f626ced97e8fc406bd2d2358074fe33542d983d11a7f039a26919ded6039","pubkey":"7c098dbaeeaca6fb27798625835dfc5c13fb31096eed76c6d77269a2a08cd22b","created_at":1771940733,"tags":[],"content":"Performance test event 1","sig":"87025c881004a7936589785bbba6171542135348e4f798c490c033eb462c9a0322882cfe1a2b742e3644b6c69ee4fee76feb3cffecd6d64c096fa8038bcd84c4"} +2026-04-01 05:46:13 - Attempt 1: .187311847s - connecting to localhost:8888... ok. +{"kind":1,"id":"470a74d32e1b739e6bfd275c0d03ab1508fb5908c0fa01523e4a3bdce6a95db3","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036773,"tags":[],"content":"Performance test event 1","sig":"0c30a125cb67d336413f006a5dfa422d788e177254e496b966854bf45183238d23f6575dba37dec6fa46b6f6b906a25afafcbbcaa7d0c15add89dd7fd1a80c22"} publishing to ws://localhost:8888... success. -2026-02-24 09:45:33 - Attempt 2: .262980094s - connecting to ws://localhost:8888... ok. -{"kind":1,"id":"bb1b3da4cccbcdc6be73646e29ecf060b68d8cb9340f8b23da0f167ffac0831d","pubkey":"7c098dbaeeaca6fb27798625835dfc5c13fb31096eed76c6d77269a2a08cd22b","created_at":1771940733,"tags":[],"content":"Performance test event 2","sig":"1349f7af97edb9f507081782cfb2d055eb810ec96056b97692920060091ccf39b8d1ba8590d25e1b3c92f4b785f6362ac0d734373d29d4159e5778f8982b086f"} +2026-04-01 05:46:13 - Attempt 2: .185565503s - connecting to localhost:8888... ok. +{"kind":1,"id":"e4f9db77d30d8e71ef01b16dcb1179d03462aaf368aae30120898f3758a9d0b9","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036773,"tags":[],"content":"Performance test event 2","sig":"24a8f38c2cb230d95120e60a246a1a03ac17d4d195d25f2ae0b2f674c3383287eca53cd7fc61bc3cdc58433a3d2c2108558f4f1a98dcfc02a97b4ec8c537cdde"} publishing to ws://localhost:8888... success. -2026-02-24 09:45:34 - Attempt 3: .296311100s - connecting to ws://localhost:8888... ok. -{"kind":1,"id":"3b171de7c00e918ad0bcbedea4c07c69f59a9d20c31b0de957488c919fa5c116","pubkey":"7c098dbaeeaca6fb27798625835dfc5c13fb31096eed76c6d77269a2a08cd22b","created_at":1771940734,"tags":[],"content":"Performance test event 3","sig":"a1699f12064d4da7ba15519eb7666cd2801f333ee837df70ce1298ac97477e900d095dce8fed0bade8f3e3e2e2b68e22800d10ba213a1de6cd1537112d6a6c2a"} +2026-04-01 05:46:14 - Attempt 3: .185110183s - connecting to localhost:8888... ok. +{"kind":1,"id":"4a85e466756452e90358fe5d98fa41bda93b6bc77cb079bb1616ca8873f4fca1","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 3","sig":"72ab716ff3b4786309fd4cef27ae8cd9df1e3c1746b5e49ba45c070cafcfb72a5b7ba045e7d798f23b5d82989526b4d3c818509f0eec639c8ad265be85a4728e"} publishing to ws://localhost:8888... success. -2026-02-24 09:45:35 - Attempt 4: .300140904s - connecting to ws://localhost:8888... ok. -{"kind":1,"id":"f3587b9955204284ec65f2b171d1fa3f330e0bbfc2d95fe7ad0550a63a6aabe6","pubkey":"7c098dbaeeaca6fb27798625835dfc5c13fb31096eed76c6d77269a2a08cd22b","created_at":1771940734,"tags":[],"content":"Performance test event 4","sig":"fb78cd3bce49097eea1c67c0a1ee92bf4cdf5bf6396ce20519481d0e4d09b03dca05c70da2ddf5ba51d23aa3253b21ec3b57bc41d000f274bb1123a4c3d64c45"} +2026-04-01 05:46:14 - Attempt 4: .185725746s - connecting to localhost:8888... ok. +{"kind":1,"id":"4c05b6359da88f85c0aa4c94a296a465f71bddaa10d849ed0923f972ac0628a9","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 4","sig":"3b0c8bd1f3d965a259a0ab228ea4c47defef3ad563ebce860ec65ab0ee341c5305c0988ac88651664d3090ce2ff282a0fe4913231448158c423a74b6e3b2a7a9"} publishing to ws://localhost:8888... success. -2026-02-24 09:45:35 - Attempt 5: .373738147s - connecting to ws://localhost:8888... ok. -{"kind":1,"id":"e1fdf2d0579f0e2b446bdd9dbe5f5e53913b7d328d6f527ac7142636979c1ff2","pubkey":"7c098dbaeeaca6fb27798625835dfc5c13fb31096eed76c6d77269a2a08cd22b","created_at":1771940735,"tags":[],"content":"Performance test event 5","sig":"a11a11991434511737092d8cf2806a700a207c81c08e341f8d98c8b3369845df35adb9247ff094378e6712d68e5b2657d628e809b17bc9f838f6f5d9c21ce96c"} +2026-04-01 05:46:15 - Attempt 5: .184467365s - connecting to localhost:8888... ok. +{"kind":1,"id":"0902e868c5e7909f0143fde7738d5e8f4c42ea0ced2c69a404cfeb6cd686ceaa","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 5","sig":"a0754e774e5bc3514dc4cf04ec51cab077413251cfce480e80badccc7660bfe8d1eac87d43c6e332d936cc171d24e8318e1ef5df7425a8218e01af661f291c47"} publishing to ws://localhost:8888... success. [SUCCESS] Performance test completed: 5/5 successful responses [INFO] Test 7: Testing kind-specific NIP-42 authentication requirements [INFO] Generated test keypair for kind-specific tests: test_pubkey [INFO] Testing kind 1 event (regular note) - should work without authentication... -2026-02-24 09:45:36 - Kind 1 event result: connecting to ws://localhost:8888... ok. -{"kind":1,"id":"85430aa1bb17e0aa7f5f05568448e35570a0d8446a79600f033e2683b17f8902","pubkey":"1cee941c80f1fa0037047151cf577503a1243cbef748f572b9fd6062a36807db","created_at":1771940736,"tags":[],"content":"Regular note - should not require auth","sig":"118a2dec1256aca39b22b9027c8f56e0b8f60bae1d95f38408ac9d9a6eb6590c48e4a37bf561e29f993b435ceecc1ff2218fcc9dec3e2dcf305cd456ac6a0ee4"} +2026-04-01 05:46:15 - Kind 1 event result: connecting to localhost:8888... ok. +{"kind":1,"id":"a1d9545c4ac16c2ab1fae4d5d77cabd368d765533b6f6d1f4dbb3c7859696a5e","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036775,"tags":[],"content":"Regular note - should not require auth","sig":"520796b21690bf03b20da28f30ffc84c9c198d0a908c07540f232f789c7a0327861e1f580d499be15b244bdf66c5c4b1cdf621bfdc5f60b1313d7f40036ffab8"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 1 event accepted without authentication (correct behavior) [INFO] Testing kind 4 event (direct message) - should require authentication... -2026-02-24 09:45:47 - Kind 4 event result: connecting to ws://localhost:8888... ok. -{"kind":4,"id":"32f20767dd83d6f1ee5e70f72c33c7caa8a5a03acbfbb4de5899f6b27b850be4","pubkey":"1cee941c80f1fa0037047151cf577503a1243cbef748f572b9fd6062a36807db","created_at":1771940737,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"cee96adf8c266120d98579734134cca652eeefa5f090d119b794a964bae4d2568e3aa39d648e5a897bb7f8d65be19f338291af3ffe87accdbf502f5eaae453ab"} +2026-04-01 05:46:25 - Kind 4 event result: connecting to localhost:8888... ok. +{"kind":4,"id":"c047f5a9fb6ff46828d0bb862e4bcc32120e50fc7804d651485e2cdbddaf993a","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036775,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"a7a027c302faf75e269d866a1b0137f2fbc02643d76435dc68707d2ba6560af95df709787f4a1a1bc7c1b9951615ad081a97cd7f694da94d5606d17100f0b0dc"} publishing to ws://localhost:8888... [SUCCESS] Kind 4 event requested authentication (correct behavior for DMs) [INFO] Testing kind 14 event (chat message) - should require authentication... -2026-02-24 09:45:57 - Kind 14 event result: connecting to ws://localhost:8888... ok. -{"kind":14,"id":"0a6d37f32fc10dae793b61d8f3afb9d28bdf12d59cf25d73eb90461d8efaa117","pubkey":"1cee941c80f1fa0037047151cf577503a1243cbef748f572b9fd6062a36807db","created_at":1771940747,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"5337ded22499a8c361445d6c660f297e636cc5adf2735a85bb7d629e5771e0c47176b2afde02f4af9645198d441fb0aedd78931d0200d8b5ed8f721e6460d5c9"} +2026-04-01 05:46:36 - Kind 14 event result: connecting to localhost:8888... ok. +{"kind":14,"id":"73e50afdaefdbb01ed021f8c888a87b00aa59e4d68c124101353fabcdfa59ae1","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036786,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"ec2c713e2add876856e6cb0d1d9790c43525c5139a0fe06fbee71898f1042173dc3af9187e6fbccfef7406c1af2f218631a1cdcb7ad73e34553cdf3aecc4f209"} publishing to ws://localhost:8888... [SUCCESS] Kind 14 event requested authentication (correct behavior for DMs) [INFO] Testing other event kinds - should work without authentication... -2026-02-24 09:45:58 - Kind 0 event result: connecting to ws://localhost:8888... ok. -{"kind":0,"id":"5836e2da48eb458c8faca084afc3827ee6e481574d0295dc7bfdc50d9495891a","pubkey":"1cee941c80f1fa0037047151cf577503a1243cbef748f572b9fd6062a36807db","created_at":1771940757,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"e454d5990a157b6211b655a9219ad494c58d49a59d61c4e1fb0aa91b96e5ff2368efc57e2acd0a24c7198b1181c37d0ab535f608289ec83afdc0b68e076def7c"} +2026-04-01 05:46:36 - Kind 0 event result: connecting to localhost:8888... ok. +{"kind":0,"id":"51fe1630721cfa3472ba7c3af911a3164c788a48c6a678179c62be48ac3d1f1c","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036796,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"d72d827742fdc86daf1389c8dad40efc56c22b9744ad23547ea0b0a37598e5c18ccdac20b9ef2e6adf46d7683bff19f2cbc11fb00a252903efc1076c8f613b38"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 0 event accepted without authentication (correct) -2026-02-24 09:45:58 - Kind 3 event result: connecting to ws://localhost:8888... ok. -{"kind":3,"id":"d11a038044f9ea859338c6ab879c4628aece258f2cad0ed7ca88808b35ec899c","pubkey":"1cee941c80f1fa0037047151cf577503a1243cbef748f572b9fd6062a36807db","created_at":1771940758,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"9fbf410cd609f9f7a6bf3118ecfe554776bb647d5ed988c0d4df06f83c1283c8baeaa0b62da900934cc8f617c73572fbce9f7232c77bb87942e4471228b06c73"} +2026-04-01 05:46:36 - Kind 3 event result: connecting to localhost:8888... ok. +{"kind":3,"id":"bf4155eb8b54bdd73a0b64365666caf886433634693cc3c4ab1d586187fc6bae","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036796,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"d27f654ccc9f95e76aacafcce7d42528545fa90b77d422a17301de6931883d8c00a0a84af56ab5d37a0f425140dbfc227458557f9050597beec42cacd0a15864"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 3 event accepted without authentication (correct) -2026-02-24 09:45:59 - Kind 7 event result: connecting to ws://localhost:8888... ok. -{"kind":7,"id":"b819482f287f6370fac7804c2cdcdac8340eafd86799b24af0a11f74c6190fed","pubkey":"1cee941c80f1fa0037047151cf577503a1243cbef748f572b9fd6062a36807db","created_at":1771940758,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"f197c7c072af111dd92220c489472473f5b4fb30e908501e4d2d0037f1a0064960550a4e51af4168572a192295d870f6a6c49473308a25dacbc0deb703003547"} +2026-04-01 05:46:37 - Kind 7 event result: connecting to localhost:8888... ok. +{"kind":7,"id":"658e0caf1c4b52062363849d55312070005124e8a10202df285078222e523132","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036797,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"50361b3aa6b85747befe20e50e317d8f905128930cec0263494434ec9029089d880ff23cdf071c5b63b8417eff216658f1229c0f0dc5174a9a16a6d5fe38fd4d"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 7 event accepted without authentication (correct) [INFO] Kind-specific authentication test completed