nip13 validation added

This commit is contained in:
2025-09-05 13:42:03 -04:00
parent 445ab7a8f4
commit 55e2a9c68e
16 changed files with 885 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -8,11 +8,41 @@
#include "nip001.h"
#include <stdint.h>
// 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

View File

@@ -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";
}
}

View File

@@ -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