diff --git a/debug.log b/debug.log index 243d98ba..f6bea001 100644 --- a/debug.log +++ b/debug.log @@ -29,3 +29,25 @@ [08:50:18.550] RECV nostr.mom:443: ["EVENT","sync_2_1755348617",{"content":"😂\nStay humble and stack zaps ⚡️ ","created_at":1755348616,"id":"974af84ac7c4041ccf44741adaeffb911aad9d5ed13543496a1ce2b519dc6fdf","kind":1,"pubkey":"3824552ea18ce24fae867d292514c40e0d4d1c39e18e752e852a51e4a0b2d7c8","sig":"04acf08ee2b2218604bc0e663f52ad6f0a52109674457e232c940e3bb91df971cc7baf33248ced999b1e294dd8de68c2e0ca5f586ed7bddecc0a88ddc58da781","tags":[["e","253a2c3eda166a82e8f42f0018c96845df080e71cf7c30ad7107e5936b6892dd","","root"],["p","7c765d407d3a9d5ea117cb8b8699628560787fc084a0c76afaa449bfbd121d84"]]}] [08:50:18.560] RECV nostr.mom:443: ["EOSE","sync_2_1755348617"] [08:50:18.560] SEND nostr.mom:443: ["CLOSE", "sync_2_1755348617"] +PoW mining: 0 attempts, best this round: 0, overall best: 0, goal: 8 +PoW SUCCESS: Found difficulty 9 (target 8) at nonce 758 after 759 attempts +Final event: { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1757093676, + "kind": 1, + "tags": [["nonce", "758", "8"]], + "content": "Testing PoW generation", + "id": "006e0b830d970626fb0d3c6eb395f29dcc729b7d671f8e6d85f48ccc8dfcccb1", + "sig": "ee3106e2b7dac582fd4ab4d851091ef566e9e4f4396c9a8976a6099cc34122e90378fe83d08d67a381e328d5e51ba6910fb451cb6c8234c85ab4ceac59830000" +} +PoW mining: 0 attempts, best this round: 0, overall best: 0, goal: 8 +PoW SUCCESS: Found difficulty 10 (target 8) at nonce 25 after 26 attempts +Final event: { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1757093771, + "kind": 1, + "tags": [["nonce", "25", "8"]], + "content": "Testing PoW generation", + "id": "0034dad20205876be164b768ba5efad8aaa23bdf16b273544785cae9f13a76f7", + "sig": "e1ef97bf916f9e1fd94e231e62833e6ff000be29a92e16fc3656142e51abbf4ee8795ab4100f448e6712c1a76bb128016c24306b165077188fa8e5b82c72f23f" +} diff --git a/nostr_core/nip013.c b/nostr_core/nip013.c index bc85e8f9..e8fafd65 100644 --- a/nostr_core/nip013.c +++ b/nostr_core/nip013.c @@ -277,3 +277,332 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key, // If we reach here, we've exceeded max attempts return NOSTR_ERROR_CRYPTO_FAILED; } + +/** + * Calculate PoW difficulty (leading zero bits) for an event ID + * + * @param event_id_hex Hexadecimal event ID string (64 characters) + * @return Number of leading zero bits, or negative error code on failure + */ +int nostr_calculate_pow_difficulty(const char* event_id_hex) { + if (!event_id_hex) { + return NOSTR_ERROR_INVALID_INPUT; + } + + // Validate hex string length (should be 64 characters for SHA-256) + size_t hex_len = strlen(event_id_hex); + if (hex_len != 64) { + return NOSTR_ERROR_NIP13_CALCULATION; + } + + // Convert hex to bytes + unsigned char hash[32]; + if (nostr_hex_to_bytes(event_id_hex, hash, 32) != NOSTR_SUCCESS) { + return NOSTR_ERROR_NIP13_CALCULATION; + } + + // Use existing NIP-13 reference implementation + return count_leading_zero_bits(hash); +} + +/** + * Extract nonce information from event tags + * + * @param event Complete event JSON object + * @param nonce_out Output pointer for nonce value (can be NULL) + * @param target_difficulty_out Output pointer for target difficulty (can be NULL) + * @return NOSTR_SUCCESS if nonce tag found, NOSTR_ERROR_NIP13_NO_NONCE_TAG if not found, other error codes on failure + */ +int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out) { + if (!event) { + return NOSTR_ERROR_INVALID_INPUT; + } + + // Initialize output values + if (nonce_out) *nonce_out = 0; + if (target_difficulty_out) *target_difficulty_out = -1; + + // Get tags array + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + if (!tags || !cJSON_IsArray(tags)) { + return NOSTR_ERROR_NIP13_NO_NONCE_TAG; + } + + // Search for nonce tag + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + + // Check if this is a nonce tag + cJSON* tag_type = cJSON_GetArrayItem(tag, 0); + if (!tag_type || !cJSON_IsString(tag_type)) { + continue; + } + + const char* tag_name = cJSON_GetStringValue(tag_type); + if (!tag_name || strcmp(tag_name, "nonce") != 0) { + continue; + } + + // Extract nonce value (second element) + cJSON* nonce_item = cJSON_GetArrayItem(tag, 1); + if (!nonce_item || !cJSON_IsString(nonce_item)) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + const char* nonce_str = cJSON_GetStringValue(nonce_item); + if (!nonce_str) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + // Parse nonce value + char* endptr; + uint64_t nonce_val = strtoull(nonce_str, &endptr, 10); + if (*endptr != '\0') { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + if (nonce_out) *nonce_out = nonce_val; + + // Extract target difficulty (third element, optional) + if (cJSON_GetArraySize(tag) >= 3) { + cJSON* target_item = cJSON_GetArrayItem(tag, 2); + if (target_item && cJSON_IsString(target_item)) { + const char* target_str = cJSON_GetStringValue(target_item); + if (target_str) { + char* endptr2; + long target_val = strtol(target_str, &endptr2, 10); + if (*endptr2 == '\0' && target_val >= 0) { + if (target_difficulty_out) *target_difficulty_out = (int)target_val; + } + } + } + } + + return NOSTR_SUCCESS; + } + + // No nonce tag found + return NOSTR_ERROR_NIP13_NO_NONCE_TAG; +} + +/** + * Validate just the nonce tag format (without PoW calculation) + * + * @param nonce_tag_array JSON array representing a nonce tag + * @return NOSTR_SUCCESS if valid format, error code otherwise + */ +int nostr_validate_nonce_tag(cJSON* nonce_tag_array) { + if (!nonce_tag_array || !cJSON_IsArray(nonce_tag_array)) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + int array_size = cJSON_GetArraySize(nonce_tag_array); + if (array_size < 2) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + // First element should be "nonce" + cJSON* tag_type = cJSON_GetArrayItem(nonce_tag_array, 0); + if (!tag_type || !cJSON_IsString(tag_type)) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + const char* tag_name = cJSON_GetStringValue(tag_type); + if (!tag_name || strcmp(tag_name, "nonce") != 0) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + // Second element should be a valid nonce (numeric string) + cJSON* nonce_item = cJSON_GetArrayItem(nonce_tag_array, 1); + if (!nonce_item || !cJSON_IsString(nonce_item)) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + const char* nonce_str = cJSON_GetStringValue(nonce_item); + if (!nonce_str) { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + // Validate nonce is a valid number + char* endptr; + strtoull(nonce_str, &endptr, 10); + if (*endptr != '\0') { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + // Third element (target difficulty) is optional, but if present should be valid + if (array_size >= 3) { + cJSON* target_item = cJSON_GetArrayItem(nonce_tag_array, 2); + if (target_item && cJSON_IsString(target_item)) { + const char* target_str = cJSON_GetStringValue(target_item); + if (target_str) { + char* endptr2; + strtol(target_str, &endptr2, 10); + if (*endptr2 != '\0') { + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + } + } + } + + return NOSTR_SUCCESS; +} + +/** + * Validate Proof of Work for an event according to NIP-13 + * + * @param event Complete event JSON object to validate + * @param min_difficulty Minimum difficulty required by the relay (0 = no requirement) + * @param validation_flags Bitflags for validation options + * @param result_info Optional output struct for detailed results (can be NULL) + * @return NOSTR_SUCCESS if PoW is valid and meets requirements, error code otherwise + */ +int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags, + nostr_pow_result_t* result_info) { + if (!event) { + return NOSTR_ERROR_INVALID_INPUT; + } + + // Initialize result info if provided + if (result_info) { + result_info->actual_difficulty = 0; + result_info->committed_target = -1; + result_info->nonce_value = 0; + result_info->has_nonce_tag = 0; + result_info->error_detail[0] = '\0'; + } + + // Get event ID for PoW calculation + cJSON* id_item = cJSON_GetObjectItem(event, "id"); + if (!id_item || !cJSON_IsString(id_item)) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Missing or invalid event ID"); + } + return NOSTR_ERROR_EVENT_INVALID_ID; + } + + const char* event_id = cJSON_GetStringValue(id_item); + if (!event_id) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Event ID is not a string"); + } + return NOSTR_ERROR_EVENT_INVALID_ID; + } + + // Calculate actual PoW difficulty + int actual_difficulty = nostr_calculate_pow_difficulty(event_id); + if (actual_difficulty < 0) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Failed to calculate PoW difficulty"); + } + return actual_difficulty; // Return the specific error from calculation + } + + if (result_info) { + result_info->actual_difficulty = actual_difficulty; + } + + // Check if minimum difficulty requirement is met + if (min_difficulty > 0 && actual_difficulty < min_difficulty) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Insufficient difficulty: %d < %d", actual_difficulty, min_difficulty); + } + return NOSTR_ERROR_NIP13_INSUFFICIENT; + } + + // Extract nonce information if validation flags require it + uint64_t nonce_value = 0; + int committed_target = -1; + int nonce_extract_result = nostr_extract_nonce_info(event, &nonce_value, &committed_target); + + if (result_info) { + result_info->nonce_value = nonce_value; + result_info->committed_target = committed_target; + result_info->has_nonce_tag = (nonce_extract_result == NOSTR_SUCCESS) ? 1 : 0; + } + + // Validate nonce tag presence if required + if (validation_flags & NOSTR_POW_VALIDATE_NONCE_TAG) { + if (nonce_extract_result == NOSTR_ERROR_NIP13_NO_NONCE_TAG) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Missing required nonce tag"); + } + return NOSTR_ERROR_NIP13_NO_NONCE_TAG; + } else if (nonce_extract_result != NOSTR_SUCCESS) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Invalid nonce tag format"); + } + return nonce_extract_result; + } + + // If strict format validation is enabled, validate the nonce tag structure + if (validation_flags & NOSTR_POW_STRICT_FORMAT) { + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + if (tags && cJSON_IsArray(tags)) { + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { + cJSON* tag_type = cJSON_GetArrayItem(tag, 0); + if (tag_type && cJSON_IsString(tag_type)) { + const char* tag_name = cJSON_GetStringValue(tag_type); + if (tag_name && strcmp(tag_name, "nonce") == 0) { + int validation_result = nostr_validate_nonce_tag(tag); + if (validation_result != NOSTR_SUCCESS) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Nonce tag failed strict format validation"); + } + return validation_result; + } + break; + } + } + } + } + } + } + } + + // Validate committed target difficulty if required + if (validation_flags & NOSTR_POW_VALIDATE_TARGET_COMMIT) { + if (nonce_extract_result == NOSTR_SUCCESS && committed_target == -1) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Missing committed target difficulty in nonce tag"); + } + return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG; + } + + // Check for target/difficulty mismatch if rejecting lower targets + if (validation_flags & NOSTR_POW_REJECT_LOWER_TARGET) { + // According to NIP-13: "if you require 40 bits to reply to your thread and see + // a committed target of 30, you can safely reject it even if the note has 40 bits difficulty" + // This means we reject if committed_target < min_difficulty, not actual_difficulty + if (committed_target != -1 && min_difficulty > 0 && committed_target < min_difficulty) { + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "Committed target (%d) is lower than required minimum (%d)", + committed_target, min_difficulty); + } + return NOSTR_ERROR_NIP13_TARGET_MISMATCH; + } + } + } + + // All validations passed + if (result_info) { + snprintf(result_info->error_detail, sizeof(result_info->error_detail), + "PoW validation successful"); + } + + return NOSTR_SUCCESS; +} diff --git a/nostr_core/nip013.h b/nostr_core/nip013.h index 2a36e741..34265cc0 100644 --- a/nostr_core/nip013.h +++ b/nostr_core/nip013.h @@ -8,11 +8,41 @@ #include "nip001.h" #include +// PoW validation flags +#define NOSTR_POW_VALIDATE_NONCE_TAG 0x01 // Require and validate nonce tag format +#define NOSTR_POW_VALIDATE_TARGET_COMMIT 0x02 // Validate committed target difficulty +#define NOSTR_POW_REJECT_LOWER_TARGET 0x04 // Reject if committed target < actual difficulty +#define NOSTR_POW_STRICT_FORMAT 0x08 // Strict nonce tag format validation + +// Convenience combinations +#define NOSTR_POW_VALIDATE_BASIC (NOSTR_POW_VALIDATE_NONCE_TAG) +#define NOSTR_POW_VALIDATE_FULL (NOSTR_POW_VALIDATE_NONCE_TAG | NOSTR_POW_VALIDATE_TARGET_COMMIT) +#define NOSTR_POW_VALIDATE_ANTI_SPAM (NOSTR_POW_VALIDATE_FULL | NOSTR_POW_REJECT_LOWER_TARGET) + +// Result information structure (optional output) +typedef struct { + int actual_difficulty; // Calculated difficulty (leading zero bits) + int committed_target; // Target difficulty from nonce tag (-1 if none) + uint64_t nonce_value; // Nonce value from tag (0 if none) + int has_nonce_tag; // 1 if nonce tag present, 0 otherwise + char error_detail[256]; // Detailed error description +} nostr_pow_result_t; + // Function declarations -int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key, +int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key, int target_difficulty, int max_attempts, int progress_report_interval, int timestamp_update_interval, void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data), void* user_data); +// PoW validation functions +int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags, + nostr_pow_result_t* result_info); + +int nostr_calculate_pow_difficulty(const char* event_id_hex); + +int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out); + +int nostr_validate_nonce_tag(cJSON* nonce_tag_array); + #endif // NIP013_H diff --git a/nostr_core/nostr_common.c b/nostr_core/nostr_common.c index b68bc837..8df544a6 100644 --- a/nostr_core/nostr_common.c +++ b/nostr_core/nostr_common.c @@ -38,6 +38,11 @@ const char* nostr_strerror(int error_code) { case NOSTR_ERROR_EVENT_INVALID_KIND: return "Event has invalid kind"; case NOSTR_ERROR_EVENT_INVALID_TAGS: return "Event has invalid tags"; case NOSTR_ERROR_EVENT_INVALID_CONTENT: return "Event has invalid content"; + case NOSTR_ERROR_NIP13_INSUFFICIENT: return "NIP-13: Insufficient PoW difficulty"; + case NOSTR_ERROR_NIP13_NO_NONCE_TAG: return "NIP-13: Missing nonce tag"; + case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: return "NIP-13: Invalid nonce tag format"; + case NOSTR_ERROR_NIP13_TARGET_MISMATCH: return "NIP-13: Target difficulty mismatch"; + case NOSTR_ERROR_NIP13_CALCULATION: return "NIP-13: PoW calculation error"; default: return "Unknown error"; } } diff --git a/nostr_core/nostr_common.h b/nostr_core/nostr_common.h index 0919ca1f..f6eb37da 100644 --- a/nostr_core/nostr_common.h +++ b/nostr_core/nostr_common.h @@ -36,6 +36,13 @@ #define NOSTR_ERROR_EVENT_INVALID_TAGS -36 #define NOSTR_ERROR_EVENT_INVALID_CONTENT -37 +// NIP-13 PoW-specific error codes +#define NOSTR_ERROR_NIP13_INSUFFICIENT -100 +#define NOSTR_ERROR_NIP13_NO_NONCE_TAG -101 +#define NOSTR_ERROR_NIP13_INVALID_NONCE_TAG -102 +#define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103 +#define NOSTR_ERROR_NIP13_CALCULATION -104 + // Constants #define NOSTR_PRIVATE_KEY_SIZE 32 diff --git a/tests/bip32_test b/tests/bip32_test index 1262f728..9e225174 100755 Binary files a/tests/bip32_test and b/tests/bip32_test differ diff --git a/tests/nip01_test b/tests/nip01_test index 8fac1380..ca7c9dcc 100755 Binary files a/tests/nip01_test and b/tests/nip01_test differ diff --git a/tests/nip04_test b/tests/nip04_test index fb080ebb..8ddc4af0 100755 Binary files a/tests/nip04_test and b/tests/nip04_test differ diff --git a/tests/nip05_test b/tests/nip05_test index d8d77dd8..32e585ae 100755 Binary files a/tests/nip05_test and b/tests/nip05_test differ diff --git a/tests/nip11_test b/tests/nip11_test index ef95320f..8a4c77df 100755 Binary files a/tests/nip11_test and b/tests/nip11_test differ diff --git a/tests/nip13_test b/tests/nip13_test new file mode 100755 index 00000000..2b4302e9 Binary files /dev/null and b/tests/nip13_test differ diff --git a/tests/nip13_test.c b/tests/nip13_test.c new file mode 100644 index 00000000..44041171 --- /dev/null +++ b/tests/nip13_test.c @@ -0,0 +1,491 @@ +/* + * NIP-13 Proof of Work Test Suite + * Tests PoW generation, difficulty calculation, nonce extraction, and validation + * Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events + */ + +#define _GNU_SOURCE // For strdup on Linux +#include +#include +#include +#include +#include +#include "../nostr_core/nip013.h" +#include "../nostr_core/nip001.h" +#include "../nostr_core/nostr_common.h" +#include "../nostr_core/utils.h" +#include "../cjson/cJSON.h" + +// Ensure strdup is declared +#ifndef strdup +extern char *strdup(const char *s); +#endif + +// Test counter for tracking progress +static int test_count = 0; +static int passed_tests = 0; + +void print_test_header(const char* test_name) { + test_count++; + printf("\n=== TEST %d: %s ===\n", test_count, test_name); +} + +void print_test_result(int passed, const char* test_name) { + if (passed) { + passed_tests++; + printf("✅ PASS: %s\n", test_name); + } else { + printf("❌ FAIL: %s\n", test_name); + } +} + +// Test 1: Difficulty calculation with NIP-13 example +int test_difficulty_calculation_reference(void) { + print_test_header("Difficulty Calculation - NIP-13 Reference"); + + // Event ID from NIP-13 spec: 000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358 + // This should have ~20 leading zero bits as stated in the spec + const char* event_id = "000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358"; + + printf("Testing event ID: %s\n", event_id); + printf("Expected: ~20 leading zero bits\n"); + + int difficulty = nostr_calculate_pow_difficulty(event_id); + printf("Actual: %d leading zero bits\n", difficulty); + + if (difficulty < 0) { + printf("❌ Error calculating difficulty: %d (%s)\n", difficulty, nostr_strerror(difficulty)); + return 0; + } + + // The NIP-13 spec example should have around 20 bits + if (difficulty >= 19 && difficulty <= 21) { + printf("✅ Difficulty calculation matches expected range (19-21 bits)\n"); + return 1; + } else { + printf("❌ Difficulty %d is outside expected range (19-21 bits)\n", difficulty); + return 0; + } +} + +// Test 2: Extract nonce info from NIP-13 reference event +int test_nonce_extraction_reference(void) { + print_test_header("Nonce Extraction - NIP-13 Reference Event"); + + // Complete event from NIP-13 spec + const char* event_json = "{" + "\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\"," + "\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\"," + "\"created_at\":1651794653," + "\"kind\":1," + "\"tags\":[[\"nonce\",\"776797\",\"20\"]]," + "\"content\":\"It's just me mining my own business\"," + "\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\"" + "}"; + + printf("Input Event JSON:\n%s\n\n", event_json); + + cJSON* event = cJSON_Parse(event_json); + if (!event) { + printf("❌ JSON Parse Error\n"); + return 0; + } + + uint64_t nonce_value = 0; + int target_difficulty = -1; + int result = nostr_extract_nonce_info(event, &nonce_value, &target_difficulty); + + printf("Extraction result: %d (%s)\n", result, nostr_strerror(result)); + printf("Expected nonce: 776797\n"); + printf("Actual nonce: %llu\n", (unsigned long long)nonce_value); + printf("Expected target: 20\n"); + printf("Actual target: %d\n", target_difficulty); + + cJSON_Delete(event); + + if (result != NOSTR_SUCCESS) { + printf("❌ Failed to extract nonce info\n"); + return 0; + } + + if (nonce_value == 776797 && target_difficulty == 20) { + printf("✅ Nonce extraction successful\n"); + return 1; + } else { + printf("❌ Extracted values don't match expected\n"); + return 0; + } +} + +// Test 3: PoW validation with reference event +int test_pow_validation_reference(void) { + print_test_header("PoW Validation - NIP-13 Reference Event"); + + const char* event_json = "{" + "\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\"," + "\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\"," + "\"created_at\":1651794653," + "\"kind\":1," + "\"tags\":[[\"nonce\",\"776797\",\"20\"]]," + "\"content\":\"It's just me mining my own business\"," + "\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\"" + "}"; + + printf("Input Event JSON:\n%s\n\n", event_json); + + cJSON* event = cJSON_Parse(event_json); + if (!event) { + printf("❌ JSON Parse Error\n"); + return 0; + } + + // Test basic validation (no minimum difficulty) + nostr_pow_result_t result_info; + int validation_result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_BASIC, &result_info); + + printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result)); + printf("Actual difficulty: %d\n", result_info.actual_difficulty); + printf("Committed target: %d\n", result_info.committed_target); + printf("Nonce value: %llu\n", (unsigned long long)result_info.nonce_value); + printf("Has nonce tag: %d\n", result_info.has_nonce_tag); + printf("Error detail: %s\n", result_info.error_detail); + + cJSON_Delete(event); + return (validation_result == NOSTR_SUCCESS && result_info.has_nonce_tag == 1); +} + +// Test 4: Generate PoW with our library and validate it +int test_pow_generation_and_validation(void) { + print_test_header("PoW Generation and Validation - Our Library"); + + // Create a test event for mining + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + unsigned char private_key[32]; + nostr_hex_to_bytes(private_key_hex, private_key, 32); + + cJSON* tags = cJSON_CreateArray(); + + printf("Creating base event...\n"); + cJSON* event = nostr_create_and_sign_event(1, "Testing PoW generation", tags, private_key, time(NULL)); + + if (!event) { + printf("❌ Event creation failed\n"); + cJSON_Delete(tags); + return 0; + } + + char* original_event_str = cJSON_Print(event); + printf("Original event (no PoW):\n%s\n\n", original_event_str); + free(original_event_str); + + // Add PoW with difficulty 8 (reasonable for testing) + printf("Adding PoW with difficulty 8...\n"); + int pow_result = nostr_add_proof_of_work(event, private_key, 8, 100000, 10000, 10000, NULL, NULL); + + if (pow_result != NOSTR_SUCCESS) { + printf("❌ PoW generation failed: %d (%s)\n", pow_result, nostr_strerror(pow_result)); + cJSON_Delete(event); + return 0; + } + + char* pow_event_str = cJSON_Print(event); + printf("Event with PoW:\n%s\n\n", pow_event_str); + free(pow_event_str); + + // Now validate the PoW + printf("Validating generated PoW...\n"); + nostr_pow_result_t result_info; + int validation_result = nostr_validate_pow(event, 8, NOSTR_POW_VALIDATE_FULL, &result_info); + + printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result)); + printf("Actual difficulty: %d\n", result_info.actual_difficulty); + printf("Committed target: %d\n", result_info.committed_target); + printf("Has nonce tag: %d\n", result_info.has_nonce_tag); + printf("Error detail: %s\n", result_info.error_detail); + + cJSON_Delete(event); + + if (validation_result == NOSTR_SUCCESS && result_info.actual_difficulty >= 8) { + printf("✅ PoW generation and validation successful\n"); + return 1; + } else { + printf("❌ PoW validation failed\n"); + return 0; + } +} + +// Test 5: Test various validation flag combinations +int test_validation_flags(void) { + print_test_header("Validation Flags - Various Combinations"); + + // Use NIP-13 reference event + const char* event_json = "{" + "\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\"," + "\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\"," + "\"created_at\":1651794653," + "\"kind\":1," + "\"tags\":[[\"nonce\",\"776797\",\"20\"]]," + "\"content\":\"It's just me mining my own business\"," + "\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\"" + "}"; + + cJSON* event = cJSON_Parse(event_json); + if (!event) { + printf("❌ JSON Parse Error\n"); + return 0; + } + + int all_passed = 1; + nostr_pow_result_t result_info; + + // Test 1: Basic validation + printf("\nSubtest 1: NOSTR_POW_VALIDATE_BASIC\n"); + int result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_BASIC, &result_info); + printf("Result: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_SUCCESS) all_passed = 0; + + // Test 2: Full validation + printf("\nSubtest 2: NOSTR_POW_VALIDATE_FULL\n"); + result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_FULL, &result_info); + printf("Result: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_SUCCESS) all_passed = 0; + + // Test 3: Anti-spam validation (should pass since committed target matches actual) + printf("\nSubtest 3: NOSTR_POW_VALIDATE_ANTI_SPAM\n"); + result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_ANTI_SPAM, &result_info); + printf("Result: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_SUCCESS) all_passed = 0; + + // Test 4: Minimum difficulty requirement (15 bits - should pass) + printf("\nSubtest 4: Minimum difficulty 15 bits\n"); + result = nostr_validate_pow(event, 15, NOSTR_POW_VALIDATE_BASIC, &result_info); + printf("Result: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_SUCCESS) all_passed = 0; + + // Test 5: Too high minimum difficulty (30 bits - should fail) + printf("\nSubtest 5: Minimum difficulty 30 bits (should fail)\n"); + result = nostr_validate_pow(event, 30, NOSTR_POW_VALIDATE_BASIC, &result_info); + printf("Result: %d (%s)\n", result, nostr_strerror(result)); + if (result == NOSTR_SUCCESS) { + printf("❌ Should have failed but didn't\n"); + all_passed = 0; + } else if (result == NOSTR_ERROR_NIP13_INSUFFICIENT) { + printf("✅ Correctly failed with insufficient difficulty\n"); + } else { + printf("❌ Failed with unexpected error\n"); + all_passed = 0; + } + + cJSON_Delete(event); + return all_passed; +} + +// Test 6: Error conditions and edge cases +int test_error_conditions(void) { + print_test_header("Error Conditions and Edge Cases"); + + int all_passed = 1; + + // Test 1: NULL event + printf("\nSubtest 1: NULL event\n"); + int result = nostr_validate_pow(NULL, 0, NOSTR_POW_VALIDATE_BASIC, NULL); + printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_INVALID_INPUT) all_passed = 0; + + // Test 2: Event without ID + printf("\nSubtest 2: Event without ID\n"); + const char* no_id_json = "{\"kind\":1,\"content\":\"test\",\"tags\":[]}"; + cJSON* no_id_event = cJSON_Parse(no_id_json); + result = nostr_validate_pow(no_id_event, 0, NOSTR_POW_VALIDATE_BASIC, NULL); + printf("Expected: NOSTR_ERROR_EVENT_INVALID_ID (-31)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_EVENT_INVALID_ID) all_passed = 0; + cJSON_Delete(no_id_event); + + // Test 3: Event without nonce tag when required + printf("\nSubtest 3: Event without nonce tag when required\n"); + const char* no_nonce_json = "{" + "\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\"," + "\"kind\":1,\"content\":\"test\",\"tags\":[]" + "}"; + cJSON* no_nonce_event = cJSON_Parse(no_nonce_json); + result = nostr_validate_pow(no_nonce_event, 0, NOSTR_POW_VALIDATE_NONCE_TAG, NULL); + printf("Expected: NOSTR_ERROR_NIP13_NO_NONCE_TAG (-101)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_NIP13_NO_NONCE_TAG) all_passed = 0; + cJSON_Delete(no_nonce_event); + + // Test 4: Invalid hex ID + printf("\nSubtest 4: Invalid hex ID\n"); + result = nostr_calculate_pow_difficulty("invalid_hex_string"); + printf("Expected: NOSTR_ERROR_NIP13_CALCULATION (-104)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_NIP13_CALCULATION) all_passed = 0; + + // Test 5: Wrong length hex ID + printf("\nSubtest 5: Wrong length hex ID\n"); + result = nostr_calculate_pow_difficulty("f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffa"); // 63 chars instead of 64 + printf("Expected: NOSTR_ERROR_NIP13_CALCULATION (-104)\n"); + printf("Actual: %d (%s)\n", result, nostr_strerror(result)); + if (result != NOSTR_ERROR_NIP13_CALCULATION) all_passed = 0; + + return all_passed; +} + +// Test 7: Integration with nak-generated PoW events +int test_nak_integration(void) { + print_test_header("Integration with nak-generated PoW events"); + + // Generate a test event with nak and PoW difficulty 8 + printf("Generating PoW event with nak (difficulty 8)...\n"); + + // Create a temporary key for this test + char temp_key[] = "/tmp/nip13_test_key_XXXXXX"; + int fd = mkstemp(temp_key); + if (fd == -1) { + printf("❌ Failed to create temporary key file\n"); + return 0; + } + + // Write test key to file (same as used in other tests) + const char* test_key = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + ssize_t bytes_written = write(fd, test_key, strlen(test_key)); + close(fd); + + if (bytes_written != (ssize_t)strlen(test_key)) { + printf("❌ Failed to write test key to temporary file\n"); + unlink(temp_key); + return 0; + } + + // Generate event with PoW using nak + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "nak event --sec %s -c \"PoW test from nak\" --pow 8 --ts %ld", + test_key, (long)time(NULL)); + + printf("Executing: %s\n", cmd); + FILE* pipe = popen(cmd, "r"); + if (!pipe) { + printf("❌ Failed to execute nak command\n"); + unlink(temp_key); + return 0; + } + + char event_json[4096] = {0}; + if (!fgets(event_json, sizeof(event_json), pipe)) { + printf("❌ Failed to read nak output\n"); + pclose(pipe); + unlink(temp_key); + return 0; + } + pclose(pipe); + unlink(temp_key); + + // Remove trailing newline + event_json[strcspn(event_json, "\n")] = 0; + + printf("nak-generated event:\n%s\n\n", event_json); + + // Parse and validate the event + cJSON* event = cJSON_Parse(event_json); + if (!event) { + printf("❌ Failed to parse nak-generated JSON\n"); + return 0; + } + + // Validate the PoW + nostr_pow_result_t result_info; + int validation_result = nostr_validate_pow(event, 8, NOSTR_POW_VALIDATE_FULL, &result_info); + + printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result)); + printf("Actual difficulty: %d\n", result_info.actual_difficulty); + printf("Committed target: %d\n", result_info.committed_target); + printf("Has nonce tag: %d\n", result_info.has_nonce_tag); + printf("Error detail: %s\n", result_info.error_detail); + + cJSON_Delete(event); + + if (validation_result == NOSTR_SUCCESS && result_info.actual_difficulty >= 8) { + printf("✅ nak-generated PoW event validates successfully\n"); + return 1; + } else { + printf("❌ nak-generated PoW event validation failed\n"); + return 0; + } +} + +int main(void) { + printf("=== NIP-13 Proof of Work Test Suite ===\n"); + printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\n"); + printf("Tests both PoW generation and validation functionality\n"); + + // Initialize crypto library + if (nostr_init() != NOSTR_SUCCESS) { + printf("❌ Failed to initialize nostr library\n"); + return 1; + } + + int all_passed = 1; + int test_result; + + // Test 1: Basic difficulty calculation + test_result = test_difficulty_calculation_reference(); + print_test_result(test_result, "Difficulty Calculation - NIP-13 Reference"); + if (!test_result) all_passed = 0; + + // Test 2: Nonce extraction + test_result = test_nonce_extraction_reference(); + print_test_result(test_result, "Nonce Extraction - NIP-13 Reference Event"); + if (!test_result) all_passed = 0; + + // Test 3: Basic PoW validation + test_result = test_pow_validation_reference(); + print_test_result(test_result, "PoW Validation - NIP-13 Reference Event"); + if (!test_result) all_passed = 0; + + // Test 4: PoW generation and validation + test_result = test_pow_generation_and_validation(); + print_test_result(test_result, "PoW Generation and Validation - Our Library"); + if (!test_result) all_passed = 0; + + // Test 5: Validation flags + test_result = test_validation_flags(); + print_test_result(test_result, "Validation Flags - Various Combinations"); + if (!test_result) all_passed = 0; + + // Test 6: Error conditions + test_result = test_error_conditions(); + print_test_result(test_result, "Error Conditions and Edge Cases"); + if (!test_result) all_passed = 0; + + // Test 7: nak integration (optional - may fail if nak not available) + printf("\n=== Optional nak Integration Test ===\n"); + test_result = test_nak_integration(); + if (test_result) { + print_test_result(test_result, "Integration with nak-generated PoW events"); + } else { + printf("⚠️ SKIP: Integration with nak-generated PoW events (nak may not be available)\n"); + // Don't fail the entire test suite for this optional test + } + + // Summary + printf("\n=== TEST SUMMARY ===\n"); + printf("Total tests: %d\n", test_count); + printf("Passed: %d\n", passed_tests); + printf("Failed: %d\n", test_count - passed_tests); + + if (all_passed) { + printf("🎉 ALL TESTS PASSED! NIP-13 PoW implementation is working correctly.\n"); + printf("✅ PoW generation works\n"); + printf("✅ PoW difficulty calculation works\n"); + printf("✅ Nonce extraction works\n"); + printf("✅ PoW validation works\n"); + printf("✅ Error handling works\n"); + } else { + printf("❌ SOME TESTS FAILED. Please review the output above.\n"); + } + + nostr_cleanup(); + return all_passed ? 0 : 1; +} \ No newline at end of file diff --git a/tests/nip44_test b/tests/nip44_test index 7bfb605b..250c0c3a 100755 Binary files a/tests/nip44_test and b/tests/nip44_test differ diff --git a/tests/simple_init_test b/tests/simple_init_test index 511e1dfc..6ebc3590 100755 Binary files a/tests/simple_init_test and b/tests/simple_init_test differ diff --git a/tests/sync_relay_test b/tests/sync_relay_test index bc61d4c7..d62fe1b0 100755 Binary files a/tests/sync_relay_test and b/tests/sync_relay_test differ diff --git a/tests/wss_test b/tests/wss_test index f4416e7a..ddd01afb 100755 Binary files a/tests/wss_test and b/tests/wss_test differ