diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45376a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nostr_core_lib/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..84dd331 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "nostr_core_lib"] + path = nostr_core_lib + url = https://git.laantungir.net/laantungir/nostr_core_lib.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6834d5c --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +# C-Relay Makefile + +CC = gcc +CFLAGS = -Wall -Wextra -std=c99 -g -O2 +INCLUDES = -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket +LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl + +# Source files +MAIN_SRC = src/main.c +NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a + +# Target binary +TARGET = src/main + +# Default target +all: $(TARGET) + +# Check if nostr_core_lib is built +$(NOSTR_CORE_LIB): + @echo "Building nostr_core_lib..." + cd nostr_core_lib && ./build.sh + +# Build the relay +$(TARGET): $(MAIN_SRC) $(NOSTR_CORE_LIB) + @echo "Compiling C-Relay..." + $(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS) + @echo "Build complete: $(TARGET)" + +# Run tests +test: $(TARGET) + @echo "Running tests..." + ./tests/1_nip_test.sh + +# Initialize database +init-db: + @echo "Initializing database..." + ./db/init.sh --force + +# Clean build artifacts +clean: + rm -f $(TARGET) + @echo "Clean complete" + +# Clean everything including nostr_core_lib +clean-all: clean + cd nostr_core_lib && make clean 2>/dev/null || true + +# Install dependencies (Ubuntu/Debian) +install-deps: + @echo "Installing dependencies..." + sudo apt update + sudo apt install -y build-essential libsqlite3-dev libssl-dev libcurl4-openssl-dev libsecp256k1-dev zlib1g-dev jq curl + +# Help +help: + @echo "C-Relay Build System" + @echo "" + @echo "Targets:" + @echo " all Build the relay (default)" + @echo " test Build and run tests" + @echo " init-db Initialize the database" + @echo " clean Clean build artifacts" + @echo " clean-all Clean everything including dependencies" + @echo " install-deps Install system dependencies" + @echo " help Show this help" + @echo "" + @echo "Usage:" + @echo " make # Build the relay" + @echo " make test # Run tests" + @echo " make init-db # Set up database" + +.PHONY: all test init-db clean clean-all install-deps help \ No newline at end of file diff --git a/README.md b/README.md index 40f8490..cf19cbd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -A nostr relay in C. +A nostr relay in C with sqlite on the back end. + + diff --git a/db/README.md b/db/README.md new file mode 100644 index 0000000..d4080cb --- /dev/null +++ b/db/README.md @@ -0,0 +1,228 @@ +# C Nostr Relay Database + +This directory contains the SQLite database schema and initialization scripts for the C Nostr Relay implementation. + +## Files + +- **`schema.sql`** - Complete database schema based on nostr-rs-relay v18 +- **`init.sh`** - Database initialization script +- **`c_nostr_relay.db`** - SQLite database file (created after running init.sh) + +## Quick Start + +1. **Initialize the database:** + ```bash + cd db + ./init.sh + ``` + +2. **Force reinitialize (removes existing database):** + ```bash + ./init.sh --force + ``` + +3. **Initialize with optimization and info:** + ```bash + ./init.sh --info --optimize + ``` + +## Database Schema + +The schema is fully compatible with the Nostr protocol and includes: + +### Core Tables + +- **`event`** - Main event storage with all Nostr event data +- **`tag`** - Denormalized tag index for efficient queries +- **`user_verification`** - NIP-05 verification tracking +- **`account`** - User account management (optional) +- **`invoice`** - Lightning payment tracking (optional) + +### Key Features + +- ✅ **NIP-01 compliant** - Full basic protocol support +- ✅ **Replaceable events** - Supports kinds 0, 3, 10000-19999 +- ✅ **Parameterized replaceable** - Supports kinds 30000-39999 with `d` tags +- ✅ **Event deletion** - NIP-09 soft deletion with `hidden` column +- ✅ **Event expiration** - NIP-40 automatic cleanup +- ✅ **Authentication** - NIP-42 client authentication +- ✅ **NIP-05 verification** - Domain-based identity verification +- ✅ **Performance optimized** - Comprehensive indexing strategy + +### Schema Version + +Current version: **v18** (compatible with nostr-rs-relay v18) + +## Database Structure + +### Event Storage +```sql +CREATE TABLE event ( + id INTEGER PRIMARY KEY, + event_hash BLOB NOT NULL, -- 32-byte SHA256 hash + first_seen INTEGER NOT NULL, -- relay receive timestamp + created_at INTEGER NOT NULL, -- event creation timestamp + expires_at INTEGER, -- NIP-40 expiration + author BLOB NOT NULL, -- 32-byte pubkey + delegated_by BLOB, -- NIP-26 delegator + kind INTEGER NOT NULL, -- event kind + hidden INTEGER DEFAULT FALSE, -- soft deletion flag + content TEXT NOT NULL -- complete JSON event +); +``` + +### Tag Indexing +```sql +CREATE TABLE tag ( + id INTEGER PRIMARY KEY, + event_id INTEGER NOT NULL, + name TEXT, -- tag name ("e", "p", etc.) + value TEXT, -- tag value + created_at INTEGER NOT NULL, -- denormalized for performance + kind INTEGER NOT NULL -- denormalized for performance +); +``` + +## Performance Features + +### Optimized Indexes +- **Hash-based lookups** - `event_hash_index` for O(1) event retrieval +- **Author queries** - `author_index`, `author_created_at_index` +- **Kind filtering** - `kind_index`, `kind_created_at_index` +- **Tag searching** - `tag_covering_index` for efficient tag queries +- **Composite queries** - Multi-column indexes for complex filters + +### Query Optimization +- **Denormalized tags** - Includes `kind` and `created_at` in tag table +- **Binary storage** - BLOBs for hex data (pubkeys, hashes) +- **WAL mode** - Write-Ahead Logging for concurrent access +- **Automatic cleanup** - Triggers for data integrity + +## Usage Examples + +### Basic Operations + +1. **Insert an event:** + ```sql + INSERT INTO event (event_hash, first_seen, created_at, author, kind, content) + VALUES (?, ?, ?, ?, ?, ?); + ``` + +2. **Query by author:** + ```sql + SELECT content FROM event + WHERE author = ? AND hidden != TRUE + ORDER BY created_at DESC; + ``` + +3. **Filter by tags:** + ```sql + SELECT e.content FROM event e + JOIN tag t ON e.id = t.event_id + WHERE t.name = 'p' AND t.value = ? AND e.hidden != TRUE; + ``` + +### Advanced Queries + +1. **Get replaceable event (latest only):** + ```sql + SELECT content FROM event + WHERE author = ? AND kind = ? AND hidden != TRUE + ORDER BY created_at DESC LIMIT 1; + ``` + +2. **Tag-based filtering (NIP-01 filters):** + ```sql + SELECT e.content FROM event e + WHERE e.id IN ( + SELECT t.event_id FROM tag t + WHERE t.name = ? AND t.value IN (?, ?, ?) + ) AND e.hidden != TRUE; + ``` + +## Maintenance + +### Regular Operations + +1. **Check database integrity:** + ```bash + sqlite3 c_nostr_relay.db "PRAGMA integrity_check;" + ``` + +2. **Optimize database:** + ```bash + sqlite3 c_nostr_relay.db "PRAGMA optimize; VACUUM; ANALYZE;" + ``` + +3. **Clean expired events:** + ```sql + DELETE FROM event WHERE expires_at <= strftime('%s', 'now'); + ``` + +### Monitoring + +1. **Database size:** + ```bash + ls -lh c_nostr_relay.db + ``` + +2. **Table statistics:** + ```sql + SELECT name, COUNT(*) as count FROM ( + SELECT 'events' as name FROM event UNION ALL + SELECT 'tags' as name FROM tag UNION ALL + SELECT 'verifications' as name FROM user_verification + ) GROUP BY name; + ``` + +## Migration Support + +The schema includes a migration system for future updates: + +```sql +CREATE TABLE schema_info ( + version INTEGER PRIMARY KEY, + applied_at INTEGER NOT NULL, + description TEXT +); +``` + +## Security Considerations + +1. **Input validation** - Always validate event JSON and signatures +2. **Rate limiting** - Implement at application level +3. **Access control** - Use `account` table for permissions +4. **Backup strategy** - Regular database backups recommended + +## Compatibility + +- **SQLite version** - Requires SQLite 3.8.0+ +- **nostr-rs-relay** - Schema compatible with v18 +- **NIPs supported** - 01, 02, 05, 09, 10, 11, 26, 40, 42 +- **C libraries** - Compatible with sqlite3 C API + +## Troubleshooting + +### Common Issues + +1. **Database locked error:** + - Ensure proper connection closing in your C code + - Check for long-running transactions + +2. **Performance issues:** + - Run `PRAGMA optimize;` regularly + - Consider `VACUUM` if database grew significantly + +3. **Schema errors:** + - Verify SQLite version compatibility + - Check foreign key constraints + +### Getting Help + +- Check the main project README for C implementation details +- Review nostr-rs-relay documentation for reference implementation +- Consult Nostr NIPs for protocol specifications + +## License + +This database schema is part of the C Nostr Relay project and follows the same license terms. \ No newline at end of file diff --git a/db/c_nostr_relay.db b/db/c_nostr_relay.db new file mode 100644 index 0000000..caf0336 Binary files /dev/null and b/db/c_nostr_relay.db differ diff --git a/db/c_nostr_relay.db-shm b/db/c_nostr_relay.db-shm new file mode 100644 index 0000000..c58baa3 Binary files /dev/null and b/db/c_nostr_relay.db-shm differ diff --git a/db/c_nostr_relay.db-wal b/db/c_nostr_relay.db-wal new file mode 100644 index 0000000..b1cb61c Binary files /dev/null and b/db/c_nostr_relay.db-wal differ diff --git a/db/init.sh b/db/init.sh new file mode 100755 index 0000000..4403b00 --- /dev/null +++ b/db/init.sh @@ -0,0 +1,234 @@ +#!/bin/bash + +# C Nostr Relay Database Initialization Script +# Creates and initializes the SQLite database with proper schema + +set -e # Exit on any error + +# Configuration +DB_DIR="$(dirname "$0")" +DB_NAME="c_nostr_relay.db" +DB_PATH="${DB_DIR}/${DB_NAME}" +SCHEMA_FILE="${DB_DIR}/schema.sql" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if SQLite3 is installed +check_sqlite() { + if ! command -v sqlite3 &> /dev/null; then + log_error "sqlite3 is not installed. Please install it first:" + echo " Ubuntu/Debian: sudo apt-get install sqlite3" + echo " CentOS/RHEL: sudo yum install sqlite" + echo " macOS: brew install sqlite3" + exit 1 + fi + + local version=$(sqlite3 --version | cut -d' ' -f1) + log_info "Using SQLite version: $version" +} + +# Create database directory if it doesn't exist +create_db_directory() { + if [ ! -d "$DB_DIR" ]; then + log_info "Creating database directory: $DB_DIR" + mkdir -p "$DB_DIR" + fi +} + +# Backup existing database if it exists +backup_existing_db() { + if [ -f "$DB_PATH" ]; then + local backup_path="${DB_PATH}.backup.$(date +%Y%m%d_%H%M%S)" + log_warning "Existing database found. Creating backup: $backup_path" + cp "$DB_PATH" "$backup_path" + fi +} + +# Initialize the database with schema +init_database() { + log_info "Initializing database: $DB_PATH" + + if [ ! -f "$SCHEMA_FILE" ]; then + log_error "Schema file not found: $SCHEMA_FILE" + exit 1 + fi + + # Remove existing database if --force flag is used + if [ "$1" = "--force" ] && [ -f "$DB_PATH" ]; then + log_warning "Force flag detected. Removing existing database." + rm -f "$DB_PATH" + fi + + # Create the database and apply schema + log_info "Applying schema from: $SCHEMA_FILE" + if sqlite3 "$DB_PATH" < "$SCHEMA_FILE"; then + log_success "Database schema applied successfully" + else + log_error "Failed to apply database schema" + exit 1 + fi +} + +# Verify database integrity +verify_database() { + log_info "Verifying database integrity..." + + # Check if database file exists and is not empty + if [ ! -s "$DB_PATH" ]; then + log_error "Database file is empty or doesn't exist" + exit 1 + fi + + # Run SQLite integrity check + local integrity_result=$(sqlite3 "$DB_PATH" "PRAGMA integrity_check;") + if [ "$integrity_result" = "ok" ]; then + log_success "Database integrity check passed" + else + log_error "Database integrity check failed: $integrity_result" + exit 1 + fi + + # Verify schema version + local schema_version=$(sqlite3 "$DB_PATH" "PRAGMA user_version;") + log_info "Database schema version: $schema_version" + + # Check that main tables exist + local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('event', 'tag');") + if [ "$table_count" -eq 2 ]; then + log_success "Core tables created successfully" + else + log_error "Missing core tables (expected 2, found $table_count)" + exit 1 + fi +} + +# Display database information +show_db_info() { + log_info "Database Information:" + echo " Location: $DB_PATH" + echo " Size: $(du -h "$DB_PATH" | cut -f1)" + + log_info "Database Tables:" + sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" | sed 's/^/ - /' + + log_info "Database Indexes:" + sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name;" | sed 's/^/ - /' + + log_info "Database Views:" + sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='view' ORDER BY name;" | sed 's/^/ - /' +} + +# Run database optimization +optimize_database() { + log_info "Running database optimization..." + sqlite3 "$DB_PATH" "PRAGMA optimize; VACUUM; ANALYZE;" + log_success "Database optimization completed" +} + +# Print usage information +print_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Initialize SQLite database for C Nostr Relay" + echo "" + echo "Options:" + echo " --force Remove existing database before initialization" + echo " --info Show database information after initialization" + echo " --optimize Run database optimization after initialization" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Initialize database (with backup if exists)" + echo " $0 --force # Force reinitialize database" + echo " $0 --info --optimize # Initialize with info and optimization" +} + +# Main execution +main() { + local force_flag=false + local show_info=false + local optimize=false + + # Parse command line arguments + while [[ $# -gt 0 ]]; do + case $1 in + --force) + force_flag=true + shift + ;; + --info) + show_info=true + shift + ;; + --optimize) + optimize=true + shift + ;; + --help) + print_usage + exit 0 + ;; + *) + log_error "Unknown option: $1" + print_usage + exit 1 + ;; + esac + done + + log_info "Starting C Nostr Relay database initialization..." + + # Execute initialization steps + check_sqlite + create_db_directory + + if [ "$force_flag" = false ]; then + backup_existing_db + fi + + if [ "$force_flag" = true ]; then + init_database --force + else + init_database + fi + + verify_database + + if [ "$optimize" = true ]; then + optimize_database + fi + + if [ "$show_info" = true ]; then + show_db_info + fi + + log_success "Database initialization completed successfully!" + echo "" + echo "Database ready at: $DB_PATH" + echo "You can now start your C Nostr Relay application." +} + +# Execute main function with all arguments +main "$@" \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..881376c --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,66 @@ +-- C Nostr Relay Database Schema +-- Simplified schema with just event and tag tables +-- SQLite database for storing Nostr events + +-- ============================================================================ +-- DATABASE SETTINGS +-- ============================================================================ + +PRAGMA encoding = "UTF-8"; +PRAGMA journal_mode = WAL; +PRAGMA auto_vacuum = FULL; +PRAGMA synchronous = NORMAL; +PRAGMA foreign_keys = ON; + +-- ============================================================================ +-- EVENT TABLE +-- ============================================================================ + +-- Main event table - stores all Nostr events +CREATE TABLE IF NOT EXISTS event ( + id TEXT PRIMARY KEY, -- Nostr event ID (hex string) + pubkey TEXT NOT NULL, -- Public key of event author (hex string) + created_at INTEGER NOT NULL, -- Event creation timestamp (Unix timestamp) + kind INTEGER NOT NULL, -- Event kind (0-65535) + content TEXT NOT NULL, -- Event content (text content only) + sig TEXT NOT NULL -- Event signature (hex string) +); + +-- Event indexes for performance +CREATE INDEX IF NOT EXISTS event_pubkey_index ON event(pubkey); +CREATE INDEX IF NOT EXISTS event_created_at_index ON event(created_at); +CREATE INDEX IF NOT EXISTS event_kind_index ON event(kind); +CREATE INDEX IF NOT EXISTS event_pubkey_created_at_index ON event(pubkey, created_at); +CREATE INDEX IF NOT EXISTS event_kind_created_at_index ON event(kind, created_at); + +-- ============================================================================ +-- TAG TABLE +-- ============================================================================ + +-- Tag table for storing event tags +CREATE TABLE IF NOT EXISTS tag ( + id TEXT NOT NULL, -- Nostr event ID (references event.id) + name TEXT NOT NULL, -- Tag name (e.g., "e", "p", "d") + value TEXT NOT NULL, -- Tag value + parameters TEXT, -- Additional tag parameters (JSON string) + FOREIGN KEY(id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Tag indexes for performance +CREATE INDEX IF NOT EXISTS tag_id_index ON tag(id); +CREATE INDEX IF NOT EXISTS tag_name_index ON tag(name); +CREATE INDEX IF NOT EXISTS tag_name_value_index ON tag(name, value); +CREATE INDEX IF NOT EXISTS tag_id_name_index ON tag(id, name); + +-- ============================================================================ +-- PERFORMANCE OPTIMIZATIONS +-- ============================================================================ + +-- Enable query planner optimizations +PRAGMA optimize; + +-- Set recommended pragmas for performance +PRAGMA main.synchronous = NORMAL; +PRAGMA foreign_keys = ON; +PRAGMA temp_store = 2; -- use memory for temp tables +PRAGMA main.cache_size = 10000; -- 40MB cache per connection \ No newline at end of file diff --git a/make_and_restart_relay.sh b/make_and_restart_relay.sh new file mode 100755 index 0000000..2c472e8 --- /dev/null +++ b/make_and_restart_relay.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# C-Relay Build and Restart Script +# Builds the project first, then stops any running relay and starts a new one in the background + +echo "=== C Nostr Relay Build and Restart Script ===" + +# Build the project first +echo "Building project..." +make clean all + +# Check if build was successful +if [ $? -ne 0 ]; then + echo "ERROR: Build failed. Cannot restart relay." + exit 1 +fi + +# Check if relay binary exists after build +if [ ! -f "./src/main" ]; then + echo "ERROR: Relay binary not found after build. Build may have failed." + exit 1 +fi + +echo "Build successful. Proceeding with relay restart..." + +# Kill existing relay if running +echo "Stopping any existing relay servers..." +pkill -f "./src/main" 2>/dev/null +sleep 2 # Give time for shutdown + +# Check if port is still bound +if lsof -i :8888 >/dev/null 2>&1; then + echo "Port 8888 still in use, force killing..." + fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888" +fi + +# Get any remaining processes +REMAINING_PIDS=$(pgrep -f "./src/main" || echo "") +if [ -n "$REMAINING_PIDS" ]; then + echo "Force killing remaining processes: $REMAINING_PIDS" + kill -9 $REMAINING_PIDS 2>/dev/null + sleep 1 +else + echo "No existing relay found" +fi + +# Clean up PID file +rm -f relay.pid + +# Initialize database if needed +if [ ! -f "./db/c_nostr_relay.db" ]; then + echo "Initializing database..." + ./db/init.sh --force >/dev/null 2>&1 +fi + +# Start relay in background with output redirection +echo "Starting relay server..." +echo "Debug: Current processes: $(ps aux | grep './src/main' | grep -v grep || echo 'None')" + +# Start relay in background and capture its PID +./src/main > relay.log 2>&1 & +RELAY_PID=$! + +echo "Started with PID: $RELAY_PID" + +# Check if server is still running after short delay +sleep 3 + +# Check if process is still alive +if ps -p "$RELAY_PID" >/dev/null 2>&1; then + echo "Relay started successfully!" + echo "PID: $RELAY_PID" + echo "WebSocket endpoint: ws://127.0.0.1:8888" + echo "Log file: relay.log" + echo "" + + # Save PID for debugging + echo $RELAY_PID > relay.pid + + echo "=== Relay server running in background ===" + echo "To kill relay: pkill -f './src/main'" + echo "To check status: ps aux | grep src/main" + echo "To view logs: tail -f relay.log" + echo "Ready for Nostr client connections!" +else + echo "ERROR: Relay failed to start" + echo "Debug: Check relay.log for error details:" + echo "--- Last 10 lines of relay.log ---" + tail -n 10 relay.log 2>/dev/null || echo "No log file found" + echo "--- End log ---" + exit 1 +fi + +echo "" \ No newline at end of file diff --git a/nips b/nips new file mode 160000 index 0000000..8c45ff5 --- /dev/null +++ b/nips @@ -0,0 +1 @@ +Subproject commit 8c45ff5d964d6d9bf329c72713e43c89c060de09 diff --git a/nostr_core_lib b/nostr_core_lib new file mode 160000 index 0000000..33129d8 --- /dev/null +++ b/nostr_core_lib @@ -0,0 +1 @@ +Subproject commit 33129d82fdce8cff280bc0b5ba7ed5e49531606d diff --git a/relay.log b/relay.log new file mode 100644 index 0000000..41e8a3b --- /dev/null +++ b/relay.log @@ -0,0 +1,11 @@ +=== C Nostr Relay Server === +[SUCCESS] Database connection established +[INFO] Starting relay server... +[INFO] Starting libwebsockets-based Nostr relay server... +[SUCCESS] WebSocket relay started on ws://127.0.0.1:8888 +[INFO] WebSocket connection established +[INFO] Received WebSocket message +[INFO] Handling EVENT message +[SUCCESS] Event stored in database +[SUCCESS] Event stored successfully +[INFO] WebSocket connection closed diff --git a/relay.pid b/relay.pid new file mode 100644 index 0000000..cb155a4 --- /dev/null +++ b/relay.pid @@ -0,0 +1 @@ +320933 diff --git a/src/main b/src/main new file mode 100755 index 0000000..81bd73b Binary files /dev/null and b/src/main differ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..ec4e68e --- /dev/null +++ b/src/main.c @@ -0,0 +1,562 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Include nostr_core_lib for Nostr functionality +#include "../nostr_core_lib/cjson/cJSON.h" +#include "../nostr_core_lib/nostr_core/nostr_core.h" + +// Configuration +#define DEFAULT_PORT 8888 +#define DEFAULT_HOST "127.0.0.1" +#define DATABASE_PATH "db/c_nostr_relay.db" +#define MAX_CLIENTS 100 + +// Global state +static sqlite3* g_db = NULL; +static int g_server_running = 1; + +// Color constants for logging +#define RED "\033[31m" +#define GREEN "\033[32m" +#define YELLOW "\033[33m" +#define BLUE "\033[34m" +#define BOLD "\033[1m" +#define RESET "\033[0m" + +// Logging functions +void log_info(const char* message) { + printf(BLUE "[INFO]" RESET " %s\n", message); + fflush(stdout); +} + +void log_success(const char* message) { + printf(GREEN "[SUCCESS]" RESET " %s\n", message); + fflush(stdout); +} + +void log_error(const char* message) { + printf(RED "[ERROR]" RESET " %s\n", message); + fflush(stdout); +} + +void log_warning(const char* message) { + printf(YELLOW "[WARNING]" RESET " %s\n", message); + fflush(stdout); +} + +// Signal handler for graceful shutdown +void signal_handler(int sig) { + if (sig == SIGINT || sig == SIGTERM) { + log_info("Received shutdown signal"); + g_server_running = 0; + } +} + +// Initialize database connection +int init_database() { + int rc = sqlite3_open(DATABASE_PATH, &g_db); + if (rc != SQLITE_OK) { + log_error("Cannot open database"); + return -1; + } + + log_success("Database connection established"); + return 0; +} + +// Close database connection +void close_database() { + if (g_db) { + sqlite3_close(g_db); + g_db = NULL; + log_info("Database connection closed"); + } +} + +// Store event in database +int store_event(cJSON* event) { + if (!g_db || !event) { + return -1; + } + + // Extract event fields + cJSON* id = cJSON_GetObjectItem(event, "id"); + cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey"); + cJSON* created_at = cJSON_GetObjectItem(event, "created_at"); + cJSON* kind = cJSON_GetObjectItem(event, "kind"); + cJSON* content = cJSON_GetObjectItem(event, "content"); + cJSON* sig = cJSON_GetObjectItem(event, "sig"); + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + + if (!id || !pubkey || !created_at || !kind || !content || !sig) { + log_error("Invalid event - missing required fields"); + return -1; + } + + // Prepare SQL statement for event insertion + const char* sql = + "INSERT INTO event (id, pubkey, created_at, kind, content, sig) " + "VALUES (?, ?, ?, ?, ?, ?)"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + log_error("Failed to prepare event insert statement"); + return -1; + } + + // Bind parameters + sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey), -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at)); + sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind)); + sqlite3_bind_text(stmt, 5, cJSON_GetStringValue(content), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(sig), -1, SQLITE_STATIC); + + // Execute statement + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + if (rc == SQLITE_CONSTRAINT) { + log_warning("Event already exists in database"); + return 0; // Not an error, just duplicate + } + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", sqlite3_errmsg(g_db)); + log_error(error_msg); + return -1; + } + + // Insert tags if present + if (tags && cJSON_IsArray(tags)) { + const char* event_id = cJSON_GetStringValue(id); + cJSON* tag; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + + if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) { + // Collect additional tag parameters if present + char* parameters = NULL; + if (cJSON_GetArraySize(tag) > 2) { + cJSON* params_array = cJSON_CreateArray(); + for (int i = 2; i < cJSON_GetArraySize(tag); i++) { + cJSON_AddItemToArray(params_array, cJSON_Duplicate(cJSON_GetArrayItem(tag, i), 1)); + } + parameters = cJSON_Print(params_array); + cJSON_Delete(params_array); + } + + const char* tag_sql = + "INSERT INTO tag (id, name, value, parameters) VALUES (?, ?, ?, ?)"; + + sqlite3_stmt* tag_stmt; + rc = sqlite3_prepare_v2(g_db, tag_sql, -1, &tag_stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_text(tag_stmt, 1, event_id, -1, SQLITE_STATIC); + sqlite3_bind_text(tag_stmt, 2, cJSON_GetStringValue(tag_name), -1, SQLITE_STATIC); + sqlite3_bind_text(tag_stmt, 3, cJSON_GetStringValue(tag_value), -1, SQLITE_STATIC); + sqlite3_bind_text(tag_stmt, 4, parameters, -1, SQLITE_TRANSIENT); + + sqlite3_step(tag_stmt); + sqlite3_finalize(tag_stmt); + } + + if (parameters) free(parameters); + } + } + } + } + + log_success("Event stored in database"); + return 0; +} + +// Retrieve event from database +cJSON* retrieve_event(const char* event_id) { + if (!g_db || !event_id) { + return NULL; + } + + const char* sql = + "SELECT id, pubkey, created_at, kind, content, sig FROM event WHERE id = ?"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return NULL; + } + + sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); + + cJSON* event = NULL; + if (sqlite3_step(stmt) == SQLITE_ROW) { + event = cJSON_CreateObject(); + + cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0)); + cJSON_AddStringToObject(event, "pubkey", (char*)sqlite3_column_text(stmt, 1)); + cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); + cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); + cJSON_AddStringToObject(event, "content", (char*)sqlite3_column_text(stmt, 4)); + cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5)); + + // Add tags array - retrieve from tag table + cJSON* tags_array = cJSON_CreateArray(); + + const char* tag_sql = "SELECT name, value, parameters FROM tag WHERE id = ?"; + sqlite3_stmt* tag_stmt; + if (sqlite3_prepare_v2(g_db, tag_sql, -1, &tag_stmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(tag_stmt, 1, event_id, -1, SQLITE_STATIC); + + while (sqlite3_step(tag_stmt) == SQLITE_ROW) { + cJSON* tag = cJSON_CreateArray(); + cJSON_AddItemToArray(tag, cJSON_CreateString((char*)sqlite3_column_text(tag_stmt, 0))); + cJSON_AddItemToArray(tag, cJSON_CreateString((char*)sqlite3_column_text(tag_stmt, 1))); + + // Add parameters if they exist + const char* parameters = (char*)sqlite3_column_text(tag_stmt, 2); + if (parameters && strlen(parameters) > 0) { + cJSON* params = cJSON_Parse(parameters); + if (params && cJSON_IsArray(params)) { + int param_count = cJSON_GetArraySize(params); + for (int i = 0; i < param_count; i++) { + cJSON* param = cJSON_GetArrayItem(params, i); + cJSON_AddItemToArray(tag, cJSON_Duplicate(param, 1)); + } + } + if (params) cJSON_Delete(params); + } + + cJSON_AddItemToArray(tags_array, tag); + } + sqlite3_finalize(tag_stmt); + } + + cJSON_AddItemToObject(event, "tags", tags_array); + } + + sqlite3_finalize(stmt); + return event; +} + +// Handle REQ message (subscription) +int handle_req_message(const char* sub_id, cJSON* filters) { + log_info("Handling REQ message"); + + // For now, just handle simple event ID requests + if (cJSON_IsArray(filters)) { + cJSON* filter = cJSON_GetArrayItem(filters, 0); + if (filter) { + cJSON* ids = cJSON_GetObjectItem(filter, "ids"); + if (ids && cJSON_IsArray(ids)) { + cJSON* event_id = cJSON_GetArrayItem(ids, 0); + if (event_id && cJSON_IsString(event_id)) { + cJSON* event = retrieve_event(cJSON_GetStringValue(event_id)); + if (event) { + log_success("Found event for subscription"); + cJSON_Delete(event); + return 1; // Found event + } + } + } + } + } + + return 0; // No events found +} + +// Handle EVENT message (publish) +int handle_event_message(cJSON* event) { + log_info("Handling EVENT message"); + + // Validate event structure (basic check) + cJSON* id = cJSON_GetObjectItem(event, "id"); + if (!id || !cJSON_IsString(id)) { + log_error("Invalid event - no ID"); + return -1; + } + + // Store event in database + if (store_event(event) == 0) { + log_success("Event stored successfully"); + return 0; + } + + return -1; +} + +// Global WebSocket context +static struct lws_context *ws_context = NULL; + +// Per-session data structure +struct per_session_data { + int authenticated; + char subscription_id[64]; +}; + +// WebSocket callback function for Nostr relay protocol +static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) { + struct per_session_data *pss = (struct per_session_data *)user; + + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: + log_info("WebSocket connection established"); + memset(pss, 0, sizeof(*pss)); + break; + + case LWS_CALLBACK_RECEIVE: + if (len > 0) { + char *message = malloc(len + 1); + if (message) { + memcpy(message, in, len); + message[len] = '\0'; + + log_info("Received WebSocket message"); + + // Parse JSON message + cJSON* json = cJSON_Parse(message); + if (json && cJSON_IsArray(json)) { + // Get message type + cJSON* type = cJSON_GetArrayItem(json, 0); + if (type && cJSON_IsString(type)) { + const char* msg_type = cJSON_GetStringValue(type); + + if (strcmp(msg_type, "EVENT") == 0) { + // Handle EVENT message + cJSON* event = cJSON_GetArrayItem(json, 1); + if (event && cJSON_IsObject(event)) { + int result = handle_event_message(event); + + // Send OK response + cJSON* event_id = cJSON_GetObjectItem(event, "id"); + if (event_id && cJSON_IsString(event_id)) { + cJSON* response = cJSON_CreateArray(); + cJSON_AddItemToArray(response, cJSON_CreateString("OK")); + cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id))); + cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0)); + cJSON_AddItemToArray(response, cJSON_CreateString(result == 0 ? "" : "error: failed to store event")); + + char *response_str = cJSON_Print(response); + if (response_str) { + size_t response_len = strlen(response_str); + unsigned char *buf = malloc(LWS_PRE + response_len); + if (buf) { + memcpy(buf + LWS_PRE, response_str, response_len); + lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT); + free(buf); + } + free(response_str); + } + cJSON_Delete(response); + } + } + } else if (strcmp(msg_type, "REQ") == 0) { + // Handle REQ message + cJSON* sub_id = cJSON_GetArrayItem(json, 1); + cJSON* filters = cJSON_GetArrayItem(json, 2); + + if (sub_id && cJSON_IsString(sub_id)) { + const char* subscription_id = cJSON_GetStringValue(sub_id); + strncpy(pss->subscription_id, subscription_id, sizeof(pss->subscription_id) - 1); + + handle_req_message(subscription_id, filters); + + // Send EOSE (End of Stored Events) + cJSON* eose_response = cJSON_CreateArray(); + cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); + cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); + + char *eose_str = cJSON_Print(eose_response); + if (eose_str) { + size_t eose_len = strlen(eose_str); + unsigned char *buf = malloc(LWS_PRE + eose_len); + if (buf) { + memcpy(buf + LWS_PRE, eose_str, eose_len); + lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT); + free(buf); + } + free(eose_str); + } + cJSON_Delete(eose_response); + } + } else if (strcmp(msg_type, "CLOSE") == 0) { + // Handle CLOSE message + log_info("Subscription closed"); + } + } + } + + if (json) cJSON_Delete(json); + free(message); + } + } + break; + + case LWS_CALLBACK_CLOSED: + log_info("WebSocket connection closed"); + break; + + default: + break; + } + + return 0; +} + +// WebSocket protocol definition +static struct lws_protocols protocols[] = { + { + "nostr-relay-protocol", + nostr_relay_callback, + sizeof(struct per_session_data), + 4096, // rx buffer size + 0, NULL, 0 + }, + { NULL, NULL, 0, 0, 0, NULL, 0 } // terminator +}; + +// Start libwebsockets-based WebSocket Nostr relay server +int start_websocket_relay() { + struct lws_context_creation_info info; + + log_info("Starting libwebsockets-based Nostr relay server..."); + + memset(&info, 0, sizeof(info)); + info.port = DEFAULT_PORT; + info.protocols = protocols; + info.gid = -1; + info.uid = -1; + + // Minimal libwebsockets configuration + info.options = LWS_SERVER_OPTION_VALIDATE_UTF8; + + // Remove interface restrictions - let system choose + // info.vhost_name = NULL; + // info.iface = NULL; + + // Increase max connections for relay usage + info.max_http_header_pool = 16; + info.timeout_secs = 10; + + // Max payload size for Nostr events + info.max_http_header_data = 4096; + + ws_context = lws_create_context(&info); + if (!ws_context) { + log_error("Failed to create libwebsockets context"); + perror("libwebsockets creation error"); + return -1; + } + + log_success("WebSocket relay started on ws://127.0.0.1:8888"); + + // Main event loop with proper signal handling + fd_set rfds; + struct timeval tv; + + while (g_server_running) { + FD_ZERO(&rfds); + tv.tv_sec = 1; + tv.tv_usec = 0; + + int result = lws_service(ws_context, 1000); + + if (result < 0) { + log_error("libwebsockets service error"); + break; + } + } + + log_info("Shutting down WebSocket server..."); + lws_context_destroy(ws_context); + ws_context = NULL; + + log_success("WebSocket relay shut down cleanly"); + return 0; +} + +// Print usage information +void print_usage(const char* program_name) { + printf("Usage: %s [OPTIONS]\n", program_name); + printf("\n"); + printf("C Nostr Relay Server\n"); + printf("\n"); + printf("Options:\n"); + printf(" -p, --port PORT Listen port (default: %d)\n", DEFAULT_PORT); + printf(" -h, --help Show this help message\n"); + printf("\n"); +} + +int main(int argc, char* argv[]) { + int port = DEFAULT_PORT; + + // Parse command line arguments + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { + if (i + 1 < argc) { + port = atoi(argv[++i]); + if (port <= 0 || port > 65535) { + log_error("Invalid port number"); + return 1; + } + } else { + log_error("Port argument requires a value"); + return 1; + } + } else { + log_error("Unknown argument"); + print_usage(argv[0]); + return 1; + } + } + + // Set up signal handlers + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n"); + + // Initialize database + if (init_database() != 0) { + log_error("Failed to initialize database"); + return 1; + } + + // Initialize nostr library + if (nostr_init() != 0) { + log_error("Failed to initialize nostr library"); + close_database(); + return 1; + } + + log_info("Starting relay server..."); + + // Start WebSocket Nostr relay server + int result = start_websocket_relay(); + + // Cleanup + nostr_cleanup(); + close_database(); + + if (result == 0) { + log_success("Server shutdown complete"); + } else { + log_error("Server shutdown with errors"); + } + + return result; +} \ No newline at end of file diff --git a/tests/1_nip_test.sh b/tests/1_nip_test.sh new file mode 100755 index 0000000..dc7774c --- /dev/null +++ b/tests/1_nip_test.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Simple C-Relay Test - Create type 1 event and upload to relay +# Uses nak to generate and publish a single event + +set -e # Exit on any error + +# Color constants +RED='\033[31m' +GREEN='\033[32m' +YELLOW='\033[33m' +BLUE='\033[34m' +BOLD='\033[1m' +RESET='\033[0m' + +# Test configuration +RELAY_URL="ws://127.0.0.1:8888" +TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99" +TEST_CONTENT="Hello from C-Relay test!" + +# Print functions +print_header() { + echo -e "${BLUE}${BOLD}=== $1 ===${RESET}" +} + +print_step() { + echo -e "${YELLOW}[STEP]${RESET} $1" +} + +print_success() { + echo -e "${GREEN}✓${RESET} $1" +} + +print_error() { + echo -e "${RED}✗${RESET} $1" +} + +print_info() { + echo -e "${BLUE}[INFO]${RESET} $1" +} + +# Main test function +run_test() { + print_header "C-Relay Simple Test" + + # Check if nak is available + print_step "Checking dependencies..." + if ! command -v nak &> /dev/null; then + print_error "nak command not found" + print_info "Please install nak: go install github.com/fiatjaf/nak@latest" + return 1 + fi + print_success "nak found" + + # Step 1: Create type 1 event with nak including tags + print_step "Creating type 1 event with nak and tags..." + + local event_json + if ! event_json=$(nak event --sec "$TEST_PRIVATE_KEY" -c "$TEST_CONTENT" -k 1 --ts $(date +%s) -e "test_event_id" -p "test_pubkey" -t "subject=Test Event" 2>/dev/null); then + print_error "Failed to generate event with nak" + return 1 + fi + + print_success "Event created successfully" + print_header "FULL EVENT JSON" + echo "$event_json" | jq . 2>/dev/null || echo "$event_json" + echo + + # Step 2: Upload to C-Relay + print_step "Uploading event to C-Relay at $RELAY_URL..." + + # Create EVENT message in Nostr format + local event_message="[\"EVENT\",$event_json]" + + # Use websocat or wscat to send to relay if available + local response="" + if command -v websocat &> /dev/null; then + print_info "Using websocat to connect to relay..." + response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") + elif command -v wscat &> /dev/null; then + print_info "Using wscat to connect to relay..." + response=$(echo "$event_message" | timeout 5s wscat -c "$RELAY_URL" 2>&1 || echo "Connection failed") + else + # Fallback: use nak publish + print_info "Using nak to publish event..." + response=$(echo "$event_json" | nak event --relay "$RELAY_URL" 2>&1 || echo "Publish failed") + fi + + print_header "FULL RELAY RESPONSE" + echo "$response" + echo + + if [[ "$response" == *"Connection failed"* ]] || [[ "$response" == *"Publish failed"* ]]; then + print_error "Failed to connect to relay or publish event" + print_info "Make sure the relay is running: ./make_and_restart_relay.sh" + return 1 + else + print_success "Event uploaded to relay" + return 0 + fi +} + +# Run the test +if run_test; then + echo + print_success "Test completed successfully" + exit 0 +else + echo + print_error "Test failed" + exit 1 +fi \ No newline at end of file