Files
super_ball_thrower/main.c
2025-12-10 10:17:24 -04:00

1180 lines
40 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
// nostr_core_lib headers
#include "nostr_core.h"
#include "cJSON.h"
// Version
#define THROWER_VERSION "v0.0.1"
// 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 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;
}
// 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) {
log_message(LOG_INFO, "Posting final event to %d relays", routing->relay_count);
// Publish the inner event directly
nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays,
routing->relay_count, event,
publish_callback, thrower);
cJSON* id = cJSON_GetObjectItem(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) {
(void)user_data; // Suppress unused parameter warning
if (success) {
log_message(LOG_INFO, "✅ Published to %s: %.16s...", relay_url, event_id);
} else {
log_message(LOG_ERROR, "❌ Failed to publish to %s: %s", relay_url, message ? message : "unknown error");
}
}
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 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 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;
}