Files
event_miner/event_miner.c
2025-08-14 15:23:30 -04:00

431 lines
13 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>
#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 <difficulty> -nsec <private_key> -threads <count> [options]\n\n", prog_name);
fprintf(stderr, "Required arguments:\n");
fprintf(stderr, " -pow <difficulty> Number of leading zero bits for proof-of-work\n");
fprintf(stderr, " -nsec <private_key> Private key in hex or nsec bech32 format\n");
fprintf(stderr, " -threads <count> Number of mining threads to use\n\n");
fprintf(stderr, "Optional arguments:\n");
fprintf(stderr, " -e <filename> Read event from file (default: stdin)\n");
fprintf(stderr, " --timeout_min <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;
}