/* * NOSTR Core Library - NIP-013: Proof of Work */ #include "nip013.h" #include "nip001.h" #include "utils.h" #include "../cjson/cJSON.h" #include #include #include #include #include #include "../nostr_core/nostr_common.h" /** * Count leading zero bits in a hash (NIP-13 reference implementation) */ static int zero_bits(unsigned char b) { int n = 0; if (b == 0) return 8; while (b >>= 1) n++; return 7-n; } /** * Find the number of leading zero bits in a hash (NIP-13 reference implementation) */ static int count_leading_zero_bits(unsigned char *hash) { int bits, total, i; for (i = 0, total = 0; i < 32; i++) { bits = zero_bits(hash[i]); total += bits; if (bits != 8) break; } return total; } /** * Add or update nonce tag with target difficulty */ static int update_nonce_tag_with_difficulty(cJSON* tags, uint64_t nonce, int target_difficulty) { if (!tags) return -1; // Look for existing nonce tag and remove it cJSON* tag = NULL; int index = 0; 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) && strcmp(cJSON_GetStringValue(tag_type), "nonce") == 0) { // Remove existing nonce tag cJSON_DetachItemFromArray(tags, index); cJSON_Delete(tag); break; } } index++; } // Add new nonce tag with format: ["nonce", "", ""] cJSON* nonce_tag = cJSON_CreateArray(); if (!nonce_tag) return -1; char nonce_str[32]; char difficulty_str[16]; snprintf(nonce_str, sizeof(nonce_str), "%llu", (unsigned long long)nonce); snprintf(difficulty_str, sizeof(difficulty_str), "%d", target_difficulty); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce")); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str)); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(difficulty_str)); cJSON_AddItemToArray(tags, nonce_tag); return 0; } /** * Helper function to replace event content with successful PoW result */ static void replace_event_content(cJSON* target_event, cJSON* source_event) { // Remove old fields cJSON_DeleteItemFromObject(target_event, "id"); cJSON_DeleteItemFromObject(target_event, "sig"); cJSON_DeleteItemFromObject(target_event, "tags"); cJSON_DeleteItemFromObject(target_event, "created_at"); // Copy new fields from successful event cJSON* id = cJSON_GetObjectItem(source_event, "id"); cJSON* sig = cJSON_GetObjectItem(source_event, "sig"); cJSON* tags = cJSON_GetObjectItem(source_event, "tags"); cJSON* created_at = cJSON_GetObjectItem(source_event, "created_at"); if (id) cJSON_AddItemToObject(target_event, "id", cJSON_Duplicate(id, 1)); if (sig) cJSON_AddItemToObject(target_event, "sig", cJSON_Duplicate(sig, 1)); if (tags) cJSON_AddItemToObject(target_event, "tags", cJSON_Duplicate(tags, 1)); if (created_at) cJSON_AddItemToObject(target_event, "created_at", cJSON_Duplicate(created_at, 1)); } /** * Add NIP-13 Proof of Work to an event * * @param event The event to add proof of work to * @param private_key The private key for re-signing the event * @param target_difficulty Target number of leading zero bits (default: 4 if 0) * @param max_attempts Maximum number of mining attempts (default: 10,000,000 if <= 0) * @param progress_report_interval How often to call progress callback (default: 10,000 if <= 0) * @param timestamp_update_interval How often to update timestamp (default: 10,000 if <= 0) * @param progress_callback Optional callback for mining progress * @param user_data User data for progress callback * @return NOSTR_SUCCESS on success, error code on failure */ 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) { if (!event || !private_key) { return NOSTR_ERROR_INVALID_INPUT; } // Set default difficulty if not specified (but allow 0 to disable PoW) if (target_difficulty < 0) { target_difficulty = 4; } // If target_difficulty is 0, skip proof of work entirely if (target_difficulty == 0) { return NOSTR_SUCCESS; } // Set default values for parameters if (max_attempts <= 0) { max_attempts = 10000000; // 10 million default } if (progress_report_interval <= 0) { progress_report_interval = 10000; // Every 10,000 attempts } if (timestamp_update_interval <= 0) { timestamp_update_interval = 10000; // Every 10,000 attempts } // Extract event data for reconstruction cJSON* kind_item = cJSON_GetObjectItem(event, "kind"); cJSON* content_item = cJSON_GetObjectItem(event, "content"); cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at"); cJSON* tags_item = cJSON_GetObjectItem(event, "tags"); if (!kind_item || !content_item || !created_at_item || !tags_item) { return NOSTR_ERROR_INVALID_INPUT; } int kind = (int)cJSON_GetNumberValue(kind_item); const char* content = cJSON_GetStringValue(content_item); time_t original_timestamp = (time_t)cJSON_GetNumberValue(created_at_item); uint64_t nonce = 0; int attempts = 0; time_t current_timestamp = original_timestamp; // PoW difficulty tracking variables int best_difficulty_this_round = 0; int best_difficulty_overall = 0; // Mining loop while (attempts < max_attempts) { // Update timestamp based on timestamp_update_interval if (attempts % timestamp_update_interval == 0) { current_timestamp = time(NULL); #ifdef ENABLE_DEBUG_LOGGING FILE* f = fopen("debug.log", "a"); if (f) { fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n", attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty); fclose(f); } #endif // Reset best difficulty for the new round best_difficulty_this_round = 0; } // Call progress callback at specified intervals if (progress_callback && (attempts % progress_report_interval == 0)) { progress_callback(best_difficulty_overall, nonce, user_data); } // Create working copy of tags and add nonce cJSON* working_tags = cJSON_Duplicate(tags_item, 1); if (!working_tags) { return NOSTR_ERROR_MEMORY_FAILED; } if (update_nonce_tag_with_difficulty(working_tags, nonce, target_difficulty) != 0) { cJSON_Delete(working_tags); return NOSTR_ERROR_CRYPTO_FAILED; } // Create and sign new event with current nonce cJSON* test_event = nostr_create_and_sign_event(kind, content, working_tags, private_key, current_timestamp); cJSON_Delete(working_tags); if (!test_event) { return NOSTR_ERROR_CRYPTO_FAILED; } // Check PoW difficulty cJSON* id_item = cJSON_GetObjectItem(test_event, "id"); if (!id_item || !cJSON_IsString(id_item)) { cJSON_Delete(test_event); return NOSTR_ERROR_CRYPTO_FAILED; } const char* event_id = cJSON_GetStringValue(id_item); unsigned char hash[32]; if (nostr_hex_to_bytes(event_id, hash, 32) != NOSTR_SUCCESS) { cJSON_Delete(test_event); return NOSTR_ERROR_CRYPTO_FAILED; } // Count leading zero bits using NIP-13 method int current_difficulty = count_leading_zero_bits(hash); // Update difficulty tracking if (current_difficulty > best_difficulty_this_round) { best_difficulty_this_round = current_difficulty; } if (current_difficulty > best_difficulty_overall) { best_difficulty_overall = current_difficulty; } // Check if we've reached the target if (current_difficulty >= target_difficulty) { #ifdef ENABLE_DEBUG_LOGGING FILE* f = fopen("debug.log", "a"); if (f) { fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n", current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1); // Print the final event JSON char* event_json = cJSON_Print(test_event); if (event_json) { fprintf(f, "Final event: %s\n", event_json); free(event_json); } fclose(f); } #endif // Copy successful result back to input event replace_event_content(event, test_event); cJSON_Delete(test_event); return NOSTR_SUCCESS; } cJSON_Delete(test_event); nonce++; attempts++; } #ifdef ENABLE_DEBUG_LOGGING // Debug logging - failure FILE* f = fopen("debug.log", "a"); if (f) { fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts); fclose(f); } #endif // 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; }