/* * Superball Thrower - C Implementation * * A high-performance privacy-focused Superball Thrower daemon in C * using the nostr_core_lib for all NOSTR protocol operations. * * Implements SUP-01 through SUP-06 of the Superball protocol. * * Author: Roo (Code Mode) * License: MIT */ // ============================================================================ // [1] INCLUDES & CONSTANTS // ============================================================================ #include #include #include #include #include #include #include #include #include // nostr_core_lib headers #include "nostr_core.h" #include "cJSON.h" // Version #define THROWER_VERSION "v0.0.2" // Configuration constants #define MAX_RELAYS 50 #define MAX_QUEUE_SIZE 1000 #define MAX_PAYLOAD_SIZE 65536 #define MAX_PADDING_SIZE 4096 #define DEFAULT_MAX_DELAY 86460 #define DEFAULT_REFRESH_RATE 300 #define CONFIG_FILE "config.json" // Log levels #define LOG_DEBUG 0 #define LOG_INFO 1 #define LOG_WARN 2 #define LOG_ERROR 3 // ============================================================================ // [2] DATA STRUCTURES // ============================================================================ // Relay configuration typedef struct { char* url; int read; int write; char* auth_status; // "no-auth", "auth-required", "error", "unknown" } relay_config_t; // Configuration structure typedef struct { char* private_key_hex; char* name; char* description; int max_delay; int refresh_rate; char* supported_sups; char* software; char* version; relay_config_t* relays; int relay_count; int max_queue_size; int log_level; } superball_config_t; // Routing payload (Type 1 - from builder) typedef struct { cJSON* event; // Inner event (encrypted or final) char** relays; // Target relay URLs int relay_count; int delay; // Delay in seconds char* next_hop_pubkey; // NULL for final posting char* audit_tag; // Required audit tag char* payment; // Optional eCash token int add_padding_bytes; // Optional padding instruction } routing_payload_t; // Padding payload (Type 2 - from previous thrower) typedef struct { cJSON* event; // Still-encrypted inner event char* padding; // Padding data to discard } padding_payload_t; // Queue item typedef struct { char event_id[65]; cJSON* wrapped_event; routing_payload_t* routing; time_t received_at; time_t process_at; char status[32]; // "queued", "processing", "completed", "failed" } queue_item_t; // Event queue typedef struct { queue_item_t** items; int count; int capacity; pthread_mutex_t mutex; } event_queue_t; // Main daemon context typedef struct { superball_config_t* config; nostr_relay_pool_t* pool; event_queue_t* queue; pthread_t auto_publish_thread; pthread_t queue_processor_thread; unsigned char private_key[32]; unsigned char public_key[32]; int running; int auto_publish_running; int processed_events; } superball_thrower_t; // Payload type enum typedef enum { PAYLOAD_ERROR = 0, PAYLOAD_ROUTING = 1, // Type 1: Routing instructions from builder PAYLOAD_PADDING = 2 // Type 2: Padding wrapper from previous thrower } payload_type_t; // ============================================================================ // [3] FORWARD DECLARATIONS // ============================================================================ // Utility functions static void log_message(int level, const char* format, ...); static int add_jitter(int delay); static char* generate_padding(int bytes); // Configuration functions static superball_config_t* config_load(const char* path); static void config_free(superball_config_t* config); static int config_validate(superball_config_t* config); // Crypto functions static int decrypt_nip44(const unsigned char* private_key, const char* sender_pubkey, const char* encrypted, char* output, size_t output_size); static int encrypt_nip44(const unsigned char* private_key, const char* recipient_pubkey, const char* plaintext, char* output, size_t output_size); // Queue functions static event_queue_t* queue_create(int capacity); static int queue_add(event_queue_t* queue, queue_item_t* item); static queue_item_t* queue_get_ready(event_queue_t* queue); static void queue_destroy(event_queue_t* queue); static void* queue_processor_thread_func(void* arg); // Relay functions static int relay_test_all(superball_thrower_t* thrower); // Event processing functions static void on_routing_event(cJSON* event, const char* relay_url, void* user_data); static void on_eose(cJSON** events, int event_count, void* user_data); static payload_type_t decrypt_payload(superball_thrower_t* thrower, cJSON* event, void** payload_out); static routing_payload_t* parse_routing_payload(cJSON* payload); static padding_payload_t* parse_padding_payload(cJSON* payload); static int validate_routing(routing_payload_t* routing, int max_delay); static void forward_to_next_thrower(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing); static void post_final_event(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing); static void publish_callback(const char* relay_url, const char* event_id, int success, const char* message, void* user_data); static void free_routing_payload(routing_payload_t* payload); static void free_padding_payload(padding_payload_t* payload); // Thrower info functions static int publish_thrower_info(superball_thrower_t* thrower); static int publish_metadata(superball_thrower_t* thrower); static int publish_relay_list(superball_thrower_t* thrower); static void* auto_publish_thread_func(void* arg); // Main functions static void signal_handler(int signum); static superball_thrower_t* thrower_create(const char* config_path); static int thrower_start(superball_thrower_t* thrower); static void thrower_stop(superball_thrower_t* thrower); static void thrower_destroy(superball_thrower_t* thrower); // ============================================================================ // [4] GLOBAL VARIABLES // ============================================================================ static volatile sig_atomic_t g_running = 1; static superball_thrower_t* g_thrower = NULL; static int g_log_level = LOG_INFO; // ============================================================================ // [5] UTILITY FUNCTIONS // ============================================================================ static void log_message(int level, const char* format, ...) { if (level < g_log_level) return; const char* level_str[] = {"DEBUG", "INFO", "WARN", "ERROR"}; time_t now = time(NULL); char timestamp[32]; strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now)); fprintf(stderr, "[%s] [%s] ", timestamp, level_str[level]); va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); fprintf(stderr, "\n"); fflush(stderr); } static int add_jitter(int delay) { // Add ±10% random jitter int jitter = (rand() % (delay / 5)) - (delay / 10); return delay + jitter; } static char* generate_padding(int bytes) { if (bytes <= 0) return strdup(""); if (bytes > MAX_PADDING_SIZE) bytes = MAX_PADDING_SIZE; const char* chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; int chars_len = strlen(chars); char* padding = malloc(bytes + 1); if (!padding) return NULL; for (int i = 0; i < bytes; i++) { padding[i] = chars[rand() % chars_len]; } padding[bytes] = '\0'; return padding; } // ============================================================================ // [6] CONFIGURATION FUNCTIONS // ============================================================================ static superball_config_t* config_load(const char* path) { FILE* fp = fopen(path, "r"); if (!fp) { log_message(LOG_ERROR, "Failed to open config file: %s", path); return NULL; } fseek(fp, 0, SEEK_END); long size = ftell(fp); fseek(fp, 0, SEEK_SET); char* buffer = malloc(size + 1); if (!buffer) { fclose(fp); return NULL; } size_t bytes_read = fread(buffer, 1, size, fp); (void)bytes_read; // Suppress unused warning buffer[size] = '\0'; fclose(fp); cJSON* json = cJSON_Parse(buffer); free(buffer); if (!json) { log_message(LOG_ERROR, "Failed to parse config JSON"); return NULL; } superball_config_t* config = calloc(1, sizeof(superball_config_t)); if (!config) { cJSON_Delete(json); return NULL; } // Parse thrower section cJSON* thrower = cJSON_GetObjectItem(json, "thrower"); if (thrower) { cJSON* item; if ((item = cJSON_GetObjectItem(thrower, "privateKey"))) config->private_key_hex = strdup(cJSON_GetStringValue(item)); if ((item = cJSON_GetObjectItem(thrower, "name"))) config->name = strdup(cJSON_GetStringValue(item)); if ((item = cJSON_GetObjectItem(thrower, "description"))) config->description = strdup(cJSON_GetStringValue(item)); if ((item = cJSON_GetObjectItem(thrower, "maxDelay"))) config->max_delay = item->valueint; else config->max_delay = DEFAULT_MAX_DELAY; if ((item = cJSON_GetObjectItem(thrower, "refreshRate"))) config->refresh_rate = item->valueint; else config->refresh_rate = DEFAULT_REFRESH_RATE; if ((item = cJSON_GetObjectItem(thrower, "supportedSups"))) config->supported_sups = strdup(cJSON_GetStringValue(item)); if ((item = cJSON_GetObjectItem(thrower, "software"))) config->software = strdup(cJSON_GetStringValue(item)); if ((item = cJSON_GetObjectItem(thrower, "version"))) config->version = strdup(cJSON_GetStringValue(item)); } // Parse relays section cJSON* relays = cJSON_GetObjectItem(json, "relays"); if (relays && cJSON_IsArray(relays)) { config->relay_count = cJSON_GetArraySize(relays); config->relays = calloc(config->relay_count, sizeof(relay_config_t)); for (int i = 0; i < config->relay_count; i++) { cJSON* relay = cJSON_GetArrayItem(relays, i); cJSON* url = cJSON_GetObjectItem(relay, "url"); cJSON* read = cJSON_GetObjectItem(relay, "read"); cJSON* write = cJSON_GetObjectItem(relay, "write"); if (url) config->relays[i].url = strdup(cJSON_GetStringValue(url)); config->relays[i].read = read ? cJSON_IsTrue(read) : 1; config->relays[i].write = write ? cJSON_IsTrue(write) : 1; config->relays[i].auth_status = strdup("unknown"); } } // Parse daemon section cJSON* daemon = cJSON_GetObjectItem(json, "daemon"); if (daemon) { cJSON* item; if ((item = cJSON_GetObjectItem(daemon, "maxQueueSize"))) config->max_queue_size = item->valueint; else config->max_queue_size = MAX_QUEUE_SIZE; if ((item = cJSON_GetObjectItem(daemon, "logLevel"))) { const char* level = cJSON_GetStringValue(item); if (strcmp(level, "debug") == 0) config->log_level = LOG_DEBUG; else if (strcmp(level, "info") == 0) config->log_level = LOG_INFO; else if (strcmp(level, "warn") == 0) config->log_level = LOG_WARN; else if (strcmp(level, "error") == 0) config->log_level = LOG_ERROR; else config->log_level = LOG_INFO; } else { config->log_level = LOG_INFO; } } cJSON_Delete(json); return config; } static void config_free(superball_config_t* config) { if (!config) return; free(config->private_key_hex); free(config->name); free(config->description); free(config->supported_sups); free(config->software); free(config->version); for (int i = 0; i < config->relay_count; i++) { free(config->relays[i].url); free(config->relays[i].auth_status); } free(config->relays); free(config); } static int config_validate(superball_config_t* config) { if (!config) return 0; if (!config->private_key_hex || strlen(config->private_key_hex) != 64) { log_message(LOG_ERROR, "Invalid private key in configuration"); return 0; } if (config->relay_count == 0) { log_message(LOG_ERROR, "No relays configured"); return 0; } return 1; } // ============================================================================ // [7] CRYPTO FUNCTIONS // ============================================================================ static int decrypt_nip44(const unsigned char* private_key, const char* sender_pubkey, const char* encrypted, char* output, size_t output_size) { unsigned char sender_pubkey_bytes[32]; if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, 32) != 0) { return NOSTR_ERROR_INVALID_INPUT; } return nostr_nip44_decrypt(private_key, sender_pubkey_bytes, encrypted, output, output_size); } static int encrypt_nip44(const unsigned char* private_key, const char* recipient_pubkey, const char* plaintext, char* output, size_t output_size) { unsigned char recipient_pubkey_bytes[32]; if (nostr_hex_to_bytes(recipient_pubkey, recipient_pubkey_bytes, 32) != 0) { return NOSTR_ERROR_INVALID_INPUT; } return nostr_nip44_encrypt(private_key, recipient_pubkey_bytes, plaintext, output, output_size); } // ============================================================================ // [8] QUEUE FUNCTIONS // ============================================================================ static event_queue_t* queue_create(int capacity) { event_queue_t* queue = malloc(sizeof(event_queue_t)); if (!queue) return NULL; queue->items = calloc(capacity, sizeof(queue_item_t*)); if (!queue->items) { free(queue); return NULL; } queue->count = 0; queue->capacity = capacity; pthread_mutex_init(&queue->mutex, NULL); return queue; } static int queue_add(event_queue_t* queue, queue_item_t* item) { pthread_mutex_lock(&queue->mutex); if (queue->count >= queue->capacity) { pthread_mutex_unlock(&queue->mutex); log_message(LOG_WARN, "Queue full, dropping oldest item"); return -1; } queue->items[queue->count++] = item; log_message(LOG_INFO, "Event queued: %s (process in %ld seconds)", item->event_id, item->process_at - time(NULL)); pthread_mutex_unlock(&queue->mutex); return 0; } static queue_item_t* queue_get_ready(event_queue_t* queue) { pthread_mutex_lock(&queue->mutex); time_t now = time(NULL); queue_item_t* ready_item = NULL; int ready_index = -1; for (int i = 0; i < queue->count; i++) { if (queue->items[i]->process_at <= now && strcmp(queue->items[i]->status, "queued") == 0) { ready_item = queue->items[i]; ready_index = i; break; } } if (ready_item) { // Remove from queue for (int i = ready_index; i < queue->count - 1; i++) { queue->items[i] = queue->items[i + 1]; } queue->count--; } pthread_mutex_unlock(&queue->mutex); return ready_item; } static void queue_destroy(event_queue_t* queue) { if (!queue) return; pthread_mutex_lock(&queue->mutex); for (int i = 0; i < queue->count; i++) { if (queue->items[i]->wrapped_event) { cJSON_Delete(queue->items[i]->wrapped_event); } if (queue->items[i]->routing) { free_routing_payload(queue->items[i]->routing); } free(queue->items[i]); } free(queue->items); pthread_mutex_unlock(&queue->mutex); pthread_mutex_destroy(&queue->mutex); free(queue); } static void* queue_processor_thread_func(void* arg) { superball_thrower_t* thrower = (superball_thrower_t*)arg; log_message(LOG_INFO, "Queue processor thread started"); while (thrower->running) { queue_item_t* item = queue_get_ready(thrower->queue); if (item) { log_message(LOG_INFO, "Processing queued event: %s", item->event_id); strcpy(item->status, "processing"); // Check if we should forward or post final if (item->routing->next_hop_pubkey) { forward_to_next_thrower(thrower, item->wrapped_event, item->routing); } else { post_final_event(thrower, item->wrapped_event, item->routing); } thrower->processed_events++; // Cleanup if (item->wrapped_event) cJSON_Delete(item->wrapped_event); if (item->routing) free_routing_payload(item->routing); free(item); } sleep(1); // Check queue every second } log_message(LOG_INFO, "Queue processor thread stopped"); return NULL; } // ============================================================================ // [9] RELAY FUNCTIONS // ============================================================================ static int relay_test_all(superball_thrower_t* thrower) { log_message(LOG_INFO, "Testing authentication for %d relays...", thrower->config->relay_count); // For now, mark all as no-auth (testing would require WebSocket implementation) // In production, implement proper AUTH testing for (int i = 0; i < thrower->config->relay_count; i++) { free(thrower->config->relays[i].auth_status); thrower->config->relays[i].auth_status = strdup("no-auth"); } log_message(LOG_INFO, "Relay authentication testing complete"); return 0; } // ============================================================================ // [10] EVENT PROCESSING FUNCTIONS // ============================================================================ static void on_routing_event(cJSON* event, const char* relay_url, void* user_data) { superball_thrower_t* thrower = (superball_thrower_t*)user_data; cJSON* id = cJSON_GetObjectItem(event, "id"); if (!id) return; const char* event_id = cJSON_GetStringValue(id); log_message(LOG_INFO, "Received routing event: %.16s... from %s", event_id, relay_url); // First decryption void* payload = NULL; payload_type_t type = decrypt_payload(thrower, event, &payload); if (type == PAYLOAD_PADDING) { // Type 2: Padding payload - perform second decryption padding_payload_t* padding_payload = (padding_payload_t*)payload; log_message(LOG_INFO, "Detected padding payload, discarding %zu bytes of padding", strlen(padding_payload->padding)); // Second decryption to get routing instructions void* routing_payload = NULL; payload_type_t inner_type = decrypt_payload(thrower, padding_payload->event, &routing_payload); if (inner_type == PAYLOAD_ROUTING) { routing_payload_t* routing = (routing_payload_t*)routing_payload; if (validate_routing(routing, thrower->config->max_delay)) { // Create queue item queue_item_t* item = calloc(1, sizeof(queue_item_t)); strncpy(item->event_id, event_id, 64); item->wrapped_event = cJSON_Duplicate(padding_payload->event, 1); item->routing = routing; item->received_at = time(NULL); item->process_at = time(NULL) + add_jitter(routing->delay); strcpy(item->status, "queued"); queue_add(thrower->queue, item); } else { free_routing_payload(routing); } } free_padding_payload(padding_payload); } else if (type == PAYLOAD_ROUTING) { // Type 1: Routing payload - process directly log_message(LOG_INFO, "Detected routing payload, processing directly"); routing_payload_t* routing = (routing_payload_t*)payload; if (validate_routing(routing, thrower->config->max_delay)) { // Create queue item queue_item_t* item = calloc(1, sizeof(queue_item_t)); strncpy(item->event_id, event_id, 64); item->wrapped_event = cJSON_Duplicate(event, 1); item->routing = routing; item->received_at = time(NULL); item->process_at = time(NULL) + add_jitter(routing->delay); strcpy(item->status, "queued"); queue_add(thrower->queue, item); } else { free_routing_payload(routing); } } else { log_message(LOG_ERROR, "Failed to decrypt payload for event %.16s...", event_id); } } static void on_eose(cJSON** events, int event_count, void* user_data) { (void)events; // Suppress unused parameter warning (void)user_data; // Suppress unused parameter warning log_message(LOG_DEBUG, "End of stored events (EOSE) - %d events received", event_count); } static payload_type_t decrypt_payload(superball_thrower_t* thrower, cJSON* event, void** payload_out) { cJSON* content = cJSON_GetObjectItem(event, "content"); cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey"); if (!content || !pubkey) return PAYLOAD_ERROR; char decrypted[MAX_PAYLOAD_SIZE]; int result = decrypt_nip44(thrower->private_key, cJSON_GetStringValue(pubkey), cJSON_GetStringValue(content), decrypted, sizeof(decrypted)); if (result != NOSTR_SUCCESS) { log_message(LOG_ERROR, "NIP-44 decryption failed"); return PAYLOAD_ERROR; } cJSON* payload = cJSON_Parse(decrypted); if (!payload) { log_message(LOG_ERROR, "Failed to parse decrypted payload JSON"); return PAYLOAD_ERROR; } // Check payload type if (cJSON_HasObjectItem(payload, "padding")) { *payload_out = parse_padding_payload(payload); cJSON_Delete(payload); return *payload_out ? PAYLOAD_PADDING : PAYLOAD_ERROR; } else if (cJSON_HasObjectItem(payload, "routing")) { *payload_out = parse_routing_payload(payload); cJSON_Delete(payload); return *payload_out ? PAYLOAD_ROUTING : PAYLOAD_ERROR; } cJSON_Delete(payload); return PAYLOAD_ERROR; } static routing_payload_t* parse_routing_payload(cJSON* payload) { routing_payload_t* routing = calloc(1, sizeof(routing_payload_t)); if (!routing) return NULL; cJSON* event = cJSON_GetObjectItem(payload, "event"); cJSON* routing_obj = cJSON_GetObjectItem(payload, "routing"); if (!event || !routing_obj) { free(routing); return NULL; } routing->event = cJSON_Duplicate(event, 1); // Parse routing instructions cJSON* relays = cJSON_GetObjectItem(routing_obj, "relays"); if (relays && cJSON_IsArray(relays)) { routing->relay_count = cJSON_GetArraySize(relays); routing->relays = calloc(routing->relay_count, sizeof(char*)); for (int i = 0; i < routing->relay_count; i++) { cJSON* relay = cJSON_GetArrayItem(relays, i); routing->relays[i] = strdup(cJSON_GetStringValue(relay)); } } cJSON* delay = cJSON_GetObjectItem(routing_obj, "delay"); routing->delay = delay ? delay->valueint : 0; cJSON* p = cJSON_GetObjectItem(routing_obj, "p"); routing->next_hop_pubkey = p ? strdup(cJSON_GetStringValue(p)) : NULL; cJSON* audit = cJSON_GetObjectItem(routing_obj, "audit"); routing->audit_tag = audit ? strdup(cJSON_GetStringValue(audit)) : NULL; cJSON* payment = cJSON_GetObjectItem(routing_obj, "payment"); routing->payment = payment ? strdup(cJSON_GetStringValue(payment)) : NULL; cJSON* add_padding = cJSON_GetObjectItem(routing_obj, "add_padding_bytes"); routing->add_padding_bytes = add_padding ? add_padding->valueint : 0; return routing; } static padding_payload_t* parse_padding_payload(cJSON* payload) { padding_payload_t* padding = calloc(1, sizeof(padding_payload_t)); if (!padding) return NULL; cJSON* event = cJSON_GetObjectItem(payload, "event"); cJSON* padding_str = cJSON_GetObjectItem(payload, "padding"); if (!event) { free(padding); return NULL; } padding->event = cJSON_Duplicate(event, 1); padding->padding = padding_str ? strdup(cJSON_GetStringValue(padding_str)) : strdup(""); return padding; } static int validate_routing(routing_payload_t* routing, int max_delay) { if (!routing) return 0; if (!routing->relays || routing->relay_count == 0) { log_message(LOG_ERROR, "No relays in routing instructions"); return 0; } if (routing->delay < 0 || routing->delay > max_delay) { log_message(LOG_ERROR, "Invalid delay: %d (max: %d)", routing->delay, max_delay); return 0; } if (!routing->audit_tag) { log_message(LOG_ERROR, "Missing audit tag"); return 0; } return 1; } static void forward_to_next_thrower(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing) { log_message(LOG_INFO, "Forwarding to next thrower: %.16s...", routing->next_hop_pubkey); // Generate ephemeral keypair unsigned char ephemeral_private[32]; unsigned char ephemeral_public[32]; nostr_generate_keypair(ephemeral_private, ephemeral_public); // Generate padding char* padding_data = generate_padding(routing->add_padding_bytes); if (routing->add_padding_bytes > 0) { log_message(LOG_INFO, "Generated %d bytes of padding", routing->add_padding_bytes); } // Create padding payload cJSON* padding_payload = cJSON_CreateObject(); cJSON_AddItemToObject(padding_payload, "event", cJSON_Duplicate(event, 1)); cJSON_AddItemToObject(padding_payload, "padding", cJSON_CreateString(padding_data)); free(padding_data); // Encrypt to next thrower char* payload_json = cJSON_PrintUnformatted(padding_payload); char encrypted[MAX_PAYLOAD_SIZE]; int result = encrypt_nip44(ephemeral_private, routing->next_hop_pubkey, payload_json, encrypted, sizeof(encrypted)); free(payload_json); cJSON_Delete(padding_payload); if (result != NOSTR_SUCCESS) { log_message(LOG_ERROR, "Failed to encrypt padding payload"); return; } // Create routing event cJSON* tags = cJSON_CreateArray(); cJSON* p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(routing->next_hop_pubkey)); cJSON_AddItemToArray(tags, p_tag); cJSON* audit_tag = cJSON_CreateArray(); cJSON_AddItemToArray(audit_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(audit_tag, cJSON_CreateString(routing->audit_tag)); cJSON_AddItemToArray(tags, audit_tag); cJSON* signed_event = nostr_create_and_sign_event(22222, encrypted, tags, ephemeral_private, time(NULL)); cJSON_Delete(tags); if (!signed_event) { log_message(LOG_ERROR, "Failed to create routing event"); return; } // Log the event JSON being published char* event_json = cJSON_Print(signed_event); if (event_json) { log_message(LOG_INFO, "Publishing routing event JSON:\n%s", event_json); free(event_json); } // Publish to relays nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays, routing->relay_count, signed_event, publish_callback, thrower); cJSON_Delete(signed_event); log_message(LOG_INFO, "Forwarded event to next thrower"); } static void post_final_event(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing) { (void)event; // The wrapped event is not used - we publish the inner event from routing log_message(LOG_INFO, "Posting final event to %d relays", routing->relay_count); // The inner event is in routing->event (this is the kind 1 note, not the kind 22222 wrapper) if (!routing->event) { log_message(LOG_ERROR, "No inner event to publish"); return; } // Log the inner event JSON being published char* event_json = cJSON_Print(routing->event); if (event_json) { log_message(LOG_INFO, "Publishing final event JSON:\n%s", event_json); free(event_json); } // Publish the inner event directly (this is the actual kind 1 note) nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays, routing->relay_count, routing->event, publish_callback, thrower); cJSON* id = cJSON_GetObjectItem(routing->event, "id"); if (id) { log_message(LOG_INFO, "Final event posted: %.16s...", cJSON_GetStringValue(id)); } } static void publish_callback(const char* relay_url, const char* event_id, int success, const char* message, void* user_data) { superball_thrower_t* thrower = (superball_thrower_t*)user_data; if (success) { log_message(LOG_INFO, "✅ Published to %s: %.16s...", relay_url, event_id); // Print relay response if available if (message) { log_message(LOG_DEBUG, "Relay response: %s", message); } } else { log_message(LOG_ERROR, "❌ Failed to publish to %s: %s", relay_url, message ? message : "unknown error"); } // Note: We don't have access to the full event JSON here in the callback // The event was already published. To see the full event, we'd need to // log it before calling nostr_relay_pool_publish_async (void)thrower; // Suppress unused warning for now } static void free_routing_payload(routing_payload_t* payload) { if (!payload) return; if (payload->event) cJSON_Delete(payload->event); for (int i = 0; i < payload->relay_count; i++) { free(payload->relays[i]); } free(payload->relays); free(payload->next_hop_pubkey); free(payload->audit_tag); free(payload->payment); free(payload); } static void free_padding_payload(padding_payload_t* payload) { if (!payload) return; if (payload->event) cJSON_Delete(payload->event); free(payload->padding); free(payload); } // ============================================================================ // [11] THROWER INFO FUNCTIONS // ============================================================================ static int publish_thrower_info(superball_thrower_t* thrower) { log_message(LOG_INFO, "Publishing Thrower Information Document (SUP-06)..."); cJSON* tags = cJSON_CreateArray(); if (thrower->config->name) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString("name")); cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->name)); cJSON_AddItemToArray(tags, tag); } if (thrower->config->description) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString("description")); cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->description)); cJSON_AddItemToArray(tags, tag); } if (thrower->config->supported_sups) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString("supported_sups")); cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->supported_sups)); cJSON_AddItemToArray(tags, tag); } if (thrower->config->software) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString("software")); cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->software)); cJSON_AddItemToArray(tags, tag); } if (thrower->config->version) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString("version")); cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->version)); cJSON_AddItemToArray(tags, tag); } char refresh_str[32]; snprintf(refresh_str, sizeof(refresh_str), "%d", thrower->config->refresh_rate); cJSON* refresh_tag = cJSON_CreateArray(); cJSON_AddItemToArray(refresh_tag, cJSON_CreateString("refresh_rate")); cJSON_AddItemToArray(refresh_tag, cJSON_CreateString(refresh_str)); cJSON_AddItemToArray(tags, refresh_tag); char max_delay_str[32]; snprintf(max_delay_str, sizeof(max_delay_str), "%d", thrower->config->max_delay); cJSON* max_delay_tag = cJSON_CreateArray(); cJSON_AddItemToArray(max_delay_tag, cJSON_CreateString("max_delay")); cJSON_AddItemToArray(max_delay_tag, cJSON_CreateString(max_delay_str)); cJSON_AddItemToArray(tags, max_delay_tag); cJSON* event = nostr_create_and_sign_event(12222, "", tags, thrower->private_key, time(NULL)); cJSON_Delete(tags); if (!event) { log_message(LOG_ERROR, "Failed to create thrower info event"); return -1; } // Get write-capable relays const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*)); int relay_count = 0; for (int i = 0; i < thrower->config->relay_count; i++) { if (thrower->config->relays[i].write && strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) { relay_urls[relay_count++] = thrower->config->relays[i].url; } } if (relay_count == 0) { log_message(LOG_WARN, "No write-capable relays for thrower info"); free(relay_urls); cJSON_Delete(event); return -1; } nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count, event, publish_callback, thrower); free(relay_urls); cJSON_Delete(event); log_message(LOG_INFO, "Thrower info published to %d relays", relay_count); return 0; } static int publish_metadata(superball_thrower_t* thrower) { log_message(LOG_INFO, "Publishing metadata (kind 0)..."); // Create metadata JSON content cJSON* metadata = cJSON_CreateObject(); if (thrower->config->name) { cJSON_AddStringToObject(metadata, "name", thrower->config->name); } if (thrower->config->description) { cJSON_AddStringToObject(metadata, "about", thrower->config->description); } // Add Superball-specific fields if (thrower->config->software) { cJSON_AddStringToObject(metadata, "nip05", thrower->config->software); } // Add version and supported SUPs if (thrower->config->version) { cJSON_AddStringToObject(metadata, "display_name", thrower->config->version); } if (thrower->config->supported_sups) { cJSON_AddStringToObject(metadata, "website", thrower->config->supported_sups); } char* content = cJSON_PrintUnformatted(metadata); cJSON_Delete(metadata); if (!content) { log_message(LOG_ERROR, "Failed to create metadata JSON"); return -1; } // Create kind 0 event with empty tags cJSON* tags = cJSON_CreateArray(); cJSON* event = nostr_create_and_sign_event(0, content, tags, thrower->private_key, time(NULL)); free(content); cJSON_Delete(tags); if (!event) { log_message(LOG_ERROR, "Failed to create metadata event"); return -1; } // Get write-capable relays const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*)); int relay_count = 0; for (int i = 0; i < thrower->config->relay_count; i++) { if (thrower->config->relays[i].write && strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) { relay_urls[relay_count++] = thrower->config->relays[i].url; } } if (relay_count == 0) { log_message(LOG_WARN, "No write-capable relays for metadata"); free(relay_urls); cJSON_Delete(event); return -1; } nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count, event, publish_callback, thrower); free(relay_urls); cJSON_Delete(event); log_message(LOG_INFO, "Metadata published to %d relays", relay_count); return 0; } static int publish_relay_list(superball_thrower_t* thrower) { log_message(LOG_INFO, "Publishing relay list (kind 10002)..."); // Create tags array with relay information cJSON* tags = cJSON_CreateArray(); for (int i = 0; i < thrower->config->relay_count; i++) { cJSON* relay_tag = cJSON_CreateArray(); cJSON_AddItemToArray(relay_tag, cJSON_CreateString("r")); cJSON_AddItemToArray(relay_tag, cJSON_CreateString(thrower->config->relays[i].url)); // Add read/write markers if (thrower->config->relays[i].read && thrower->config->relays[i].write) { // Both read and write - no marker needed (default) } else if (thrower->config->relays[i].read) { cJSON_AddItemToArray(relay_tag, cJSON_CreateString("read")); } else if (thrower->config->relays[i].write) { cJSON_AddItemToArray(relay_tag, cJSON_CreateString("write")); } cJSON_AddItemToArray(tags, relay_tag); } // Create kind 10002 event with empty content cJSON* event = nostr_create_and_sign_event(10002, "", tags, thrower->private_key, time(NULL)); cJSON_Delete(tags); if (!event) { log_message(LOG_ERROR, "Failed to create relay list event"); return -1; } // Get write-capable relays const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*)); int relay_count = 0; for (int i = 0; i < thrower->config->relay_count; i++) { if (thrower->config->relays[i].write && strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) { relay_urls[relay_count++] = thrower->config->relays[i].url; } } if (relay_count == 0) { log_message(LOG_WARN, "No write-capable relays for relay list"); free(relay_urls); cJSON_Delete(event); return -1; } nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count, event, publish_callback, thrower); free(relay_urls); cJSON_Delete(event); log_message(LOG_INFO, "Relay list published to %d relays", relay_count); return 0; } static void* auto_publish_thread_func(void* arg) { superball_thrower_t* thrower = (superball_thrower_t*)arg; log_message(LOG_INFO, "Auto-publish thread started (interval: %d seconds)", thrower->config->refresh_rate); int elapsed = 0; while (thrower->auto_publish_running) { // Sleep in 1-second intervals to allow responsive shutdown sleep(1); elapsed++; if (elapsed >= thrower->config->refresh_rate && thrower->auto_publish_running) { publish_thrower_info(thrower); elapsed = 0; } } log_message(LOG_INFO, "Auto-publish thread stopped"); return NULL; } // ============================================================================ // [12] MAIN FUNCTIONS // ============================================================================ static void signal_handler(int signum) { log_message(LOG_INFO, "Received signal %d, shutting down...", signum); g_running = 0; if (g_thrower) { g_thrower->running = 0; g_thrower->auto_publish_running = 0; } } static superball_thrower_t* thrower_create(const char* config_path) { superball_thrower_t* thrower = calloc(1, sizeof(superball_thrower_t)); if (!thrower) return NULL; // Load configuration thrower->config = config_load(config_path); if (!thrower->config || !config_validate(thrower->config)) { log_message(LOG_ERROR, "Failed to load or validate configuration"); free(thrower); return NULL; } g_log_level = thrower->config->log_level; // Parse private key if (nostr_hex_to_bytes(thrower->config->private_key_hex, thrower->private_key, 32) != 0) { log_message(LOG_ERROR, "Failed to parse private key"); config_free(thrower->config); free(thrower); return NULL; } // Derive public key nostr_ec_public_key_from_private_key(thrower->private_key, thrower->public_key); char pubkey_hex[65]; nostr_bytes_to_hex(thrower->public_key, 32, pubkey_hex); log_message(LOG_INFO, "Thrower public key: %s", pubkey_hex); // Create relay pool thrower->pool = nostr_relay_pool_create(NULL); if (!thrower->pool) { log_message(LOG_ERROR, "Failed to create relay pool"); config_free(thrower->config); free(thrower); return NULL; } // Add relays to pool for (int i = 0; i < thrower->config->relay_count; i++) { nostr_relay_pool_add_relay(thrower->pool, thrower->config->relays[i].url); log_message(LOG_INFO, "Added relay: %s", thrower->config->relays[i].url); } // Create event queue thrower->queue = queue_create(thrower->config->max_queue_size); if (!thrower->queue) { log_message(LOG_ERROR, "Failed to create event queue"); nostr_relay_pool_destroy(thrower->pool); config_free(thrower->config); free(thrower); return NULL; } thrower->running = 1; thrower->auto_publish_running = 0; thrower->processed_events = 0; return thrower; } static int thrower_start(superball_thrower_t* thrower) { log_message(LOG_INFO, "Starting Superball Thrower daemon..."); // Test relay authentication relay_test_all(thrower); // Start queue processor thread if (pthread_create(&thrower->queue_processor_thread, NULL, queue_processor_thread_func, thrower) != 0) { log_message(LOG_ERROR, "Failed to create queue processor thread"); return -1; } // Subscribe to routing events char pubkey_hex[65]; nostr_bytes_to_hex(thrower->public_key, 32, pubkey_hex); cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(22222)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON* p_tags = cJSON_CreateArray(); cJSON_AddItemToArray(p_tags, cJSON_CreateString(pubkey_hex)); cJSON_AddItemToObject(filter, "#p", p_tags); cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(time(NULL))); const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*)); for (int i = 0; i < thrower->config->relay_count; i++) { relay_urls[i] = thrower->config->relays[i].url; } nostr_relay_pool_subscribe(thrower->pool, relay_urls, thrower->config->relay_count, filter, on_routing_event, on_eose, thrower, 0, 1, NOSTR_POOL_EOSE_FULL_SET, 30, 60); free(relay_urls); cJSON_Delete(filter); log_message(LOG_INFO, "Monitoring %d relays for routing events", thrower->config->relay_count); // Publish initial metadata and relay list publish_metadata(thrower); publish_relay_list(thrower); // Publish initial thrower info publish_thrower_info(thrower); // Start auto-publish thread thrower->auto_publish_running = 1; if (pthread_create(&thrower->auto_publish_thread, NULL, auto_publish_thread_func, thrower) != 0) { log_message(LOG_ERROR, "Failed to create auto-publish thread"); return -1; } log_message(LOG_INFO, "Superball Thrower daemon started successfully"); return 0; } static void thrower_stop(superball_thrower_t* thrower) { if (!thrower) return; log_message(LOG_INFO, "Stopping Superball Thrower daemon..."); thrower->running = 0; thrower->auto_publish_running = 0; // Wait for threads pthread_join(thrower->queue_processor_thread, NULL); pthread_join(thrower->auto_publish_thread, NULL); log_message(LOG_INFO, "Superball Thrower daemon stopped (processed %d events)", thrower->processed_events); } static void thrower_destroy(superball_thrower_t* thrower) { if (!thrower) return; if (thrower->pool) nostr_relay_pool_destroy(thrower->pool); if (thrower->queue) queue_destroy(thrower->queue); if (thrower->config) config_free(thrower->config); free(thrower); } // ============================================================================ // MAIN // ============================================================================ int main(int argc, char* argv[]) { const char* config_path = CONFIG_FILE; // Parse command line arguments if (argc > 1) { if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) { printf("Superball Thrower - C Implementation\n\n"); printf("Usage: %s [config_file]\n\n", argv[0]); printf("Options:\n"); printf(" config_file Path to configuration file (default: config.json)\n"); printf(" --help, -h Show this help message\n\n"); return 0; } config_path = argv[1]; } // Initialize crypto nostr_crypto_init(); // Seed random number generator srand(time(NULL)); // Setup signal handlers signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); // Create thrower g_thrower = thrower_create(config_path); if (!g_thrower) { log_message(LOG_ERROR, "Failed to create thrower"); nostr_crypto_cleanup(); return 1; } // Start thrower if (thrower_start(g_thrower) != 0) { log_message(LOG_ERROR, "Failed to start thrower"); thrower_destroy(g_thrower); nostr_crypto_cleanup(); return 1; } // Main event loop while (g_running) { nostr_relay_pool_poll(g_thrower->pool, 1000); } // Cleanup thrower_stop(g_thrower); thrower_destroy(g_thrower); nostr_crypto_cleanup(); log_message(LOG_INFO, "Shutdown complete"); return 0; }