diff --git a/README.md b/README.md index ba16e02..e3d1ded 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,8 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo - [x] NIP-01: Basic protocol flow implementation - [x] NIP-09: Event deletion - [x] NIP-11: Relay information document -- [ ] NIP-12: Generic tag queries -- [ ] NIP-13: Proof of Work +- [x] NIP-13: Proof of Work - [x] NIP-15: End of Stored Events Notice -- [ ] NIP-16: Event Treatment - [x] NIP-20: Command Results - [ ] NIP-22: Event `created_at` Limits - [ ] NIP-25: Reactions @@ -27,3 +25,65 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo - [ ] NIP-50: Keywords filter. [experimental](#search) - [ ] NIP-70: Protected Events +## NIP-13: Proof of Work Configuration + +The relay supports NIP-13 Proof of Work validation with configurable settings. PoW validation helps prevent spam and ensures computational commitment from event publishers. + +### Environment Variables + +Configure PoW validation using these environment variables: + +- `RELAY_POW_ENABLED` - Enable/disable PoW validation (default: `1`) + - `1`, `true`, or `yes` to enable + - `0`, `false`, or `no` to disable + +- `RELAY_MIN_POW_DIFFICULTY` - Minimum required difficulty (default: `0`) + - Range: `0-64` (reasonable bounds) + - `0` = no minimum requirement (events without PoW are accepted) + - Higher values require more computational work + +- `RELAY_POW_MODE` - Validation mode (default: `basic`) + - `basic` - Basic PoW validation + - `full` - Full validation with nonce tag requirements + - `strict` - Strict anti-spam mode with committed target validation + - `disabled` - Disable PoW validation entirely + +### Examples + +```bash +# Basic setup - accept events with or without PoW +export RELAY_POW_ENABLED=1 +export RELAY_MIN_POW_DIFFICULTY=0 +export RELAY_POW_MODE=basic + +# Anti-spam setup - require minimum difficulty 16 +export RELAY_POW_ENABLED=1 +export RELAY_MIN_POW_DIFFICULTY=16 +export RELAY_POW_MODE=strict + +# Disable PoW validation completely +export RELAY_POW_ENABLED=0 +``` + +### Behavior + +- **min_difficulty=0**: Events without PoW are accepted; events with PoW are validated +- **min_difficulty>0**: All events must have valid PoW meeting minimum difficulty +- **strict mode**: Additional validation prevents difficulty commitment gaming +- **NIP-11 integration**: PoW configuration is advertised via relay information document + +### Testing + +Run the comprehensive PoW test suite: + +```bash +./tests/13_nip_test.sh +``` + +The test suite validates: +- NIP-11 PoW support advertisement +- Event acceptance without PoW (when min_difficulty=0) +- Event validation with valid PoW +- Configuration via environment variables +- NIP-13 reference event validation + diff --git a/c-relay-x86_64 b/c-relay-x86_64 new file mode 100755 index 0000000..2b58d2c Binary files /dev/null and b/c-relay-x86_64 differ diff --git a/db/c_nostr_relay.db-shm b/db/c_nostr_relay.db-shm index 579e5b3..c7a42c3 100644 Binary files a/db/c_nostr_relay.db-shm and b/db/c_nostr_relay.db-shm differ diff --git a/db/c_nostr_relay.db-wal b/db/c_nostr_relay.db-wal index e4c8bbe..b10bb0a 100644 Binary files a/db/c_nostr_relay.db-wal and b/db/c_nostr_relay.db-wal differ diff --git a/nostr_core_lib b/nostr_core_lib index 33129d8..55e2a9c 160000 --- a/nostr_core_lib +++ b/nostr_core_lib @@ -1 +1 @@ -Subproject commit 33129d82fdce8cff280bc0b5ba7ed5e49531606d +Subproject commit 55e2a9c68e449ac375d1bdbb72a2bcf3e6eec9f3 diff --git a/relay.log b/relay.log index 7f8e33b..a9003d0 100644 --- a/relay.log +++ b/relay.log @@ -1,6 +1,8 @@ === C Nostr Relay Server === [SUCCESS] Database connection established [SUCCESS] Relay information initialized with default values +[INFO] Initializing NIP-13 Proof of Work configuration +[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full [INFO] Starting relay server... [INFO] Starting libwebsockets-based Nostr relay server... [SUCCESS] WebSocket relay started on ws://127.0.0.1:8888 @@ -9,4 +11,107 @@ [SUCCESS] NIP-11 relay information served successfully [INFO] HTTP request received [INFO] Handling NIP-11 relay information request -[WARNING] HTTP request without proper Accept header for NIP-11 +[SUCCESS] NIP-11 relay information served successfully +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[INFO] PoW validated: difficulty=10, target=8, nonce=1839 +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] WebSocket connection closed +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[INFO] PoW validated: difficulty=21, target=20, nonce=776797 +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[INFO] PoW validated: difficulty=8, target=8, nonce=385 +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] Received WebSocket message +[INFO] WebSocket connection closed +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[INFO] PoW validated: difficulty=21, target=20, nonce=776797 +[WARNING] Event already exists in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[INFO] PoW validated: difficulty=8, target=8, nonce=1669 +[SUCCESS] Event stored in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed +[INFO] HTTP request received +[INFO] Handling NIP-11 relay information request +[SUCCESS] NIP-11 relay information served successfully +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message with full NIP-01 validation +[INFO] PoW validated: difficulty=21, target=20, nonce=776797 +[WARNING] Event already exists in database +[SUCCESS] Event validated and stored successfully +[INFO] WebSocket connection closed diff --git a/relay.pid b/relay.pid index 2236e83..34db337 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -714947 +736301 diff --git a/src/main.c b/src/main.c index e4922a3..1d3b7f7 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,7 @@ // Include nostr_core_lib for Nostr functionality #include "../nostr_core_lib/cjson/cJSON.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" +#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work // Server Configuration #define DEFAULT_PORT 8888 @@ -74,6 +75,28 @@ struct relay_info { // Global relay information instance static struct relay_info g_relay_info = {0}; +// NIP-13 PoW configuration structure +struct pow_config { + int enabled; // 0 = disabled, 1 = enabled + int min_pow_difficulty; // Minimum required difficulty (0 = no requirement) + int validation_flags; // Bitflags for validation options + int require_nonce_tag; // 1 = require nonce tag presence + int reject_lower_targets; // 1 = reject if committed < actual difficulty + int strict_format; // 1 = enforce strict nonce tag format + int anti_spam_mode; // 1 = full anti-spam validation +}; + +// Global PoW configuration instance +static struct pow_config g_pow_config = { + .enabled = 1, // Enable PoW validation by default + .min_pow_difficulty = 0, // No minimum difficulty by default + .validation_flags = NOSTR_POW_VALIDATE_BASIC, + .require_nonce_tag = 0, // Don't require nonce tags by default + .reject_lower_targets = 0, // Allow lower committed targets by default + .strict_format = 0, // Relaxed format validation by default + .anti_spam_mode = 0 // Basic validation by default +}; + ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// @@ -190,6 +213,10 @@ void cleanup_relay_info(); cJSON* generate_relay_info_json(); int handle_nip11_http_request(struct lws* wsi, const char* accept_header); +// Forward declarations for NIP-13 PoW validation +void init_pow_config(); +int validate_event_pow(cJSON* event, char* error_message, size_t error_size); + ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// @@ -1238,6 +1265,7 @@ void init_relay_info() { cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information + cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results } @@ -1251,7 +1279,7 @@ void init_relay_info() { cJSON_AddNumberToObject(g_relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH); cJSON_AddNumberToObject(g_relay_info.limitation, "max_event_tags", 100); cJSON_AddNumberToObject(g_relay_info.limitation, "max_content_length", 8196); - cJSON_AddNumberToObject(g_relay_info.limitation, "min_pow_difficulty", 0); + cJSON_AddNumberToObject(g_relay_info.limitation, "min_pow_difficulty", g_pow_config.min_pow_difficulty); cJSON_AddBoolToObject(g_relay_info.limitation, "auth_required", cJSON_False); cJSON_AddBoolToObject(g_relay_info.limitation, "payment_required", cJSON_False); cJSON_AddBoolToObject(g_relay_info.limitation, "restricted_writes", cJSON_False); @@ -1564,6 +1592,171 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) { return 0; } +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// +// NIP-13 PROOF OF WORK VALIDATION +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +// Initialize PoW configuration with environment variables and defaults +void init_pow_config() { + log_info("Initializing NIP-13 Proof of Work configuration"); + + // Initialize with defaults (already set in struct initialization) + + // Check environment variables for configuration + const char* pow_enabled_env = getenv("RELAY_POW_ENABLED"); + if (pow_enabled_env) { + g_pow_config.enabled = (strcmp(pow_enabled_env, "1") == 0 || + strcmp(pow_enabled_env, "true") == 0 || + strcmp(pow_enabled_env, "yes") == 0); + } + + const char* min_diff_env = getenv("RELAY_MIN_POW_DIFFICULTY"); + if (min_diff_env) { + int min_diff = atoi(min_diff_env); + if (min_diff >= 0 && min_diff <= 64) { // Reasonable bounds + g_pow_config.min_pow_difficulty = min_diff; + } + } + + const char* pow_mode_env = getenv("RELAY_POW_MODE"); + if (pow_mode_env) { + if (strcmp(pow_mode_env, "strict") == 0) { + g_pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT; + g_pow_config.require_nonce_tag = 1; + g_pow_config.reject_lower_targets = 1; + g_pow_config.strict_format = 1; + g_pow_config.anti_spam_mode = 1; + log_info("PoW configured in strict anti-spam mode"); + } else if (strcmp(pow_mode_env, "full") == 0) { + g_pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL; + g_pow_config.require_nonce_tag = 1; + log_info("PoW configured in full validation mode"); + } else if (strcmp(pow_mode_env, "basic") == 0) { + g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC; + log_info("PoW configured in basic validation mode"); + } else if (strcmp(pow_mode_env, "disabled") == 0) { + g_pow_config.enabled = 0; + log_info("PoW validation disabled via RELAY_POW_MODE"); + } + } + + // Log final configuration + char config_msg[512]; + snprintf(config_msg, sizeof(config_msg), + "PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s", + g_pow_config.enabled ? "true" : "false", + g_pow_config.min_pow_difficulty, + g_pow_config.validation_flags, + g_pow_config.anti_spam_mode ? "anti-spam" : + (g_pow_config.validation_flags & NOSTR_POW_VALIDATE_FULL) ? "full" : "basic"); + log_info(config_msg); +} + +// Validate event Proof of Work according to NIP-13 +int validate_event_pow(cJSON* event, char* error_message, size_t error_size) { + if (!g_pow_config.enabled) { + return 0; // PoW validation disabled + } + + if (!event) { + snprintf(error_message, error_size, "pow: null event"); + return NOSTR_ERROR_INVALID_INPUT; + } + + // If min_pow_difficulty is 0, only validate events that have nonce tags + // This allows events without PoW when difficulty requirement is 0 + if (g_pow_config.min_pow_difficulty == 0) { + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + int has_nonce_tag = 0; + + if (tags && cJSON_IsArray(tags)) { + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + if (cJSON_IsString(tag_name)) { + const char* name = cJSON_GetStringValue(tag_name); + if (name && strcmp(name, "nonce") == 0) { + has_nonce_tag = 1; + break; + } + } + } + } + } + + // If no minimum difficulty required and no nonce tag, skip PoW validation + if (!has_nonce_tag) { + return 0; // Accept event without PoW when min_difficulty=0 + } + } + + // Perform PoW validation using nostr_core_lib + nostr_pow_result_t pow_result; + int validation_result = nostr_validate_pow(event, g_pow_config.min_pow_difficulty, + g_pow_config.validation_flags, &pow_result); + + if (validation_result != NOSTR_SUCCESS) { + // Handle specific error cases with appropriate messages + switch (validation_result) { + case NOSTR_ERROR_NIP13_INSUFFICIENT: + snprintf(error_message, error_size, + "pow: insufficient difficulty: %d < %d", + pow_result.actual_difficulty, g_pow_config.min_pow_difficulty); + log_warning("Event rejected: insufficient PoW difficulty"); + break; + case NOSTR_ERROR_NIP13_NO_NONCE_TAG: + // This should not happen with min_difficulty=0 after our check above + if (g_pow_config.min_pow_difficulty > 0) { + snprintf(error_message, error_size, "pow: missing required nonce tag"); + log_warning("Event rejected: missing nonce tag"); + } else { + return 0; // Allow when min_difficulty=0 + } + break; + case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: + snprintf(error_message, error_size, "pow: invalid nonce tag format"); + log_warning("Event rejected: invalid nonce tag format"); + break; + case NOSTR_ERROR_NIP13_TARGET_MISMATCH: + snprintf(error_message, error_size, + "pow: committed target (%d) lower than minimum (%d)", + pow_result.committed_target, g_pow_config.min_pow_difficulty); + log_warning("Event rejected: committed target too low (anti-spam protection)"); + break; + case NOSTR_ERROR_NIP13_CALCULATION: + snprintf(error_message, error_size, "pow: difficulty calculation failed"); + log_error("PoW difficulty calculation error"); + break; + case NOSTR_ERROR_EVENT_INVALID_ID: + snprintf(error_message, error_size, "pow: invalid event ID format"); + log_warning("Event rejected: invalid event ID for PoW calculation"); + break; + default: + snprintf(error_message, error_size, "pow: validation failed - %s", + strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error"); + log_warning("Event rejected: PoW validation failed"); + } + return validation_result; + } + + // Log successful PoW validation (only if minimum difficulty is required) + if (g_pow_config.min_pow_difficulty > 0 || pow_result.has_nonce_tag) { + char debug_msg[256]; + snprintf(debug_msg, sizeof(debug_msg), + "PoW validated: difficulty=%d, target=%d, nonce=%llu%s", + pow_result.actual_difficulty, + pow_result.committed_target, + (unsigned long long)pow_result.nonce_value, + pow_result.has_nonce_tag ? "" : " (no nonce tag)"); + log_info(debug_msg); + } + + return 0; // Success +} + ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // DATABASE FUNCTIONS @@ -2189,14 +2382,20 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size) { return signature_result; } - // Step 3: Complete event validation (combines structure + signature + additional checks) + // Step 3: Validate Proof of Work (NIP-13) if enabled + int pow_result = validate_event_pow(event, error_message, error_size); + if (pow_result != 0) { + return pow_result; // PoW validation failed, error message already set + } + + // Step 4: Complete event validation (combines structure + signature + additional checks) int validation_result = nostr_validate_event(event); if (validation_result != NOSTR_SUCCESS) { snprintf(error_message, error_size, "invalid: complete event validation failed"); return validation_result; } - // Step 4: Check for special event types and handle accordingly + // Step 5: Check for special event types and handle accordingly cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at"); @@ -2236,7 +2435,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size) { } } - // Step 5: Store event in database + // Step 6: Store event in database if (store_event(event) == 0) { error_message[0] = '\0'; // Success - empty error message log_success("Event validated and stored successfully"); @@ -2603,6 +2802,9 @@ int main(int argc, char* argv[]) { // Initialize NIP-11 relay information init_relay_info(); + // Initialize NIP-13 PoW configuration + init_pow_config(); + log_info("Starting relay server..."); // Start WebSocket Nostr relay server diff --git a/tests/11_nip_information.sh b/tests/11_nip_information.sh new file mode 100755 index 0000000..d0fc99a --- /dev/null +++ b/tests/11_nip_information.sh @@ -0,0 +1,432 @@ +#!/bin/bash + +# NIP-11 Relay Information Document Test +# Tests HTTP endpoint for relay information according to NIP-11 specification + +set -e # Exit on any error + +# Color constants +RED='\033[31m' +GREEN='\033[32m' +YELLOW='\033[33m' +BLUE='\033[34m' +BOLD='\033[1m' +RESET='\033[0m' + +# Test configuration +RELAY_URL="http://127.0.0.1:8888" +RELAY_WS_URL="ws://127.0.0.1:8888" + +# Print functions +print_header() { + echo -e "${BLUE}${BOLD}=== $1 ===${RESET}" +} + +print_step() { + echo -e "${YELLOW}[STEP]${RESET} $1" +} + +print_success() { + echo -e "${GREEN}✓${RESET} $1" +} + +print_error() { + echo -e "${RED}✗${RESET} $1" +} + +print_info() { + echo -e "${BLUE}[INFO]${RESET} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${RESET} $1" +} + +# Test functions +test_http_with_correct_header() { + print_step "Testing HTTP request with correct Accept header" + + local response="" + local http_code="" + + if command -v curl &> /dev/null; then + # Use curl to test with proper Accept header + response=$(curl -s -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "") + http_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "000") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + if [[ "$http_code" == "200" ]]; then + print_success "HTTP 200 OK received with correct Accept header" + + # Validate JSON response + if echo "$response" | jq . >/dev/null 2>&1; then + print_success "Response is valid JSON" + return 0 + else + print_error "Response is not valid JSON" + return 1 + fi + else + print_error "Expected HTTP 200, got HTTP $http_code" + return 1 + fi +} + +test_http_without_header() { + print_step "Testing HTTP request without Accept header (should return 406)" + + local http_code="" + + if command -v curl &> /dev/null; then + http_code=$(curl -s -o /dev/null -w "%{http_code}" "$RELAY_URL/" 2>/dev/null || echo "000") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + if [[ "$http_code" == "406" ]]; then + print_success "HTTP 406 Not Acceptable received without proper Accept header" + return 0 + else + print_error "Expected HTTP 406, got HTTP $http_code" + return 1 + fi +} + +test_http_with_wrong_header() { + print_step "Testing HTTP request with wrong Accept header (should return 406)" + + local http_code="" + + if command -v curl &> /dev/null; then + http_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/json" "$RELAY_URL/" 2>/dev/null || echo "000") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + if [[ "$http_code" == "406" ]]; then + print_success "HTTP 406 Not Acceptable received with wrong Accept header" + return 0 + else + print_error "Expected HTTP 406, got HTTP $http_code" + return 1 + fi +} + +test_cors_headers() { + print_step "Testing CORS headers presence" + + local headers="" + + if command -v curl &> /dev/null; then + headers=$(curl -s -I -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + local cors_origin_found=false + local cors_headers_found=false + local cors_methods_found=false + + if echo "$headers" | grep -qi "access-control-allow-origin"; then + cors_origin_found=true + print_success "Access-Control-Allow-Origin header found" + fi + + if echo "$headers" | grep -qi "access-control-allow-headers"; then + cors_headers_found=true + print_success "Access-Control-Allow-Headers header found" + fi + + if echo "$headers" | grep -qi "access-control-allow-methods"; then + cors_methods_found=true + print_success "Access-Control-Allow-Methods header found" + fi + + if [[ "$cors_origin_found" == true && "$cors_headers_found" == true && "$cors_methods_found" == true ]]; then + print_success "All required CORS headers present" + return 0 + else + print_error "Missing CORS headers" + return 1 + fi +} + +test_json_structure() { + print_step "Testing NIP-11 JSON structure and required fields" + + local response="" + + if command -v curl &> /dev/null; then + response=$(curl -s -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + if [[ -z "$response" ]]; then + print_error "Empty response received" + return 1 + fi + + # Validate JSON structure using jq + if ! echo "$response" | jq . >/dev/null 2>&1; then + print_error "Response is not valid JSON" + return 1 + fi + + print_success "Valid JSON structure confirmed" + + # Check for required fields + local required_checks=0 + local total_checks=0 + + # Test name field + ((total_checks++)) + if echo "$response" | jq -e '.name' >/dev/null 2>&1; then + local name=$(echo "$response" | jq -r '.name') + print_success "Name field present: $name" + ((required_checks++)) + else + print_warning "Name field missing (optional)" + fi + + # Test supported_nips field (required) + ((total_checks++)) + if echo "$response" | jq -e '.supported_nips' >/dev/null 2>&1; then + local nips=$(echo "$response" | jq -r '.supported_nips | @json') + print_success "Supported NIPs field present: $nips" + ((required_checks++)) + + # Verify NIP-11 is in the supported list + if echo "$response" | jq -e '.supported_nips | contains([11])' >/dev/null 2>&1; then + print_success "NIP-11 correctly listed in supported NIPs" + else + print_warning "NIP-11 not found in supported NIPs list" + fi + else + print_error "Supported NIPs field missing (should be present)" + fi + + # Test software field + ((total_checks++)) + if echo "$response" | jq -e '.software' >/dev/null 2>&1; then + local software=$(echo "$response" | jq -r '.software') + print_success "Software field present: $software" + ((required_checks++)) + else + print_warning "Software field missing (optional)" + fi + + # Test version field + ((total_checks++)) + if echo "$response" | jq -e '.version' >/dev/null 2>&1; then + local version=$(echo "$response" | jq -r '.version') + print_success "Version field present: $version" + ((required_checks++)) + else + print_warning "Version field missing (optional)" + fi + + # Test limitation object + ((total_checks++)) + if echo "$response" | jq -e '.limitation' >/dev/null 2>&1; then + print_success "Limitation object present" + ((required_checks++)) + + # Check some common limitation fields + if echo "$response" | jq -e '.limitation.max_message_length' >/dev/null 2>&1; then + local max_msg=$(echo "$response" | jq -r '.limitation.max_message_length') + print_info " max_message_length: $max_msg" + fi + + if echo "$response" | jq -e '.limitation.max_subscriptions' >/dev/null 2>&1; then + local max_subs=$(echo "$response" | jq -r '.limitation.max_subscriptions') + print_info " max_subscriptions: $max_subs" + fi + else + print_warning "Limitation object missing (recommended)" + fi + + # Test description field + if echo "$response" | jq -e '.description' >/dev/null 2>&1; then + local description=$(echo "$response" | jq -r '.description') + print_success "Description field present: ${description:0:50}..." + else + print_warning "Description field missing (optional)" + fi + + print_info "JSON structure validation: $required_checks/$total_checks core fields present" + return 0 +} + +test_content_type_header() { + print_step "Testing Content-Type header" + + local headers="" + + if command -v curl &> /dev/null; then + headers=$(curl -s -I -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + if echo "$headers" | grep -qi "content-type.*application/nostr+json"; then + print_success "Correct Content-Type header: application/nostr+json" + return 0 + else + print_warning "Content-Type header not exactly 'application/nostr+json'" + echo "$headers" | grep -i "content-type" | head -1 + return 1 + fi +} + +test_non_root_path() { + print_step "Testing non-root path (should return 404)" + + local http_code="" + + if command -v curl &> /dev/null; then + http_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/nostr+json" "$RELAY_URL/nonexistent" 2>/dev/null || echo "000") + else + print_error "curl command not found - required for NIP-11 testing" + return 1 + fi + + if [[ "$http_code" == "404" ]]; then + print_success "HTTP 404 Not Found received for non-root path" + return 0 + else + print_error "Expected HTTP 404 for non-root path, got HTTP $http_code" + return 1 + fi +} + +test_websocket_still_works() { + print_step "Testing that WebSocket functionality still works on same port" + + if ! command -v websocat &> /dev/null; then + print_warning "websocat not available - skipping WebSocket test" + return 0 + fi + + # Try to connect to WebSocket and send a simple REQ + local response="" + response=$(echo '["REQ","test_ws_nip11",{}]' | timeout 3s websocat "$RELAY_WS_URL" 2>/dev/null || echo "Connection failed") + + if [[ "$response" == *"Connection failed"* ]]; then + print_error "WebSocket connection failed" + return 1 + elif [[ "$response" == *"EOSE"* ]]; then + print_success "WebSocket still functional - received EOSE response" + return 0 + else + print_warning "WebSocket response unclear, but connection succeeded" + return 0 + fi +} + +# Main test function +run_nip11_tests() { + print_header "NIP-11 Relay Information Document Tests" + + # Check dependencies + print_step "Checking dependencies..." + if ! command -v curl &> /dev/null; then + print_error "curl command not found - required for NIP-11 HTTP testing" + return 1 + fi + if ! command -v jq &> /dev/null; then + print_error "jq command not found - required for JSON validation" + return 1 + fi + print_success "All dependencies found" + + print_header "PHASE 1: Basic HTTP Functionality" + + # Test 1: Correct Accept header + if ! test_http_with_correct_header; then + return 1 + fi + + # Test 2: Missing Accept header + if ! test_http_without_header; then + return 1 + fi + + # Test 3: Wrong Accept header + if ! test_http_with_wrong_header; then + return 1 + fi + + print_header "PHASE 2: HTTP Headers Validation" + + # Test 4: CORS headers + if ! test_cors_headers; then + return 1 + fi + + # Test 5: Content-Type header + if ! test_content_type_header; then + return 1 + fi + + print_header "PHASE 3: JSON Structure Validation" + + # Test 6: JSON structure and required fields + if ! test_json_structure; then + return 1 + fi + + print_header "PHASE 4: Additional Endpoint Behavior" + + # Test 7: Non-root paths + if ! test_non_root_path; then + return 1 + fi + + # Test 8: WebSocket compatibility + if ! test_websocket_still_works; then + return 1 + fi + + print_header "PHASE 5: NIP-11 Compliance Summary" + + # Final validation - get the actual response and display it + print_step "Displaying complete NIP-11 response..." + local response="" + if command -v curl &> /dev/null; then + response=$(curl -s -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "") + if [[ -n "$response" ]] && echo "$response" | jq . >/dev/null 2>&1; then + echo "$response" | jq . + else + print_error "Failed to retrieve or parse final response" + fi + fi + + print_success "All NIP-11 tests passed!" + return 0 +} + +# Main execution +print_header "Starting NIP-11 Relay Information Document Test Suite" +echo + +if run_nip11_tests; then + echo + print_success "All NIP-11 tests completed successfully!" + print_info "The C-Relay NIP-11 implementation is fully compliant" + print_info "✅ HTTP endpoint, Accept header validation, CORS, and JSON structure all working" + echo + exit 0 +else + echo + print_error "Some NIP-11 tests failed" + exit 1 +fi \ No newline at end of file diff --git a/tests/13_nip_test.sh b/tests/13_nip_test.sh new file mode 100755 index 0000000..262394a --- /dev/null +++ b/tests/13_nip_test.sh @@ -0,0 +1,384 @@ +#!/bin/bash + +# NIP-13 Proof of Work Validation Test Suite for C Nostr Relay +# Tests PoW validation in the relay's event processing pipeline +# Based on nostr_core_lib/tests/nip13_test.c + +set -e # Exit on error + +# Color constants +RED='\033[31m' +GREEN='\033[32m' +YELLOW='\033[33m' +BLUE='\033[34m' +BOLD='\033[1m' +RESET='\033[0m' + +# Test configuration +RELAY_URL="ws://127.0.0.1:8888" +HTTP_URL="http://127.0.0.1:8888" +TEST_COUNT=0 +PASSED_COUNT=0 +FAILED_COUNT=0 + +# Test results tracking +declare -a TEST_RESULTS=() + +print_info() { + echo -e "${BLUE}[INFO]${RESET} $1" +} + +print_success() { + echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${RESET} $1" +} + +print_error() { + echo -e "${RED}${BOLD}[ERROR]${RESET} $1" +} + +print_test_header() { + TEST_COUNT=$((TEST_COUNT + 1)) + echo "" + echo -e "${BOLD}=== TEST $TEST_COUNT: $1 ===${RESET}" +} + +record_test_result() { + local test_name="$1" + local result="$2" + local details="$3" + + TEST_RESULTS+=("$test_name|$result|$details") + + if [ "$result" = "PASS" ]; then + PASSED_COUNT=$((PASSED_COUNT + 1)) + print_success "PASS: $test_name" + else + FAILED_COUNT=$((FAILED_COUNT + 1)) + print_error "FAIL: $test_name" + if [ -n "$details" ]; then + echo " Details: $details" + fi + fi +} + +# Check if relay is running +check_relay_running() { + print_info "Checking if relay is running..." + + if ! curl -s -H "Accept: application/nostr+json" "$HTTP_URL/" >/dev/null 2>&1; then + print_error "Relay is not running or not accessible at $HTTP_URL" + print_info "Please start the relay with: ./make_and_restart_relay.sh" + exit 1 + fi + + print_success "Relay is running and accessible" +} + +# Test NIP-11 relay information includes NIP-13 +test_nip11_pow_support() { + print_test_header "NIP-11 PoW Support Advertisement" + + print_info "Fetching relay information..." + RELAY_INFO=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL/") + + echo "Relay Info Response:" + echo "$RELAY_INFO" | jq '.' + echo "" + + # Check if NIP-13 is in supported_nips + if echo "$RELAY_INFO" | jq -e '.supported_nips | index(13)' >/dev/null 2>&1; then + print_success "✓ NIP-13 found in supported_nips array" + NIP13_SUPPORTED=true + else + print_error "✗ NIP-13 not found in supported_nips array" + NIP13_SUPPORTED=false + fi + + # Check if min_pow_difficulty is present + MIN_POW_DIFF=$(echo "$RELAY_INFO" | jq -r '.limitation.min_pow_difficulty // "missing"') + if [ "$MIN_POW_DIFF" != "missing" ]; then + print_success "✓ min_pow_difficulty found: $MIN_POW_DIFF" + MIN_POW_PRESENT=true + else + print_error "✗ min_pow_difficulty not found in limitations" + MIN_POW_PRESENT=false + fi + + if [ "$NIP13_SUPPORTED" = true ] && [ "$MIN_POW_PRESENT" = true ]; then + record_test_result "NIP-11 PoW Support Advertisement" "PASS" "NIP-13 supported, min_pow_difficulty=$MIN_POW_DIFF" + return 0 + else + record_test_result "NIP-11 PoW Support Advertisement" "FAIL" "Missing NIP-13 support or min_pow_difficulty" + return 1 + fi +} + +# Test event submission without PoW (should be accepted when min_difficulty=0) +test_event_without_pow() { + print_test_header "Event Submission Without PoW (min_difficulty=0)" + + # Create a simple event without PoW + print_info "Generating test event without PoW..." + + # Use nak to generate a simple event + if ! command -v nak &> /dev/null; then + print_warning "nak command not found - skipping PoW generation tests" + record_test_result "Event Submission Without PoW" "SKIP" "nak not available" + return 0 + fi + + # Generate event without PoW using direct private key + PRIVATE_KEY="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe" + EVENT_JSON=$(nak event --sec "$PRIVATE_KEY" -c "Test event without PoW" --ts $(date +%s)) + + print_info "Generated event:" + echo "$EVENT_JSON" | jq '.' + echo "" + + # Send event to relay via WebSocket using websocat + print_info "Sending event to relay..." + + # Create EVENT message in Nostr format + EVENT_MESSAGE="[\"EVENT\",$EVENT_JSON]" + + # Send to relay and capture response + if command -v websocat &> /dev/null; then + RESPONSE=$(echo "$EVENT_MESSAGE" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") + + print_info "Relay response: $RESPONSE" + + if [[ "$RESPONSE" == *"Connection failed"* ]]; then + print_error "✗ Failed to connect to relay" + record_test_result "Event Submission Without PoW" "FAIL" "Connection failed" + return 1 + elif [[ "$RESPONSE" == *"true"* ]]; then + print_success "✓ Event without PoW accepted (expected when min_difficulty=0)" + record_test_result "Event Submission Without PoW" "PASS" "Event accepted as expected" + return 0 + else + print_error "✗ Event without PoW rejected (unexpected when min_difficulty=0)" + record_test_result "Event Submission Without PoW" "FAIL" "Event rejected: $RESPONSE" + return 1 + fi + else + print_error "websocat not found - required for testing" + record_test_result "Event Submission Without PoW" "SKIP" "websocat not available" + return 0 + fi +} + +# Test event with valid PoW +test_event_with_pow() { + print_test_header "Event Submission With Valid PoW" + + if ! command -v nak &> /dev/null; then + print_warning "nak command not found - skipping PoW validation tests" + record_test_result "Event Submission With Valid PoW" "SKIP" "nak not available" + return 0 + fi + + print_info "Generating event with PoW difficulty 8..." + + # Generate event with PoW (difficulty 8 for reasonable test time) using direct private key + PRIVATE_KEY="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe" + POW_EVENT_JSON=$(nak event --sec "$PRIVATE_KEY" -c "Test event with PoW difficulty 8" --pow 8 --ts $(date +%s)) + + if [ -z "$POW_EVENT_JSON" ]; then + print_error "Failed to generate PoW event" + record_test_result "Event Submission With Valid PoW" "FAIL" "PoW event generation failed" + return 1 + fi + + print_info "Generated PoW event:" + echo "$POW_EVENT_JSON" | jq '.' + echo "" + + # Extract nonce info for verification + NONCE_TAG=$(echo "$POW_EVENT_JSON" | jq -r '.tags[] | select(.[0] == "nonce") | .[1]' 2>/dev/null || echo "") + TARGET_DIFF=$(echo "$POW_EVENT_JSON" | jq -r '.tags[] | select(.[0] == "nonce") | .[2]' 2>/dev/null || echo "") + + if [ -n "$NONCE_TAG" ] && [ -n "$TARGET_DIFF" ]; then + print_info "PoW details: nonce=$NONCE_TAG, target_difficulty=$TARGET_DIFF" + fi + + # Send event to relay via WebSocket using websocat + print_info "Sending PoW event to relay..." + + # Create EVENT message in Nostr format + POW_EVENT_MESSAGE="[\"EVENT\",$POW_EVENT_JSON]" + + # Send to relay and capture response + if command -v websocat &> /dev/null; then + RESPONSE=$(echo "$POW_EVENT_MESSAGE" | timeout 10s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") + + print_info "Relay response: $RESPONSE" + + if [[ "$RESPONSE" == *"Connection failed"* ]]; then + print_error "✗ Failed to connect to relay" + record_test_result "Event Submission With Valid PoW" "FAIL" "Connection failed" + return 1 + elif [[ "$RESPONSE" == *"true"* ]]; then + print_success "✓ Event with valid PoW accepted" + record_test_result "Event Submission With Valid PoW" "PASS" "PoW event accepted" + return 0 + else + print_error "✗ Event with valid PoW rejected" + record_test_result "Event Submission With Valid PoW" "FAIL" "PoW event rejected: $RESPONSE" + return 1 + fi + else + print_error "websocat not found - required for testing" + record_test_result "Event Submission With Valid PoW" "SKIP" "websocat not available" + return 0 + fi +} + +# Test relay configuration with environment variables +test_pow_configuration() { + print_test_header "PoW Configuration Via Environment Variables" + + print_info "Testing different PoW configurations requires relay restart" + print_info "Current configuration from logs:" + + if [ -f "relay.log" ]; then + grep "PoW Configuration:" relay.log | tail -1 + else + print_warning "No relay.log found" + fi + + # Test current configuration values + RELAY_INFO=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL/") + MIN_POW_DIFF=$(echo "$RELAY_INFO" | jq -r '.limitation.min_pow_difficulty') + + print_info "Current min_pow_difficulty from NIP-11: $MIN_POW_DIFF" + + # For now, just verify the configuration is readable + if [ "$MIN_POW_DIFF" != "null" ] && [ "$MIN_POW_DIFF" != "missing" ]; then + print_success "✓ PoW configuration is accessible via NIP-11" + record_test_result "PoW Configuration Via Environment Variables" "PASS" "min_pow_difficulty=$MIN_POW_DIFF" + return 0 + else + print_error "✗ PoW configuration not accessible" + record_test_result "PoW Configuration Via Environment Variables" "FAIL" "Cannot read min_pow_difficulty" + return 1 + fi +} + +# Test NIP-13 reference event validation +test_nip13_reference_event() { + print_test_header "NIP-13 Reference Event Validation" + + # This is the official NIP-13 reference event + NIP13_REF_EVENT='{"id":"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358","pubkey":"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243","created_at":1651794653,"kind":1,"tags":[["nonce","776797","20"]],"content":"It'\''s just me mining my own business","sig":"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977"}' + + print_info "Testing NIP-13 reference event from specification:" + echo "$NIP13_REF_EVENT" | jq '.' + echo "" + + # Send reference event to relay via WebSocket using websocat + print_info "Sending NIP-13 reference event to relay..." + + # Create EVENT message in Nostr format + REF_EVENT_MESSAGE="[\"EVENT\",$NIP13_REF_EVENT]" + + # Send to relay and capture response + if command -v websocat &> /dev/null; then + RESPONSE=$(echo "$REF_EVENT_MESSAGE" | timeout 10s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") + + print_info "Relay response: $RESPONSE" + + if [[ "$RESPONSE" == *"Connection failed"* ]] || [[ -z "$RESPONSE" ]]; then + print_error "✗ Failed to connect to relay or no response" + record_test_result "NIP-13 Reference Event Validation" "FAIL" "Connection failed or timeout" + return 1 + elif [[ "$RESPONSE" == *"true"* ]]; then + print_success "✓ NIP-13 reference event accepted" + record_test_result "NIP-13 Reference Event Validation" "PASS" "Reference event accepted" + return 0 + else + print_error "✗ NIP-13 reference event rejected" + record_test_result "NIP-13 Reference Event Validation" "FAIL" "Reference event rejected: $RESPONSE" + return 1 + fi + else + print_error "websocat not found - required for testing" + record_test_result "NIP-13 Reference Event Validation" "SKIP" "websocat not available" + return 0 + fi +} + +# Print test summary +print_test_summary() { + echo "" + echo -e "${BOLD}=== TEST SUMMARY ===${RESET}" + echo "Total tests run: $TEST_COUNT" + echo -e "${GREEN}Passed: $PASSED_COUNT${RESET}" + echo -e "${RED}Failed: $FAILED_COUNT${RESET}" + + if [ $FAILED_COUNT -gt 0 ]; then + echo "" + echo -e "${RED}${BOLD}Failed tests:${RESET}" + for result in "${TEST_RESULTS[@]}"; do + IFS='|' read -r name status details <<< "$result" + if [ "$status" = "FAIL" ]; then + echo -e " ${RED}✗ $name${RESET}" + if [ -n "$details" ]; then + echo " $details" + fi + fi + done + fi + + echo "" + if [ $FAILED_COUNT -eq 0 ]; then + echo -e "${GREEN}${BOLD}🎉 ALL TESTS PASSED!${RESET}" + echo -e "${GREEN}✅ NIP-13 PoW validation is working correctly in the relay${RESET}" + return 0 + else + echo -e "${RED}${BOLD}❌ SOME TESTS FAILED${RESET}" + echo "Please review the output above and check relay logs for more details." + return 1 + fi +} + +# Main test execution +main() { + echo -e "${BOLD}=== NIP-13 Proof of Work Relay Test Suite ===${RESET}" + echo "Testing NIP-13 PoW validation in the C Nostr Relay" + echo "Relay URL: $RELAY_URL" + echo "" + + # Check prerequisites + if ! command -v curl &> /dev/null; then + print_error "curl is required but not installed" + exit 1 + fi + + if ! command -v jq &> /dev/null; then + print_error "jq is required but not installed" + exit 1 + fi + + if ! command -v websocat &> /dev/null; then + print_warning "websocat not found - WebSocket tests will be skipped" + fi + + # Run tests + check_relay_running + test_nip11_pow_support + test_event_without_pow + test_event_with_pow + test_pow_configuration + test_nip13_reference_event + + # Print summary + print_test_summary + exit $? +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/tests/9_delete_test.sh b/tests/9_nip_delete_test.sh similarity index 100% rename from tests/9_delete_test.sh rename to tests/9_nip_delete_test.sh