/* * Event Miner - Nostr Proof-of-Work Mining Tool * * A multithreaded command-line tool for adding NIP-13 Proof-of-Work to Nostr events. * Uses the nostr_core_lib for cryptographic operations and event handling. */ #define _GNU_SOURCE // For strdup #define _POSIX_C_SOURCE 200112L // For usleep #include #include #include #include #include #include #include #include #include "nostr_core_lib/nostr_core/nostr_core.h" #include "nostr_core_lib/nostr_core/cJSON.h" // Constants #define MAX_EVENT_SIZE 1048576 // 1MB max event size #define DEFAULT_THREADS 4 #define DEFAULT_POW 2 // Data structures typedef struct { cJSON* event; unsigned char private_key[32]; int target_difficulty; volatile int found; cJSON* result_event; pthread_mutex_t mutex; pthread_cond_t cond; time_t start_time; int timeout_seconds; int thread_count; } mining_context_t; typedef struct { int pow; char* nsec; int threads; char* event_file; int timeout_min; int help; } args_t; // Function declarations static void usage(const char* prog_name); static int parse_arguments(int argc, char* argv[], args_t* args); static char* read_event_json(const char* filename); static char* read_stdin_json(void); static void* miner_thread(void* arg); static int mine_event(mining_context_t* ctx); static void cleanup_context(mining_context_t* ctx); // Usage information static void usage(const char* prog_name) { fprintf(stderr, "Usage: %s -pow -nsec -threads [options]\n\n", prog_name); fprintf(stderr, "Required arguments:\n"); fprintf(stderr, " -pow Number of leading zero bits for proof-of-work\n"); fprintf(stderr, " -nsec Private key in hex or nsec bech32 format\n"); fprintf(stderr, " -threads Number of mining threads to use\n\n"); fprintf(stderr, "Optional arguments:\n"); fprintf(stderr, " -e Read event from file (default: stdin)\n"); fprintf(stderr, " --timeout_min Timeout in minutes (default: no timeout)\n"); fprintf(stderr, " -h, --help Show this help message\n\n"); fprintf(stderr, "Examples:\n"); fprintf(stderr, " echo '{\"kind\":1,...}' | %s -pow 4 -nsec nsec1... -threads 8\n", prog_name); fprintf(stderr, " %s -pow 4 -nsec abc123... -threads 8 -e event.json --timeout_min 10\n", prog_name); } // Parse command line arguments static int parse_arguments(int argc, char* argv[], args_t* args) { // Initialize args structure memset(args, 0, sizeof(args_t)); args->pow = -1; // Indicates not set args->threads = -1; // Indicates not set // Simple manual parsing to avoid getopt complexities with multi-char options for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-pow") == 0 && i + 1 < argc) { args->pow = atoi(argv[i + 1]); if (args->pow <= 0) { fprintf(stderr, "Error: pow must be a positive integer\n"); return -1; } i++; // Skip the next argument } else if (strcmp(argv[i], "-nsec") == 0 && i + 1 < argc) { if (args->nsec) free(args->nsec); args->nsec = strdup(argv[i + 1]); if (!args->nsec) { fprintf(stderr, "Error: memory allocation failed\n"); return -1; } i++; // Skip the next argument } else if (strcmp(argv[i], "-threads") == 0 && i + 1 < argc) { args->threads = atoi(argv[i + 1]); if (args->threads <= 0) { fprintf(stderr, "Error: threads must be a positive integer\n"); return -1; } i++; // Skip the next argument } else if (strcmp(argv[i], "-e") == 0 && i + 1 < argc) { args->event_file = strdup(argv[i + 1]); if (!args->event_file) { fprintf(stderr, "Error: memory allocation failed\n"); return -1; } i++; // Skip the next argument } else if (strcmp(argv[i], "--timeout_min") == 0 && i + 1 < argc) { args->timeout_min = atoi(argv[i + 1]); if (args->timeout_min <= 0) { fprintf(stderr, "Error: timeout_min must be a positive integer\n"); return -1; } i++; // Skip the next argument } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { args->help = 1; return 0; } else if (argv[i][0] == '-') { fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); return -1; } } // Check required arguments if (args->pow == -1 || !args->nsec || args->threads == -1) { fprintf(stderr, "Error: Missing required arguments\n"); return -1; } return 0; } // Read event JSON from file static char* read_event_json(const char* filename) { FILE* file = fopen(filename, "r"); if (!file) { fprintf(stderr, "Error: Cannot open file '%s': %s\n", filename, strerror(errno)); return NULL; } // Get file size fseek(file, 0, SEEK_END); long file_size = ftell(file); fseek(file, 0, SEEK_SET); if (file_size > MAX_EVENT_SIZE) { fprintf(stderr, "Error: Event file too large (max %d bytes)\n", MAX_EVENT_SIZE); fclose(file); return NULL; } // Allocate buffer and read file char* buffer = malloc(file_size + 1); if (!buffer) { fprintf(stderr, "Error: Memory allocation failed\n"); fclose(file); return NULL; } size_t bytes_read = fread(buffer, 1, file_size, file); buffer[bytes_read] = '\0'; fclose(file); return buffer; } // Read event JSON from stdin static char* read_stdin_json(void) { char* buffer = malloc(MAX_EVENT_SIZE); if (!buffer) { fprintf(stderr, "Error: Memory allocation failed\n"); return NULL; } size_t total_read = 0; char chunk[4096]; while (fgets(chunk, sizeof(chunk), stdin)) { size_t chunk_len = strlen(chunk); if (total_read + chunk_len >= MAX_EVENT_SIZE - 1) { fprintf(stderr, "Error: Input too large (max %d bytes)\n", MAX_EVENT_SIZE); free(buffer); return NULL; } strcpy(buffer + total_read, chunk); total_read += chunk_len; } buffer[total_read] = '\0'; if (total_read == 0) { fprintf(stderr, "Error: No input received\n"); free(buffer); return NULL; } return buffer; } // Mining thread function static void* miner_thread(void* arg) { mining_context_t* ctx = (mining_context_t*)arg; // Create a copy of the event for this thread char* event_str = cJSON_Print(ctx->event); if (!event_str) { return NULL; } cJSON* local_event = cJSON_Parse(event_str); free(event_str); if (!local_event) { return NULL; } // Mine until solution found or timeout while (!ctx->found) { // Check timeout if (ctx->timeout_seconds > 0) { time_t current_time = time(NULL); if (current_time - ctx->start_time >= ctx->timeout_seconds) { pthread_mutex_lock(&ctx->mutex); if (!ctx->found) { ctx->found = -1; // Timeout flag } pthread_cond_broadcast(&ctx->cond); pthread_mutex_unlock(&ctx->mutex); break; } } // Attempt mining int result = nostr_add_proof_of_work(local_event, ctx->private_key, ctx->target_difficulty, NULL, NULL); if (result == NOSTR_SUCCESS) { pthread_mutex_lock(&ctx->mutex); if (!ctx->found) { ctx->found = 1; // Success flag ctx->result_event = cJSON_Duplicate(local_event, 1); pthread_cond_broadcast(&ctx->cond); } pthread_mutex_unlock(&ctx->mutex); break; } // Small delay to prevent CPU overuse usleep(1000); // 1ms } cJSON_Delete(local_event); return NULL; } // Main mining function static int mine_event(mining_context_t* ctx) { // Initialize synchronization objects if (pthread_mutex_init(&ctx->mutex, NULL) != 0) { fprintf(stderr, "Error: Failed to initialize mutex\n"); return -1; } if (pthread_cond_init(&ctx->cond, NULL) != 0) { fprintf(stderr, "Error: Failed to initialize condition variable\n"); pthread_mutex_destroy(&ctx->mutex); return -1; } // Create worker threads pthread_t* threads = malloc(ctx->thread_count * sizeof(pthread_t)); if (!threads) { fprintf(stderr, "Error: Memory allocation failed\n"); pthread_mutex_destroy(&ctx->mutex); pthread_cond_destroy(&ctx->cond); return -1; } ctx->start_time = time(NULL); // Start threads for (int i = 0; i < ctx->thread_count; i++) { if (pthread_create(&threads[i], NULL, miner_thread, ctx) != 0) { fprintf(stderr, "Error: Failed to create thread %d\n", i); // Clean up already created threads ctx->found = -2; // Error flag for (int j = 0; j < i; j++) { pthread_join(threads[j], NULL); } free(threads); pthread_mutex_destroy(&ctx->mutex); pthread_cond_destroy(&ctx->cond); return -1; } } // Wait for solution or timeout pthread_mutex_lock(&ctx->mutex); while (ctx->found == 0) { pthread_cond_wait(&ctx->cond, &ctx->mutex); } pthread_mutex_unlock(&ctx->mutex); // Wait for all threads to finish for (int i = 0; i < ctx->thread_count; i++) { pthread_join(threads[i], NULL); } free(threads); pthread_mutex_destroy(&ctx->mutex); pthread_cond_destroy(&ctx->cond); return ctx->found; } // Cleanup context static void cleanup_context(mining_context_t* ctx) { if (ctx->event) { cJSON_Delete(ctx->event); ctx->event = NULL; } if (ctx->result_event) { cJSON_Delete(ctx->result_event); ctx->result_event = NULL; } } // Main function int main(int argc, char* argv[]) { args_t args; mining_context_t ctx; int exit_code = 0; // Initialize context memset(&ctx, 0, sizeof(mining_context_t)); // Parse arguments if (parse_arguments(argc, argv, &args) != 0) { usage(argv[0]); exit_code = 1; goto cleanup_args; } if (args.help) { usage(argv[0]); goto cleanup_args; } // Initialize nostr library if (nostr_init() != NOSTR_SUCCESS) { fprintf(stderr, "Error: Failed to initialize nostr_core library\n"); exit_code = 1; goto cleanup_args; } // Decode private key if (nostr_decode_nsec(args.nsec, ctx.private_key) != NOSTR_SUCCESS) { fprintf(stderr, "Error: Invalid private key format\n"); exit_code = 1; goto cleanup_nostr; } // Read event JSON char* event_json = NULL; if (args.event_file) { event_json = read_event_json(args.event_file); } else { event_json = read_stdin_json(); } if (!event_json) { exit_code = 1; goto cleanup_nostr; } // Parse JSON event ctx.event = cJSON_Parse(event_json); free(event_json); if (!ctx.event) { fprintf(stderr, "Error: Invalid JSON event format\n"); exit_code = 1; goto cleanup_nostr; } // Set mining parameters ctx.target_difficulty = args.pow; ctx.thread_count = args.threads; ctx.timeout_seconds = args.timeout_min > 0 ? args.timeout_min * 60 : 0; // Start mining int mining_result = mine_event(&ctx); if (mining_result == 1 && ctx.result_event) { // Success - output mined event char* output_json = cJSON_Print(ctx.result_event); if (output_json) { printf("%s\n", output_json); free(output_json); } else { fprintf(stderr, "Error: Failed to serialize result event\n"); exit_code = 1; } } else if (mining_result == -1) { fprintf(stderr, "Error: Mining timeout reached\n"); exit_code = 1; } else { fprintf(stderr, "Error: Mining failed\n"); exit_code = 1; } // Cleanup cleanup_context(&ctx); cleanup_nostr: nostr_cleanup(); cleanup_args: if (args.nsec) free(args.nsec); if (args.event_file) free(args.event_file); return exit_code; }