# Unified Startup Implementation Plan ## Overview This document provides a detailed implementation plan for refactoring the startup sequence to use atomic config creation followed by CLI overrides. This plan breaks down the work into discrete, testable steps. --- ## Phase 1: Create New Functions in config.c ### Step 1.1: Implement `populate_all_config_values_atomic()` **Location**: `src/config.c` **Purpose**: Create complete config table in single transaction for first-time startup **Function Signature**: ```c int populate_all_config_values_atomic(const cli_options_t* cli_options); ``` **Implementation Details**: ```c int populate_all_config_values_atomic(const cli_options_t* cli_options) { if (!g_database) { DEBUG_ERROR("Database not initialized"); return -1; } // Begin transaction char* err_msg = NULL; int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to begin transaction: %s", err_msg); sqlite3_free(err_msg); return -1; } // Prepare INSERT statement sqlite3_stmt* stmt = NULL; const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)"; rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database)); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } // Insert all default config values for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) { sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, DEFAULT_CONFIG_VALUES[i].key, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, DEFAULT_CONFIG_VALUES[i].value, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to insert config key '%s': %s", DEFAULT_CONFIG_VALUES[i].key, sqlite3_errmsg(g_database)); sqlite3_finalize(stmt); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } } // Insert admin_pubkey from cache sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_unified_cache.admin_pubkey, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to insert admin_pubkey: %s", sqlite3_errmsg(g_database)); sqlite3_finalize(stmt); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } // Insert relay_pubkey from cache sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, "relay_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_unified_cache.relay_pubkey, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to insert relay_pubkey: %s", sqlite3_errmsg(g_database)); sqlite3_finalize(stmt); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } sqlite3_finalize(stmt); // Commit transaction rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to commit transaction: %s", err_msg); sqlite3_free(err_msg); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } DEBUG_INFO("Successfully populated all config values atomically"); return 0; } ``` **Testing**: - Verify transaction atomicity (all or nothing) - Verify all DEFAULT_CONFIG_VALUES inserted - Verify admin_pubkey and relay_pubkey inserted - Verify error handling on failure --- ### Step 1.2: Implement `apply_cli_overrides_atomic()` **Location**: `src/config.c` **Purpose**: Apply CLI overrides to existing config table in single transaction **Function Signature**: ```c int apply_cli_overrides_atomic(const cli_options_t* cli_options); ``` **Implementation Details**: ```c int apply_cli_overrides_atomic(const cli_options_t* cli_options) { if (!g_database) { DEBUG_ERROR("Database not initialized"); return -1; } if (!cli_options) { DEBUG_ERROR("CLI options is NULL"); return -1; } // Check if any overrides exist bool has_overrides = false; if (cli_options->port_override > 0) has_overrides = true; if (cli_options->admin_pubkey_override[0] != '\0') has_overrides = true; if (cli_options->relay_privkey_override[0] != '\0') has_overrides = true; if (!has_overrides) { DEBUG_INFO("No CLI overrides to apply"); return 0; } // Begin transaction char* err_msg = NULL; int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to begin transaction: %s", err_msg); sqlite3_free(err_msg); return -1; } // Prepare UPDATE statement sqlite3_stmt* stmt = NULL; const char* sql = "UPDATE config SET value = ? WHERE key = ?"; rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database)); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } // Apply port override if (cli_options->port_override > 0) { char port_str[16]; snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override); sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, port_str, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, "relay_port", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to update relay_port: %s", sqlite3_errmsg(g_database)); sqlite3_finalize(stmt); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } DEBUG_INFO("Applied CLI override: relay_port = %s", port_str); } // Apply admin_pubkey override if (cli_options->admin_pubkey_override[0] != '\0') { sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, cli_options->admin_pubkey_override, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, "admin_pubkey", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to update admin_pubkey: %s", sqlite3_errmsg(g_database)); sqlite3_finalize(stmt); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } DEBUG_INFO("Applied CLI override: admin_pubkey"); } // Apply relay_privkey override if (cli_options->relay_privkey_override[0] != '\0') { sqlite3_reset(stmt); sqlite3_bind_text(stmt, 1, cli_options->relay_privkey_override, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, "relay_privkey", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to update relay_privkey: %s", sqlite3_errmsg(g_database)); sqlite3_finalize(stmt); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } DEBUG_INFO("Applied CLI override: relay_privkey"); } sqlite3_finalize(stmt); // Commit transaction rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to commit transaction: %s", err_msg); sqlite3_free(err_msg); sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL); return -1; } // Invalidate cache to force refresh invalidate_config_cache(); DEBUG_INFO("Successfully applied CLI overrides atomically"); return 0; } ``` **Testing**: - Verify transaction atomicity - Verify each override type (port, admin_pubkey, relay_privkey) - Verify cache invalidation after overrides - Verify no-op when no overrides present --- ### Step 1.3: Implement `validate_config_table_completeness()` **Location**: `src/config.c` **Purpose**: Validate config table has all required keys, populate missing ones **Function Signature**: ```c int validate_config_table_completeness(void); ``` **Implementation Details**: ```c int validate_config_table_completeness(void) { if (!g_database) { DEBUG_ERROR("Database not initialized"); return -1; } DEBUG_INFO("Validating config table completeness"); // Check each default config key for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) { const char* key = DEFAULT_CONFIG_VALUES[i].key; // Check if key exists sqlite3_stmt* stmt = NULL; const char* sql = "SELECT COUNT(*) FROM config WHERE key = ?"; int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database)); return -1; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); int count = 0; if (rc == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); // If key missing, populate it if (count == 0) { DEBUG_WARN("Config key '%s' missing, populating with default", key); rc = populate_missing_config_key(key, DEFAULT_CONFIG_VALUES[i].value); if (rc != 0) { DEBUG_ERROR("Failed to populate missing key '%s'", key); return -1; } } } DEBUG_INFO("Config table validation complete"); return 0; } ``` **Helper Function**: ```c static int populate_missing_config_key(const char* key, const char* value) { sqlite3_stmt* stmt = NULL; const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)"; int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database)); return -1; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { DEBUG_ERROR("Failed to insert config key '%s': %s", key, sqlite3_errmsg(g_database)); return -1; } return 0; } ``` **Testing**: - Verify detection of missing keys - Verify population of missing keys with defaults - Verify no changes when all keys present - Verify error handling --- ### Step 1.4: Implement `has_cli_overrides()` **Location**: `src/config.c` **Purpose**: Check if any CLI overrides are present **Function Signature**: ```c bool has_cli_overrides(const cli_options_t* cli_options); ``` **Implementation Details**: ```c bool has_cli_overrides(const cli_options_t* cli_options) { if (!cli_options) { return false; } return (cli_options->port_override > 0 || cli_options->admin_pubkey_override[0] != '\0' || cli_options->relay_privkey_override[0] != '\0'); } ``` **Testing**: - Verify returns true when any override present - Verify returns false when no overrides - Verify NULL safety --- ## Phase 2: Update Function Declarations in config.h ### Step 2.1: Add New Function Declarations **Location**: `src/config.h` **Changes**: ```c // Add after existing function declarations // Atomic config population for first-time startup int populate_all_config_values_atomic(const cli_options_t* cli_options); // Atomic CLI override application int apply_cli_overrides_atomic(const cli_options_t* cli_options); // Config validation for existing databases int validate_config_table_completeness(void); // Helper function to check for CLI overrides bool has_cli_overrides(const cli_options_t* cli_options); ``` --- ## Phase 3: Refactor Startup Flow in main.c ### Step 3.1: Update First-Time Startup Branch **Location**: `src/main.c` (around lines 1624-1740) **Current Code**: ```c if (is_first_time_startup()) { first_time_startup_sequence(&cli_options); init_database(g_database_path); // Current incremental approach populate_default_config_values(); if (cli_options.port_override > 0) { char port_str[16]; snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override); update_config_in_table("relay_port", port_str); } add_pubkeys_to_config_table(); store_relay_private_key(relay_privkey); refresh_unified_cache_from_table(); } ``` **New Code**: ```c if (is_first_time_startup()) { // 1. Generate keys and set database path first_time_startup_sequence(&cli_options); // 2. Create database with schema init_database(g_database_path); // 3. Populate ALL config values atomically (defaults + pubkeys) if (populate_all_config_values_atomic(&cli_options) != 0) { DEBUG_ERROR("Failed to populate config values"); return EXIT_FAILURE; } // 4. Apply CLI overrides atomically (separate transaction) if (apply_cli_overrides_atomic(&cli_options) != 0) { DEBUG_ERROR("Failed to apply CLI overrides"); return EXIT_FAILURE; } // 5. Store relay private key securely store_relay_private_key(relay_privkey); // 6. Load complete config into cache refresh_unified_cache_from_table(); } ``` **Testing**: - Verify first-time startup creates complete config - Verify CLI overrides applied correctly - Verify cache loads complete config - Verify error handling at each step --- ### Step 3.2: Update Existing Relay Startup Branch **Location**: `src/main.c` (around lines 1741-1928) **Current Code**: ```c else { char** existing_files = find_existing_db_files(); char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]); startup_existing_relay(relay_pubkey); init_database(g_database_path); // Current approach - unclear when overrides applied populate_default_config_values(); if (cli_options.port_override > 0) { // ... override logic ... } refresh_unified_cache_from_table(); } ``` **New Code**: ```c else { // 1. Discover existing database char** existing_files = find_existing_db_files(); if (!existing_files || !existing_files[0]) { DEBUG_ERROR("No existing database files found"); return EXIT_FAILURE; } char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]); startup_existing_relay(relay_pubkey); // 2. Open existing database init_database(g_database_path); // 3. Validate config table completeness (populate missing keys) if (validate_config_table_completeness() != 0) { DEBUG_ERROR("Failed to validate config table"); return EXIT_FAILURE; } // 4. Apply CLI overrides if present (separate transaction) if (has_cli_overrides(&cli_options)) { if (apply_cli_overrides_atomic(&cli_options) != 0) { DEBUG_ERROR("Failed to apply CLI overrides"); return EXIT_FAILURE; } } // 5. Load complete config into cache refresh_unified_cache_from_table(); } ``` **Testing**: - Verify existing relay startup with complete config - Verify missing keys populated - Verify CLI overrides applied when present - Verify no changes when no overrides - Verify cache loads correctly --- ## Phase 4: Deprecate Old Functions ### Step 4.1: Mark Functions as Deprecated **Location**: `src/config.c` **Functions to Deprecate**: 1. `populate_default_config_values()` - replaced by `populate_all_config_values_atomic()` 2. `add_pubkeys_to_config_table()` - logic moved to `populate_all_config_values_atomic()` **Changes**: ```c // Mark as deprecated in comments // DEPRECATED: Use populate_all_config_values_atomic() instead // This function will be removed in a future version int populate_default_config_values(void) { // ... existing implementation ... } // DEPRECATED: Use populate_all_config_values_atomic() instead // This function will be removed in a future version int add_pubkeys_to_config_table(void) { // ... existing implementation ... } ``` --- ## Phase 5: Testing Strategy ### Unit Tests 1. **Test `populate_all_config_values_atomic()`** - Test with valid cli_options - Test transaction rollback on error - Test all config keys inserted - Test pubkeys inserted correctly 2. **Test `apply_cli_overrides_atomic()`** - Test port override - Test admin_pubkey override - Test relay_privkey override - Test multiple overrides - Test no overrides - Test transaction rollback on error 3. **Test `validate_config_table_completeness()`** - Test with complete config - Test with missing keys - Test population of missing keys 4. **Test `has_cli_overrides()`** - Test with each override type - Test with no overrides - Test with NULL cli_options ### Integration Tests 1. **First-Time Startup** ```bash # Clean environment rm -f *.db # Start relay with defaults ./build/c_relay_x86 # Verify config table complete sqlite3 .db "SELECT COUNT(*) FROM config;" # Expected: 20+ rows (all defaults + pubkeys) # Verify cache loaded # Check relay.log for cache refresh message ``` 2. **First-Time Startup with CLI Overrides** ```bash # Clean environment rm -f *.db # Start relay with port override ./build/c_relay_x86 --port 9999 # Verify port override applied sqlite3 .db "SELECT value FROM config WHERE key='relay_port';" # Expected: 9999 ``` 3. **Restart with Existing Database** ```bash # Start relay (creates database) ./build/c_relay_x86 # Stop relay pkill -f c_relay_ # Restart relay ./build/c_relay_x86 # Verify config unchanged # Check relay.log for validation message ``` 4. **Restart with CLI Overrides** ```bash # Start relay (creates database) ./build/c_relay_x86 # Stop relay pkill -f c_relay_ # Restart with port override ./build/c_relay_x86 --port 9999 # Verify port override applied sqlite3 .db "SELECT value FROM config WHERE key='relay_port';" # Expected: 9999 ``` ### Regression Tests Run existing test suite to ensure no breakage: ```bash ./tests/run_all_tests.sh ``` --- ## Phase 6: Documentation Updates ### Files to Update 1. **docs/configuration_guide.md** - Update startup sequence description - Document new atomic config creation - Document CLI override behavior 2. **docs/startup_flows_complete.md** - Update with new flow diagrams - Document new function calls 3. **README.md** - Update CLI options documentation - Document override behavior --- ## Implementation Timeline ### Week 1: Core Functions - Day 1-2: Implement `populate_all_config_values_atomic()` - Day 3-4: Implement `apply_cli_overrides_atomic()` - Day 5: Implement `validate_config_table_completeness()` and `has_cli_overrides()` ### Week 2: Integration - Day 1-2: Update main.c startup flow - Day 3-4: Testing and bug fixes - Day 5: Documentation updates ### Week 3: Cleanup - Day 1-2: Deprecate old functions - Day 3-4: Final testing and validation - Day 5: Code review and merge --- ## Risk Mitigation ### Potential Issues 1. **Database Lock Contention** - Risk: Multiple transactions could cause locks - Mitigation: Use BEGIN IMMEDIATE for write transactions 2. **Cache Invalidation Timing** - Risk: Cache could be read before overrides applied - Mitigation: Invalidate cache immediately after overrides 3. **Backward Compatibility** - Risk: Existing databases might have incomplete config - Mitigation: `validate_config_table_completeness()` handles this 4. **Transaction Rollback** - Risk: Partial config on error - Mitigation: All operations in transactions with proper rollback --- ## Success Criteria 1. ✅ All config values created atomically in first-time startup 2. ✅ CLI overrides applied in separate atomic transaction 3. ✅ Existing databases validated and missing keys populated 4. ✅ Cache only loaded after complete config exists 5. ✅ All existing tests pass 6. ✅ No race conditions in config creation 7. ✅ Clear separation between config creation and override phases --- ## Rollback Plan If issues arise during implementation: 1. **Revert main.c changes** - restore original startup flow 2. **Keep new functions** - they can coexist with old code 3. **Add feature flag** - allow toggling between old and new behavior 4. **Gradual migration** - enable new behavior per scenario ```c // Feature flag approach #define USE_ATOMIC_CONFIG_CREATION 1 #if USE_ATOMIC_CONFIG_CREATION // New atomic approach populate_all_config_values_atomic(&cli_options); apply_cli_overrides_atomic(&cli_options); #else // Old incremental approach populate_default_config_values(); // ... existing code ... #endif ```