From d3bd6b8514ddc389adb82c2a28e0ff14c3baaa30 Mon Sep 17 00:00:00 2001 From: Justin Filip Date: Thu, 15 Oct 2026 19:09:06 -0500 Subject: [PATCH] Add a secondary DATUM pool for failover. Keep mining when the primary connection fails by using a configured backup pool, and show which pool is connected. --- doc/example_datum_gateway_config.json | 3 + src/datum_api.c | 119 +++++++++++++++++- src/datum_conf.c | 19 +++ src/datum_conf.h | 5 +- src/datum_protocol.c | 173 +++++++++++++++++++++++--- src/datum_protocol.h | 18 +++ src/datum_protocol_tests.c | 146 ++++++++++++++++++++++ src/datum_utils.c | 8 ++ src/datum_utils.h | 1 + www/config.html | 30 ++++- www/home.html | 6 +- 11 files changed, 505 insertions(+), 23 deletions(-) diff --git a/doc/example_datum_gateway_config.json b/doc/example_datum_gateway_config.json index 667db6d7..780da0d5 100644 --- a/doc/example_datum_gateway_config.json +++ b/doc/example_datum_gateway_config.json @@ -28,6 +28,9 @@ "log_level_file": 1 }, "datum": { + "pool_host_secondary": "", + "pool_port_secondary": 28915, + "pool_pubkey_secondary": "", "migration_max_seconds": 86400, "pool_pass_workers": true, "pool_pass_full_users": true, diff --git a/src/datum_api.c b/src/datum_api.c index 6d7f52ea..12d392d7 100644 --- a/src/datum_api.c +++ b/src/datum_api.c @@ -131,6 +131,8 @@ bool datum_api_var_DATUM_CONNECTION_STATUS(struct buf * const buf, const T_DATUM const char *colour = "lime"; const char *s, *s2 = ""; const char * const bt_err = datum_blocktemplates_error; + T_DATUM_POOL_ENDPOINT_VIEW pool_ep; + datum_protocol_active_pool(&pool_ep); if (!datum_protocol_abw_health_ok()) { colour = "red"; s = "CRITICAL: "; @@ -144,6 +146,12 @@ bool datum_api_var_DATUM_CONNECTION_STATUS(struct buf * const buf, const T_DATUM s = "Initialising..."; } else if (datum_protocol_is_active()) { s = "Connected and Ready"; + if (pool_ep.role == DATUM_POOL_ENDPOINT_SECONDARY) { + colour = "yellow"; + s2 = " (secondary pool active)"; + } else if (pool_ep.role == DATUM_POOL_ENDPOINT_MIGRATION) { + s2 = " (migrated)"; + } } else if (datum_config.datum_pooled_mining_only && datum_config.datum_pool_host[0]) { colour = "red"; s = "Not Ready"; @@ -156,11 +164,18 @@ bool datum_api_var_DATUM_CONNECTION_STATUS(struct buf * const buf, const T_DATUM return buf_printf(buf, " %s%s", colour, s, s2); } bool datum_api_var_DATUM_POOL_HOST(struct buf * const buf, const T_DATUM_API_DASH_VARS *vardata) { + (void)vardata; if (datum_config.datum_pool_host[0]) { return buf_printf(buf, "%s:%u", datum_config.datum_pool_host, (unsigned)datum_config.datum_pool_port); - } else { - return buf_strcat(buf, "N/A"); } + return buf_strcat(buf, "N/A"); +} +bool datum_api_var_DATUM_POOL_SECONDARY(struct buf * const buf, const T_DATUM_API_DASH_VARS *vardata) { + (void)vardata; + if (datum_config.datum_pool_host_secondary[0]) { + return buf_printf(buf, "%s:%u", datum_config.datum_pool_host_secondary, (unsigned)datum_config.datum_pool_port_secondary); + } + return buf_strcat(buf, "Not configured"); } bool datum_api_var_DATUM_POOL_TAG(struct buf * const buf, const T_DATUM_API_DASH_VARS *vardata) { if (!buf_strcat(buf, "\"")) return false; @@ -179,6 +194,9 @@ bool datum_api_var_DATUM_POOL_DIFF(struct buf * const buf, const T_DATUM_API_DAS return buf_datum_format_difficulty(buf, datum_pdiff_to_diff(datum_config.override_vardiff_min)); } bool datum_api_var_DATUM_POOL_PUBKEY(struct buf * const buf, const T_DATUM_API_DASH_VARS *vardata) { + T_DATUM_POOL_ENDPOINT_VIEW pool_ep; + datum_protocol_active_pool(&pool_ep); + if (pool_ep.pubkey[0]) return buf_strcat(buf, pool_ep.pubkey); return buf_strcat(buf, datum_config.datum_pool_pubkey); } bool datum_api_var_STRATUM_ACTIVE_THREADS(struct buf * const buf, const T_DATUM_API_DASH_VARS *vardata) { @@ -268,6 +286,7 @@ DATUM_API_VarEntry var_entries[] = { {"DATUM_SHARES_REJECTED", datum_api_var_DATUM_SHARES_REJECTED}, {"DATUM_CONNECTION_STATUS", datum_api_var_DATUM_CONNECTION_STATUS}, {"DATUM_POOL_HOST", datum_api_var_DATUM_POOL_HOST}, + {"DATUM_POOL_SECONDARY", datum_api_var_DATUM_POOL_SECONDARY}, {"DATUM_POOL_TAG", datum_api_var_DATUM_POOL_TAG}, {"DATUM_MINER_TAG", datum_api_var_DATUM_MINER_TAG}, {"DATUM_POOL_DIFF", datum_api_var_DATUM_POOL_DIFF}, @@ -1342,6 +1361,38 @@ bool datum_api_config_set(const char * const key, const char * const val, struct datum_api_json_modify_new("datum", "pool_pubkey", json_string(val)); // TODO: apply change without restarting // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) + } else if (0 == strcmp(key, "datum_pool_host_secondary")) { + if (0 == strcmp(val, datum_config.datum_pool_host_secondary)) return true; + if (strlen(val) > 1023) { + json_array_append_new(errors, json_string_nocheck("Secondary Pool Host is too long")); + return false; + } + if (!datum_api_config_require_restart(status)) return false; + strcpy(datum_config.datum_pool_host_secondary, val); + datum_api_json_modify_new("datum", "pool_host_secondary", json_string(val)); + } else if (0 == strcmp(key, "datum_pool_port_secondary")) { + const int val_int = datum_atoi_strict(val, strlen(val)); + if (val_int == datum_config.datum_pool_port_secondary) return true; + if (val_int > 65535 || val_int < 1) { + json_array_append_new(errors, json_string_nocheck("Secondary Pool Port must be between 1 and 65535")); + return false; + } + if (!datum_api_config_require_restart(status)) return false; + datum_config.datum_pool_port_secondary = val_int; + datum_api_json_modify_new("datum", "pool_port_secondary", json_integer(val_int)); + } else if (0 == strcmp(key, "datum_pool_pubkey_secondary")) { + if (0 == strcmp(val, datum_config.datum_pool_pubkey_secondary)) return true; + if (strlen(val) > 1023) { + json_array_append_new(errors, json_string_nocheck("Secondary Pool Pubkey is too long")); + return false; + } + if (val[0] && !datum_pubkey_hex_is_valid(val)) { + json_array_append_new(errors, json_string_nocheck("Secondary Pool Pubkey must be empty or 128 hexadecimal characters")); + return false; + } + if (!datum_api_config_require_restart(status)) return false; + strcpy(datum_config.datum_pool_pubkey_secondary, val); + datum_api_json_modify_new("datum", "pool_pubkey_secondary", json_string(val)); } else if (0 == strcmp(key, "stratum_fingerprint_miners")) { bool val_bool; if (!datum_str_to_bool_strict(val, &val_bool)) { @@ -1426,6 +1477,38 @@ static void datum_api_test_config_var(const char * const name, const struct datu buf_destroy(&output); } +static bool datum_api_secondary_pool_change_ok(json_t * const posted, struct datum_api_config_set_status * const status) { + const char *host = datum_config.datum_pool_host_secondary; + const char *pubkey = datum_config.datum_pool_pubkey_secondary; + int port = datum_config.datum_pool_port_secondary; + bool ok = true; + json_t *j; + + j = json_object_get(posted, "datum_pool_host_secondary"); + if (j && json_is_string(j)) host = json_string_value(j); + j = json_object_get(posted, "datum_pool_pubkey_secondary"); + if (j && json_is_string(j)) pubkey = json_string_value(j); + j = json_object_get(posted, "datum_pool_port_secondary"); + if (j && json_is_string(j)) { + port = datum_atoi_strict(json_string_value(j), json_string_length(j)); + } + if (!host || !host[0]) return true; + if (!pubkey || !datum_pubkey_hex_is_valid(pubkey)) { + json_array_append_new(status->errors, json_string_nocheck("Secondary pool pubkey must be 128 hexadecimal characters when a secondary host is set")); + ok = false; + } + if (port < 1 || port > 65535) { + json_array_append_new(status->errors, json_string_nocheck("Secondary pool port must be between 1 and 65535")); + ok = false; + } + if (!ok) { + json_object_del(posted, "datum_pool_host_secondary"); + json_object_del(posted, "datum_pool_port_secondary"); + json_object_del(posted, "datum_pool_pubkey_secondary"); + } + return ok; +} + void datum_api_tests(void) { char * const executable_path = datum_executable_path; datum_executable_path = NULL; @@ -1450,6 +1533,9 @@ void datum_api_tests(void) { {"datum_pool_host", "other.example"}, {"datum_pool_port", "4321"}, {"datum_pool_pubkey", "new-key"}, + {"datum_pool_host_secondary", "backup.example"}, + {"datum_pool_port_secondary", "28916"}, + {"datum_pool_pubkey_secondary", "abcd"}, {"bitcoind_work_update_seconds", "120"}, }; for (size_t i = 0; i < sizeof(changes) / sizeof(changes[0]); ++i) { @@ -1506,6 +1592,34 @@ void datum_api_tests(void) { datum_api_test_config_var("disabled:*restart_ro", &context, " disabled"); datum_test(datum_api_config_set("datum_pool_port", "4321", &status)); datum_test(datum_config.datum_pool_port == 4321 && status.need_restart); + char secondary_pk[129]; + memset(secondary_pk, 'a', 128); + secondary_pk[128] = '\0'; + datum_test(!datum_api_config_set("datum_pool_pubkey_secondary", "abcd", &status)); + datum_test(!datum_config.datum_pool_pubkey_secondary[0]); + datum_test(datum_api_config_set("datum_pool_host_secondary", "backup.example", &status)); + datum_test(!strcmp(datum_config.datum_pool_host_secondary, "backup.example")); + datum_test(datum_api_config_set("datum_pool_port_secondary", "28916", &status)); + datum_test(datum_config.datum_pool_port_secondary == 28916); + datum_test(datum_api_config_set("datum_pool_pubkey_secondary", secondary_pk, &status)); + datum_test(!strcmp(datum_config.datum_pool_pubkey_secondary, secondary_pk)); + datum_test(status.need_restart); + datum_api_test_config_var("datum_pool_host_secondary", &context, "backup.example"); + datum_api_test_config_var("datum_pool_port_secondary", &context, "28916"); + datum_api_test_config_var("datum_pool_pubkey_secondary", &context, secondary_pk); + json_t *posted = json_object(); + json_object_set_new(posted, "datum_pool_host_secondary", json_string("other.example")); + json_object_set_new(posted, "datum_pool_pubkey_secondary", json_string("zz")); + datum_test(!datum_api_secondary_pool_change_ok(posted, &status)); + datum_test(json_object_get(posted, "datum_pool_host_secondary") == NULL); + json_decref(posted); + posted = json_object(); + json_object_set_new(posted, "datum_pool_host_secondary", json_string("other.example")); + json_object_set_new(posted, "datum_pool_pubkey_secondary", json_string(secondary_pk)); + json_object_set_new(posted, "datum_pool_port_secondary", json_string("28915")); + datum_test(datum_api_secondary_pool_change_ok(posted, &status)); + datum_test(json_object_get(posted, "datum_pool_host_secondary") != NULL); + json_decref(posted); status.cannot_restart = EACCES; datum_test(!datum_api_config_require_restart(&status)); datum_test(status.need_restart); @@ -1602,6 +1716,7 @@ enum MHD_Result datum_api_config_post(struct MHD_Connection * const connection, .errors = errors, .cannot_restart = datum_reexec_check(), }; + datum_api_secondary_pool_change_ok(j, &status); json_object_foreach(j, key, j_it) { datum_api_config_set(key, json_string_value(j_it), &status); } diff --git a/src/datum_conf.c b/src/datum_conf.c index 3a890ffc..2deafaf6 100644 --- a/src/datum_conf.c +++ b/src/datum_conf.c @@ -186,6 +186,15 @@ const T_DATUM_CONFIG_ITEM datum_config_options[] = { .required = false, .ptr = &datum_config.datum_pool_port, .default_int = 28915 }, { .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_pubkey", .description = "Public key of the DATUM server for initiating encrypted connection. Get from secure location, or set to empty to auto-fetch.", .required = false, .ptr = datum_config.datum_pool_pubkey, .default_string[0] = "dbb11fa0c2b5403e4f798fa6071bb97e6079d219598366032fdf2ae01962b13c5e66e2be7d6b008f0b2603f3e6f6fc64768fa786c8129c46d3e30a5867734b62", .max_string_len = sizeof(datum_config.datum_pool_pubkey) }, + { .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_host_secondary", .description = "Backup DATUM server host/ip. Used when the primary pool cannot be reached. The gateway stays with whichever pool is connected, and tries the other after that connection fails. Empty disables failover.", + .example_default = true, + .required = false, .ptr = datum_config.datum_pool_host_secondary, .default_string[0] = "", .max_string_len = sizeof(datum_config.datum_pool_host_secondary) }, + { .var_type = DATUM_CONF_INT, .category = "datum", .name = "pool_port_secondary", .description = "Backup DATUM server port", + .example_default = true, + .required = false, .ptr = &datum_config.datum_pool_port_secondary, .default_int = 28915 }, + { .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_pubkey_secondary", .description = "Public key of the backup DATUM server. Required when pool_host_secondary is set.", + .example_default = true, + .required = false, .ptr = datum_config.datum_pool_pubkey_secondary, .default_string[0] = "", .max_string_len = sizeof(datum_config.datum_pool_pubkey_secondary) }, { .var_type = DATUM_CONF_INT, .category = "datum", .name = "migration_max_seconds", .description = "Maximum time to remain away from the configured DATUM server after a server-requested migration (0 disables migration)", .example_default = true, .required = false, .ptr = &datum_config.datum_pool_migration_max_seconds, .default_int = 86400 }, @@ -743,6 +752,16 @@ int datum_read_config(const char *conffile) { DLOG_FATAL("Pooled mining only is set to true, but pool host is not specified."); return 0; } + if (datum_config.datum_pool_host_secondary[0] != '\0') { + if (datum_config.datum_pool_port_secondary < 1 || datum_config.datum_pool_port_secondary > 65535) { + DLOG_FATAL("datum.pool_port_secondary must be between 1 and 65535"); + return 0; + } + if (!datum_pubkey_hex_is_valid(datum_config.datum_pool_pubkey_secondary)) { + DLOG_FATAL("datum.pool_pubkey_secondary must be 128 hexadecimal characters when datum.pool_host_secondary is set"); + return 0; + } + } // Save some multiplication later datum_config.datum_protocol_global_timeout_ms = datum_config.datum_protocol_global_timeout * 1000; diff --git a/src/datum_conf.h b/src/datum_conf.h index f230419c..60a7be99 100644 --- a/src/datum_conf.h +++ b/src/datum_conf.h @@ -162,6 +162,10 @@ typedef struct { char datum_pool_host[1024]; int datum_pool_port; + char datum_pool_pubkey[1024]; + char datum_pool_host_secondary[1024]; + int datum_pool_port_secondary; + char datum_pool_pubkey_secondary[1024]; char datum_pool_migration_host[1024]; int datum_pool_migration_port; char datum_pool_migration_pubkey[129]; @@ -170,7 +174,6 @@ typedef struct { bool datum_pool_pass_workers; bool datum_pool_pass_full_users; bool datum_pooled_mining_only; - char datum_pool_pubkey[1024]; int datum_protocol_global_timeout; uint64_t datum_protocol_global_timeout_ms; diff --git a/src/datum_protocol.c b/src/datum_protocol.c index 3a5eb96b..dfc00a38 100644 --- a/src/datum_protocol.c +++ b/src/datum_protocol.c @@ -109,6 +109,7 @@ pthread_mutex_t datum_protocol_sender_stage1_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t datum_protocol_send_buffer_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t datum_protocol_migration_lock = PTHREAD_MUTEX_INITIALIZER; static uint64_t datum_protocol_current_migration_deadline_ms; +static bool datum_protocol_intentional_reconnect; #define DATUM_BULK_QUEUE_CAPACITY 2 typedef struct { @@ -2105,6 +2106,11 @@ static void datum_protocol_log_migration_target( (unsigned int)port); } +static int datum_protocol_reconnect_for_migration(void) { + datum_protocol_intentional_reconnect = true; + return -1; +} + int datum_protocol_migration_request(int len, const unsigned char *data) { size_t host_len; size_t expected_len; @@ -2135,7 +2141,7 @@ int datum_protocol_migration_request(int len, const unsigned char *data) { datum_protocol_current_migration_deadline_ms = 0; pthread_mutex_unlock(&datum_protocol_migration_lock); DLOG_INFO("DATUM server requested return to configured server"); - return -1; + return datum_protocol_reconnect_for_migration(); } // revision=0 | action=0 | host_len:u16 | host | port:u16 | pubkey:64 | FE @@ -2176,7 +2182,7 @@ int datum_protocol_migration_request(int len, const unsigned char *data) { if (deadline_ms <= current_time_millis()) { pthread_mutex_unlock(&datum_protocol_migration_lock); DLOG_WARN("Ignoring DATUM redirect because the migration time limit expired"); - return -1; + return datum_protocol_reconnect_for_migration(); } } memcpy(datum_config.datum_pool_migration_host, data + 4, host_len); @@ -2188,7 +2194,101 @@ int datum_protocol_migration_request(int len, const unsigned char *data) { datum_protocol_log_migration_target( "DATUM server requested migration to", data + 4, host_len, port); - return -1; + return datum_protocol_reconnect_for_migration(); +} + +static T_DATUM_POOL_ENDPOINT_ROLE datum_pool_failover_role = DATUM_POOL_ENDPOINT_PRIMARY; +static T_DATUM_POOL_ENDPOINT_VIEW datum_pool_active; + +static bool datum_protocol_secondary_configured(void) { + return datum_config.datum_pool_host_secondary[0] != '\0'; +} + +static void datum_protocol_store_endpoint_locked( + const char *host, int port, const char *pubkey, T_DATUM_POOL_ENDPOINT_ROLE role) { + snprintf(datum_pool_active.host, sizeof(datum_pool_active.host), "%s", host ? host : ""); + datum_pool_active.port = port; + snprintf(datum_pool_active.pubkey, sizeof(datum_pool_active.pubkey), "%s", pubkey ? pubkey : ""); + datum_pool_active.role = datum_pool_active.host[0] ? role : DATUM_POOL_ENDPOINT_NONE; +} + +static void datum_protocol_store_configured_locked(T_DATUM_POOL_ENDPOINT_ROLE role) { + if (role == DATUM_POOL_ENDPOINT_SECONDARY && datum_protocol_secondary_configured()) { + datum_pool_failover_role = DATUM_POOL_ENDPOINT_SECONDARY; + datum_protocol_store_endpoint_locked( + datum_config.datum_pool_host_secondary, + datum_config.datum_pool_port_secondary, + datum_config.datum_pool_pubkey_secondary, + DATUM_POOL_ENDPOINT_SECONDARY); + return; + } + datum_pool_failover_role = DATUM_POOL_ENDPOINT_PRIMARY; + datum_protocol_store_endpoint_locked( + datum_config.datum_pool_host, + datum_config.datum_pool_port, + datum_config.datum_pool_pubkey, + DATUM_POOL_ENDPOINT_PRIMARY); +} + +void datum_protocol_active_pool(T_DATUM_POOL_ENDPOINT_VIEW *out) { + if (!out) return; + pthread_mutex_lock(&datum_protocol_migration_lock); + if (datum_pool_active.host[0]) { + *out = datum_pool_active; + } else if (datum_pool_failover_role == DATUM_POOL_ENDPOINT_SECONDARY && + datum_protocol_secondary_configured()) { + memset(out, 0, sizeof(*out)); + snprintf(out->host, sizeof(out->host), "%s", datum_config.datum_pool_host_secondary); + out->port = datum_config.datum_pool_port_secondary; + snprintf(out->pubkey, sizeof(out->pubkey), "%s", datum_config.datum_pool_pubkey_secondary); + out->role = DATUM_POOL_ENDPOINT_SECONDARY; + } else if (datum_config.datum_pool_host[0]) { + memset(out, 0, sizeof(*out)); + snprintf(out->host, sizeof(out->host), "%s", datum_config.datum_pool_host); + out->port = datum_config.datum_pool_port; + snprintf(out->pubkey, sizeof(out->pubkey), "%s", datum_config.datum_pool_pubkey); + out->role = DATUM_POOL_ENDPOINT_PRIMARY; + } else { + memset(out, 0, sizeof(*out)); + } + pthread_mutex_unlock(&datum_protocol_migration_lock); +} + +void datum_protocol_note_pool_down(T_DATUM_POOL_ENDPOINT_ROLE role) { + T_DATUM_POOL_ENDPOINT_ROLE next = role; + bool changed = false; + + pthread_mutex_lock(&datum_protocol_migration_lock); + if (role == DATUM_POOL_ENDPOINT_PRIMARY && datum_protocol_secondary_configured()) { + next = DATUM_POOL_ENDPOINT_SECONDARY; + changed = datum_pool_failover_role != DATUM_POOL_ENDPOINT_SECONDARY; + datum_protocol_store_configured_locked(DATUM_POOL_ENDPOINT_SECONDARY); + } else if (role == DATUM_POOL_ENDPOINT_SECONDARY) { + next = DATUM_POOL_ENDPOINT_PRIMARY; + changed = datum_pool_failover_role != DATUM_POOL_ENDPOINT_PRIMARY; + datum_protocol_store_configured_locked(DATUM_POOL_ENDPOINT_PRIMARY); + } + pthread_mutex_unlock(&datum_protocol_migration_lock); + + if (changed && next == DATUM_POOL_ENDPOINT_SECONDARY) { + DLOG_WARN("Primary DATUM pool is unavailable. Next connection will use the secondary pool."); + } else if (changed && next == DATUM_POOL_ENDPOINT_PRIMARY) { + DLOG_WARN("Secondary DATUM pool is unavailable. Next connection will use the primary pool."); + } +} + +void datum_protocol_reset_pool_failover(void) { + pthread_mutex_lock(&datum_protocol_migration_lock); + datum_pool_failover_role = DATUM_POOL_ENDPOINT_PRIMARY; + memset(&datum_pool_active, 0, sizeof(datum_pool_active)); + datum_protocol_intentional_reconnect = false; + pthread_mutex_unlock(&datum_protocol_migration_lock); +} + +bool datum_protocol_session_failure_switches_pool(bool migrated, bool migration_return) { + const bool intentional = datum_protocol_intentional_reconnect; + datum_protocol_intentional_reconnect = false; + return !migrated && !migration_return && !intentional; } bool datum_protocol_take_connect_endpoint( @@ -2210,13 +2310,17 @@ bool datum_protocol_take_connect_endpoint( selected_host = datum_config.datum_pool_migration_host; selected_port = datum_config.datum_pool_migration_port; selected_pubkey = datum_config.datum_pool_migration_pubkey; + datum_protocol_store_endpoint_locked( + selected_host, selected_port, selected_pubkey, + DATUM_POOL_ENDPOINT_MIGRATION); datum_protocol_current_migration_deadline_ms = datum_config.datum_pool_migration_deadline_ms; migrated = true; } else { - selected_host = datum_config.datum_pool_host; - selected_port = datum_config.datum_pool_port; - selected_pubkey = datum_config.datum_pool_pubkey; + datum_protocol_store_configured_locked(datum_pool_failover_role); + selected_host = datum_pool_active.host; + selected_port = datum_pool_active.port; + selected_pubkey = datum_pool_active.pubkey; datum_protocol_current_migration_deadline_ms = 0; } @@ -2980,6 +3084,7 @@ void *datum_protocol_client(void *args) { struct timeval start, now; int ret,i,n; datum_protocol_client_active = 1; + datum_protocol_intentional_reconnect = false; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; @@ -3027,15 +3132,23 @@ void *datum_protocol_client(void *args) { const bool migrated = datum_protocol_take_connect_endpoint( pool_host, sizeof(pool_host), &pool_port, pool_pubkey, sizeof(pool_pubkey)); + T_DATUM_POOL_ENDPOINT_VIEW chosen_pool; + datum_protocol_active_pool(&chosen_pool); + const T_DATUM_POOL_ENDPOINT_ROLE connect_role = chosen_pool.role; + const char *connect_message = "Connecting to primary DATUM pool"; if (migrated) { - datum_protocol_log_migration_target( - "Connecting to one-time DATUM migration endpoint", - (const unsigned char *)pool_host, strlen(pool_host), - (uint16_t)pool_port); + connect_message = "Connecting to one-time DATUM migration endpoint"; + } else if (connect_role == DATUM_POOL_ENDPOINT_SECONDARY) { + connect_message = "Connecting to secondary DATUM pool"; } + datum_protocol_log_migration_target( + connect_message, + (const unsigned char *)pool_host, strlen(pool_host), + (uint16_t)pool_port); memset(&pool_keys, 0, sizeof(DATUM_ENC_KEYS)); - if (datum_pubkey_to_struct(pool_pubkey, &pool_keys) != 0) { + if (!datum_pubkey_hex_is_valid(pool_pubkey) || datum_pubkey_to_struct(pool_pubkey, &pool_keys) != 0) { DLOG_ERROR("DATUM connection public key is invalid"); + if (!migrated) datum_protocol_note_pool_down(connect_role); datum_protocol_client_active = 0; return NULL; } @@ -3045,6 +3158,7 @@ void *datum_protocol_client(void *args) { if ((ret = getaddrinfo(pool_host, port_str, &hints, &res)) != 0) { DLOG_ERROR("getaddrinfo: %s", gai_strerror(ret)); + if (!migrated) datum_protocol_note_pool_down(connect_role); datum_protocol_client_active = 0; return NULL; } @@ -3096,7 +3210,8 @@ void *datum_protocol_client(void *args) { freeaddrinfo(res); if (sockfd == -1) { - DLOG_FATAL("Could not connect to DATUM server!"); + DLOG_FATAL("Could not connect to DATUM server %s:%d", pool_host, pool_port); + if (!migrated) datum_protocol_note_pool_down(connect_role); datum_protocol_client_active = 0; return NULL; } @@ -3123,13 +3238,15 @@ void *datum_protocol_client(void *args) { datum_last_accepted_share_tsms = 0; datum_last_accepted_share_local_tsms = 0; latest_server_msg_tsms = current_time_millis(); + bool migration_return = false; while (1) { i++; datum_protocol_mainloop_tsms = current_time_millis(); if (datum_protocol_migration_expired(datum_protocol_mainloop_tsms)) { - DLOG_INFO("DATUM migration time limit reached; returning to configured server"); + DLOG_INFO("DATUM migration time limit reached; returning to the selected configured pool"); + migration_return = true; break; } @@ -3364,6 +3481,9 @@ void *datum_protocol_client(void *args) { // Must clean up this thread, as it can be restarted by the main thread. DLOG_DEBUG("DATUM Protocol Thread is exiting."); + if (datum_protocol_session_failure_switches_pool(migrated, migration_return)) { + datum_protocol_note_pool_down(connect_role); + } close(sockfd); close(epollfd); datum_protocol_client_active = 0; @@ -3443,14 +3563,35 @@ int datum_protocol_init(void) { DLOG_INFO("Our public keys:"); datum_encrypt_log_pubkeys(&local_datum_keys); - if (datum_pubkey_to_struct(datum_config.datum_pool_pubkey, &pool_keys) != 0) { - DLOG_WARN("Pool pubkey not specified or invalid."); - return -1; + DATUM_ENC_KEYS secondary_keys; + bool secondary_ready = false; + memset(&secondary_keys, 0, sizeof(secondary_keys)); + if (datum_config.datum_pool_host_secondary[0] && + datum_pubkey_hex_is_valid(datum_config.datum_pool_pubkey_secondary) && + datum_pubkey_to_struct(datum_config.datum_pool_pubkey_secondary, &secondary_keys) == 0) { + secondary_keys.is_remote = true; + secondary_ready = true; + } + const bool primary_key_ok = + datum_pubkey_hex_is_valid(datum_config.datum_pool_pubkey) && + datum_pubkey_to_struct(datum_config.datum_pool_pubkey, &pool_keys) == 0; + if (!primary_key_ok) { + if (!secondary_ready) { + DLOG_WARN("Pool pubkey not specified or invalid."); + return -1; + } + DLOG_WARN("Primary pool pubkey is invalid. Connecting to the secondary pool."); + pool_keys = secondary_keys; + datum_protocol_note_pool_down(DATUM_POOL_ENDPOINT_PRIMARY); } pool_keys.is_remote = true; DLOG_INFO("Pool's public keys: (You should periodically verify that these are what you expect!)"); datum_encrypt_log_pubkeys(&pool_keys); + if (secondary_ready && primary_key_ok) { + DLOG_INFO("Secondary pool's public keys: (You should periodically verify that these are what you expect!)"); + datum_encrypt_log_pubkeys(&secondary_keys); + } datum_protocol_start_connector(); return 0; diff --git a/src/datum_protocol.h b/src/datum_protocol.h index dd27aecf..3216b47b 100644 --- a/src/datum_protocol.h +++ b/src/datum_protocol.h @@ -141,6 +141,20 @@ typedef struct { uint32_t version; } T_DATUM_PROTOCOL_POW; +typedef enum { + DATUM_POOL_ENDPOINT_NONE = 0, + DATUM_POOL_ENDPOINT_PRIMARY = 1, + DATUM_POOL_ENDPOINT_SECONDARY = 2, + DATUM_POOL_ENDPOINT_MIGRATION = 3 +} T_DATUM_POOL_ENDPOINT_ROLE; + +typedef struct { + char host[1024]; + int port; + char pubkey[1024]; + T_DATUM_POOL_ENDPOINT_ROLE role; +} T_DATUM_POOL_ENDPOINT_VIEW; + int datum_protocol_init(void); int datum_encrypt_generate_keys(DATUM_ENC_KEYS *keys); bool datum_protocol_is_active(void); @@ -149,6 +163,10 @@ void datum_increment_session_nonce(void *s); int datum_protocol_fetch_coinbaser(uint64_t value); int datum_protocol_coinbaser_fetch(void *s); int datum_protocol_migration_request(int len, const unsigned char *data); +void datum_protocol_active_pool(T_DATUM_POOL_ENDPOINT_VIEW *out); +void datum_protocol_note_pool_down(T_DATUM_POOL_ENDPOINT_ROLE role); +void datum_protocol_reset_pool_failover(void); +bool datum_protocol_session_failure_switches_pool(bool migrated, bool migration_return); bool datum_protocol_take_connect_endpoint( char *host, size_t host_size, diff --git a/src/datum_protocol_tests.c b/src/datum_protocol_tests.c index 0812fcdd..3ec0aa5f 100644 --- a/src/datum_protocol_tests.c +++ b/src/datum_protocol_tests.c @@ -980,6 +980,8 @@ static void datum_protocol_resume_tests(void) { static void datum_protocol_migration_tests(void) { global_config_t saved_config = datum_config; + datum_protocol_reset_pool_failover(); + datum_config.datum_pool_host_secondary[0] = '\0'; unsigned char payload[192] = {0}; unsigned char command[193] = {0xA4}; unsigned char home_payload[192] = {0}; @@ -1630,6 +1632,149 @@ static void datum_protocol_job_validation_bounds_test(void) { free(req); } +static void datum_protocol_failover_tests(void) { + global_config_t saved_config = datum_config; + char endpoint[sizeof(datum_config.datum_pool_host)]; + char endpoint_pubkey[sizeof(datum_config.datum_pool_pubkey)]; + char primary_pk[129]; + char secondary_pk[129]; + char bad_pk[129]; + int port; + T_DATUM_POOL_ENDPOINT_VIEW active; + + memset(primary_pk, 'a', 128); + primary_pk[128] = '\0'; + memset(secondary_pk, 'b', 128); + secondary_pk[128] = '\0'; + memset(bad_pk, 'g', 128); + bad_pk[128] = '\0'; + datum_test(datum_pubkey_hex_is_valid(primary_pk)); + datum_test(datum_pubkey_hex_is_valid(secondary_pk)); + datum_test(!datum_pubkey_hex_is_valid(bad_pk)); + datum_test(!datum_pubkey_hex_is_valid("")); + datum_test(!datum_pubkey_hex_is_valid("abcd")); + + datum_protocol_reset_pool_failover(); + strcpy(datum_config.datum_pool_host, "primary.example"); + datum_config.datum_pool_port = 28915; + strcpy(datum_config.datum_pool_pubkey, primary_pk); + datum_config.datum_pool_host_secondary[0] = '\0'; + datum_config.datum_pool_port_secondary = 28916; + datum_config.datum_pool_pubkey_secondary[0] = '\0'; + datum_config.datum_pool_migration_host[0] = '\0'; + datum_config.datum_pool_migration_port = 0; + datum_config.datum_pool_migration_pubkey[0] = '\0'; + + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "primary.example")); + datum_test(port == 28915); + datum_protocol_note_pool_down(DATUM_POOL_ENDPOINT_PRIMARY); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "primary.example")); + + strcpy(datum_config.datum_pool_host_secondary, "secondary.example"); + strcpy(datum_config.datum_pool_pubkey_secondary, secondary_pk); + datum_protocol_reset_pool_failover(); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "primary.example")); + datum_test(!strcmp(endpoint_pubkey, primary_pk)); + datum_protocol_active_pool(&active); + datum_test(active.role == DATUM_POOL_ENDPOINT_PRIMARY); + + datum_protocol_note_pool_down(DATUM_POOL_ENDPOINT_PRIMARY); + datum_protocol_active_pool(&active); + datum_test(active.role == DATUM_POOL_ENDPOINT_SECONDARY); + datum_test(!strcmp(active.host, "secondary.example")); + datum_test(active.port == 28916); + datum_test(!strcmp(active.pubkey, secondary_pk)); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "secondary.example")); + datum_test(port == 28916); + datum_test(!strcmp(endpoint_pubkey, secondary_pk)); + + strcpy(datum_config.datum_pool_migration_host, "mig.example"); + datum_config.datum_pool_migration_port = 1111; + strcpy(datum_config.datum_pool_migration_pubkey, primary_pk); + datum_test(datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "mig.example")); + datum_test(port == 1111); + datum_protocol_active_pool(&active); + datum_test(active.role == DATUM_POOL_ENDPOINT_MIGRATION); + datum_test(!datum_config.datum_pool_migration_host[0]); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "secondary.example")); + + datum_protocol_note_pool_down(DATUM_POOL_ENDPOINT_SECONDARY); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "primary.example")); + datum_test(port == 28915); + datum_protocol_note_pool_down(DATUM_POOL_ENDPOINT_MIGRATION); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "primary.example")); + + { + unsigned char migrate[160]; + const char mig_host[] = "mig-away.example"; + size_t n = 0; + const unsigned char bad_migrate[] = {1, 0, 0xFE}; + + datum_protocol_reset_pool_failover(); + datum_config.datum_pool_migration_host[0] = '\0'; + datum_config.datum_pool_migration_port = 0; + datum_config.datum_pool_migration_pubkey[0] = '\0'; + datum_config.datum_pool_migration_deadline_ms = 0; + datum_config.datum_pool_migration_max_seconds = 86400; + migrate[n++] = 0; + migrate[n++] = 0; + pk_u16le(migrate, n, sizeof(mig_host) - 1); n += 2; + memcpy(migrate + n, mig_host, sizeof(mig_host) - 1); n += sizeof(mig_host) - 1; + pk_u16le(migrate, n, 29634); n += 2; + for (size_t j = 0; j < 64; ++j) migrate[n++] = (unsigned char)j; + migrate[n++] = 0xFE; + + datum_test(datum_protocol_migration_request((int)n, migrate) == -1); + datum_test(!datum_protocol_session_failure_switches_pool(false, false)); + datum_protocol_active_pool(&active); + datum_test(active.role == DATUM_POOL_ENDPOINT_PRIMARY); + datum_test(datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, mig_host)); + datum_test(!datum_protocol_take_connect_endpoint( + endpoint, sizeof(endpoint), &port, + endpoint_pubkey, sizeof(endpoint_pubkey))); + datum_test(!strcmp(endpoint, "primary.example")); + datum_test(port == 28915); + + datum_test(datum_protocol_migration_request( + (int)sizeof(bad_migrate), bad_migrate) == 0); + datum_test(datum_protocol_session_failure_switches_pool(false, false)); + datum_test(!datum_protocol_session_failure_switches_pool(true, false)); + datum_test(!datum_protocol_session_failure_switches_pool(false, true)); + datum_protocol_active_pool(&active); + datum_test(active.role == DATUM_POOL_ENDPOINT_PRIMARY); + } + + datum_protocol_reset_pool_failover(); + datum_config = saved_config; +} + void datum_protocol_tests(void) { datum_protocol_hello_framing_offer_tests(); datum_protocol_receive_mode_tests(); @@ -1639,6 +1784,7 @@ void datum_protocol_tests(void) { datum_protocol_acceptance_watchdog_tests(); datum_protocol_config_v3_tests(); datum_protocol_migration_tests(); + datum_protocol_failover_tests(); datum_protocol_bulk_tests(); datum_protocol_resume_tests(); datum_protocol_abw_cache_tests(); diff --git a/src/datum_utils.c b/src/datum_utils.c index c46fb700..19e0fb39 100644 --- a/src/datum_utils.c +++ b/src/datum_utils.c @@ -176,6 +176,14 @@ unsigned char hex2bin_uchar(const char *in) { return hex_lookup[i]; } +bool datum_pubkey_hex_is_valid(const char *input) { + if (!input || strlen(input) != 128) return false; + for (size_t i = 0; i < 128; ++i) { + if (!isxdigit((unsigned char)input[i])) return false; + } + return true; +} + void uchar_to_hex(char *s, const unsigned char b) { // place the ASCII hexidecimal value for the unsigned char into the string at ptr s // this does NOT null terminate the string! diff --git a/src/datum_utils.h b/src/datum_utils.h index 47b94053..d6113ba0 100644 --- a/src/datum_utils.h +++ b/src/datum_utils.h @@ -77,6 +77,7 @@ uint64_t current_time_millis(void); uint64_t current_time_micros(void); uint64_t get_process_uptime_seconds(void); unsigned char hex2bin_uchar(const char *in); +bool datum_pubkey_hex_is_valid(const char *input); void build_hex_lookup(void); bool my_sha256(void *digest, const void *buffer, size_t length); void nbits_to_target(uint32_t nbits, uint8_t *target); diff --git a/www/config.html b/www/config.html index 37b8b1dd..01f15a2a 100644 --- a/www/config.html +++ b/www/config.html @@ -188,25 +188,49 @@

Basic

Pool

- +
- +
- +
+
+ +
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
diff --git a/www/home.html b/www/home.html index b01ea37e..ddbc5f64 100644 --- a/www/home.html +++ b/www/home.html @@ -68,9 +68,13 @@

Decentralized Client Stats

${DATUM_CONNECTION_STATUS} - Pool Host: + Primary Host: ${DATUM_POOL_HOST} + + Secondary Host: + ${DATUM_POOL_SECONDARY} + Pool Tag: ${DATUM_POOL_TAG}