diff --git a/Makefile b/Makefile index 69ee8e4..5041f97 100644 --- a/Makefile +++ b/Makefile @@ -86,18 +86,18 @@ force-version: @$(MAKE) src/version.h # Build the relay -$(TARGET): $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB) +$(TARGET): $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) @echo "Compiling C-Relay for architecture: $(ARCH)" $(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS) @echo "Build complete: $(TARGET)" # Build for specific architectures -x86: $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB) +x86: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) @echo "Building C-Relay for x86_64..." $(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS) @echo "Build complete: $(BUILD_DIR)/c_relay_x86" -arm64: $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB) +arm64: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) @echo "Cross-compiling C-Relay for ARM64..." @if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \ echo "ERROR: ARM64 cross-compiler not found."; \ @@ -161,10 +161,11 @@ test: $(TARGET) @echo "Running tests..." ./tests/1_nip_test.sh -# Initialize database +# Initialize database (now handled automatically when server starts) init-db: - @echo "Initializing database..." - ./db/init.sh --force + @echo "Database initialization is now handled automatically when the server starts." + @echo "The schema is embedded in the binary - no external files needed." + @echo "To manually recreate database: rm -f db/c_nostr_relay.db && ./build/c_relay_x86" # Clean build artifacts clean: diff --git a/db/README.md b/db/README.md index d4080cb..997d3d3 100644 --- a/db/README.md +++ b/db/README.md @@ -1,228 +1 @@ -# 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 +Only README.md will remain diff --git a/db/c_nostr_relay.db b/db/c_nostr_relay.db index 0f9f3fa..6daae4e 100644 Binary files a/db/c_nostr_relay.db and b/db/c_nostr_relay.db differ diff --git a/db/c_nostr_relay.db-shm b/db/c_nostr_relay.db-shm index 4d80fe2..825010a 100644 Binary files a/db/c_nostr_relay.db-shm 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 index 87e7174..8895bc4 100644 Binary files a/db/c_nostr_relay.db-wal and b/db/c_nostr_relay.db-wal differ diff --git a/db/c_nostr_relay.db.backup.20250905_152104 b/db/c_nostr_relay.db.backup.20250905_152104 deleted file mode 100644 index a79b8e8..0000000 Binary files a/db/c_nostr_relay.db.backup.20250905_152104 and /dev/null differ diff --git a/db/init.sh b/db/init.sh deleted file mode 100755 index d85816c..0000000 --- a/db/init.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/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 (including configuration tables) - local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info', 'server_config');") - if [ "$table_count" -eq 3 ]; then - log_success "Core tables created successfully (including configuration tables)" - else - log_error "Missing core tables (expected 3, 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 deleted file mode 100644 index 87f3d7a..0000000 --- a/db/schema.sql +++ /dev/null @@ -1,299 +0,0 @@ --- C Nostr Relay Database Schema --- SQLite schema for storing Nostr events with JSON tags support - --- Schema version tracking -PRAGMA user_version = 3; - --- Enable foreign key support -PRAGMA foreign_keys = ON; - --- Optimize for performance -PRAGMA journal_mode = WAL; -PRAGMA synchronous = NORMAL; -PRAGMA cache_size = 10000; - --- Core events table with hybrid single-table design -CREATE TABLE events ( - 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) - event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')), - content TEXT NOT NULL, -- Event content (text content only) - sig TEXT NOT NULL, -- Event signature (hex string) - tags JSON NOT NULL DEFAULT '[]', -- Event tags as JSON array - first_seen INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -- When relay received event -); - --- Core performance indexes -CREATE INDEX idx_events_pubkey ON events(pubkey); -CREATE INDEX idx_events_kind ON events(kind); -CREATE INDEX idx_events_created_at ON events(created_at DESC); -CREATE INDEX idx_events_event_type ON events(event_type); - --- Composite indexes for common query patterns -CREATE INDEX idx_events_kind_created_at ON events(kind, created_at DESC); -CREATE INDEX idx_events_pubkey_created_at ON events(pubkey, created_at DESC); -CREATE INDEX idx_events_pubkey_kind ON events(pubkey, kind); - --- Schema information table -CREATE TABLE schema_info ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -); - --- Insert schema metadata -INSERT INTO schema_info (key, value) VALUES - ('version', '3'), - ('description', 'Hybrid single-table Nostr relay schema with JSON tags and configuration management'), - ('created_at', strftime('%s', 'now')); - --- Helper views for common queries -CREATE VIEW recent_events AS -SELECT id, pubkey, created_at, kind, event_type, content -FROM events -WHERE event_type != 'ephemeral' -ORDER BY created_at DESC -LIMIT 1000; - -CREATE VIEW event_stats AS -SELECT - event_type, - COUNT(*) as count, - AVG(length(content)) as avg_content_length, - MIN(created_at) as earliest, - MAX(created_at) as latest -FROM events -GROUP BY event_type; - --- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour -CREATE TRIGGER cleanup_ephemeral_events - AFTER INSERT ON events - WHEN NEW.event_type = 'ephemeral' -BEGIN - DELETE FROM events - WHERE event_type = 'ephemeral' - AND first_seen < (strftime('%s', 'now') - 3600); -END; - --- Replaceable event handling trigger -CREATE TRIGGER handle_replaceable_events - AFTER INSERT ON events - WHEN NEW.event_type = 'replaceable' -BEGIN - DELETE FROM events - WHERE pubkey = NEW.pubkey - AND kind = NEW.kind - AND event_type = 'replaceable' - AND id != NEW.id; -END; - --- Persistent Subscriptions Logging Tables (Phase 2) --- Optional database logging for subscription analytics and debugging - --- Subscription events log -CREATE TABLE subscription_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - subscription_id TEXT NOT NULL, -- Subscription ID from client - client_ip TEXT NOT NULL, -- Client IP address - event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')), - filter_json TEXT, -- JSON representation of filters (for created events) - events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected) - duration INTEGER -- Computed: ended_at - created_at -); - --- Subscription metrics summary -CREATE TABLE subscription_metrics ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, -- Date (YYYY-MM-DD) - total_created INTEGER DEFAULT 0, -- Total subscriptions created - total_closed INTEGER DEFAULT 0, -- Total subscriptions closed - total_events_broadcast INTEGER DEFAULT 0, -- Total events broadcast - avg_duration REAL DEFAULT 0, -- Average subscription duration - peak_concurrent INTEGER DEFAULT 0, -- Peak concurrent subscriptions - updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - UNIQUE(date) -); - --- Event broadcasting log (optional, for detailed analytics) -CREATE TABLE event_broadcasts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_id TEXT NOT NULL, -- Event ID that was broadcast - subscription_id TEXT NOT NULL, -- Subscription that received it - client_ip TEXT NOT NULL, -- Client IP - broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - FOREIGN KEY (event_id) REFERENCES events(id) -); - --- Indexes for subscription logging performance -CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id); -CREATE INDEX idx_subscription_events_type ON subscription_events(event_type); -CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC); -CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip); - -CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC); - -CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id); -CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id); -CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC); - --- Trigger to update subscription duration when ended -CREATE TRIGGER update_subscription_duration - AFTER UPDATE OF ended_at ON subscription_events - WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL -BEGIN - UPDATE subscription_events - SET duration = NEW.ended_at - NEW.created_at - WHERE id = NEW.id; -END; - --- View for subscription analytics -CREATE VIEW subscription_analytics AS -SELECT - date(created_at, 'unixepoch') as date, - COUNT(*) as subscriptions_created, - COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) as subscriptions_ended, - AVG(CASE WHEN duration IS NOT NULL THEN duration END) as avg_duration_seconds, - MAX(events_sent) as max_events_sent, - AVG(events_sent) as avg_events_sent, - COUNT(DISTINCT client_ip) as unique_clients -FROM subscription_events -GROUP BY date(created_at, 'unixepoch') -ORDER BY date DESC; - --- View for current active subscriptions (from log perspective) -CREATE VIEW active_subscriptions_log AS -SELECT - subscription_id, - client_ip, - filter_json, - events_sent, - created_at, - (strftime('%s', 'now') - created_at) as duration_seconds -FROM subscription_events -WHERE event_type = 'created' -AND subscription_id NOT IN ( - SELECT subscription_id FROM subscription_events - WHERE event_type IN ('closed', 'expired', 'disconnected') -); - --- ================================ --- CONFIGURATION MANAGEMENT TABLES --- ================================ - --- Core server configuration table -CREATE TABLE server_config ( - key TEXT PRIMARY KEY, -- Configuration key (unique identifier) - value TEXT NOT NULL, -- Configuration value (stored as string) - description TEXT, -- Human-readable description - config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')), - data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')), - validation_rules TEXT, -- JSON validation rules (optional) - is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs - requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -); - --- Configuration change history table -CREATE TABLE config_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - config_key TEXT NOT NULL, -- Key that was changed - old_value TEXT, -- Previous value (NULL for new keys) - new_value TEXT NOT NULL, -- New value - changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user) - change_reason TEXT, -- Optional reason for change - changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - FOREIGN KEY (config_key) REFERENCES server_config(key) -); - --- Configuration validation errors log -CREATE TABLE config_validation_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - config_key TEXT NOT NULL, - attempted_value TEXT, - validation_error TEXT NOT NULL, - error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint - attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -); - --- Cache for file-based configuration events -CREATE TABLE config_file_cache ( - file_path TEXT PRIMARY KEY, -- Full path to config file - file_hash TEXT NOT NULL, -- SHA256 hash of file content - event_id TEXT, -- Nostr event ID from file - event_pubkey TEXT, -- Admin pubkey that signed event - loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')), - validation_error TEXT -- Error details if invalid -); - --- Performance indexes for configuration tables -CREATE INDEX idx_server_config_type ON server_config(config_type); -CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC); -CREATE INDEX idx_config_history_key ON config_history(config_key); -CREATE INDEX idx_config_history_time ON config_history(changed_at DESC); -CREATE INDEX idx_config_validation_key ON config_validation_log(config_key); -CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC); - --- Trigger to update timestamp on configuration changes -CREATE TRIGGER update_config_timestamp - AFTER UPDATE ON server_config -BEGIN - UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key; -END; - --- Trigger to log configuration changes to history -CREATE TRIGGER log_config_changes - AFTER UPDATE ON server_config - WHEN OLD.value != NEW.value -BEGIN - INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason) - VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update'); -END; - --- Active Configuration View -CREATE VIEW active_config AS -SELECT - key, - value, - description, - config_type, - data_type, - requires_restart, - updated_at -FROM server_config -WHERE config_type IN ('system', 'user') -ORDER BY config_type, key; - --- Runtime Statistics View -CREATE VIEW runtime_stats AS -SELECT - key, - value, - description, - updated_at -FROM server_config -WHERE config_type = 'runtime' -ORDER BY key; - --- Configuration Change Summary -CREATE VIEW recent_config_changes AS -SELECT - ch.config_key, - sc.description, - ch.old_value, - ch.new_value, - ch.changed_by, - ch.change_reason, - ch.changed_at -FROM config_history ch -JOIN server_config sc ON ch.config_key = sc.key -ORDER BY ch.changed_at DESC -LIMIT 50; - --- Runtime Statistics (initialized by server on startup) --- These will be populated when configuration system initializes \ No newline at end of file diff --git a/relay.pid b/relay.pid index a1f85d6..abaec94 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -871512 +891896 diff --git a/src/main.c b/src/main.c index 26a1fb2..c834f66 100644 --- a/src/main.c +++ b/src/main.c @@ -16,6 +16,7 @@ #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work #include "config.h" // Configuration management system +#include "sql_schema.h" // Embedded database schema // Color constants for logging #define RED "\033[31m" @@ -1938,7 +1939,7 @@ int validate_event_expiration(cJSON* event, char* error_message, size_t error_si ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// -// Initialize database connection +// Initialize database connection and schema int init_database() { // Use configurable database path, falling back to default const char* db_path = get_config_value("database_path"); @@ -1955,6 +1956,64 @@ int init_database() { char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Database connection established: %s", db_path); log_success(success_msg); + + // Check if database is already initialized by looking for the events table + const char* check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='events'"; + sqlite3_stmt* check_stmt; + rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); + if (rc == SQLITE_OK) { + int has_events_table = (sqlite3_step(check_stmt) == SQLITE_ROW); + sqlite3_finalize(check_stmt); + + if (has_events_table) { + log_info("Database schema already exists, skipping initialization"); + + // Log existing schema version if available + const char* version_sql = "SELECT value FROM schema_info WHERE key = 'version'"; + sqlite3_stmt* version_stmt; + if (sqlite3_prepare_v2(g_db, version_sql, -1, &version_stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(version_stmt) == SQLITE_ROW) { + const char* db_version = (char*)sqlite3_column_text(version_stmt, 0); + char version_msg[256]; + snprintf(version_msg, sizeof(version_msg), "Existing database schema version: %s", + db_version ? db_version : "unknown"); + log_info(version_msg); + } else { + log_info("Database exists but no version information found"); + } + sqlite3_finalize(version_stmt); + } + } else { + // Initialize database schema using embedded SQL + log_info("Initializing database schema from embedded SQL"); + + // Execute the embedded schema SQL + char* error_msg = NULL; + rc = sqlite3_exec(g_db, EMBEDDED_SCHEMA_SQL, NULL, NULL, &error_msg); + if (rc != SQLITE_OK) { + char error_log[512]; + snprintf(error_log, sizeof(error_log), "Failed to initialize database schema: %s", + error_msg ? error_msg : "unknown error"); + log_error(error_log); + if (error_msg) { + sqlite3_free(error_msg); + } + return -1; + } + + log_success("Database schema initialized successfully"); + + // Log schema version information + char version_msg[256]; + snprintf(version_msg, sizeof(version_msg), "Database schema version: %s", + EMBEDDED_SCHEMA_VERSION); + log_info(version_msg); + } + } else { + log_error("Failed to check existing database schema"); + return -1; + } + return 0; } diff --git a/src/sql_schema.h b/src/sql_schema.h new file mode 100644 index 0000000..21b1dda --- /dev/null +++ b/src/sql_schema.h @@ -0,0 +1,313 @@ +/* Embedded SQL Schema for C Nostr Relay + * Generated from db/schema.sql - Do not edit manually + * Schema Version: 3 + */ +#ifndef SQL_SCHEMA_H +#define SQL_SCHEMA_H + +/* Schema version constant */ +#define EMBEDDED_SCHEMA_VERSION "3" + +/* Embedded SQL schema as C string literal */ +static const char* const EMBEDDED_SCHEMA_SQL = +"-- C Nostr Relay Database Schema\n\ +-- SQLite schema for storing Nostr events with JSON tags support\n\ +\n\ +-- Schema version tracking\n\ +PRAGMA user_version = 3;\n\ +\n\ +-- Enable foreign key support\n\ +PRAGMA foreign_keys = ON;\n\ +\n\ +-- Optimize for performance\n\ +PRAGMA journal_mode = WAL;\n\ +PRAGMA synchronous = NORMAL;\n\ +PRAGMA cache_size = 10000;\n\ +\n\ +-- Core events table with hybrid single-table design\n\ +CREATE TABLE events (\n\ + id TEXT PRIMARY KEY, -- Nostr event ID (hex string)\n\ + pubkey TEXT NOT NULL, -- Public key of event author (hex string)\n\ + created_at INTEGER NOT NULL, -- Event creation timestamp (Unix timestamp)\n\ + kind INTEGER NOT NULL, -- Event kind (0-65535)\n\ + event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),\n\ + content TEXT NOT NULL, -- Event content (text content only)\n\ + sig TEXT NOT NULL, -- Event signature (hex string)\n\ + tags JSON NOT NULL DEFAULT '[]', -- Event tags as JSON array\n\ + first_seen INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -- When relay received event\n\ +);\n\ +\n\ +-- Core performance indexes\n\ +CREATE INDEX idx_events_pubkey ON events(pubkey);\n\ +CREATE INDEX idx_events_kind ON events(kind);\n\ +CREATE INDEX idx_events_created_at ON events(created_at DESC);\n\ +CREATE INDEX idx_events_event_type ON events(event_type);\n\ +\n\ +-- Composite indexes for common query patterns\n\ +CREATE INDEX idx_events_kind_created_at ON events(kind, created_at DESC);\n\ +CREATE INDEX idx_events_pubkey_created_at ON events(pubkey, created_at DESC);\n\ +CREATE INDEX idx_events_pubkey_kind ON events(pubkey, kind);\n\ +\n\ +-- Schema information table\n\ +CREATE TABLE schema_info (\n\ + key TEXT PRIMARY KEY,\n\ + value TEXT NOT NULL,\n\ + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\ +);\n\ +\n\ +-- Insert schema metadata\n\ +INSERT INTO schema_info (key, value) VALUES\n\ + ('version', '3'),\n\ + ('description', 'Hybrid single-table Nostr relay schema with JSON tags and configuration management'),\n\ + ('created_at', strftime('%s', 'now'));\n\ +\n\ +-- Helper views for common queries\n\ +CREATE VIEW recent_events AS\n\ +SELECT id, pubkey, created_at, kind, event_type, content\n\ +FROM events\n\ +WHERE event_type != 'ephemeral'\n\ +ORDER BY created_at DESC\n\ +LIMIT 1000;\n\ +\n\ +CREATE VIEW event_stats AS\n\ +SELECT \n\ + event_type,\n\ + COUNT(*) as count,\n\ + AVG(length(content)) as avg_content_length,\n\ + MIN(created_at) as earliest,\n\ + MAX(created_at) as latest\n\ +FROM events\n\ +GROUP BY event_type;\n\ +\n\ +-- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour\n\ +CREATE TRIGGER cleanup_ephemeral_events\n\ + AFTER INSERT ON events\n\ + WHEN NEW.event_type = 'ephemeral'\n\ +BEGIN\n\ + DELETE FROM events \n\ + WHERE event_type = 'ephemeral' \n\ + AND first_seen < (strftime('%s', 'now') - 3600);\n\ +END;\n\ +\n\ +-- Replaceable event handling trigger\n\ +CREATE TRIGGER handle_replaceable_events\n\ + AFTER INSERT ON events\n\ + WHEN NEW.event_type = 'replaceable'\n\ +BEGIN\n\ + DELETE FROM events \n\ + WHERE pubkey = NEW.pubkey \n\ + AND kind = NEW.kind \n\ + AND event_type = 'replaceable'\n\ + AND id != NEW.id;\n\ +END;\n\ +\n\ +-- Persistent Subscriptions Logging Tables (Phase 2)\n\ +-- Optional database logging for subscription analytics and debugging\n\ +\n\ +-- Subscription events log\n\ +CREATE TABLE subscription_events (\n\ + id INTEGER PRIMARY KEY AUTOINCREMENT,\n\ + subscription_id TEXT NOT NULL, -- Subscription ID from client\n\ + client_ip TEXT NOT NULL, -- Client IP address\n\ + event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n\ + filter_json TEXT, -- JSON representation of filters (for created events)\n\ + events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription\n\ + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)\n\ + duration INTEGER -- Computed: ended_at - created_at\n\ +);\n\ +\n\ +-- Subscription metrics summary\n\ +CREATE TABLE subscription_metrics (\n\ + id INTEGER PRIMARY KEY AUTOINCREMENT,\n\ + date TEXT NOT NULL, -- Date (YYYY-MM-DD)\n\ + total_created INTEGER DEFAULT 0, -- Total subscriptions created\n\ + total_closed INTEGER DEFAULT 0, -- Total subscriptions closed\n\ + total_events_broadcast INTEGER DEFAULT 0, -- Total events broadcast\n\ + avg_duration REAL DEFAULT 0, -- Average subscription duration\n\ + peak_concurrent INTEGER DEFAULT 0, -- Peak concurrent subscriptions\n\ + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + UNIQUE(date)\n\ +);\n\ +\n\ +-- Event broadcasting log (optional, for detailed analytics)\n\ +CREATE TABLE event_broadcasts (\n\ + id INTEGER PRIMARY KEY AUTOINCREMENT,\n\ + event_id TEXT NOT NULL, -- Event ID that was broadcast\n\ + subscription_id TEXT NOT NULL, -- Subscription that received it\n\ + client_ip TEXT NOT NULL, -- Client IP\n\ + broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + FOREIGN KEY (event_id) REFERENCES events(id)\n\ +);\n\ +\n\ +-- Indexes for subscription logging performance\n\ +CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);\n\ +CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);\n\ +CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);\n\ +CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);\n\ +\n\ +CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);\n\ +\n\ +CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id);\n\ +CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id);\n\ +CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);\n\ +\n\ +-- Trigger to update subscription duration when ended\n\ +CREATE TRIGGER update_subscription_duration\n\ + AFTER UPDATE OF ended_at ON subscription_events\n\ + WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL\n\ +BEGIN\n\ + UPDATE subscription_events\n\ + SET duration = NEW.ended_at - NEW.created_at\n\ + WHERE id = NEW.id;\n\ +END;\n\ +\n\ +-- View for subscription analytics\n\ +CREATE VIEW subscription_analytics AS\n\ +SELECT\n\ + date(created_at, 'unixepoch') as date,\n\ + COUNT(*) as subscriptions_created,\n\ + COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) as subscriptions_ended,\n\ + AVG(CASE WHEN duration IS NOT NULL THEN duration END) as avg_duration_seconds,\n\ + MAX(events_sent) as max_events_sent,\n\ + AVG(events_sent) as avg_events_sent,\n\ + COUNT(DISTINCT client_ip) as unique_clients\n\ +FROM subscription_events\n\ +GROUP BY date(created_at, 'unixepoch')\n\ +ORDER BY date DESC;\n\ +\n\ +-- View for current active subscriptions (from log perspective)\n\ +CREATE VIEW active_subscriptions_log AS\n\ +SELECT\n\ + subscription_id,\n\ + client_ip,\n\ + filter_json,\n\ + events_sent,\n\ + created_at,\n\ + (strftime('%s', 'now') - created_at) as duration_seconds\n\ +FROM subscription_events\n\ +WHERE event_type = 'created'\n\ +AND subscription_id NOT IN (\n\ + SELECT subscription_id FROM subscription_events\n\ + WHERE event_type IN ('closed', 'expired', 'disconnected')\n\ +);\n\ +\n\ +-- ================================\n\ +-- CONFIGURATION MANAGEMENT TABLES\n\ +-- ================================\n\ +\n\ +-- Core server configuration table\n\ +CREATE TABLE server_config (\n\ + key TEXT PRIMARY KEY, -- Configuration key (unique identifier)\n\ + value TEXT NOT NULL, -- Configuration value (stored as string)\n\ + description TEXT, -- Human-readable description\n\ + config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')),\n\ + data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\ + validation_rules TEXT, -- JSON validation rules (optional)\n\ + is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs\n\ + requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart\n\ + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\ +);\n\ +\n\ +-- Configuration change history table\n\ +CREATE TABLE config_history (\n\ + id INTEGER PRIMARY KEY AUTOINCREMENT,\n\ + config_key TEXT NOT NULL, -- Key that was changed\n\ + old_value TEXT, -- Previous value (NULL for new keys)\n\ + new_value TEXT NOT NULL, -- New value\n\ + changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user)\n\ + change_reason TEXT, -- Optional reason for change\n\ + changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + FOREIGN KEY (config_key) REFERENCES server_config(key)\n\ +);\n\ +\n\ +-- Configuration validation errors log\n\ +CREATE TABLE config_validation_log (\n\ + id INTEGER PRIMARY KEY AUTOINCREMENT,\n\ + config_key TEXT NOT NULL,\n\ + attempted_value TEXT,\n\ + validation_error TEXT NOT NULL,\n\ + error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint\n\ + attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\ +);\n\ +\n\ +-- Cache for file-based configuration events\n\ +CREATE TABLE config_file_cache (\n\ + file_path TEXT PRIMARY KEY, -- Full path to config file\n\ + file_hash TEXT NOT NULL, -- SHA256 hash of file content\n\ + event_id TEXT, -- Nostr event ID from file\n\ + event_pubkey TEXT, -- Admin pubkey that signed event\n\ + loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')),\n\ + validation_error TEXT -- Error details if invalid\n\ +);\n\ +\n\ +-- Performance indexes for configuration tables\n\ +CREATE INDEX idx_server_config_type ON server_config(config_type);\n\ +CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC);\n\ +CREATE INDEX idx_config_history_key ON config_history(config_key);\n\ +CREATE INDEX idx_config_history_time ON config_history(changed_at DESC);\n\ +CREATE INDEX idx_config_validation_key ON config_validation_log(config_key);\n\ +CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC);\n\ +\n\ +-- Trigger to update timestamp on configuration changes\n\ +CREATE TRIGGER update_config_timestamp\n\ + AFTER UPDATE ON server_config\n\ +BEGIN\n\ + UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\ +END;\n\ +\n\ +-- Trigger to log configuration changes to history\n\ +CREATE TRIGGER log_config_changes\n\ + AFTER UPDATE ON server_config\n\ + WHEN OLD.value != NEW.value\n\ +BEGIN\n\ + INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason)\n\ + VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update');\n\ +END;\n\ +\n\ +-- Active Configuration View\n\ +CREATE VIEW active_config AS\n\ +SELECT\n\ + key,\n\ + value,\n\ + description,\n\ + config_type,\n\ + data_type,\n\ + requires_restart,\n\ + updated_at\n\ +FROM server_config\n\ +WHERE config_type IN ('system', 'user')\n\ +ORDER BY config_type, key;\n\ +\n\ +-- Runtime Statistics View\n\ +CREATE VIEW runtime_stats AS\n\ +SELECT\n\ + key,\n\ + value,\n\ + description,\n\ + updated_at\n\ +FROM server_config\n\ +WHERE config_type = 'runtime'\n\ +ORDER BY key;\n\ +\n\ +-- Configuration Change Summary\n\ +CREATE VIEW recent_config_changes AS\n\ +SELECT\n\ + ch.config_key,\n\ + sc.description,\n\ + ch.old_value,\n\ + ch.new_value,\n\ + ch.changed_by,\n\ + ch.change_reason,\n\ + ch.changed_at\n\ +FROM config_history ch\n\ +JOIN server_config sc ON ch.config_key = sc.key\n\ +ORDER BY ch.changed_at DESC\n\ +LIMIT 50;\n\ +\n\ +-- Runtime Statistics (initialized by server on startup)\n\ +-- These will be populated when configuration system initializes"; + +#endif /* SQL_SCHEMA_H */ \ No newline at end of file