From 14be65ee75b38742d22cf76019c07bf0a1de6c2d Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:59:55 +1000 Subject: [PATCH 01/12] Refactor client --- src/core/client.c | 8 +- src/core/client_init.c | 15 +- src/core/client_keys.c | 487 ++++++++---- src/core/client_network.c | 732 +++++++++++------- src/core/client_peers.c | 25 +- src/core/client_reqresp.c | 1080 ++++++++++++++++++--------- src/core/client_services_internal.h | 12 +- 7 files changed, 1569 insertions(+), 790 deletions(-) diff --git a/src/core/client.c b/src/core/client.c index 1b0005a..aad20a1 100644 --- a/src/core/client.c +++ b/src/core/client.c @@ -1190,7 +1190,7 @@ static lantern_client_error client_setup_validators( return LANTERN_CLIENT_ERR_CONFIG; } - if (configure_hash_sig_sources(client, options) != 0) + if (lantern_client_configure_hash_sig_sources(client, options) != 0) { lantern_log_error( "client", @@ -1241,7 +1241,7 @@ static lantern_client_error client_setup_validators( return LANTERN_CLIENT_ERR_VALIDATOR; } - if (load_hash_sig_keys(client) != 0) + if (lantern_client_load_hash_sig_keys(client) != 0) { return LANTERN_CLIENT_ERR_VALIDATOR; } @@ -1529,7 +1529,7 @@ static void shutdown_validator_and_keys(struct lantern_client *client) stop_validator_service(client); stop_ping_service(client); stop_peer_dialer(client); - free_hash_sig_pubkeys(client); + lantern_client_free_hash_sig_pubkeys(client); free(client->hash_sig_key_dir); client->hash_sig_key_dir = NULL; free(client->hash_sig_public_template); @@ -1842,7 +1842,7 @@ static void shutdown_state_and_runtime(struct lantern_client *client) } lantern_fork_choice_reset(&client->fork_choice); client->has_fork_choice = false; - reset_local_validators(client); + lantern_client_reset_local_validators(client); lantern_validator_assignment_reset(&client->validator_assignment); client->has_validator_assignment = false; lantern_consensus_runtime_reset(&client->runtime); diff --git a/src/core/client_init.c b/src/core/client_init.c index 58344fb..e8c1aab 100644 --- a/src/core/client_init.c +++ b/src/core/client_init.c @@ -324,7 +324,8 @@ int populate_local_validators(struct lantern_client *client) uint8_t *decoded_secret = NULL; size_t decoded_len = 0; - if (decode_validator_secret(priv_hex, &decoded_secret, &decoded_len) != 0 || decoded_len == 0) + if (lantern_client_decode_validator_secret(priv_hex, &decoded_secret, &decoded_len) != 0 + || decoded_len == 0) { lantern_log_error( "client", @@ -368,7 +369,7 @@ int populate_local_validators(struct lantern_client *client) { for (size_t j = 0; j < i; ++j) { - local_validator_cleanup(&validators[j]); + lantern_client_local_validator_cleanup(&validators[j]); } free(validators); lantern_secure_zero(decoded_secret, decoded_len); @@ -385,7 +386,7 @@ int populate_local_validators(struct lantern_client *client) { for (size_t j = 0; j <= i; ++j) { - local_validator_cleanup(&validators[j]); + lantern_client_local_validator_cleanup(&validators[j]); } free(validators); lantern_secure_zero(decoded_secret, decoded_len); @@ -407,7 +408,7 @@ int populate_local_validators(struct lantern_client *client) { for (size_t i = 0; i < count; ++i) { - local_validator_cleanup(&validators[i]); + lantern_client_local_validator_cleanup(&validators[i]); } free(validators); lantern_secure_zero(decoded_secret, decoded_len); @@ -426,7 +427,7 @@ int populate_local_validators(struct lantern_client *client) free(enabled); for (size_t i = 0; i < count; ++i) { - local_validator_cleanup(&validators[i]); + lantern_client_local_validator_cleanup(&validators[i]); } free(validators); lantern_secure_zero(decoded_secret, decoded_len); @@ -441,7 +442,7 @@ int populate_local_validators(struct lantern_client *client) free(enabled); for (size_t i = 0; i < count; ++i) { - local_validator_cleanup(&validators[i]); + lantern_client_local_validator_cleanup(&validators[i]); } free(validators); lantern_secure_zero(decoded_secret, decoded_len); @@ -453,7 +454,7 @@ int populate_local_validators(struct lantern_client *client) client->validator_enabled = enabled; enabled = NULL; - reset_local_validators(client); + lantern_client_reset_local_validators(client); client->local_validators = validators; client->local_validator_count = count; validators = NULL; diff --git a/src/core/client_keys.c b/src/core/client_keys.c index 8a6aa0f..e3a316f 100644 --- a/src/core/client_keys.c +++ b/src/core/client_keys.c @@ -16,19 +16,42 @@ #include "client_internal.h" -#include "lantern/crypto/hash_sig.h" -#include "lantern/support/log.h" -#include "lantern/support/secure_mem.h" -#include "lantern/support/strings.h" -#include "internal/yaml_parser.h" - #include #include #include +#include #include #include #include +#include "internal/yaml_parser.h" +#include "lantern/crypto/hash_sig.h" +#include "lantern/support/log.h" +#include "lantern/support/secure_mem.h" +#include "lantern/support/strings.h" + + +/* ============================================================================ + * Forward Declarations + * ============================================================================ */ + +static int hash_sig_join_path(const char *dir, const char *leaf, char **out_path); + + +/* ============================================================================ + * Constants + * ============================================================================ */ + +static const size_t HASH_SIG_HEX_PREFIX_LENGTH = 2u; +static const size_t HASH_SIG_HEX_CHARS_PER_BYTE = 2u; +static const size_t HASH_SIG_KEY_FILENAME_MAX_LEN = 64u; +static const size_t HASH_SIG_WINDOWS_ABS_PATH_MIN_LEN = 3u; +static const size_t HASH_SIG_WINDOWS_COLON_INDEX = 1u; +static const size_t HASH_SIG_WINDOWS_SEPARATOR_INDEX = 2u; + +static const char HASH_SIG_DEFAULT_KEYS_DIR[] = "hash-sig-keys"; +static const char HASH_SIG_MANIFEST_FILENAME[] = "validator-keys-manifest.yaml"; + /* ============================================================================ * Local Validator Lifecycle @@ -37,30 +60,38 @@ /** * Clean up a single local validator's resources. * + * @spec subspecs/xmss/keygen.py - key management + * * @param validator Validator to clean up * * @note Thread safety: Caller must ensure exclusive access to the validator */ -void local_validator_cleanup(struct lantern_local_validator *validator) +void lantern_client_local_validator_cleanup(struct lantern_local_validator *validator) { if (!validator) { return; } - if (validator->secret && validator->secret_len > 0) + + if (validator->secret) { - lantern_secure_zero(validator->secret, validator->secret_len); + if (validator->secret_len > 0) + { + lantern_secure_zero(validator->secret, validator->secret_len); + } free(validator->secret); + validator->secret = NULL; } - validator->secret = NULL; validator->secret_len = 0; validator->has_secret = false; + if (validator->secret_key) { pq_secret_key_free(validator->secret_key); validator->secret_key = NULL; } validator->has_secret_handle = false; + validator->last_proposed_slot = UINT64_MAX; validator->last_attested_slot = UINT64_MAX; validator->has_pending_attestation = false; @@ -72,21 +103,24 @@ void local_validator_cleanup(struct lantern_local_validator *validator) /** * Reset all local validators and free resources. * + * @spec subspecs/xmss/keygen.py - key management + * * @param client Client instance * * @note Thread safety: Caller must ensure exclusive access during shutdown */ -void reset_local_validators(struct lantern_client *client) +void lantern_client_reset_local_validators(struct lantern_client *client) { if (!client) { return; } + if (client->local_validators) { for (size_t i = 0; i < client->local_validator_count; ++i) { - local_validator_cleanup(&client->local_validators[i]); + lantern_client_local_validator_cleanup(&client->local_validators[i]); } free(client->local_validators); client->local_validators = NULL; @@ -102,70 +136,98 @@ void reset_local_validators(struct lantern_client *client) /** * Decode a hex-encoded validator secret key. * + * @spec subspecs/xmss/keygen.py - key encoding + * * @param hex Hex string (with optional 0x prefix) * @param out_key Output buffer (caller must free) * @param out_len Output length - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_VALIDATOR if the hex string is invalid * * @note Thread safety: This function is thread-safe */ -int decode_validator_secret(const char *hex, uint8_t **out_key, size_t *out_len) +int lantern_client_decode_validator_secret( + const char *hex, + uint8_t **out_key, + size_t *out_len) { if (!hex || !out_key || !out_len) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } - char *dup = lantern_string_duplicate(hex); + *out_key = NULL; + *out_len = 0; + + int result = LANTERN_CLIENT_ERR_VALIDATOR; + char *dup = NULL; + size_t dup_len = 0; + uint8_t *secret = NULL; + size_t secret_len = 0; + + dup = lantern_string_duplicate(hex); if (!dup) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } + dup_len = strlen(dup); + char *trimmed = lantern_trim_whitespace(dup); if (!trimmed || *trimmed == '\0') { - lantern_secure_zero(dup, strlen(dup)); - free(dup); - return -1; + goto cleanup; } const char *hex_start = trimmed; if (hex_start[0] == '0' && (hex_start[1] == 'x' || hex_start[1] == 'X')) { - hex_start += 2; + hex_start += HASH_SIG_HEX_PREFIX_LENGTH; } size_t hex_len = strlen(hex_start); - if (hex_len == 0 || (hex_len % 2) != 0) + if (hex_len == 0 || (hex_len % HASH_SIG_HEX_CHARS_PER_BYTE) != 0) { - lantern_secure_zero(dup, strlen(dup)); - free(dup); - return -1; + goto cleanup; } - size_t secret_len = hex_len / 2; - uint8_t *secret = malloc(secret_len); + secret_len = hex_len / HASH_SIG_HEX_CHARS_PER_BYTE; + secret = malloc(secret_len); if (!secret) { - lantern_secure_zero(dup, strlen(dup)); - free(dup); - return -1; + result = LANTERN_CLIENT_ERR_ALLOC; + goto cleanup; } - if (lantern_hex_decode(trimmed, secret, secret_len) != 0) + if (lantern_hex_decode(hex_start, secret, secret_len) != 0) { - lantern_secure_zero(secret, secret_len); - free(secret); - lantern_secure_zero(dup, strlen(dup)); - free(dup); - return -1; + goto cleanup; } - lantern_secure_zero(dup, strlen(dup)); - free(dup); - *out_key = secret; *out_len = secret_len; - return 0; + secret = NULL; + secret_len = 0; + result = LANTERN_CLIENT_OK; + +cleanup: + if (secret) + { + if (secret_len > 0) + { + lantern_secure_zero(secret, secret_len); + } + free(secret); + } + if (dup) + { + if (dup_len > 0) + { + lantern_secure_zero(dup, dup_len); + } + free(dup); + } + return result; } @@ -178,9 +240,9 @@ int decode_validator_secret(const char *hex, uint8_t **out_key, size_t *out_len) */ struct hash_sig_manifest_entry { - uint64_t index; - char *public_file; - char *secret_file; + uint64_t index; /**< Validator global index */ + char *public_file; /**< Public key path or filename */ + char *secret_file; /**< Secret key path or filename */ }; @@ -189,8 +251,8 @@ struct hash_sig_manifest_entry */ struct hash_sig_manifest { - struct hash_sig_manifest_entry *entries; - size_t count; + struct hash_sig_manifest_entry *entries; /**< Manifest entries */ + size_t count; /**< Entry count */ }; @@ -225,8 +287,14 @@ static void hash_sig_manifest_init(struct hash_sig_manifest *manifest) */ static void hash_sig_manifest_reset(struct hash_sig_manifest *manifest) { - if (!manifest || !manifest->entries) + if (!manifest) + { + return; + } + + if (!manifest->entries) { + manifest->count = 0; return; } for (size_t i = 0; i < manifest->count; ++i) @@ -271,7 +339,9 @@ static const char *hash_sig_yaml_value(const LanternYamlObject *object, const ch * * @param text Text to parse * @param out_value Output value - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_CONFIG if parsing fails * * @note Thread safety: This function is thread-safe */ @@ -279,17 +349,32 @@ static int hash_sig_parse_u64(const char *text, uint64_t *out_value) { if (!text || !out_value) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + char *end = NULL; errno = 0; unsigned long long parsed = strtoull(text, &end, 0); if (errno != 0 || end == text) { - return -1; + return LANTERN_CLIENT_ERR_CONFIG; + } + + while (*end != '\0' && isspace((unsigned char)*end)) + { + ++end; + } + if (*end != '\0') + { + return LANTERN_CLIENT_ERR_CONFIG; } + if (parsed > UINT64_MAX) + { + return LANTERN_CLIENT_ERR_CONFIG; + } + *out_value = (uint64_t)parsed; - return 0; + return LANTERN_CLIENT_OK; } @@ -298,53 +383,50 @@ static int hash_sig_parse_u64(const char *text, uint64_t *out_value) * * @param dir Directory containing the manifest file * @param manifest Output manifest structure - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if the manifest is missing or invalid * * @note Thread safety: This function is thread-safe */ static int hash_sig_manifest_load(const char *dir, struct hash_sig_manifest *manifest) { + int result = LANTERN_CLIENT_ERR_CONFIG; + char *manifest_path = NULL; + LanternYamlObject *objects = NULL; + size_t count = 0; + struct hash_sig_manifest_entry *entries = NULL; + if (!dir || !manifest) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } hash_sig_manifest_reset(manifest); - char *manifest_path = NULL; - size_t dir_len = strlen(dir); - const char *filename = "validator-keys-manifest.yaml"; - size_t filename_len = strlen(filename); - bool need_sep = dir_len > 0 && dir[dir_len - 1] != '/' && dir[dir_len - 1] != '\\'; - size_t total = dir_len + (need_sep ? 1 : 0) + filename_len + 1; - manifest_path = malloc(total); - if (!manifest_path) - { - return -1; - } - memcpy(manifest_path, dir, dir_len); - size_t offset = dir_len; - if (need_sep) + result = hash_sig_join_path(dir, HASH_SIG_MANIFEST_FILENAME, &manifest_path); + if (result != 0) { - manifest_path[offset++] = '/'; + goto cleanup; } - memcpy(manifest_path + offset, filename, filename_len); - manifest_path[offset + filename_len] = '\0'; - size_t count = 0; - LanternYamlObject *objects = lantern_yaml_read_array(manifest_path, "validators", &count); - free(manifest_path); - manifest_path = NULL; + objects = lantern_yaml_read_array(manifest_path, "validators", &count); if (!objects || count == 0) { - lantern_yaml_free_objects(objects, count); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } - struct hash_sig_manifest_entry *entries = calloc(count, sizeof(*entries)); + if (count > (SIZE_MAX / sizeof(*entries))) + { + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; + } + entries = calloc(count, sizeof(*entries)); if (!entries) { - lantern_yaml_free_objects(objects, count); - return -1; + result = LANTERN_CLIENT_ERR_ALLOC; + goto cleanup; } for (size_t i = 0; i < count; ++i) @@ -354,32 +436,39 @@ static int hash_sig_manifest_load(const char *dir, struct hash_sig_manifest *man const char *secret_file = hash_sig_yaml_value(&objects[i], "secret_key_file"); if (!index_text || !public_file || !secret_file) { - lantern_yaml_free_objects(objects, count); - hash_sig_manifest_reset(&(struct hash_sig_manifest){.entries = entries, .count = count}); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } uint64_t index = 0; - if (hash_sig_parse_u64(index_text, &index) != 0) + result = hash_sig_parse_u64(index_text, &index); + if (result != 0) { - lantern_yaml_free_objects(objects, count); - hash_sig_manifest_reset(&(struct hash_sig_manifest){.entries = entries, .count = count}); - return -1; + goto cleanup; } entries[i].index = index; entries[i].public_file = lantern_string_duplicate(public_file); entries[i].secret_file = lantern_string_duplicate(secret_file); if (!entries[i].public_file || !entries[i].secret_file) { - lantern_yaml_free_objects(objects, count); - hash_sig_manifest_reset(&(struct hash_sig_manifest){.entries = entries, .count = count}); - return -1; + result = LANTERN_CLIENT_ERR_ALLOC; + goto cleanup; } } - lantern_yaml_free_objects(objects, count); manifest->entries = entries; manifest->count = count; - return 0; + entries = NULL; + result = LANTERN_CLIENT_OK; + +cleanup: + lantern_yaml_free_objects(objects, count); + free(manifest_path); + if (entries) + { + struct hash_sig_manifest tmp = {.entries = entries, .count = count}; + hash_sig_manifest_reset(&tmp); + } + return result; } @@ -429,13 +518,41 @@ static const char *hash_sig_non_empty(const char *value) } +/** + * Add two size_t values with overflow checking. + * + * @param a First value + * @param b Second value + * @param out_sum Output sum + * @return true if overflow would occur, false otherwise + * + * @note Thread safety: This function is thread-safe + */ +static bool hash_sig_size_add_overflow(size_t a, size_t b, size_t *out_sum) +{ + if (!out_sum) + { + return true; + } + if (SIZE_MAX - a < b) + { + return true; + } + *out_sum = a + b; + return false; +} + + /** * Join a directory and filename into a path. * * @param dir Directory path * @param leaf Filename * @param out_path Output path (caller must free) - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if path length overflows * * @note Thread safety: This function is thread-safe */ @@ -443,16 +560,25 @@ static int hash_sig_join_path(const char *dir, const char *leaf, char **out_path { if (!dir || !leaf || !out_path) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + + *out_path = NULL; + size_t dir_len = strlen(dir); size_t leaf_len = strlen(leaf); bool need_sep = dir_len > 0 && dir[dir_len - 1] != '/' && dir[dir_len - 1] != '\\'; - size_t total = dir_len + (need_sep ? 1 : 0) + leaf_len + 1; + size_t total = 0; + if (hash_sig_size_add_overflow(dir_len, need_sep ? 1u : 0u, &total) + || hash_sig_size_add_overflow(total, leaf_len, &total) + || hash_sig_size_add_overflow(total, 1u, &total)) + { + return LANTERN_CLIENT_ERR_CONFIG; + } char *buffer = malloc(total); if (!buffer) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } memcpy(buffer, dir, dir_len); size_t offset = dir_len; @@ -463,7 +589,7 @@ static int hash_sig_join_path(const char *dir, const char *leaf, char **out_path memcpy(buffer + offset, leaf, leaf_len); buffer[offset + leaf_len] = '\0'; *out_path = buffer; - return 0; + return LANTERN_CLIENT_OK; } @@ -473,7 +599,10 @@ static int hash_sig_join_path(const char *dir, const char *leaf, char **out_path * @param template Path template with %llu placeholder * @param index Validator index * @param out_path Output path (caller must free) - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if the template is invalid or overflows * * @note Thread safety: This function is thread-safe */ @@ -481,27 +610,30 @@ static int hash_sig_format_index_template(const char *template, uint64_t index, { if (!template || !out_path) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + + *out_path = NULL; + unsigned long long value = (unsigned long long)index; int required = snprintf(NULL, 0, template, value); - if (required < 0) + if (required < 0 || (size_t)required > SIZE_MAX - 1u) { - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } size_t length = (size_t)required + 1u; char *buffer = malloc(length); if (!buffer) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } if (snprintf(buffer, length, template, value) < 0) { free(buffer); - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } *out_path = buffer; - return 0; + return LANTERN_CLIENT_OK; } @@ -536,9 +668,14 @@ static char *hash_sig_derive_default_dir(const struct lantern_genesis_paths *pat { return NULL; } - const char *suffix = "hash-sig-keys"; - size_t suffix_len = strlen(suffix); - size_t total = dir_len + 1 + suffix_len + 1; + size_t suffix_len = strlen(HASH_SIG_DEFAULT_KEYS_DIR); + size_t total = 0; + if (hash_sig_size_add_overflow(dir_len, 1u, &total) + || hash_sig_size_add_overflow(total, suffix_len, &total) + || hash_sig_size_add_overflow(total, 1u, &total)) + { + return NULL; + } char *buffer = malloc(total); if (!buffer) { @@ -546,7 +683,7 @@ static char *hash_sig_derive_default_dir(const struct lantern_genesis_paths *pat } memcpy(buffer, config_path, dir_len); buffer[dir_len] = '/'; - memcpy(buffer + dir_len + 1, suffix, suffix_len); + memcpy(buffer + dir_len + 1, HASH_SIG_DEFAULT_KEYS_DIR, suffix_len); buffer[dir_len + 1 + suffix_len] = '\0'; return buffer; } @@ -596,7 +733,11 @@ static bool hash_sig_path_is_absolute(const char *path) { return true; } - if (strlen(path) >= 3 && isalpha((unsigned char)path[0]) && path[1] == ':' && (path[2] == '/' || path[2] == '\\')) + if (strlen(path) >= HASH_SIG_WINDOWS_ABS_PATH_MIN_LEN + && isalpha((unsigned char)path[0]) + && path[HASH_SIG_WINDOWS_COLON_INDEX] == ':' + && (path[HASH_SIG_WINDOWS_SEPARATOR_INDEX] == '/' + || path[HASH_SIG_WINDOWS_SEPARATOR_INDEX] == '\\')) { return true; } @@ -611,7 +752,10 @@ static bool hash_sig_path_is_absolute(const char *path) * @param manifest Optional manifest * @param index Validator global index * @param out_path Output path (caller must free) - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if no path can be resolved * * @note Thread safety: This function is thread-safe */ @@ -623,8 +767,11 @@ static int resolve_public_key_path( { if (!client || !out_path) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + + *out_path = NULL; + if (client->hash_sig_public_template) { return hash_sig_format_index_template(client->hash_sig_public_template, index, out_path); @@ -639,10 +786,10 @@ static int resolve_public_key_path( char *copy = lantern_string_duplicate(entry->public_file); if (!copy) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } *out_path = copy; - return 0; + return LANTERN_CLIENT_OK; } if (client->hash_sig_key_dir) { @@ -652,11 +799,11 @@ static int resolve_public_key_path( } if (client->hash_sig_key_dir) { - char filename[64]; + char filename[HASH_SIG_KEY_FILENAME_MAX_LEN]; int written = snprintf(filename, sizeof(filename), "validator_%" PRIu64 "_pk.json", index); if (written < 0 || (size_t)written >= sizeof(filename)) { - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } return hash_sig_join_path(client->hash_sig_key_dir, filename, out_path); } @@ -665,12 +812,12 @@ static int resolve_public_key_path( char *copy = lantern_string_duplicate(client->hash_sig_public_path); if (!copy) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } *out_path = copy; - return 0; + return LANTERN_CLIENT_OK; } - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } @@ -681,7 +828,10 @@ static int resolve_public_key_path( * @param manifest Optional manifest * @param index Validator global index * @param out_path Output path (caller must free) - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if no path can be resolved * * @note Thread safety: This function is thread-safe */ @@ -693,8 +843,11 @@ static int resolve_secret_key_path( { if (!client || !out_path) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + + *out_path = NULL; + if (client->hash_sig_secret_template) { return hash_sig_format_index_template(client->hash_sig_secret_template, index, out_path); @@ -709,10 +862,10 @@ static int resolve_secret_key_path( char *copy = lantern_string_duplicate(entry->secret_file); if (!copy) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } *out_path = copy; - return 0; + return LANTERN_CLIENT_OK; } if (client->hash_sig_key_dir) { @@ -722,11 +875,11 @@ static int resolve_secret_key_path( } if (client->hash_sig_key_dir) { - char filename[64]; + char filename[HASH_SIG_KEY_FILENAME_MAX_LEN]; int written = snprintf(filename, sizeof(filename), "validator_%" PRIu64 "_sk.json", index); if (written < 0 || (size_t)written >= sizeof(filename)) { - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } return hash_sig_join_path(client->hash_sig_key_dir, filename, out_path); } @@ -734,17 +887,17 @@ static int resolve_secret_key_path( { if (client->validator_assignment.count > 1) { - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } char *copy = lantern_string_duplicate(client->hash_sig_secret_path); if (!copy) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } *out_path = copy; - return 0; + return LANTERN_CLIENT_OK; } - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } @@ -757,31 +910,35 @@ static int resolve_secret_key_path( * * @param client Client instance * @param manifest Optional manifest - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if client is NULL * * @note Thread safety: Caller must ensure exclusive access during key operations */ -static int load_hash_sig_secret_keys(struct lantern_client *client, const struct hash_sig_manifest *manifest) +static int load_hash_sig_secret_keys( + struct lantern_client *client, + const struct hash_sig_manifest *manifest) { if (!client) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } if (client->local_validator_count == 0) { - return 0; + return LANTERN_CLIENT_OK; } bool has_template = client->hash_sig_secret_template != NULL; bool has_dir = client->hash_sig_key_dir != NULL; - bool has_single = client->hash_sig_secret_path != NULL && client->validator_assignment.count == 1; + bool has_single = (client->hash_sig_secret_path != NULL) + && (client->validator_assignment.count == 1); if (!has_template && !has_dir && !has_single) { lantern_log_debug( "crypto", &(const struct lantern_log_metadata){.validator = client->node_id}, "hash-sig secret key sources unavailable; skipping local key load"); - return 0; + return LANTERN_CLIENT_OK; } clear_local_secret_handles(client); @@ -834,7 +991,7 @@ static int load_hash_sig_secret_keys(struct lantern_client *client, const struct resolved, client->hash_sig_key_dir ? client->hash_sig_key_dir : "-", client->hash_sig_secret_template ? client->hash_sig_secret_template : "-"); - return 0; + return LANTERN_CLIENT_OK; } @@ -845,11 +1002,13 @@ static int load_hash_sig_secret_keys(struct lantern_client *client, const struct /** * Free all loaded public key handles. * + * @spec subspecs/xmss/keygen.py - key management + * * @param client Client instance * * @note Thread safety: Caller must ensure exclusive access during shutdown */ -void free_hash_sig_pubkeys(struct lantern_client *client) +void lantern_client_free_hash_sig_pubkeys(struct lantern_client *client) { if (!client || !client->validator_pubkeys) { @@ -876,17 +1035,23 @@ void free_hash_sig_pubkeys(struct lantern_client *client) /** * Configure hash-sig key sources from options and environment. * + * @spec subspecs/xmss/keygen.py - key management + * * @param client Client instance * @param options Client options - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails * * @note Thread safety: This function should be called during initialization */ -int configure_hash_sig_sources(struct lantern_client *client, const struct lantern_client_options *options) +int lantern_client_configure_hash_sig_sources( + struct lantern_client *client, + const struct lantern_client_options *options) { if (!client || !options) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_log_metadata meta = {.validator = client->node_id}; const char *env_dir = hash_sig_non_empty(getenv("HASH_SIG_KEY_DIR")); @@ -908,7 +1073,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante { if (set_owned_string(&client->hash_sig_key_dir, resolved_dir) != 0) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } else @@ -920,7 +1085,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante free(derived); if (rc != 0) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } } @@ -934,7 +1099,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante { if (set_owned_string(&client->hash_sig_public_template, resolved_public_template) != 0) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } @@ -947,7 +1112,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante { if (set_owned_string(&client->hash_sig_secret_template, resolved_secret_template) != 0) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } @@ -960,7 +1125,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante { if (set_owned_string(&client->hash_sig_public_path, resolved_public_path) != 0) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } @@ -973,7 +1138,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante { if (set_owned_string(&client->hash_sig_secret_path, resolved_secret_path) != 0) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } lantern_log_info( @@ -985,7 +1150,7 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante client->hash_sig_secret_path ? client->hash_sig_secret_path : "-", client->hash_sig_public_template ? client->hash_sig_public_template : "-", client->hash_sig_secret_template ? client->hash_sig_secret_template : "-"); - return 0; + return LANTERN_CLIENT_OK; } @@ -996,16 +1161,21 @@ int configure_hash_sig_sources(struct lantern_client *client, const struct lante /** * Load all hash-sig keys for the client. * + * @spec subspecs/xmss/keygen.py - key loading + * * @param client Client instance - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if client is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_RUNTIME if hash-sig bindings are unavailable * * @note Thread safety: This function should be called during initialization */ -int load_hash_sig_keys(struct lantern_client *client) +int lantern_client_load_hash_sig_keys(struct lantern_client *client) { if (!client) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_log_metadata meta = {.validator = client->node_id}; if (!lantern_hash_sig_is_available()) @@ -1014,15 +1184,25 @@ int load_hash_sig_keys(struct lantern_client *client) "crypto", &meta, "hash-sig bindings unavailable"); - return -1; + return LANTERN_CLIENT_ERR_RUNTIME; } struct hash_sig_manifest manifest; hash_sig_manifest_init(&manifest); bool manifest_loaded = false; - if (client->hash_sig_key_dir && hash_sig_manifest_load(client->hash_sig_key_dir, &manifest) == 0) + + if (client->hash_sig_key_dir) { - manifest_loaded = true; + int manifest_result = hash_sig_manifest_load(client->hash_sig_key_dir, &manifest); + if (manifest_result == LANTERN_CLIENT_OK) + { + manifest_loaded = true; + } + else if (manifest_result != LANTERN_CLIENT_ERR_CONFIG) + { + hash_sig_manifest_reset(&manifest); + return manifest_result; + } } const struct hash_sig_manifest *manifest_ptr = manifest_loaded ? &manifest : NULL; @@ -1038,12 +1218,13 @@ int load_hash_sig_keys(struct lantern_client *client) * 52-byte serialized pubkeys from state, not full JSON key handles */ if (client->local_validator_count > 0) { - if (load_hash_sig_secret_keys(client, manifest_ptr) != 0) + int result = load_hash_sig_secret_keys(client, manifest_ptr); + if (result != 0) { hash_sig_manifest_reset(&manifest); - return -1; + return result; } } hash_sig_manifest_reset(&manifest); - return 0; + return LANTERN_CLIENT_OK; } diff --git a/src/core/client_network.c b/src/core/client_network.c index 42e4a37..e1a6dd0 100644 --- a/src/core/client_network.c +++ b/src/core/client_network.c @@ -16,28 +16,61 @@ #include "client_internal.h" -#include "lantern/networking/libp2p.h" -#include "lantern/support/log.h" -#include "lantern/support/string_list.h" +#include +#include +#include +#include +#include #include #include #include #include -#include -#include -#include -#include -#include +#include "lantern/networking/libp2p.h" +#include "lantern/support/log.h" +#include "lantern/support/string_list.h" /* ============================================================================ * Constants * ============================================================================ */ -#define LANTERN_PING_INTERVAL_SECONDS 15 -#define LANTERN_PING_TIMEOUT_MS 5000 +static const unsigned LANTERN_PING_INTERVAL_SECONDS = 15u; +static const uint64_t LANTERN_PING_TIMEOUT_MS = 5000ULL; + + +/* ============================================================================ + * Utilities + * ============================================================================ */ + +/** + * Format a peer ID as base58 legacy text. + * + * @param peer Peer ID (may be NULL) + * @param out Output buffer + * @param out_len Size of output buffer + * + * @note Thread safety: This function is thread-safe + */ +static void format_peer_id_text(const peer_id_t *peer, char *out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + out[0] = '\0'; + if (!peer) + { + return; + } + + if (peer_id_to_string(peer, PEER_ID_FMT_BASE58_LEGACY, out, out_len) < 0) + { + out[0] = '\0'; + } +} /* ============================================================================ @@ -104,16 +137,7 @@ void connection_counter_update( } char peer_text[128]; - peer_text[0] = '\0'; - if (peer) - { - if (peer_id_to_string(peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - } - - const char *label = peer_text[0] ? peer_text : NULL; + format_peer_id_text(peer, peer_text, sizeof(peer_text)); size_t total = 0; if (pthread_mutex_lock(&client->connection_lock) == 0) { @@ -158,8 +182,6 @@ void connection_counter_update( { return; } - - (void)label; lantern_log_trace( "network", &(const struct lantern_log_metadata){ @@ -230,16 +252,10 @@ void request_status_now(struct lantern_client *client, const peer_id_t *peer, co char peer_buffer[128]; peer_buffer[0] = '\0'; const char *status_peer = (peer_text && peer_text[0]) ? peer_text : NULL; - if ((!status_peer || status_peer[0] == '\0') && peer) + if (!status_peer && peer) { - if (peer_id_to_string(peer, PEER_ID_FMT_BASE58_LEGACY, peer_buffer, sizeof(peer_buffer)) == 0) - { - status_peer = peer_buffer; - } - else - { - status_peer = NULL; - } + format_peer_id_text(peer, peer_buffer, sizeof(peer_buffer)); + status_peer = peer_buffer[0] ? peer_buffer : NULL; } if (status_peer && !lantern_client_is_peer_connected(client, status_peer)) { @@ -247,7 +263,8 @@ void request_status_now(struct lantern_client *client, const peer_id_t *peer, co "reqresp", &(const struct lantern_log_metadata){ .validator = client->node_id, - .peer = status_peer}, + .peer = status_peer, + }, "cannot request status; peer is not connected"); return; } @@ -282,10 +299,14 @@ void request_status_now(struct lantern_client *client, const peer_id_t *peer, co int status_rc = lantern_reqresp_service_request_status(&client->reqresp, peer, status_peer); if (status_peer) { + const char *msg = (status_rc == 0) + ? "initiated status request to peer" + : "unable to initiate status request to peer"; lantern_log_trace( "reqresp", &meta, - status_rc == 0 ? "initiated status request to peer" : "unable to initiate status request to peer"); + "%s", + msg); } else if (status_rc != 0) { @@ -330,7 +351,8 @@ void lantern_client_seed_reqresp_peer_modes(struct lantern_client *client) { return; } -#if defined(LANTERN_REQRESP_STATUS_PROTOCOL_LEGACY) || defined(LANTERN_REQRESP_BLOCKS_BY_ROOT_PROTOCOL_LEGACY) +#if defined(LANTERN_REQRESP_STATUS_PROTOCOL_LEGACY) \ + || defined(LANTERN_REQRESP_BLOCKS_BY_ROOT_PROTOCOL_LEGACY) const struct lantern_validator_config *config = &client->genesis.validator_config; if (!config || !config->entries) { @@ -444,7 +466,10 @@ void adopt_validator_listen_address(struct lantern_client *client) * * @note Thread safety: This function is thread-safe */ -void identify_dial_multiaddr(struct lantern_client *client, const char *multiaddr, const char *peer_label) +void identify_dial_multiaddr( + struct lantern_client *client, + const char *multiaddr, + const char *peer_label) { if (!client || !client->network.host || !multiaddr || multiaddr[0] == '\0') { @@ -466,7 +491,8 @@ void identify_dial_multiaddr(struct lantern_client *client, const char *multiadd "network", &(const struct lantern_log_metadata){ .validator = client->node_id, - .peer = peer_label}, + .peer = peer_label, + }, "identify dial succeeded addr=%s", multiaddr); return; @@ -476,7 +502,8 @@ void identify_dial_multiaddr(struct lantern_client *client, const char *multiadd "network", &(const struct lantern_log_metadata){ .validator = client->node_id, - .peer = peer_label}, + .peer = peer_label, + }, "identify dial failed rc=%d addr=%s", rc, multiaddr); @@ -535,11 +562,7 @@ void redial_peer_on_timeout(struct lantern_client *client, const peer_id_t *peer } char peer_text[128]; - peer_text[0] = '\0'; - if (peer_id_to_string(peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } + format_peer_id_text(peer, peer_text, sizeof(peer_text)); /* Check if we're still connected to this peer (e.g., via another connection). * If so, skip the redial to avoid creating duplicate connections. */ @@ -566,7 +589,11 @@ void redial_peer_on_timeout(struct lantern_client *client, const peer_id_t *peer char multiaddr[256]; peer_id_t enr_peer_id = {0}; - if (lantern_libp2p_enr_to_multiaddr(record, multiaddr, sizeof(multiaddr), &enr_peer_id) != 0) + if (lantern_libp2p_enr_to_multiaddr( + record, + multiaddr, + sizeof(multiaddr), + &enr_peer_id) != 0) { continue; } @@ -586,7 +613,10 @@ void redial_peer_on_timeout(struct lantern_client *client, const peer_id_t *peer "redialing peer after disconnect addr=%s", multiaddr); - (void)lantern_libp2p_host_add_enr_peer(&client->network, record, LANTERN_LIBP2P_DEFAULT_PEER_TTL_MS); + (void)lantern_libp2p_host_add_enr_peer( + &client->network, + record, + LANTERN_LIBP2P_DEFAULT_PEER_TTL_MS); identify_dial_multiaddr(client, multiaddr, peer_text[0] ? peer_text : record->encoded); return; } @@ -603,158 +633,234 @@ void redial_peer_on_timeout(struct lantern_client *client, const peer_id_t *peer } +/* ============================================================================ + * Peer Dialer Helpers + * ============================================================================ */ + /** - * Attempt to dial peers from genesis ENRs. + * Take a snapshot of connected peer IDs. * - * @param client Client instance + * @param client Client instance + * @param out_snapshot Output list (must be initialized) + * @return Best-effort unique connected peer count * - * @note Thread safety: This function acquires connection_lock + * @note Thread safety: This function acquires connection_lock if initialized */ -void peer_dialer_attempt(struct lantern_client *client) +static size_t snapshot_connected_peers( + struct lantern_client *client, + struct lantern_string_list *out_snapshot) { - if (!client || !client->network.host) - { - return; - } - - const struct lantern_enr_record_list *enrs = &client->genesis.enrs; - if (!enrs || enrs->count == 0) + if (!client || !out_snapshot || !client->connection_lock_initialized) { - return; + return 0; } - struct lantern_string_list connected_snapshot; - lantern_string_list_init(&connected_snapshot); - size_t connected_unique = 0; - if (client->connection_lock_initialized) + if (pthread_mutex_lock(&client->connection_lock) == 0) { - if (pthread_mutex_lock(&client->connection_lock) == 0) - { - connected_unique = client->connected_peer_ids.len; - if (lantern_string_list_copy(&connected_snapshot, &client->connected_peer_ids) != 0) - { - lantern_string_list_reset(&connected_snapshot); - lantern_string_list_init(&connected_snapshot); - connected_unique = client->connected_peers; - } - pthread_mutex_unlock(&client->connection_lock); - } - else + connected_unique = client->connected_peer_ids.len; + if (lantern_string_list_copy(out_snapshot, &client->connected_peer_ids) != 0) { + lantern_string_list_reset(out_snapshot); + lantern_string_list_init(out_snapshot); connected_unique = client->connected_peers; } + pthread_mutex_unlock(&client->connection_lock); + } + else + { + connected_unique = client->connected_peers; + } + + return connected_unique; +} + + +/** + * Get local peer ID from the libp2p host. + * + * @param client Client instance + * @return Allocated peer ID pointer, or NULL on failure + * + * @note Thread safety: This function is thread-safe + */ +static peer_id_t *get_local_peer_id(struct lantern_client *client) +{ + if (!client || !client->network.host) + { + return NULL; } peer_id_t *local_peer = NULL; if (libp2p_host_get_peer_id(client->network.host, &local_peer) != 0) { - local_peer = NULL; + return NULL; } - size_t target = 0; - if (enrs->count > 0) + return local_peer; +} + + +/** + * Compute dial target count based on genesis ENRs. + * + * @param enrs Genesis ENR list + * @param local_peer Local peer ID (may be NULL) + * @return Target number of connections to attempt + * + * @note Thread safety: This function is thread-safe + */ +static size_t compute_peer_dial_target( + const struct lantern_enr_record_list *enrs, + const peer_id_t *local_peer) +{ + if (!enrs || enrs->count == 0) { - target = enrs->count; - if (local_peer && local_peer->bytes && local_peer->size) - { - if (target > 0) - { - target -= 1; - } - } + return 0; } - if (target > 0 && connected_unique >= target) + size_t target = enrs->count; + if (local_peer && local_peer->bytes && local_peer->size && target > 0) + { + target -= 1; + } + + return target; +} + + +/** + * Dial a peer from a genesis ENR record. + * + * @param client Client instance + * @param record ENR record to dial + * @param local_peer Local peer ID (may be NULL) + * @param connected_snapshot Snapshot of currently connected peers + * + * @note Thread safety: This function is thread-safe + */ +static void peer_dialer_handle_record( + struct lantern_client *client, + const struct lantern_enr_record *record, + const peer_id_t *local_peer, + const struct lantern_string_list *connected_snapshot) +{ + if (!client || !record || !record->encoded) { - if (local_peer) - { - peer_id_destroy(local_peer); - free(local_peer); - } - lantern_string_list_reset(&connected_snapshot); return; } - for (size_t idx = 0; idx < enrs->count; ++idx) + char multiaddr[256]; + peer_id_t peer_id = {0}; + if (lantern_libp2p_enr_to_multiaddr(record, multiaddr, sizeof(multiaddr), &peer_id) != 0) { - if (__atomic_load_n(&client->dialer_stop_flag, __ATOMIC_RELAXED) != 0) - { - break; - } + peer_id_destroy(&peer_id); + return; + } - const struct lantern_enr_record *record = &enrs->records[idx]; - if (!record || !record->encoded) - { - continue; - } + if (local_peer && peer_id_equals(local_peer, &peer_id) == 1) + { + peer_id_destroy(&peer_id); + return; + } - char multiaddr[256]; - peer_id_t peer_id = {0}; - if (lantern_libp2p_enr_to_multiaddr(record, multiaddr, sizeof(multiaddr), &peer_id) != 0) + char peer_text[128]; + format_peer_id_text(&peer_id, peer_text, sizeof(peer_text)); + + if (peer_text[0] && connected_snapshot && string_list_contains(connected_snapshot, peer_text)) + { + peer_id_destroy(&peer_id); + return; + } + + (void)lantern_libp2p_host_add_enr_peer( + &client->network, + record, + LANTERN_LIBP2P_DEFAULT_PEER_TTL_MS); + + const char *peer_label = peer_text[0] ? peer_text : record->encoded; + identify_dial_multiaddr(client, multiaddr, peer_label); + + if (client->gossip_running && client->gossip.gossipsub) + { + bool already_added = false; + if (peer_text[0]) { - continue; + already_added = string_list_contains(&client->dialer_peers, peer_text); } - bool is_self = false; - if (local_peer) + if (!already_added) { - int eq = peer_id_equals(local_peer, &peer_id); - if (eq == 1) + libp2p_err_t perr = libp2p_gossipsub_peering_add( + client->gossip.gossipsub, + &peer_id); + if (perr == LIBP2P_ERR_OK) { - is_self = true; + if (peer_text[0]) + { + (void)lantern_string_list_append(&client->dialer_peers, peer_text); + } + lantern_log_trace( + "network", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_label, + }, + "dialer added peer to gossipsub peering"); } } + } - if (!is_self) - { - char peer_text[128]; - peer_text[0] = '\0'; - if (peer_id_to_string(&peer_id, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } + peer_id_destroy(&peer_id); +} - if (peer_text[0] && string_list_contains(&connected_snapshot, peer_text)) - { - peer_id_destroy(&peer_id); - continue; - } - (void)lantern_libp2p_host_add_enr_peer(&client->network, record, LANTERN_LIBP2P_DEFAULT_PEER_TTL_MS); - identify_dial_multiaddr(client, multiaddr, peer_text[0] ? peer_text : record->encoded); +/** + * Attempt to dial peers from genesis ENRs. + * + * @param client Client instance + * + * @note Thread safety: This function acquires connection_lock + */ +void peer_dialer_attempt(struct lantern_client *client) +{ + if (!client || !client->network.host) + { + return; + } - bool already_added = false; - if (peer_text[0]) - { - already_added = string_list_contains(&client->dialer_peers, peer_text); - } + const struct lantern_enr_record_list *enrs = &client->genesis.enrs; + if (!enrs || enrs->count == 0) + { + return; + } - if (client->gossip_running && client->gossip.gossipsub) - { - if (!already_added) - { - libp2p_err_t perr = libp2p_gossipsub_peering_add(client->gossip.gossipsub, &peer_id); - if (perr == LIBP2P_ERR_OK) - { - if (peer_text[0]) - { - (void)lantern_string_list_append(&client->dialer_peers, peer_text); - } - lantern_log_trace( - "network", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_text[0] ? peer_text : record->encoded}, - "dialer added peer to gossipsub peering"); - } - } - } + struct lantern_string_list connected_snapshot; + lantern_string_list_init(&connected_snapshot); + size_t connected_unique = snapshot_connected_peers(client, &connected_snapshot); + peer_id_t *local_peer = get_local_peer_id(client); + + size_t target = compute_peer_dial_target(enrs, local_peer); + if (target > 0 && connected_unique >= target) + { + goto cleanup; + } + + for (size_t idx = 0; idx < enrs->count; ++idx) + { + if (__atomic_load_n(&client->dialer_stop_flag, __ATOMIC_RELAXED) != 0) + { + break; } - peer_id_destroy(&peer_id); + peer_dialer_handle_record( + client, + &enrs->records[idx], + local_peer, + &connected_snapshot); } +cleanup: if (local_peer) { peer_id_destroy(local_peer); @@ -853,8 +959,8 @@ void stop_peer_dialer(struct lantern_client *client) */ struct ping_dial_ctx { - struct lantern_client *client; - char peer_text[128]; + struct lantern_client *client; /**< Client instance */ + char peer_text[128]; /**< Peer ID as text */ }; @@ -943,11 +1049,13 @@ static void ping_all_peers(struct lantern_client *client) lantern_string_list_init(&peers); if (pthread_mutex_lock(&client->connection_lock) != 0) { + lantern_string_list_reset(&peers); return; } if (lantern_string_list_copy(&peers, &client->connected_peer_ids) != 0) { pthread_mutex_unlock(&client->connection_lock); + lantern_string_list_reset(&peers); return; } pthread_mutex_unlock(&client->connection_lock); @@ -1015,13 +1123,16 @@ static void *ping_thread(void *arg) lantern_log_info( "network", &(const struct lantern_log_metadata){.validator = client->node_id}, - "ping service started interval=%ds", + "ping service started interval=%us", LANTERN_PING_INTERVAL_SECONDS); while (__atomic_load_n(&client->ping_stop_flag, __ATOMIC_RELAXED) == 0) { ping_all_peers(client); /* Sleep in small increments to allow quick shutdown. */ - for (unsigned i = 0; i < LANTERN_PING_INTERVAL_SECONDS && __atomic_load_n(&client->ping_stop_flag, __ATOMIC_RELAXED) == 0; i++) + for (unsigned i = 0; + i < LANTERN_PING_INTERVAL_SECONDS + && __atomic_load_n(&client->ping_stop_flag, __ATOMIC_RELAXED) == 0; + i++) { struct timespec ts = {.tv_sec = 1, .tv_nsec = 0}; nanosleep(&ts, NULL); @@ -1089,6 +1200,197 @@ void stop_ping_service(struct lantern_client *client) * Connection Events * ============================================================================ */ +/** + * Handle connection opened events. + * + * @param client Client instance + * @param peer Peer ID (may be NULL) + * @param inbound True if inbound connection + * + * @note Thread safety: This function is called from libp2p thread + */ +static void handle_connection_opened_event( + struct lantern_client *client, + const peer_id_t *peer, + bool inbound) +{ + if (!client) + { + return; + } + + connection_counter_update(client, 1, peer, inbound, 0); + + if (!peer) + { + return; + } + + char peer_text[128]; + format_peer_id_text(peer, peer_text, sizeof(peer_text)); + request_status_now(client, peer, peer_text[0] ? peer_text : NULL); +} + + +/** + * Handle connection closed events. + * + * @param client Client instance + * @param peer Peer ID (may be NULL) + * @param reason Disconnect reason code + * + * @note Thread safety: This function is called from libp2p thread + */ +static void handle_connection_closed_event( + struct lantern_client *client, + const peer_id_t *peer, + int reason) +{ + if (!client) + { + return; + } + + connection_counter_update(client, -1, peer, false, reason); + + if (!peer) + { + return; + } + + char peer_text[128]; + format_peer_id_text(peer, peer_text, sizeof(peer_text)); + + lantern_log_info( + "network", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_text[0] ? peer_text : NULL, + }, + "connection closed reason=%d (%s)", + reason, + connection_reason_text(reason)); + + /* If disconnected unexpectedly, attempt to redial the peer. + * We redial for timeout, reset, EOF, and closed reasons since the remote + * peer may have closed due to their own timeout or network issues. */ + if (reason == LIBP2P_ERR_TIMEOUT || + reason == LIBP2P_ERR_RESET || + reason == LIBP2P_ERR_EOF || + reason == LIBP2P_ERR_CLOSED) + { + redial_peer_on_timeout(client, peer); + } +} + + +/** + * Handle dialing events. + * + * @param client Client instance + * @param peer Peer ID (may be NULL) + * @param addr Multiaddr being dialed (may be NULL) + * + * @note Thread safety: This function is called from libp2p thread + */ +static void handle_dialing_event( + struct lantern_client *client, + const peer_id_t *peer, + const char *addr) +{ + if (!client) + { + return; + } + + char peer_text[128]; + format_peer_id_text(peer, peer_text, sizeof(peer_text)); + + lantern_log_debug( + "network", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_text[0] ? peer_text : NULL, + }, + "dialing peer addr=%s", + addr ? addr : "-"); +} + + +/** + * Handle outgoing connection error events. + * + * @param client Client instance + * @param peer Peer ID (may be NULL) + * @param code Error code + * @param msg Error message (may be NULL) + * + * @note Thread safety: This function is called from libp2p thread + */ +static void handle_outgoing_connection_error_event( + struct lantern_client *client, + const peer_id_t *peer, + int code, + const char *msg) +{ + if (!client) + { + return; + } + + char peer_text[128]; + format_peer_id_text(peer, peer_text, sizeof(peer_text)); + + lantern_log_warn( + "network", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_text[0] ? peer_text : NULL, + }, + "outgoing connection error code=%d (%s) msg=%s", + code, + connection_reason_text(code), + msg ? msg : "-"); +} + + +/** + * Handle incoming connection error events. + * + * @param client Client instance + * @param peer Peer ID (may be NULL) + * @param code Error code + * @param msg Error message (may be NULL) + * + * @note Thread safety: This function is called from libp2p thread + */ +static void handle_incoming_connection_error_event( + struct lantern_client *client, + const peer_id_t *peer, + int code, + const char *msg) +{ + if (!client) + { + return; + } + + char peer_text[128]; + format_peer_id_text(peer, peer_text, sizeof(peer_text)); + + lantern_log_warn( + "network", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_text[0] ? peer_text : NULL, + }, + "incoming connection error code=%d (%s) msg=%s", + code, + connection_reason_text(code), + msg ? msg : "-"); +} + + /** * Connection event callback for libp2p host. * @@ -1107,119 +1409,37 @@ void connection_events_cb(const libp2p_event_t *evt, void *user_data) switch (evt->kind) { case LIBP2P_EVT_CONN_OPENED: - connection_counter_update(client, 1, evt->u.conn_opened.peer, evt->u.conn_opened.inbound, 0); - if (evt->u.conn_opened.peer) - { - char peer_text[128]; - peer_text[0] = '\0'; - if (peer_id_to_string(evt->u.conn_opened.peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - request_status_now(client, evt->u.conn_opened.peer, peer_text[0] ? peer_text : NULL); - } + handle_connection_opened_event( + client, + evt->u.conn_opened.peer, + evt->u.conn_opened.inbound); break; case LIBP2P_EVT_CONN_CLOSED: - { - connection_counter_update(client, -1, evt->u.conn_closed.peer, false, evt->u.conn_closed.reason); - /* If disconnected unexpectedly, attempt to redial the peer. - * We redial for timeout, reset, EOF, and closed reasons since the remote - * peer may have closed due to their own timeout or network issues. */ - if (evt->u.conn_closed.peer) - { - int reason = evt->u.conn_closed.reason; - char peer_text[128]; - peer_text[0] = '\0'; - if (peer_id_to_string(evt->u.conn_closed.peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - lantern_log_info( - "network", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_text[0] ? peer_text : NULL, - }, - "connection closed reason=%d (%s)", - reason, - connection_reason_text(reason)); - if (reason == LIBP2P_ERR_TIMEOUT || - reason == LIBP2P_ERR_RESET || - reason == LIBP2P_ERR_EOF || - reason == LIBP2P_ERR_CLOSED) - { - redial_peer_on_timeout(client, evt->u.conn_closed.peer); - } - } + handle_connection_closed_event( + client, + evt->u.conn_closed.peer, + evt->u.conn_closed.reason); break; - } case LIBP2P_EVT_DIALING: - { - char peer_text[128]; - peer_text[0] = '\0'; - if (evt->u.dialing.peer) - { - if (peer_id_to_string(evt->u.dialing.peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - } - lantern_log_debug( - "network", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_text[0] ? peer_text : NULL, - }, - "dialing peer addr=%s", - evt->u.dialing.addr ? evt->u.dialing.addr : "-"); + handle_dialing_event( + client, + evt->u.dialing.peer, + evt->u.dialing.addr); break; - } case LIBP2P_EVT_OUTGOING_CONNECTION_ERROR: - { - char peer_text[128]; - peer_text[0] = '\0'; - if (evt->u.outgoing_conn_error.peer) - { - if (peer_id_to_string(evt->u.outgoing_conn_error.peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - } - lantern_log_warn( - "network", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_text[0] ? peer_text : NULL, - }, - "outgoing connection error code=%d (%s) msg=%s", + handle_outgoing_connection_error_event( + client, + evt->u.outgoing_conn_error.peer, evt->u.outgoing_conn_error.code, - connection_reason_text(evt->u.outgoing_conn_error.code), - evt->u.outgoing_conn_error.msg ? evt->u.outgoing_conn_error.msg : "-"); + evt->u.outgoing_conn_error.msg); break; - } case LIBP2P_EVT_INCOMING_CONNECTION_ERROR: - { - char peer_text[128]; - peer_text[0] = '\0'; - if (evt->u.incoming_conn_error.peer) - { - if (peer_id_to_string(evt->u.incoming_conn_error.peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - } - lantern_log_warn( - "network", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_text[0] ? peer_text : NULL, - }, - "incoming connection error code=%d (%s) msg=%s", + handle_incoming_connection_error_event( + client, + evt->u.incoming_conn_error.peer, evt->u.incoming_conn_error.code, - connection_reason_text(evt->u.incoming_conn_error.code), - evt->u.incoming_conn_error.msg ? evt->u.incoming_conn_error.msg : "-"); + evt->u.incoming_conn_error.msg); break; - } default: break; } diff --git a/src/core/client_peers.c b/src/core/client_peers.c index bb08a37..a5dc3fd 100644 --- a/src/core/client_peers.c +++ b/src/core/client_peers.c @@ -379,7 +379,8 @@ void lantern_client_note_vote_outcome( * * @param client Client instance * @param peer_id Peer ID to request status from - * @return true if request can proceed, false if already in flight + * @return true if request can proceed + * @return false if request already in flight or parameters are invalid * * @note Thread safety: This function acquires status_lock */ @@ -387,7 +388,12 @@ bool lantern_client_try_begin_status_request( struct lantern_client *client, const char *peer_id) { - if (!client || !peer_id || !peer_id[0] || !client->status_lock_initialized) + if (!client || !peer_id || !peer_id[0]) + { + return false; + } + + if (!client->status_lock_initialized) { return true; } @@ -516,7 +522,15 @@ void lantern_client_status_request_update_locked( entry->outstanding_status_requests += increase; } - client->status_requests_inflight_total += (size_t)increase; + const size_t increase_size = (size_t)increase; + if (client->status_requests_inflight_total > SIZE_MAX - increase_size) + { + client->status_requests_inflight_total = SIZE_MAX; + } + else + { + client->status_requests_inflight_total += increase_size; + } if (client->status_requests_inflight_total > client->status_requests_peak) { @@ -525,7 +539,7 @@ void lantern_client_status_request_update_locked( } else { - uint32_t decrease = (uint32_t)(-delta); + uint32_t decrease = (uint32_t)(-(int64_t)delta); if (entry->outstanding_status_requests > decrease) { entry->outstanding_status_requests -= decrease; @@ -551,7 +565,8 @@ void lantern_client_status_request_update_locked( .validator = client->node_id, .peer = (peer_id && peer_id[0]) ? peer_id : NULL, }, - "status guard %s delta=%d peer_outstanding=%u total_outstanding=%zu peak=%zu guard_disabled=%s", + "status guard %s delta=%d peer_outstanding=%u total_outstanding=%zu " + "peak=%zu guard_disabled=%s", phase ? phase : "update", delta, entry->outstanding_status_requests, diff --git a/src/core/client_reqresp.c b/src/core/client_reqresp.c index 9925d42..12f8752 100644 --- a/src/core/client_reqresp.c +++ b/src/core/client_reqresp.c @@ -22,20 +22,20 @@ #include "client_internal.h" +#include +#include +#include +#include + +#include "libp2p/errors.h" +#include "peer_id/peer_id.h" + #include "lantern/consensus/hash.h" #include "lantern/networking/messages.h" #include "lantern/networking/reqresp_service.h" #include "lantern/storage/storage.h" -#include "lantern/support/strings.h" #include "lantern/support/log.h" - -#include "libp2p/errors.h" -#include "peer_id/peer_id.h" - -#include -#include -#include -#include +#include "lantern/support/strings.h" /* ============================================================================ @@ -62,10 +62,650 @@ static void lantern_client_adopt_peer_genesis( const LanternStatusMessage *peer_status, const char *peer_id_text); +static void copy_peer_id_text(const char *peer_id, char *out, size_t out_len); +static bool record_status_failure_peer_id(struct lantern_client *client, const char *peer_id_text); +static void log_status_failure( + const struct lantern_client *client, + const char *peer_id_text, + int error, + bool first_failure); +static void peer_status_local_view( + struct lantern_client *client, + const LanternRoot *head_root, + uint64_t *out_local_slot, + bool *out_head_known); +static struct lantern_peer_status_entry *lantern_client_update_peer_status_entry_locked( + struct lantern_client *client, + const LanternStatusMessage *peer_status, + const char *peer_id_text, + bool *out_head_changed); +static bool lantern_client_apply_blocks_request_backoff_locked( + const struct lantern_client *client, + struct lantern_peer_status_entry *entry, + const char *peer_id_text, + const char *head_root_text); +static bool lantern_client_peer_status_maybe_request_blocks( + struct lantern_client *client, + const LanternStatusMessage *peer_status, + const char *peer_id_text, + const char *head_root_text, + uint64_t local_slot, + bool head_known, + LanternRoot *out_request_root); +static bool lantern_client_update_blocks_request_tracking( + struct lantern_client *client, + const char *peer_id, + enum lantern_blocks_request_outcome outcome, + uint32_t *out_failure_count, + bool *out_entry_found); +static const char *lantern_blocks_request_outcome_text(enum lantern_blocks_request_outcome outcome); +static void lantern_client_clear_pending_parent_requested( + struct lantern_client *client, + const LanternRoot *request_root); +static void lantern_client_request_status_after_blocks_success( + struct lantern_client *client, + const char *peer_id); + + +/* ============================================================================ + * Reqresp Callbacks + * ============================================================================ */ + +/** + * @brief Copies a peer ID string into a fixed-size buffer. + * + * @param peer_id Peer ID string (may be NULL) + * @param out Output buffer + * @param out_len Output buffer length + */ +static void copy_peer_id_text(const char *peer_id, char *out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + out[0] = '\0'; + if (!peer_id || peer_id[0] == '\0') + { + return; + } + + strncpy(out, peer_id, out_len - 1); + out[out_len - 1] = '\0'; +} + + +/** + * @brief Record the peer for failure log throttling. + * + * @param client Client instance + * @param peer_id_text Peer ID string (may be empty) + * @return true if this is the first recorded failure, false otherwise + * + * @note Thread safety: This function acquires status_lock + */ +static bool record_status_failure_peer_id(struct lantern_client *client, const char *peer_id_text) +{ + if (!client || !peer_id_text || peer_id_text[0] == '\0') + { + return true; + } + + if (!client->status_lock_initialized) + { + return true; + } + + bool first_failure = true; + if (pthread_mutex_lock(&client->status_lock) != 0) + { + return true; + } + + if (string_list_contains(&client->status_failure_peer_ids, peer_id_text)) + { + first_failure = false; + } + else if (lantern_string_list_append(&client->status_failure_peer_ids, peer_id_text) != 0) + { + first_failure = true; + } + + pthread_mutex_unlock(&client->status_lock); + return first_failure; +} + + +/** + * @brief Log a status request failure with throttling. + * + * @param client Client instance + * @param peer_id_text Peer ID string (may be empty) + * @param error Error code + * @param first_failure True if this is the first recorded failure + */ +static void log_status_failure( + const struct lantern_client *client, + const char *peer_id_text, + int error, + bool first_failure) +{ + const char *reason = connection_reason_text(error); + struct lantern_log_metadata meta = { + .validator = client ? client->node_id : NULL, + .peer = peer_id_text && peer_id_text[0] ? peer_id_text : NULL, + }; + + if (error == LIBP2P_ERR_PROTO_NEGOTIATION_FAILED || error == LIBP2P_ERR_UNSUPPORTED) + { + if (first_failure) + { + lantern_log_info( + "reqresp", + &meta, + "peer does not support %s error=%d (%s)", + LANTERN_STATUS_PROTOCOL_ID, + error, + reason ? reason : "-"); + } + else + { + lantern_log_trace( + "reqresp", + &meta, + "peer still misses %s support error=%d (%s)", + LANTERN_STATUS_PROTOCOL_ID, + error, + reason ? reason : "-"); + } + return; + } + + if (error == LIBP2P_ERR_TIMEOUT) + { + if (first_failure) + { + lantern_log_warn( + "reqresp", + &meta, + "status request to peer timed out error=%d (%s)", + error, + reason ? reason : "-"); + } + else + { + lantern_log_debug( + "reqresp", + &meta, + "status request still timing out error=%d (%s)", + error, + reason ? reason : "-"); + } + return; + } + + if (first_failure) + { + lantern_log_warn( + "reqresp", + &meta, + "status request failed error=%d (%s)", + error, + reason ? reason : "-"); + } + else + { + lantern_log_debug( + "reqresp", + &meta, + "status request still failing error=%d (%s)", + error, + reason ? reason : "-"); + } +} + + +/** + * @brief Determine local slot and whether a head root is known. + * + * @param client Client instance + * @param head_root Head root to check + * @param out_local_slot Output local slot snapshot + * @param out_head_known Output true if head is known locally + * + * @note Thread safety: This function may acquire state_lock + */ +static void peer_status_local_view( + struct lantern_client *client, + const LanternRoot *head_root, + uint64_t *out_local_slot, + bool *out_head_known) +{ + if (out_local_slot) + { + *out_local_slot = 0; + } + if (out_head_known) + { + *out_head_known = false; + } + + if (!client || !head_root || !out_local_slot || !out_head_known) + { + return; + } + + uint64_t local_slot = 0; + bool head_known = false; + bool state_locked = lantern_client_lock_state(client); + if (state_locked) + { + local_slot = client->state.slot; + head_known = lantern_client_block_known_locked(client, head_root, NULL); + } + else if (client->has_state) + { + local_slot = client->state.slot; + if (client->has_fork_choice) + { + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + head_root, + &fork_slot, + NULL, + NULL) + == 0) + { + head_known = true; + } + } + } + lantern_client_unlock_state(client, state_locked); + + *out_local_slot = local_slot; + *out_head_known = head_known; +} + + +/** + * @brief Update stored peer status and mark status request complete. + * + * @param client Client instance + * @param peer_status Peer status message + * @param peer_id_text Peer ID string for tracking/logging + * @param out_head_changed Output whether the head changed since last status + * @return Peer status entry on success, NULL on failure + * + * @note Thread safety: Caller must hold status_lock + */ +static struct lantern_peer_status_entry *lantern_client_update_peer_status_entry_locked( + struct lantern_client *client, + const LanternStatusMessage *peer_status, + const char *peer_id_text, + bool *out_head_changed) +{ + if (out_head_changed) + { + *out_head_changed = false; + } + + if (!client || !peer_status || !peer_id_text) + { + return NULL; + } + + struct lantern_peer_status_entry *entry = + lantern_client_ensure_status_entry_locked(client, peer_id_text); + if (!entry) + { + return NULL; + } + + entry->status_request_inflight = false; + lantern_client_status_request_update_locked(client, entry, peer_id_text, -1, "complete"); + + string_list_remove(&client->status_failure_peer_ids, peer_id_text); + + bool head_changed = !entry->has_status + || entry->status.head.slot != peer_status->head.slot + || memcmp( + entry->status.head.root.bytes, + peer_status->head.root.bytes, + LANTERN_ROOT_SIZE) + != 0; + + entry->status = *peer_status; + entry->has_status = true; + + if (out_head_changed) + { + *out_head_changed = head_changed; + } + + return entry; +} + + +/** + * @brief Apply blocks request backoff and update tracking entry. + * + * @param client Client instance + * @param entry Peer status entry + * @param peer_id_text Peer ID string for logging + * @param head_root_text Formatted head root text + * @return true if a blocks_by_root request should be scheduled now + * + * @note Thread safety: Caller must hold status_lock + */ +static bool lantern_client_apply_blocks_request_backoff_locked( + const struct lantern_client *client, + struct lantern_peer_status_entry *entry, + const char *peer_id_text, + const char *head_root_text) +{ + if (!client || !entry || !peer_id_text || !head_root_text) + { + return false; + } + + if (entry->requested_head) + { + return false; + } + + uint64_t now_ms = monotonic_millis(); + uint64_t backoff_ms = blocks_request_backoff_ms(entry->consecutive_blocks_failures); + if (entry->consecutive_blocks_failures == 0 + && backoff_ms < LANTERN_BLOCKS_REQUEST_MIN_POLL_MS) + { + backoff_ms = LANTERN_BLOCKS_REQUEST_MIN_POLL_MS; + } + + if (entry->last_blocks_request_ms != 0 + && now_ms < entry->last_blocks_request_ms + backoff_ms) + { + uint64_t resume_ms = entry->last_blocks_request_ms + backoff_ms; + uint64_t remaining_ms = resume_ms > now_ms ? (resume_ms - now_ms) : 0; + lantern_log_debug( + "reqresp", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text}, + "backing off blocks_by_root head=%s failures=%u remaining_ms=%" PRIu64, + head_root_text, + entry->consecutive_blocks_failures, + remaining_ms); + return false; + } + + entry->requested_head = true; + entry->last_blocks_request_ms = now_ms; + return true; +} + + +/** + * @brief Process peer status under status_lock and decide on a block request. + * + * @param client Client instance + * @param peer_status Peer status message + * @param peer_id_text Peer ID string for tracking/logging + * @param head_root_text Formatted head root (e.g., "0x1234..."), must be non-NULL + * @param local_slot Local slot snapshot + * @param head_known Whether the peer head is known locally + * @param out_request_root Output root to request (optional) + * @return true if a blocks_by_root request should be scheduled + * + * @note Thread safety: This function acquires status_lock + */ +static bool lantern_client_peer_status_maybe_request_blocks( + struct lantern_client *client, + const LanternStatusMessage *peer_status, + const char *peer_id_text, + const char *head_root_text, + uint64_t local_slot, + bool head_known, + LanternRoot *out_request_root) +{ + if (!client || !peer_status || !peer_id_text || !head_root_text) + { + return false; + } + + if (pthread_mutex_lock(&client->status_lock) != 0) + { + return false; + } + + bool should_request = false; + LanternRoot request_root = peer_status->head.root; + bool head_changed = false; + struct lantern_peer_status_entry *entry = + lantern_client_update_peer_status_entry_locked( + client, + peer_status, + peer_id_text, + &head_changed); + if (!entry) + { + pthread_mutex_unlock(&client->status_lock); + return false; + } + + bool needs_block = !head_known; + const char *needs_block_reason = NULL; + if (!head_known) + { + needs_block_reason = "head unknown locally"; + } + if (!needs_block && head_changed && peer_status->head.slot > local_slot) + { + needs_block = true; + needs_block_reason = "remote head ahead of local slot"; + } + + struct lantern_log_metadata status_meta = { + .validator = client->node_id, + .peer = peer_id_text[0] ? peer_id_text : NULL, + }; + if (needs_block) + { + lantern_log_info( + "reqresp", + &status_meta, + "status needs block head_slot=%" PRIu64 " local_slot=%" PRIu64 " " + "head_root=%s reason=%s", + peer_status->head.slot, + local_slot, + head_root_text, + needs_block_reason ? needs_block_reason : "unspecified"); + should_request = lantern_client_apply_blocks_request_backoff_locked( + client, + entry, + peer_id_text, + head_root_text); + } + else if (!needs_block) + { + lantern_log_trace( + "reqresp", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text}, + "skipping blocks_by_root for known head slot=%" PRIu64 " root=%s", + peer_status->head.slot, + head_root_text); + } + + pthread_mutex_unlock(&client->status_lock); + + if (out_request_root) + { + *out_request_root = request_root; + } + + return should_request; +} + + +/** + * @brief Update blocks request tracking state for a peer. + * + * @param client Client instance + * @param peer_id Peer ID string + * @param outcome Request outcome + * @param out_failure_count Output consecutive failure count + * @param out_entry_found Output whether peer entry was found + * @return true if tracking was updated (status_lock acquired), false otherwise + * + * @note Thread safety: This function acquires status_lock + */ +static bool lantern_client_update_blocks_request_tracking( + struct lantern_client *client, + const char *peer_id, + enum lantern_blocks_request_outcome outcome, + uint32_t *out_failure_count, + bool *out_entry_found) +{ + if (!client || !peer_id || !out_failure_count || !out_entry_found) + { + return false; + } + + *out_failure_count = 0; + *out_entry_found = false; + + const size_t peer_cap = sizeof(((struct lantern_peer_status_entry *)0)->peer_id); + if (pthread_mutex_lock(&client->status_lock) != 0) + { + return false; + } + + for (size_t i = 0; i < client->peer_status_count; ++i) + { + struct lantern_peer_status_entry *entry = &client->peer_status_entries[i]; + if (strncmp(entry->peer_id, peer_id, peer_cap) == 0) + { + entry->requested_head = false; + switch (outcome) + { + case LANTERN_BLOCKS_REQUEST_SUCCESS: + entry->consecutive_blocks_failures = 0; + break; + case LANTERN_BLOCKS_REQUEST_FAILED: + if (entry->consecutive_blocks_failures < UINT32_MAX) + { + entry->consecutive_blocks_failures += 1; + } + break; + case LANTERN_BLOCKS_REQUEST_ABORTED: + entry->last_blocks_request_ms = 0; + break; + default: + break; + } + if (outcome != LANTERN_BLOCKS_REQUEST_ABORTED && entry->last_blocks_request_ms == 0) + { + entry->last_blocks_request_ms = monotonic_millis(); + } + *out_failure_count = entry->consecutive_blocks_failures; + *out_entry_found = true; + break; + } + } + + pthread_mutex_unlock(&client->status_lock); + return true; +} + + +/** + * @brief Convert blocks request outcome to text. + * + * @param outcome Request outcome + * @return Static string label + */ +static const char *lantern_blocks_request_outcome_text(enum lantern_blocks_request_outcome outcome) +{ + switch (outcome) + { + case LANTERN_BLOCKS_REQUEST_SUCCESS: + return "success"; + case LANTERN_BLOCKS_REQUEST_FAILED: + return "failed"; + case LANTERN_BLOCKS_REQUEST_ABORTED: + return "aborted"; + default: + return "unknown"; + } +} + + +/** + * @brief Clear parent_requested flags for pending blocks matching request_root. + * + * @param client Client instance + * @param request_root Requested parent root + * + * @note Thread safety: This function acquires pending_lock + */ +static void lantern_client_clear_pending_parent_requested( + struct lantern_client *client, + const LanternRoot *request_root) +{ + if (!client || !request_root || lantern_root_is_zero(request_root)) + { + return; + } + + bool locked = lantern_client_lock_pending(client); + if (!locked) + { + return; + } + + for (size_t i = 0; i < client->pending_blocks.length; ++i) + { + struct lantern_pending_block *entry = &client->pending_blocks.items[i]; + if (memcmp(entry->parent_root.bytes, request_root->bytes, LANTERN_ROOT_SIZE) == 0) + { + entry->parent_requested = false; + } + } + + lantern_client_unlock_pending(client, locked); +} + + +/** + * @brief Issue a follow-up status request after successful blocks fetch. + * + * @param client Client instance + * @param peer_id Peer ID string + * + * @note Thread safety: This function is thread-safe + */ +static void lantern_client_request_status_after_blocks_success( + struct lantern_client *client, + const char *peer_id) +{ + if (!client || !peer_id || peer_id[0] == '\0') + { + return; + } + + peer_id_t parsed_peer = {0}; + bool parsed = peer_id_create_from_string(peer_id, &parsed_peer) == PEER_ID_SUCCESS; -/* ============================================================================ - * Reqresp Callbacks - * ============================================================================ */ + request_status_now(client, parsed ? &parsed_peer : NULL, peer_id); + + if (parsed) + { + peer_id_destroy(&parsed_peer); + } +} /** * Build a status message for reqresp protocol. @@ -78,7 +718,8 @@ static void lantern_client_adopt_peer_genesis( * * @param context Client instance * @param out_status Output status message - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs * * @note Thread safety: This function is thread-safe */ @@ -86,13 +727,13 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) { if (!context || !out_status) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_client *client = context; memset(out_status, 0, sizeof(*out_status)); if (!client->has_state) { - return 0; + return LANTERN_CLIENT_OK; } out_status->finalized = client->state.latest_finalized; @@ -103,7 +744,13 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) LanternRoot fork_head = {{0}}; uint64_t fork_slot = 0; if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head) == 0 - && lantern_fork_choice_block_info(&client->fork_choice, &fork_head, &fork_slot, NULL, NULL) == 0) + && lantern_fork_choice_block_info( + &client->fork_choice, + &fork_head, + &fork_slot, + NULL, + NULL) + == 0) { out_status->head.root = fork_head; out_status->head.slot = fork_slot; @@ -114,12 +761,15 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) if (!head_set) { out_status->head.slot = client->state.latest_block_header.slot; - if (lantern_hash_tree_root_block_header(&client->state.latest_block_header, &out_status->head.root) != 0) + if (lantern_hash_tree_root_block_header( + &client->state.latest_block_header, + &out_status->head.root) + != 0) { memset(&out_status->head.root, 0, sizeof(out_status->head.root)); } } - return 0; + return LANTERN_CLIENT_OK; } @@ -134,15 +784,19 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) * @param context Client instance * @param peer_status Status message from peer * @param peer_id Peer ID string - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs * * @note Thread safety: This function acquires status_lock */ -int reqresp_handle_status(void *context, const LanternStatusMessage *peer_status, const char *peer_id) +int reqresp_handle_status( + void *context, + const LanternStatusMessage *peer_status, + const char *peer_id) { if (!context || !peer_status) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_client *client = context; char head_hex[2 * LANTERN_ROOT_SIZE + 3]; @@ -155,13 +809,14 @@ int reqresp_handle_status(void *context, const LanternStatusMessage *peer_status &(const struct lantern_log_metadata){ .validator = client->node_id, .peer = peer_id}, - "peer status head_slot=%" PRIu64 " head_root=%s finalized_slot=%" PRIu64 " finalized_root=%s", + "peer status head_slot=%" PRIu64 " head_root=%s " + "finalized_slot=%" PRIu64 " finalized_root=%s", peer_status->head.slot, head_hex[0] ? head_hex : "0x0", peer_status->finalized.slot, finalized_hex[0] ? finalized_hex : "0x0"); lantern_client_on_peer_status(client, peer_status, peer_id); - return 0; + return LANTERN_CLIENT_OK; } @@ -191,12 +846,7 @@ void reqresp_status_failure(void *context, const char *peer_id, int error) } struct lantern_client *client = context; char peer_copy[sizeof(((struct lantern_peer_status_entry *)0)->peer_id)]; - memset(peer_copy, 0, sizeof(peer_copy)); - if (peer_id && *peer_id) - { - strncpy(peer_copy, peer_id, sizeof(peer_copy) - 1); - peer_copy[sizeof(peer_copy) - 1] = '\0'; - } + copy_peer_id_text(peer_id, peer_copy, sizeof(peer_copy)); if (error == 0) { error = LIBP2P_ERR_INTERNAL; @@ -207,117 +857,8 @@ void reqresp_status_failure(void *context, const char *peer_id, int error) lantern_client_status_request_failed(client, peer_copy); } - bool first_failure = true; - if (peer_copy[0] != '\0') - { - if (client->status_lock_initialized) - { - if (pthread_mutex_lock(&client->status_lock) == 0) - { - if (string_list_contains(&client->status_failure_peer_ids, peer_copy)) - { - first_failure = false; - } - else - { - (void)lantern_string_list_append(&client->status_failure_peer_ids, peer_copy); - } - pthread_mutex_unlock(&client->status_lock); - } - else - { - if (string_list_contains(&client->status_failure_peer_ids, peer_copy)) - { - first_failure = false; - } - else - { - (void)lantern_string_list_append(&client->status_failure_peer_ids, peer_copy); - } - } - } - else if (string_list_contains(&client->status_failure_peer_ids, peer_copy)) - { - first_failure = false; - } - else - { - (void)lantern_string_list_append(&client->status_failure_peer_ids, peer_copy); - } - } - - const char *reason = connection_reason_text(error); - struct lantern_log_metadata meta = { - .validator = client->node_id, - .peer = peer_copy[0] ? peer_copy : NULL, - }; - - if (error == LIBP2P_ERR_PROTO_NEGOTIATION_FAILED || error == LIBP2P_ERR_UNSUPPORTED) - { - if (first_failure) - { - lantern_log_info( - "reqresp", - &meta, - "peer does not support %s error=%d (%s)", - LANTERN_STATUS_PROTOCOL_ID, - error, - reason ? reason : "-"); - } - else - { - lantern_log_trace( - "reqresp", - &meta, - "peer still misses %s support error=%d (%s)", - LANTERN_STATUS_PROTOCOL_ID, - error, - reason ? reason : "-"); - } - return; - } - - if (error == LIBP2P_ERR_TIMEOUT) - { - if (first_failure) - { - lantern_log_warn( - "reqresp", - &meta, - "status request to peer timed out error=%d (%s)", - error, - reason ? reason : "-"); - } - else - { - lantern_log_debug( - "reqresp", - &meta, - "status request still timing out error=%d (%s)", - error, - reason ? reason : "-"); - } - return; - } - - if (first_failure) - { - lantern_log_warn( - "reqresp", - &meta, - "status request failed error=%d (%s)", - error, - reason ? reason : "-"); - } - else - { - lantern_log_debug( - "reqresp", - &meta, - "status request still failing error=%d (%s)", - error, - reason ? reason : "-"); - } + bool first_failure = record_status_failure_peer_id(client, peer_copy); + log_status_failure(client, peer_copy, error, first_failure); } @@ -333,7 +874,10 @@ void reqresp_status_failure(void *context, const char *peer_id, int error) * @param roots Array of block roots to collect * @param root_count Number of roots * @param out_blocks Output response structure - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs + * @return LANTERN_CLIENT_ERR_ALLOC on response allocation failure + * @return LANTERN_CLIENT_ERR_STORAGE on storage retrieval failure * * @note Thread safety: This function is thread-safe */ @@ -345,23 +889,26 @@ int reqresp_collect_blocks( { if (!context || !out_blocks) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_client *client = context; if (!client->data_dir) { - return lantern_blocks_by_root_response_resize(out_blocks, 0); + if (lantern_blocks_by_root_response_resize(out_blocks, 0) != 0) + { + return LANTERN_CLIENT_ERR_ALLOC; + } + return LANTERN_CLIENT_OK; } - int rc = lantern_storage_collect_blocks(client->data_dir, roots, root_count, out_blocks); - if (rc != 0) + if (lantern_storage_collect_blocks(client->data_dir, roots, root_count, out_blocks) != 0) { lantern_log_error( "reqresp", &(const struct lantern_log_metadata){.validator = client->node_id}, "failed to collect blocks from storage"); - return -1; + return LANTERN_CLIENT_ERR_STORAGE; } - return 0; + return LANTERN_CLIENT_OK; } @@ -406,36 +953,14 @@ static void lantern_client_on_peer_status( char head_hex[2 * LANTERN_ROOT_SIZE + 3]; format_root_hex(&peer_status->head.root, head_hex, sizeof(head_hex)); + const char *head_root_text = head_hex[0] ? head_hex : "0x0"; - const size_t peer_cap = sizeof(((struct lantern_peer_status_entry *)0)->peer_id); char peer_copy[sizeof(((struct lantern_peer_status_entry *)0)->peer_id)]; - memset(peer_copy, 0, sizeof(peer_copy)); - strncpy(peer_copy, peer_id, peer_cap - 1); + copy_peer_id_text(peer_id, peer_copy, sizeof(peer_copy)); - LanternRoot request_root = peer_status->head.root; uint64_t local_slot = 0; bool head_known = false; - bool state_locked = lantern_client_lock_state(client); - if (state_locked) - { - local_slot = client->state.slot; - head_known = lantern_client_block_known_locked(client, &peer_status->head.root, NULL); - } - else if (client->has_state) - { - local_slot = client->state.slot; - if (client->has_fork_choice) - { - uint64_t fork_slot = 0; - if (lantern_fork_choice_block_info(&client->fork_choice, &peer_status->head.root, &fork_slot, NULL, NULL) == 0) - { - head_known = true; - } - } - } - lantern_client_unlock_state(client, state_locked); - - bool should_request = false; + peer_status_local_view(client, &peer_status->head.root, &local_slot, &head_known); /* If we bootstrapped via genesis fallback and the peer advertises the genesis head, adopt the peer's head root as our anchor so that subsequent block requests use @@ -447,101 +972,15 @@ static void lantern_client_on_peer_status( head_known = true; } - if (pthread_mutex_lock(&client->status_lock) != 0) - { - return; - } - - struct lantern_peer_status_entry *entry = lantern_client_ensure_status_entry_locked(client, peer_copy); - if (!entry) - { - pthread_mutex_unlock(&client->status_lock); - return; - } - entry->status_request_inflight = false; - lantern_client_status_request_update_locked(client, entry, peer_copy, -1, "complete"); - - string_list_remove(&client->status_failure_peer_ids, peer_copy); - - bool had_status = entry->has_status; - LanternStatusMessage previous_status = entry->status; - bool head_changed = !had_status - || previous_status.head.slot != peer_status->head.slot - || memcmp(previous_status.head.root.bytes, peer_status->head.root.bytes, LANTERN_ROOT_SIZE) != 0; - - entry->status = *peer_status; - entry->has_status = true; - bool needs_block = !head_known; - const char *needs_block_reason = NULL; - if (!head_known) - { - needs_block_reason = "head unknown locally"; - } - if (!needs_block && head_changed && peer_status->head.slot > local_slot) - { - needs_block = true; - needs_block_reason = "remote head ahead of local slot"; - } - struct lantern_log_metadata status_meta = { - .validator = client->node_id, - .peer = peer_copy[0] ? peer_copy : NULL, - }; - if (needs_block) - { - lantern_log_info( - "reqresp", - &status_meta, - "status needs block head_slot=%" PRIu64 " local_slot=%" PRIu64 " head_root=%s reason=%s", - peer_status->head.slot, - local_slot, - head_hex[0] ? head_hex : "0x0", - needs_block_reason ? needs_block_reason : "unspecified"); - } - if (needs_block && !entry->requested_head) - { - uint64_t now_ms = monotonic_millis(); - uint64_t backoff_ms = blocks_request_backoff_ms(entry->consecutive_blocks_failures); - if (entry->consecutive_blocks_failures == 0 && backoff_ms < LANTERN_BLOCKS_REQUEST_MIN_POLL_MS) - { - backoff_ms = LANTERN_BLOCKS_REQUEST_MIN_POLL_MS; - } - bool within_backoff = entry->last_blocks_request_ms != 0 - && now_ms < entry->last_blocks_request_ms + backoff_ms; - if (!within_backoff) - { - entry->requested_head = true; - entry->last_blocks_request_ms = now_ms; - should_request = true; - } - else - { - uint64_t resume_ms = entry->last_blocks_request_ms + backoff_ms; - uint64_t remaining_ms = resume_ms > now_ms ? (resume_ms - now_ms) : 0; - lantern_log_debug( - "reqresp", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_copy}, - "backing off blocks_by_root head=%s failures=%u remaining_ms=%" PRIu64, - head_hex[0] ? head_hex : "0x0", - entry->consecutive_blocks_failures, - remaining_ms); - } - } - else if (!needs_block) - { - lantern_log_trace( - "reqresp", - &(const struct lantern_log_metadata){ - .validator = client->node_id, - .peer = peer_copy}, - "skipping blocks_by_root for known head slot=%" PRIu64 " root=%s", - peer_status->head.slot, - head_hex[0] ? head_hex : "0x0"); - } - - pthread_mutex_unlock(&client->status_lock); - + LanternRoot request_root = peer_status->head.root; + bool should_request = lantern_client_peer_status_maybe_request_blocks( + client, + peer_status, + peer_copy, + head_root_text, + local_slot, + head_known, + &request_root); if (should_request) { if (lantern_client_schedule_blocks_request(client, peer_copy, &request_root, false) != 0) @@ -588,8 +1027,6 @@ static void lantern_client_adopt_peer_genesis( /* Use the peer's advertised head root as both state_root and hint so our fork-choice anchor matches the peer even if we cannot reproduce their SSZ state. */ anchor.state_root = peer_status->head.root; - /* empty body / zero attestations */ - LanternCheckpoint zero_cp = {.root = {{0}}, .slot = 0}; if (lantern_fork_choice_set_anchor( &client->fork_choice, @@ -601,7 +1038,9 @@ static void lantern_client_adopt_peer_genesis( { lantern_log_warn( "fork_choice", - &(const struct lantern_log_metadata){.validator = client->node_id, .peer = peer_id_text}, + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text}, "failed to adopt peer genesis root"); return; } @@ -616,7 +1055,9 @@ static void lantern_client_adopt_peer_genesis( format_root_hex(&peer_status->head.root, head_hex, sizeof(head_hex)); lantern_log_info( "fork_choice", - &(const struct lantern_log_metadata){.validator = client->node_id, .peer = peer_id_text}, + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text}, "adopted peer genesis head_slot=0 root=%s", head_hex); } @@ -650,46 +1091,17 @@ void lantern_client_on_blocks_request_complete( { return; } - const size_t peer_cap = sizeof(((struct lantern_peer_status_entry *)0)->peer_id); uint32_t failure_count = 0; bool entry_found = false; - if (pthread_mutex_lock(&client->status_lock) != 0) + if (!lantern_client_update_blocks_request_tracking( + client, + peer_id, + outcome, + &failure_count, + &entry_found)) { return; } - for (size_t i = 0; i < client->peer_status_count; ++i) - { - struct lantern_peer_status_entry *entry = &client->peer_status_entries[i]; - if (strncmp(entry->peer_id, peer_id, peer_cap) == 0) - { - entry->requested_head = false; - switch (outcome) - { - case LANTERN_BLOCKS_REQUEST_SUCCESS: - entry->consecutive_blocks_failures = 0; - break; - case LANTERN_BLOCKS_REQUEST_FAILED: - if (entry->consecutive_blocks_failures < UINT32_MAX) - { - entry->consecutive_blocks_failures += 1; - } - break; - case LANTERN_BLOCKS_REQUEST_ABORTED: - entry->last_blocks_request_ms = 0; - break; - default: - break; - } - if (outcome != LANTERN_BLOCKS_REQUEST_ABORTED && entry->last_blocks_request_ms == 0) - { - entry->last_blocks_request_ms = monotonic_millis(); - } - failure_count = entry->consecutive_blocks_failures; - entry_found = true; - break; - } - } - pthread_mutex_unlock(&client->status_lock); char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; root_hex[0] = '\0'; @@ -697,21 +1109,7 @@ void lantern_client_on_blocks_request_complete( { format_root_hex(request_root, root_hex, sizeof(root_hex)); } - const char *outcome_text = "unknown"; - switch (outcome) - { - case LANTERN_BLOCKS_REQUEST_SUCCESS: - outcome_text = "success"; - break; - case LANTERN_BLOCKS_REQUEST_FAILED: - outcome_text = "failed"; - break; - case LANTERN_BLOCKS_REQUEST_ABORTED: - outcome_text = "aborted"; - break; - default: - break; - } + const char *outcome_text = lantern_blocks_request_outcome_text(outcome); lantern_log_info( "reqresp", &(const struct lantern_log_metadata){ @@ -723,46 +1121,10 @@ void lantern_client_on_blocks_request_complete( entry_found ? "true" : "false", failure_count); - if (request_root && !lantern_root_is_zero(request_root)) - { - bool locked = lantern_client_lock_pending(client); - if (locked) - { - for (size_t i = 0; i < client->pending_blocks.length; ++i) - { - struct lantern_pending_block *entry = &client->pending_blocks.items[i]; - if (memcmp(entry->parent_root.bytes, request_root->bytes, LANTERN_ROOT_SIZE) == 0) - { - entry->parent_requested = false; - } - } - lantern_client_unlock_pending(client, locked); - } - else - { - for (size_t i = 0; i < client->pending_blocks.length; ++i) - { - struct lantern_pending_block *entry = &client->pending_blocks.items[i]; - if (memcmp(entry->parent_root.bytes, request_root->bytes, LANTERN_ROOT_SIZE) == 0) - { - entry->parent_requested = false; - } - } - } - } + lantern_client_clear_pending_parent_requested(client, request_root); if (outcome == LANTERN_BLOCKS_REQUEST_SUCCESS && peer_id && peer_id[0] != '\0') { - peer_id_t parsed_peer = {0}; - bool parsed = false; - if (peer_id_create_from_string(peer_id, &parsed_peer) == PEER_ID_SUCCESS) - { - parsed = true; - } - request_status_now(client, parsed ? &parsed_peer : NULL, peer_id); - if (parsed) - { - peer_id_destroy(&parsed_peer); - } + lantern_client_request_status_after_blocks_success(client, peer_id); } } diff --git a/src/core/client_services_internal.h b/src/core/client_services_internal.h index d77e067..28bd5db 100644 --- a/src/core/client_services_internal.h +++ b/src/core/client_services_internal.h @@ -475,7 +475,7 @@ int lantern_reqresp_read_response_chunk( * * @note Thread safety: Caller must ensure exclusive access to the validator */ -void local_validator_cleanup(struct lantern_local_validator *validator); +void lantern_client_local_validator_cleanup(struct lantern_local_validator *validator); /** @@ -485,7 +485,7 @@ void local_validator_cleanup(struct lantern_local_validator *validator); * * @note Thread safety: Caller must ensure exclusive access during shutdown */ -void reset_local_validators(struct lantern_client *client); +void lantern_client_reset_local_validators(struct lantern_client *client); /** @@ -500,7 +500,7 @@ void reset_local_validators(struct lantern_client *client); * * @note Thread safety: This function is thread-safe */ -int decode_validator_secret(const char *hex, uint8_t **out_key, size_t *out_len); +int lantern_client_decode_validator_secret(const char *hex, uint8_t **out_key, size_t *out_len); /** @@ -512,7 +512,7 @@ int decode_validator_secret(const char *hex, uint8_t **out_key, size_t *out_len) * * @note Thread safety: This function should be called during initialization */ -int configure_hash_sig_sources( +int lantern_client_configure_hash_sig_sources( struct lantern_client *client, const struct lantern_client_options *options); @@ -527,7 +527,7 @@ int configure_hash_sig_sources( * * @note Thread safety: This function should be called during initialization */ -int load_hash_sig_keys(struct lantern_client *client); +int lantern_client_load_hash_sig_keys(struct lantern_client *client); /** @@ -537,7 +537,7 @@ int load_hash_sig_keys(struct lantern_client *client); * * @note Thread safety: Caller must ensure exclusive access during shutdown */ -void free_hash_sig_pubkeys(struct lantern_client *client); +void lantern_client_free_hash_sig_pubkeys(struct lantern_client *client); #ifdef __cplusplus From c8f984bf4e692f6701a05c08bec4c7494f7c02b2 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:12:45 +1000 Subject: [PATCH 02/12] Refactor --- include/lantern/networking/reqresp_service.h | 21 + src/core/client_reqresp.c | 11 - src/core/client_reqresp_blocks.c | 46 +- src/core/client_reqresp_stream.c | 1233 ++++++++++++------ src/core/client_services_internal.h | 53 +- 5 files changed, 906 insertions(+), 458 deletions(-) diff --git a/include/lantern/networking/reqresp_service.h b/include/lantern/networking/reqresp_service.h index 777596d..f65ed62 100644 --- a/include/lantern/networking/reqresp_service.h +++ b/include/lantern/networking/reqresp_service.h @@ -26,6 +26,27 @@ #define LANTERN_REQRESP_RESPONSE_INVALID_REQUEST 2u #define LANTERN_REQRESP_RESPONSE_SERVER_ERROR 3u +/** + * Reqresp service error codes. + * + * Functions return 0 on success and a negative value on failure. + * + * When a function also provides an `out_err` parameter, `out_err` contains the + * underlying libp2p error code or `-errno` value for debugging. + */ +typedef enum +{ + LANTERN_REQRESP_OK = 0, + LANTERN_REQRESP_ERR_INVALID_PARAM = -1000, + LANTERN_REQRESP_ERR_SET_DEADLINE = -1001, + LANTERN_REQRESP_ERR_SET_READ_INTEREST = -1002, + LANTERN_REQRESP_ERR_STREAM_READ = -1003, + LANTERN_REQRESP_ERR_STREAM_WRITE = -1004, + LANTERN_REQRESP_ERR_VARINT_HEADER_TOO_LONG = -1005, + LANTERN_REQRESP_ERR_PAYLOAD_TOO_LARGE = -1006, + LANTERN_REQRESP_ERR_ALLOC = -1007, +} lantern_reqresp_error; + enum lantern_reqresp_protocol_kind { LANTERN_REQRESP_PROTOCOL_STATUS = 0, LANTERN_REQRESP_PROTOCOL_BLOCKS_BY_ROOT = 1, diff --git a/src/core/client_reqresp.c b/src/core/client_reqresp.c index 12f8752..5850ace 100644 --- a/src/core/client_reqresp.c +++ b/src/core/client_reqresp.c @@ -38,17 +38,6 @@ #include "lantern/support/strings.h" -/* ============================================================================ - * External Functions (from client_reqresp_blocks.c) - * ============================================================================ */ - -extern int lantern_client_schedule_blocks_request( - struct lantern_client *client, - const char *peer_id_text, - const LanternRoot *root, - bool use_legacy); - - /* ============================================================================ * Forward Declarations * ============================================================================ */ diff --git a/src/core/client_reqresp_blocks.c b/src/core/client_reqresp_blocks.c index 87d1835..09265c1 100644 --- a/src/core/client_reqresp_blocks.c +++ b/src/core/client_reqresp_blocks.c @@ -43,17 +43,6 @@ * Constants * ============================================================================ */ -/** Maximum bytes for a reqresp header varint */ -#define LANTERN_REQRESP_HEADER_MAX_BYTES 10u - - -/* ============================================================================ - * External Stream I/O Functions (from client_reqresp_stream.c) - * ============================================================================ */ - -extern int stream_write_all(libp2p_stream_t *stream, const uint8_t *data, size_t length); - - /* ============================================================================ * Forward Declarations * ============================================================================ */ @@ -68,13 +57,6 @@ static bool lantern_client_process_stream_block_chunk( static void *block_request_worker(void *arg); static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int err); -/* Forward declaration for recursive call - defined later in this file */ -int lantern_client_schedule_blocks_request( - struct lantern_client *client, - const char *peer_id_text, - const LanternRoot *root, - bool use_legacy); - /* ============================================================================ * Block Request Context Management @@ -419,12 +401,15 @@ static void *block_request_worker(void *arg) root_hex[0] ? root_hex : "0x0", payload_len); - if (stream_write_all(stream, header, header_len) != 0 || stream_write_all(stream, payload, payload_len) != 0) + ssize_t write_err = 0; + if (stream_write_all(stream, header, header_len, &write_err) != 0 + || stream_write_all(stream, payload, payload_len, &write_err) != 0) { lantern_log_error( "reqresp", &meta, - "failed to write blocks_by_root request"); + "failed to write blocks_by_root request err=%zd", + write_err); schedule_legacy = !ctx->using_legacy; goto cleanup; } @@ -912,7 +897,10 @@ static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int * @param peer_id_text Peer ID string * @param root Block root to request * @param use_legacy True to use legacy protocol - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL, the peer ID is invalid, or the root is zero + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_NETWORK if stream dialing fails or networking is unavailable * * @note Thread safety: This function is thread-safe */ @@ -922,13 +910,17 @@ int lantern_client_schedule_blocks_request( const LanternRoot *root, bool use_legacy) { - if (!client || !peer_id_text || !root || !client->network.host) + if (!client || !peer_id_text || !root) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + if (!client->network.host) { - return -1; + return LANTERN_CLIENT_ERR_NETWORK; } if (lantern_root_is_zero(root)) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } if (client->debug_disable_block_requests) @@ -950,7 +942,7 @@ int lantern_client_schedule_blocks_request( struct block_request_ctx *ctx = (struct block_request_ctx *)calloc(1, sizeof(*ctx)); if (!ctx) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } ctx->client = client; ctx->root = *root; @@ -981,7 +973,7 @@ int lantern_client_schedule_blocks_request( .peer = peer_id_text}, "failed to parse peer id for blocks_by_root request"); block_request_ctx_free(ctx); - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; @@ -1011,7 +1003,7 @@ int lantern_client_schedule_blocks_request( "libp2p open stream failed rc=%d", rc); block_request_ctx_free(ctx); - return -1; + return LANTERN_CLIENT_ERR_NETWORK; } return 0; } diff --git a/src/core/client_reqresp_stream.c b/src/core/client_reqresp_stream.c index 96f6033..6db2623 100644 --- a/src/core/client_reqresp_stream.c +++ b/src/core/client_reqresp_stream.c @@ -18,33 +18,123 @@ #include "client_internal.h" -#include "lantern/networking/reqresp_service.h" -#include "lantern/support/strings.h" -#include "lantern/support/log.h" +#include +#include +#include +#include +#include +#include #include "libp2p/errors.h" #include "libp2p/stream.h" #include "multiformats/unsigned_varint/unsigned_varint.h" #include "peer_id/peer_id.h" -#include -#include -#include -#include +#include "lantern/networking/reqresp_service.h" +#include "lantern/support/log.h" +#include "lantern/support/strings.h" /* ============================================================================ * Constants * ============================================================================ */ -/** Maximum bytes for a reqresp header varint */ -#define LANTERN_REQRESP_HEADER_MAX_BYTES 10u +enum +{ + /** Peer ID string buffer size */ + LANTERN_REQRESP_PEER_TEXT_BYTES = 128, + + /** Payload length threshold for additional warning logs */ + LANTERN_REQRESP_SUSPICIOUS_PAYLOAD_BYTES = 512, +}; /* ============================================================================ * Forward Declarations * ============================================================================ */ +static void init_peer_log_metadata( + libp2p_stream_t *stream, + char *peer_text, + size_t peer_text_len, + struct lantern_log_metadata *out_meta); +static void hint_peer_legacy_framing( + struct lantern_reqresp_service *service, + const char *peer_text, + bool is_legacy); +static bool protocol_expects_response_code(enum lantern_reqresp_protocol_kind protocol); +static int set_stream_deadline( + libp2p_stream_t *stream, + uint64_t deadline_ms, + const struct lantern_log_metadata *meta, + const char *label, + ssize_t *out_err); +static int read_stream_byte_with_retry( + libp2p_stream_t *stream, + const struct lantern_log_metadata *meta, + const char *label, + uint8_t *out_byte, + ssize_t *out_err); +static int read_response_code_prefix( + struct lantern_reqresp_service *service, + libp2p_stream_t *stream, + bool expect_code, + const struct lantern_log_metadata *meta, + const char *peer_text, + uint8_t *out_frame_code, + uint8_t *out_response_code_byte, + bool *out_legacy_no_code, + uint8_t *out_response_code, + ssize_t *out_err); +static int read_payload_header_first_byte( + libp2p_stream_t *stream, + bool expect_code, + bool legacy_no_code, + uint8_t response_code_byte, + const struct lantern_log_metadata *meta, + uint8_t *out_header_first_byte, + ssize_t *out_err); +static int read_stream_exact( + libp2p_stream_t *stream, + const struct lantern_log_metadata *meta, + const char *label, + uint8_t *buffer, + size_t buffer_len, + size_t *out_read, + ssize_t *out_err); +static int read_varint_header_from_first_byte( + libp2p_stream_t *stream, + uint8_t first_byte, + uint8_t *header, + size_t header_len, + uint64_t *out_value, + size_t *out_consumed, + ssize_t *out_err, + const struct lantern_log_metadata *meta, + const char *label); +static void log_varint_header_details( + const uint8_t *header, + size_t consumed, + uint64_t payload_len, + const struct lantern_log_metadata *meta, + const char *label); +static int validate_payload_len( + uint64_t payload_len, + ssize_t *out_err, + const struct lantern_log_metadata *meta, + const char *label); +static int read_payload_bytes( + libp2p_stream_t *stream, + size_t payload_size, + uint8_t **out_buffer, + ssize_t *out_err, + const struct lantern_log_metadata *meta, + const char *label); +static void log_payload_read_complete( + const uint8_t *buffer, + size_t payload_size, + const struct lantern_log_metadata *meta, + const char *label); static int read_varint_payload_chunk( libp2p_stream_t *stream, uint8_t first_byte, @@ -56,215 +146,722 @@ static int read_varint_payload_chunk( /* ============================================================================ - * Stream Write Operations + * Internal Helpers * ============================================================================ */ /** - * Write all bytes to a stream. - * - * @spec Ethernet 2.0 Networking Spec - ReqResp Protocol - * - * Implements reliable stream writing with retry on AGAIN/TIMEOUT errors. - * Used for sending request payloads to peers. - * - * @param stream libp2p stream - * @param data Data to write - * @param length Number of bytes to write - * @return 0 on success, -1 on failure - * - * @note Thread safety: This function is thread-safe + * @brief Builds peer log metadata for a stream. */ -int stream_write_all(libp2p_stream_t *stream, const uint8_t *data, size_t length) +static void init_peer_log_metadata( + libp2p_stream_t *stream, + char *peer_text, + size_t peer_text_len, + struct lantern_log_metadata *out_meta) { - if (!stream || (!data && length > 0)) + if (!peer_text || peer_text_len == 0 || !out_meta) { - return -1; + return; } - size_t offset = 0; - while (offset < length) + + peer_text[0] = '\0'; + if (stream) { - ssize_t written = libp2p_stream_write(stream, data + offset, length - offset); - if (written > 0) + const peer_id_t *peer = libp2p_stream_remote_peer(stream); + if (peer + && peer_id_to_string(peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, peer_text_len) < 0) { - offset += (size_t)written; - continue; + peer_text[0] = '\0'; } - if (written == (ssize_t)LIBP2P_ERR_AGAIN || written == (ssize_t)LIBP2P_ERR_TIMEOUT) + } + + *out_meta = (struct lantern_log_metadata){.peer = peer_text[0] ? peer_text : NULL}; +} + + +/** + * @brief Records peer legacy framing preference. + */ +static void hint_peer_legacy_framing( + struct lantern_reqresp_service *service, + const char *peer_text, + bool is_legacy) +{ + if (!service || !peer_text || peer_text[0] == '\0') + { + return; + } + +#if defined(LANTERN_REQRESP_STATUS_PROTOCOL_LEGACY) \ + || defined(LANTERN_REQRESP_BLOCKS_BY_ROOT_PROTOCOL_LEGACY) + lantern_reqresp_service_hint_peer_legacy(service, peer_text, is_legacy ? 1 : 0); +#else + (void)is_legacy; +#endif +} + + +/** + * @brief Returns whether a protocol expects a response code prefix. + */ +static bool protocol_expects_response_code(enum lantern_reqresp_protocol_kind protocol) +{ + return (protocol == LANTERN_REQRESP_PROTOCOL_STATUS) + || (protocol == LANTERN_REQRESP_PROTOCOL_BLOCKS_BY_ROOT); +} + + +/** + * @brief Sets a stream deadline and validates the result. + */ +static int set_stream_deadline( + libp2p_stream_t *stream, + uint64_t deadline_ms, + const struct lantern_log_metadata *meta, + const char *label, + ssize_t *out_err) +{ + if (!stream) + { + if (out_err) + { + *out_err = LIBP2P_ERR_NULL_PTR; + } + return LANTERN_REQRESP_ERR_INVALID_PARAM; + } + + int rc = libp2p_stream_set_deadline(stream, deadline_ms); + if (rc != 0) + { + if (out_err) + { + *out_err = (ssize_t)rc; + } + lantern_log_warn( + "reqresp", + meta, + "%s failed to set deadline_ms=%" PRIu64 " err=%d", + label ? label : "stream", + deadline_ms, + rc); + return LANTERN_REQRESP_ERR_SET_DEADLINE; + } + if (out_err) + { + *out_err = 0; + } + return 0; +} + + +/** + * @brief Reads a single byte from a stream, retrying on AGAIN. + */ +static int read_stream_byte_with_retry( + libp2p_stream_t *stream, + const struct lantern_log_metadata *meta, + const char *label, + uint8_t *out_byte, + ssize_t *out_err) +{ + if (!stream || !out_byte) + { + if (out_err) + { + *out_err = LIBP2P_ERR_NULL_PTR; + } + return LANTERN_REQRESP_ERR_INVALID_PARAM; + } + + while (true) + { + int rc = set_stream_deadline( + stream, + LANTERN_REQRESP_STALL_TIMEOUT_MS, + meta, + label, + out_err); + if (rc != 0) + { + return rc; + } + + ssize_t n = libp2p_stream_read(stream, out_byte, 1); + if (n == 1) + { + if (set_stream_deadline(stream, 0, meta, label, NULL) != 0) + { + /* Best-effort: already logged */ + } + if (out_err) + { + *out_err = 0; + } + return 0; + } + if (n == (ssize_t)LIBP2P_ERR_AGAIN) { continue; } - return -1; + + if (set_stream_deadline(stream, 0, meta, label, NULL) != 0) + { + /* Best-effort: already logged */ + } + if (out_err) + { + *out_err = (n == 0) ? (ssize_t)LIBP2P_ERR_EOF : n; + } + return LANTERN_REQRESP_ERR_STREAM_READ; + } +} + + +/** + * @brief Reads and interprets the response code prefix for a chunk. + */ +static int read_response_code_prefix( + struct lantern_reqresp_service *service, + libp2p_stream_t *stream, + bool expect_code, + const struct lantern_log_metadata *meta, + const char *peer_text, + uint8_t *out_frame_code, + uint8_t *out_response_code_byte, + bool *out_legacy_no_code, + uint8_t *out_response_code, + ssize_t *out_err) +{ + if (!out_frame_code || !out_response_code_byte || !out_legacy_no_code) + { + if (out_err) + { + *out_err = LIBP2P_ERR_NULL_PTR; + } + return LANTERN_REQRESP_ERR_INVALID_PARAM; + } + + *out_frame_code = 0; + *out_response_code_byte = 0; + *out_legacy_no_code = !expect_code; + + if (!expect_code) + { + if (out_response_code) + { + *out_response_code = LANTERN_REQRESP_RESPONSE_SUCCESS; + } + if (out_err) + { + *out_err = 0; + } + return 0; + } + + uint8_t response_code_byte = 0; + ssize_t read_err = 0; + int rc = read_stream_byte_with_retry( + stream, + meta, + "response code", + &response_code_byte, + &read_err); + if (rc != 0) + { + if (out_err) + { + *out_err = read_err; + } + lantern_log_trace("reqresp", meta, "response code read failed err=%zd", read_err); + return rc; + } + + *out_frame_code = response_code_byte; + *out_response_code_byte = response_code_byte; + + if (response_code_byte > LANTERN_REQRESP_RESPONSE_SERVER_ERROR) + { + *out_legacy_no_code = true; + if (out_response_code) + { + *out_response_code = LANTERN_REQRESP_RESPONSE_SUCCESS; + } + + lantern_log_trace( + "reqresp", + meta, + "legacy response missing code, treating first byte as header (0x%02x)", + (unsigned)response_code_byte); + lantern_log_info( + "reqresp", + meta, + "response legacy framing first_byte=0x%02x", + (unsigned)response_code_byte); + hint_peer_legacy_framing(service, peer_text, true); + } + else + { + *out_legacy_no_code = false; + if (out_response_code) + { + *out_response_code = response_code_byte; + } + lantern_log_info("reqresp", meta, "response code=%u", (unsigned)response_code_byte); + hint_peer_legacy_framing(service, peer_text, false); + } + + if (out_err) + { + *out_err = 0; } return 0; } -/* ============================================================================ - * Varint Reading Operations - * ============================================================================ */ +/** + * @brief Reads the first byte of the varint payload header for a chunk. + */ +static int read_payload_header_first_byte( + libp2p_stream_t *stream, + bool expect_code, + bool legacy_no_code, + uint8_t response_code_byte, + const struct lantern_log_metadata *meta, + uint8_t *out_header_first_byte, + ssize_t *out_err) +{ + if (!stream || !out_header_first_byte) + { + if (out_err) + { + *out_err = LIBP2P_ERR_NULL_PTR; + } + return LANTERN_REQRESP_ERR_INVALID_PARAM; + } + + if (legacy_no_code && expect_code) + { + *out_header_first_byte = response_code_byte; + if (out_err) + { + *out_err = 0; + } + return 0; + } + + ssize_t read_err = 0; + int rc = read_stream_byte_with_retry( + stream, + meta, + "payload header", + out_header_first_byte, + &read_err); + if (rc != 0) + { + if (out_err) + { + *out_err = read_err; + } + lantern_log_trace("reqresp", meta, "response payload header read failed err=%zd", read_err); + return rc; + } + + if (out_err) + { + *out_err = 0; + } + return 0; +} + /** - * Read a varint from a stream. - * - * @spec Ethernet 2.0 Networking Spec - SSZ-snappy encoding with varint length prefix - * - * Reads an unsigned varint (LEB128-style) from the stream byte by byte. - * Used for decoding length-prefixed payloads in the reqresp protocol. - * - * @param stream libp2p stream - * @param out_value Output value - * @param meta Log metadata - * @param label Label for logging - * @param out_err Output error code (may be NULL) - * @return 0 on success, -1 on failure - * - * @note Thread safety: This function is thread-safe + * @brief Reads exactly buffer_len bytes from a stream. */ -int read_stream_varint( +static int read_stream_exact( libp2p_stream_t *stream, - uint64_t *out_value, const struct lantern_log_metadata *meta, const char *label, + uint8_t *buffer, + size_t buffer_len, + size_t *out_read, ssize_t *out_err) { - if (!stream || !out_value) + if (!stream || (!buffer && buffer_len > 0)) { if (out_err) { *out_err = LIBP2P_ERR_NULL_PTR; } - return -1; + return LANTERN_REQRESP_ERR_INVALID_PARAM; } - uint8_t header[LANTERN_REQRESP_HEADER_MAX_BYTES]; - size_t header_used = 0; - uint64_t value = 0; - ssize_t last_err = 0; - - while (header_used < sizeof(header)) + size_t collected = 0; + while (collected < buffer_len) { - (void)libp2p_stream_set_deadline(stream, LANTERN_REQRESP_STALL_TIMEOUT_MS); - ssize_t n = libp2p_stream_read(stream, &header[header_used], 1); - if (n == 1) + int rc = set_stream_deadline( + stream, + LANTERN_REQRESP_STALL_TIMEOUT_MS, + meta, + label, + out_err); + if (rc != 0) { - header_used += 1; - size_t consumed = 0; - if (unsigned_varint_decode(header, header_used, &value, &consumed) == UNSIGNED_VARINT_OK) + if (out_read) { - lantern_log_trace( - "reqresp", - meta, - "%s decoded length=%" PRIu64, - label ? label : "varint", - value); - (void)libp2p_stream_set_deadline(stream, 0); - *out_value = value; - if (out_err) - { - *out_err = 0; - } - return 0; + *out_read = collected; } + return rc; + } + + ssize_t n = libp2p_stream_read(stream, buffer + collected, buffer_len - collected); + if (n > 0) + { + collected += (size_t)n; continue; } if (n == (ssize_t)LIBP2P_ERR_AGAIN) { continue; } - if (n == 0 || n == (ssize_t)LIBP2P_ERR_EOF || n == (ssize_t)LIBP2P_ERR_CLOSED || n == (ssize_t)LIBP2P_ERR_RESET) + + if (set_stream_deadline(stream, 0, meta, label, NULL) != 0) + { + /* Best-effort: already logged */ + } + if (out_read) + { + *out_read = collected; + } + if (out_err) + { + *out_err = (n == 0) ? (ssize_t)LIBP2P_ERR_EOF : n; + } + return LANTERN_REQRESP_ERR_STREAM_READ; + } + if (set_stream_deadline(stream, 0, meta, label, NULL) != 0) + { + /* Best-effort: already logged */ + } + if (out_read) + { + *out_read = collected; + } + if (out_err) + { + *out_err = 0; + } + return 0; +} + + +/** + * @brief Reads and decodes a varint header after the first byte. + */ +static int read_varint_header_from_first_byte( + libp2p_stream_t *stream, + uint8_t first_byte, + uint8_t *header, + size_t header_len, + uint64_t *out_value, + size_t *out_consumed, + ssize_t *out_err, + const struct lantern_log_metadata *meta, + const char *label) +{ + if (!stream || !header || !out_value || !out_consumed) + { + if (out_err) + { + *out_err = LIBP2P_ERR_NULL_PTR; + } + return LANTERN_REQRESP_ERR_INVALID_PARAM; + } + + size_t used = 0; + size_t consumed = 0; + uint64_t value = 0; + header[used++] = first_byte; + + while (unsigned_varint_decode(header, used, &value, &consumed) != UNSIGNED_VARINT_OK) + { + if (used == header_len) + { + if (out_err) + { + *out_err = LIBP2P_ERR_INTERNAL; + } + lantern_log_warn( + "reqresp", + meta, + "%s varint header exceeded limit", + label ? label : "chunk"); + return LANTERN_REQRESP_ERR_VARINT_HEADER_TOO_LONG; + } + + uint8_t next_byte = 0; + ssize_t read_err = 0; + int rc = read_stream_byte_with_retry(stream, meta, label, &next_byte, &read_err); + if (rc != 0) + { + if (out_err) + { + *out_err = read_err; + } + lantern_log_warn( + "reqresp", + meta, + "%s header read failed err=%zd", + label ? label : "chunk", + read_err); + return rc; + } + + header[used++] = next_byte; + } + + *out_value = value; + *out_consumed = consumed; + if (out_err) + { + *out_err = 0; + } + return 0; +} + + +/** + * @brief Logs decoded varint header details. + */ +static void log_varint_header_details( + const uint8_t *header, + size_t consumed, + uint64_t payload_len, + const struct lantern_log_metadata *meta, + const char *label) +{ + char header_hex[(LANTERN_REQRESP_HEADER_MAX_BYTES * 2) + 1]; + header_hex[0] = '\0'; + if (lantern_bytes_to_hex(header, consumed, header_hex, sizeof(header_hex), 0) != 0) + { + header_hex[0] = '\0'; + } + + lantern_log_info( + "reqresp", + meta, + "%s payload_len=%" PRIu64 " header_hex=%s", + label ? label : "chunk", + payload_len, + header_hex[0] ? header_hex : "-"); + if (payload_len > (uint64_t)LANTERN_REQRESP_SUSPICIOUS_PAYLOAD_BYTES) + { + lantern_log_warn( + "reqresp", + meta, + "%s suspicious large payload_len=%" PRIu64 " header_hex=%s", + label ? label : "chunk", + payload_len, + header_hex[0] ? header_hex : "-"); + } +} + + +/** + * @brief Validates a decoded payload length. + */ +static int validate_payload_len( + uint64_t payload_len, + ssize_t *out_err, + const struct lantern_log_metadata *meta, + const char *label) +{ + if ((payload_len > (uint64_t)LANTERN_REQRESP_MAX_CHUNK_BYTES) + || (payload_len > (uint64_t)SIZE_MAX)) + { + if (out_err) + { + *out_err = LIBP2P_ERR_MSG_TOO_LARGE; + } + lantern_log_warn( + "reqresp", + meta, + "%s payload too large=%" PRIu64, + label ? label : "chunk", + payload_len); + return LANTERN_REQRESP_ERR_PAYLOAD_TOO_LARGE; + } + return 0; +} + + +/** + * @brief Allocates and reads a payload buffer. + */ +static int read_payload_bytes( + libp2p_stream_t *stream, + size_t payload_size, + uint8_t **out_buffer, + ssize_t *out_err, + const struct lantern_log_metadata *meta, + const char *label) +{ + if (!stream || !out_buffer) + { + if (out_err) + { + *out_err = LIBP2P_ERR_NULL_PTR; + } + return LANTERN_REQRESP_ERR_INVALID_PARAM; + } + + uint8_t *buffer = malloc(payload_size); + if (!buffer) + { + if (out_err) + { + *out_err = -ENOMEM; + } + lantern_log_error( + "reqresp", + meta, + "%s payload allocation failed bytes=%zu", + label ? label : "chunk", + payload_size); + return LANTERN_REQRESP_ERR_ALLOC; + } + + size_t collected = 0; + ssize_t read_err = 0; + int rc = read_stream_exact(stream, meta, label, buffer, payload_size, &collected, &read_err); + if (rc != 0) + { + if (collected > 0) + { + char partial_hex[(LANTERN_STATUS_PREVIEW_BYTES * 2u) + 1u]; + size_t preview_max = (size_t)LANTERN_STATUS_PREVIEW_BYTES; + size_t preview_len = collected < preview_max ? collected : preview_max; + if (lantern_bytes_to_hex(buffer, preview_len, partial_hex, sizeof(partial_hex), 0) != 0) + { + partial_hex[0] = '\0'; + } + lantern_log_trace( + "reqresp", + meta, + "%s payload partial hex=%s%s", + label ? label : "chunk", + partial_hex[0] ? partial_hex : "-", + (collected > preview_len) ? "..." : ""); + } + + free(buffer); + if (out_err) { - last_err = n == 0 ? (ssize_t)LIBP2P_ERR_EOF : n; - break; + *out_err = read_err; } - last_err = n; - break; + lantern_log_warn( + "reqresp", + meta, + "%s payload read failed err=%zd collected=%zu/%zu", + label ? label : "chunk", + read_err, + collected, + payload_size); + return rc; } - (void)libp2p_stream_set_deadline(stream, 0); if (out_err) { - *out_err = last_err == 0 ? LIBP2P_ERR_INTERNAL : last_err; + *out_err = 0; } - lantern_log_trace( + *out_buffer = buffer; + return 0; +} + + +/** + * @brief Logs a completed payload read with a hex preview. + */ +static void log_payload_read_complete( + const uint8_t *buffer, + size_t payload_size, + const struct lantern_log_metadata *meta, + const char *label) +{ + char payload_hex[(LANTERN_STATUS_PREVIEW_BYTES * 2u) + 1u]; + payload_hex[0] = '\0'; + size_t preview = payload_size < (size_t)LANTERN_STATUS_PREVIEW_BYTES + ? payload_size + : (size_t)LANTERN_STATUS_PREVIEW_BYTES; + if (preview > 0 + && lantern_bytes_to_hex(buffer, preview, payload_hex, sizeof(payload_hex), 0) != 0) + { + payload_hex[0] = '\0'; + } + lantern_log_info( "reqresp", meta, - "%s decode failed err=%zd bytes=%zu", - label ? label : "varint", - last_err == 0 ? (ssize_t)LIBP2P_ERR_INTERNAL : last_err, - header_used); - return -1; + "%s payload read complete bytes=%zu%s%s", + label ? label : "chunk", + payload_size, + payload_hex[0] ? " hex=" : "", + payload_hex[0] ? payload_hex : ""); } +/* ============================================================================ + * Stream Write Operations + * ============================================================================ */ + /** - * Discard bytes from a stream. + * Write all bytes to a stream. * - * @spec Ethernet 2.0 Networking Spec - Error recovery + * @spec Ethernet 2.0 Networking Spec - ReqResp Protocol * - * Reads and discards a specified number of bytes from the stream. - * Used for skipping error message payloads or unwanted data. + * Implements reliable stream writing with retry on AGAIN/TIMEOUT errors. + * Used for sending request payloads to peers. * - * @param stream libp2p stream - * @param length Number of bytes to discard - * @param meta Log metadata - * @param label Label for logging - * @param out_err Output error code (may be NULL) - * @return 0 on success, -1 on failure + * @param stream libp2p stream + * @param data Data to write + * @param length Number of bytes to write + * @param out_err Optional output error code (may be NULL) + * @return 0 on success + * @return LANTERN_REQRESP_ERR_INVALID_PARAM if parameters are invalid + * @return LANTERN_REQRESP_ERR_STREAM_WRITE on stream write failure * * @note Thread safety: This function is thread-safe */ -int discard_stream_bytes( +int stream_write_all( libp2p_stream_t *stream, - uint64_t length, - const struct lantern_log_metadata *meta, - const char *label, + const uint8_t *data, + size_t length, ssize_t *out_err) { - if (!stream) + if (!stream || (!data && length > 0)) { if (out_err) { *out_err = LIBP2P_ERR_NULL_PTR; } - return -1; + return LANTERN_REQRESP_ERR_INVALID_PARAM; } - uint8_t buffer[256]; - uint64_t remaining = length; - while (remaining > 0) + size_t offset = 0; + while (offset < length) { - size_t chunk = remaining > sizeof(buffer) ? sizeof(buffer) : (size_t)remaining; - (void)libp2p_stream_set_deadline(stream, LANTERN_REQRESP_STALL_TIMEOUT_MS); - ssize_t n = libp2p_stream_read(stream, buffer, chunk); - if (n > 0) + ssize_t written = libp2p_stream_write(stream, data + offset, length - offset); + if (written > 0) { - remaining -= (size_t)n; + offset += (size_t)written; continue; } - if (n == (ssize_t)LIBP2P_ERR_AGAIN) + if (written == (ssize_t)LIBP2P_ERR_AGAIN || written == (ssize_t)LIBP2P_ERR_TIMEOUT) { continue; } - (void)libp2p_stream_set_deadline(stream, 0); if (out_err) { - *out_err = n == 0 ? (ssize_t)LIBP2P_ERR_EOF : n; + *out_err = (written == 0) ? (ssize_t)LIBP2P_ERR_CLOSED : written; } - lantern_log_trace( - "reqresp", - meta, - "%s discard failed err=%zd remaining=%" PRIu64, - label ? label : "context", - n, - remaining); - return -1; + return LANTERN_REQRESP_ERR_STREAM_WRITE; } - (void)libp2p_stream_set_deadline(stream, 0); - lantern_log_trace( - "reqresp", - meta, - "%s discarded bytes=%" PRIu64, - label ? label : "context", - length); if (out_err) { *out_err = 0; @@ -300,7 +897,14 @@ int discard_stream_bytes( * @param out_err Output error code (may be NULL) * @param out_response_code Output response code (may be NULL) * @param response_code_pending Tracks whether response code is still expected - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_REQRESP_ERR_INVALID_PARAM if required parameters are NULL + * @return LANTERN_REQRESP_ERR_SET_READ_INTEREST if enabling read interest fails + * @return LANTERN_REQRESP_ERR_SET_DEADLINE if setting a stream deadline fails + * @return LANTERN_REQRESP_ERR_STREAM_READ if reading from the stream fails + * @return LANTERN_REQRESP_ERR_VARINT_HEADER_TOO_LONG if the varint header exceeds limits + * @return LANTERN_REQRESP_ERR_PAYLOAD_TOO_LARGE if the payload length exceeds limits + * @return LANTERN_REQRESP_ERR_ALLOC if allocating the payload buffer fails * * @note Thread safety: This function is thread-safe */ @@ -320,110 +924,52 @@ int lantern_reqresp_read_response_chunk( { *out_err = LIBP2P_ERR_NULL_PTR; } - return -1; + return LANTERN_REQRESP_ERR_INVALID_PARAM; } if (out_response_code) { *out_response_code = LANTERN_REQRESP_RESPONSE_SERVER_ERROR; } - char peer_text[128]; - peer_text[0] = '\0'; - const peer_id_t *peer = libp2p_stream_remote_peer(stream); - if (peer && peer_id_to_string(peer, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) + char peer_text[LANTERN_REQRESP_PEER_TEXT_BYTES]; + struct lantern_log_metadata meta; + init_peer_log_metadata(stream, peer_text, sizeof(peer_text), &meta); + + int interest_rc = libp2p_stream_set_read_interest(stream, true); + if (interest_rc != 0) { - peer_text[0] = '\0'; + if (out_err) + { + *out_err = (ssize_t)interest_rc; + } + lantern_log_warn( + "reqresp", + &meta, + "failed to set read interest err=%d", + interest_rc); + return LANTERN_REQRESP_ERR_SET_READ_INTEREST; } - const struct lantern_log_metadata meta = {.peer = peer_text[0] ? peer_text : NULL}; - - (void)libp2p_stream_set_read_interest(stream, true); - uint8_t response_code = 0; bool expect_code = response_code_pending ? *response_code_pending - : ((protocol == LANTERN_REQRESP_PROTOCOL_STATUS) || (protocol == LANTERN_REQRESP_PROTOCOL_BLOCKS_BY_ROOT)); - bool legacy_no_code = !expect_code; - ssize_t last_err = 0; + : protocol_expects_response_code(protocol); uint8_t frame_code = 0; - if (expect_code) - { - while (true) - { - (void)libp2p_stream_set_deadline(stream, LANTERN_REQRESP_STALL_TIMEOUT_MS); - ssize_t n = libp2p_stream_read(stream, &response_code, 1); - if (n == 1) - { - frame_code = response_code; - break; - } - if (n == (ssize_t)LIBP2P_ERR_AGAIN) - { - continue; - } - (void)libp2p_stream_set_deadline(stream, 0); - last_err = n == 0 ? (ssize_t)LIBP2P_ERR_EOF : n; - if (out_err) - { - *out_err = last_err; - } - lantern_log_trace( - "reqresp", - &meta, - "response code read failed err=%zd", - last_err); - return -1; - } - (void)libp2p_stream_set_deadline(stream, 0); - if (response_code > LANTERN_REQRESP_RESPONSE_SERVER_ERROR) - { - legacy_no_code = true; - if (out_response_code) - { - *out_response_code = LANTERN_REQRESP_RESPONSE_SUCCESS; - } - lantern_log_trace( - "reqresp", - &meta, - "legacy response missing code, treating first byte as header (0x%02x)", - (unsigned)response_code); - lantern_log_info( - "reqresp", - &meta, - "response legacy framing first_byte=0x%02x", - (unsigned)response_code); - if (service && peer_text[0] != '\0') - { -#if defined(LANTERN_REQRESP_STATUS_PROTOCOL_LEGACY) || defined(LANTERN_REQRESP_BLOCKS_BY_ROOT_PROTOCOL_LEGACY) - lantern_reqresp_service_hint_peer_legacy(service, peer_text, 1); -#endif - } - } - else - { - if (out_response_code) - { - *out_response_code = response_code; - } - frame_code = response_code; - lantern_log_info( - "reqresp", - &meta, - "response code=%u", - (unsigned)response_code); - if (service && peer_text[0] != '\0') - { -#if defined(LANTERN_REQRESP_STATUS_PROTOCOL_LEGACY) || defined(LANTERN_REQRESP_BLOCKS_BY_ROOT_PROTOCOL_LEGACY) - lantern_reqresp_service_hint_peer_legacy(service, peer_text, 0); -#endif - } - } - } - else + uint8_t response_code_byte = 0; + bool legacy_no_code = false; + int rc = read_response_code_prefix( + service, + stream, + expect_code, + &meta, + peer_text, + &frame_code, + &response_code_byte, + &legacy_no_code, + out_response_code, + out_err); + if (rc != 0) { - if (out_response_code) - { - *out_response_code = LANTERN_REQRESP_RESPONSE_SUCCESS; - } + return rc; } if (response_code_pending) { @@ -431,38 +977,17 @@ int lantern_reqresp_read_response_chunk( } uint8_t header_first_byte = 0; - if (legacy_no_code && expect_code) - { - header_first_byte = response_code; - } - else + rc = read_payload_header_first_byte( + stream, + expect_code, + legacy_no_code, + response_code_byte, + &meta, + &header_first_byte, + out_err); + if (rc != 0) { - while (true) - { - (void)libp2p_stream_set_deadline(stream, LANTERN_REQRESP_STALL_TIMEOUT_MS); - ssize_t n = libp2p_stream_read(stream, &header_first_byte, 1); - if (n == 1) - { - break; - } - if (n == (ssize_t)LIBP2P_ERR_AGAIN) - { - continue; - } - (void)libp2p_stream_set_deadline(stream, 0); - last_err = n == 0 ? (ssize_t)LIBP2P_ERR_EOF : n; - if (out_err) - { - *out_err = last_err; - } - lantern_log_trace( - "reqresp", - &meta, - "response payload header read failed err=%zd", - last_err); - return -1; - } - (void)libp2p_stream_set_deadline(stream, 0); + return rc; } lantern_log_trace( @@ -498,7 +1023,13 @@ int lantern_reqresp_read_response_chunk( * @param out_err Output error code (may be NULL) * @param meta Log metadata * @param label Label for logging - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_REQRESP_ERR_INVALID_PARAM if required parameters are NULL + * @return LANTERN_REQRESP_ERR_SET_DEADLINE if setting a stream deadline fails + * @return LANTERN_REQRESP_ERR_STREAM_READ if reading from the stream fails + * @return LANTERN_REQRESP_ERR_VARINT_HEADER_TOO_LONG if the varint header exceeds limits + * @return LANTERN_REQRESP_ERR_PAYLOAD_TOO_LARGE if the payload length exceeds limits + * @return LANTERN_REQRESP_ERR_ALLOC if allocating the payload buffer fails * * @note Thread safety: This function is thread-safe */ @@ -517,98 +1048,33 @@ static int read_varint_payload_chunk( { *out_err = LIBP2P_ERR_NULL_PTR; } - return -1; + return LANTERN_REQRESP_ERR_INVALID_PARAM; } uint8_t header[LANTERN_REQRESP_HEADER_MAX_BYTES]; - size_t used = 0; uint64_t payload_len = 0; size_t consumed = 0; - header[used++] = first_byte; - - while (true) - { - if (unsigned_varint_decode(header, used, &payload_len, &consumed) == UNSIGNED_VARINT_OK) - { - break; - } - if (used == sizeof(header)) - { - if (out_err) - { - *out_err = LIBP2P_ERR_INTERNAL; - } - lantern_log_warn( - "reqresp", - meta, - "%s varint header exceeded limit", - label ? label : "chunk"); - return -1; - } - (void)libp2p_stream_set_deadline(stream, LANTERN_REQRESP_STALL_TIMEOUT_MS); - ssize_t n = libp2p_stream_read(stream, &header[used], 1); - if (n == 1) - { - used += 1; - continue; - } - if (n == (ssize_t)LIBP2P_ERR_AGAIN) - { - continue; - } - (void)libp2p_stream_set_deadline(stream, 0); - if (out_err) - { - *out_err = n == 0 ? (ssize_t)LIBP2P_ERR_EOF : n; - } - lantern_log_warn( - "reqresp", - meta, - "%s header read failed err=%zd", - label ? label : "chunk", - n); - return -1; - } - (void)libp2p_stream_set_deadline(stream, 0); - - char header_hex[(sizeof(header) * 2) + 1]; - header_hex[0] = '\0'; - if (lantern_bytes_to_hex(header, consumed, header_hex, sizeof(header_hex), 0) != 0) - { - header_hex[0] = '\0'; - } - - lantern_log_info( - "reqresp", + int rc = read_varint_header_from_first_byte( + stream, + first_byte, + header, + sizeof(header), + &payload_len, + &consumed, + out_err, meta, - "%s payload_len=%" PRIu64 " header_hex=%s", - label ? label : "chunk", - payload_len, - header_hex[0] ? header_hex : "-"); - if (payload_len > 512) + label); + if (rc != 0) { - lantern_log_warn( - "reqresp", - meta, - "%s suspicious large payload_len=%" PRIu64 " header_hex=%s", - label ? label : "chunk", - payload_len, - header_hex[0] ? header_hex : "-"); + return rc; } - if (payload_len > LANTERN_REQRESP_MAX_CHUNK_BYTES || payload_len > SIZE_MAX) + log_varint_header_details(header, consumed, payload_len, meta, label); + + rc = validate_payload_len(payload_len, out_err, meta, label); + if (rc != 0) { - if (out_err) - { - *out_err = LIBP2P_ERR_MSG_TOO_LARGE; - } - lantern_log_warn( - "reqresp", - meta, - "%s payload too large=%" PRIu64, - label ? label : "chunk", - payload_len); - return -1; + return rc; } if (payload_len == 0) @@ -623,69 +1089,12 @@ static int read_varint_payload_chunk( } size_t payload_size = (size_t)payload_len; - uint8_t *buffer = (uint8_t *)malloc(payload_size); - if (!buffer) - { - if (out_err) - { - *out_err = -ENOMEM; - } - lantern_log_error( - "reqresp", - meta, - "%s payload allocation failed bytes=%zu", - label ? label : "chunk", - payload_size); - return -1; - } - - size_t collected = 0; - while (collected < payload_size) + uint8_t *buffer = NULL; + rc = read_payload_bytes(stream, payload_size, &buffer, out_err, meta, label); + if (rc != 0) { - (void)libp2p_stream_set_deadline(stream, LANTERN_REQRESP_STALL_TIMEOUT_MS); - ssize_t n = libp2p_stream_read(stream, buffer + collected, payload_size - collected); - if (n > 0) - { - collected += (size_t)n; - continue; - } - if (n == (ssize_t)LIBP2P_ERR_AGAIN) - { - continue; - } - (void)libp2p_stream_set_deadline(stream, 0); - if (collected > 0) - { - char partial_hex[(LANTERN_STATUS_PREVIEW_BYTES * 2u) + 1u]; - size_t preview_len = collected < LANTERN_STATUS_PREVIEW_BYTES ? collected : LANTERN_STATUS_PREVIEW_BYTES; - if (lantern_bytes_to_hex(buffer, preview_len, partial_hex, sizeof(partial_hex), 0) != 0) - { - partial_hex[0] = '\0'; - } - lantern_log_trace( - "reqresp", - meta, - "%s payload partial hex=%s%s", - label ? label : "chunk", - partial_hex[0] ? partial_hex : "-", - (collected > preview_len) ? "..." : ""); - } - free(buffer); - if (out_err) - { - *out_err = n == 0 ? (ssize_t)LIBP2P_ERR_EOF : n; - } - lantern_log_warn( - "reqresp", - meta, - "%s payload read failed err=%zd collected=%zu/%zu", - label ? label : "chunk", - n, - collected, - payload_size); - return -1; + return rc; } - (void)libp2p_stream_set_deadline(stream, 0); *out_data = buffer; *out_len = payload_size; @@ -693,21 +1102,7 @@ static int read_varint_payload_chunk( { *out_err = 0; } - char payload_hex[(LANTERN_STATUS_PREVIEW_BYTES * 2u) + 1u]; - payload_hex[0] = '\0'; - size_t preview = payload_size < LANTERN_STATUS_PREVIEW_BYTES ? payload_size : LANTERN_STATUS_PREVIEW_BYTES; - if (preview > 0 - && lantern_bytes_to_hex(buffer, preview, payload_hex, sizeof(payload_hex), 0) != 0) - { - payload_hex[0] = '\0'; - } - lantern_log_info( - "reqresp", - meta, - "%s payload read complete bytes=%zu%s%s", - label ? label : "chunk", - payload_size, - payload_hex[0] ? " hex=" : "", - payload_hex[0] ? payload_hex : ""); + + log_payload_read_complete(buffer, payload_size, meta, label); return 0; } diff --git a/src/core/client_services_internal.h b/src/core/client_services_internal.h index 28bd5db..78fafa5 100644 --- a/src/core/client_services_internal.h +++ b/src/core/client_services_internal.h @@ -449,7 +449,14 @@ void lantern_client_on_blocks_request_complete( * @param out_err Output error code (may be NULL) * @param out_response_code Output response code (may be NULL) * @param response_code_pending Tracks whether response code is still expected - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_REQRESP_ERR_INVALID_PARAM if required parameters are NULL + * @return LANTERN_REQRESP_ERR_SET_READ_INTEREST if enabling read interest fails + * @return LANTERN_REQRESP_ERR_SET_DEADLINE if setting a stream deadline fails + * @return LANTERN_REQRESP_ERR_STREAM_READ if reading from the stream fails + * @return LANTERN_REQRESP_ERR_VARINT_HEADER_TOO_LONG if the varint header exceeds limits + * @return LANTERN_REQRESP_ERR_PAYLOAD_TOO_LARGE if the payload length exceeds limits + * @return LANTERN_REQRESP_ERR_ALLOC if allocating the payload buffer fails * * @note Thread safety: This function is thread-safe */ @@ -463,6 +470,50 @@ int lantern_reqresp_read_response_chunk( uint8_t *out_response_code, bool *response_code_pending); +/** + * Schedule a blocks_by_root request to a peer. + * + * @spec subspecs/networking/reqresp/message.py - BlocksByRoot protocol + * + * @param client Client instance + * @param peer_id_text Peer ID string + * @param root Block root to request + * @param use_legacy True to use legacy protocol + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL, the peer ID is invalid, or the root is zero + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_NETWORK if stream dialing fails or networking is unavailable + * + * @note Thread safety: This function is thread-safe + */ +int lantern_client_schedule_blocks_request( + struct lantern_client *client, + const char *peer_id_text, + const LanternRoot *root, + bool use_legacy); + + +/** + * Write all bytes to a stream. + * + * Retries on AGAIN/TIMEOUT errors until all bytes are written. + * + * @param stream libp2p stream + * @param data Data to write + * @param length Number of bytes to write + * @param out_err Optional output error code (may be NULL) + * @return 0 on success + * @return LANTERN_REQRESP_ERR_INVALID_PARAM if parameters are invalid + * @return LANTERN_REQRESP_ERR_STREAM_WRITE on stream write failure + * + * @note Thread safety: This function is thread-safe + */ +int stream_write_all( + libp2p_stream_t *stream, + const uint8_t *data, + size_t length, + ssize_t *out_err); + /* ============================================================================ * Key Management Functions From 85b7f9bfdce54151e017ac7f3b8846934020d455 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:33:37 +1000 Subject: [PATCH 03/12] Refactor core --- src/core/client_http.c | 227 ++++++--- src/core/client_sync.c | 552 ++++++++++++++++------ src/core/client_sync_blocks.c | 759 +++++++++++++++++++++---------- src/core/client_sync_votes.c | 834 +++++++++++++++++++--------------- src/core/client_utils.c | 241 +++++++--- src/core/client_validator.c | 464 +++++++++++++------ 6 files changed, 2058 insertions(+), 1019 deletions(-) diff --git a/src/core/client_http.c b/src/core/client_http.c index 6455ab6..9182fbe 100644 --- a/src/core/client_http.c +++ b/src/core/client_http.c @@ -15,16 +15,54 @@ #include "client_internal.h" -#include "lantern/consensus/hash.h" +#include +#include +#include +#include +#include + #include "lantern/consensus/fork_choice.h" +#include "lantern/consensus/hash.h" #include "lantern/http/server.h" #include "lantern/metrics/lean_metrics.h" #include "lantern/support/log.h" -#include -#include -#include -#include + +enum +{ + LANTERN_CLIENT_HTTP_OK = 0, + LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM = -1, + LANTERN_CLIENT_HTTP_ERR_NOT_FOUND = -2, + LANTERN_CLIENT_HTTP_ERR_INVALID_STATE = -3, + LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED = -4, + LANTERN_CLIENT_HTTP_ERR_HASH_FAILED = -5, +}; + + +/** + * @brief Unlock a mutex and log on failure. + */ +static void unlock_mutex_with_log( + pthread_mutex_t *mutex, + const char *validator_id, + const char *name) +{ + if (!mutex || !name) + { + return; + } + + int unlock_rc = pthread_mutex_unlock(mutex); + if (unlock_rc != 0) + { + lantern_log_warn( + "client_http", + &(const struct lantern_log_metadata){.validator = validator_id}, + "failed to unlock %s: %d", + name, + unlock_rc); + } +} /* ============================================================================ @@ -37,7 +75,10 @@ * @param client Client instance * @param global_index Global validator index to find * @param out_index Output for local index - * @return 0 on success, -1 if not found + * + * @return 0 on success + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM if client or out_index is NULL + * @return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND if validator is not found * * @note Thread safety: This function is thread-safe */ @@ -46,22 +87,20 @@ int find_local_validator_index( uint64_t global_index, size_t *out_index) { - if (!client) + if (!client || !out_index) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; } for (size_t i = 0; i < client->local_validator_count; ++i) { - if (client->local_validators && client->local_validators[i].global_index == global_index) + if (client->local_validators + && client->local_validators[i].global_index == global_index) { - if (out_index) - { - *out_index = i; - } - return 0; + *out_index = i; + return LANTERN_CLIENT_HTTP_OK; } } - return -1; + return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND; } @@ -74,30 +113,51 @@ int find_local_validator_index( * * @param context Client instance * @param out_snapshot Output snapshot structure - * @return 0 on success, -1 on failure * - * @note Thread safety: This function is thread-safe + * @return 0 on success + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM if context or out_snapshot is NULL + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_STATE if client has no state + * @return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED if state_lock is initialized but cannot be acquired + * @return LANTERN_CLIENT_HTTP_ERR_HASH_FAILED if head root cannot be computed + * + * @note Thread safety: This function may acquire state_lock */ int http_snapshot_head(void *context, struct lantern_http_head_snapshot *out_snapshot) { if (!context || !out_snapshot) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; } struct lantern_client *client = context; + memset(out_snapshot, 0, sizeof(*out_snapshot)); + + const bool expect_state_lock = client->state_lock_initialized; + bool state_locked = lantern_client_lock_state(client); + if (expect_state_lock && !state_locked) + { + return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + } + if (!client->has_state) { - return -1; + lantern_client_unlock_state(client, state_locked); + return LANTERN_CLIENT_HTTP_ERR_INVALID_STATE; } - memset(out_snapshot, 0, sizeof(*out_snapshot)); - out_snapshot->slot = client->state.slot; - if (lantern_hash_tree_root_block_header(&client->state.latest_block_header, &out_snapshot->head_root) != 0) + + LanternBlockHeader head_header = client->state.latest_block_header; + LanternCheckpoint justified = client->state.latest_justified; + LanternCheckpoint finalized = client->state.latest_finalized; + lantern_client_unlock_state(client, state_locked); + + out_snapshot->slot = head_header.slot; + out_snapshot->justified = justified; + out_snapshot->finalized = finalized; + if (lantern_hash_tree_root_block_header(&head_header, &out_snapshot->head_root) != 0) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_HASH_FAILED; } - out_snapshot->justified = client->state.latest_justified; - out_snapshot->finalized = client->state.latest_finalized; - return 0; + + return LANTERN_CLIENT_HTTP_OK; } @@ -130,20 +190,29 @@ size_t http_validator_count_cb(void *context) * @param context Client instance * @param index Local validator index * @param out_info Output info structure - * @return 0 on success, -1 on failure * - * @note Thread safety: This function acquires validator_lock + * @return 0 on success + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM if context or out_info is NULL + * @return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND if index is out of bounds or validator data is + * unavailable + * @return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED if validator_lock is initialized but cannot be + * acquired + * + * @note Thread safety: This function may acquire validator_lock */ -int http_validator_info_cb(void *context, size_t index, struct lantern_http_validator_info *out_info) +int http_validator_info_cb( + void *context, + size_t index, + struct lantern_http_validator_info *out_info) { if (!context || !out_info) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; } struct lantern_client *client = context; if (index >= client->local_validator_count || !client->local_validators) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND; } memset(out_info, 0, sizeof(*out_info)); out_info->global_index = client->local_validators[index].global_index; @@ -153,13 +222,13 @@ int http_validator_info_cb(void *context, size_t index, struct lantern_http_vali { if (pthread_mutex_lock(&client->validator_lock) != 0) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; } if (client->validator_enabled && index < client->local_validator_count) { enabled = client->validator_enabled[index]; } - pthread_mutex_unlock(&client->validator_lock); + unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); } else if (client->validator_enabled && index < client->local_validator_count) { @@ -168,13 +237,18 @@ int http_validator_info_cb(void *context, size_t index, struct lantern_http_vali out_info->enabled = enabled; const char *base = client->node_id ? client->node_id : "validator"; - int written = snprintf(out_info->label, sizeof(out_info->label), "%s#%" PRIu64, base, out_info->global_index); + int written = snprintf( + out_info->label, + sizeof(out_info->label), + "%s#%" PRIu64, + base, + out_info->global_index); if (written < 0 || (size_t)written >= sizeof(out_info->label)) { strncpy(out_info->label, base, sizeof(out_info->label)); out_info->label[sizeof(out_info->label) - 1] = '\0'; } - return 0; + return LANTERN_CLIENT_HTTP_OK; } @@ -184,7 +258,12 @@ int http_validator_info_cb(void *context, size_t index, struct lantern_http_vali * @param context Client instance * @param global_index Global validator index * @param enabled New enabled status - * @return 0 on success, -1 on failure + * + * @return 0 on success + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM if context is NULL + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_STATE if validator tracking is not initialized + * @return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED if validator_lock cannot be acquired + * @return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND if global_index is not a local validator * * @note Thread safety: This function acquires validator_lock */ @@ -192,46 +271,41 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab { if (!context) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; } struct lantern_client *client = context; if (!client->validator_lock_initialized || !client->validator_enabled) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_INVALID_STATE; } if (pthread_mutex_lock(&client->validator_lock) != 0) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; } size_t local_index = 0; if (find_local_validator_index(client, global_index, &local_index) != 0 || local_index >= client->local_validator_count) { - pthread_mutex_unlock(&client->validator_lock); - return -1; + unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); + return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND; } client->validator_enabled[local_index] = enabled; + size_t enabled_count = 0; size_t disabled_count = 0; - if (client->validator_enabled) + for (size_t i = 0; i < client->local_validator_count; ++i) { - for (size_t i = 0; i < client->local_validator_count; ++i) + if (client->validator_enabled[i]) { - if (client->validator_enabled[i]) - { - ++enabled_count; - } + ++enabled_count; } - if (client->local_validator_count > enabled_count) + else { - disabled_count = client->local_validator_count - enabled_count; + ++disabled_count; } } - else - { - enabled_count = client->local_validator_count; - } - pthread_mutex_unlock(&client->validator_lock); + + unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); lantern_log_info( "validator", @@ -241,7 +315,8 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab enabled ? "activated" : "deactivated", enabled_count, disabled_count); - return 0; + + return LANTERN_CLIENT_HTTP_OK; } @@ -254,19 +329,29 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab * * @param context Client instance * @param out_snapshot Output snapshot structure - * @return 0 on success, -1 on failure * - * @note Thread safety: This function acquires state_lock and peer_vote_lock + * @return 0 on success + * @return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM if context or out_snapshot is NULL + * @return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED if required locks cannot be acquired + * + * @note Thread safety: This function may acquire state_lock and peer_vote_lock */ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snapshot) { if (!context || !out_snapshot) { - return -1; + return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; } struct lantern_client *client = context; memset(out_snapshot, 0, sizeof(*out_snapshot)); + const bool expect_state_lock = client->state_lock_initialized; + bool state_locked = lantern_client_lock_state(client); + if (expect_state_lock && !state_locked) + { + return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + } + bool have_fork_head = false; LanternRoot fork_head_root; memset(&fork_head_root, 0, sizeof(fork_head_root)); @@ -276,7 +361,13 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head_root) == 0) { uint64_t slot = 0; - if (lantern_fork_choice_block_info(&client->fork_choice, &fork_head_root, &slot, NULL, NULL) == 0) + if (lantern_fork_choice_block_info( + &client->fork_choice, + &fork_head_root, + &slot, + NULL, + NULL) + == 0) { fork_head_slot = slot; have_fork_head = true; @@ -289,7 +380,6 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap LanternCheckpoint state_finalized; memset(&state_justified, 0, sizeof(state_justified)); memset(&state_finalized, 0, sizeof(state_finalized)); - bool state_locked = lantern_client_lock_state(client); if (client->has_state) { /* Use the latest_block_header slot which is the actual block slot, @@ -307,19 +397,26 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap out_snapshot->peer_vote_metrics_count = 0; if (client->peer_vote_lock_initialized) { - if (pthread_mutex_lock(&client->peer_vote_lock) == 0) + if (pthread_mutex_lock(&client->peer_vote_lock) != 0) + { + return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + } + { - size_t limit = LANTERN_METRICS_MAX_PEER_VOTE_STATS; - for (size_t i = 0; i < client->peer_vote_stats_len && out_snapshot->peer_vote_metrics_count < limit; ++i) + const size_t limit = LANTERN_METRICS_MAX_PEER_VOTE_STATS; + for (size_t i = 0; + i < client->peer_vote_stats_len + && out_snapshot->peer_vote_metrics_count < limit; + ++i) { const struct lantern_peer_vote_metric *entry = &client->peer_vote_stats[i]; struct lantern_peer_vote_metric *metric = &out_snapshot->peer_vote_metrics[out_snapshot->peer_vote_metrics_count++]; *metric = *entry; } - pthread_mutex_unlock(&client->peer_vote_lock); + unlock_mutex_with_log(&client->peer_vote_lock, client->node_id, "peer_vote_lock"); } } lean_metrics_snapshot(&out_snapshot->lean_metrics); - return 0; + return LANTERN_CLIENT_HTTP_OK; } diff --git a/src/core/client_sync.c b/src/core/client_sync.c index 4176935..554f04c 100644 --- a/src/core/client_sync.c +++ b/src/core/client_sync.c @@ -17,6 +17,12 @@ #include "client_internal.h" +#include +#include +#include + +#include "peer_id/peer_id.h" + #include "lantern/consensus/containers.h" #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" @@ -25,11 +31,16 @@ #include "lantern/support/log.h" #include "lantern/support/strings.h" -#include "peer_id/peer_id.h" +/* ============================================================================ + * Constants + * ============================================================================ */ -#include -#include -#include +enum +{ + ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, + PEER_TEXT_BUFFER_LEN = 128, + VALIDATOR_PUBKEY_HEX_BUFFER_LEN = (LANTERN_VALIDATOR_PUBKEY_SIZE * 2u) + 3u, +}; /* ============================================================================ @@ -126,6 +137,39 @@ size_t lantern_client_enabled_validator_count(struct lantern_client *client) * Gossip Handlers * ============================================================================ */ +/** + * @brief Convert a peer ID to text for logging. + * + * @param from Peer ID (may be NULL) + * @param out Output buffer + * @param out_len Output buffer length + * @return Peer ID text, or NULL if unavailable + * + * @note Thread safety: This function is thread-safe + */ +static const char *peer_id_to_text(const peer_id_t *from, char *out, size_t out_len) +{ + if (!out || out_len == 0) + { + return NULL; + } + + out[0] = '\0'; + if (!from) + { + return NULL; + } + + if (peer_id_to_string(from, PEER_ID_FMT_BASE58_LEGACY, out, out_len) < 0) + { + out[0] = '\0'; + return NULL; + } + + return out[0] ? out : NULL; +} + + /** * Handle a block received via gossip. * @@ -139,6 +183,9 @@ size_t lantern_client_enabled_validator_count(struct lantern_client *client) * @param from Peer ID of sender * @param context Client instance * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if block or context is NULL + * + * @note Thread safety: This function is thread-safe */ int gossip_block_handler( const LanternSignedBlock *block, @@ -147,19 +194,15 @@ int gossip_block_handler( { if (!block || !context) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_client *client = context; - char peer_text[128]; - peer_text[0] = '\0'; - if (from && peer_id_to_string(from, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } + char peer_text[PEER_TEXT_BUFFER_LEN]; + const char *peer_id_text = peer_id_to_text(from, peer_text, sizeof(peer_text)); - lantern_client_record_block(client, block, NULL, peer_text[0] ? peer_text : NULL, "gossip"); - return 0; + lantern_client_record_block(client, block, NULL, peer_id_text, "gossip"); + return LANTERN_CLIENT_OK; } @@ -176,6 +219,9 @@ int gossip_block_handler( * @param from Peer ID of sender * @param context Client instance * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if vote or context is NULL + * + * @note Thread safety: This function is thread-safe */ int gossip_vote_handler( const LanternSignedVote *vote, @@ -184,22 +230,16 @@ int gossip_vote_handler( { if (!vote || !context) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_client *client = context; - char peer_text[128]; - peer_text[0] = '\0'; - if (from) - { - if (peer_id_to_string(from, PEER_ID_FMT_BASE58_LEGACY, peer_text, sizeof(peer_text)) < 0) - { - peer_text[0] = '\0'; - } - } - const char *peer_id_text = peer_text[0] ? peer_text : NULL; + + char peer_text[PEER_TEXT_BUFFER_LEN]; + const char *peer_id_text = peer_id_to_text(from, peer_text, sizeof(peer_text)); + lantern_client_note_vote_delivery(client, peer_id_text, vote); lantern_client_record_vote(client, vote, peer_id_text); - return 0; + return LANTERN_CLIENT_OK; } @@ -249,7 +289,7 @@ void persist_anchor_block( root_to_log = &computed_root; } } - char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char root_hex[ROOT_HEX_BUFFER_LEN]; root_hex[0] = '\0'; if (root_to_log) { @@ -281,6 +321,88 @@ void persist_anchor_block( * Fork Choice Initialization * ============================================================================ */ +/** + * @brief Compute genesis anchor roots for fork choice initialization. + * + * @param client Client instance + * @param meta Logging metadata + * @param out_state_root Output computed state root + * @param out_anchor_header Output anchor header (state_root updated) + * @param out_anchor_root Output computed anchor root + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_ERR_RUNTIME on hashing failure + * + * @note Thread safety: Caller must ensure exclusive access during initialization + */ +static int compute_fork_choice_anchor_roots( + struct lantern_client *client, + const struct lantern_log_metadata *meta, + LanternRoot *out_state_root, + LanternBlockHeader *out_anchor_header, + LanternRoot *out_anchor_root) +{ + if (!client || !meta || !out_state_root || !out_anchor_header || !out_anchor_root) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + if (lantern_hash_tree_root_state(&client->state, out_state_root) != 0) + { + lantern_log_error("forkchoice", meta, "failed to hash anchor state"); + return LANTERN_CLIENT_ERR_RUNTIME; + } + + *out_anchor_header = client->state.latest_block_header; + out_anchor_header->state_root = *out_state_root; + + if (lantern_hash_tree_root_block_header(out_anchor_header, out_anchor_root) != 0) + { + lantern_log_error("forkchoice", meta, "failed to hash anchor block header"); + return LANTERN_CLIENT_ERR_RUNTIME; + } + + return LANTERN_CLIENT_OK; +} + + +/** + * @brief Log genesis anchor roots for debugging mismatches. + * + * @param meta Logging metadata + * @param anchor_root Anchor block root + * @param state_root Anchor state root + * @param body_root Anchor body root + * @param slot Anchor slot + * + * @note Thread safety: This function is thread-safe + */ +static void log_genesis_anchor_roots( + const struct lantern_log_metadata *meta, + const LanternRoot *anchor_root, + const LanternRoot *state_root, + const LanternRoot *body_root, + uint64_t slot) +{ + char anchor_root_hex[ROOT_HEX_BUFFER_LEN]; + char state_root_hex[ROOT_HEX_BUFFER_LEN]; + char body_root_hex[ROOT_HEX_BUFFER_LEN]; + + format_root_hex(anchor_root, anchor_root_hex, sizeof(anchor_root_hex)); + format_root_hex(state_root, state_root_hex, sizeof(state_root_hex)); + format_root_hex(body_root, body_root_hex, sizeof(body_root_hex)); + + lantern_log_info( + "forkchoice", + meta, + "genesis anchor_root=%s state_root=%s body_root=%s slot=%" PRIu64, + anchor_root_hex[0] ? anchor_root_hex : "0x0", + state_root_hex[0] ? state_root_hex : "0x0", + body_root_hex[0] ? body_root_hex : "0x0", + slot); +} + + /** * Initialize fork choice from genesis state. * @@ -299,7 +421,9 @@ void persist_anchor_block( * with state_root = ZERO. * * @param client Client instance - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if client is NULL or missing state + * @return LANTERN_CLIENT_ERR_RUNTIME on fork choice initialization failure * * @note Thread safety: Should be called during initialization */ @@ -307,27 +431,10 @@ int initialize_fork_choice(struct lantern_client *client) { if (!client || !client->has_state) { - return -1; - } - lantern_fork_choice_reset(&client->fork_choice); - if (lantern_fork_choice_configure(&client->fork_choice, &client->state.config) != 0) - { - lantern_log_error( - "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, - "failed to configure fork choice"); - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } - LanternRoot anchor_state_root; - if (lantern_hash_tree_root_state(&client->state, &anchor_state_root) != 0) - { - lantern_log_error( - "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, - "failed to hash anchor state"); - return -1; - } + const struct lantern_log_metadata meta = {.validator = client->node_id}; /* Create a copy of the header for computing anchor_root. * @@ -339,37 +446,37 @@ int initialize_fork_choice(struct lantern_client *client) * We compute anchor_root from a header with the ACTUAL state_root, * matching Zeam's genStateBlockHeader() behavior. */ - LanternBlockHeader anchor_header = client->state.latest_block_header; - anchor_header.state_root = anchor_state_root; - - LanternRoot anchor_root; - if (lantern_hash_tree_root_block_header(&anchor_header, &anchor_root) != 0) + lantern_fork_choice_reset(&client->fork_choice); + if (lantern_fork_choice_configure(&client->fork_choice, &client->state.config) != 0) { lantern_log_error( "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, - "failed to hash anchor block header"); - return -1; + &meta, + "failed to configure fork choice"); + return LANTERN_CLIENT_ERR_RUNTIME; } - /* Log the anchor root for debugging genesis mismatch issues */ + LanternRoot anchor_state_root; + LanternBlockHeader anchor_header; + LanternRoot anchor_root; + int root_rc = compute_fork_choice_anchor_roots( + client, + &meta, + &anchor_state_root, + &anchor_header, + &anchor_root); + if (root_rc != LANTERN_CLIENT_OK) { - char anchor_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char state_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char body_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(&anchor_root, anchor_root_hex, sizeof(anchor_root_hex)); - format_root_hex(&anchor_state_root, state_root_hex, sizeof(state_root_hex)); - format_root_hex(&anchor_header.body_root, body_root_hex, sizeof(body_root_hex)); - lantern_log_info( - "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, - "genesis anchor_root=%s state_root=%s body_root=%s slot=%lu", - anchor_root_hex, - state_root_hex, - body_root_hex, - (unsigned long)anchor_header.slot); + return root_rc; } + log_genesis_anchor_roots( + &meta, + &anchor_root, + &anchor_state_root, + &anchor_header.body_root, + anchor_header.slot); + /* Also update the state's header state_root for subsequent state transitions */ if (memcmp( client->state.latest_block_header.state_root.bytes, @@ -380,7 +487,7 @@ int initialize_fork_choice(struct lantern_client *client) client->state.latest_block_header.state_root = anchor_state_root; lantern_log_debug( "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, + &meta, "updated genesis header state_root"); } @@ -403,31 +510,39 @@ int initialize_fork_choice(struct lantern_client *client) lantern_block_body_reset(&anchor.body); lantern_log_error( "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, + &meta, "failed to set fork choice anchor"); - return -1; + return LANTERN_CLIENT_ERR_RUNTIME; } - if (memcmp(client->state.latest_justified.root.bytes, anchor_root.bytes, LANTERN_ROOT_SIZE) != 0) + if (memcmp( + client->state.latest_justified.root.bytes, + anchor_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { client->state.latest_justified.root = anchor_root; lantern_log_debug( "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, + &meta, "updated justified checkpoint root to anchor"); } - if (memcmp(client->state.latest_finalized.root.bytes, anchor_root.bytes, LANTERN_ROOT_SIZE) != 0) + if (memcmp( + client->state.latest_finalized.root.bytes, + anchor_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { client->state.latest_finalized.root = anchor_root; lantern_log_debug( "forkchoice", - &(const struct lantern_log_metadata){.validator = client->node_id}, + &meta, "updated finalized checkpoint root to anchor"); } persist_anchor_block(client, &anchor, &anchor_root); lantern_block_body_reset(&anchor.body); lantern_state_attach_fork_choice(&client->state, &client->fork_choice); client->has_fork_choice = true; - return 0; + return LANTERN_CLIENT_OK; } @@ -436,20 +551,37 @@ int initialize_fork_choice(struct lantern_client *client) * ============================================================================ */ /** - * Visitor callback for storage block iteration. + * @brief Visitor callback for storage block iteration. + * + * @param block Persisted block + * @param root Block root + * @param context Persisted block list + * @return 0 on success, non-zero to abort iteration + * + * @note Thread safety: Should be called during initialization */ static int collect_block_visitor( const LanternSignedBlock *block, const LanternRoot *root, void *context) { + if (!block || !root || !context) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } struct lantern_persisted_block_list *list = context; return persisted_block_list_append(list, block, root); } /** - * Compare persisted blocks by slot for sorting. + * @brief Compare persisted blocks by slot for sorting. + * + * @param lhs_ptr Left block entry + * @param rhs_ptr Right block entry + * @return <0 if lhs < rhs, >0 if lhs > rhs, 0 if equal + * + * @note Thread safety: This function is thread-safe */ static int compare_blocks_by_slot(const void *lhs_ptr, const void *rhs_ptr) { @@ -477,7 +609,8 @@ static int compare_blocks_by_slot(const void *lhs_ptr, const void *rhs_ptr) * from a previous state after restart. * * @param client Client instance - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success (including when nothing to restore) + * @return LANTERN_CLIENT_ERR_STORAGE if block enumeration fails * * @note Thread safety: Should be called during initialization */ @@ -485,11 +618,14 @@ int restore_persisted_blocks(struct lantern_client *client) { if (!client || !client->has_state || !client->data_dir || !client->has_fork_choice) { - return 0; + return LANTERN_CLIENT_OK; } struct lantern_persisted_block_list list; persisted_block_list_init(&list); - int iterate_rc = lantern_storage_iterate_blocks(client->data_dir, collect_block_visitor, &list); + int iterate_rc = lantern_storage_iterate_blocks( + client->data_dir, + collect_block_visitor, + &list); if (iterate_rc < 0) { lantern_log_error( @@ -497,12 +633,12 @@ int restore_persisted_blocks(struct lantern_client *client) &(const struct lantern_log_metadata){.validator = client->node_id}, "failed to enumerate persisted blocks"); persisted_block_list_reset(&list); - return -1; + return LANTERN_CLIENT_ERR_STORAGE; } if (list.length == 0) { persisted_block_list_reset(&list); - return 0; + return LANTERN_CLIENT_OK; } qsort(list.items, list.length, sizeof(list.items[0]), compare_blocks_by_slot); @@ -544,7 +680,7 @@ int restore_persisted_blocks(struct lantern_client *client) } persisted_block_list_reset(&list); - return 0; + return LANTERN_CLIENT_OK; } @@ -552,6 +688,141 @@ int restore_persisted_blocks(struct lantern_client *client) * Validator State Refresh * ============================================================================ */ +/** + * @brief Update a registry record from a state pubkey fallback. + * + * Copies the pubkey bytes into the registry record and refreshes the + * cached hex string when possible. + * + * @param record Registry record to update + * @param pubkey Pubkey bytes (LANTERN_VALIDATOR_PUBKEY_SIZE bytes) + * @param meta Logging metadata + * @param index Validator index (for logging) + * + * @note Thread safety: Caller must ensure exclusive access during initialization + */ +static void update_registry_record_from_state_pubkey( + struct lantern_validator_record *record, + const uint8_t *pubkey, + const struct lantern_log_metadata *meta, + size_t index) +{ + if (!record || !pubkey || !meta) + { + return; + } + + memcpy(record->pubkey_bytes, pubkey, LANTERN_VALIDATOR_PUBKEY_SIZE); + record->has_pubkey_bytes = true; + + char hex[VALIDATOR_PUBKEY_HEX_BUFFER_LEN]; + if (lantern_bytes_to_hex(pubkey, LANTERN_VALIDATOR_PUBKEY_SIZE, hex, sizeof(hex), 1) != 0) + { + return; + } + + char *dup = lantern_string_duplicate(hex); + if (!dup) + { + lantern_log_warn( + "client", + meta, + "failed to allocate pubkey hex for validator=%zu", + index); + return; + } + + free(record->pubkey_hex); + record->pubkey_hex = dup; +} + + +/** + * @brief Populate a packed validator pubkey buffer from registry/state sources. + * + * Writes a packed array of validator pubkeys into `packed` and opportunistically + * fills missing registry pubkeys from the state. + * + * @param client Client instance + * @param registry Validator registry (must have records) + * @param state_count Validator count in state + * @param packed Output packed buffer (count * LANTERN_VALIDATOR_PUBKEY_SIZE bytes) + * @param count Number of validators to write + * @param meta Logging metadata + * @param out_registry_used Output count of pubkeys sourced from registry + * @param out_state_used Output count of pubkeys sourced from state fallback + * @param out_missing_pubkeys Output count of missing pubkeys + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if any parameter is NULL + * + * @note Thread safety: Caller must ensure exclusive access during initialization + */ +static int populate_validator_pubkeys( + struct lantern_client *client, + struct lantern_validator_registry *registry, + size_t state_count, + uint8_t *packed, + size_t count, + const struct lantern_log_metadata *meta, + size_t *out_registry_used, + size_t *out_state_used, + size_t *out_missing_pubkeys) +{ + if (!client || !registry || !registry->records || !packed || !meta + || !out_registry_used || !out_state_used || !out_missing_pubkeys) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + *out_registry_used = 0; + *out_state_used = 0; + *out_missing_pubkeys = 0; + + for (size_t i = 0; i < count; ++i) + { + struct lantern_validator_record *record = ®istry->records[i]; + const uint8_t *registry_pub = NULL; + if (record->has_pubkey_bytes && !lantern_validator_pubkey_is_zero(record->pubkey_bytes)) + { + registry_pub = record->pubkey_bytes; + } + + const uint8_t *state_pub = NULL; + if (state_count > i) + { + state_pub = lantern_state_validator_pubkey(&client->state, i); + } + if (state_pub && lantern_validator_pubkey_is_zero(state_pub)) + { + state_pub = NULL; + } + + const uint8_t *chosen = registry_pub ? registry_pub : state_pub; + size_t offset = i * LANTERN_VALIDATOR_PUBKEY_SIZE; + if (chosen) + { + memcpy(packed + offset, chosen, LANTERN_VALIDATOR_PUBKEY_SIZE); + if (!registry_pub && state_pub) + { + update_registry_record_from_state_pubkey(record, state_pub, meta, i); + ++(*out_state_used); + } + else if (registry_pub) + { + ++(*out_registry_used); + } + } + else + { + memset(packed + offset, 0, LANTERN_VALIDATOR_PUBKEY_SIZE); + ++(*out_missing_pubkeys); + } + } + + return LANTERN_CLIENT_OK; +} + + /** * Refresh state validator pubkeys from genesis registry. * @@ -562,15 +833,18 @@ int restore_persisted_blocks(struct lantern_client *client) * registry when available, falling back to state pubkeys otherwise. * * @param client Client instance - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if client is NULL or missing state + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_RUNTIME if state update fails * - * @note Thread safety: Acquires validator_lock + * @note Thread safety: Caller must ensure exclusive access during initialization */ int lantern_client_refresh_state_validators(struct lantern_client *client) { if (!client || !client->has_state) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_log_metadata meta = {.validator = client->node_id}; struct lantern_validator_registry *registry = &client->genesis.validator_registry; @@ -582,14 +856,18 @@ int lantern_client_refresh_state_validators(struct lantern_client *client) { if (state_count == 0) { - return lantern_state_set_validator_pubkeys(&client->state, NULL, 0); + if (lantern_state_set_validator_pubkeys(&client->state, NULL, 0) != 0) + { + return LANTERN_CLIENT_ERR_RUNTIME; + } + return LANTERN_CLIENT_OK; } lantern_log_info( "client", &meta, "validator registry missing; retaining existing state pubkeys count=%zu", state_count); - return 0; + return LANTERN_CLIENT_OK; } if (state_count > 0 && state_count != registry_count) @@ -603,60 +881,33 @@ int lantern_client_refresh_state_validators(struct lantern_client *client) } size_t count = registry_count; + if (count > SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE) + { + return LANTERN_CLIENT_ERR_ALLOC; + } size_t total_bytes = count * LANTERN_VALIDATOR_PUBKEY_SIZE; uint8_t *packed = malloc(total_bytes); if (!packed) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } size_t registry_used = 0; size_t state_used = 0; size_t missing_pubkeys = 0; - for (size_t i = 0; i < count; ++i) + int pack_rc = populate_validator_pubkeys( + client, + registry, + state_count, + packed, + count, + &meta, + ®istry_used, + &state_used, + &missing_pubkeys); + if (pack_rc != LANTERN_CLIENT_OK) { - struct lantern_validator_record *record = ®istry->records[i]; - const uint8_t *registry_pub = - (record && record->has_pubkey_bytes && !lantern_validator_pubkey_is_zero(record->pubkey_bytes)) - ? record->pubkey_bytes - : NULL; - const uint8_t *state_pub = (state_count > i) ? lantern_state_validator_pubkey(&client->state, i) : NULL; - if (state_pub && lantern_validator_pubkey_is_zero(state_pub)) - { - state_pub = NULL; - } - - const uint8_t *chosen = registry_pub ? registry_pub : state_pub; - if (chosen) - { - memcpy(packed + (i * LANTERN_VALIDATOR_PUBKEY_SIZE), chosen, LANTERN_VALIDATOR_PUBKEY_SIZE); - if (!registry_pub && state_pub && record) - { - memcpy(record->pubkey_bytes, state_pub, LANTERN_VALIDATOR_PUBKEY_SIZE); - record->has_pubkey_bytes = true; - char hex[(LANTERN_VALIDATOR_PUBKEY_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - state_pub, - LANTERN_VALIDATOR_PUBKEY_SIZE, - hex, - sizeof(hex), - 1) - == 0) - { - free(record->pubkey_hex); - record->pubkey_hex = lantern_string_duplicate(hex); - } - ++state_used; - } - else if (registry_pub) - { - ++registry_used; - } - } - else - { - memset(packed + (i * LANTERN_VALIDATOR_PUBKEY_SIZE), 0, LANTERN_VALIDATOR_PUBKEY_SIZE); - ++missing_pubkeys; - } + free(packed); + return pack_rc; } int rc = lantern_state_set_validator_pubkeys(&client->state, packed, count); free(packed); @@ -666,20 +917,21 @@ int lantern_client_refresh_state_validators(struct lantern_client *client) "client", &meta, "failed to copy validator pubkeys into parent state"); - return -1; + return LANTERN_CLIENT_ERR_RUNTIME; } size_t enabled = lantern_client_enabled_validator_count(client); lantern_log_info( "client", &meta, - "refreshed validator pubkeys count=%zu registry=%zu state_fallback=%zu missing=%zu local_validators=%zu enabled=%zu", + "refreshed validator pubkeys count=%zu registry=%zu state_fallback=%zu missing=%zu " + "local_validators=%zu enabled=%zu", count, registry_used, state_used, missing_pubkeys, client->local_validator_count, enabled); - return 0; + return LANTERN_CLIENT_OK; } @@ -688,7 +940,12 @@ int lantern_client_refresh_state_validators(struct lantern_client *client) * ============================================================================ */ /** - * Remove a pending block by root (internal, no locking). + * @brief Remove a pending block by root (internal, no locking). + * + * @param client Client instance + * @param root Block root to remove + * + * @note Thread safety: Caller must hold pending_lock */ static void lantern_client_pending_remove_by_root_locked( struct lantern_client *client, @@ -775,9 +1032,6 @@ void lantern_client_enqueue_pending_block( LanternRoot block_root_local = *block_root; LanternRoot parent_root_local = *parent_root; - char schedule_peer[128]; - schedule_peer[0] = '\0'; - bool schedule_parent = false; bool locked = lantern_client_lock_pending(client); if (!locked) @@ -806,7 +1060,7 @@ void lantern_client_enqueue_pending_block( if (list->length >= LANTERN_PENDING_BLOCK_LIMIT && list->length > 0) { - char dropped_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char dropped_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(&list->items[0].root, dropped_hex, sizeof(dropped_hex)); lantern_log_warn( "state", @@ -829,8 +1083,8 @@ void lantern_client_enqueue_pending_block( return; } - char block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char block_hex[ROOT_HEX_BUFFER_LEN]; + char parent_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(&block_root_local, block_hex, sizeof(block_hex)); format_root_hex(&parent_root_local, parent_hex, sizeof(parent_hex)); @@ -843,8 +1097,6 @@ void lantern_client_enqueue_pending_block( req/resp should only be used for sync recovery, not for normal block propagation. The parent_requested flag is no longer used for immediate requests. */ entry->parent_requested = false; - (void)schedule_peer; - (void)schedule_parent; lantern_client_unlock_pending(client, locked); @@ -872,7 +1124,9 @@ void lantern_client_enqueue_pending_block( * * @note Thread safety: Acquires pending_lock and state_lock */ -void lantern_client_process_pending_children(struct lantern_client *client, const LanternRoot *parent_root) +void lantern_client_process_pending_children( + struct lantern_client *client, + const LanternRoot *parent_root) { if (!client || !parent_root) { @@ -882,7 +1136,7 @@ void lantern_client_process_pending_children(struct lantern_client *client, cons { LanternSignedBlock replay; LanternRoot child_root; - char peer_copy[128]; + char peer_copy[PEER_TEXT_BUFFER_LEN]; bool have_replay = false; bool locked = lantern_client_lock_pending(client); diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index 5c421d7..880ccc0 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -19,6 +19,11 @@ #include "client_internal.h" +#include +#include +#include +#include + #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" @@ -27,9 +32,15 @@ #include "lantern/support/log.h" #include "lantern/support/strings.h" -#include -#include -#include + +/* ============================================================================ + * Constants + * ============================================================================ */ + +enum +{ + ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, +}; /* ============================================================================ @@ -68,7 +79,7 @@ extern bool lantern_client_verify_vote_signature( * * @note Thread safety: Thread-safe, reads immutable validator registry */ -static bool lantern_client_verify_block_signatures( +static bool signed_block_signatures_are_valid( const struct lantern_client *client, const LanternSignedBlock *block, const struct lantern_log_metadata *meta) @@ -78,11 +89,31 @@ static bool lantern_client_verify_block_signatures( return false; } const LanternAttestations *attestations = &block->message.block.body.attestations; + if (attestations->length > SIZE_MAX - 1u) + { + lantern_log_warn( + "state", + meta, + "signed block slot=%" PRIu64 " attestation count overflow length=%zu", + block->message.block.slot, + attestations->length); + return false; + } size_t expected_signatures = attestations->length + 1u; if (!client->genesis.validator_registry.records) { return true; } + if (attestations->length > 0 && !attestations->data) + { + lantern_log_warn( + "state", + meta, + "signed block slot=%" PRIu64 " attestations missing data length=%zu", + block->message.block.slot, + attestations->length); + return false; + } if (block->signatures.length == 0) { lantern_log_warn( @@ -105,8 +136,7 @@ static bool lantern_client_verify_block_signatures( } for (size_t i = 0; i < attestations->length; ++i) { - LanternSignedVote signed_vote; - memset(&signed_vote, 0, sizeof(signed_vote)); + LanternSignedVote signed_vote = {0}; signed_vote.data = attestations->data[i]; signed_vote.signature = block->signatures.data[i]; if (!lantern_client_verify_vote_signature( @@ -119,8 +149,7 @@ static bool lantern_client_verify_block_signatures( return false; } } - LanternSignedVote proposer_signed; - memset(&proposer_signed, 0, sizeof(proposer_signed)); + LanternSignedVote proposer_signed = {0}; proposer_signed.data = block->message.proposer_attestation; proposer_signed.signature = block->signatures.data[attestations->length]; return lantern_client_verify_vote_signature( @@ -133,257 +162,301 @@ static bool lantern_client_verify_block_signatures( /* ============================================================================ - * Block Import + * Block Import Helpers * ============================================================================ */ /** - * Import a block into the client state and fork choice. + * @brief Computes the block root if not provided. * - * @spec subspecs/containers/state/state.py - State.state_transition() - * @spec subspecs/forkchoice/store.py - Store.on_block() + * @param block Signed block to hash + * @param provided Optional precomputed root + * @param out_root Output root (filled on success) + * @param meta Logging metadata + * @return true on success, false on failure * - * Performs the complete block import pipeline: - * 1. Validates block slot against local state - * 2. Checks if block root is already known - * 3. Handles parent tracking: - * - Unknown parent: queue as pending - * - Parent on competing fork: add to fork choice without state transition - * - Parent matches head: proceed with full import - * 4. Verifies all block signatures - * 5. Validates attestation constraints - * 6. Applies state transition - * 7. Updates fork choice - * 8. Persists state and votes - * 9. Processes pending children - * - * Per leanSpec: Blocks on competing forks are added to fork choice so - * attestations can reference them and fork choice can determine which - * chain has more weight. + * @note Thread safety: This function is thread-safe + */ +static bool get_block_root_local( + const LanternSignedBlock *block, + const LanternRoot *provided, + LanternRoot *out_root, + const struct lantern_log_metadata *meta) +{ + if (!block || !out_root) + { + return false; + } + if (provided) + { + *out_root = *provided; + return true; + } + if (lantern_hash_tree_root_block(&block->message.block, out_root) != 0) + { + lantern_log_warn( + "state", + meta, + "failed to hash block at slot=%" PRIu64, + block->message.block.slot); + return false; + } + return true; +} + + +/** + * @brief Returns true if the block should be processed. * - * @param client Client instance - * @param block Signed block to import - * @param block_root Precomputed block root (may be NULL) + * @param slot Block slot + * @param local_slot Client state slot + * @param root_known Whether the block root is known + * @param known_slot Slot of the known root (if root_known) * @param meta Logging metadata - * @return true if block was imported successfully + * @return true if block should be processed, false otherwise * - * @note Thread safety: Acquires state_lock and pending_lock + * @note Thread safety: This function is thread-safe */ -bool lantern_client_import_block( +static bool should_process_block( + uint64_t slot, + uint64_t local_slot, + bool root_known, + uint64_t known_slot, + const struct lantern_log_metadata *meta) +{ + if (root_known && slot <= known_slot) + { + lantern_log_trace("state", meta, "skipping known block slot=%" PRIu64, slot); + return false; + } + if (slot < local_slot && !root_known) + { + lantern_log_debug( + "state", + meta, + "ignoring block slot=%" PRIu64 " local_slot=%" PRIu64, + slot, + local_slot); + return false; + } + return true; +} + + +/** + * Handle parent tracking and competing forks. + * + * @param client Client instance + * @param block Block being imported + * @param block_root Root of the block + * @param meta Logging metadata + * @param state_locked In/out state lock flag (may be cleared if unlocked here) + * @return true if import should continue, false if deferred or on error + * + * @note Thread safety: Caller must hold state_lock + */ +static bool handle_block_parent_locked( struct lantern_client *client, const LanternSignedBlock *block, const LanternRoot *block_root, - const struct lantern_log_metadata *meta) + const struct lantern_log_metadata *meta, + bool *state_locked) { - if (!client || !block || !client->has_state) + if (!client || !block || !block_root || !state_locked || !*state_locked) { return false; } - bool state_locked = lantern_client_lock_state(client); - uint64_t local_slot = client->state.slot; + LanternRoot parent_root = block->message.block.parent_root; + if (lantern_root_is_zero(&parent_root)) + { + return true; + } - LanternRoot hashed_block_root; - const LanternRoot *effective_block_root = block_root; - if (!effective_block_root) + bool parent_known = lantern_client_block_known_locked(client, &parent_root, NULL); + if (!parent_known) { - if (lantern_hash_tree_root_block(&block->message.block, &hashed_block_root) != 0) - { - lantern_client_unlock_state(client, state_locked); - lantern_log_warn( - "state", - meta, - "failed to hash block at slot=%" PRIu64, - block->message.block.slot); - return false; - } - effective_block_root = &hashed_block_root; + const char *peer_text = meta && meta->peer ? meta->peer : NULL; + lantern_client_unlock_state(client, *state_locked); + *state_locked = false; + lantern_client_enqueue_pending_block(client, block, block_root, &parent_root, peer_text); + return false; } - LanternRoot block_root_local = *effective_block_root; + bool have_head_root = false; + bool parent_matches_head = false; + LanternRoot latest_header_root = {0}; - if (block->message.block.slot < local_slot) + /* Ensure state_root is filled in latest_block_header before computing its hash. + This is required because state_root is zeroed when a block is applied and only + filled in lazily by lantern_state_process_slot. Without this, the computed + header root may differ from what other clients expect. */ + if (lantern_state_process_slot(&client->state) != 0) + { + lantern_log_warn( + "state", + meta, + "failed to compute cached header state root at slot=%" PRIu64, + client->state.slot); + } + else if (lantern_hash_tree_root_block_header( + &client->state.latest_block_header, + &latest_header_root) == 0) { - lantern_client_unlock_state(client, state_locked); + have_head_root = true; + parent_matches_head = + memcmp(latest_header_root.bytes, parent_root.bytes, LANTERN_ROOT_SIZE) == 0; + } + + if (parent_matches_head) + { + return true; + } + + const char *peer_text = meta && meta->peer ? meta->peer : NULL; + + if (have_head_root) + { + char parent_hex[ROOT_HEX_BUFFER_LEN]; + char head_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&parent_root, parent_hex, sizeof(parent_hex)); + format_root_hex(&latest_header_root, head_hex, sizeof(head_hex)); lantern_log_debug( "state", meta, - "ignoring block slot=%" PRIu64 " local_slot=%" PRIu64, + "block on competing fork slot=%" PRIu64 " parent=%s current_head=%s", block->message.block.slot, - local_slot); - return false; + parent_hex[0] ? parent_hex : "0x0", + head_hex[0] ? head_hex : "0x0"); } - uint64_t known_slot = 0; - bool root_known = false; - if (effective_block_root) + /* + * Parent is known in fork choice but doesn't match our current head. + * This indicates a competing fork. Per leanSpec, we should still add + * the block to fork choice so attestations can reference it and fork + * choice can properly determine which chain has more weight. + * + * We add the block to fork choice (without post-state checkpoints since + * we can't compute state transition), then queue it for later processing. + * If fork choice later determines this is the better chain, pending block + * processing will handle the reorg. + */ + if (client->has_fork_choice) { - if (state_locked) + LanternSignedVote proposer_signed = {0}; + proposer_signed.data = block->message.proposer_attestation; + + size_t proposer_index = block->message.block.body.attestations.length; + if (block->signatures.data && block->signatures.length > proposer_index) { - root_known = lantern_client_block_known_locked(client, effective_block_root, &known_slot); + proposer_signed.signature = block->signatures.data[proposer_index]; } - else if (client->has_fork_choice) + + if (lantern_fork_choice_add_block( + &client->fork_choice, + &block->message.block, + &proposer_signed, + NULL, /* No post-justified - we can't compute state transition */ + NULL, /* No post-finalized */ + block_root) == 0) { - root_known = (lantern_fork_choice_block_info(&client->fork_choice, effective_block_root, &known_slot, NULL, NULL) == 0); + char block_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(block_root, block_hex, sizeof(block_hex)); + lantern_log_info( + "forkchoice", + meta, + "added competing fork block to fork choice slot=%" PRIu64 " root=%s", + block->message.block.slot, + block_hex[0] ? block_hex : "0x0"); } } - if (root_known && block->message.block.slot <= known_slot) + lantern_client_unlock_state(client, *state_locked); + *state_locked = false; + lantern_client_enqueue_pending_block(client, block, block_root, &parent_root, peer_text); + return false; +} + + +/** + * @brief Validates attestation constraints for the block. + * + * @param client Client instance + * @param block Signed block + * @param meta Logging metadata + * @return true if constraints pass, false otherwise + * + * @note Thread safety: Caller must hold state_lock + */ +static bool validate_block_vote_constraints_locked( + struct lantern_client *client, + const LanternSignedBlock *block, + const struct lantern_log_metadata *meta) +{ + if (!client || !block) { - lantern_client_unlock_state(client, state_locked); - lantern_log_trace( - "state", - meta, - "skipping known block slot=%" PRIu64, - block->message.block.slot); return false; } + if (!client->has_fork_choice) + { + return true; + } - if (block->message.block.slot < local_slot && !root_known) + const LanternAttestations *attestations = &block->message.block.body.attestations; + if (attestations->length > 0 && !attestations->data) { - lantern_client_unlock_state(client, state_locked); - lantern_log_debug( + lantern_log_warn( "state", meta, - "ignoring block slot=%" PRIu64 " local_slot=%" PRIu64, + "block slot=%" PRIu64 " attestations missing data length=%zu", block->message.block.slot, - local_slot); + attestations->length); return false; } - LanternRoot parent_root_local = block->message.block.parent_root; - if (!lantern_root_is_zero(&parent_root_local)) + for (size_t i = 0; i < attestations->length; ++i) { - bool parent_known = false; - bool parent_matches_head = false; - bool have_head_root = false; - LanternRoot latest_header_root; - memset(&latest_header_root, 0, sizeof(latest_header_root)); - if (state_locked) - { - parent_known = lantern_client_block_known_locked(client, &parent_root_local, NULL); - /* Ensure state_root is filled in latest_block_header before computing its hash. - This is required because state_root is zeroed when a block is applied and only - filled in lazily by lantern_state_process_slot. Without this, the computed - header root may differ from what other clients expect. */ - (void)lantern_state_process_slot(&client->state); - if (lantern_hash_tree_root_block_header(&client->state.latest_block_header, &latest_header_root) == 0) - { - have_head_root = true; - parent_matches_head = - memcmp(latest_header_root.bytes, parent_root_local.bytes, LANTERN_ROOT_SIZE) == 0; - } - } - else if (client->has_fork_choice) - { - parent_known = (lantern_fork_choice_block_info(&client->fork_choice, &parent_root_local, NULL, NULL, NULL) == 0); - } - if (!parent_known) - { - /* Parent unknown - queue block as pending and request parent */ - const char *peer_text = meta && meta->peer ? meta->peer : NULL; - lantern_client_unlock_state(client, state_locked); - lantern_client_enqueue_pending_block(client, block, &block_root_local, &parent_root_local, peer_text); - return false; - } - if (!parent_matches_head) + if (!lantern_client_validate_vote_constraints( + client, + &attestations->data[i], + "state", + meta, + "block attestation", + NULL)) { - /* - * Parent is known in fork choice but doesn't match our current head. - * This indicates a competing fork. Per leanSpec, we should still add - * the block to fork choice so attestations can reference it and fork - * choice can properly determine which chain has more weight. - * - * We add the block to fork choice (without post-state checkpoints since - * we can't compute state transition), then queue it for later processing. - * If fork choice later determines this is the better chain, pending block - * processing will handle the reorg. - */ - const char *peer_text = meta && meta->peer ? meta->peer : NULL; - char parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (have_head_root) - { - format_root_hex(&parent_root_local, parent_hex, sizeof(parent_hex)); - format_root_hex(&latest_header_root, head_hex, sizeof(head_hex)); - lantern_log_debug( - "state", - meta, - "block on competing fork slot=%" PRIu64 " parent=%s current_head=%s", - block->message.block.slot, - parent_hex[0] ? parent_hex : "0x0", - head_hex[0] ? head_hex : "0x0"); - } - - /* Add block to fork choice even without state transition so fork choice - * can track competing chains and attestations can reference this block */ - if (client->has_fork_choice) - { - LanternSignedVote proposer_signed; - memset(&proposer_signed, 0, sizeof(proposer_signed)); - proposer_signed.data = block->message.proposer_attestation; - size_t proposer_index = block->message.block.body.attestations.length; - if (block->signatures.length > proposer_index && block->signatures.data) - { - proposer_signed.signature = block->signatures.data[proposer_index]; - } - if (lantern_fork_choice_add_block( - &client->fork_choice, - &block->message.block, - &proposer_signed, - NULL, /* No post-justified - we can't compute state transition */ - NULL, /* No post-finalized */ - &block_root_local) == 0) - { - char block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(&block_root_local, block_hex, sizeof(block_hex)); - lantern_log_info( - "forkchoice", - meta, - "added competing fork block to fork choice slot=%" PRIu64 " root=%s", - block->message.block.slot, - block_hex[0] ? block_hex : "0x0"); - } - } - - lantern_client_unlock_state(client, state_locked); - lantern_client_enqueue_pending_block(client, block, &block_root_local, &parent_root_local, peer_text); return false; } } - if (!lantern_client_verify_block_signatures(client, block, meta)) - { - lantern_client_unlock_state(client, state_locked); - return false; - } + /* Skip proposer attestation validation here - the proposer's head checkpoint + * references the block being imported, which isn't in fork choice yet. + * The proposer attestation will be validated during state transition. */ + return true; +} - if (client->has_fork_choice) + +/** + * @brief Applies the state transition for a block. + * + * @param client Client instance + * @param block Signed block to import + * @param meta Logging metadata + * @return true on success, false on failure + * + * @note Thread safety: Caller must hold state_lock + */ +static bool apply_state_transition_locked( + struct lantern_client *client, + const LanternSignedBlock *block, + const struct lantern_log_metadata *meta) +{ + if (!client || !block) { - const LanternAttestations *attestations = &block->message.block.body.attestations; - for (size_t i = 0; i < attestations->length; ++i) - { - if (!lantern_client_validate_vote_constraints( - client, - &attestations->data[i], - "state", - meta, - "block attestation", - NULL)) - { - lantern_client_unlock_state(client, state_locked); - return false; - } - } - /* Skip proposer attestation validation here - the proposer's head checkpoint - * references the block being imported, which isn't in fork choice yet. - * The proposer attestation will be validated during state transition. */ + return false; } LanternSignedBlock import_block = *block; - if (lantern_state_transition(&client->state, &import_block) != 0) { - lantern_client_unlock_state(client, state_locked); lantern_log_warn( "state", meta, @@ -392,61 +465,144 @@ bool lantern_client_import_block( return false; } - if (client->has_fork_choice) + return true; +} + + +/** + * @brief Advances fork choice time after a successful import. + * + * @param client Client instance + * @param block Imported block (for logging) + * @param meta Logging metadata + * + * @note Thread safety: Caller must hold state_lock + */ +static void advance_fork_choice_time_locked( + struct lantern_client *client, + const LanternSignedBlock *block, + const struct lantern_log_metadata *meta) +{ + if (!client || !block || !client->has_fork_choice) { - uint64_t now_seconds = validator_wall_time_now_seconds(); - if (lantern_fork_choice_advance_time(&client->fork_choice, now_seconds, false) != 0) - { - lantern_log_debug( - "forkchoice", - meta, - "advancing fork choice time failed after slot=%" PRIu64, - block->message.block.slot); - } + return; } - uint64_t head_slot = client->state.slot; - LanternRoot head_root; - memset(&head_root, 0, sizeof(head_root)); - if (client->has_fork_choice) + uint64_t now_seconds = validator_wall_time_now_seconds(); + if (lantern_fork_choice_advance_time(&client->fork_choice, now_seconds, false) != 0) { - if (lantern_fork_choice_current_head(&client->fork_choice, &head_root) == 0) - { - uint64_t fork_slot = 0; - if (lantern_fork_choice_block_info(&client->fork_choice, &head_root, &fork_slot, NULL, NULL) == 0) - { - head_slot = fork_slot; - } - } + lantern_log_debug( + "forkchoice", + meta, + "advancing fork choice time failed after slot=%" PRIu64, + block->message.block.slot); } +} - if (client->data_dir) + +/** + * @brief Computes head slot/root for logging. + * + * @param client Client instance + * @param out_head_root Output head root + * @param out_head_slot Output head slot + * + * @note Thread safety: Caller must hold state_lock + */ +static void get_head_info_locked( + struct lantern_client *client, + LanternRoot *out_head_root, + uint64_t *out_head_slot) +{ + if (!client || !out_head_root || !out_head_slot) { - if (lantern_storage_save_state(client->data_dir, &client->state) != 0) - { - lantern_log_warn( - "storage", - meta, - "failed to persist state after slot=%" PRIu64, - client->state.slot); - } - if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) - { - lantern_log_warn( - "storage", - meta, - "failed to persist votes after slot=%" PRIu64, - client->state.slot); - } + return; } - lantern_client_unlock_state(client, state_locked); + *out_head_slot = client->state.slot; + *out_head_root = (LanternRoot){0}; + if (!client->has_fork_choice) + { + return; + } + + if (lantern_fork_choice_current_head(&client->fork_choice, out_head_root) != 0) + { + return; + } + + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + out_head_root, + &fork_slot, + NULL, + NULL) == 0) + { + *out_head_slot = fork_slot; + } +} + + +/** + * @brief Persists client state/votes if storage is enabled. + * + * @param client Client instance + * @param meta Logging metadata + * + * @note Thread safety: Caller must hold state_lock + */ +static void persist_state_locked( + const struct lantern_client *client, + const struct lantern_log_metadata *meta) +{ + if (!client || !client->data_dir) + { + return; + } - lantern_client_pending_remove_by_root(client, &block_root_local); - lantern_client_process_pending_children(client, &block_root_local); + if (lantern_storage_save_state(client->data_dir, &client->state) != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist state after slot=%" PRIu64, + client->state.slot); + } + if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist votes after slot=%" PRIu64, + client->state.slot); + } +} + + +/** + * @brief Logs a successful block import. + * + * @param block Imported block + * @param head_root New head root + * @param head_slot New head slot + * @param meta Logging metadata + * + * @note Thread safety: This function is thread-safe + */ +static void log_imported_block( + const LanternSignedBlock *block, + const LanternRoot *head_root, + uint64_t head_slot, + const struct lantern_log_metadata *meta) +{ + if (!block || !head_root) + { + return; + } - char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(&head_root, head_hex, sizeof(head_hex)); + char head_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(head_root, head_hex, sizeof(head_hex)); lantern_log_info( "state", meta, @@ -454,8 +610,121 @@ bool lantern_client_import_block( block->message.block.slot, head_slot, head_hex[0] ? head_hex : "0x0"); +} - return true; + +/* ============================================================================ + * Block Import + * ============================================================================ */ + +/** + * Import a block into the client state and fork choice. + * + * @spec subspecs/containers/state/state.py - State.state_transition() + * @spec subspecs/forkchoice/store.py - Store.on_block() + * + * Performs the complete block import pipeline: + * 1. Validates block slot against local state + * 2. Checks if block root is already known + * 3. Handles parent tracking: + * - Unknown parent: queue as pending + * - Parent on competing fork: add to fork choice without state transition + * - Parent matches head: proceed with full import + * 4. Verifies all block signatures + * 5. Validates attestation constraints + * 6. Applies state transition + * 7. Updates fork choice + * 8. Persists state and votes + * 9. Processes pending children + * + * Per leanSpec: Blocks on competing forks are added to fork choice so + * attestations can reference them and fork choice can determine which + * chain has more weight. + * + * @param client Client instance + * @param block Signed block to import + * @param block_root Precomputed block root (may be NULL) + * @param meta Logging metadata + * @return true if block was imported successfully + * + * @note Thread safety: Acquires state_lock and pending_lock + */ +bool lantern_client_import_block( + struct lantern_client *client, + const LanternSignedBlock *block, + const LanternRoot *block_root, + const struct lantern_log_metadata *meta) +{ + if (!client || !block || !client->has_state) + { + return false; + } + + bool imported = false; + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) + { + lantern_log_warn( + "state", + meta, + "failed to acquire state lock for block import slot=%" PRIu64, + block->message.block.slot); + return false; + } + + uint64_t local_slot = client->state.slot; + LanternRoot block_root_local = {0}; + LanternRoot head_root = {0}; + uint64_t head_slot = 0; + + if (!get_block_root_local(block, block_root, &block_root_local, meta)) + { + goto cleanup; + } + + uint64_t known_slot = 0; + bool root_known = lantern_client_block_known_locked(client, &block_root_local, &known_slot); + if (!should_process_block(block->message.block.slot, local_slot, root_known, known_slot, meta)) + { + goto cleanup; + } + + if (!handle_block_parent_locked(client, block, &block_root_local, meta, &state_locked)) + { + goto cleanup; + } + + if (!signed_block_signatures_are_valid(client, block, meta)) + { + goto cleanup; + } + + if (!validate_block_vote_constraints_locked(client, block, meta)) + { + goto cleanup; + } + + if (!apply_state_transition_locked(client, block, meta)) + { + goto cleanup; + } + + advance_fork_choice_time_locked(client, block, meta); + get_head_info_locked(client, &head_root, &head_slot); + persist_state_locked(client, meta); + imported = true; + +cleanup: + lantern_client_unlock_state(client, state_locked); + + if (imported) + { + lantern_client_pending_remove_by_root(client, &block_root_local); + lantern_client_process_pending_children(client, &block_root_local); + log_imported_block(block, &head_root, head_slot, meta); + } + + return imported; } @@ -503,7 +772,7 @@ void lantern_client_record_block( selected_root = &computed_root; } - char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char root_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(selected_root, root_hex, sizeof(root_hex)); struct lantern_log_metadata meta = { diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index 0dd0d8c..eacd8c7 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -18,6 +18,9 @@ #include "client_internal.h" +#include +#include + #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" @@ -26,9 +29,10 @@ #include "lantern/support/log.h" #include "lantern/support/strings.h" -#include -#include -#include +enum +{ + VOTE_ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, +}; /* ============================================================================ @@ -39,6 +43,410 @@ extern const struct lantern_validator_record *lantern_client_get_validator_recor const struct lantern_client *client, uint64_t validator_id); +bool lantern_client_verify_vote_signature( + const struct lantern_client *client, + const LanternSignedVote *vote, + const LanternSignature *signature, + const struct lantern_log_metadata *meta, + const char *context); + + +/* ============================================================================ + * Internal Helpers + * ============================================================================ */ + +/** + * @brief Validates a vote checkpoint against fork choice. + * + * @param client Client instance + * @param vote Vote being validated + * @param checkpoint Checkpoint to validate + * @param name Checkpoint name for logging + * @param log_facility Log facility name + * @param meta Logging metadata + * @param label Vote label for logging + * @param out_rejection Output rejection info (may be NULL) + * @return true if checkpoint is valid + * + * @note Thread safety: Caller must hold state_lock + */ +static bool validate_vote_checkpoint( + struct lantern_client *client, + const LanternVote *vote, + const LanternCheckpoint *checkpoint, + const char *name, + const char *log_facility, + const struct lantern_log_metadata *meta, + const char *label, + struct lantern_vote_rejection_info *out_rejection) +{ + char root_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + format_root_hex(&checkpoint->root, root_hex, sizeof(root_hex)); + + if (lantern_root_is_zero(&checkpoint->root)) + { + lantern_log_debug( + log_facility, + meta, + "dropping %s validator=%" PRIu64 " slot=%" PRIu64 " %s root=%s " + "(zero root)", + label, + vote->validator_id, + vote->slot, + name, + root_hex[0] ? root_hex : "0x0"); + if (out_rejection) + { + lantern_vote_rejection_set( + out_rejection, + "%s checkpoint root zero slot=%" PRIu64 " root=%s", + name, + checkpoint->slot, + root_hex[0] ? root_hex : "0x0"); + } + return false; + } + + uint64_t block_slot = 0; + if (!lantern_client_block_known_locked(client, &checkpoint->root, &block_slot)) + { + lantern_log_debug( + log_facility, + meta, + "dropping %s validator=%" PRIu64 " slot=%" PRIu64 " unknown %s root=%s", + label, + vote->validator_id, + vote->slot, + name, + root_hex[0] ? root_hex : "0x0"); + if (out_rejection) + { + lantern_vote_rejection_set( + out_rejection, + "unknown %s root=%s slot=%" PRIu64, + name, + root_hex[0] ? root_hex : "0x0", + checkpoint->slot); + } + return false; + } + + if (block_slot != checkpoint->slot) + { + lantern_log_debug( + log_facility, + meta, + "dropping %s validator=%" PRIu64 " slot=%" PRIu64 " %s slot mismatch " + "vote=%" PRIu64 " block=%" PRIu64 " root=%s", + label, + vote->validator_id, + vote->slot, + name, + checkpoint->slot, + block_slot, + root_hex[0] ? root_hex : "0x0"); + if (out_rejection) + { + lantern_vote_rejection_set( + out_rejection, + "%s checkpoint slot mismatch vote=%" PRIu64 " block=%" PRIu64, + name, + checkpoint->slot, + block_slot); + } + return false; + } + + return true; +} + + +/** + * @brief Validates vote cache availability and basic vote consistency. + * + * @param client Client instance + * @param vote Vote to validate + * @param meta Logging metadata + * @param rejection Rejection info to populate on failure + * @return true if vote cache state is valid + * + * @note Thread safety: Caller must hold state_lock + */ +static bool validate_vote_cache_state( + const struct lantern_client *client, + const LanternVote *vote, + const struct lantern_log_metadata *meta, + struct lantern_vote_rejection_info *rejection) +{ + if (!client || !vote || !rejection) + { + return false; + } + + uint64_t validator_count = client->state.config.num_validators; + bool cache_available = (validator_count != 0) && client->state.validator_votes + && (client->state.validator_votes_len != 0); + if (!cache_available) + { + lantern_log_debug( + "gossip", + meta, + "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " " + "(state vote cache unavailable)", + vote->validator_id, + vote->slot); + lantern_vote_rejection_set(rejection, "state vote cache unavailable"); + return false; + } + + bool validator_in_range = (vote->validator_id < validator_count) + && (vote->validator_id < (uint64_t)client->state.validator_votes_len); + if (!validator_in_range) + { + lantern_log_debug( + "gossip", + meta, + "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " " + "(validator out of range)", + vote->validator_id, + vote->slot); + lantern_vote_rejection_set( + rejection, + "validator out of range id=%" PRIu64, + vote->validator_id); + return false; + } + + if (vote->target.slot < vote->source.slot) + { + lantern_log_debug( + "gossip", + meta, + "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " " + "(target slot < source)", + vote->validator_id, + vote->slot); + lantern_vote_rejection_set( + rejection, + "target slot %" PRIu64 " < source slot %" PRIu64, + vote->target.slot, + vote->source.slot); + return false; + } + + return true; +} + + +/** + * @brief Cache a signed validator vote in state. + * + * @param client Client instance + * @param vote Signed vote to cache + * @param meta Logging metadata + * @param rejection Rejection info to populate on failure + * @return true if vote is cached successfully + * + * @note Thread safety: Caller must hold state_lock + */ +static bool cache_state_vote_locked( + struct lantern_client *client, + const LanternSignedVote *vote, + const struct lantern_log_metadata *meta, + struct lantern_vote_rejection_info *rejection) +{ + if (!client || !vote || !meta || !rejection) + { + return false; + } + + int result = lantern_state_set_signed_validator_vote( + &client->state, + (size_t)vote->data.validator_id, + vote); + if (result != 0) + { + lantern_log_debug( + "state", + meta, + "failed to cache gossip vote validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + lantern_vote_rejection_set( + rejection, + "failed to cache vote validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + return false; + } + + return true; +} + + +/** + * @brief Apply a vote to fork choice and advance time. + * + * @param client Client instance + * @param vote Signed vote to apply + * @param meta Logging metadata + * + * @note Thread safety: Caller must hold state_lock + */ +static void apply_vote_to_fork_choice_locked( + struct lantern_client *client, + const LanternSignedVote *vote, + const struct lantern_log_metadata *meta) +{ + if (!client || !vote || !meta || !client->has_fork_choice) + { + return; + } + + int result = lantern_fork_choice_add_vote(&client->fork_choice, vote, false); + if (result != 0) + { + lantern_log_debug( + "forkchoice", + meta, + "failed to track gossip vote validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + return; + } + + if (client->debug_disable_fork_choice_time) + { + return; + } + + uint64_t now_seconds = 0; + if (!lantern_client_vote_time_seconds(client, vote->data.slot, &now_seconds)) + { + now_seconds = validator_wall_time_now_seconds(); + } + + result = lantern_fork_choice_advance_time(&client->fork_choice, now_seconds, false); + if (result != 0) + { + lantern_log_debug( + "forkchoice", + meta, + "advancing fork choice time failed after validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + } +} + + +/** + * @brief Persist votes to storage if configured. + * + * @param client Client instance + * @param vote Vote used for log context + * @param meta Logging metadata + * + * @note Thread safety: Caller must hold state_lock + */ +static void persist_votes_if_configured_locked( + const struct lantern_client *client, + const LanternSignedVote *vote, + const struct lantern_log_metadata *meta) +{ + if (!client || !vote || !meta || !client->data_dir) + { + return; + } + + if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist votes after validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + } +} + + +/** + * @brief Validate and apply a received vote while holding state_lock. + * + * @param client Client instance + * @param vote Vote to validate and apply (may be modified in place) + * @param meta Logging metadata + * @param rejection Rejection info to populate on failure + * @return true if vote was processed successfully + * + * @note Thread safety: Caller must hold state_lock + */ +static bool process_vote_locked( + struct lantern_client *client, + LanternSignedVote *vote, + const struct lantern_log_metadata *meta, + struct lantern_vote_rejection_info *rejection) +{ + if (!client || !vote || !meta || !rejection) + { + return false; + } + + if (!client->has_fork_choice) + { + lantern_log_debug( + "gossip", + meta, + "deferring vote validator=%" PRIu64 " slot=%" PRIu64 " (fork choice unavailable)", + vote->data.validator_id, + vote->data.slot); + lantern_vote_rejection_set(rejection, "fork choice unavailable"); + return false; + } + + if (!lantern_client_validate_vote_constraints( + client, + &vote->data, + "gossip", + meta, + "gossip", + rejection)) + { + return false; + } + + if (!lantern_client_verify_vote_signature( + client, + vote, + &vote->signature, + meta, + "gossip")) + { + lantern_log_debug( + "gossip", + meta, + "rejected vote validator=%" PRIu64 " slot=%" PRIu64 " (invalid XMSS signature)", + vote->data.validator_id, + vote->data.slot); + lantern_vote_rejection_set(rejection, "invalid XMSS signature"); + return false; + } + + if (!validate_vote_cache_state(client, &vote->data, meta, rejection)) + { + return false; + } + + if (!cache_state_vote_locked(client, vote, meta, rejection)) + { + return false; + } + + apply_vote_to_fork_choice_locked(client, vote, meta); + persist_votes_if_configured_locked(client, vote, meta); + return true; +} + /* ============================================================================ * Vote Signature Verification @@ -77,8 +485,12 @@ bool lantern_client_verify_vote_signature( return false; } const uint8_t *pubkey_bytes = NULL; - bool state_has_registry = client && client->has_state; - size_t state_validator_count = state_has_registry ? lantern_state_validator_count(&client->state) : 0; + bool state_has_registry = client->has_state; + size_t state_validator_count = 0; + if (state_has_registry) + { + state_validator_count = lantern_state_validator_count(&client->state); + } if (state_has_registry && state_validator_count > 0) { if (vote->data.validator_id >= state_validator_count) @@ -91,7 +503,9 @@ bool lantern_client_verify_vote_signature( state_validator_count); return false; } - pubkey_bytes = lantern_state_validator_pubkey(&client->state, (size_t)vote->data.validator_id); + pubkey_bytes = lantern_state_validator_pubkey( + &client->state, + (size_t)vote->data.validator_id); if (lantern_validator_pubkey_is_zero(pubkey_bytes)) { pubkey_bytes = NULL; @@ -116,17 +530,21 @@ bool lantern_client_verify_vote_signature( LanternRoot vote_root; if (lantern_hash_tree_root_vote(&vote->data, &vote_root) != 0) { - lantern_log_warn("state", meta, "failed to hash attestation for validator=%" PRIu64, vote->data.validator_id); + lantern_log_warn( + "state", + meta, + "failed to hash attestation for validator=%" PRIu64, + vote->data.validator_id); return false; } - bool ok = lantern_signature_verify( + bool is_signature_valid = lantern_signature_verify( pubkey_bytes, LANTERN_VALIDATOR_PUBKEY_SIZE, vote->data.slot, signature, vote_root.bytes, sizeof(vote_root.bytes)); - if (!ok) + if (!is_signature_valid) { lantern_log_warn( "state", @@ -135,7 +553,7 @@ bool lantern_client_verify_vote_signature( vote->data.validator_id, context ? context : "unknown"); } - return ok; + return is_signature_valid; } @@ -183,91 +601,43 @@ bool lantern_client_validate_vote_constraints( const char *log_facility = (facility && *facility) ? facility : "state"; const char *label = (context && *context) ? context : "vote"; - struct checkpoint_rule + if (!validate_vote_checkpoint( + client, + vote, + &vote->source, + "source", + log_facility, + meta, + label, + out_rejection)) { - const LanternCheckpoint *checkpoint; - const char *name; - } rules[] = { - {.checkpoint = &vote->source, .name = "source"}, - {.checkpoint = &vote->target, .name = "target"}, - {.checkpoint = &vote->head, .name = "head"}, - }; + return false; + } - for (size_t i = 0; i < (sizeof(rules) / sizeof(rules[0])); ++i) + if (!validate_vote_checkpoint( + client, + vote, + &vote->target, + "target", + log_facility, + meta, + label, + out_rejection)) { - const struct checkpoint_rule *rule = &rules[i]; - char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(&rule->checkpoint->root, root_hex, sizeof(root_hex)); - if (lantern_root_is_zero(&rule->checkpoint->root)) - { - lantern_log_debug( - log_facility, - meta, - "dropping %s validator=%" PRIu64 " slot=%" PRIu64 " %s root=%s (zero root)", - label, - vote->validator_id, - vote->slot, - rule->name, - root_hex[0] ? root_hex : "0x0"); - if (out_rejection) - { - lantern_vote_rejection_set( - out_rejection, - "%s checkpoint root zero slot=%" PRIu64 " root=%s", - rule->name, - rule->checkpoint->slot, - root_hex[0] ? root_hex : "0x0"); - } - return false; - } - uint64_t block_slot = 0; - if (!lantern_client_block_known_locked(client, &rule->checkpoint->root, &block_slot)) - { - lantern_log_debug( - log_facility, - meta, - "dropping %s validator=%" PRIu64 " slot=%" PRIu64 " unknown %s root=%s", - label, - vote->validator_id, - vote->slot, - rule->name, - root_hex[0] ? root_hex : "0x0"); - if (out_rejection) - { - lantern_vote_rejection_set( - out_rejection, - "unknown %s root=%s slot=%" PRIu64, - rule->name, - root_hex[0] ? root_hex : "0x0", - rule->checkpoint->slot); - } - return false; - } - if (block_slot != rule->checkpoint->slot) - { - lantern_log_debug( - log_facility, - meta, - "dropping %s validator=%" PRIu64 " slot=%" PRIu64 " %s slot mismatch vote=%" PRIu64 - " block=%" PRIu64 " root=%s", - label, - vote->validator_id, - vote->slot, - rule->name, - rule->checkpoint->slot, - block_slot, - root_hex[0] ? root_hex : "0x0"); - if (out_rejection) - { - lantern_vote_rejection_set( - out_rejection, - "%s checkpoint slot mismatch vote=%" PRIu64 " block=%" PRIu64, - rule->name, - rule->checkpoint->slot, - block_slot); - } - return false; - } + return false; + } + + if (!validate_vote_checkpoint( + client, + vote, + &vote->head, + "head", + log_facility, + meta, + label, + out_rejection)) + { + return false; } uint64_t current_slot = 0; @@ -326,14 +696,10 @@ bool lantern_client_validate_vote_constraints( * Performs the complete vote validation and recording pipeline: * 1. Validates vote constraints (checkpoints, slots) * 2. Verifies XMSS signature - * 3. Checks block availability in fork choice or state justified window + * 3. Validates vote cache availability and validator range * 4. Updates fork choice with the vote * 5. Caches vote in state - * 6. Persists vote to storage - * - * Per leanSpec: Attestation validation only requires that the referenced - * blocks (source, target, head) exist in the store. We check fork choice - * first, then fall back to state's justified window for backwards compat. + * 6. Persists votes to storage * * @param client Client instance * @param vote Signed vote to record @@ -364,12 +730,11 @@ void lantern_client_record_vote( struct lantern_vote_rejection_info rejection; memset(&rejection, 0, sizeof(rejection)); - bool vote_processed = false; - char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char target_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char source_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; LanternSignedVote vote_copy = *vote; + char head_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + char target_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + char source_hex[VOTE_ROOT_HEX_BUFFER_LEN]; format_root_hex(&vote_copy.data.head.root, head_hex, sizeof(head_hex)); format_root_hex(&vote_copy.data.target.root, target_hex, sizeof(target_hex)); format_root_hex(&vote_copy.data.source.root, source_hex, sizeof(source_hex)); @@ -383,265 +748,22 @@ void lantern_client_record_vote( target_hex[0] ? target_hex : "0x0", vote_copy.data.target.slot); - if (!client->has_fork_choice) + bool vote_processed = process_vote_locked(client, &vote_copy, &meta, &rejection); + if (vote_processed) { - lantern_log_debug( - "gossip", - &meta, - "deferring vote validator=%" PRIu64 " slot=%" PRIu64 " (fork choice unavailable)", - vote_copy.data.validator_id, - vote_copy.data.slot); - lantern_vote_rejection_set(&rejection, "fork choice unavailable"); - goto cleanup; - } - - if (!lantern_client_validate_vote_constraints( - client, - &vote_copy.data, - "gossip", - &meta, - "gossip", - &rejection)) - { - goto cleanup; - } - - if (!lantern_client_verify_vote_signature( - client, - &vote_copy, - &vote_copy.signature, - &meta, - "gossip")) - { - lantern_log_debug( + lantern_log_info( "gossip", &meta, - "rejected vote validator=%" PRIu64 " slot=%" PRIu64 " (invalid XMSS signature)", + "processed vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 + " source=%s@%" PRIu64, vote_copy.data.validator_id, - vote_copy.data.slot); - lantern_vote_rejection_set(&rejection, "invalid XMSS signature"); - goto cleanup; - } - - const LanternVote *vote_data = &vote_copy.data; - uint64_t validator_count = client->state.config.num_validators; - if (validator_count == 0 || !client->state.validator_votes || client->state.validator_votes_len == 0) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (state vote cache unavailable)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set(&rejection, "state vote cache unavailable"); - goto cleanup; - } - if ((vote_data->validator_id >= validator_count) - || (vote_data->validator_id >= (uint64_t)client->state.validator_votes_len)) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (validator out of range)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set(&rejection, "validator out of range id=%" PRIu64, vote_data->validator_id); - goto cleanup; - } - if (vote_data->target.slot < vote_data->source.slot) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (target slot < source)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set( - &rejection, - "target slot %" PRIu64 " < source slot %" PRIu64, - vote_data->target.slot, - vote_data->source.slot); - goto cleanup; - } - - /* - * Per leanSpec, attestation validation only requires that the referenced - * blocks (source, target, head) exist in the store. We check fork choice - * first, then fall back to state's justified window for backwards compat. - * This allows attestations from competing forks to be processed correctly. - */ - bool source_block_known = false; - bool target_block_known = false; - bool head_block_known = false; - uint64_t source_block_slot = 0; - uint64_t target_block_slot = 0; - - if (client->has_fork_choice) - { - source_block_known = (lantern_fork_choice_block_info( - &client->fork_choice, &vote_data->source.root, &source_block_slot, NULL, NULL) == 0); - target_block_known = (lantern_fork_choice_block_info( - &client->fork_choice, &vote_data->target.root, &target_block_slot, NULL, NULL) == 0); - head_block_known = (lantern_fork_choice_block_info( - &client->fork_choice, &vote_data->head.root, NULL, NULL, NULL) == 0); - } - - if (!source_block_known) - { - /* Source block not in fork choice - check state's justified window as fallback */ - if (!lantern_state_slot_in_justified_window(&client->state, vote_data->source.slot)) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (source block unknown and outside justified window)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set( - &rejection, - "source slot=%" PRIu64 " block unknown and outside justified window", - vote_data->source.slot); - goto cleanup; - } - bool source_is_justified = false; - if (lantern_state_get_justified_slot_bit(&client->state, vote_data->source.slot, &source_is_justified) != 0 - || !source_is_justified) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (source not justified in state)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set(&rejection, "source slot=%" PRIu64 " not justified", vote_data->source.slot); - goto cleanup; - } - } - else - { - /* Source block is in fork choice - verify checkpoint slot matches block slot */ - if (source_block_slot != vote_data->source.slot) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (source checkpoint slot mismatch)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set( - &rejection, - "source checkpoint slot=%" PRIu64 " != block slot=%" PRIu64, - vote_data->source.slot, - source_block_slot); - goto cleanup; - } - } - - if (!target_block_known && !head_block_known) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (target and head blocks unknown)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set(&rejection, "target and head blocks unknown"); - goto cleanup; - } - - if (target_block_known && target_block_slot != vote_data->target.slot) - { - lantern_log_debug( - "gossip", - &meta, - "dropping vote validator=%" PRIu64 " slot=%" PRIu64 " (target checkpoint slot mismatch)", - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set( - &rejection, - "target checkpoint slot=%" PRIu64 " != block slot=%" PRIu64, - vote_data->target.slot, - target_block_slot); - goto cleanup; - } - - if (lantern_state_set_signed_validator_vote(&client->state, (size_t)vote_data->validator_id, &vote_copy) != 0) - { - lantern_log_debug( - "state", - &meta, - "failed to cache gossip vote validator=%" PRIu64 " slot=%" PRIu64, - vote_data->validator_id, - vote_data->slot); - lantern_vote_rejection_set( - &rejection, - "failed to cache vote validator=%" PRIu64 " slot=%" PRIu64, - vote_data->validator_id, - vote_data->slot); - goto cleanup; - } - - if (client->has_fork_choice) - { - if (lantern_fork_choice_add_vote(&client->fork_choice, &vote_copy, false) != 0) - { - lantern_log_debug( - "forkchoice", - &meta, - "failed to track gossip vote validator=%" PRIu64 " slot=%" PRIu64, - vote_copy.data.validator_id, - vote_copy.data.slot); - } - else - { - if (!client->debug_disable_fork_choice_time) - { - uint64_t now_seconds = 0; - if (!lantern_client_vote_time_seconds(client, vote_copy.data.slot, &now_seconds)) - { - now_seconds = validator_wall_time_now_seconds(); - } - if (lantern_fork_choice_advance_time(&client->fork_choice, now_seconds, false) != 0) - { - lantern_log_debug( - "forkchoice", - &meta, - "advancing fork choice time failed after validator=%" PRIu64 " slot=%" PRIu64, - vote_copy.data.validator_id, - vote_copy.data.slot); - } - } - } - } - - if (client->data_dir) - { - if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) - { - lantern_log_warn( - "storage", - &meta, - "failed to persist votes after validator=%" PRIu64 " slot=%" PRIu64, - vote_copy.data.validator_id, - vote_copy.data.slot); - } + vote_copy.data.slot, + head_hex[0] ? head_hex : "0x0", + target_hex[0] ? target_hex : "0x0", + vote_copy.data.target.slot, + source_hex[0] ? source_hex : "0x0", + vote_copy.data.source.slot); } - - vote_processed = true; - lantern_log_info( - "gossip", - &meta, - "processed vote validator=%" PRIu64 - " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 " source=%s@%" PRIu64, - vote_copy.data.validator_id, - vote_copy.data.slot, - head_hex[0] ? head_hex : "0x0", - target_hex[0] ? target_hex : "0x0", - vote_copy.data.target.slot, - source_hex[0] ? source_hex : "0x0", - vote_copy.data.source.slot); - -cleanup: lantern_client_unlock_state(client, state_locked); lantern_client_note_vote_outcome(client, peer_text, &vote_copy, vote_processed); if (!vote_processed) diff --git a/src/core/client_utils.c b/src/core/client_utils.c index e720452..43ec259 100644 --- a/src/core/client_utils.c +++ b/src/core/client_utils.c @@ -36,9 +36,25 @@ #include "lantern/consensus/fork_choice.h" #include "lantern/support/log.h" +#include "lantern/support/secure_mem.h" #include "lantern/support/strings.h" +/* ============================================================================ + * Constants + * ============================================================================ */ + +#if !defined(_WIN32) +static const uint64_t CLIENT_UTILS_MILLIS_PER_SECOND = 1000ULL; +static const uint64_t CLIENT_UTILS_MICROS_PER_MILLI = 1000ULL; +static const uint64_t CLIENT_UTILS_NANOS_PER_MILLI = 1000000ULL; +static const uint64_t CLIENT_UTILS_NANOS_PER_SECOND = 1000000000ULL; +#endif + +static const size_t CLIENT_UTILS_ROOT_HEX_MIN_LEN = 4; +static const size_t CLIENT_UTILS_NODE_KEY_SIZE = 32; + + /* ============================================================================ * Time Utilities * ============================================================================ */ @@ -60,29 +76,63 @@ uint64_t monotonic_millis(void) { return 0; } - return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; + if (ts.tv_sec < 0 || ts.tv_nsec < 0 || (uint64_t)ts.tv_nsec >= CLIENT_UTILS_NANOS_PER_SECOND) + { + return 0; + } + + uint64_t millis = (uint64_t)ts.tv_sec; + if (millis > UINT64_MAX / CLIENT_UTILS_MILLIS_PER_SECOND) + { + return 0; + } + millis *= CLIENT_UTILS_MILLIS_PER_SECOND; + + uint64_t nanos_part = (uint64_t)ts.tv_nsec / CLIENT_UTILS_NANOS_PER_MILLI; + if (millis > UINT64_MAX - nanos_part) + { + return 0; + } + return millis + nanos_part; #elif defined(__APPLE__) - static mach_timebase_info_data_t timebase = {0}; - if ((timebase.denom == 0) && (mach_timebase_info(&timebase) != KERN_SUCCESS)) + mach_timebase_info_data_t timebase = {0}; + if (mach_timebase_info(&timebase) != KERN_SUCCESS || timebase.denom == 0 || timebase.numer == 0) { return 0; } uint64_t ticks = mach_absolute_time(); - if ((timebase.numer != 0) && (ticks > UINT64_MAX / timebase.numer)) + if (ticks > UINT64_MAX / timebase.numer) { return 0; } uint64_t nanos = (ticks * timebase.numer) / timebase.denom; - return nanos / 1000000; + return nanos / CLIENT_UTILS_NANOS_PER_MILLI; #else struct timeval tv; if (gettimeofday(&tv, NULL) != 0) { return 0; } - return (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000; + if (tv.tv_sec < 0 || tv.tv_usec < 0) + { + return 0; + } + + uint64_t millis = (uint64_t)tv.tv_sec; + if (millis > UINT64_MAX / CLIENT_UTILS_MILLIS_PER_SECOND) + { + return 0; + } + millis *= CLIENT_UTILS_MILLIS_PER_SECOND; + + uint64_t usec_part = (uint64_t)tv.tv_usec / CLIENT_UTILS_MICROS_PER_MILLI; + if (millis > UINT64_MAX - usec_part) + { + return 0; + } + return millis + usec_part; #endif } @@ -97,20 +147,34 @@ uint64_t monotonic_millis(void) uint64_t validator_wall_time_now_seconds(void) { #if defined(_WIN32) + static const uint64_t WINDOWS_UNIX_EPOCH_DIFF_100NS = 116444736000000000ULL; + static const uint64_t WINDOWS_100NS_PER_SECOND = 10000000ULL; + FILETIME ft; GetSystemTimeAsFileTime(&ft); ULARGE_INTEGER uli; uli.LowPart = ft.dwLowDateTime; uli.HighPart = ft.dwHighDateTime; - // FILETIME is 100-nanosecond intervals since Jan 1, 1601 - // Convert to Unix epoch (seconds since Jan 1, 1970) - return (uint64_t)((uli.QuadPart - 116444736000000000ULL) / 10000000ULL); + uint64_t time_100ns = (uint64_t)uli.QuadPart; + if (time_100ns < WINDOWS_UNIX_EPOCH_DIFF_100NS) + { + return 0; + } + + // FILETIME is 100-nanosecond intervals since Jan 1, 1601. + // Convert to Unix epoch (seconds since Jan 1, 1970). + uint64_t unix_100ns = time_100ns - WINDOWS_UNIX_EPOCH_DIFF_100NS; + return unix_100ns / WINDOWS_100NS_PER_SECOND; #else struct timeval tv; if (gettimeofday(&tv, NULL) != 0) { return 0; } + if (tv.tv_sec < 0) + { + return 0; + } return (uint64_t)tv.tv_sec; #endif } @@ -129,8 +193,9 @@ void validator_sleep_ms(uint32_t ms) Sleep(ms); #else struct timespec ts; - ts.tv_sec = ms / 1000; - ts.tv_nsec = (ms % 1000) * 1000000L; + ts.tv_sec = (time_t)(ms / (uint32_t)CLIENT_UTILS_MILLIS_PER_SECOND); + ts.tv_nsec = (long)((ms % (uint32_t)CLIENT_UTILS_MILLIS_PER_SECOND) + * (uint32_t)CLIENT_UTILS_NANOS_PER_MILLI); while (nanosleep(&ts, &ts) != 0) { if (errno != EINTR) @@ -187,6 +252,32 @@ uint64_t blocks_request_backoff_ms(uint32_t failures) * State Locking * ============================================================================ */ +/** + * @brief Unlock a mutex and log on failure. + */ +static void unlock_mutex_with_log( + pthread_mutex_t *mutex, + const char *validator_id, + const char *name) +{ + if (!mutex || !name) + { + return; + } + + int unlock_rc = pthread_mutex_unlock(mutex); + if (unlock_rc != 0) + { + lantern_log_warn( + "client", + &(const struct lantern_log_metadata){.validator = validator_id}, + "failed to unlock %s: %d", + name, + unlock_rc); + } +} + + /** * Acquire the client state lock. * @@ -217,7 +308,7 @@ void lantern_client_unlock_state(struct lantern_client *client, bool locked) { if (locked && client && client->state_lock_initialized) { - pthread_mutex_unlock(&client->state_lock); + unlock_mutex_with_log(&client->state_lock, client->node_id, "state_lock"); } } @@ -252,7 +343,7 @@ void lantern_client_unlock_pending(struct lantern_client *client, bool locked) { if (locked && client && client->pending_lock_initialized) { - pthread_mutex_unlock(&client->pending_lock); + unlock_mutex_with_log(&client->pending_lock, client->node_id, "pending_lock"); } } @@ -280,28 +371,25 @@ void format_root_hex(const LanternRoot *root, char *out, size_t out_len) } out[0] = '\0'; - if (!root || out_len < 5) + if (!root || out_len < CLIENT_UTILS_ROOT_HEX_MIN_LEN) { return; } // Check if root is all zeros - bool all_zero = true; + bool is_all_zero = true; for (size_t i = 0; i < LANTERN_ROOT_SIZE; ++i) { if (root->bytes[i] != 0) { - all_zero = false; + is_all_zero = false; break; } } - if (all_zero) + if (is_all_zero) { - if (out_len >= 4) - { - strncpy(out, "0x0", out_len - 1); - out[out_len - 1] = '\0'; - } + strncpy(out, "0x0", out_len - 1); + out[out_len - 1] = '\0'; return; } @@ -504,7 +592,9 @@ bool lantern_client_block_known_locked( * * @param dest Pointer to destination string pointer * @param value Value to copy - * @return 0 on success, -1 on error + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if dest or value is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails * * @note Thread safety: This function is thread-safe */ @@ -512,16 +602,16 @@ int set_owned_string(char **dest, const char *value) { if (!dest || !value) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } char *copy = lantern_string_duplicate(value); if (!copy) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } free(*dest); *dest = copy; - return 0; + return LANTERN_CLIENT_OK; } @@ -530,7 +620,10 @@ int set_owned_string(char **dest, const char *value) * * @param path File path * @param out_text Output buffer (caller owns) - * @return 0 on success, -1 on error + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if path or out_text is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if the file cannot be read or trimmed * * @note Thread safety: This function is thread-safe */ @@ -538,10 +631,16 @@ int read_trimmed_file(const char *path, char **out_text) { if (!path || !out_text) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } - FILE *fp = fopen(path, "rb"); + int result = LANTERN_CLIENT_OK; + FILE *fp = NULL; + char *buffer = NULL; + + *out_text = NULL; + + fp = fopen(path, "rb"); if (!fp) { lantern_log_error( @@ -549,36 +648,35 @@ int read_trimmed_file(const char *path, char **out_text) &(const struct lantern_log_metadata){0}, "unable to open %s for reading", path); - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } if (fseek(fp, 0, SEEK_END) != 0) { - fclose(fp); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } long file_size = ftell(fp); - if (file_size < 0 || (unsigned long)file_size > SIZE_MAX - 1) + if (file_size < 0 || (uint64_t)file_size > SIZE_MAX - 1) { - fclose(fp); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } if (fseek(fp, 0, SEEK_SET) != 0) { - fclose(fp); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } size_t alloc_size = (size_t)file_size + 1; - char *buffer = malloc(alloc_size); + buffer = malloc(alloc_size); if (!buffer) { - fclose(fp); - return -1; + result = LANTERN_CLIENT_ERR_ALLOC; + goto cleanup; } size_t read_len = fread(buffer, 1, (size_t)file_size, fp); - fclose(fp); if (read_len != (size_t)file_size) { lantern_log_error( @@ -588,21 +686,38 @@ int read_trimmed_file(const char *path, char **out_text) path, read_len, file_size); - free(buffer); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } buffer[read_len] = '\0'; char *trimmed = lantern_trim_whitespace(buffer); if (!trimmed) { - free(buffer); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } size_t trimmed_len = strlen(trimmed); memmove(buffer, trimmed, trimmed_len + 1); *out_text = buffer; - return 0; + buffer = NULL; + result = LANTERN_CLIENT_OK; + +cleanup: + if (fp) + { + if (fclose(fp) != 0) + { + lantern_log_warn( + "client", + &(const struct lantern_log_metadata){0}, + "failed to close %s: errno=%d", + path, + errno); + } + } + free(buffer); + return result; } @@ -613,7 +728,10 @@ int read_trimmed_file(const char *path, char **out_text) * * @param options Client options * @param out_key Output buffer (32 bytes) - * @return 0 on success, -1 on error + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if options or out_key is NULL + * @return LANTERN_CLIENT_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_ERR_CONFIG if the key is missing or invalid * * @note Thread safety: This function is thread-safe */ @@ -621,25 +739,28 @@ int load_node_key_bytes(const struct lantern_client_options *options, uint8_t ou { if (!options || !out_key) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + lantern_secure_zero(out_key, CLIENT_UTILS_NODE_KEY_SIZE); + + int result = LANTERN_CLIENT_OK; char *owned = NULL; - int rc = -1; if (options->node_key_hex) { owned = lantern_string_duplicate(options->node_key_hex); if (!owned) { - return -1; + return LANTERN_CLIENT_ERR_ALLOC; } } else if (options->node_key_path) { - if (read_trimmed_file(options->node_key_path, &owned) != 0) + int rc = read_trimmed_file(options->node_key_path, &owned); + if (rc != LANTERN_CLIENT_OK) { - return -1; + return rc; } } else @@ -648,32 +769,32 @@ int load_node_key_bytes(const struct lantern_client_options *options, uint8_t ou "client", &(const struct lantern_log_metadata){.validator = options->node_id}, "--node-key or --node-key-path is required"); - return -1; + return LANTERN_CLIENT_ERR_CONFIG; } char *trimmed = lantern_trim_whitespace(owned); if (!trimmed) { - free(owned); - return -1; + result = LANTERN_CLIENT_ERR_CONFIG; + goto cleanup; } - rc = lantern_hex_decode(trimmed, out_key, 32); - if (rc != 0) + if (lantern_hex_decode(trimmed, out_key, CLIENT_UTILS_NODE_KEY_SIZE) != 0) { lantern_log_error( "client", &(const struct lantern_log_metadata){.validator = options->node_id}, "invalid node key (expected 32-byte hex string)"); + result = LANTERN_CLIENT_ERR_CONFIG; } +cleanup: if (owned) { - memset(owned, 0, strlen(owned)); + lantern_secure_zero(owned, strlen(owned)); free(owned); } - - return rc; + return result; } diff --git a/src/core/client_validator.c b/src/core/client_validator.c index 1d7217f..ec0bdcf 100644 --- a/src/core/client_validator.c +++ b/src/core/client_validator.c @@ -15,12 +15,13 @@ */ #include "client_services_internal.h" -#include "client_internal.h" #include #include #include +#include "client_internal.h" + #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/runtime.h" @@ -41,6 +42,37 @@ static const uint32_t VALIDATOR_SERVICE_IDLE_SLEEP_MS = 200; static const uint32_t VALIDATOR_SERVICE_POLL_SLEEP_MS = 50; +/* ============================================================================ + * Mutex Utilities + * ============================================================================ */ + +/** + * @brief Unlock a mutex and log on failure. + * + * @note Thread safety: This function is thread-safe + */ +static void unlock_mutex_with_log( + pthread_mutex_t *mutex, + const char *validator_id, + const char *name) +{ + if (!mutex || !name) + { + return; + } + int unlock_rc = pthread_mutex_unlock(mutex); + if (unlock_rc != 0) + { + lantern_log_warn( + "validator", + &(const struct lantern_log_metadata){.validator = validator_id}, + "failed to unlock %s: %d", + name, + unlock_rc); + } +} + + /* ============================================================================ * Validator Duty State * ============================================================================ */ @@ -183,12 +215,13 @@ bool validator_is_enabled(const struct lantern_client *client, size_t local_inde { return client->validator_enabled[local_index]; } - if (pthread_mutex_lock((pthread_mutex_t *)&client->validator_lock) != 0) + pthread_mutex_t *lock = (pthread_mutex_t *)&client->validator_lock; + if (pthread_mutex_lock(lock) != 0) { return client->validator_enabled[local_index]; } bool enabled = client->validator_enabled[local_index]; - pthread_mutex_unlock((pthread_mutex_t *)&client->validator_lock); + unlock_mutex_with_log(lock, client->node_id, "validator_lock"); return enabled; } @@ -255,6 +288,224 @@ int validator_sign_vote( } +/** + * @brief Collect parent root, checkpoints, and attestations for a new block. + * + * Computes the parent root and vote checkpoints from state, signs the + * proposer's vote, and collects attestations/signatures to include in the + * proposed block. + * + * @param client Client instance + * @param slot Slot number + * @param local Local validator + * @param out_parent_root Output for selected parent root + * @param out_proposer_vote Output for the proposer's signed vote + * @param att_list Initialized attestation list to populate + * @param att_signatures Initialized signature list to populate + * + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_RUNTIME on lock/state errors + * @return Propagated error codes from validator_sign_vote() + * + * @note Thread safety: This function acquires state_lock + */ +static lantern_client_error validator_build_block_collect_attestations( + struct lantern_client *client, + uint64_t slot, + struct lantern_local_validator *local, + LanternRoot *out_parent_root, + LanternSignedVote *out_proposer_vote, + LanternAttestations *att_list, + LanternBlockSignatures *att_signatures) +{ + lantern_client_error result = LANTERN_CLIENT_OK; + + if (!client || !local || !out_parent_root || !out_proposer_vote || !att_list || !att_signatures) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) + { + return LANTERN_CLIENT_ERR_RUNTIME; + } + + if (!client->has_state) + { + result = LANTERN_CLIENT_ERR_RUNTIME; + goto cleanup; + } + + if (lantern_state_select_block_parent(&client->state, out_parent_root) != 0) + { + result = LANTERN_CLIENT_ERR_RUNTIME; + goto cleanup; + } + + LanternCheckpoint head_cp; + LanternCheckpoint target_cp; + LanternCheckpoint source_cp; + if (lantern_state_compute_vote_checkpoints( + &client->state, + &head_cp, + &target_cp, + &source_cp) + != 0) + { + result = LANTERN_CLIENT_ERR_RUNTIME; + goto cleanup; + } + + out_proposer_vote->data.validator_id = local->global_index; + out_proposer_vote->data.slot = slot; + out_proposer_vote->data.head = head_cp; + out_proposer_vote->data.target = target_cp; + out_proposer_vote->data.source = source_cp; + + result = (lantern_client_error)validator_sign_vote(local, slot, out_proposer_vote); + if (result != LANTERN_CLIENT_OK) + { + goto cleanup; + } + + if (lantern_state_collect_attestations_for_block( + &client->state, + slot, + local->global_index, + out_parent_root, + out_proposer_vote, + att_list, + att_signatures) + != 0) + { + result = LANTERN_CLIENT_ERR_RUNTIME; + goto cleanup; + } + +cleanup: + lantern_client_unlock_state(client, state_locked); + return result; +} + + +/** + * @brief Populate a signed block from collected attestations and signatures. + * + * Initializes the block message fields, copies attestations into the body, + * and fills the signatures array with attestation signatures followed by the + * proposer signature. + * + * @param slot Slot number + * @param proposer_index Proposer's global validator index + * @param parent_root Parent block root + * @param proposer_vote Proposer's signed vote + * @param att_list Collected attestations + * @param att_signatures Collected attestation signatures + * @param out_block Block to populate + * + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs + * @return LANTERN_CLIENT_ERR_RUNTIME on overflow or inconsistent inputs + * @return LANTERN_CLIENT_ERR_ALLOC on allocation/copy failures + * + * @note Thread safety: This function is thread-safe + */ +static lantern_client_error validator_build_block_populate_message( + uint64_t slot, + uint64_t proposer_index, + const LanternRoot *parent_root, + const LanternSignedVote *proposer_vote, + const LanternAttestations *att_list, + const LanternBlockSignatures *att_signatures, + LanternSignedBlock *out_block) +{ + if (!parent_root || !proposer_vote || !att_list || !att_signatures || !out_block) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + LanternBlock *message_block = &out_block->message.block; + message_block->slot = slot; + message_block->proposer_index = proposer_index; + message_block->parent_root = *parent_root; + memset(&message_block->state_root, 0, sizeof(message_block->state_root)); + + if (lantern_attestations_copy(&message_block->body.attestations, att_list) != 0) + { + return LANTERN_CLIENT_ERR_ALLOC; + } + + out_block->message.proposer_attestation = proposer_vote->data; + + if (message_block->body.attestations.length > SIZE_MAX - 1u) + { + return LANTERN_CLIENT_ERR_RUNTIME; + } + size_t signature_count = message_block->body.attestations.length + 1u; + if (lantern_block_signatures_resize(&out_block->signatures, signature_count) != 0) + { + return LANTERN_CLIENT_ERR_ALLOC; + } + for (size_t i = 0; i + 1u < signature_count; ++i) + { + if (i < att_signatures->length && att_signatures->data) + { + out_block->signatures.data[i] = att_signatures->data[i]; + } + else + { + memset(out_block->signatures.data[i].bytes, 0, LANTERN_SIGNATURE_SIZE); + } + } + out_block->signatures.data[signature_count - 1u] = proposer_vote->signature; + + return LANTERN_CLIENT_OK; +} + + +/** + * @brief Preview post-state root for a built block. + * + * Computes the expected post-state root for the proposed block without + * mutating state. + * + * @param client Client instance + * @param block Built block to preview + * @param out_state_root Output for the computed post-state root + * + * @return LANTERN_CLIENT_OK on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs + * @return LANTERN_CLIENT_ERR_RUNTIME on lock/state failures + * + * @note Thread safety: This function acquires state_lock + */ +static lantern_client_error validator_build_block_preview_state_root( + struct lantern_client *client, + LanternSignedBlock *block, + LanternRoot *out_state_root) +{ + if (!client || !block || !out_state_root) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) + { + return LANTERN_CLIENT_ERR_RUNTIME; + } + + lantern_client_error result = LANTERN_CLIENT_OK; + if (lantern_state_preview_post_state_root(&client->state, block, out_state_root) != 0) + { + result = LANTERN_CLIENT_ERR_RUNTIME; + } + lantern_client_unlock_state(client, state_locked); + return result; +} + + /** * Refresh a cached vote with updated checkpoints and re-sign if needed. * @@ -516,12 +767,8 @@ int validator_build_block( { lantern_client_error result = LANTERN_CLIENT_OK; LanternRoot parent_root; - LanternCheckpoint head_cp; - LanternCheckpoint target_cp; - LanternCheckpoint source_cp; LanternAttestations att_list; LanternBlockSignatures att_signatures; - bool state_locked = false; bool att_list_initialized = false; bool att_signatures_initialized = false; @@ -537,129 +784,48 @@ int validator_build_block( lantern_signed_block_with_attestation_init(out_block); memset(out_proposer_vote, 0, sizeof(*out_proposer_vote)); - state_locked = lantern_client_lock_state(client); - if (!state_locked) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - if (!client->has_state) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - - if (lantern_state_select_block_parent(&client->state, &parent_root) != 0) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - - if (lantern_state_compute_vote_checkpoints( - &client->state, - &head_cp, - &target_cp, - &source_cp) - != 0) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - - out_proposer_vote->data.validator_id = local->global_index; - out_proposer_vote->data.slot = slot; - out_proposer_vote->data.head = head_cp; - out_proposer_vote->data.target = target_cp; - out_proposer_vote->data.source = source_cp; - - result = (lantern_client_error)validator_sign_vote(local, slot, out_proposer_vote); - if (result != LANTERN_CLIENT_OK) - { - goto cleanup; - } - lantern_attestations_init(&att_list); att_list_initialized = true; lantern_block_signatures_init(&att_signatures); att_signatures_initialized = true; - if (lantern_state_collect_attestations_for_block( - &client->state, - slot, - local->global_index, - &parent_root, - out_proposer_vote, - &att_list, - &att_signatures) - != 0) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - lantern_client_unlock_state(client, state_locked); - state_locked = false; - - LanternBlock *message_block = &out_block->message.block; - message_block->slot = slot; - message_block->proposer_index = local->global_index; - message_block->parent_root = parent_root; - memset(&message_block->state_root, 0, sizeof(message_block->state_root)); - - if (lantern_attestations_copy(&message_block->body.attestations, &att_list) != 0) + result = validator_build_block_collect_attestations( + client, + slot, + local, + &parent_root, + out_proposer_vote, + &att_list, + &att_signatures); + if (result != LANTERN_CLIENT_OK) { - result = LANTERN_CLIENT_ERR_ALLOC; goto cleanup; } - out_block->message.proposer_attestation = out_proposer_vote->data; - - if (message_block->body.attestations.length > SIZE_MAX - 1u) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - size_t signature_count = message_block->body.attestations.length + 1u; - if (lantern_block_signatures_resize(&out_block->signatures, signature_count) != 0) + result = validator_build_block_populate_message( + slot, + local->global_index, + &parent_root, + out_proposer_vote, + &att_list, + &att_signatures, + out_block); + if (result != LANTERN_CLIENT_OK) { - result = LANTERN_CLIENT_ERR_ALLOC; goto cleanup; } - for (size_t i = 0; i + 1u < signature_count; ++i) - { - if (i < att_signatures.length && att_signatures.data) - { - out_block->signatures.data[i] = att_signatures.data[i]; - } - else - { - memset(out_block->signatures.data[i].bytes, 0, LANTERN_SIGNATURE_SIZE); - } - } - out_block->signatures.data[signature_count - 1u] = out_proposer_vote->signature; LanternRoot computed_state_root; - state_locked = lantern_client_lock_state(client); - if (!state_locked) - { - result = LANTERN_CLIENT_ERR_RUNTIME; - goto cleanup; - } - if (lantern_state_preview_post_state_root(&client->state, out_block, &computed_state_root) != 0) + result = validator_build_block_preview_state_root(client, out_block, &computed_state_root); + if (result != LANTERN_CLIENT_OK) { - result = LANTERN_CLIENT_ERR_RUNTIME; goto cleanup; } - lantern_client_unlock_state(client, state_locked); - state_locked = false; - message_block->state_root = computed_state_root; + out_block->message.block.state_root = computed_state_root; result = LANTERN_CLIENT_OK; cleanup: - if (state_locked) - { - lantern_client_unlock_state(client, state_locked); - } if (att_list_initialized) { lantern_attestations_reset(&att_list); @@ -734,7 +900,7 @@ int validator_propose_block(struct lantern_client *client, uint64_t slot, size_t local->pending_attestation_slot = slot; local->has_pending_attestation = true; } - pthread_mutex_unlock(&client->validator_lock); + unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); } lantern_signed_block_with_attestation_reset(&block); @@ -854,7 +1020,7 @@ int validator_publish_attestations(struct lantern_client *client, uint64_t slot) if (have_lock) { - pthread_mutex_unlock(&client->validator_lock); + unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); } return result; } @@ -918,18 +1084,18 @@ void *validator_thread(void *arg) bool is_local = false; uint64_t local_index = 0; - if (lantern_consensus_runtime_local_proposer( - &client->runtime, - tp->slot, - &is_local, - &local_index) - == 0 - && is_local - && local_index < client->local_validator_count) - { - duty->pending_local_proposal = true; - duty->pending_local_index = local_index; - } + if (lantern_consensus_runtime_local_proposer( + &client->runtime, + tp->slot, + &is_local, + &local_index) + == 0 + && is_local + && local_index < client->local_validator_count) + { + duty->pending_local_proposal = true; + duty->pending_local_index = local_index; + } } duty->last_interval = tp->interval_index; @@ -941,30 +1107,32 @@ void *validator_thread(void *arg) switch (tp->phase) { - case LANTERN_DUTY_PHASE_PROPOSAL: - if (duty->pending_local_proposal && !duty->slot_proposed) - { - if (validator_propose_block( - client, - tp->slot, - (size_t)duty->pending_local_index) - == LANTERN_CLIENT_OK) + case LANTERN_DUTY_PHASE_PROPOSAL: + if (duty->pending_local_proposal && !duty->slot_proposed) { - duty->slot_proposed = true; + if (validator_propose_block( + client, + tp->slot, + (size_t)duty->pending_local_index) + == LANTERN_CLIENT_OK) + { + duty->slot_proposed = true; + } } - } - break; - case LANTERN_DUTY_PHASE_VOTE: - if (!duty->slot_attested) - { - if (validator_publish_attestations(client, tp->slot) == LANTERN_CLIENT_OK) + break; + + case LANTERN_DUTY_PHASE_VOTE: + if (!duty->slot_attested) { - duty->slot_attested = true; + if (validator_publish_attestations(client, tp->slot) == LANTERN_CLIENT_OK) + { + duty->slot_attested = true; + } } - } - break; - default: - break; + break; + + default: + break; } validator_sleep_ms(VALIDATOR_SERVICE_POLL_SLEEP_MS); @@ -1039,7 +1207,15 @@ void stop_validator_service(struct lantern_client *client) return; } __atomic_store_n(&client->validator_stop_flag, 1, __ATOMIC_RELAXED); - (void)pthread_join(client->validator_thread, NULL); + int join_rc = pthread_join(client->validator_thread, NULL); + if (join_rc != 0) + { + lantern_log_warn( + "validator", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pthread_join failed: %d", + join_rc); + } client->validator_thread_started = false; lantern_log_info( "validator", From 628e087481d98ac6e20409e5d1a48a2511291f23 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:49:16 +1000 Subject: [PATCH 04/12] Update client_pending.c --- src/core/client_pending.c | 199 +++++++++++++++++++++++++++++++------- 1 file changed, 163 insertions(+), 36 deletions(-) diff --git a/src/core/client_pending.c b/src/core/client_pending.c index 8214fab..0709406 100644 --- a/src/core/client_pending.c +++ b/src/core/client_pending.c @@ -5,15 +5,142 @@ * Implements list operations for pending blocks (waiting for parent) * and persisted blocks (stored for replay). * - * @note Thread safety: List functions require caller to hold pending_lock - * unless otherwise noted. + * @note Thread safety: + * - Pending list functions require caller to hold pending_lock. + * - Persisted list helpers are thread-safe. */ #include "client_internal.h" +#include #include #include +enum +{ + LANTERN_CLIENT_PENDING_OK = 0, + LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM = -1, + LANTERN_CLIENT_PENDING_ERR_ALLOC = -2, + LANTERN_CLIENT_PENDING_ERR_OVERFLOW = -3, + LANTERN_CLIENT_PENDING_ERR_COPY = -4, +}; + +static const size_t BLOCK_LIST_INITIAL_CAPACITY = 4u; + + +/* ============================================================================ + * Helpers + * ============================================================================ */ + +/** + * @brief Ensure the persisted block list can hold at least `required` entries. + */ +static int ensure_persisted_block_list_capacity( + struct lantern_persisted_block_list *list, + size_t required) +{ + if (!list) + { + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; + } + + if (list->capacity >= required) + { + return LANTERN_CLIENT_PENDING_OK; + } + + size_t new_capacity = BLOCK_LIST_INITIAL_CAPACITY; + if (list->capacity > 0) + { + size_t half = list->capacity / 2u; + if (list->capacity > SIZE_MAX - half) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + new_capacity = list->capacity + half; + if (new_capacity < BLOCK_LIST_INITIAL_CAPACITY) + { + new_capacity = BLOCK_LIST_INITIAL_CAPACITY; + } + } + + if (new_capacity < required) + { + new_capacity = required; + } + + if (new_capacity > SIZE_MAX / sizeof(*list->items)) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + + struct lantern_persisted_block *expanded = realloc( + list->items, + new_capacity * sizeof(*expanded)); + if (!expanded) + { + return LANTERN_CLIENT_PENDING_ERR_ALLOC; + } + list->items = expanded; + list->capacity = new_capacity; + return LANTERN_CLIENT_PENDING_OK; +} + + +/** + * @brief Ensure the pending block list can hold at least `required` entries. + */ +static int ensure_pending_block_list_capacity( + struct lantern_pending_block_list *list, + size_t required) +{ + if (!list) + { + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; + } + + if (list->capacity >= required) + { + return LANTERN_CLIENT_PENDING_OK; + } + + size_t new_capacity = BLOCK_LIST_INITIAL_CAPACITY; + if (list->capacity > 0) + { + size_t half = list->capacity / 2u; + if (list->capacity > SIZE_MAX - half) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + new_capacity = list->capacity + half; + if (new_capacity < BLOCK_LIST_INITIAL_CAPACITY) + { + new_capacity = BLOCK_LIST_INITIAL_CAPACITY; + } + } + + if (new_capacity < required) + { + new_capacity = required; + } + + if (new_capacity > SIZE_MAX / sizeof(*list->items)) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + + struct lantern_pending_block *expanded = realloc( + list->items, + new_capacity * sizeof(*expanded)); + if (!expanded) + { + return LANTERN_CLIENT_PENDING_ERR_ALLOC; + } + list->items = expanded; + list->capacity = new_capacity; + return LANTERN_CLIENT_PENDING_OK; +} + /* ============================================================================ * Block Cloning @@ -24,7 +151,9 @@ * * @param source Source block to clone * @param dest Destination block (will be initialized) - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_PENDING_OK on success + * @return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_PENDING_ERR_COPY if block cloning fails * * @note Thread safety: This function is thread-safe */ @@ -32,7 +161,7 @@ int clone_signed_block(const LanternSignedBlock *source, LanternSignedBlock *des { if (!source || !dest) { - return -1; + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; } lantern_signed_block_with_attestation_init(dest); @@ -46,7 +175,7 @@ int clone_signed_block(const LanternSignedBlock *source, LanternSignedBlock *des &source->message.block.body.attestations) != 0) { lantern_signed_block_with_attestation_reset(dest); - return -1; + return LANTERN_CLIENT_PENDING_ERR_COPY; } dest->message.proposer_attestation = source->message.proposer_attestation; @@ -54,10 +183,10 @@ int clone_signed_block(const LanternSignedBlock *source, LanternSignedBlock *des if (lantern_block_signatures_copy(&dest->signatures, &source->signatures) != 0) { lantern_signed_block_with_attestation_reset(dest); - return -1; + return LANTERN_CLIENT_PENDING_ERR_COPY; } - return 0; + return LANTERN_CLIENT_PENDING_OK; } @@ -117,7 +246,11 @@ void persisted_block_list_reset(struct lantern_persisted_block_list *list) * @param list List to append to * @param block Block to append * @param root Root of the block - * @return 0 on success, -1 on failure + * @return LANTERN_CLIENT_PENDING_OK on success + * @return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM if any parameter is NULL + * @return LANTERN_CLIENT_PENDING_ERR_OVERFLOW if the list size would overflow + * @return LANTERN_CLIENT_PENDING_ERR_ALLOC if allocation fails + * @return LANTERN_CLIENT_PENDING_ERR_COPY if block cloning fails * * @note Thread safety: This function is thread-safe */ @@ -128,32 +261,30 @@ int persisted_block_list_append( { if (!list || !block || !root) { - return -1; + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; } - if (list->length == list->capacity) + if (list->length == SIZE_MAX) { - size_t new_capacity = list->capacity == 0 ? 4u : list->capacity * 2u; - struct lantern_persisted_block *expanded = realloc( - list->items, - new_capacity * sizeof(*expanded)); - if (!expanded) - { - return -1; - } - list->items = expanded; - list->capacity = new_capacity; + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + + int ensure_rc = ensure_persisted_block_list_capacity(list, list->length + 1u); + if (ensure_rc != LANTERN_CLIENT_PENDING_OK) + { + return ensure_rc; } struct lantern_persisted_block *entry = &list->items[list->length]; - if (clone_signed_block(block, &entry->block) != 0) + int clone_rc = clone_signed_block(block, &entry->block); + if (clone_rc != LANTERN_CLIENT_PENDING_OK) { - return -1; + return clone_rc; } entry->root = *root; list->length += 1; - return 0; + return LANTERN_CLIENT_PENDING_OK; } @@ -299,24 +430,20 @@ struct lantern_pending_block *pending_block_list_append( return NULL; } - if (list->length == list->capacity) + if (list->length == SIZE_MAX) { - size_t new_capacity = list->capacity == 0 ? 4u : list->capacity * 2u; - struct lantern_pending_block *expanded = realloc( - list->items, - new_capacity * sizeof(*expanded)); - if (!expanded) - { - return NULL; - } - list->items = expanded; - list->capacity = new_capacity; + return NULL; + } + + int ensure_rc = ensure_pending_block_list_capacity(list, list->length + 1u); + if (ensure_rc != LANTERN_CLIENT_PENDING_OK) + { + return NULL; } struct lantern_pending_block *entry = &list->items[list->length]; - if (clone_signed_block(block, &entry->block) != 0) + if (clone_signed_block(block, &entry->block) != LANTERN_CLIENT_PENDING_OK) { - lantern_signed_block_with_attestation_reset(&entry->block); memset(entry, 0, sizeof(*entry)); return NULL; } From d6a37af7ec4d1f2e73d05471ca1c37567e93e97d Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Sun, 14 Dec 2025 13:42:29 +1000 Subject: [PATCH 05/12] Update client_debug.c --- src/core/client_debug.c | 231 +++++++++++++++------------------------- 1 file changed, 87 insertions(+), 144 deletions(-) diff --git a/src/core/client_debug.c b/src/core/client_debug.c index 487e642..c3ad981 100644 --- a/src/core/client_debug.c +++ b/src/core/client_debug.c @@ -17,40 +17,9 @@ #include "client_internal.h" -#include "lantern/support/log.h" - -#include #include - -/* ============================================================================ - * External Functions (from client_sync.c, client_reqresp.c) - * ============================================================================ */ - -extern void lantern_client_record_vote( - struct lantern_client *client, - const LanternSignedVote *vote, - const char *peer_id_text); - -extern bool lantern_client_import_block( - struct lantern_client *client, - const LanternSignedBlock *block, - const LanternRoot *block_root, - const struct lantern_log_metadata *meta); - -extern void lantern_client_enqueue_pending_block( - struct lantern_client *client, - const LanternSignedBlock *block, - const LanternRoot *block_root, - const LanternRoot *parent_root, - const char *peer_id_text); - -extern void lantern_client_on_blocks_request_complete( - struct lantern_client *client, - const char *peer_id, - const LanternRoot *request_root, - enum lantern_blocks_request_outcome outcome); - +#include "lantern/support/log.h" /* ============================================================================ * Debug Vote/Block Recording @@ -62,7 +31,8 @@ extern void lantern_client_on_blocks_request_complete( * @param client Client instance * @param vote Vote to record * @param peer_id_text Peer ID text (may be NULL) - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs * * @note Thread safety: Acquires appropriate locks internally */ @@ -73,10 +43,10 @@ int lantern_client_debug_record_vote( { if (!client || !vote) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } lantern_client_record_vote(client, vote, peer_id_text); - return 0; + return LANTERN_CLIENT_OK; } @@ -87,7 +57,9 @@ int lantern_client_debug_record_vote( * @param block Block to import * @param block_root Block root hash * @param peer_id_text Peer ID text (may be NULL) - * @return 1 if imported, 0 if not imported, -1 on error + * @return 1 if imported + * @return 0 if not imported + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs * * @note Thread safety: Acquires appropriate locks internally */ @@ -97,11 +69,20 @@ int lantern_client_debug_import_block( const LanternRoot *block_root, const char *peer_id_text) { - struct lantern_log_metadata meta = { - .validator = client ? client->node_id : NULL, - .peer = peer_id_text, - }; - return lantern_client_import_block(client, block, block_root, &meta) ? 1 : 0; + if (!client || !block || !block_root) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + return lantern_client_import_block( + client, + block, + block_root, + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text}) + ? 1 + : 0; } @@ -126,10 +107,7 @@ size_t lantern_client_pending_block_count(const struct lantern_client *client) struct lantern_client *mutable_client = (struct lantern_client *)client; bool locked = lantern_client_lock_pending(mutable_client); size_t count = client->pending_blocks.length; - if (locked) - { - lantern_client_unlock_pending(mutable_client, locked); - } + lantern_client_unlock_pending(mutable_client, locked); return count; } @@ -142,7 +120,8 @@ size_t lantern_client_pending_block_count(const struct lantern_client *client) * @param block_root Block root hash * @param parent_root Parent block root hash * @param peer_id_text Peer ID text (may be NULL) - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on NULL inputs * * @note Thread safety: Acquires pending_lock internally */ @@ -155,10 +134,15 @@ int lantern_client_debug_enqueue_pending_block( { if (!client || !block || !block_root || !parent_root) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } - lantern_client_enqueue_pending_block(client, block, block_root, parent_root, peer_id_text); - return 0; + lantern_client_enqueue_pending_block( + client, + block, + block_root, + parent_root, + peer_id_text); + return LANTERN_CLIENT_OK; } @@ -172,7 +156,8 @@ int lantern_client_debug_enqueue_pending_block( * @param out_parent_requested Output: whether parent was requested (may be NULL) * @param out_peer_text Output: peer text buffer (may be NULL) * @param peer_text_len Size of peer text buffer - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on invalid inputs * * @note Thread safety: Acquires pending_lock internally */ @@ -187,55 +172,37 @@ int lantern_client_debug_pending_entry( { if (!client) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } + if (out_peer_text && peer_text_len == 0) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + struct lantern_client *mutable_client = (struct lantern_client *)client; bool locked = lantern_client_lock_pending(mutable_client); - if (locked && index >= client->pending_blocks.length) + if (index >= client->pending_blocks.length) { lantern_client_unlock_pending(mutable_client, locked); - return -1; - } - if (!locked && index >= client->pending_blocks.length) - { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } LanternRoot root_copy; LanternRoot parent_copy; bool requested = false; - char peer_copy[128]; + char peer_copy[sizeof(((struct lantern_pending_block){0}).peer_text)]; peer_copy[0] = '\0'; - if (locked) + const struct lantern_pending_block *entry = &client->pending_blocks.items[index]; + root_copy = entry->root; + parent_copy = entry->parent_root; + requested = entry->parent_requested; + if (entry->peer_text[0]) { - const struct lantern_pending_block *entry = &client->pending_blocks.items[index]; - root_copy = entry->root; - parent_copy = entry->parent_root; - requested = entry->parent_requested; - if (entry->peer_text[0]) - { - strncpy(peer_copy, entry->peer_text, sizeof(peer_copy) - 1u); - peer_copy[sizeof(peer_copy) - 1u] = '\0'; - } - lantern_client_unlock_pending(mutable_client, locked); - } - else - { - const struct lantern_pending_block *entry = &client->pending_blocks.items[index]; - if (!entry) - { - return -1; - } - root_copy = entry->root; - parent_copy = entry->parent_root; - requested = entry->parent_requested; - if (entry->peer_text[0]) - { - strncpy(peer_copy, entry->peer_text, sizeof(peer_copy) - 1u); - peer_copy[sizeof(peer_copy) - 1u] = '\0'; - } + strncpy(peer_copy, entry->peer_text, sizeof(peer_copy) - 1u); + peer_copy[sizeof(peer_copy) - 1u] = '\0'; } + lantern_client_unlock_pending(mutable_client, locked); if (out_root) { @@ -251,24 +218,14 @@ int lantern_client_debug_pending_entry( } if (out_peer_text && peer_text_len > 0) { - if (peer_text_len == 1) - { - out_peer_text[0] = '\0'; - } - else + out_peer_text[0] = '\0'; + if (peer_text_len > 1 && peer_copy[0]) { - if (peer_copy[0]) - { - strncpy(out_peer_text, peer_copy, peer_text_len - 1u); - out_peer_text[peer_text_len - 1u] = '\0'; - } - else - { - out_peer_text[0] = '\0'; - } + strncpy(out_peer_text, peer_copy, peer_text_len - 1u); + out_peer_text[peer_text_len - 1u] = '\0'; } } - return 0; + return LANTERN_CLIENT_OK; } @@ -286,15 +243,8 @@ void lantern_client_debug_pending_reset(struct lantern_client *client) return; } bool locked = lantern_client_lock_pending(client); - if (locked) - { - pending_block_list_reset(&client->pending_blocks); - lantern_client_unlock_pending(client, locked); - } - else - { - pending_block_list_reset(&client->pending_blocks); - } + pending_block_list_reset(&client->pending_blocks); + lantern_client_unlock_pending(client, locked); } @@ -304,7 +254,8 @@ void lantern_client_debug_pending_reset(struct lantern_client *client) * @param client Client instance * @param root Block root to find * @param requested New value for parent_requested flag - * @return 0 on success, -1 if not found or error + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on invalid inputs or when root is not found * * @note Thread safety: Acquires pending_lock internally */ @@ -315,28 +266,16 @@ int lantern_client_debug_set_parent_requested( { if (!client || !root) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } bool locked = lantern_client_lock_pending(client); - struct lantern_pending_block *entry = NULL; - if (locked) - { - entry = pending_block_list_find(&client->pending_blocks, root); - if (entry) - { - entry->parent_requested = requested; - } - lantern_client_unlock_pending(client, locked); - } - else + struct lantern_pending_block *entry = pending_block_list_find(&client->pending_blocks, root); + if (entry) { - entry = pending_block_list_find(&client->pending_blocks, root); - if (entry) - { - entry->parent_requested = requested; - } + entry->parent_requested = requested; } - return entry ? 0 : -1; + lantern_client_unlock_pending(client, locked); + return entry ? LANTERN_CLIENT_OK : LANTERN_CLIENT_ERR_INVALID_PARAM; } @@ -358,7 +297,7 @@ void lantern_client_debug_disable_block_requests(struct lantern_client *client, { return; } - client->debug_disable_block_requests = disable ? true : false; + client->debug_disable_block_requests = disable; } @@ -369,7 +308,8 @@ void lantern_client_debug_disable_block_requests(struct lantern_client *client, * @param peer_id Peer ID text * @param request_root Root that was requested * @param outcome_code Outcome code (LANTERN_DEBUG_BLOCKS_REQUEST_*) - * @return 0 on success, -1 on failure + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM on invalid inputs * * @note Thread safety: Acquires appropriate locks internally */ @@ -379,25 +319,28 @@ int lantern_client_debug_on_blocks_request_complete( const LanternRoot *request_root, int outcome_code) { - if (!client) + if (!client || !peer_id || peer_id[0] == '\0' || !request_root) { - return -1; + return LANTERN_CLIENT_ERR_INVALID_PARAM; } enum lantern_blocks_request_outcome outcome; switch (outcome_code) { - case LANTERN_DEBUG_BLOCKS_REQUEST_SUCCESS: - outcome = LANTERN_BLOCKS_REQUEST_SUCCESS; - break; - case LANTERN_DEBUG_BLOCKS_REQUEST_FAILED: - outcome = LANTERN_BLOCKS_REQUEST_FAILED; - break; - case LANTERN_DEBUG_BLOCKS_REQUEST_ABORTED: - outcome = LANTERN_BLOCKS_REQUEST_ABORTED; - break; - default: - return -1; + case LANTERN_DEBUG_BLOCKS_REQUEST_SUCCESS: + outcome = LANTERN_BLOCKS_REQUEST_SUCCESS; + break; + + case LANTERN_DEBUG_BLOCKS_REQUEST_FAILED: + outcome = LANTERN_BLOCKS_REQUEST_FAILED; + break; + + case LANTERN_DEBUG_BLOCKS_REQUEST_ABORTED: + outcome = LANTERN_BLOCKS_REQUEST_ABORTED; + break; + + default: + return LANTERN_CLIENT_ERR_INVALID_PARAM; } lantern_client_on_blocks_request_complete(client, peer_id, request_root, outcome); - return 0; + return LANTERN_CLIENT_OK; } From 1d18176a34d872c2ac0d5e34b1152623266eb171 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:15:00 +1000 Subject: [PATCH 06/12] Refactor encoding --- CMakeLists.txt | 10 +- src/encoding/rlp.c | 958 ++++++++++++---- src/encoding/snappy.c | 861 ++++++++++---- src/genesis/genesis.c | 1295 +++------------------ src/genesis/genesis_internal.h | 48 + src/genesis/genesis_parse.c | 1444 ++++++++++++++++++++++++ src/genesis/genesis_validator_config.c | 460 ++++++++ tests/unit/test_snappy.c | 30 +- 8 files changed, 3566 insertions(+), 1540 deletions(-) create mode 100644 src/genesis/genesis_internal.h create mode 100644 src/genesis/genesis_parse.c create mode 100644 src/genesis/genesis_validator_config.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a64d86..cbec282 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,10 +49,12 @@ add_library(lantern STATIC src/core/client_sync.c src/core/client_sync_blocks.c src/core/client_sync_votes.c - src/core/client_utils.c - src/core/client_validator.c - src/genesis/genesis.c - src/encoding/snappy.c + src/core/client_utils.c + src/core/client_validator.c + src/genesis/genesis.c + src/genesis/genesis_parse.c + src/genesis/genesis_validator_config.c + src/encoding/snappy.c src/networking/enr.c src/networking/gossip.c src/networking/gossipsub_service.c diff --git a/src/encoding/rlp.c b/src/encoding/rlp.c index 267b84b..cf6fe43 100644 --- a/src/encoding/rlp.c +++ b/src/encoding/rlp.c @@ -1,372 +1,858 @@ +/** + * @file rlp.c + * @brief Recursive Length Prefix (RLP) encoding and decoding. + * + * Implements Ethereum RLP encoding/decoding for byte strings and lists. + * + * @spec Ethereum Recursive Length Prefix (RLP) + */ + #include "lantern/encoding/rlp.h" -#include #include #include #include -struct lantern_rlp_cursor { - const uint8_t *data; - size_t length; - size_t offset; +/** + * RLP module-specific error codes. + */ +typedef enum +{ + LANTERN_RLP_OK = 0, + LANTERN_RLP_ERR_INVALID_PARAM = -1, + LANTERN_RLP_ERR_OUT_OF_MEMORY = -2, + LANTERN_RLP_ERR_OVERFLOW = -3, + LANTERN_RLP_ERR_INVALID_ENCODING = -4, + LANTERN_RLP_ERR_TRAILING_DATA = -5, +} lantern_rlp_error_t; + +static const size_t RLP_SHORT_PAYLOAD_MAX = 55; +static const size_t RLP_INITIAL_LIST_CAPACITY = 4; + +static const uint8_t RLP_PREFIX_SINGLE_BYTE_MAX = 0x7Fu; +static const uint8_t RLP_PREFIX_SHORT_STRING_BASE = 0x80u; +static const uint8_t RLP_PREFIX_SHORT_STRING_MAX = 0xB7u; +static const uint8_t RLP_PREFIX_LONG_STRING_BASE = 0xB7u; +static const uint8_t RLP_PREFIX_LONG_STRING_MAX = 0xBFu; +static const uint8_t RLP_PREFIX_SHORT_LIST_BASE = 0xC0u; +static const uint8_t RLP_PREFIX_SHORT_LIST_MAX = 0xF7u; +static const uint8_t RLP_PREFIX_LONG_LIST_BASE = 0xF7u; + +/** + * @brief Cursor for safe incremental decoding. + */ +struct lantern_rlp_cursor +{ + const uint8_t *data; /**< Start of encoded buffer */ + size_t length; /**< Total bytes available */ + size_t offset; /**< Current read offset */ }; -static void lantern_rlp_view_zero(struct lantern_rlp_view *view) { - if (view) { - view->kind = 0; - view->data = NULL; - view->length = 0; - view->items = NULL; - view->item_count = 0; +static int decode_item(struct lantern_rlp_cursor *cursor, struct lantern_rlp_view *view); + +static int decode_list_payload( + struct lantern_rlp_cursor *cursor, + size_t payload_length, + struct lantern_rlp_view *view); + +/** + * @brief Zero an RLP view (does not free child items). + */ +static void rlp_view_zero(struct lantern_rlp_view *view) +{ + if (!view) + { + return; } + + view->kind = 0; + view->data = NULL; + view->length = 0; + view->items = NULL; + view->item_count = 0; } -void lantern_rlp_view_reset(struct lantern_rlp_view *view) { - if (!view) { + +/** + * Reset an RLP view and free any owned children. + * + * @param view View to reset. Safe to call with NULL. + * + * @note Thread safety: Caller must ensure exclusive access to view. + */ +void lantern_rlp_view_reset(struct lantern_rlp_view *view) +{ + if (!view) + { return; } - if (view->kind == LANTERN_RLP_KIND_LIST && view->items) { - for (size_t i = 0; i < view->item_count; ++i) { + + if (view->kind == LANTERN_RLP_KIND_LIST && view->items) + { + for (size_t i = 0; i < view->item_count; i++) + { lantern_rlp_view_reset(&view->items[i]); } + free(view->items); } - lantern_rlp_view_zero(view); + + rlp_view_zero(view); } -void lantern_rlp_buffer_reset(struct lantern_rlp_buffer *buffer) { - if (!buffer) { + +/** + * Reset an RLP buffer and free any owned data. + * + * @param buffer Buffer to reset. Safe to call with NULL. + * + * @note Thread safety: Caller must ensure exclusive access to buffer. + */ +void lantern_rlp_buffer_reset(struct lantern_rlp_buffer *buffer) +{ + if (!buffer) + { return; } + free(buffer->data); buffer->data = NULL; buffer->length = 0; } -static size_t bytes_required(size_t value) { + +/** + * @brief Returns true if a + b would overflow size_t. + */ +static bool size_add_would_overflow(size_t a, size_t b, size_t *out) +{ + if (!out) + { + return true; + } + + if (a > SIZE_MAX - b) + { + return true; + } + + *out = a + b; + return false; +} + + +/** + * @brief Returns true if a * b would overflow size_t. + */ +static bool size_mul_would_overflow(size_t a, size_t b, size_t *out) +{ + if (!out) + { + return true; + } + + if (a != 0 && b > SIZE_MAX / a) + { + return true; + } + + *out = a * b; + return false; +} + + +/** + * @brief Returns the number of bytes required to represent value in big-endian. + */ +static size_t rlp_be_bytes_required(size_t value) +{ size_t count = 0; - do { - ++count; + + do + { + count++; value >>= 8; } while (value != 0); + return count; } -static size_t rlp_string_encoded_length(const uint8_t *data, size_t length) { - if (length == 1 && data && data[0] < 0x80) { - return 1; - } - if (length < 56) { - return 1 + length; - } - return 1 + bytes_required(length) + length; -} -static size_t rlp_list_header_length(size_t payload_length) { - if (payload_length < 56) { +/** + * @brief Returns the number of bytes needed to encode an RLP length prefix. + */ +static size_t rlp_length_prefix_size(size_t length) +{ + if (length <= RLP_SHORT_PAYLOAD_MAX) + { return 1; } - return 1 + bytes_required(payload_length); + + return 1 + rlp_be_bytes_required(length); } -static bool size_add_overflow(size_t a, size_t b, size_t *out) { - if (SIZE_MAX - a < b) { - return true; + +/** + * @brief Writes an RLP length prefix for a string or list. + */ +static int rlp_write_length_prefix( + uint8_t *dest, + size_t length, + uint8_t short_base, + uint8_t long_base, + size_t *out_written) +{ + if (!dest || !out_written) + { + return LANTERN_RLP_ERR_INVALID_PARAM; } - *out = a + b; - return false; -} -static int write_length(uint8_t *dest, size_t length, uint8_t short_base, uint8_t long_base) { - if (length < 56) { + if (length <= RLP_SHORT_PAYLOAD_MAX) + { dest[0] = (uint8_t)(short_base + length); - return 1; + *out_written = 1; + return LANTERN_RLP_OK; + } + + size_t len_of_len = rlp_be_bytes_required(length); + if (len_of_len == 0 || len_of_len > sizeof(size_t)) + { + return LANTERN_RLP_ERR_OVERFLOW; } - size_t len_of_len = bytes_required(length); dest[0] = (uint8_t)(long_base + len_of_len); - for (size_t i = 0; i < len_of_len; ++i) { + for (size_t i = 0; i < len_of_len; i++) + { size_t shift = (len_of_len - i - 1) * 8; - dest[1 + i] = (uint8_t)((length >> shift) & 0xFF); + dest[1 + i] = (uint8_t)((length >> shift) & 0xFFu); } - return (int)(1 + len_of_len); + + *out_written = 1 + len_of_len; + return LANTERN_RLP_OK; } -int lantern_rlp_encode_bytes(struct lantern_rlp_buffer *buffer, const uint8_t *data, size_t length) { - if (!buffer || (length > 0 && !data)) { - return -1; + +/** + * Encode a byte string as RLP. + * + * @param buffer Output buffer to receive the encoded item (reset on entry). + * @param data Byte string to encode (may be NULL if length == 0). + * @param length Number of bytes in data. + * + * On success, `buffer->data` is allocated and owned by the caller and must be freed with + * `lantern_rlp_buffer_reset()`. + * + * @return 0 on success. + * @return LANTERN_RLP_ERR_INVALID_PARAM on invalid input. + * @return LANTERN_RLP_ERR_OUT_OF_MEMORY if allocation fails. + * @return LANTERN_RLP_ERR_OVERFLOW if the encoded size would overflow size_t. + * + * @note Thread safety: Caller must ensure exclusive access to buffer. + */ +int lantern_rlp_encode_bytes(struct lantern_rlp_buffer *buffer, const uint8_t *data, size_t length) +{ + int result = LANTERN_RLP_OK; + uint8_t *encoded = NULL; + size_t total_length = 0; + size_t offset = 0; + + if (!buffer || (length > 0 && !data)) + { + return LANTERN_RLP_ERR_INVALID_PARAM; } lantern_rlp_buffer_reset(buffer); - size_t total = rlp_string_encoded_length(data, length); - uint8_t *encoded = malloc(total); - if (!encoded) { - return -1; + if (length == 1 && data[0] <= RLP_PREFIX_SINGLE_BYTE_MAX) + { + encoded = malloc(1); + if (!encoded) + { + return LANTERN_RLP_ERR_OUT_OF_MEMORY; + } + + encoded[0] = data[0]; + buffer->data = encoded; + buffer->length = 1; + return LANTERN_RLP_OK; } - size_t offset = 0; - if (length == 1 && data[0] < 0x80) { - encoded[offset++] = data[0]; - } else if (length < 56) { - encoded[offset++] = (uint8_t)(0x80 + length); - if (length > 0) { - memcpy(encoded + offset, data, length); - } - offset += length; - } else { - int header = write_length(encoded + offset, length, 0x80, 0xB7); - if (header <= 0) { - free(encoded); - return -1; - } - offset += (size_t)header; + if (size_add_would_overflow(rlp_length_prefix_size(length), length, &total_length)) + { + return LANTERN_RLP_ERR_OVERFLOW; + } + + encoded = malloc(total_length); + if (!encoded) + { + return LANTERN_RLP_ERR_OUT_OF_MEMORY; + } + + size_t prefix_written = 0; + result = rlp_write_length_prefix( + encoded, + length, + RLP_PREFIX_SHORT_STRING_BASE, + RLP_PREFIX_LONG_STRING_BASE, + &prefix_written + ); + if (result != LANTERN_RLP_OK) + { + goto cleanup; + } + + offset += prefix_written; + if (length > 0) + { memcpy(encoded + offset, data, length); offset += length; } buffer->data = encoded; buffer->length = offset; - return 0; + encoded = NULL; + +cleanup: + free(encoded); + if (result != LANTERN_RLP_OK) + { + lantern_rlp_buffer_reset(buffer); + } + return result; } -int lantern_rlp_encode_uint64(struct lantern_rlp_buffer *buffer, uint64_t value) { - uint8_t bytes[8]; - size_t length = 0; - if (value == 0) { + +/** + * Encode a uint64 value as an RLP byte string (big-endian, minimal length). + * + * @param buffer Output buffer to receive the encoded item (reset on entry). + * @param value Value to encode. + * + * On success, `buffer->data` is allocated and owned by the caller and must be freed with + * `lantern_rlp_buffer_reset()`. + * + * @return 0 on success. + * @return LANTERN_RLP_ERR_INVALID_PARAM if buffer is NULL. + * @return LANTERN_RLP_ERR_OUT_OF_MEMORY if allocation fails. + * @return LANTERN_RLP_ERR_OVERFLOW if the encoded size would overflow size_t. + * + * @note Thread safety: Caller must ensure exclusive access to buffer. + */ +int lantern_rlp_encode_uint64(struct lantern_rlp_buffer *buffer, uint64_t value) +{ + if (!buffer) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + if (value == 0) + { return lantern_rlp_encode_bytes(buffer, NULL, 0); } - for (size_t i = 0; i < sizeof(bytes); ++i) { - bytes[sizeof(bytes) - 1 - i] = (uint8_t)(value & 0xFF); + uint8_t bytes[sizeof(uint64_t)]; + for (size_t i = 0; i < sizeof(bytes); i++) + { + bytes[sizeof(bytes) - 1 - i] = (uint8_t)(value & 0xFFu); value >>= 8; } - while (length < sizeof(bytes) && bytes[length] == 0) { - ++length; + size_t first_non_zero = 0; + while (first_non_zero < sizeof(bytes) && bytes[first_non_zero] == 0) + { + first_non_zero++; } - const uint8_t *start = bytes + length; - size_t remaining = sizeof(bytes) - length; - return lantern_rlp_encode_bytes(buffer, start, remaining); + return lantern_rlp_encode_bytes(buffer, bytes + first_non_zero, sizeof(bytes) - first_non_zero); } + +/** + * Encode a list of already-RLP-encoded items into an RLP list. + * + * @param buffer Output buffer to receive the encoded list (reset on entry). + * @param items Array of item buffers, each containing a complete RLP item. + * @param item_count Number of items in items. + * + * On success, `buffer->data` is allocated and owned by the caller and must be freed with + * `lantern_rlp_buffer_reset()`. + * + * @return 0 on success. + * @return LANTERN_RLP_ERR_INVALID_PARAM on invalid input. + * @return LANTERN_RLP_ERR_OUT_OF_MEMORY if allocation fails. + * @return LANTERN_RLP_ERR_OVERFLOW if size calculations overflow. + * + * @note Thread safety: Caller must ensure exclusive access to buffer. + */ int lantern_rlp_encode_list( struct lantern_rlp_buffer *buffer, const struct lantern_rlp_buffer *items, - size_t item_count) { - if (!buffer || (!items && item_count > 0)) { - return -1; + size_t item_count) +{ + int result = LANTERN_RLP_OK; + + if (!buffer) + { + return LANTERN_RLP_ERR_INVALID_PARAM; } lantern_rlp_buffer_reset(buffer); + if (!items && item_count > 0) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + size_t payload_length = 0; - for (size_t i = 0; i < item_count; ++i) { - if (items[i].length == 0 || !items[i].data) { - return -1; + for (size_t i = 0; i < item_count; i++) + { + if (!items[i].data || items[i].length == 0) + { + return LANTERN_RLP_ERR_INVALID_PARAM; } - if (size_add_overflow(payload_length, items[i].length, &payload_length)) { - return -1; + if (size_add_would_overflow(payload_length, items[i].length, &payload_length)) + { + return LANTERN_RLP_ERR_OVERFLOW; } } - size_t header_length = rlp_list_header_length(payload_length); - size_t total = 0; - if (size_add_overflow(header_length, payload_length, &total)) { - return -1; + size_t total_length = 0; + if (size_add_would_overflow( + rlp_length_prefix_size(payload_length), + payload_length, + &total_length)) + { + return LANTERN_RLP_ERR_OVERFLOW; } - uint8_t *encoded = malloc(total); - if (!encoded) { - return -1; + uint8_t *encoded = malloc(total_length); + if (!encoded) + { + return LANTERN_RLP_ERR_OUT_OF_MEMORY; } size_t offset = 0; - int header_written = write_length(encoded, payload_length, 0xC0, 0xF7); - if (header_written <= 0) { + size_t header_written = 0; + result = rlp_write_length_prefix( + encoded, + payload_length, + RLP_PREFIX_SHORT_LIST_BASE, + RLP_PREFIX_LONG_LIST_BASE, + &header_written + ); + if (result != LANTERN_RLP_OK) + { free(encoded); - return -1; + return result; } - offset += (size_t)header_written; + offset += header_written; - for (size_t i = 0; i < item_count; ++i) { + for (size_t i = 0; i < item_count; i++) + { memcpy(encoded + offset, items[i].data, items[i].length); offset += items[i].length; } buffer->data = encoded; buffer->length = offset; - return 0; + return LANTERN_RLP_OK; } -static bool cursor_read(const struct lantern_rlp_cursor *cursor, size_t offset, size_t size) { - return offset <= cursor->length && size <= cursor->length - offset; + +/** + * @brief Returns true if cursor can read size bytes from offset. + */ +static bool rlp_cursor_can_read(const struct lantern_rlp_cursor *cursor, size_t offset, size_t size) +{ + if (!cursor) + { + return false; + } + + return (offset <= cursor->length) && (size <= cursor->length - offset); } -static int read_long_length(struct lantern_rlp_cursor *cursor, size_t len_of_len, size_t *out_length) { - if (len_of_len == 0 || len_of_len > sizeof(size_t)) { - return -1; + +/** + * @brief Read an RLP length (big-endian) and advance the cursor. + */ +static int rlp_cursor_read_length( + struct lantern_rlp_cursor *cursor, + size_t len_of_len, + size_t *out_length) +{ + if (!cursor || !out_length) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + if (len_of_len == 0 || len_of_len > sizeof(size_t)) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; } - if (!cursor_read(cursor, cursor->offset, len_of_len)) { - return -1; + + if (!rlp_cursor_can_read(cursor, cursor->offset, len_of_len)) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + if (cursor->data[cursor->offset] == 0) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; } size_t value = 0; - for (size_t i = 0; i < len_of_len; ++i) { + for (size_t i = 0; i < len_of_len; i++) + { uint8_t byte = cursor->data[cursor->offset + i]; - if (value > (SIZE_MAX >> 8)) { - return -1; + if (value > (SIZE_MAX >> 8)) + { + return LANTERN_RLP_ERR_OVERFLOW; } - value = (value << 8) | byte; + value = (value << 8) | (size_t)byte; } cursor->offset += len_of_len; *out_length = value; - return 0; + return LANTERN_RLP_OK; } -static int decode_list_payload( - struct lantern_rlp_cursor *cursor, - size_t payload_length, - struct lantern_rlp_view *view); -static int decode_item(struct lantern_rlp_cursor *cursor, struct lantern_rlp_view *view) { - if (!cursor || !view) { - return -1; +/** + * @brief Ensures a dynamic RLP view array has at least required capacity. + */ +static int rlp_view_list_ensure_capacity( + struct lantern_rlp_view **items, + size_t *capacity, + size_t required) +{ + if (!items || !capacity) + { + return LANTERN_RLP_ERR_INVALID_PARAM; } - lantern_rlp_view_zero(view); - if (cursor->offset >= cursor->length) { - return -1; + if (*capacity >= required) + { + return LANTERN_RLP_OK; } - uint8_t prefix = cursor->data[cursor->offset++]; - if (prefix <= 0x7F) { - view->kind = LANTERN_RLP_KIND_BYTES; - view->data = &cursor->data[cursor->offset - 1]; - view->length = 1; - return 0; + size_t new_capacity = *capacity; + if (new_capacity == 0) + { + new_capacity = RLP_INITIAL_LIST_CAPACITY; } - if (prefix <= 0xB7) { - size_t str_len = (size_t)(prefix - 0x80); - if (!cursor_read(cursor, cursor->offset, str_len)) { - return -1; - } - view->kind = LANTERN_RLP_KIND_BYTES; - view->data = cursor->data + cursor->offset; - view->length = str_len; - cursor->offset += str_len; - return 0; - } - - if (prefix <= 0xBF) { - size_t len_of_len = (size_t)(prefix - 0xB7); - size_t str_len = 0; - if (read_long_length(cursor, len_of_len, &str_len) != 0) { - return -1; - } - if (!cursor_read(cursor, cursor->offset, str_len)) { - return -1; + while (new_capacity < required) + { + size_t grown = new_capacity + (new_capacity / 2); + if (grown <= new_capacity) + { + return LANTERN_RLP_ERR_OVERFLOW; } - view->kind = LANTERN_RLP_KIND_BYTES; - view->data = cursor->data + cursor->offset; - view->length = str_len; - cursor->offset += str_len; - return 0; - } - - if (prefix <= 0xF7) { - size_t payload_length = (size_t)(prefix - 0xC0); - const uint8_t *payload_start = cursor->data + cursor->offset; - if (!cursor_read(cursor, cursor->offset, payload_length)) { - return -1; - } - if (decode_list_payload(cursor, payload_length, view) != 0) { - return -1; - } - view->data = payload_start; - view->length = payload_length; - return 0; + new_capacity = grown; } - size_t len_of_len = (size_t)(prefix - 0xF7); - size_t payload_length = 0; - if (read_long_length(cursor, len_of_len, &payload_length) != 0) { - return -1; + size_t bytes = 0; + if (size_mul_would_overflow(new_capacity, sizeof(**items), &bytes)) + { + return LANTERN_RLP_ERR_OVERFLOW; } + + struct lantern_rlp_view *resized = realloc(*items, bytes); + if (!resized) + { + return LANTERN_RLP_ERR_OUT_OF_MEMORY; + } + + for (size_t i = *capacity; i < new_capacity; i++) + { + rlp_view_zero(&resized[i]); + } + + *items = resized; + *capacity = new_capacity; + return LANTERN_RLP_OK; +} + + +/** + * @brief Decode a single-byte RLP bytes item. + */ +static int decode_single_byte_item(struct lantern_rlp_cursor *cursor, struct lantern_rlp_view *view) +{ + view->kind = LANTERN_RLP_KIND_BYTES; + view->data = &cursor->data[cursor->offset - 1]; + view->length = 1; + return LANTERN_RLP_OK; +} + + +/** + * @brief Decode a short-string RLP bytes item. + */ +static int decode_short_string_item( + struct lantern_rlp_cursor *cursor, + struct lantern_rlp_view *view, + uint8_t prefix) +{ + size_t str_len = (size_t)prefix - (size_t)RLP_PREFIX_SHORT_STRING_BASE; + if (!rlp_cursor_can_read(cursor, cursor->offset, str_len)) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + if (str_len == 1 && cursor->data[cursor->offset] <= RLP_PREFIX_SINGLE_BYTE_MAX) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + view->kind = LANTERN_RLP_KIND_BYTES; + view->data = cursor->data + cursor->offset; + view->length = str_len; + cursor->offset += str_len; + return LANTERN_RLP_OK; +} + + +/** + * @brief Decode a long-string RLP bytes item. + */ +static int decode_long_string_item( + struct lantern_rlp_cursor *cursor, + struct lantern_rlp_view *view, + uint8_t prefix) +{ + size_t len_of_len = (size_t)prefix - (size_t)RLP_PREFIX_LONG_STRING_BASE; + size_t str_len = 0; + int result = rlp_cursor_read_length(cursor, len_of_len, &str_len); + if (result != LANTERN_RLP_OK) + { + return result; + } + + if (str_len <= RLP_SHORT_PAYLOAD_MAX) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + if (!rlp_cursor_can_read(cursor, cursor->offset, str_len)) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + view->kind = LANTERN_RLP_KIND_BYTES; + view->data = cursor->data + cursor->offset; + view->length = str_len; + cursor->offset += str_len; + return LANTERN_RLP_OK; +} + + +/** + * @brief Decode a short-list RLP item. + */ +static int decode_short_list_item( + struct lantern_rlp_cursor *cursor, + struct lantern_rlp_view *view, + uint8_t prefix) +{ + size_t payload_length = (size_t)prefix - (size_t)RLP_PREFIX_SHORT_LIST_BASE; + if (!rlp_cursor_can_read(cursor, cursor->offset, payload_length)) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + const uint8_t *payload_start = cursor->data + cursor->offset; - if (!cursor_read(cursor, cursor->offset, payload_length)) { - return -1; + int result = decode_list_payload(cursor, payload_length, view); + if (result != LANTERN_RLP_OK) + { + return result; } - if (decode_list_payload(cursor, payload_length, view) != 0) { - return -1; + + view->data = payload_start; + view->length = payload_length; + return LANTERN_RLP_OK; +} + + +/** + * @brief Decode a long-list RLP item. + */ +static int decode_long_list_item( + struct lantern_rlp_cursor *cursor, + struct lantern_rlp_view *view, + uint8_t prefix) +{ + size_t len_of_len = (size_t)prefix - (size_t)RLP_PREFIX_LONG_LIST_BASE; + size_t payload_length = 0; + int result = rlp_cursor_read_length(cursor, len_of_len, &payload_length); + if (result != LANTERN_RLP_OK) + { + return result; + } + + if (payload_length <= RLP_SHORT_PAYLOAD_MAX) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; } + + if (!rlp_cursor_can_read(cursor, cursor->offset, payload_length)) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + const uint8_t *payload_start = cursor->data + cursor->offset; + result = decode_list_payload(cursor, payload_length, view); + if (result != LANTERN_RLP_OK) + { + return result; + } + view->data = payload_start; view->length = payload_length; - return 0; + return LANTERN_RLP_OK; +} + + +/** + * @brief Decode an RLP item. + */ +static int decode_item(struct lantern_rlp_cursor *cursor, struct lantern_rlp_view *view) +{ + if (!cursor || !view) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + rlp_view_zero(view); + + if (cursor->offset >= cursor->length) + { + return LANTERN_RLP_ERR_INVALID_ENCODING; + } + + uint8_t prefix = cursor->data[cursor->offset]; + cursor->offset++; + + if (prefix <= RLP_PREFIX_SINGLE_BYTE_MAX) + { + return decode_single_byte_item(cursor, view); + } + + if (prefix <= RLP_PREFIX_SHORT_STRING_MAX) + { + return decode_short_string_item(cursor, view, prefix); + } + + if (prefix <= RLP_PREFIX_LONG_STRING_MAX) + { + return decode_long_string_item(cursor, view, prefix); + } + + if (prefix <= RLP_PREFIX_SHORT_LIST_MAX) + { + return decode_short_list_item(cursor, view, prefix); + } + + return decode_long_list_item(cursor, view, prefix); } + +/** + * @brief Decode the payload of an RLP list. + */ static int decode_list_payload( struct lantern_rlp_cursor *cursor, size_t payload_length, - struct lantern_rlp_view *view) { + struct lantern_rlp_view *view) +{ + if (!cursor || !view) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + struct lantern_rlp_cursor nested = { .data = cursor->data + cursor->offset, .length = payload_length, .offset = 0, }; - size_t capacity = 0; struct lantern_rlp_view *items = NULL; + size_t capacity = 0; size_t count = 0; - while (nested.offset < nested.length) { - if (count == capacity) { - size_t new_capacity = capacity == 0 ? 4 : capacity * 2; - struct lantern_rlp_view *resized = realloc(items, new_capacity * sizeof(*resized)); - if (!resized) { - goto error; - } - for (size_t i = capacity; i < new_capacity; ++i) { - lantern_rlp_view_zero(&resized[i]); - } - items = resized; - capacity = new_capacity; + int result = LANTERN_RLP_OK; + + while (nested.offset < nested.length) + { + result = rlp_view_list_ensure_capacity(&items, &capacity, count + 1); + if (result != LANTERN_RLP_OK) + { + goto cleanup; } - if (decode_item(&nested, &items[count]) != 0) { - goto error; + + result = decode_item(&nested, &items[count]); + if (result != LANTERN_RLP_OK) + { + goto cleanup; } + count++; } - if (nested.offset != nested.length) { - goto error; + if (nested.offset != nested.length) + { + result = LANTERN_RLP_ERR_INVALID_ENCODING; + goto cleanup; } cursor->offset += payload_length; view->kind = LANTERN_RLP_KIND_LIST; view->items = items; view->item_count = count; - return 0; + items = NULL; -error: - if (items) { - for (size_t i = 0; i < count; ++i) { +cleanup: + if (items) + { + for (size_t i = 0; i < count; i++) + { lantern_rlp_view_reset(&items[i]); } } free(items); - return -1; + return result; } -int lantern_rlp_decode(const uint8_t *encoded, size_t encoded_length, struct lantern_rlp_view *out_view) { - if (!encoded || encoded_length == 0 || !out_view) { - return -1; - } - lantern_rlp_view_zero(out_view); +/** + * Decode an RLP item. + * + * @param encoded RLP-encoded data buffer. + * @param encoded_length Length of encoded in bytes. + * @param out_view Output view (reset on entry). + * + * On success, `out_view` may contain dynamically allocated children for list items and must be + * freed with `lantern_rlp_view_reset()`. + * + * @return 0 on success. + * @return LANTERN_RLP_ERR_INVALID_PARAM on invalid input. + * @return LANTERN_RLP_ERR_INVALID_ENCODING if the RLP encoding is malformed or non-canonical. + * @return LANTERN_RLP_ERR_TRAILING_DATA if extra bytes remain after the item. + * + * @note Thread safety: Caller must ensure exclusive access to out_view. + */ +int lantern_rlp_decode( + const uint8_t *encoded, + size_t encoded_length, + struct lantern_rlp_view *out_view) +{ + if (!encoded || encoded_length == 0 || !out_view) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + rlp_view_zero(out_view); struct lantern_rlp_cursor cursor = { .data = encoded, @@ -374,28 +860,62 @@ int lantern_rlp_decode(const uint8_t *encoded, size_t encoded_length, struct lan .offset = 0, }; - if (decode_item(&cursor, out_view) != 0) { + int result = decode_item(&cursor, out_view); + if (result != LANTERN_RLP_OK) + { lantern_rlp_view_reset(out_view); - return -1; + return result; } - if (cursor.offset != cursor.length) { + if (cursor.offset != cursor.length) + { lantern_rlp_view_reset(out_view); - return -1; + return LANTERN_RLP_ERR_TRAILING_DATA; } - return 0; + + return LANTERN_RLP_OK; } -int lantern_rlp_view_as_uint64(const struct lantern_rlp_view *view, uint64_t *out_value) { - if (!view || !out_value || view->kind != LANTERN_RLP_KIND_BYTES || view->length > sizeof(uint64_t) - || (view->length > 0 && !view->data)) { - return -1; + +/** + * Convert an RLP bytes item into a uint64 (big-endian). + * + * @param view RLP view to read (must be a bytes item). + * @param out_value Output decoded value. + * + * @return 0 on success. + * @return LANTERN_RLP_ERR_INVALID_PARAM on invalid input or if the view is not a bytes item. + * + * @note Thread safety: Caller must ensure exclusive access to out_value. + */ +int lantern_rlp_view_as_uint64(const struct lantern_rlp_view *view, uint64_t *out_value) +{ + if (!view || !out_value) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + if (view->kind != LANTERN_RLP_KIND_BYTES) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + if (view->length > sizeof(uint64_t)) + { + return LANTERN_RLP_ERR_INVALID_PARAM; + } + + if (view->length > 0 && !view->data) + { + return LANTERN_RLP_ERR_INVALID_PARAM; } uint64_t value = 0; - for (size_t i = 0; i < view->length; ++i) { - value = (value << 8) | view->data[i]; + for (size_t i = 0; i < view->length; i++) + { + value = (value << 8) | (uint64_t)view->data[i]; } + *out_value = value; - return 0; + return LANTERN_RLP_OK; } diff --git a/src/encoding/snappy.c b/src/encoding/snappy.c index 4c16dc1..854d658 100644 --- a/src/encoding/snappy.c +++ b/src/encoding/snappy.c @@ -1,6 +1,17 @@ +/** + * @file snappy.c + * @brief Snappy compression and decompression helpers (raw and framed streams). + * + * Supports: + * - Raw Snappy blocks (no framing, no CRC) + * - Snappy framed streams (stream identifier + per-chunk CRC32C + chunked compression) + * + * The framed format is commonly used for `/ssz_snappy` request/response payloads. + */ + #include "lantern/encoding/snappy.h" -#include +#include #include #include #include @@ -8,7 +19,8 @@ #include "snappy.h" -enum { +enum +{ LANTERN_SNAPPY_CHUNK_COMPRESSED = 0x00, LANTERN_SNAPPY_CHUNK_UNCOMPRESSED = 0x01, LANTERN_SNAPPY_CHUNK_PADDING_START = 0x02, @@ -18,194 +30,508 @@ enum { LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER = 0xff, }; -enum { +enum +{ LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN = 6, - LANTERN_SNAPPY_STREAM_HEADER_BYTES = 4 + LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN, LANTERN_SNAPPY_CHUNK_HEADER_BYTES = 4, LANTERN_SNAPPY_CHUNK_CRC_BYTES = 4, + LANTERN_SNAPPY_STREAM_HEADER_BYTES = LANTERN_SNAPPY_CHUNK_HEADER_BYTES + + LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN, LANTERN_SNAPPY_MAX_CHUNK_LEN = 0x00ffffffu, }; -static uint32_t lantern_snappy_read_le24(const uint8_t *data) { +static const size_t LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE = 65536u; + +static const uint8_t LANTERN_SNAPPY_STREAM_IDENTIFIER_MAGIC[ + LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN] = { + 's', + 'N', + 'a', + 'P', + 'p', + 'Y', +}; + +/** + * @brief Parsed view of a Snappy framed chunk. + */ +struct lantern_snappy_chunk_view +{ + uint8_t type; /**< Chunk type byte */ + const uint8_t *payload; /**< Chunk payload bytes */ + size_t payload_len; /**< Length of chunk payload in bytes */ +}; + +/** + * @brief Reads a 24-bit little-endian integer. + */ +static uint32_t read_le24(const uint8_t *data) +{ return (uint32_t)data[0] | ((uint32_t)data[1] << 8u) | ((uint32_t)data[2] << 16u); } -static void lantern_snappy_write_le24(uint32_t value, uint8_t *dst) { + +/** + * @brief Writes a 24-bit little-endian integer. + */ +static void write_le24(uint32_t value, uint8_t *dst) +{ dst[0] = (uint8_t)(value & 0xffu); dst[1] = (uint8_t)((value >> 8u) & 0xffu); dst[2] = (uint8_t)((value >> 16u) & 0xffu); } -static void lantern_snappy_write_le32(uint32_t value, uint8_t *dst) { + +/** + * @brief Reads a 32-bit little-endian integer. + */ +static uint32_t read_le32(const uint8_t *data) +{ + return (uint32_t)data[0] + | ((uint32_t)data[1] << 8u) + | ((uint32_t)data[2] << 16u) + | ((uint32_t)data[3] << 24u); +} + + +/** + * @brief Writes a 32-bit little-endian integer. + */ +static void write_le32(uint32_t value, uint8_t *dst) +{ dst[0] = (uint8_t)(value & 0xffu); dst[1] = (uint8_t)((value >> 8u) & 0xffu); dst[2] = (uint8_t)((value >> 16u) & 0xffu); dst[3] = (uint8_t)((value >> 24u) & 0xffu); } -static uint32_t lantern_snappy_crc32c(const uint8_t *data, size_t len) { - const uint32_t poly = 0x82f63b78u; - uint32_t crc = 0xffffffffu; - for (size_t i = 0; i < len; ++i) { - crc ^= data[i]; - for (int bit = 0; bit < 8; ++bit) { - if (crc & 1u) { - crc = (crc >> 1) ^ poly; - } else { - crc >>= 1; + +/** + * @brief Computes CRC32C for a byte slice. + */ +static uint32_t crc32c(const uint8_t *data, size_t len) +{ + const uint32_t poly = UINT32_C(0x82f63b78); + uint32_t crc = UINT32_C(0xffffffff); + + for (size_t i = 0; i < len; ++i) + { + crc ^= (uint32_t)data[i]; + for (size_t bit = 0; bit < 8u; ++bit) + { + if ((crc & 1u) != 0u) + { + crc = (crc >> 1u) ^ poly; + } + else + { + crc >>= 1u; } } } + return ~crc; } -static uint32_t lantern_snappy_mask_crc32c(uint32_t crc) { - return ((crc >> 15u) | (crc << 17u)) + 0xa282ead8u; + +/** + * @brief Applies Snappy's masking to a CRC32C value (framed stream format). + */ +static uint32_t mask_crc32c(uint32_t crc) +{ + uint32_t rotated = (crc >> 15u) | (crc << 17u); + return rotated + UINT32_C(0xa282ead8); } -static int lantern_snappy_framed_uncompressed_length( - const uint8_t *input, - size_t input_len, - size_t *result); -static int lantern_snappy_decompress_framed( +/** + * @brief Returns true if a chunk type is skippable padding. + */ +static bool is_padding_chunk_type(uint8_t chunk_type) +{ + return chunk_type >= (uint8_t)LANTERN_SNAPPY_CHUNK_PADDING_START + && chunk_type <= (uint8_t)LANTERN_SNAPPY_CHUNK_PADDING_END; +} + + +/** + * @brief Parses the next chunk from a framed stream. + * + * @param input Input buffer + * @param input_len Input buffer length in bytes + * @param offset In/out offset into the buffer + * @param chunk Output chunk view + * @param has_chunk Set to true when a chunk is parsed, false at end-of-stream + * + * @return LANTERN_SNAPPY_OK on success + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT if framing is malformed + */ +static int parse_next_chunk( const uint8_t *input, size_t input_len, - uint8_t *output, - size_t output_len, - size_t *written); + size_t *offset, + struct lantern_snappy_chunk_view *chunk, + bool *has_chunk) +{ + if (!input || !offset || !chunk || !has_chunk) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + if (*offset == input_len) + { + *has_chunk = false; + return LANTERN_SNAPPY_OK; + } + + if (*offset > input_len) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (input_len - *offset < (size_t)LANTERN_SNAPPY_CHUNK_HEADER_BYTES) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + chunk->type = input[*offset]; + uint32_t chunk_len_u32 = read_le24(&input[*offset + 1u]); + size_t chunk_len = (size_t)chunk_len_u32; -int lantern_snappy_max_compressed_size(size_t input_len, size_t *max_size) { - if (!max_size) { + *offset += (size_t)LANTERN_SNAPPY_CHUNK_HEADER_BYTES; + if (chunk_len > input_len - *offset) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - size_t raw_required = snappy_max_compressed_length(input_len); - size_t framing_overhead = LANTERN_SNAPPY_STREAM_HEADER_BYTES - + LANTERN_SNAPPY_CHUNK_HEADER_BYTES - + LANTERN_SNAPPY_CHUNK_CRC_BYTES; - if (SIZE_MAX - raw_required < framing_overhead) { + chunk->payload = input + *offset; + chunk->payload_len = chunk_len; + *offset += chunk_len; + + *has_chunk = true; + return LANTERN_SNAPPY_OK; +} + + +/** + * @brief Validates the framed stream identifier chunk. + */ +static int validate_stream_identifier(const struct lantern_snappy_chunk_view *chunk) +{ + if (!chunk) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (chunk->type != (uint8_t)LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (chunk->payload_len != (size_t)LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (memcmp(chunk->payload, LANTERN_SNAPPY_STREAM_IDENTIFIER_MAGIC, chunk->payload_len) != 0) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + return LANTERN_SNAPPY_OK; +} + + +/** + * @brief Computes the worst-case framed output size for an input length. + */ +static int framed_max_compressed_size(size_t input_len, size_t *max_size) +{ + if (!max_size) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - *max_size = raw_required + framing_overhead; + + size_t total = (size_t)LANTERN_SNAPPY_STREAM_HEADER_BYTES; + size_t remaining = input_len; + + do + { + size_t chunk_len = remaining > LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE + ? LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE + : remaining; + + size_t max_chunk_compressed = snappy_max_compressed_length(chunk_len); + size_t chunk_overhead = (size_t)LANTERN_SNAPPY_CHUNK_HEADER_BYTES + + (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES; + + if (SIZE_MAX - total < chunk_overhead) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + total += chunk_overhead; + + if (SIZE_MAX - total < max_chunk_compressed) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + total += max_chunk_compressed; + + remaining -= chunk_len; + } while (remaining > 0); + + *max_size = total; return LANTERN_SNAPPY_OK; } -int lantern_snappy_max_compressed_size_raw(size_t input_len, size_t *max_size) { - if (!max_size) { + +/** + * @brief Computes the uncompressed length of a framed Snappy stream. + */ +static int framed_uncompressed_length(const uint8_t *input, size_t input_len, size_t *result); + +/** + * @brief Decompresses a framed Snappy stream into an output buffer. + */ +static int decompress_framed( + const uint8_t *input, + size_t input_len, + uint8_t *output, + size_t output_len, + size_t *written); + + +/** + * Computes the maximum size required to compress a payload using the framed + * Snappy stream format. + * + * @param input_len Length of input data in bytes. + * @param max_size Output pointer receiving the maximum required size in bytes. + * + * @return LANTERN_SNAPPY_OK on success. + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT if `max_size` is NULL or on overflow. + * + * @note Thread safety: This function is thread-safe. + */ +int lantern_snappy_max_compressed_size(size_t input_len, size_t *max_size) +{ + return framed_max_compressed_size(input_len, max_size); +} + + +/** + * Computes the maximum size required to compress a payload using raw (unframed) + * Snappy. + * + * @param input_len Length of input data in bytes. + * @param max_size Output pointer receiving the maximum required size in bytes. + * + * @return LANTERN_SNAPPY_OK on success. + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT if `max_size` is NULL. + * + * @note Thread safety: This function is thread-safe. + */ +int lantern_snappy_max_compressed_size_raw(size_t input_len, size_t *max_size) +{ + if (!max_size) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } + *max_size = snappy_max_compressed_length(input_len); return LANTERN_SNAPPY_OK; } + +/** + * Compresses input bytes using the framed Snappy stream format. + * + * Produces a stream identifier chunk followed by one or more data chunks. Each + * data chunk contains a CRC32C (masked) of the uncompressed bytes for that chunk. + * + * @param input Input data to compress. + * @param input_len Input length in bytes. + * @param output Output buffer for framed data. + * @param output_len Output buffer capacity in bytes. + * @param written Output pointer receiving bytes written (or required size on + * LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL). + * + * @return LANTERN_SNAPPY_OK on success. + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT on invalid arguments or overflow. + * @return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL if `output_len` is too small. + * @return LANTERN_SNAPPY_ERROR_UNSUPPORTED if the Snappy backend cannot initialize. + * + * @note Thread safety: This function is thread-safe. + */ int lantern_snappy_compress( const uint8_t *input, size_t input_len, uint8_t *output, size_t output_len, - size_t *written) { - if (!input || !output || !written) { + size_t *written) +{ + if (!input || !output || !written) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - size_t raw_required = snappy_max_compressed_length(input_len); - size_t framing_overhead = LANTERN_SNAPPY_STREAM_HEADER_BYTES - + LANTERN_SNAPPY_CHUNK_HEADER_BYTES - + LANTERN_SNAPPY_CHUNK_CRC_BYTES; - if (SIZE_MAX - raw_required < framing_overhead) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + size_t required = 0; + int result = framed_max_compressed_size(input_len, &required); + if (result != LANTERN_SNAPPY_OK) + { + return result; } - size_t required = raw_required + framing_overhead; - if (output_len < required) { + + if (output_len < required) + { *written = required; return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; } uint8_t *cursor = output; - cursor[0] = LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER; - lantern_snappy_write_le24(LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN, &cursor[1]); - memcpy(cursor + LANTERN_SNAPPY_CHUNK_HEADER_BYTES, "sNaPpY", LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN); - cursor += LANTERN_SNAPPY_STREAM_HEADER_BYTES; - - uint8_t *chunk = cursor; - chunk[0] = LANTERN_SNAPPY_CHUNK_COMPRESSED; - uint8_t *crc_ptr = chunk + LANTERN_SNAPPY_CHUNK_HEADER_BYTES; - uint8_t *payload = crc_ptr + LANTERN_SNAPPY_CHUNK_CRC_BYTES; - - size_t payload_offset = (size_t)(payload - output); - if (payload_offset > output_len) { - *written = 0; - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } - size_t payload_capacity = output_len - payload_offset; + + cursor[0] = (uint8_t)LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER; + write_le24((uint32_t)LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN, cursor + 1u); + memcpy( + cursor + (size_t)LANTERN_SNAPPY_CHUNK_HEADER_BYTES, + LANTERN_SNAPPY_STREAM_IDENTIFIER_MAGIC, + (size_t)LANTERN_SNAPPY_STREAM_IDENTIFIER_LEN); + cursor += (size_t)LANTERN_SNAPPY_STREAM_HEADER_BYTES; struct snappy_env env; - if (snappy_init_env(&env) != 0) { + bool env_initialized = false; + + if (snappy_init_env(&env) != 0) + { return LANTERN_SNAPPY_ERROR_UNSUPPORTED; } + env_initialized = true; + + const uint8_t *input_cursor = input; + size_t remaining = input_len; + + do + { + size_t chunk_input_len = remaining > LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE + ? LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE + : remaining; + + cursor[0] = (uint8_t)LANTERN_SNAPPY_CHUNK_COMPRESSED; + uint8_t *chunk_len_bytes = cursor + 1u; + uint8_t *crc_ptr = cursor + (size_t)LANTERN_SNAPPY_CHUNK_HEADER_BYTES; + uint8_t *payload = crc_ptr + (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES; + + size_t max_chunk_compressed = snappy_max_compressed_length(chunk_input_len); + size_t payload_offset = (size_t)(payload - output); + if (payload_offset > output_len || output_len - payload_offset < max_chunk_compressed) + { + result = LANTERN_SNAPPY_ERROR_INVALID_INPUT; + goto cleanup; + } - size_t compressed_len = payload_capacity; - int rc = snappy_compress( - &env, - (const char *)input, - input_len, - (char *)payload, - &compressed_len); - snappy_free_env(&env); - if (rc != 0 || compressed_len > raw_required) { - *written = 0; - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } + size_t compressed_len = max_chunk_compressed; + int snappy_rc = snappy_compress( + &env, + (const char *)input_cursor, + chunk_input_len, + (char *)payload, + &compressed_len); + if (snappy_rc != 0) + { + result = LANTERN_SNAPPY_ERROR_INVALID_INPUT; + goto cleanup; + } + + uint32_t crc = mask_crc32c(crc32c(input_cursor, chunk_input_len)); + write_le32(crc, crc_ptr); + + size_t chunk_payload_len = (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES + compressed_len; + if (chunk_payload_len > (size_t)LANTERN_SNAPPY_MAX_CHUNK_LEN) + { + result = LANTERN_SNAPPY_ERROR_INVALID_INPUT; + goto cleanup; + } - uint32_t crc = lantern_snappy_mask_crc32c(lantern_snappy_crc32c(input, input_len)); - lantern_snappy_write_le32(crc, crc_ptr); + write_le24((uint32_t)chunk_payload_len, chunk_len_bytes); + cursor += (size_t)LANTERN_SNAPPY_CHUNK_HEADER_BYTES + chunk_payload_len; - size_t chunk_len = LANTERN_SNAPPY_CHUNK_CRC_BYTES + compressed_len; - if (chunk_len > LANTERN_SNAPPY_MAX_CHUNK_LEN) { + input_cursor += chunk_input_len; + remaining -= chunk_input_len; + } while (remaining > 0); + + *written = (size_t)(cursor - output); + result = LANTERN_SNAPPY_OK; + +cleanup: + if (env_initialized) + { + snappy_free_env(&env); + } + + if (result != LANTERN_SNAPPY_OK) + { *written = 0; - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - lantern_snappy_write_le24((uint32_t)chunk_len, &chunk[1]); - cursor += LANTERN_SNAPPY_CHUNK_HEADER_BYTES + chunk_len; - *written = (size_t)(cursor - output); - return LANTERN_SNAPPY_OK; + return result; } + +/** + * Compresses input bytes using raw (unframed) Snappy. + * + * @param input Input data to compress. + * @param input_len Input length in bytes. + * @param output Output buffer for raw Snappy data. + * @param output_len Output buffer capacity in bytes. + * @param written Output pointer receiving bytes written (or required size on + * LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL). + * + * @return LANTERN_SNAPPY_OK on success. + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT on invalid arguments. + * @return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL if `output_len` is too small. + * @return LANTERN_SNAPPY_ERROR_UNSUPPORTED if the Snappy backend cannot initialize. + * + * @note Thread safety: This function is thread-safe. + */ int lantern_snappy_compress_raw( const uint8_t *input, size_t input_len, uint8_t *output, size_t output_len, - size_t *written) { - if (!input || !output || !written) { + size_t *written) +{ + if (!input || !output || !written) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } size_t required = snappy_max_compressed_length(input_len); - if (output_len < required) { + if (output_len < required) + { *written = required; return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; } struct snappy_env env; - if (snappy_init_env(&env) != 0) { + if (snappy_init_env(&env) != 0) + { return LANTERN_SNAPPY_ERROR_UNSUPPORTED; } size_t compressed_len = output_len; - int rc = snappy_compress( + int snappy_rc = snappy_compress( &env, (const char *)input, input_len, (char *)output, &compressed_len); snappy_free_env(&env); - if (rc != 0) { + + if (snappy_rc != 0) + { *written = 0; return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } @@ -214,22 +540,36 @@ int lantern_snappy_compress_raw( return LANTERN_SNAPPY_OK; } -int lantern_snappy_uncompressed_length( - const uint8_t *input, - size_t input_len, - size_t *result) { - if (!input || !result) { + +/** + * Computes the uncompressed length of either a framed or raw Snappy input. + * + * @param input Input buffer (framed or raw Snappy). + * @param input_len Input length in bytes. + * @param result Output pointer receiving the uncompressed length in bytes. + * + * @return LANTERN_SNAPPY_OK on success. + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT on invalid arguments or malformed data. + * + * @note Thread safety: This function is thread-safe. + */ +int lantern_snappy_uncompressed_length(const uint8_t *input, size_t input_len, size_t *result) +{ + if (!input || !result) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } size_t framed_length = 0; - if (lantern_snappy_framed_uncompressed_length(input, input_len, &framed_length) == LANTERN_SNAPPY_OK) { + if (framed_uncompressed_length(input, input_len, &framed_length) == LANTERN_SNAPPY_OK) + { *result = framed_length; return LANTERN_SNAPPY_OK; } size_t raw_length = 0; - if (snappy_uncompressed_length((const char *)input, input_len, &raw_length)) { + if (snappy_uncompressed_length((const char *)input, input_len, &raw_length)) + { *result = raw_length; return LANTERN_SNAPPY_OK; } @@ -237,171 +577,312 @@ int lantern_snappy_uncompressed_length( return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } + +/** + * Decompresses either framed or raw Snappy input into an output buffer. + * + * @param input Input buffer (framed or raw Snappy). + * @param input_len Input length in bytes. + * @param output Output buffer for uncompressed bytes. + * @param output_len Output buffer capacity in bytes. + * @param written Output pointer receiving bytes written (or required size on + * LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL). + * + * @return LANTERN_SNAPPY_OK on success. + * @return LANTERN_SNAPPY_ERROR_INVALID_INPUT on invalid arguments or malformed data. + * @return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL if `output_len` is too small. + * + * @note Thread safety: This function is thread-safe. + */ int lantern_snappy_decompress( const uint8_t *input, size_t input_len, uint8_t *output, size_t output_len, - size_t *written) { - if (!input || !output || !written) { + size_t *written) +{ + if (!input || !output || !written) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } size_t expected = 0; - if (lantern_snappy_framed_uncompressed_length(input, input_len, &expected) == LANTERN_SNAPPY_OK) { - if (output_len < expected) { + if (framed_uncompressed_length(input, input_len, &expected) == LANTERN_SNAPPY_OK) + { + if (output_len < expected) + { *written = expected; return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; } - return lantern_snappy_decompress_framed(input, input_len, output, output_len, written); + + return decompress_framed(input, input_len, output, output_len, written); } - if (!snappy_uncompressed_length((const char *)input, input_len, &expected)) { + if (!snappy_uncompressed_length((const char *)input, input_len, &expected)) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - if (output_len < expected) { + + if (output_len < expected) + { *written = expected; return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; } - int rc = snappy_uncompress((const char *)input, input_len, (char *)output); - if (rc != 0) { + + if (snappy_uncompress((const char *)input, input_len, (char *)output) != 0) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } + *written = expected; return LANTERN_SNAPPY_OK; } -static int lantern_snappy_framed_uncompressed_length( - const uint8_t *input, - size_t input_len, - size_t *result) { - size_t pos = 0; + +static int framed_uncompressed_length(const uint8_t *input, size_t input_len, size_t *result) +{ + if (!input || !result) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + size_t offset = 0; size_t total = 0; - while (pos + 4 <= input_len) { - uint8_t chunk_type = input[pos]; - uint32_t chunk_len = lantern_snappy_read_le24(&input[pos + 1]); - pos += 4; - if (chunk_len > input_len - pos) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + struct lantern_snappy_chunk_view chunk = {0}; + bool has_chunk = false; + + int rc = parse_next_chunk(input, input_len, &offset, &chunk, &has_chunk); + if (rc != LANTERN_SNAPPY_OK || !has_chunk) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (validate_stream_identifier(&chunk) != LANTERN_SNAPPY_OK) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + while (true) + { + rc = parse_next_chunk(input, input_len, &offset, &chunk, &has_chunk); + if (rc != LANTERN_SNAPPY_OK) + { + return rc; + } + if (!has_chunk) + { + break; } - const uint8_t *chunk = &input[pos]; - pos += chunk_len; - if (chunk_type == LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER) { - if (chunk_len != 6 || memcmp(chunk, "sNaPpY", 6) != 0) { + if (chunk.type == (uint8_t)LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER) + { + if (validate_stream_identifier(&chunk) != LANTERN_SNAPPY_OK) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } continue; } - if (chunk_type == LANTERN_SNAPPY_CHUNK_COMPRESSED || chunk_type == LANTERN_SNAPPY_CHUNK_UNCOMPRESSED) { - if (chunk_len < 4) { + if (is_padding_chunk_type(chunk.type)) + { + continue; + } + + if (chunk.type != (uint8_t)LANTERN_SNAPPY_CHUNK_COMPRESSED + && chunk.type != (uint8_t)LANTERN_SNAPPY_CHUNK_UNCOMPRESSED) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (chunk.payload_len < (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + const uint8_t *payload = chunk.payload + (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES; + size_t payload_len = chunk.payload_len - (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES; + + if (chunk.type == (uint8_t)LANTERN_SNAPPY_CHUNK_COMPRESSED) + { + size_t chunk_expected = 0; + if (!snappy_uncompressed_length((const char *)payload, payload_len, &chunk_expected)) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - size_t payload_len = chunk_len - 4; - const uint8_t *payload = chunk + 4; - if (chunk_type == LANTERN_SNAPPY_CHUNK_COMPRESSED) { - size_t chunk_expected = 0; - if (!snappy_uncompressed_length((const char *)payload, payload_len, &chunk_expected)) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } - if (SIZE_MAX - total < chunk_expected) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } - total += chunk_expected; - } else { - if (SIZE_MAX - total < payload_len) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } - total += payload_len; + + if (chunk_expected > LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (SIZE_MAX - total < chunk_expected) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } + total += chunk_expected; continue; } - if (chunk_type >= LANTERN_SNAPPY_CHUNK_PADDING_START && chunk_type <= LANTERN_SNAPPY_CHUNK_PADDING_END) { - continue; + if (payload_len > LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } + uint32_t expected_crc = read_le32(chunk.payload); + uint32_t computed_crc = mask_crc32c(crc32c(payload, payload_len)); + if (expected_crc != computed_crc) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } - if (pos != input_len) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + if (SIZE_MAX - total < payload_len) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + total += payload_len; } - if (result) { - *result = total; - } + *result = total; return LANTERN_SNAPPY_OK; } -static int lantern_snappy_decompress_framed( + +static int decompress_framed( const uint8_t *input, size_t input_len, uint8_t *output, size_t output_len, - size_t *written) { - size_t pos = 0; + size_t *written) +{ + if (!input || !output || !written) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + size_t offset = 0; size_t produced = 0; - while (pos + 4 <= input_len) { - uint8_t chunk_type = input[pos]; - uint32_t chunk_len = lantern_snappy_read_le24(&input[pos + 1]); - pos += 4; - if (chunk_len > input_len - pos) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + struct lantern_snappy_chunk_view chunk = {0}; + bool has_chunk = false; + + int rc = parse_next_chunk(input, input_len, &offset, &chunk, &has_chunk); + if (rc != LANTERN_SNAPPY_OK || !has_chunk) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (validate_stream_identifier(&chunk) != LANTERN_SNAPPY_OK) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + while (true) + { + rc = parse_next_chunk(input, input_len, &offset, &chunk, &has_chunk); + if (rc != LANTERN_SNAPPY_OK) + { + return rc; + } + if (!has_chunk) + { + break; } - const uint8_t *chunk = &input[pos]; - pos += chunk_len; - if (chunk_type == LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER) { - if (chunk_len != 6 || memcmp(chunk, "sNaPpY", 6) != 0) { + if (chunk.type == (uint8_t)LANTERN_SNAPPY_CHUNK_STREAM_IDENTIFIER) + { + if (validate_stream_identifier(&chunk) != LANTERN_SNAPPY_OK) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } continue; } - if (chunk_type == LANTERN_SNAPPY_CHUNK_COMPRESSED || chunk_type == LANTERN_SNAPPY_CHUNK_UNCOMPRESSED) { - if (chunk_len < 4) { + if (is_padding_chunk_type(chunk.type)) + { + continue; + } + + if (chunk.type != (uint8_t)LANTERN_SNAPPY_CHUNK_COMPRESSED + && chunk.type != (uint8_t)LANTERN_SNAPPY_CHUNK_UNCOMPRESSED) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (chunk.payload_len < (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + uint32_t expected_crc = read_le32(chunk.payload); + const uint8_t *payload = chunk.payload + (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES; + size_t payload_len = chunk.payload_len - (size_t)LANTERN_SNAPPY_CHUNK_CRC_BYTES; + + if (chunk.type == (uint8_t)LANTERN_SNAPPY_CHUNK_COMPRESSED) + { + size_t chunk_expected = 0; + if (!snappy_uncompressed_length((const char *)payload, payload_len, &chunk_expected)) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - const uint8_t *payload = chunk + 4; - size_t payload_len = chunk_len - 4; - if (chunk_type == LANTERN_SNAPPY_CHUNK_COMPRESSED) { - size_t chunk_expected = 0; - if (!snappy_uncompressed_length((const char *)payload, payload_len, &chunk_expected)) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } - if (produced > output_len || chunk_expected > output_len - produced) { - *written = produced + chunk_expected; - return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; - } - if (snappy_uncompress((const char *)payload, payload_len, (char *)output + produced) != 0) { + if (chunk_expected > LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + if (produced > output_len || chunk_expected > output_len - produced) + { + if (produced > SIZE_MAX - chunk_expected) + { return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - produced += chunk_expected; - } else { - if (produced > output_len || payload_len > output_len - produced) { - *written = produced + payload_len; - return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; - } - memcpy(output + produced, payload, payload_len); - produced += payload_len; + *written = produced + chunk_expected; + return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; + } + + if (snappy_uncompress( + (const char *)payload, + payload_len, + (char *)output + produced) + != 0) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + uint32_t computed_crc = mask_crc32c(crc32c(output + produced, chunk_expected)); + if (expected_crc != computed_crc) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } + + produced += chunk_expected; continue; } - if (chunk_type >= LANTERN_SNAPPY_CHUNK_PADDING_START && chunk_type <= LANTERN_SNAPPY_CHUNK_PADDING_END) { - continue; + if (payload_len > LANTERN_SNAPPY_MAX_UNCOMPRESSED_CHUNK_SIZE) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; } - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; - } + if (produced > output_len || payload_len > output_len - produced) + { + if (produced > SIZE_MAX - payload_len) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + *written = produced + payload_len; + return LANTERN_SNAPPY_ERROR_BUFFER_TOO_SMALL; + } - if (pos != input_len) { - return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + uint32_t computed_crc = mask_crc32c(crc32c(payload, payload_len)); + if (expected_crc != computed_crc) + { + return LANTERN_SNAPPY_ERROR_INVALID_INPUT; + } + + memcpy(output + produced, payload, payload_len); + produced += payload_len; } *written = produced; diff --git a/src/genesis/genesis.c b/src/genesis/genesis.c index 2a738c6..211188a 100644 --- a/src/genesis/genesis.c +++ b/src/genesis/genesis.c @@ -1,1192 +1,235 @@ -#include "lantern/genesis/genesis.h" +/** + * @file genesis.c + * @brief Public API for loading genesis artifacts. + * + * Implements lifecycle helpers for `struct lantern_genesis_artifacts` and the + * top-level `lantern_genesis_load()` entry point. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + */ -#include "lantern/support/log.h" -#include "lantern/support/strings.h" -#include "lantern/support/secure_mem.h" -#include "lantern/networking/libp2p.h" -#include "internal/yaml_parser.h" -#include "peer_id/peer_id.h" +#include "lantern/genesis/genesis.h" -#include -#include -#include #include -#include #include #include -static void free_validator_registry(struct lantern_validator_registry *registry); -static void free_validator_config(struct lantern_validator_config *config); -static void free_validator_config_entry(struct lantern_validator_config_entry *entry); - -static int parse_chain_config(const char *path, struct lantern_chain_config *config); -static int parse_validator_registry(const char *path, struct lantern_validator_registry *registry); -static int parse_validator_registry_mapping(const char *path, struct lantern_validator_registry *registry); -static int parse_validator_config(const char *path, struct lantern_validator_config *config); -static int parse_nodes_file(const char *path, struct lantern_enr_record_list *list); -static int read_state_blob(const char *path, uint8_t **bytes, size_t *size); -static int parse_genesis_validator_pubkeys(const char *path, uint8_t **out_pubkeys, size_t *out_count); -static void merge_chain_pubkeys_into_registry( - const struct lantern_chain_config *config, - struct lantern_validator_registry *registry); - -static uint64_t parse_u64(const char *value, int *ok); -static char *dup_trimmed(const char *value); -static const char *yaml_object_value(const LanternYamlObject *object, const char *key); -static int read_scalar_value(const char *path, const char *key, char **out_value); -static enum lantern_validator_client_kind classify_validator_client(const char *name); -static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id); -static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]); -static int set_record_pubkey(struct lantern_validator_record *record); -static char *trim_whitespace(char *value); +#include "genesis_internal.h" +#include "lantern/support/log.h" -void lantern_genesis_artifacts_init(struct lantern_genesis_artifacts *artifacts) { - if (!artifacts) { +static int load_chain_config_and_pubkeys( + struct lantern_chain_config *config, + const char *config_path); + + +/** + * Initialize a genesis artifacts container. + * + * @spec Lantern genesis artifact loader. + * + * @param artifacts Artifacts container to initialize (caller-owned). + * + * @note Thread safety: Caller must ensure exclusive access to `artifacts`. + */ +void lantern_genesis_artifacts_init(struct lantern_genesis_artifacts *artifacts) +{ + if (!artifacts) + { return; } - memset(&artifacts->chain_config, 0, sizeof(artifacts->chain_config)); + + memset(artifacts, 0, sizeof(*artifacts)); lantern_enr_record_list_init(&artifacts->enrs); - artifacts->validator_registry.records = NULL; - artifacts->validator_registry.count = 0; - artifacts->validator_config.shuffle = NULL; - artifacts->validator_config.entries = NULL; - artifacts->validator_config.count = 0; - artifacts->state_bytes = NULL; - artifacts->state_size = 0; } -void lantern_genesis_artifacts_reset(struct lantern_genesis_artifacts *artifacts) { - if (!artifacts) { + +/** + * Reset a genesis artifacts container and free any owned memory. + * + * @spec Lantern genesis artifact loader. + * + * @param artifacts Artifacts container to reset. Safe to call with NULL. + * + * @note Thread safety: Caller must ensure exclusive access to `artifacts`. + */ +void lantern_genesis_artifacts_reset(struct lantern_genesis_artifacts *artifacts) +{ + if (!artifacts) + { return; } + lantern_enr_record_list_reset(&artifacts->enrs); - free_validator_registry(&artifacts->validator_registry); - free_validator_config(&artifacts->validator_config); + genesis_free_validator_registry(&artifacts->validator_registry); + genesis_free_validator_config(&artifacts->validator_config); + free(artifacts->state_bytes); artifacts->state_bytes = NULL; artifacts->state_size = 0; - artifacts->chain_config.genesis_time = 0; - artifacts->chain_config.validator_count = 0; - if (artifacts->chain_config.validator_pubkeys) { - free(artifacts->chain_config.validator_pubkeys); - artifacts->chain_config.validator_pubkeys = NULL; - } - artifacts->chain_config.validator_pubkeys_count = 0; + + free(artifacts->chain_config.validator_pubkeys); + memset(&artifacts->chain_config, 0, sizeof(artifacts->chain_config)); } -int lantern_genesis_load(struct lantern_genesis_artifacts *artifacts, const struct lantern_genesis_paths *paths) { - if (!artifacts || !paths) { - return -1; - } - if (!paths->config_path || !paths->validator_registry_path || !paths->nodes_path || !paths->state_path - || !paths->validator_config_path) { +/** + * Load genesis artifacts from disk. + * + * Populates `artifacts` by parsing the provided YAML/SSZ files. On success, the + * caller owns the returned buffers via `artifacts` and must call + * `lantern_genesis_artifacts_reset()` to free them. + * + * @spec Lantern genesis artifact loader. + * + * @param artifacts Output artifacts container (must be initialized). + * @param paths Paths to genesis artifact files. + * + * @return LANTERN_GENESIS_OK on success + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on NULL inputs or missing required paths + * @return LANTERN_GENESIS_ERR_IO on file I/O failures + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure + * @return LANTERN_GENESIS_ERR_INVALID_DATA on parse/validation failures + * + * @note Thread safety: Caller must ensure exclusive access to `artifacts`. + */ +int lantern_genesis_load( + struct lantern_genesis_artifacts *artifacts, + const struct lantern_genesis_paths *paths) +{ + if (!artifacts || !paths) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (!paths->config_path + || !paths->validator_registry_path + || !paths->nodes_path + || !paths->state_path + || !paths->validator_config_path) + { lantern_log_error("genesis", NULL, "missing required genesis path"); - return -1; + return LANTERN_GENESIS_ERR_INVALID_PARAM; } lantern_genesis_artifacts_reset(artifacts); - lantern_genesis_artifacts_init(artifacts); - if (parse_chain_config(paths->config_path, &artifacts->chain_config) != 0) { - lantern_log_error("genesis", NULL, "failed to parse chain config at %s", paths->config_path); + int result = load_chain_config_and_pubkeys(&artifacts->chain_config, paths->config_path); + if (result != LANTERN_GENESIS_OK) + { goto error; } - if (!artifacts->chain_config.validator_pubkeys || artifacts->chain_config.validator_pubkeys_count == 0) { - uint8_t *pubkeys = NULL; - size_t pubkey_count = 0; - if (parse_genesis_validator_pubkeys(paths->config_path, &pubkeys, &pubkey_count) == 0 - && pubkeys && pubkey_count > 0) { - artifacts->chain_config.validator_pubkeys = pubkeys; - artifacts->chain_config.validator_pubkeys_count = pubkey_count; - if (artifacts->chain_config.validator_count == 0) { - artifacts->chain_config.validator_count = pubkey_count; - } - lantern_log_info("genesis", NULL, "loaded %zu genesis pubkeys from %s", pubkey_count, paths->config_path); - } else { - free(pubkeys); - lantern_log_warn("genesis", NULL, "no genesis pubkeys found in %s", paths->config_path); - } - } - - if (parse_validator_registry(paths->validator_registry_path, &artifacts->validator_registry) != 0) { - lantern_log_error("genesis", NULL, "failed to parse validator registry at %s", paths->validator_registry_path); + result = genesis_parse_validator_registry( + paths->validator_registry_path, + &artifacts->validator_registry); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error( + "genesis", + NULL, + "failed to parse validator registry at %s", + paths->validator_registry_path); goto error; } - /* If validators.yaml only lists indices (lean quickstart), hydrate pubkeys from config.yaml */ - merge_chain_pubkeys_into_registry(&artifacts->chain_config, &artifacts->validator_registry); + genesis_merge_chain_pubkeys_into_registry( + &artifacts->chain_config, + &artifacts->validator_registry); - if (parse_nodes_file(paths->nodes_path, &artifacts->enrs) != 0) { + result = genesis_parse_nodes_file(paths->nodes_path, &artifacts->enrs); + if (result != LANTERN_GENESIS_OK) + { lantern_log_error("genesis", NULL, "failed to parse nodes at %s", paths->nodes_path); goto error; } - if (parse_validator_config(paths->validator_config_path, &artifacts->validator_config) != 0) { - lantern_log_error("genesis", NULL, "failed to parse validator-config at %s", paths->validator_config_path); + result = genesis_parse_validator_config( + paths->validator_config_path, + &artifacts->validator_config); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error( + "genesis", + NULL, + "failed to parse validator-config at %s", + paths->validator_config_path); goto error; } - if (read_state_blob(paths->state_path, &artifacts->state_bytes, &artifacts->state_size) != 0) { - lantern_log_error("genesis", NULL, "failed to read genesis state at %s", paths->state_path); + result = genesis_read_state_blob( + paths->state_path, + &artifacts->state_bytes, + &artifacts->state_size); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error( + "genesis", + NULL, + "failed to read genesis state at %s", + paths->state_path); goto error; } - return 0; + return LANTERN_GENESIS_OK; error: lantern_genesis_artifacts_reset(artifacts); - return -1; -} - -struct lantern_validator_config_entry *lantern_validator_config_find( - struct lantern_validator_config *config, - const char *name) { - if (!config || !name) { - return NULL; - } - for (size_t i = 0; i < config->count; ++i) { - if (config->entries[i].name && strcmp(config->entries[i].name, name) == 0) { - return &config->entries[i]; - } - } - return NULL; -} - -int lantern_validator_config_assign_ranges( - struct lantern_validator_config *config, - uint64_t validator_count) { - if (!config || !config->entries || config->count == 0) { - return -1; - } - uint64_t next_index = 0; - for (size_t i = 0; i < config->count; ++i) { - struct lantern_validator_config_entry *entry = &config->entries[i]; - entry->start_index = next_index; - uint64_t end = next_index + entry->count; - if (end > validator_count) { - return -1; - } - entry->end_index = end; - entry->has_range = true; - next_index = end; - } - if (next_index != validator_count) { - return -1; - } - return 0; -} - -static int compare_u64(const void *lhs, const void *rhs) { - const uint64_t *a = lhs; - const uint64_t *b = rhs; - if (*a < *b) { - return -1; - } - if (*a > *b) { - return 1; - } - return 0; -} - -static int append_assignment_index(struct lantern_validator_config_entry *entry, uint64_t index) { - if (!entry) { - return -1; - } - for (size_t i = 0; i < entry->indices_len; ++i) { - if (entry->indices[i] == index) { - return -1; - } - } - if (entry->indices_len == entry->indices_cap) { - size_t new_cap = entry->indices_cap == 0 ? 4 : entry->indices_cap * 2; - uint64_t *grown = realloc(entry->indices, new_cap * sizeof(*grown)); - if (!grown) { - return -1; - } - entry->indices = grown; - entry->indices_cap = new_cap; - } - entry->indices[entry->indices_len++] = index; - return 0; -} - -int lantern_validator_config_apply_assignments( - struct lantern_validator_config *config, - const char *path, - uint64_t validator_count) { - if (!config || !config->entries || config->count == 0 || !path) { - return -1; - } - FILE *fp = fopen(path, "r"); - if (!fp) { - return -1; - } - - bool *assigned = NULL; - if (validator_count > 0) { - assigned = calloc((size_t)validator_count, sizeof(*assigned)); - if (!assigned) { - fclose(fp); - return -1; - } - } - - bool saw_mapping = false; - size_t assigned_total = 0; - struct lantern_validator_config_entry *current = NULL; - - char line[2048]; - while (fgets(line, sizeof(line), fp)) { - char *trimmed = trim_whitespace(line); - if (!trimmed || *trimmed == '\0' || *trimmed == '#') { - continue; - } - if (*trimmed == '-') { - if (!current || !assigned) { - continue; - } - char *value = trimmed + 1; - value = trim_whitespace(value); - if (!value || *value == '\0') { - continue; - } - char *endptr = NULL; - unsigned long long parsed = strtoull(value, &endptr, 10); - if (endptr == value) { - continue; - } - if (parsed >= validator_count) { - free(assigned); - fclose(fp); - return -1; - } - if (assigned[(size_t)parsed]) { - free(assigned); - fclose(fp); - return -1; - } - if (append_assignment_index(current, (uint64_t)parsed) != 0) { - free(assigned); - fclose(fp); - return -1; - } - assigned[(size_t)parsed] = true; - assigned_total++; - continue; - } - char *colon = strchr(trimmed, ':'); - if (!colon) { - current = NULL; - continue; - } - bool has_value = false; - for (char *p = colon + 1; *p; ++p) { - if (!isspace((unsigned char)*p)) { - has_value = true; - break; - } - } - if (has_value) { - current = NULL; - continue; - } - *colon = '\0'; - char *name = trim_whitespace(trimmed); - if (!name || *name == '\0') { - current = NULL; - continue; - } - size_t name_len = strlen(name); - if (name_len >= 2 && ((name[0] == '"' && name[name_len - 1] == '"') - || (name[0] == '\'' && name[name_len - 1] == '\''))) { - name[name_len - 1] = '\0'; - ++name; - } - struct lantern_validator_config_entry *entry = lantern_validator_config_find(config, name); - current = entry; - if (entry) { - saw_mapping = true; - entry->indices_len = 0; - } - } - - fclose(fp); - - if (!saw_mapping) { - free(assigned); - return 0; - } - if (assigned_total != validator_count) { - free(assigned); - return -1; - } - - for (size_t i = 0; i < config->count; ++i) { - struct lantern_validator_config_entry *entry = &config->entries[i]; - if (entry->indices_len != entry->count || entry->indices_len == 0) { - free(assigned); - return -1; - } - qsort(entry->indices, entry->indices_len, sizeof(*entry->indices), compare_u64); - entry->start_index = entry->indices[0]; - entry->end_index = entry->indices[entry->indices_len - 1] + 1u; - entry->has_range = true; - } - - free(assigned); - return 0; -} - -static void free_validator_registry(struct lantern_validator_registry *registry) { - if (!registry || !registry->records) { - return; - } - for (size_t i = 0; i < registry->count; ++i) { - free(registry->records[i].pubkey_hex); - free(registry->records[i].withdrawal_credentials_hex); - } - free(registry->records); - registry->records = NULL; - registry->count = 0; -} - -static void free_validator_config(struct lantern_validator_config *config) { - if (!config) { - return; - } - if (config->entries) { - for (size_t i = 0; i < config->count; ++i) { - free_validator_config_entry(&config->entries[i]); - } - free(config->entries); - } - config->entries = NULL; - config->count = 0; - free(config->shuffle); - config->shuffle = NULL; -} - -static void free_validator_config_entry(struct lantern_validator_config_entry *entry) { - if (!entry) { - return; - } - free(entry->name); - entry->name = NULL; - if (entry->privkey_hex) { - size_t len = strlen(entry->privkey_hex); - if (len > 0) { - lantern_secure_zero(entry->privkey_hex, len); - } - free(entry->privkey_hex); - } - entry->privkey_hex = NULL; - free(entry->peer_id_text); - entry->peer_id_text = NULL; - entry->client_kind = LANTERN_VALIDATOR_CLIENT_UNKNOWN; - free(entry->enr.ip); - entry->enr.ip = NULL; - entry->enr.quic_port = 0; - entry->enr.sequence = 0; - entry->count = 0; - free(entry->hash_sig_dir); - entry->hash_sig_dir = NULL; - entry->has_range = false; - entry->start_index = 0; - entry->end_index = 0; - free(entry->indices); - entry->indices = NULL; - entry->indices_len = 0; - entry->indices_cap = 0; -} - -static char *trim_whitespace(char *value) { - while (*value && isspace((unsigned char)*value)) { - ++value; - } - char *end = value + strlen(value); - while (end > value && isspace((unsigned char)*(end - 1))) { - --end; - } - *end = '\0'; - return value; + return result; } -static enum lantern_validator_client_kind classify_validator_client(const char *name) { - if (!name) { - return LANTERN_VALIDATOR_CLIENT_UNKNOWN; - } - if (strncmp(name, "lantern", 7) == 0) { - return LANTERN_VALIDATOR_CLIENT_LANTERN; - } - if (strncmp(name, "qlean", 5) == 0) { - return LANTERN_VALIDATOR_CLIENT_QLEAN; - } - if (strncmp(name, "ream", 4) == 0) { - return LANTERN_VALIDATOR_CLIENT_REAM; - } - if (strncmp(name, "zeam", 4) == 0) { - return LANTERN_VALIDATOR_CLIENT_ZEAM; - } - return LANTERN_VALIDATOR_CLIENT_UNKNOWN; -} -static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id) { - if (!hex || !out_peer_id) { - return -1; - } - uint8_t secret[32]; - if (lantern_hex_decode(hex, secret, sizeof(secret)) != 0) { - return -1; - } - uint8_t *encoded = NULL; - size_t encoded_len = 0; - if (lantern_libp2p_encode_secp256k1_private_key_proto(secret, sizeof(secret), &encoded, &encoded_len) != 0) { - lantern_secure_zero(secret, sizeof(secret)); - return -1; - } - lantern_secure_zero(secret, sizeof(secret)); - peer_id_t peer_id = {0}; - peer_id_error_t perr = peer_id_create_from_private_key(encoded, encoded_len, &peer_id); - free(encoded); - if (perr != PEER_ID_SUCCESS) { - return -1; +static int load_chain_config_and_pubkeys( + struct lantern_chain_config *config, + const char *config_path) +{ + if (!config || !config_path) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; } - char buffer[128]; - if (peer_id_to_string(&peer_id, PEER_ID_FMT_BASE58_LEGACY, buffer, sizeof(buffer)) < 0) { - peer_id_destroy(&peer_id); - return -1; - } - peer_id_destroy(&peer_id); - char *dup = lantern_string_duplicate(buffer); - if (!dup) { - return -1; - } - *out_peer_id = dup; - return 0; -} -static const char *strip_hex_prefix(const char *hex) { - if (!hex) { - return NULL; - } - if (hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X')) { - return hex + 2; - } - return hex; -} - -static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]) { - if (!hex || !out) { - return -1; - } - const char *trimmed = strip_hex_prefix(hex); - if (!trimmed) { - return -1; - } - size_t len = strlen(trimmed); - if (len != (size_t)LANTERN_VALIDATOR_PUBKEY_SIZE * 2u) { - return -1; - } - return lantern_hex_decode(trimmed, out, LANTERN_VALIDATOR_PUBKEY_SIZE); -} - -static int set_record_pubkey(struct lantern_validator_record *record) { - if (!record || !record->pubkey_hex) { - return -1; - } - if (decode_validator_pubkey_hex(record->pubkey_hex, record->pubkey_bytes) != 0) { - return -1; - } - record->has_pubkey_bytes = true; - return 0; -} - -static void merge_chain_pubkeys_into_registry( - const struct lantern_chain_config *config, - struct lantern_validator_registry *registry) { - if (!config || !registry || !registry->records || registry->count == 0) { - return; - } - if (!config->validator_pubkeys || config->validator_pubkeys_count == 0) { - return; - } - size_t limit = registry->count; - if (config->validator_pubkeys_count < limit) { - limit = config->validator_pubkeys_count; - } - for (size_t i = 0; i < limit; ++i) { - struct lantern_validator_record *rec = ®istry->records[i]; - if (!rec->has_pubkey_bytes) { - memcpy( - rec->pubkey_bytes, - config->validator_pubkeys + (i * LANTERN_VALIDATOR_PUBKEY_SIZE), - LANTERN_VALIDATOR_PUBKEY_SIZE); - rec->has_pubkey_bytes = true; - } - if (!rec->pubkey_hex) { - char hex[(LANTERN_VALIDATOR_PUBKEY_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - rec->pubkey_bytes, - LANTERN_VALIDATOR_PUBKEY_SIZE, - hex, - sizeof(hex), - 1) - == 0) { - rec->pubkey_hex = lantern_string_duplicate(hex); - } - } - } -} - -static int parse_chain_config(const char *path, struct lantern_chain_config *config) { - if (!config) { - return -1; - } - - /* clear any existing pubkeys before re-populating */ - if (config->validator_pubkeys) { - free(config->validator_pubkeys); - config->validator_pubkeys = NULL; - } - config->validator_pubkeys_count = 0; - - FILE *fp = fopen(path, "r"); - if (!fp) { - perror("lantern: fopen chain config"); - return -1; + int result = genesis_parse_chain_config(config_path, config); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error("genesis", NULL, "failed to parse chain config at %s", config_path); + return result; } uint8_t *pubkeys = NULL; - size_t pubkeys_count = 0; - size_t pubkeys_cap = 0; - bool in_pubkey_array = false; - - char line[1024]; - while (fgets(line, sizeof(line), fp)) { - char *trimmed = trim_whitespace(line); - if (*trimmed == '#' || *trimmed == '\0') { - continue; - } - - if (strncmp(trimmed, "GENESIS_VALIDATORS", strlen("GENESIS_VALIDATORS")) == 0) { - in_pubkey_array = true; - continue; - } - if (in_pubkey_array) { - if (*trimmed != '-') { - /* end of the list */ - in_pubkey_array = false; - } else { - char *val = trimmed + 1; - while (*val && isspace((unsigned char)*val)) { - ++val; - } - if (*val == '"') { - ++val; - char *endq = strrchr(val, '"'); - if (endq) { - *endq = '\0'; - } - } - uint8_t decoded[LANTERN_VALIDATOR_PUBKEY_SIZE]; - if (decode_validator_pubkey_hex(val, decoded) != 0) { - fclose(fp); - free(pubkeys); - return -1; - } - if (pubkeys_count == pubkeys_cap) { - size_t new_cap = pubkeys_cap == 0 ? 4 : pubkeys_cap * 2; - uint8_t *grown = realloc(pubkeys, new_cap * LANTERN_VALIDATOR_PUBKEY_SIZE); - if (!grown) { - fclose(fp); - free(pubkeys); - return -1; - } - pubkeys = grown; - pubkeys_cap = new_cap; - } - memcpy(pubkeys + (pubkeys_count * LANTERN_VALIDATOR_PUBKEY_SIZE), decoded, LANTERN_VALIDATOR_PUBKEY_SIZE); - pubkeys_count++; - continue; - } - } - - char *sep = strchr(trimmed, ':'); - if (!sep) { - continue; - } - *sep = '\0'; - char *key = trimmed; - char *value = trim_whitespace(sep + 1); - - if (strcmp(key, "GENESIS_TIME") == 0) { - int ok = 0; - config->genesis_time = parse_u64(value, &ok); - if (!ok) { - fclose(fp); - free(pubkeys); - return -1; - } - } else if (strcmp(key, "VALIDATOR_COUNT") == 0) { - int ok = 0; - config->validator_count = parse_u64(value, &ok); - if (!ok) { - fclose(fp); - free(pubkeys); - return -1; - } - } + size_t pubkey_count = 0; + result = genesis_parse_genesis_validator_pubkeys(config_path, &pubkeys, &pubkey_count); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error("genesis", NULL, "failed to parse genesis pubkeys at %s", config_path); + return result; } - fclose(fp); - - if (pubkeys_count > 0) { + if (pubkeys && pubkey_count > 0) + { config->validator_pubkeys = pubkeys; - config->validator_pubkeys_count = pubkeys_count; - } else { - free(pubkeys); - } - - if (config->validator_count == 0 && pubkeys_count > 0) { - config->validator_count = pubkeys_count; - } - - if (config->genesis_time == 0 || config->validator_count == 0) { - return -1; - } - return 0; -} - -/* Lightweight parser for GENESIS_VALIDATORS array used by lean quickstart configs. */ -static int parse_genesis_validator_pubkeys(const char *path, uint8_t **out_pubkeys, size_t *out_count) { - if (!path || !out_pubkeys || !out_count) { - return -1; - } - *out_pubkeys = NULL; - *out_count = 0; - - FILE *fp = fopen(path, "r"); - if (!fp) { - return -1; - } - - bool in_array = false; - size_t count = 0; - size_t cap = 0; - uint8_t *pubkeys = NULL; - - char line[1024]; - while (fgets(line, sizeof(line), fp)) { - char *trimmed = trim_whitespace(line); - if (*trimmed == '#' || *trimmed == '\0') { - continue; - } - if (strncmp(trimmed, "GENESIS_VALIDATORS", strlen("GENESIS_VALIDATORS")) == 0) { - in_array = true; - continue; - } - if (!in_array) { - continue; - } - if (*trimmed != '-') { - /* end of list */ - in_array = false; - continue; + config->validator_pubkeys_count = pubkey_count; + if (config->validator_count == 0) + { + config->validator_count = pubkey_count; } - char *val = trimmed + 1; - while (*val && isspace((unsigned char)*val)) { - ++val; - } - if (*val == '"') { - ++val; - char *endq = strrchr(val, '"'); - if (endq) { - *endq = '\0'; - } - } - uint8_t decoded[LANTERN_VALIDATOR_PUBKEY_SIZE]; - if (decode_validator_pubkey_hex(val, decoded) != 0) { - free(pubkeys); - fclose(fp); - return -1; - } - if (count == cap) { - size_t new_cap = cap == 0 ? 4 : cap * 2; - uint8_t *grown = realloc(pubkeys, new_cap * LANTERN_VALIDATOR_PUBKEY_SIZE); - if (!grown) { - free(pubkeys); - fclose(fp); - return -1; - } - pubkeys = grown; - cap = new_cap; - } - memcpy(pubkeys + (count * LANTERN_VALIDATOR_PUBKEY_SIZE), decoded, LANTERN_VALIDATOR_PUBKEY_SIZE); - count++; + lantern_log_info( + "genesis", + NULL, + "loaded %zu genesis pubkeys from %s", + pubkey_count, + config_path); } - - fclose(fp); - if (count == 0) { + else + { free(pubkeys); - return 0; + lantern_log_warn("genesis", NULL, "no genesis pubkeys found in %s", config_path); } - *out_pubkeys = pubkeys; - *out_count = count; - return 0; -} - -static int parse_validator_registry(const char *path, struct lantern_validator_registry *registry) { - size_t count = 0; - LanternYamlObject *objects = lantern_yaml_read_array(path, "validators", &count); - if (!objects || count == 0) { - lantern_yaml_free_objects(objects, count); - return parse_validator_registry_mapping(path, registry); - } - - bool has_pubkey_field = false; - for (size_t i = 0; i < count; ++i) { - if (yaml_object_value(&objects[i], "pubkey")) { - has_pubkey_field = true; - break; - } - } - - if (!has_pubkey_field) { - lantern_yaml_free_objects(objects, count); - return parse_validator_registry_mapping(path, registry); - } - - bool have_explicit_indices = false; - size_t max_index = 0; - for (size_t i = 0; i < count; ++i) { - const char *index_val = yaml_object_value(&objects[i], "index"); - if (!index_val) { - continue; - } - int ok = 0; - uint64_t parsed_index = parse_u64(index_val, &ok); - if (ok) { - have_explicit_indices = true; - if (parsed_index > SIZE_MAX) { - lantern_yaml_free_objects(objects, count); - return -1; - } - if ((size_t)parsed_index > max_index) { - max_index = (size_t)parsed_index; - } - } - } - - size_t record_count = have_explicit_indices ? (max_index + 1) : count; - struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); - if (!records) { - lantern_yaml_free_objects(objects, count); - return -1; - } - - bool *assigned = calloc(record_count, sizeof(*assigned)); - if (!assigned) { - free(records); - lantern_yaml_free_objects(objects, count); - return -1; - } - - for (size_t i = 0; i < count; ++i) { - size_t slot = i; - if (have_explicit_indices) { - const char *index_val = yaml_object_value(&objects[i], "index"); - int ok = 0; - uint64_t parsed_index = parse_u64(index_val, &ok); - if (!index_val || !ok || parsed_index >= record_count) { - free(assigned); - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - lantern_yaml_free_objects(objects, count); - return -1; - } - slot = (size_t)parsed_index; - } - - if (assigned[slot]) { - free(assigned); - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - lantern_yaml_free_objects(objects, count); - return -1; - } - - const char *pubkey = yaml_object_value(&objects[i], "pubkey"); - const char *withdrawal = yaml_object_value(&objects[i], "withdrawal_credentials"); - if (!pubkey || !withdrawal) { - free(assigned); - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - lantern_yaml_free_objects(objects, count); - return -1; - } - - char *pubkey_hex = dup_trimmed(pubkey); - char *withdrawal_hex = dup_trimmed(withdrawal); - if (!pubkey_hex || !withdrawal_hex) { - free(pubkey_hex); - free(withdrawal_hex); - free(assigned); - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - lantern_yaml_free_objects(objects, count); - return -1; - } - - records[slot].index = (uint64_t)slot; - records[slot].pubkey_hex = pubkey_hex; - records[slot].withdrawal_credentials_hex = withdrawal_hex; - if (set_record_pubkey(&records[slot]) != 0) { - free(assigned); - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - lantern_yaml_free_objects(objects, count); - return -1; - } - assigned[slot] = true; - } - - if (have_explicit_indices) { - for (size_t i = 0; i < record_count; ++i) { - if (!assigned[i]) { - free(assigned); - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - lantern_yaml_free_objects(objects, count); - return -1; - } - } - } - - free(assigned); - lantern_yaml_free_objects(objects, count); - registry->records = records; - registry->count = record_count; - return 0; -} - -static int parse_validator_registry_mapping(const char *path, struct lantern_validator_registry *registry) { - FILE *fp = fopen(path, "r"); - if (!fp) { - return -1; - } - - size_t *indices = NULL; - size_t count = 0; - size_t capacity = 0; - size_t max_index = 0; - - char line[1024]; - while (fgets(line, sizeof(line), fp)) { - char *trimmed = trim_whitespace(line); - if (!trimmed || *trimmed != '-') { - continue; - } - ++trimmed; - while (*trimmed && isspace((unsigned char)*trimmed)) { - ++trimmed; - } - if (*trimmed == '\0') { - continue; - } - char *endptr = NULL; - unsigned long long value = strtoull(trimmed, &endptr, 10); - if (endptr == trimmed) { - continue; - } - if (value > SIZE_MAX) { - fclose(fp); - free(indices); - return -1; - } - if (count == capacity) { - size_t new_capacity = capacity == 0 ? 8 : capacity * 2; - size_t *new_indices = realloc(indices, new_capacity * sizeof(*new_indices)); - if (!new_indices) { - fclose(fp); - free(indices); - return -1; - } - indices = new_indices; - capacity = new_capacity; - } - indices[count++] = (size_t)value; - if ((size_t)value > max_index) { - max_index = (size_t)value; - } - } - fclose(fp); - - if (count == 0) { - free(indices); - return -1; - } - - size_t record_count = max_index + 1; - struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); - if (!records) { - free(indices); - return -1; - } - - const char *zero_hex = "0x00"; - for (size_t i = 0; i < record_count; ++i) { - records[i].index = i; - records[i].pubkey_hex = strdup(zero_hex); - records[i].withdrawal_credentials_hex = strdup(zero_hex); - if (!records[i].pubkey_hex || !records[i].withdrawal_credentials_hex) { - free_validator_registry(&(struct lantern_validator_registry){.records = records, .count = record_count}); - free(indices); - return -1; - } - (void)set_record_pubkey(&records[i]); - } - - registry->records = records; - registry->count = record_count; - free(indices); - return 0; -} - -static int parse_validator_config(const char *path, struct lantern_validator_config *config) { - if (read_scalar_value(path, "shuffle", &config->shuffle) != 0) { - return -1; - } - - size_t count = 0; - LanternYamlObject *objects = lantern_yaml_read_array(path, "validators", &count); - if (!objects || count == 0) { - lantern_yaml_free_objects(objects, count); - return -1; - } - - struct lantern_validator_config_entry *entries = calloc(count, sizeof(*entries)); - if (!entries) { - lantern_yaml_free_objects(objects, count); - return -1; - } - - for (size_t i = 0; i < count; ++i) { - const char *name_val = yaml_object_value(&objects[i], "name"); - const char *priv_val = yaml_object_value(&objects[i], "privkey"); - const char *count_val = yaml_object_value(&objects[i], "count"); - const char *ip_val = yaml_object_value(&objects[i], "ip"); - const char *quic_val = yaml_object_value(&objects[i], "quic"); - const char *seq_val = yaml_object_value(&objects[i], "seq"); - const char *hash_dir_val = yaml_object_value(&objects[i], "hashSigDir"); - - entries[i].name = dup_trimmed(name_val); - entries[i].privkey_hex = dup_trimmed(priv_val); - entries[i].client_kind = classify_validator_client(entries[i].name); - if (!entries[i].privkey_hex - || derive_peer_id_from_privkey_hex(entries[i].privkey_hex, &entries[i].peer_id_text) != 0) { - lantern_yaml_free_objects(objects, count); - free_validator_config_entry(&entries[i]); - free(entries); - return -1; - } - - int ok = 0; - entries[i].count = parse_u64(count_val, &ok); - if (!ok) { - lantern_yaml_free_objects(objects, count); - free_validator_config_entry(&entries[i]); - free(entries); - return -1; - } - - entries[i].enr.ip = dup_trimmed(ip_val); - uint64_t quic_port = parse_u64(quic_val, &ok); - if (!ok || quic_port > UINT16_MAX) { - lantern_yaml_free_objects(objects, count); - free_validator_config_entry(&entries[i]); - free(entries); - return -1; - } - entries[i].enr.quic_port = (uint16_t)quic_port; - - entries[i].enr.sequence = 1; // default ENR sequence if unspecified - if (seq_val && *seq_val) { - entries[i].enr.sequence = parse_u64(seq_val, &ok); - if (!ok) { - lantern_yaml_free_objects(objects, count); - free_validator_config_entry(&entries[i]); - free(entries); - return -1; - } - } - entries[i].hash_sig_dir = dup_trimmed(hash_dir_val); - entries[i].has_range = false; - entries[i].start_index = 0; - entries[i].end_index = 0; - entries[i].indices = NULL; - entries[i].indices_len = 0; - entries[i].indices_cap = 0; - } - - lantern_yaml_free_objects(objects, count); - config->entries = entries; - config->count = count; - return 0; -} - -static int parse_nodes_file(const char *path, struct lantern_enr_record_list *list) { - FILE *fp = fopen(path, "r"); - if (!fp) { - perror("lantern: fopen nodes"); - return -1; - } - - char line[2048]; - while (fgets(line, sizeof(line), fp)) { - char *trimmed = trim_whitespace(line); - if (*trimmed == '#' || *trimmed == '\0') { - continue; - } - char *enr = strstr(trimmed, "enr:"); - if (!enr) { - continue; - } - enr = trim_whitespace(enr); - if (*enr == '\0') { - continue; - } - if (lantern_enr_record_list_append(list, enr) != 0) { - fclose(fp); - return -1; - } - } - - fclose(fp); - return 0; -} - -static int read_state_blob(const char *path, uint8_t **bytes, size_t *size) { - FILE *fp = fopen(path, "rb"); - if (!fp) { - perror("lantern: fopen genesis ssz"); - return -1; - } - - if (fseek(fp, 0, SEEK_END) != 0) { - fclose(fp); - return -1; - } - long file_size = ftell(fp); - if (file_size < 0) { - fclose(fp); - return -1; - } - if (fseek(fp, 0, SEEK_SET) != 0) { - fclose(fp); - return -1; - } - - uint8_t *buffer = malloc((size_t)file_size); - if (!buffer) { - fclose(fp); - return -1; - } - - size_t read_bytes = fread(buffer, 1, (size_t)file_size, fp); - fclose(fp); - if (read_bytes != (size_t)file_size) { - free(buffer); - return -1; - } - - *bytes = buffer; - *size = read_bytes; - return 0; -} - -static uint64_t parse_u64(const char *value, int *ok) { - if (ok) { - *ok = 0; - } - if (!value) { - return 0; - } - - char *end = NULL; - errno = 0; - uint64_t parsed = strtoull(value, &end, 0); - if (errno != 0 || end == value) { - return 0; - } - if (ok) { - *ok = 1; - } - return parsed; -} - -static char *dup_trimmed(const char *value) { - if (!value) { - return NULL; - } - const char *start = value; - while (*start && isspace((unsigned char)*start)) { - ++start; - } - const char *end = start + strlen(start); - while (end > start && isspace((unsigned char)*(end - 1))) { - --end; - } - - if (end - start >= 2 && ((*start == '"' && *(end - 1) == '"') || (*start == '\'' && *(end - 1) == '\''))) { - ++start; - --end; - } - - return lantern_string_duplicate_len(start, (size_t)(end - start)); -} - -static const char *yaml_object_value(const LanternYamlObject *object, const char *key) { - if (!object || !key) { - return NULL; - } - for (size_t i = 0; i < object->num_pairs; ++i) { - if (object->pairs[i].key && strcmp(object->pairs[i].key, key) == 0) { - return object->pairs[i].value; - } - } - return NULL; -} - -static int read_scalar_value(const char *path, const char *key, char **out_value) { - if (!path || !key || !out_value) { - return -1; - } - - FILE *fp = fopen(path, "r"); - if (!fp) { - perror("lantern: fopen validator-config"); - return -1; - } - - char line[1024]; - size_t key_len = strlen(key); - while (fgets(line, sizeof(line), fp)) { - char *trimmed = trim_whitespace(line); - if (*trimmed == '#' || *trimmed == '\0') { - continue; - } - - if (strncmp(trimmed, key, key_len) != 0) { - continue; - } - if (trimmed[key_len] != ':') { - continue; - } - - char *value = trim_whitespace(trimmed + key_len + 1); - *out_value = dup_trimmed(value); - fclose(fp); - return *out_value ? 0 : -1; + if (config->validator_count == 0) + { + lantern_log_error("genesis", NULL, "validator count missing or zero in %s", config_path); + return LANTERN_GENESIS_ERR_INVALID_DATA; } - fclose(fp); - return -1; + return LANTERN_GENESIS_OK; } diff --git a/src/genesis/genesis_internal.h b/src/genesis/genesis_internal.h new file mode 100644 index 0000000..ab93df3 --- /dev/null +++ b/src/genesis/genesis_internal.h @@ -0,0 +1,48 @@ +/** + * @file genesis_internal.h + * @brief Internal helpers for parsing and managing genesis artifacts. + * + * This header is NOT part of the public API and is only intended for use by + * source files within `src/genesis/`. + */ + +#ifndef LANTERN_GENESIS_INTERNAL_H +#define LANTERN_GENESIS_INTERNAL_H + +#include +#include + +#include "lantern/genesis/genesis.h" + +/** + * Genesis module-specific error codes. + */ +typedef enum +{ + LANTERN_GENESIS_OK = 0, + LANTERN_GENESIS_ERR_INVALID_PARAM = -1, + LANTERN_GENESIS_ERR_IO = -2, + LANTERN_GENESIS_ERR_OUT_OF_MEMORY = -3, + LANTERN_GENESIS_ERR_OVERFLOW = -4, + LANTERN_GENESIS_ERR_PARSE = -5, + LANTERN_GENESIS_ERR_INVALID_DATA = -6, +} lantern_genesis_error_t; + +void genesis_free_validator_registry(struct lantern_validator_registry *registry); +void genesis_free_validator_config(struct lantern_validator_config *config); + +int genesis_parse_chain_config(const char *path, struct lantern_chain_config *config); +int genesis_parse_genesis_validator_pubkeys( + const char *path, + uint8_t **out_pubkeys, + size_t *out_count); +int genesis_parse_validator_registry(const char *path, struct lantern_validator_registry *registry); +int genesis_parse_validator_config(const char *path, struct lantern_validator_config *config); +int genesis_parse_nodes_file(const char *path, struct lantern_enr_record_list *list); +int genesis_read_state_blob(const char *path, uint8_t **bytes, size_t *size); + +void genesis_merge_chain_pubkeys_into_registry( + const struct lantern_chain_config *config, + struct lantern_validator_registry *registry); + +#endif /* LANTERN_GENESIS_INTERNAL_H */ diff --git a/src/genesis/genesis_parse.c b/src/genesis/genesis_parse.c new file mode 100644 index 0000000..ac8a8f9 --- /dev/null +++ b/src/genesis/genesis_parse.c @@ -0,0 +1,1444 @@ +/** + * @file genesis_parse.c + * @brief Parsing and memory helpers for Lantern genesis artifacts. + * + * Implements internal helpers used by the public genesis API: + * - YAML parsing for config/validators/validator-config/nodes files + * - Binary blob loading for genesis SSZ state + * - Memory ownership helpers for registry/config structures + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + */ + +#include "genesis_internal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "peer_id/peer_id.h" + +#include "internal/yaml_parser.h" +#include "lantern/networking/libp2p.h" +#include "lantern/support/secure_mem.h" +#include "lantern/support/strings.h" + +static const size_t GENESIS_LINE_BUFFER_LEN = 2048; +static const size_t GENESIS_SMALL_LINE_BUFFER_LEN = 1024; +static const size_t GENESIS_INITIAL_PUBKEY_CAPACITY = 4; +static const size_t GENESIS_INITIAL_MAPPING_INDEX_CAPACITY = 8; +static const size_t GENESIS_PEER_ID_BUFFER_LEN = 128; +static const size_t GENESIS_PUBKEY_HEX_BUFFER_LEN = (LANTERN_VALIDATOR_PUBKEY_SIZE * 2u) + 3u; + +static const char *CHAIN_CONFIG_KEY_GENESIS_TIME = "GENESIS_TIME"; +static const char *CHAIN_CONFIG_KEY_VALIDATOR_COUNT = "VALIDATOR_COUNT"; +static const char *CHAIN_CONFIG_KEY_GENESIS_VALIDATORS = "GENESIS_VALIDATORS"; + +static const char *VALIDATOR_REGISTRY_ARRAY_KEY = "validators"; +static const char *VALIDATOR_REGISTRY_FIELD_INDEX = "index"; +static const char *VALIDATOR_REGISTRY_FIELD_PUBKEY = "pubkey"; +static const char *VALIDATOR_REGISTRY_FIELD_WITHDRAWAL_CREDENTIALS = "withdrawal_credentials"; + +static const char *VALIDATOR_CONFIG_SCALAR_SHUFFLE = "shuffle"; +static const char *VALIDATOR_CONFIG_ARRAY_VALIDATORS = "validators"; +static const char *VALIDATOR_CONFIG_FIELD_NAME = "name"; +static const char *VALIDATOR_CONFIG_FIELD_PRIVKEY = "privkey"; +static const char *VALIDATOR_CONFIG_FIELD_COUNT = "count"; +static const char *VALIDATOR_CONFIG_FIELD_IP = "ip"; +static const char *VALIDATOR_CONFIG_FIELD_QUIC = "quic"; +static const char *VALIDATOR_CONFIG_FIELD_SEQ = "seq"; +static const char *VALIDATOR_CONFIG_FIELD_HASH_SIG_DIR = "hashSigDir"; + +static uint64_t parse_u64(const char *value, int *ok); +static char *dup_trimmed(const char *value); +static const char *yaml_object_value(const LanternYamlObject *object, const char *key); +static int read_scalar_value(const char *path, const char *key, char **out_value); +static enum lantern_validator_client_kind classify_validator_client(const char *name); +static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id); +static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]); +static int set_record_pubkey(struct lantern_validator_record *record); + +static int ensure_pubkey_capacity(uint8_t **pubkeys, size_t *cap, size_t required); +static int collect_registry_mapping_indices( + const char *path, + size_t **out_indices, + size_t *out_count, + size_t *out_max_index); +static int validate_registry_index_coverage( + const size_t *indices, + size_t count, + size_t max_index, + size_t *out_record_count); +static int build_index_only_registry( + size_t record_count, + struct lantern_validator_registry *registry); +static int scan_registry_objects( + const LanternYamlObject *objects, + size_t object_count, + bool *out_has_pubkey_field, + bool *out_have_explicit_indices, + size_t *out_record_count); +static int populate_registry_records_from_objects( + LanternYamlObject *objects, + size_t object_count, + bool has_pubkey_field, + bool have_explicit_indices, + struct lantern_validator_record *records, + size_t record_count, + bool *assigned); +static int validate_registry_full_coverage(const bool *assigned, size_t record_count); +static int parse_validator_registry_objects( + LanternYamlObject *objects, + size_t object_count, + struct lantern_validator_registry *registry); +static int parse_validator_registry_mapping( + const char *path, + struct lantern_validator_registry *registry); +static int parse_validator_config_entry( + const LanternYamlObject *object, + struct lantern_validator_config_entry *entry); +static void free_validator_config_entry(struct lantern_validator_config_entry *entry); + + +/** @brief Parse a uint64_t with optional trailing comment. */ +static uint64_t parse_u64(const char *value, int *ok) +{ + if (ok) + { + *ok = 0; + } + if (!value) + { + return 0; + } + + errno = 0; + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 0); + if (errno != 0 || end == value) + { + return 0; + } + + while (end && *end && isspace((unsigned char)*end)) + { + ++end; + } + if (end && *end != '\0' && *end != '#') + { + return 0; + } + if (parsed > UINT64_MAX) + { + return 0; + } + + if (ok) + { + *ok = 1; + } + return (uint64_t)parsed; +} + + +/** @brief Duplicate a string after trimming whitespace and optional surrounding quotes. */ +static char *dup_trimmed(const char *value) +{ + if (!value) + { + return NULL; + } + + const char *start = value; + while (*start && isspace((unsigned char)*start)) + { + ++start; + } + + const char *end = start + strlen(start); + while (end > start && isspace((unsigned char)*(end - 1))) + { + --end; + } + + if ((end - start) >= 2 + && ((*start == '"' && *(end - 1) == '"') || (*start == '\'' && *(end - 1) == '\''))) + { + ++start; + --end; + } + + return lantern_string_duplicate_len(start, (size_t)(end - start)); +} + + +/** @brief Lookup a key value in a YAML object. */ +static const char *yaml_object_value(const LanternYamlObject *object, const char *key) +{ + if (!object || !key) + { + return NULL; + } + + for (size_t i = 0; i < object->num_pairs; ++i) + { + if (object->pairs[i].key && strcmp(object->pairs[i].key, key) == 0) + { + return object->pairs[i].value; + } + } + + return NULL; +} + + +/** @brief Read a top-level scalar value from a YAML file (`key: value`). */ +static int read_scalar_value(const char *path, const char *key, char **out_value) +{ + if (!path || !key || !out_value) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_value = NULL; + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + int result = LANTERN_GENESIS_ERR_PARSE; + char line[GENESIS_SMALL_LINE_BUFFER_LEN]; + const size_t key_len = strlen(key); + + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed == '\0' || *trimmed == '#') + { + continue; + } + + if (strncmp(trimmed, key, key_len) != 0) + { + continue; + } + if (trimmed[key_len] != ':') + { + continue; + } + + char *value = lantern_trim_whitespace(trimmed + key_len + 1); + *out_value = dup_trimmed(value); + result = *out_value ? LANTERN_GENESIS_OK : LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + break; + } + + fclose(fp); + return result; +} + + +/** @brief Classify validator client kind based on its name prefix. */ +static enum lantern_validator_client_kind classify_validator_client(const char *name) +{ + if (!name) + { + return LANTERN_VALIDATOR_CLIENT_UNKNOWN; + } + + if (strncmp(name, "lantern", sizeof("lantern") - 1) == 0) + { + return LANTERN_VALIDATOR_CLIENT_LANTERN; + } + if (strncmp(name, "qlean", sizeof("qlean") - 1) == 0) + { + return LANTERN_VALIDATOR_CLIENT_QLEAN; + } + if (strncmp(name, "ream", sizeof("ream") - 1) == 0) + { + return LANTERN_VALIDATOR_CLIENT_REAM; + } + if (strncmp(name, "zeam", sizeof("zeam") - 1) == 0) + { + return LANTERN_VALIDATOR_CLIENT_ZEAM; + } + + return LANTERN_VALIDATOR_CLIENT_UNKNOWN; +} + + +/** @brief Derive a libp2p peer ID from a secp256k1 private key (hex). */ +static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id) +{ + if (!hex || !out_peer_id) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_peer_id = NULL; + + uint8_t secret[32]; + if (lantern_hex_decode(hex, secret, sizeof(secret)) != 0) + { + lantern_secure_zero(secret, sizeof(secret)); + return LANTERN_GENESIS_ERR_PARSE; + } + + uint8_t *encoded = NULL; + size_t encoded_len = 0; + if (lantern_libp2p_encode_secp256k1_private_key_proto( + secret, + sizeof(secret), + &encoded, + &encoded_len) + != 0) + { + lantern_secure_zero(secret, sizeof(secret)); + return LANTERN_GENESIS_ERR_PARSE; + } + lantern_secure_zero(secret, sizeof(secret)); + + peer_id_t peer_id = {0}; + peer_id_error_t perr = peer_id_create_from_private_key(encoded, encoded_len, &peer_id); + if (encoded) + { + lantern_secure_zero(encoded, encoded_len); + } + free(encoded); + + if (perr != PEER_ID_SUCCESS) + { + return LANTERN_GENESIS_ERR_PARSE; + } + + char buffer[GENESIS_PEER_ID_BUFFER_LEN]; + if (peer_id_to_string(&peer_id, PEER_ID_FMT_BASE58_LEGACY, buffer, sizeof(buffer)) < 0) + { + peer_id_destroy(&peer_id); + return LANTERN_GENESIS_ERR_PARSE; + } + peer_id_destroy(&peer_id); + + char *dup = lantern_string_duplicate(buffer); + if (!dup) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + *out_peer_id = dup; + return LANTERN_GENESIS_OK; +} + + +/** @brief Decode a validator pubkey hex string into bytes. */ +static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]) +{ + if (!hex || !out) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (lantern_hex_decode(hex, out, LANTERN_VALIDATOR_PUBKEY_SIZE) != 0) + { + return LANTERN_GENESIS_ERR_PARSE; + } + + return LANTERN_GENESIS_OK; +} + + +/** @brief Populate a registry record's pubkey bytes from its pubkey hex string. */ +static int set_record_pubkey(struct lantern_validator_record *record) +{ + if (!record || !record->pubkey_hex) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + int result = decode_validator_pubkey_hex(record->pubkey_hex, record->pubkey_bytes); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + record->has_pubkey_bytes = true; + return LANTERN_GENESIS_OK; +} + + +/** @brief Ensure capacity for a packed pubkey buffer (count elements). */ +static int ensure_pubkey_capacity(uint8_t **pubkeys, size_t *cap, size_t required) +{ + if (!pubkeys || !cap) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (*cap >= required) + { + return LANTERN_GENESIS_OK; + } + + size_t new_cap = (*cap == 0) ? GENESIS_INITIAL_PUBKEY_CAPACITY : *cap; + while (new_cap < required) + { + if (new_cap > SIZE_MAX / 2) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + new_cap *= 2; + } + + if (new_cap > SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + void *grown = realloc(*pubkeys, new_cap * LANTERN_VALIDATOR_PUBKEY_SIZE); + if (!grown) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + *pubkeys = grown; + *cap = new_cap; + return LANTERN_GENESIS_OK; +} + + +void genesis_free_validator_registry(struct lantern_validator_registry *registry) +{ + if (!registry) + { + return; + } + + if (registry->records) + { + for (size_t i = 0; i < registry->count; ++i) + { + free(registry->records[i].pubkey_hex); + free(registry->records[i].withdrawal_credentials_hex); + } + free(registry->records); + } + + registry->records = NULL; + registry->count = 0; +} + + +/** @brief Free resources held by a validator config entry. */ +static void free_validator_config_entry(struct lantern_validator_config_entry *entry) +{ + if (!entry) + { + return; + } + + free(entry->name); + entry->name = NULL; + + if (entry->privkey_hex) + { + size_t len = strlen(entry->privkey_hex); + if (len > 0) + { + lantern_secure_zero(entry->privkey_hex, len); + } + free(entry->privkey_hex); + } + entry->privkey_hex = NULL; + + free(entry->peer_id_text); + entry->peer_id_text = NULL; + + entry->client_kind = LANTERN_VALIDATOR_CLIENT_UNKNOWN; + + free(entry->enr.ip); + entry->enr.ip = NULL; + entry->enr.quic_port = 0; + entry->enr.sequence = 0; + + entry->count = 0; + + free(entry->hash_sig_dir); + entry->hash_sig_dir = NULL; + + entry->start_index = 0; + entry->end_index = 0; + entry->has_range = false; + + free(entry->indices); + entry->indices = NULL; + entry->indices_len = 0; + entry->indices_cap = 0; +} + + +void genesis_free_validator_config(struct lantern_validator_config *config) +{ + if (!config) + { + return; + } + + if (config->entries) + { + for (size_t i = 0; i < config->count; ++i) + { + free_validator_config_entry(&config->entries[i]); + } + free(config->entries); + } + + config->entries = NULL; + config->count = 0; + + free(config->shuffle); + config->shuffle = NULL; +} + + +void genesis_merge_chain_pubkeys_into_registry( + const struct lantern_chain_config *config, + struct lantern_validator_registry *registry) +{ + if (!config || !registry || !registry->records || registry->count == 0) + { + return; + } + if (!config->validator_pubkeys || config->validator_pubkeys_count == 0) + { + return; + } + if (config->validator_pubkeys_count > SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE) + { + return; + } + + size_t limit = registry->count; + if (config->validator_pubkeys_count < limit) + { + limit = config->validator_pubkeys_count; + } + + for (size_t i = 0; i < limit; ++i) + { + struct lantern_validator_record *rec = ®istry->records[i]; + + if (!rec->has_pubkey_bytes) + { + memcpy( + rec->pubkey_bytes, + config->validator_pubkeys + (i * LANTERN_VALIDATOR_PUBKEY_SIZE), + LANTERN_VALIDATOR_PUBKEY_SIZE); + rec->has_pubkey_bytes = true; + } + + if (!rec->pubkey_hex) + { + char hex[GENESIS_PUBKEY_HEX_BUFFER_LEN]; + if (lantern_bytes_to_hex( + rec->pubkey_bytes, + LANTERN_VALIDATOR_PUBKEY_SIZE, + hex, + sizeof(hex), + 1) + == 0) + { + rec->pubkey_hex = lantern_string_duplicate(hex); + } + } + } +} + + +int genesis_parse_chain_config(const char *path, struct lantern_chain_config *config) +{ + if (!path || !config) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + config->genesis_time = 0; + config->validator_count = 0; + + free(config->validator_pubkeys); + config->validator_pubkeys = NULL; + config->validator_pubkeys_count = 0; + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + int result = LANTERN_GENESIS_OK; + char line[GENESIS_SMALL_LINE_BUFFER_LEN]; + + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed == '\0' || *trimmed == '#') + { + continue; + } + + char *sep = strchr(trimmed, ':'); + if (!sep) + { + continue; + } + + *sep = '\0'; + const char *key = trimmed; + char *value = lantern_trim_whitespace(sep + 1); + + if (strcmp(key, CHAIN_CONFIG_KEY_GENESIS_TIME) == 0) + { + int ok = 0; + config->genesis_time = parse_u64(value, &ok); + if (!ok || config->genesis_time == 0) + { + result = LANTERN_GENESIS_ERR_INVALID_DATA; + break; + } + } + else if (strcmp(key, CHAIN_CONFIG_KEY_VALIDATOR_COUNT) == 0) + { + int ok = 0; + config->validator_count = parse_u64(value, &ok); + if (!ok) + { + result = LANTERN_GENESIS_ERR_INVALID_DATA; + break; + } + } + } + + fclose(fp); + + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + if (config->genesis_time == 0) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + return LANTERN_GENESIS_OK; +} + + +int genesis_parse_genesis_validator_pubkeys( + const char *path, + uint8_t **out_pubkeys, + size_t *out_count) +{ + if (!path || !out_pubkeys || !out_count) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_pubkeys = NULL; + *out_count = 0; + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + bool in_array = false; + size_t count = 0; + size_t cap = 0; + uint8_t *pubkeys = NULL; + int result = LANTERN_GENESIS_OK; + + char line[GENESIS_SMALL_LINE_BUFFER_LEN]; + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed == '\0' || *trimmed == '#') + { + continue; + } + + if (!in_array + && strncmp( + trimmed, + CHAIN_CONFIG_KEY_GENESIS_VALIDATORS, + strlen(CHAIN_CONFIG_KEY_GENESIS_VALIDATORS)) + == 0) + { + in_array = true; + continue; + } + + if (!in_array) + { + continue; + } + + if (*trimmed != '-') + { + in_array = false; + continue; + } + + char *val = lantern_trim_whitespace(trimmed + 1); + if (!val || *val == '\0') + { + continue; + } + + if (*val == '"' || *val == '\'') + { + char quote = *val; + ++val; + char *endq = strrchr(val, quote); + if (endq) + { + *endq = '\0'; + } + } + + uint8_t decoded[LANTERN_VALIDATOR_PUBKEY_SIZE]; + result = decode_validator_pubkey_hex(val, decoded); + if (result != LANTERN_GENESIS_OK) + { + result = LANTERN_GENESIS_ERR_INVALID_DATA; + break; + } + + result = ensure_pubkey_capacity(&pubkeys, &cap, count + 1); + if (result != LANTERN_GENESIS_OK) + { + break; + } + + memcpy( + pubkeys + (count * LANTERN_VALIDATOR_PUBKEY_SIZE), + decoded, + LANTERN_VALIDATOR_PUBKEY_SIZE); + ++count; + } + + fclose(fp); + + if (result != LANTERN_GENESIS_OK) + { + free(pubkeys); + return result; + } + + if (count == 0) + { + free(pubkeys); + return LANTERN_GENESIS_OK; + } + + *out_pubkeys = pubkeys; + *out_count = count; + return LANTERN_GENESIS_OK; +} + + +/** @brief Collect all validator indices from a mapping/scalar-list validators.yaml. */ +static int collect_registry_mapping_indices( + const char *path, + size_t **out_indices, + size_t *out_count, + size_t *out_max_index) +{ + if (!path || !out_indices || !out_count || !out_max_index) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_indices = NULL; + *out_count = 0; + *out_max_index = 0; + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + size_t *indices = NULL; + size_t count = 0; + size_t cap = 0; + size_t max_index = 0; + int result = LANTERN_GENESIS_OK; + + char line[GENESIS_SMALL_LINE_BUFFER_LEN]; + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed != '-') + { + continue; + } + + trimmed = lantern_trim_whitespace(trimmed + 1); + if (!trimmed || *trimmed == '\0') + { + continue; + } + + int ok = 0; + uint64_t value = parse_u64(trimmed, &ok); + if (!ok || value > SIZE_MAX) + { + result = LANTERN_GENESIS_ERR_INVALID_DATA; + break; + } + + if (count == cap) + { + if (cap > SIZE_MAX / 2) + { + result = LANTERN_GENESIS_ERR_OVERFLOW; + break; + } + + size_t new_cap = (cap == 0) ? GENESIS_INITIAL_MAPPING_INDEX_CAPACITY : (cap * 2); + void *grown = realloc(indices, new_cap * sizeof(*indices)); + if (!grown) + { + result = LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + break; + } + indices = grown; + cap = new_cap; + } + + indices[count++] = (size_t)value; + if ((size_t)value > max_index) + { + max_index = (size_t)value; + } + } + + fclose(fp); + + if (result != LANTERN_GENESIS_OK) + { + free(indices); + return result; + } + + if (count == 0) + { + free(indices); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + *out_indices = indices; + *out_count = count; + *out_max_index = max_index; + return LANTERN_GENESIS_OK; +} + + +/** @brief Validate that indices are unique and cover [0, max_index]. */ +static int validate_registry_index_coverage( + const size_t *indices, + size_t count, + size_t max_index, + size_t *out_record_count) +{ + if (!indices || count == 0 || !out_record_count) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (max_index == SIZE_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + size_t record_count = max_index + 1; + bool *seen = calloc(record_count, sizeof(*seen)); + if (!seen) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + for (size_t i = 0; i < count; ++i) + { + size_t idx = indices[i]; + if (idx >= record_count || seen[idx]) + { + free(seen); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + seen[idx] = true; + } + + for (size_t i = 0; i < record_count; ++i) + { + if (!seen[i]) + { + free(seen); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + free(seen); + *out_record_count = record_count; + return LANTERN_GENESIS_OK; +} + + +/** @brief Allocate and populate an index-only validator registry. */ +static int build_index_only_registry( + size_t record_count, + struct lantern_validator_registry *registry) +{ + if (!registry || record_count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); + if (!records) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + for (size_t i = 0; i < record_count; ++i) + { + records[i].index = (uint64_t)i; + } + + registry->records = records; + registry->count = record_count; + return LANTERN_GENESIS_OK; +} + + +/** @brief Populate an index-only registry from mapping/scalar list indices. */ +static int parse_validator_registry_mapping( + const char *path, + struct lantern_validator_registry *registry) +{ + if (!path || !registry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + size_t *indices = NULL; + size_t count = 0; + size_t max_index = 0; + int result = collect_registry_mapping_indices(path, &indices, &count, &max_index); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + size_t record_count = 0; + result = validate_registry_index_coverage(indices, count, max_index, &record_count); + free(indices); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + return build_index_only_registry(record_count, registry); +} + + +/** @brief Scan registry objects to determine format and record count. */ +static int scan_registry_objects( + const LanternYamlObject *objects, + size_t object_count, + bool *out_has_pubkey_field, + bool *out_have_explicit_indices, + size_t *out_record_count) +{ + if (!objects + || object_count == 0 + || !out_has_pubkey_field + || !out_have_explicit_indices + || !out_record_count) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + bool has_pubkey_field = false; + for (size_t i = 0; i < object_count; ++i) + { + if (yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_PUBKEY)) + { + has_pubkey_field = true; + break; + } + } + + bool have_explicit_indices = false; + size_t max_index = 0; + for (size_t i = 0; i < object_count; ++i) + { + const char *index_val = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_INDEX); + if (!index_val) + { + continue; + } + + int ok = 0; + uint64_t parsed_index = parse_u64(index_val, &ok); + if (!ok || parsed_index > SIZE_MAX) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + have_explicit_indices = true; + if ((size_t)parsed_index > max_index) + { + max_index = (size_t)parsed_index; + } + } + + if (have_explicit_indices && max_index == SIZE_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + *out_has_pubkey_field = has_pubkey_field; + *out_have_explicit_indices = have_explicit_indices; + *out_record_count = have_explicit_indices ? (max_index + 1) : object_count; + return LANTERN_GENESIS_OK; +} + + +/** @brief Populate registry records from YAML objects. */ +static int populate_registry_records_from_objects( + LanternYamlObject *objects, + size_t object_count, + bool has_pubkey_field, + bool have_explicit_indices, + struct lantern_validator_record *records, + size_t record_count, + bool *assigned) +{ + if (!objects || object_count == 0 || !records || record_count == 0 || !assigned) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + for (size_t i = 0; i < object_count; ++i) + { + size_t slot = i; + if (have_explicit_indices) + { + const char *index_val = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_INDEX); + int ok = 0; + uint64_t parsed_index = parse_u64(index_val, &ok); + if (!index_val || !ok || parsed_index >= record_count) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + slot = (size_t)parsed_index; + } + + if (assigned[slot]) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + records[slot].index = (uint64_t)slot; + + if (has_pubkey_field) + { + const char *pubkey = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_PUBKEY); + const char *withdrawal = yaml_object_value( + &objects[i], + VALIDATOR_REGISTRY_FIELD_WITHDRAWAL_CREDENTIALS); + if (!pubkey || !withdrawal) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + records[slot].pubkey_hex = dup_trimmed(pubkey); + records[slot].withdrawal_credentials_hex = dup_trimmed(withdrawal); + if (!records[slot].pubkey_hex || !records[slot].withdrawal_credentials_hex) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + int rc = set_record_pubkey(&records[slot]); + if (rc != LANTERN_GENESIS_OK) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + assigned[slot] = true; + } + + return LANTERN_GENESIS_OK; +} + + +/** @brief Validate full coverage for explicit-index registries. */ +static int validate_registry_full_coverage(const bool *assigned, size_t record_count) +{ + if (!assigned || record_count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + for (size_t i = 0; i < record_count; ++i) + { + if (!assigned[i]) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + return LANTERN_GENESIS_OK; +} + + +/** @brief Parse a validator registry from YAML objects (annotated or index-only). */ +static int parse_validator_registry_objects( + LanternYamlObject *objects, + size_t object_count, + struct lantern_validator_registry *registry) +{ + if (!objects || object_count == 0 || !registry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + bool has_pubkey_field = false; + bool have_explicit_indices = false; + size_t record_count = 0; + + int result = scan_registry_objects( + objects, + object_count, + &has_pubkey_field, + &have_explicit_indices, + &record_count); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); + if (!records) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + bool *assigned = calloc(record_count, sizeof(*assigned)); + if (!assigned) + { + free(records); + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + struct lantern_validator_registry tmp = {.records = records, .count = record_count}; + + result = populate_registry_records_from_objects( + objects, + object_count, + has_pubkey_field, + have_explicit_indices, + records, + record_count, + assigned); + if (result == LANTERN_GENESIS_OK && have_explicit_indices) + { + result = validate_registry_full_coverage(assigned, record_count); + } + + free(assigned); + + if (result != LANTERN_GENESIS_OK) + { + genesis_free_validator_registry(&tmp); + return result; + } + + registry->records = records; + registry->count = record_count; + return LANTERN_GENESIS_OK; +} + + +int genesis_parse_validator_registry(const char *path, struct lantern_validator_registry *registry) +{ + if (!path || !registry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + size_t object_count = 0; + LanternYamlObject *objects = lantern_yaml_read_array( + path, + VALIDATOR_REGISTRY_ARRAY_KEY, + &object_count); + if (!objects || object_count == 0) + { + lantern_yaml_free_objects(objects, object_count); + return parse_validator_registry_mapping(path, registry); + } + + int result = parse_validator_registry_objects(objects, object_count, registry); + lantern_yaml_free_objects(objects, object_count); + return result; +} + + +/** @brief Parse a validator-config.yaml entry into a config entry struct. */ +static int parse_validator_config_entry( + const LanternYamlObject *object, + struct lantern_validator_config_entry *entry) +{ + const char *name_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_NAME); + const char *priv_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_PRIVKEY); + const char *count_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_COUNT); + const char *ip_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_IP); + const char *quic_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_QUIC); + const char *seq_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_SEQ); + const char *hash_dir_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_HASH_SIG_DIR); + + entry->name = dup_trimmed(name_val); + entry->privkey_hex = dup_trimmed(priv_val); + if (!entry->name || !entry->privkey_hex) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + entry->client_kind = classify_validator_client(entry->name); + + int result = derive_peer_id_from_privkey_hex(entry->privkey_hex, &entry->peer_id_text); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + int ok = 0; + entry->count = parse_u64(count_val, &ok); + if (!ok || entry->count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + entry->enr.ip = dup_trimmed(ip_val); + + uint64_t quic_port = parse_u64(quic_val, &ok); + if (!ok || quic_port > UINT16_MAX) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + entry->enr.quic_port = (uint16_t)quic_port; + + entry->enr.sequence = 1; + if (seq_val && *seq_val) + { + entry->enr.sequence = parse_u64(seq_val, &ok); + if (!ok) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + entry->hash_sig_dir = dup_trimmed(hash_dir_val); + return LANTERN_GENESIS_OK; +} + + +int genesis_parse_validator_config(const char *path, struct lantern_validator_config *config) +{ + if (!path || !config) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + char *shuffle = NULL; + int result = read_scalar_value(path, VALIDATOR_CONFIG_SCALAR_SHUFFLE, &shuffle); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + size_t object_count = 0; + LanternYamlObject *objects = lantern_yaml_read_array( + path, + VALIDATOR_CONFIG_ARRAY_VALIDATORS, + &object_count); + if (!objects || object_count == 0) + { + lantern_yaml_free_objects(objects, object_count); + free(shuffle); + return LANTERN_GENESIS_ERR_PARSE; + } + + if (object_count > SIZE_MAX / sizeof(*config->entries)) + { + lantern_yaml_free_objects(objects, object_count); + free(shuffle); + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + struct lantern_validator_config_entry *entries = calloc(object_count, sizeof(*entries)); + if (!entries) + { + lantern_yaml_free_objects(objects, object_count); + free(shuffle); + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + for (size_t i = 0; i < object_count; ++i) + { + result = parse_validator_config_entry(&objects[i], &entries[i]); + if (result != LANTERN_GENESIS_OK) + { + for (size_t j = 0; j <= i; ++j) + { + free_validator_config_entry(&entries[j]); + } + free(entries); + lantern_yaml_free_objects(objects, object_count); + free(shuffle); + return result; + } + } + + lantern_yaml_free_objects(objects, object_count); + + config->shuffle = shuffle; + config->entries = entries; + config->count = object_count; + return LANTERN_GENESIS_OK; +} + + +int genesis_parse_nodes_file(const char *path, struct lantern_enr_record_list *list) +{ + if (!path || !list) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + int result = LANTERN_GENESIS_OK; + char line[GENESIS_LINE_BUFFER_LEN]; + + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed == '\0' || *trimmed == '#') + { + continue; + } + + char *enr = strstr(trimmed, "enr:"); + if (!enr) + { + continue; + } + + enr = lantern_trim_whitespace(enr); + if (!enr || *enr == '\0') + { + continue; + } + + if (lantern_enr_record_list_append(list, enr) != 0) + { + result = LANTERN_GENESIS_ERR_PARSE; + break; + } + } + + fclose(fp); + return result; +} + + +int genesis_read_state_blob(const char *path, uint8_t **bytes, size_t *size) +{ + if (!path || !bytes || !size) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *bytes = NULL; + *size = 0; + + FILE *fp = fopen(path, "rb"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + int result = LANTERN_GENESIS_OK; + + if (fseek(fp, 0, SEEK_END) != 0) + { + result = LANTERN_GENESIS_ERR_IO; + goto cleanup; + } + + long file_size = ftell(fp); + if (file_size <= 0) + { + result = LANTERN_GENESIS_ERR_INVALID_DATA; + goto cleanup; + } + if ((unsigned long long)file_size > SIZE_MAX) + { + result = LANTERN_GENESIS_ERR_OVERFLOW; + goto cleanup; + } + + if (fseek(fp, 0, SEEK_SET) != 0) + { + result = LANTERN_GENESIS_ERR_IO; + goto cleanup; + } + + size_t length = (size_t)file_size; + uint8_t *buffer = malloc(length); + if (!buffer) + { + result = LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + goto cleanup; + } + + size_t read_bytes = fread(buffer, 1, length, fp); + if (read_bytes != length) + { + free(buffer); + result = LANTERN_GENESIS_ERR_IO; + goto cleanup; + } + + *bytes = buffer; + *size = read_bytes; + +cleanup: + fclose(fp); + return result; +} diff --git a/src/genesis/genesis_validator_config.c b/src/genesis/genesis_validator_config.c new file mode 100644 index 0000000..e5f69c5 --- /dev/null +++ b/src/genesis/genesis_validator_config.c @@ -0,0 +1,460 @@ +/** + * @file genesis_validator_config.c + * @brief Validator configuration helpers for genesis bootstrapping. + * + * Implements: + * - Lookup helpers for validator-config entries + * - Default contiguous range assignment + * - Explicit validator index assignment parsing from validators.yaml mappings + * + * @spec Lantern validator-config.yaml and validators.yaml mapping formats. + */ + +#include "lantern/genesis/genesis.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "genesis_internal.h" +#include "lantern/support/strings.h" + +static const size_t GENESIS_LINE_BUFFER_LEN = 2048; +static const size_t GENESIS_INITIAL_INDEX_CAPACITY = 4; + +static uint64_t parse_u64(const char *value, int *ok); +static int compare_u64(const void *lhs, const void *rhs); +static int append_assignment_index(struct lantern_validator_config_entry *entry, uint64_t index); +static int parse_assignment_mapping_key( + char *line, + struct lantern_validator_config *config, + struct lantern_validator_config_entry **out_entry); + +static int parse_assignment_file( + FILE *fp, + struct lantern_validator_config *config, + bool *assigned, + uint64_t validator_count, + bool *out_saw_mapping, + size_t *out_assigned_total); + +static int finalize_assignment_entries( + struct lantern_validator_config *config, + uint64_t validator_count); + + +/** @brief Parse a uint64_t with optional trailing comment. */ +static uint64_t parse_u64(const char *value, int *ok) +{ + if (ok) + { + *ok = 0; + } + if (!value) + { + return 0; + } + + errno = 0; + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 0); + if (errno != 0 || end == value) + { + return 0; + } + + while (end && *end && isspace((unsigned char)*end)) + { + ++end; + } + if (end && *end != '\0' && *end != '#') + { + return 0; + } + if (parsed > UINT64_MAX) + { + return 0; + } + + if (ok) + { + *ok = 1; + } + return (uint64_t)parsed; +} + + +/** @brief Compare two uint64_t values for qsort. */ +static int compare_u64(const void *lhs, const void *rhs) +{ + const uint64_t *a = lhs; + const uint64_t *b = rhs; + if (*a < *b) + { + return -1; + } + if (*a > *b) + { + return 1; + } + return 0; +} + + +/** @brief Append a validator index to an entry's explicit assignment list. */ +static int append_assignment_index(struct lantern_validator_config_entry *entry, uint64_t index) +{ + if (!entry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + for (size_t i = 0; i < entry->indices_len; ++i) + { + if (entry->indices[i] == index) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + if (entry->indices_len == entry->indices_cap) + { + if (entry->indices_cap > SIZE_MAX / 2) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + size_t new_cap = GENESIS_INITIAL_INDEX_CAPACITY; + if (entry->indices_cap != 0) + { + new_cap = entry->indices_cap * 2; + } + if (new_cap < (entry->indices_len + 1)) + { + new_cap = entry->indices_len + 1; + } + if (new_cap > SIZE_MAX / sizeof(*entry->indices)) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + void *grown = realloc(entry->indices, new_cap * sizeof(*entry->indices)); + if (!grown) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + entry->indices = grown; + entry->indices_cap = new_cap; + } + + entry->indices[entry->indices_len] = index; + entry->indices_len++; + return LANTERN_GENESIS_OK; +} + + +/** @brief Parse a YAML mapping key of the form ":" with no inline value. */ +static int parse_assignment_mapping_key( + char *line, + struct lantern_validator_config *config, + struct lantern_validator_config_entry **out_entry) +{ + if (!line || !config || !out_entry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_entry = NULL; + + char *colon = strchr(line, ':'); + if (!colon) + { + return 0; + } + + for (char *p = colon + 1; p && *p; ++p) + { + if (!isspace((unsigned char)*p)) + { + return 0; + } + } + + *colon = '\0'; + char *name = lantern_trim_whitespace(line); + if (!name || *name == '\0') + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + size_t name_len = strlen(name); + if (name_len >= 2 + && ((name[0] == '"' && name[name_len - 1] == '"') + || (name[0] == '\'' && name[name_len - 1] == '\''))) + { + name[name_len - 1] = '\0'; + ++name; + } + + *out_entry = lantern_validator_config_find(config, name); + return 1; +} + + +/** @brief Parse validators.yaml mapping into explicit indices on config entries. */ +static int parse_assignment_file( + FILE *fp, + struct lantern_validator_config *config, + bool *assigned, + uint64_t validator_count, + bool *out_saw_mapping, + size_t *out_assigned_total) +{ + if (!fp || !config || !out_saw_mapping || !out_assigned_total) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_saw_mapping = false; + *out_assigned_total = 0; + + struct lantern_validator_config_entry *current = NULL; + + char line[GENESIS_LINE_BUFFER_LEN]; + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed == '\0' || *trimmed == '#') + { + continue; + } + + if (*trimmed == '-') + { + if (!current || !assigned) + { + continue; + } + + char *value = lantern_trim_whitespace(trimmed + 1); + if (!value || *value == '\0') + { + continue; + } + + int ok = 0; + uint64_t parsed = parse_u64(value, &ok); + if (!ok || parsed >= validator_count) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + if (assigned[(size_t)parsed]) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + int result = append_assignment_index(current, parsed); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + assigned[(size_t)parsed] = true; + (*out_assigned_total)++; + continue; + } + + struct lantern_validator_config_entry *entry = NULL; + int mapping_rc = parse_assignment_mapping_key(trimmed, config, &entry); + if (mapping_rc < 0) + { + return mapping_rc; + } + if (mapping_rc == 0) + { + current = NULL; + continue; + } + + current = entry; + if (entry) + { + *out_saw_mapping = true; + entry->indices_len = 0; + } + } + + return LANTERN_GENESIS_OK; +} + + +/** @brief Validate and finalize assignment entries after parsing. */ +static int finalize_assignment_entries( + struct lantern_validator_config *config, + uint64_t validator_count) +{ + if (!config || !config->entries || config->count == 0 || validator_count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + for (size_t i = 0; i < config->count; ++i) + { + struct lantern_validator_config_entry *entry = &config->entries[i]; + if (entry->indices_len != entry->count || entry->indices_len == 0) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + qsort(entry->indices, entry->indices_len, sizeof(*entry->indices), compare_u64); + + entry->start_index = entry->indices[0]; + + uint64_t last = entry->indices[entry->indices_len - 1]; + if (last == UINT64_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + entry->end_index = last + 1; + entry->has_range = true; + } + + return LANTERN_GENESIS_OK; +} + + +struct lantern_validator_config_entry *lantern_validator_config_find( + struct lantern_validator_config *config, + const char *name) +{ + if (!config || !name || !config->entries) + { + return NULL; + } + + for (size_t i = 0; i < config->count; ++i) + { + if (config->entries[i].name && strcmp(config->entries[i].name, name) == 0) + { + return &config->entries[i]; + } + } + + return NULL; +} + + +int lantern_validator_config_assign_ranges( + struct lantern_validator_config *config, + uint64_t validator_count) +{ + if (!config || !config->entries || config->count == 0 || validator_count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + uint64_t next_index = 0; + for (size_t i = 0; i < config->count; ++i) + { + struct lantern_validator_config_entry *entry = &config->entries[i]; + if (entry->count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + entry->start_index = next_index; + + if (entry->count > (validator_count - next_index)) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + uint64_t end = next_index + entry->count; + entry->end_index = end; + entry->has_range = true; + next_index = end; + } + + if (next_index != validator_count) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + return LANTERN_GENESIS_OK; +} + + +int lantern_validator_config_apply_assignments( + struct lantern_validator_config *config, + const char *path, + uint64_t validator_count) +{ + if (!config || !config->entries || config->count == 0 || !path) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (validator_count > SIZE_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + size_t assigned_len = (size_t)validator_count; + bool *assigned = NULL; + if (assigned_len > 0) + { + assigned = calloc(assigned_len, sizeof(*assigned)); + if (!assigned) + { + fclose(fp); + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + } + + bool saw_mapping = false; + size_t assigned_total = 0; + int result = parse_assignment_file( + fp, + config, + assigned, + validator_count, + &saw_mapping, + &assigned_total); + + fclose(fp); + + if (!saw_mapping) + { + free(assigned); + return (result == LANTERN_GENESIS_OK) ? LANTERN_GENESIS_OK : result; + } + + if (result != LANTERN_GENESIS_OK) + { + free(assigned); + return result; + } + + if (assigned_total != assigned_len) + { + free(assigned); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + result = finalize_assignment_entries(config, validator_count); + free(assigned); + return result; +} diff --git a/tests/unit/test_snappy.c b/tests/unit/test_snappy.c index d595556..1c148be 100644 --- a/tests/unit/test_snappy.c +++ b/tests/unit/test_snappy.c @@ -32,6 +32,33 @@ static void fill_pattern(uint8_t *dst, size_t len, uint8_t seed) { } } +static uint32_t crc32c(const uint8_t *data, size_t len) { + const uint32_t poly = 0x82F63B78u; + uint32_t crc = 0xFFFFFFFFu; + for (size_t i = 0; i < len; ++i) { + crc ^= (uint32_t)data[i]; + for (size_t bit = 0; bit < 8u; ++bit) { + if (crc & 1u) { + crc = (crc >> 1u) ^ poly; + } else { + crc >>= 1u; + } + } + } + return ~crc; +} + +static uint32_t mask_crc32c(uint32_t crc) { + return ((crc >> 15u) | (crc << 17u)) + 0xA282EAD8u; +} + +static void write_u32_le(uint32_t value, uint8_t *dst) { + dst[0] = (uint8_t)(value & 0xFFu); + dst[1] = (uint8_t)((value >> 8u) & 0xFFu); + dst[2] = (uint8_t)((value >> 16u) & 0xFFu); + dst[3] = (uint8_t)((value >> 24u) & 0xFFu); +} + static size_t append_chunk(uint8_t type, const uint8_t *payload, size_t payload_len, uint8_t *dst) { dst[0] = type; dst[1] = (uint8_t)(payload_len & 0xffu); @@ -112,7 +139,8 @@ static void test_invalid_payload(void) { static void test_framed_uncompressed_chunks(void) { uint8_t payload[] = {0x00, 0x11, 0x22, 0x33}; uint8_t chunk_data[sizeof(payload) + 4u]; - memset(chunk_data, 0, 4u); + uint32_t crc = mask_crc32c(crc32c(payload, sizeof(payload))); + write_u32_le(crc, chunk_data); memcpy(chunk_data + 4u, payload, sizeof(payload)); uint8_t frame[64]; From 189929f2b45b42606f0e392a137ad82a70655010 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:12:53 +1000 Subject: [PATCH 07/12] Update genesis.c --- src/genesis/genesis.c | 140 +++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 63 deletions(-) diff --git a/src/genesis/genesis.c b/src/genesis/genesis.c index 211188a..d368855 100644 --- a/src/genesis/genesis.c +++ b/src/genesis/genesis.c @@ -10,18 +10,14 @@ #include "lantern/genesis/genesis.h" -#include +#include +#include #include #include #include "genesis_internal.h" #include "lantern/support/log.h" -static int load_chain_config_and_pubkeys( - struct lantern_chain_config *config, - const char *config_path); - - /** * Initialize a genesis artifacts container. * @@ -72,6 +68,81 @@ void lantern_genesis_artifacts_reset(struct lantern_genesis_artifacts *artifacts } +/** + * @brief Loads chain configuration and genesis validator pubkeys. + * + * Populates `config` by parsing the chain configuration file. On success, any + * genesis pubkeys returned via `config->validator_pubkeys` are owned by `config` + * and must be freed by the caller (typically via `lantern_genesis_artifacts_reset()`). + * + * @param config Chain config to populate. + * @param config_path Path to the chain config file. + * + * @return LANTERN_GENESIS_OK on success + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on NULL inputs + * @return LANTERN_GENESIS_ERR_IO on file I/O failures + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure + * @return LANTERN_GENESIS_ERR_INVALID_DATA on parse/validation failures + * + * @note Thread safety: Caller must ensure exclusive access to `config`. + */ +static int load_chain_config_and_pubkeys( + struct lantern_chain_config *config, + const char *config_path) +{ + if (!config || !config_path) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + int result = genesis_parse_chain_config(config_path, config); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error("genesis", NULL, "failed to parse chain config at %s", config_path); + return result; + } + + uint8_t *pubkeys = NULL; + size_t pubkey_count = 0; + result = genesis_parse_genesis_validator_pubkeys(config_path, &pubkeys, &pubkey_count); + if (result != LANTERN_GENESIS_OK) + { + lantern_log_error("genesis", NULL, "failed to parse genesis pubkeys at %s", config_path); + return result; + } + + if (pubkeys && pubkey_count > 0) + { + config->validator_pubkeys = pubkeys; + config->validator_pubkeys_count = pubkey_count; + if (config->validator_count == 0) + { + config->validator_count = pubkey_count; + } + + lantern_log_info( + "genesis", + NULL, + "loaded %zu genesis pubkeys from %s", + pubkey_count, + config_path); + } + else + { + free(pubkeys); + lantern_log_warn("genesis", NULL, "no genesis pubkeys found in %s", config_path); + } + + if (config->validator_count == 0) + { + lantern_log_error("genesis", NULL, "validator count missing or zero in %s", config_path); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + return LANTERN_GENESIS_OK; +} + + /** * Load genesis artifacts from disk. * @@ -176,60 +247,3 @@ int lantern_genesis_load( lantern_genesis_artifacts_reset(artifacts); return result; } - - -static int load_chain_config_and_pubkeys( - struct lantern_chain_config *config, - const char *config_path) -{ - if (!config || !config_path) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - int result = genesis_parse_chain_config(config_path, config); - if (result != LANTERN_GENESIS_OK) - { - lantern_log_error("genesis", NULL, "failed to parse chain config at %s", config_path); - return result; - } - - uint8_t *pubkeys = NULL; - size_t pubkey_count = 0; - result = genesis_parse_genesis_validator_pubkeys(config_path, &pubkeys, &pubkey_count); - if (result != LANTERN_GENESIS_OK) - { - lantern_log_error("genesis", NULL, "failed to parse genesis pubkeys at %s", config_path); - return result; - } - - if (pubkeys && pubkey_count > 0) - { - config->validator_pubkeys = pubkeys; - config->validator_pubkeys_count = pubkey_count; - if (config->validator_count == 0) - { - config->validator_count = pubkey_count; - } - - lantern_log_info( - "genesis", - NULL, - "loaded %zu genesis pubkeys from %s", - pubkey_count, - config_path); - } - else - { - free(pubkeys); - lantern_log_warn("genesis", NULL, "no genesis pubkeys found in %s", config_path); - } - - if (config->validator_count == 0) - { - lantern_log_error("genesis", NULL, "validator count missing or zero in %s", config_path); - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - - return LANTERN_GENESIS_OK; -} From c354457042b8dc738a205d57c61e36fdd14e8115 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:56:49 +1000 Subject: [PATCH 08/12] Update genesis_validator_config.c --- src/genesis/genesis_validator_config.c | 305 +++++++++++++++++++------ 1 file changed, 235 insertions(+), 70 deletions(-) diff --git a/src/genesis/genesis_validator_config.c b/src/genesis/genesis_validator_config.c index e5f69c5..ad64848 100644 --- a/src/genesis/genesis_validator_config.c +++ b/src/genesis/genesis_validator_config.c @@ -14,8 +14,8 @@ #include #include -#include #include +#include #include #include #include @@ -26,20 +26,21 @@ static const size_t GENESIS_LINE_BUFFER_LEN = 2048; static const size_t GENESIS_INITIAL_INDEX_CAPACITY = 4; -static uint64_t parse_u64(const char *value, int *ok); +static uint64_t parse_u64(const char *value, bool *out_is_valid); static int compare_u64(const void *lhs, const void *rhs); static int append_assignment_index(struct lantern_validator_config_entry *entry, uint64_t index); static int parse_assignment_mapping_key( - char *line, struct lantern_validator_config *config, + char *line, + bool *out_is_mapping_key, struct lantern_validator_config_entry **out_entry); static int parse_assignment_file( - FILE *fp, struct lantern_validator_config *config, + FILE *fp, bool *assigned, uint64_t validator_count, - bool *out_saw_mapping, + bool *out_has_matching_entry, size_t *out_assigned_total); static int finalize_assignment_entries( @@ -47,12 +48,25 @@ static int finalize_assignment_entries( uint64_t validator_count); -/** @brief Parse a uint64_t with optional trailing comment. */ -static uint64_t parse_u64(const char *value, int *ok) +/** + * Parse an unsigned 64-bit integer from a string, allowing a trailing comment. + * + * The parsed value may be followed by whitespace and an optional `#` comment. + * Callers must use `out_is_valid` to disambiguate a successful parse of `0` + * from a failure. + * + * @param value Input string to parse (not modified). + * @param out_is_valid Optional output flag set to true on success, false on failure. + * + * @return Parsed value on success, 0 on failure. + * + * @note Thread safety: Thread-safe. + */ +static uint64_t parse_u64(const char *value, bool *out_is_valid) { - if (ok) + if (out_is_valid) { - *ok = 0; + *out_is_valid = false; } if (!value) { @@ -67,37 +81,48 @@ static uint64_t parse_u64(const char *value, int *ok) return 0; } - while (end && *end && isspace((unsigned char)*end)) + while (*end != '\0' && isspace((unsigned char)*end)) { ++end; } - if (end && *end != '\0' && *end != '#') + if (*end != '\0' && *end != '#') { return 0; } - if (parsed > UINT64_MAX) + if (parsed > (unsigned long long)UINT64_MAX) { return 0; } - if (ok) + if (out_is_valid) { - *ok = 1; + *out_is_valid = true; } return (uint64_t)parsed; } -/** @brief Compare two uint64_t values for qsort. */ +/** + * Compare two `uint64_t` values for ascending sort order. + * + * @param lhs Pointer to a `uint64_t`. + * @param rhs Pointer to a `uint64_t`. + * + * @return -1 if lhs < rhs. + * @return 1 if lhs > rhs. + * @return 0 if equal. + * + * @note Thread safety: Thread-safe. + */ static int compare_u64(const void *lhs, const void *rhs) { - const uint64_t *a = lhs; - const uint64_t *b = rhs; - if (*a < *b) + const uint64_t *left_value = lhs; + const uint64_t *right_value = rhs; + if (*left_value < *right_value) { return -1; } - if (*a > *b) + if (*left_value > *right_value) { return 1; } @@ -105,7 +130,22 @@ static int compare_u64(const void *lhs, const void *rhs) } -/** @brief Append a validator index to an entry's explicit assignment list. */ +/** + * Append a validator index to an entry's explicit index list. + * + * Rejects duplicate indices and grows `entry->indices` as needed. + * + * @param entry Entry to update. + * @param index Validator index to append. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM if entry is NULL. + * @return LANTERN_GENESIS_ERR_INVALID_DATA if index is already present. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on capacity overflow. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to entry. + */ static int append_assignment_index(struct lantern_validator_config_entry *entry, uint64_t index) { if (!entry) @@ -113,9 +153,9 @@ static int append_assignment_index(struct lantern_validator_config_entry *entry, return LANTERN_GENESIS_ERR_INVALID_PARAM; } - for (size_t i = 0; i < entry->indices_len; ++i) + for (size_t index_pos = 0; index_pos < entry->indices_len; ++index_pos) { - if (entry->indices[i] == index) + if (entry->indices[index_pos] == index) { return LANTERN_GENESIS_ERR_INVALID_DATA; } @@ -123,6 +163,11 @@ static int append_assignment_index(struct lantern_validator_config_entry *entry, if (entry->indices_len == entry->indices_cap) { + if (entry->indices_len == SIZE_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + if (entry->indices_cap > SIZE_MAX / 2) { return LANTERN_GENESIS_ERR_OVERFLOW; @@ -158,31 +203,52 @@ static int append_assignment_index(struct lantern_validator_config_entry *entry, } -/** @brief Parse a YAML mapping key of the form ":" with no inline value. */ +/** + * Parse a validators.yaml mapping key line. + * + * Accepts `:` lines with optional trailing whitespace and/or a `#` comment, + * and rejects inline values (e.g. `name: 1`). When a mapping is detected, `line` + * is modified in place by replacing the colon with `\0`. + * + * @param config Validator config to search. + * @param line Line buffer to parse (modified in place). + * @param out_is_mapping_key Set to true if the line is a mapping key, false otherwise. + * @param out_entry Set to the matching entry, or NULL if not found. + * + * @return LANTERN_GENESIS_OK on success (including non-mapping lines). + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_INVALID_DATA if the mapping key is empty. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ static int parse_assignment_mapping_key( - char *line, struct lantern_validator_config *config, + char *line, + bool *out_is_mapping_key, struct lantern_validator_config_entry **out_entry) { - if (!line || !config || !out_entry) + if (!config || !line || !out_is_mapping_key || !out_entry) { return LANTERN_GENESIS_ERR_INVALID_PARAM; } + *out_is_mapping_key = false; *out_entry = NULL; char *colon = strchr(line, ':'); if (!colon) { - return 0; + return LANTERN_GENESIS_OK; } - for (char *p = colon + 1; p && *p; ++p) + char *value = colon + 1; + while (*value != '\0' && isspace((unsigned char)*value)) { - if (!isspace((unsigned char)*p)) - { - return 0; - } + ++value; + } + if (*value != '\0' && *value != '#') + { + return LANTERN_GENESIS_OK; } *colon = '\0'; @@ -201,26 +267,53 @@ static int parse_assignment_mapping_key( ++name; } + if (*name == '\0') + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + *out_entry = lantern_validator_config_find(config, name); - return 1; + *out_is_mapping_key = true; + return LANTERN_GENESIS_OK; } -/** @brief Parse validators.yaml mapping into explicit indices on config entries. */ +/** + * Parse a validators.yaml mapping file into explicit indices on config entries. + * + * Updates `entry->indices` for each mapping key that matches a config entry. List + * items under unknown mapping keys are ignored. If `assigned` is provided, it is + * used as a global uniqueness tracker across all entries. + * + * @param config Validator config to update in place. + * @param fp Open file handle to read from. + * @param assigned Optional bitset of length validator_count, or NULL. + * @param validator_count Maximum allowed validator index is validator_count - 1. + * @param out_has_matching_entry Set true if at least one mapping key matched an entry. + * @param out_assigned_total Total number of unique indices assigned across entries. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on malformed, duplicate, or out-of-range data. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on overflow. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ static int parse_assignment_file( - FILE *fp, struct lantern_validator_config *config, + FILE *fp, bool *assigned, uint64_t validator_count, - bool *out_saw_mapping, + bool *out_has_matching_entry, size_t *out_assigned_total) { - if (!fp || !config || !out_saw_mapping || !out_assigned_total) + if (!fp || !config || !out_has_matching_entry || !out_assigned_total) { return LANTERN_GENESIS_ERR_INVALID_PARAM; } - *out_saw_mapping = false; + *out_has_matching_entry = false; *out_assigned_total = 0; struct lantern_validator_config_entry *current = NULL; @@ -247,9 +340,9 @@ static int parse_assignment_file( continue; } - int ok = 0; - uint64_t parsed = parse_u64(value, &ok); - if (!ok || parsed >= validator_count) + bool is_valid_index = false; + uint64_t parsed = parse_u64(value, &is_valid_index); + if (!is_valid_index || parsed >= validator_count) { return LANTERN_GENESIS_ERR_INVALID_DATA; } @@ -271,12 +364,13 @@ static int parse_assignment_file( } struct lantern_validator_config_entry *entry = NULL; - int mapping_rc = parse_assignment_mapping_key(trimmed, config, &entry); - if (mapping_rc < 0) + bool is_mapping_key = false; + int mapping_rc = parse_assignment_mapping_key(config, trimmed, &is_mapping_key, &entry); + if (mapping_rc != LANTERN_GENESIS_OK) { return mapping_rc; } - if (mapping_rc == 0) + if (!is_mapping_key) { current = NULL; continue; @@ -285,7 +379,7 @@ static int parse_assignment_file( current = entry; if (entry) { - *out_saw_mapping = true; + *out_has_matching_entry = true; entry->indices_len = 0; } } @@ -294,7 +388,22 @@ static int parse_assignment_file( } -/** @brief Validate and finalize assignment entries after parsing. */ +/** + * Validate and finalize config entries after explicit assignments are parsed. + * + * Ensures each entry has exactly `entry->count` explicit indices, sorts them, + * and derives a `[start_index, end_index)` range from the minimum and maximum. + * + * @param config Validator config to validate and update in place. + * @param validator_count Total number of validators (must be non-zero). + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_INVALID_DATA if any entry is missing indices or has a mismatch. + * @return LANTERN_GENESIS_ERR_OVERFLOW if computing the end index would overflow. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ static int finalize_assignment_entries( struct lantern_validator_config *config, uint64_t validator_count) @@ -304,9 +413,9 @@ static int finalize_assignment_entries( return LANTERN_GENESIS_ERR_INVALID_PARAM; } - for (size_t i = 0; i < config->count; ++i) + for (size_t entry_index = 0; entry_index < config->count; ++entry_index) { - struct lantern_validator_config_entry *entry = &config->entries[i]; + struct lantern_validator_config_entry *entry = &config->entries[entry_index]; if (entry->indices_len != entry->count || entry->indices_len == 0) { return LANTERN_GENESIS_ERR_INVALID_DATA; @@ -330,6 +439,18 @@ static int finalize_assignment_entries( } +/** + * Find a validator config entry by name. + * + * @spec Lantern validator-config.yaml and validators.yaml mapping formats. + * + * @param config Validator config to search. + * @param name Entry name to match (exact string compare). + * + * @return Matching entry pointer, or NULL if not found. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ struct lantern_validator_config_entry *lantern_validator_config_find( struct lantern_validator_config *config, const char *name) @@ -339,11 +460,12 @@ struct lantern_validator_config_entry *lantern_validator_config_find( return NULL; } - for (size_t i = 0; i < config->count; ++i) + for (size_t entry_index = 0; entry_index < config->count; ++entry_index) { - if (config->entries[i].name && strcmp(config->entries[i].name, name) == 0) + if (config->entries[entry_index].name + && strcmp(config->entries[entry_index].name, name) == 0) { - return &config->entries[i]; + return &config->entries[entry_index]; } } @@ -351,6 +473,23 @@ struct lantern_validator_config_entry *lantern_validator_config_find( } +/** + * Assign contiguous validator index ranges for each config entry. + * + * Entries are assigned sequential ranges starting at index 0, in the order they + * appear in `config->entries`. The assigned range is `[start_index, end_index)`. + * + * @spec Lantern validator-config.yaml and validators.yaml mapping formats. + * + * @param config Validator config to update in place. + * @param validator_count Total number of validators expected across all entries. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_INVALID_DATA if counts do not sum to validator_count. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ int lantern_validator_config_assign_ranges( struct lantern_validator_config *config, uint64_t validator_count) @@ -361,9 +500,9 @@ int lantern_validator_config_assign_ranges( } uint64_t next_index = 0; - for (size_t i = 0; i < config->count; ++i) + for (size_t entry_index = 0; entry_index < config->count; ++entry_index) { - struct lantern_validator_config_entry *entry = &config->entries[i]; + struct lantern_validator_config_entry *entry = &config->entries[entry_index]; if (entry->count == 0) { return LANTERN_GENESIS_ERR_INVALID_DATA; @@ -391,12 +530,39 @@ int lantern_validator_config_assign_ranges( } +/** + * Apply explicit validator index assignments from a validators.yaml mapping file. + * + * The file is expected to contain YAML mappings of the form: + * + * : + * - + * - ... + * + * Each config entry must list exactly `entry->count` unique indices, and the union + * of all indices must cover `[0, validator_count)` with no duplicates. + * + * @spec Lantern validator-config.yaml and validators.yaml mapping formats. + * + * @param config Validator config to update in place. + * @param path Filesystem path to validators.yaml. + * @param validator_count Total number of validators expected. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO if the file cannot be opened. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on malformed or inconsistent assignments. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/count overflow. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ int lantern_validator_config_apply_assignments( struct lantern_validator_config *config, const char *path, uint64_t validator_count) { - if (!config || !config->entries || config->count == 0 || !path) + if (!config || !config->entries || config->count == 0 || !path || validator_count == 0) { return LANTERN_GENESIS_ERR_INVALID_PARAM; } @@ -406,6 +572,7 @@ int lantern_validator_config_apply_assignments( return LANTERN_GENESIS_ERR_OVERFLOW; } + int result = LANTERN_GENESIS_OK; FILE *fp = fopen(path, "r"); if (!fp) { @@ -419,42 +586,40 @@ int lantern_validator_config_apply_assignments( assigned = calloc(assigned_len, sizeof(*assigned)); if (!assigned) { - fclose(fp); - return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + result = LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + goto cleanup; } } - bool saw_mapping = false; + bool has_matching_entry = false; size_t assigned_total = 0; - int result = parse_assignment_file( - fp, + result = parse_assignment_file( config, + fp, assigned, validator_count, - &saw_mapping, + &has_matching_entry, &assigned_total); - - fclose(fp); - - if (!saw_mapping) + if (result != LANTERN_GENESIS_OK) { - free(assigned); - return (result == LANTERN_GENESIS_OK) ? LANTERN_GENESIS_OK : result; + goto cleanup; } - if (result != LANTERN_GENESIS_OK) + if (!has_matching_entry) { - free(assigned); - return result; + goto cleanup; } if (assigned_total != assigned_len) { - free(assigned); - return LANTERN_GENESIS_ERR_INVALID_DATA; + result = LANTERN_GENESIS_ERR_INVALID_DATA; + goto cleanup; } result = finalize_assignment_entries(config, validator_count); + +cleanup: + fclose(fp); free(assigned); return result; } From 99470991b351f90d24ed3fa0d8811916cbedde10 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:43:41 +1000 Subject: [PATCH 09/12] Refactor genesis --- CMakeLists.txt | 13 +- src/genesis/genesis_parse.c | 877 ++++++++++----------------- src/genesis/genesis_parse_registry.c | 838 +++++++++++++++++++++++++ 3 files changed, 1177 insertions(+), 551 deletions(-) create mode 100644 src/genesis/genesis_parse_registry.c diff --git a/CMakeLists.txt b/CMakeLists.txt index cbec282..fe39444 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,12 +50,13 @@ add_library(lantern STATIC src/core/client_sync_blocks.c src/core/client_sync_votes.c src/core/client_utils.c - src/core/client_validator.c - src/genesis/genesis.c - src/genesis/genesis_parse.c - src/genesis/genesis_validator_config.c - src/encoding/snappy.c - src/networking/enr.c + src/core/client_validator.c + src/genesis/genesis.c + src/genesis/genesis_parse.c + src/genesis/genesis_parse_registry.c + src/genesis/genesis_validator_config.c + src/encoding/snappy.c + src/networking/enr.c src/networking/gossip.c src/networking/gossipsub_service.c src/networking/gossip_payloads.c diff --git a/src/genesis/genesis_parse.c b/src/genesis/genesis_parse.c index ac8a8f9..f97e06f 100644 --- a/src/genesis/genesis_parse.c +++ b/src/genesis/genesis_parse.c @@ -31,81 +31,52 @@ static const size_t GENESIS_LINE_BUFFER_LEN = 2048; static const size_t GENESIS_SMALL_LINE_BUFFER_LEN = 1024; static const size_t GENESIS_INITIAL_PUBKEY_CAPACITY = 4; -static const size_t GENESIS_INITIAL_MAPPING_INDEX_CAPACITY = 8; static const size_t GENESIS_PEER_ID_BUFFER_LEN = 128; static const size_t GENESIS_PUBKEY_HEX_BUFFER_LEN = (LANTERN_VALIDATOR_PUBKEY_SIZE * 2u) + 3u; -static const char *CHAIN_CONFIG_KEY_GENESIS_TIME = "GENESIS_TIME"; -static const char *CHAIN_CONFIG_KEY_VALIDATOR_COUNT = "VALIDATOR_COUNT"; -static const char *CHAIN_CONFIG_KEY_GENESIS_VALIDATORS = "GENESIS_VALIDATORS"; - -static const char *VALIDATOR_REGISTRY_ARRAY_KEY = "validators"; -static const char *VALIDATOR_REGISTRY_FIELD_INDEX = "index"; -static const char *VALIDATOR_REGISTRY_FIELD_PUBKEY = "pubkey"; -static const char *VALIDATOR_REGISTRY_FIELD_WITHDRAWAL_CREDENTIALS = "withdrawal_credentials"; - -static const char *VALIDATOR_CONFIG_SCALAR_SHUFFLE = "shuffle"; -static const char *VALIDATOR_CONFIG_ARRAY_VALIDATORS = "validators"; -static const char *VALIDATOR_CONFIG_FIELD_NAME = "name"; -static const char *VALIDATOR_CONFIG_FIELD_PRIVKEY = "privkey"; -static const char *VALIDATOR_CONFIG_FIELD_COUNT = "count"; -static const char *VALIDATOR_CONFIG_FIELD_IP = "ip"; -static const char *VALIDATOR_CONFIG_FIELD_QUIC = "quic"; -static const char *VALIDATOR_CONFIG_FIELD_SEQ = "seq"; -static const char *VALIDATOR_CONFIG_FIELD_HASH_SIG_DIR = "hashSigDir"; +static const char *const CHAIN_CONFIG_KEY_GENESIS_TIME = "GENESIS_TIME"; +static const char *const CHAIN_CONFIG_KEY_VALIDATOR_COUNT = "VALIDATOR_COUNT"; +static const char *const CHAIN_CONFIG_KEY_GENESIS_VALIDATORS = "GENESIS_VALIDATORS"; + +static const char *const VALIDATOR_CONFIG_SCALAR_SHUFFLE = "shuffle"; +static const char *const VALIDATOR_CONFIG_ARRAY_VALIDATORS = "validators"; +static const char *const VALIDATOR_CONFIG_FIELD_NAME = "name"; +static const char *const VALIDATOR_CONFIG_FIELD_PRIVKEY = "privkey"; +static const char *const VALIDATOR_CONFIG_FIELD_COUNT = "count"; +static const char *const VALIDATOR_CONFIG_FIELD_IP = "ip"; +static const char *const VALIDATOR_CONFIG_FIELD_QUIC = "quic"; +static const char *const VALIDATOR_CONFIG_FIELD_SEQ = "seq"; +static const char *const VALIDATOR_CONFIG_FIELD_HASH_SIG_DIR = "hashSigDir"; static uint64_t parse_u64(const char *value, int *ok); static char *dup_trimmed(const char *value); +static char *strip_optional_quotes(char *value); static const char *yaml_object_value(const LanternYamlObject *object, const char *key); static int read_scalar_value(const char *path, const char *key, char **out_value); static enum lantern_validator_client_kind classify_validator_client(const char *name); static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id); static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]); -static int set_record_pubkey(struct lantern_validator_record *record); static int ensure_pubkey_capacity(uint8_t **pubkeys, size_t *cap, size_t required); -static int collect_registry_mapping_indices( - const char *path, - size_t **out_indices, - size_t *out_count, - size_t *out_max_index); -static int validate_registry_index_coverage( - const size_t *indices, - size_t count, - size_t max_index, - size_t *out_record_count); -static int build_index_only_registry( - size_t record_count, - struct lantern_validator_registry *registry); -static int scan_registry_objects( - const LanternYamlObject *objects, - size_t object_count, - bool *out_has_pubkey_field, - bool *out_have_explicit_indices, - size_t *out_record_count); -static int populate_registry_records_from_objects( - LanternYamlObject *objects, - size_t object_count, - bool has_pubkey_field, - bool have_explicit_indices, - struct lantern_validator_record *records, - size_t record_count, - bool *assigned); -static int validate_registry_full_coverage(const bool *assigned, size_t record_count); -static int parse_validator_registry_objects( - LanternYamlObject *objects, - size_t object_count, - struct lantern_validator_registry *registry); -static int parse_validator_registry_mapping( - const char *path, - struct lantern_validator_registry *registry); static int parse_validator_config_entry( const LanternYamlObject *object, struct lantern_validator_config_entry *entry); static void free_validator_config_entry(struct lantern_validator_config_entry *entry); -/** @brief Parse a uint64_t with optional trailing comment. */ +/** + * Parse an unsigned 64-bit integer from a string, allowing a trailing comment. + * + * The parsed value may be followed by whitespace and an optional `#` comment. + * Callers must use `ok` to disambiguate a successful parse of `0` from failure. + * + * @param value Input string to parse (not modified). + * @param ok Optional output flag set to 1 on success, 0 on failure. + * + * @return Parsed value on success, 0 on failure. + * + * @note Thread safety: Thread-safe. + */ static uint64_t parse_u64(const char *value, int *ok) { if (ok) @@ -133,7 +104,7 @@ static uint64_t parse_u64(const char *value, int *ok) { return 0; } - if (parsed > UINT64_MAX) + if (parsed > (unsigned long long)UINT64_MAX) { return 0; } @@ -146,7 +117,15 @@ static uint64_t parse_u64(const char *value, int *ok) } -/** @brief Duplicate a string after trimming whitespace and optional surrounding quotes. */ +/** + * Duplicate a string after trimming whitespace and optional surrounding quotes. + * + * @param value Input string to trim and duplicate. + * + * @return Newly allocated trimmed string, or NULL on allocation failure. + * + * @note Thread safety: Thread-safe. + */ static char *dup_trimmed(const char *value) { if (!value) @@ -177,7 +156,50 @@ static char *dup_trimmed(const char *value) } -/** @brief Lookup a key value in a YAML object. */ +/** + * Strip optional surrounding quotes from a YAML scalar (in place). + * + * Removes matching surrounding single/double quotes. The input buffer is + * modified in place. + * + * @param value Input string buffer to modify. + * + * @return Pointer to the unquoted string within `value`. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to `value`. + */ +static char *strip_optional_quotes(char *value) +{ + if (!value || *value == '\0') + { + return value; + } + + if (*value == '"' || *value == '\'') + { + char quote = *value; + ++value; + char *endq = strrchr(value, quote); + if (endq) + { + *endq = '\0'; + } + } + + return value; +} + + +/** + * Lookup a key value in a YAML object. + * + * @param object YAML object to search. + * @param key Key to match (exact string compare). + * + * @return Matching value pointer, or NULL if not found. + * + * @note Thread safety: Thread-safe. + */ static const char *yaml_object_value(const LanternYamlObject *object, const char *key) { if (!object || !key) @@ -197,7 +219,23 @@ static const char *yaml_object_value(const LanternYamlObject *object, const char } -/** @brief Read a top-level scalar value from a YAML file (`key: value`). */ +/** + * Read a top-level scalar value from a YAML file (`key: value`). + * + * On success, `*out_value` is allocated and must be freed by the caller. + * + * @param path Filesystem path to the YAML file. + * @param key Scalar key to read. + * @param out_value Output pointer for the allocated value string. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO if the file cannot be opened. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_PARSE if the key cannot be found. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ static int read_scalar_value(const char *path, const char *key, char **out_value) { if (!path || !key || !out_value) @@ -245,7 +283,15 @@ static int read_scalar_value(const char *path, const char *key, char **out_value } -/** @brief Classify validator client kind based on its name prefix. */ +/** + * Classify validator client kind based on its name prefix. + * + * @param name Validator client name. + * + * @return Parsed validator client kind enum value. + * + * @note Thread safety: Thread-safe. + */ static enum lantern_validator_client_kind classify_validator_client(const char *name) { if (!name) @@ -274,7 +320,21 @@ static enum lantern_validator_client_kind classify_validator_client(const char * } -/** @brief Derive a libp2p peer ID from a secp256k1 private key (hex). */ +/** + * Derive a libp2p peer ID from a secp256k1 private key (hex). + * + * On success, `*out_peer_id` is allocated and must be freed by the caller. + * + * @param hex Private key as a hex string. + * @param out_peer_id Output pointer for the allocated peer ID text. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_PARSE on decode/derivation failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id) { if (!hex || !out_peer_id) @@ -337,7 +397,18 @@ static int derive_peer_id_from_privkey_hex(const char *hex, char **out_peer_id) } -/** @brief Decode a validator pubkey hex string into bytes. */ +/** + * Decode a validator pubkey hex string into bytes. + * + * @param hex Pubkey as a hex string (no `0x` prefix expected). + * @param out Output buffer for the decoded pubkey bytes. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_PARSE on decode failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]) { if (!hex || !out) @@ -354,26 +425,23 @@ static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALI } -/** @brief Populate a registry record's pubkey bytes from its pubkey hex string. */ -static int set_record_pubkey(struct lantern_validator_record *record) -{ - if (!record || !record->pubkey_hex) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - int result = decode_validator_pubkey_hex(record->pubkey_hex, record->pubkey_bytes); - if (result != LANTERN_GENESIS_OK) - { - return result; - } - - record->has_pubkey_bytes = true; - return LANTERN_GENESIS_OK; -} - - -/** @brief Ensure capacity for a packed pubkey buffer (count elements). */ +/** + * Ensure capacity for a packed pubkey buffer (count elements). + * + * Grows the allocation for `*pubkeys` to hold at least `required` pubkeys, + * returning `LANTERN_VALIDATOR_PUBKEY_SIZE` bytes per element. + * + * @param pubkeys Pointer to the packed pubkey buffer pointer. + * @param cap Pointer to the current capacity in pubkey elements. + * @param required Minimum required capacity in pubkey elements. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on capacity overflow. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to `pubkeys` and `cap`. + */ static int ensure_pubkey_capacity(uint8_t **pubkeys, size_t *cap, size_t required) { if (!pubkeys || !cap) @@ -413,6 +481,18 @@ static int ensure_pubkey_capacity(uint8_t **pubkeys, size_t *cap, size_t require } +/** + * Free resources held by a validator registry. + * + * Frees any owned record array and associated strings, then resets `registry` + * to an empty state. Safe to call with NULL. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param registry Registry to reset. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to registry. + */ void genesis_free_validator_registry(struct lantern_validator_registry *registry) { if (!registry) @@ -435,7 +515,15 @@ void genesis_free_validator_registry(struct lantern_validator_registry *registry } -/** @brief Free resources held by a validator config entry. */ +/** + * Free resources held by a validator config entry. + * + * Clears any sensitive material (privkey hex) before freeing. + * + * @param entry Entry to reset. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to entry. + */ static void free_validator_config_entry(struct lantern_validator_config_entry *entry) { if (!entry) @@ -483,6 +571,18 @@ static void free_validator_config_entry(struct lantern_validator_config_entry *e } +/** + * Free resources held by a validator config. + * + * Frees any owned entries and the shuffle string, then resets `config` to an + * empty state. Safe to call with NULL. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param config Config to reset. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ void genesis_free_validator_config(struct lantern_validator_config *config) { if (!config) @@ -507,6 +607,19 @@ void genesis_free_validator_config(struct lantern_validator_config *config) } +/** + * Merge chain config pubkeys into an existing validator registry. + * + * Populates missing `pubkey_bytes` and (best-effort) `pubkey_hex` for records + * in `registry` using the packed pubkey buffer in `config`. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param config Chain config containing packed pubkeys. + * @param registry Registry to update in place. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to registry. + */ void genesis_merge_chain_pubkeys_into_registry( const struct lantern_chain_config *config, struct lantern_validator_registry *registry) @@ -561,6 +674,25 @@ void genesis_merge_chain_pubkeys_into_registry( } +/** + * Parse chain configuration scalars from a chain config file. + * + * Parses the top-level `GENESIS_TIME` and `VALIDATOR_COUNT` scalars. Any prior + * packed pubkeys in `config->validator_pubkeys` are freed and the pubkey fields + * are reset. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param path Path to the chain config YAML file. + * @param config Config to populate (modified in place). + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO on file I/O errors. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on parse/validation failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ int genesis_parse_chain_config(const char *path, struct lantern_chain_config *config) { if (!path || !config) @@ -640,6 +772,27 @@ int genesis_parse_chain_config(const char *path, struct lantern_chain_config *co } +/** + * Parse genesis validator pubkeys from a chain config file. + * + * Reads the `GENESIS_VALIDATORS` list and returns a packed pubkey buffer. On + * success, `*out_pubkeys` is caller-owned and must be freed with `free()`. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param path Path to the chain config YAML file. + * @param out_pubkeys Output pointer for the packed pubkeys buffer. + * @param out_count Output pointer for the pubkey count. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO on file I/O errors. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/capacity overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on parse/validation failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ int genesis_parse_genesis_validator_pubkeys( const char *path, uint8_t **out_pubkeys, @@ -702,16 +855,7 @@ int genesis_parse_genesis_validator_pubkeys( continue; } - if (*val == '"' || *val == '\'') - { - char quote = *val; - ++val; - char *endq = strrchr(val, quote); - if (endq) - { - *endq = '\0'; - } - } + val = strip_optional_quotes(val); uint8_t decoded[LANTERN_VALIDATOR_PUBKEY_SIZE]; result = decode_validator_pubkey_hex(val, decoded); @@ -754,461 +898,33 @@ int genesis_parse_genesis_validator_pubkeys( } -/** @brief Collect all validator indices from a mapping/scalar-list validators.yaml. */ -static int collect_registry_mapping_indices( - const char *path, - size_t **out_indices, - size_t *out_count, - size_t *out_max_index) -{ - if (!path || !out_indices || !out_count || !out_max_index) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - *out_indices = NULL; - *out_count = 0; - *out_max_index = 0; - - FILE *fp = fopen(path, "r"); - if (!fp) - { - return LANTERN_GENESIS_ERR_IO; - } - - size_t *indices = NULL; - size_t count = 0; - size_t cap = 0; - size_t max_index = 0; - int result = LANTERN_GENESIS_OK; - - char line[GENESIS_SMALL_LINE_BUFFER_LEN]; - while (fgets(line, sizeof(line), fp)) - { - char *trimmed = lantern_trim_whitespace(line); - if (!trimmed || *trimmed != '-') - { - continue; - } - - trimmed = lantern_trim_whitespace(trimmed + 1); - if (!trimmed || *trimmed == '\0') - { - continue; - } - - int ok = 0; - uint64_t value = parse_u64(trimmed, &ok); - if (!ok || value > SIZE_MAX) - { - result = LANTERN_GENESIS_ERR_INVALID_DATA; - break; - } - - if (count == cap) - { - if (cap > SIZE_MAX / 2) - { - result = LANTERN_GENESIS_ERR_OVERFLOW; - break; - } - - size_t new_cap = (cap == 0) ? GENESIS_INITIAL_MAPPING_INDEX_CAPACITY : (cap * 2); - void *grown = realloc(indices, new_cap * sizeof(*indices)); - if (!grown) - { - result = LANTERN_GENESIS_ERR_OUT_OF_MEMORY; - break; - } - indices = grown; - cap = new_cap; - } - - indices[count++] = (size_t)value; - if ((size_t)value > max_index) - { - max_index = (size_t)value; - } - } - - fclose(fp); - - if (result != LANTERN_GENESIS_OK) - { - free(indices); - return result; - } - - if (count == 0) - { - free(indices); - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - - *out_indices = indices; - *out_count = count; - *out_max_index = max_index; - return LANTERN_GENESIS_OK; -} - - -/** @brief Validate that indices are unique and cover [0, max_index]. */ -static int validate_registry_index_coverage( - const size_t *indices, - size_t count, - size_t max_index, - size_t *out_record_count) -{ - if (!indices || count == 0 || !out_record_count) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - if (max_index == SIZE_MAX) - { - return LANTERN_GENESIS_ERR_OVERFLOW; - } - - size_t record_count = max_index + 1; - bool *seen = calloc(record_count, sizeof(*seen)); - if (!seen) - { - return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; - } - - for (size_t i = 0; i < count; ++i) - { - size_t idx = indices[i]; - if (idx >= record_count || seen[idx]) - { - free(seen); - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - seen[idx] = true; - } - - for (size_t i = 0; i < record_count; ++i) - { - if (!seen[i]) - { - free(seen); - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - } - - free(seen); - *out_record_count = record_count; - return LANTERN_GENESIS_OK; -} - - -/** @brief Allocate and populate an index-only validator registry. */ -static int build_index_only_registry( - size_t record_count, - struct lantern_validator_registry *registry) -{ - if (!registry || record_count == 0) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); - if (!records) - { - return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; - } - - for (size_t i = 0; i < record_count; ++i) - { - records[i].index = (uint64_t)i; - } - - registry->records = records; - registry->count = record_count; - return LANTERN_GENESIS_OK; -} - - -/** @brief Populate an index-only registry from mapping/scalar list indices. */ -static int parse_validator_registry_mapping( - const char *path, - struct lantern_validator_registry *registry) -{ - if (!path || !registry) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - size_t *indices = NULL; - size_t count = 0; - size_t max_index = 0; - int result = collect_registry_mapping_indices(path, &indices, &count, &max_index); - if (result != LANTERN_GENESIS_OK) - { - return result; - } - - size_t record_count = 0; - result = validate_registry_index_coverage(indices, count, max_index, &record_count); - free(indices); - if (result != LANTERN_GENESIS_OK) - { - return result; - } - - return build_index_only_registry(record_count, registry); -} - - -/** @brief Scan registry objects to determine format and record count. */ -static int scan_registry_objects( - const LanternYamlObject *objects, - size_t object_count, - bool *out_has_pubkey_field, - bool *out_have_explicit_indices, - size_t *out_record_count) -{ - if (!objects - || object_count == 0 - || !out_has_pubkey_field - || !out_have_explicit_indices - || !out_record_count) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - bool has_pubkey_field = false; - for (size_t i = 0; i < object_count; ++i) - { - if (yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_PUBKEY)) - { - has_pubkey_field = true; - break; - } - } - - bool have_explicit_indices = false; - size_t max_index = 0; - for (size_t i = 0; i < object_count; ++i) - { - const char *index_val = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_INDEX); - if (!index_val) - { - continue; - } - - int ok = 0; - uint64_t parsed_index = parse_u64(index_val, &ok); - if (!ok || parsed_index > SIZE_MAX) - { - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - - have_explicit_indices = true; - if ((size_t)parsed_index > max_index) - { - max_index = (size_t)parsed_index; - } - } - - if (have_explicit_indices && max_index == SIZE_MAX) - { - return LANTERN_GENESIS_ERR_OVERFLOW; - } - - *out_has_pubkey_field = has_pubkey_field; - *out_have_explicit_indices = have_explicit_indices; - *out_record_count = have_explicit_indices ? (max_index + 1) : object_count; - return LANTERN_GENESIS_OK; -} - - -/** @brief Populate registry records from YAML objects. */ -static int populate_registry_records_from_objects( - LanternYamlObject *objects, - size_t object_count, - bool has_pubkey_field, - bool have_explicit_indices, - struct lantern_validator_record *records, - size_t record_count, - bool *assigned) -{ - if (!objects || object_count == 0 || !records || record_count == 0 || !assigned) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - for (size_t i = 0; i < object_count; ++i) - { - size_t slot = i; - if (have_explicit_indices) - { - const char *index_val = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_INDEX); - int ok = 0; - uint64_t parsed_index = parse_u64(index_val, &ok); - if (!index_val || !ok || parsed_index >= record_count) - { - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - slot = (size_t)parsed_index; - } - - if (assigned[slot]) - { - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - - records[slot].index = (uint64_t)slot; - - if (has_pubkey_field) - { - const char *pubkey = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_PUBKEY); - const char *withdrawal = yaml_object_value( - &objects[i], - VALIDATOR_REGISTRY_FIELD_WITHDRAWAL_CREDENTIALS); - if (!pubkey || !withdrawal) - { - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - - records[slot].pubkey_hex = dup_trimmed(pubkey); - records[slot].withdrawal_credentials_hex = dup_trimmed(withdrawal); - if (!records[slot].pubkey_hex || !records[slot].withdrawal_credentials_hex) - { - return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; - } - - int rc = set_record_pubkey(&records[slot]); - if (rc != LANTERN_GENESIS_OK) - { - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - } - - assigned[slot] = true; - } - - return LANTERN_GENESIS_OK; -} - - -/** @brief Validate full coverage for explicit-index registries. */ -static int validate_registry_full_coverage(const bool *assigned, size_t record_count) -{ - if (!assigned || record_count == 0) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - for (size_t i = 0; i < record_count; ++i) - { - if (!assigned[i]) - { - return LANTERN_GENESIS_ERR_INVALID_DATA; - } - } - - return LANTERN_GENESIS_OK; -} - - -/** @brief Parse a validator registry from YAML objects (annotated or index-only). */ -static int parse_validator_registry_objects( - LanternYamlObject *objects, - size_t object_count, - struct lantern_validator_registry *registry) -{ - if (!objects || object_count == 0 || !registry) - { - return LANTERN_GENESIS_ERR_INVALID_PARAM; - } - - bool has_pubkey_field = false; - bool have_explicit_indices = false; - size_t record_count = 0; - - int result = scan_registry_objects( - objects, - object_count, - &has_pubkey_field, - &have_explicit_indices, - &record_count); - if (result != LANTERN_GENESIS_OK) - { - return result; - } - - struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); - if (!records) - { - return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; - } - - bool *assigned = calloc(record_count, sizeof(*assigned)); - if (!assigned) - { - free(records); - return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; - } - - struct lantern_validator_registry tmp = {.records = records, .count = record_count}; - - result = populate_registry_records_from_objects( - objects, - object_count, - has_pubkey_field, - have_explicit_indices, - records, - record_count, - assigned); - if (result == LANTERN_GENESIS_OK && have_explicit_indices) - { - result = validate_registry_full_coverage(assigned, record_count); - } - - free(assigned); - - if (result != LANTERN_GENESIS_OK) - { - genesis_free_validator_registry(&tmp); - return result; - } - - registry->records = records; - registry->count = record_count; - return LANTERN_GENESIS_OK; -} - - -int genesis_parse_validator_registry(const char *path, struct lantern_validator_registry *registry) +/** + * Parse a validator-config.yaml entry into a config entry struct. + * + * Populates `entry` by duplicating required fields from `object`. On success, + * `entry` owns any allocated strings and must be released with + * `free_validator_config_entry()`. + * + * @param object YAML object to parse. + * @param entry Entry to populate (modified in place). + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * @return LANTERN_GENESIS_ERR_PARSE on decode/derivation failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to entry. + */ +static int parse_validator_config_entry( + const LanternYamlObject *object, + struct lantern_validator_config_entry *entry) { - if (!path || !registry) + if (!object || !entry) { return LANTERN_GENESIS_ERR_INVALID_PARAM; } - size_t object_count = 0; - LanternYamlObject *objects = lantern_yaml_read_array( - path, - VALIDATOR_REGISTRY_ARRAY_KEY, - &object_count); - if (!objects || object_count == 0) - { - lantern_yaml_free_objects(objects, object_count); - return parse_validator_registry_mapping(path, registry); - } - - int result = parse_validator_registry_objects(objects, object_count, registry); - lantern_yaml_free_objects(objects, object_count); - return result; -} - - -/** @brief Parse a validator-config.yaml entry into a config entry struct. */ -static int parse_validator_config_entry( - const LanternYamlObject *object, - struct lantern_validator_config_entry *entry) -{ const char *name_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_NAME); const char *priv_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_PRIVKEY); const char *count_val = yaml_object_value(object, VALIDATOR_CONFIG_FIELD_COUNT); @@ -1240,6 +956,10 @@ static int parse_validator_config_entry( } entry->enr.ip = dup_trimmed(ip_val); + if (ip_val && !entry->enr.ip) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } uint64_t quic_port = parse_u64(quic_val, &ok); if (!ok || quic_port > UINT16_MAX) @@ -1259,10 +979,35 @@ static int parse_validator_config_entry( } entry->hash_sig_dir = dup_trimmed(hash_dir_val); + if (hash_dir_val && !entry->hash_sig_dir) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } return LANTERN_GENESIS_OK; } +/** + * Parse validator configuration entries from a validator-config.yaml file. + * + * On success, `config` owns the returned buffers and must be released with + * `genesis_free_validator_config()`. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param path Path to validator-config.yaml. + * @param config Config to populate. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO on file I/O errors. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/capacity overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * @return LANTERN_GENESIS_ERR_PARSE on parse failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to config. + */ int genesis_parse_validator_config(const char *path, struct lantern_validator_config *config) { if (!path || !config) @@ -1270,6 +1015,8 @@ int genesis_parse_validator_config(const char *path, struct lantern_validator_co return LANTERN_GENESIS_ERR_INVALID_PARAM; } + genesis_free_validator_config(config); + char *shuffle = NULL; int result = read_scalar_value(path, VALIDATOR_CONFIG_SCALAR_SHUFFLE, &shuffle); if (result != LANTERN_GENESIS_OK) @@ -1329,6 +1076,21 @@ int genesis_parse_validator_config(const char *path, struct lantern_validator_co } +/** + * Parse `nodes.yaml` and append ENR entries to a list. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param path Path to nodes.yaml. + * @param list Record list to append to. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO on file I/O errors. + * @return LANTERN_GENESIS_ERR_PARSE on parse failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to list. + */ int genesis_parse_nodes_file(const char *path, struct lantern_enr_record_list *list) { if (!path || !list) @@ -1377,6 +1139,26 @@ int genesis_parse_nodes_file(const char *path, struct lantern_enr_record_list *l } +/** + * Read a genesis state SSZ blob from disk. + * + * On success, `*bytes` is allocated and must be freed by the caller. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param path Path to the genesis state SSZ file. + * @param bytes Output pointer for the allocated buffer. + * @param size Output pointer for the buffer length in bytes. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO on file I/O errors. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW if the file is too large. + * @return LANTERN_GENESIS_ERR_INVALID_DATA if the file is empty or unreadable. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ int genesis_read_state_blob(const char *path, uint8_t **bytes, size_t *size) { if (!path || !bytes || !size) @@ -1402,7 +1184,12 @@ int genesis_read_state_blob(const char *path, uint8_t **bytes, size_t *size) } long file_size = ftell(fp); - if (file_size <= 0) + if (file_size < 0) + { + result = LANTERN_GENESIS_ERR_IO; + goto cleanup; + } + if (file_size == 0) { result = LANTERN_GENESIS_ERR_INVALID_DATA; goto cleanup; diff --git a/src/genesis/genesis_parse_registry.c b/src/genesis/genesis_parse_registry.c new file mode 100644 index 0000000..024f11d --- /dev/null +++ b/src/genesis/genesis_parse_registry.c @@ -0,0 +1,838 @@ +/** + * @file genesis_parse_registry.c + * @brief Parsing helpers for Lantern validator registry genesis artifacts. + * + * Implements internal helpers for parsing validators registry YAML files. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + */ + +#include "genesis_internal.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "internal/yaml_parser.h" +#include "lantern/support/strings.h" + +static const size_t GENESIS_SMALL_LINE_BUFFER_LEN = 1024; +static const size_t GENESIS_INITIAL_MAPPING_INDEX_CAPACITY = 8; + +static const char *const VALIDATOR_REGISTRY_ARRAY_KEY = "validators"; +static const char *const VALIDATOR_REGISTRY_FIELD_INDEX = "index"; +static const char *const VALIDATOR_REGISTRY_FIELD_PUBKEY = "pubkey"; +static const char *const VALIDATOR_REGISTRY_FIELD_WITHDRAWAL_CREDENTIALS = "withdrawal_credentials"; + +static uint64_t parse_u64(const char *value, int *ok); +static char *dup_trimmed(const char *value); +static const char *yaml_object_value(const LanternYamlObject *object, const char *key); +static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]); +static int set_record_pubkey(struct lantern_validator_record *record); + +static int collect_registry_mapping_indices( + const char *path, + size_t **out_indices, + size_t *out_count, + size_t *out_max_index); +static int validate_registry_index_coverage( + const size_t *indices, + size_t count, + size_t max_index, + size_t *out_record_count); +static int build_index_only_registry( + size_t record_count, + struct lantern_validator_registry *registry); +static int scan_registry_objects( + const LanternYamlObject *objects, + size_t object_count, + bool *out_has_pubkey_field, + bool *out_have_explicit_indices, + size_t *out_record_count); +static int populate_registry_records_from_objects( + LanternYamlObject *objects, + size_t object_count, + bool has_pubkey_field, + bool have_explicit_indices, + struct lantern_validator_record *records, + size_t record_count, + bool *assigned); +static int validate_registry_full_coverage(const bool *assigned, size_t record_count); +static int parse_validator_registry_objects( + LanternYamlObject *objects, + size_t object_count, + struct lantern_validator_registry *registry); +static int parse_validator_registry_mapping( + const char *path, + struct lantern_validator_registry *registry); + + +/** + * Parse an unsigned 64-bit integer from a string, allowing a trailing comment. + * + * The parsed value may be followed by whitespace and an optional `#` comment. + * Callers must use `ok` to disambiguate a successful parse of `0` from failure. + * + * @param value Input string to parse (not modified). + * @param ok Optional output flag set to 1 on success, 0 on failure. + * + * @return Parsed value on success, 0 on failure. + * + * @note Thread safety: Thread-safe. + */ +static uint64_t parse_u64(const char *value, int *ok) +{ + if (ok) + { + *ok = 0; + } + if (!value) + { + return 0; + } + + errno = 0; + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 0); + if (errno != 0 || end == value) + { + return 0; + } + + while (end && *end && isspace((unsigned char)*end)) + { + ++end; + } + if (end && *end != '\0' && *end != '#') + { + return 0; + } + if (parsed > (unsigned long long)UINT64_MAX) + { + return 0; + } + + if (ok) + { + *ok = 1; + } + return (uint64_t)parsed; +} + + +/** + * Duplicate a string after trimming whitespace and optional surrounding quotes. + * + * @param value Input string to trim and duplicate. + * + * @return Newly allocated trimmed string, or NULL on allocation failure. + * + * @note Thread safety: Thread-safe. + */ +static char *dup_trimmed(const char *value) +{ + if (!value) + { + return NULL; + } + + const char *start = value; + while (*start && isspace((unsigned char)*start)) + { + ++start; + } + + const char *end = start + strlen(start); + while (end > start && isspace((unsigned char)*(end - 1))) + { + --end; + } + + if ((end - start) >= 2 + && ((*start == '"' && *(end - 1) == '"') || (*start == '\'' && *(end - 1) == '\''))) + { + ++start; + --end; + } + + return lantern_string_duplicate_len(start, (size_t)(end - start)); +} + + +/** + * Lookup a key value in a YAML object. + * + * @param object YAML object to search. + * @param key Key to match (exact string compare). + * + * @return Matching value pointer, or NULL if not found. + * + * @note Thread safety: Thread-safe. + */ +static const char *yaml_object_value(const LanternYamlObject *object, const char *key) +{ + if (!object || !key) + { + return NULL; + } + + for (size_t i = 0; i < object->num_pairs; ++i) + { + if (object->pairs[i].key && strcmp(object->pairs[i].key, key) == 0) + { + return object->pairs[i].value; + } + } + + return NULL; +} + + +/** + * Decode a validator pubkey hex string into bytes. + * + * @param hex Pubkey as a hex string (no `0x` prefix expected). + * @param out Output buffer for the decoded pubkey bytes. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_PARSE on decode failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ +static int decode_validator_pubkey_hex(const char *hex, uint8_t out[LANTERN_VALIDATOR_PUBKEY_SIZE]) +{ + if (!hex || !out) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (lantern_hex_decode(hex, out, LANTERN_VALIDATOR_PUBKEY_SIZE) != 0) + { + return LANTERN_GENESIS_ERR_PARSE; + } + + return LANTERN_GENESIS_OK; +} + + +/** + * Populate a registry record's pubkey bytes from its pubkey hex string. + * + * @param record Record to update. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_PARSE on decode failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to record. + */ +static int set_record_pubkey(struct lantern_validator_record *record) +{ + if (!record || !record->pubkey_hex) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + int result = decode_validator_pubkey_hex(record->pubkey_hex, record->pubkey_bytes); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + record->has_pubkey_bytes = true; + return LANTERN_GENESIS_OK; +} + + +/** + * Collect all validator indices from a mapping/scalar-list validators.yaml. + * + * @param path Filesystem path to validators.yaml. + * @param out_indices Output pointer for the allocated indices array (caller-owned). + * @param out_count Output pointer for the number of indices. + * @param out_max_index Output pointer for the maximum index encountered. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO if the file cannot be opened. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/capacity overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on parse/validation failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ +static int collect_registry_mapping_indices( + const char *path, + size_t **out_indices, + size_t *out_count, + size_t *out_max_index) +{ + if (!path || !out_indices || !out_count || !out_max_index) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + *out_indices = NULL; + *out_count = 0; + *out_max_index = 0; + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return LANTERN_GENESIS_ERR_IO; + } + + size_t *indices = NULL; + size_t count = 0; + size_t cap = 0; + size_t max_index = 0; + int result = LANTERN_GENESIS_OK; + + char line[GENESIS_SMALL_LINE_BUFFER_LEN]; + while (fgets(line, sizeof(line), fp)) + { + char *trimmed = lantern_trim_whitespace(line); + if (!trimmed || *trimmed != '-') + { + continue; + } + + trimmed = lantern_trim_whitespace(trimmed + 1); + if (!trimmed || *trimmed == '\0') + { + continue; + } + + int ok = 0; + uint64_t value = parse_u64(trimmed, &ok); + if (!ok || value > SIZE_MAX) + { + result = LANTERN_GENESIS_ERR_INVALID_DATA; + break; + } + + if (count == cap) + { + if (cap > SIZE_MAX / 2) + { + result = LANTERN_GENESIS_ERR_OVERFLOW; + break; + } + + size_t new_cap = (cap == 0) ? GENESIS_INITIAL_MAPPING_INDEX_CAPACITY : (cap * 2); + void *grown = realloc(indices, new_cap * sizeof(*indices)); + if (!grown) + { + result = LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + break; + } + indices = grown; + cap = new_cap; + } + + indices[count++] = (size_t)value; + if ((size_t)value > max_index) + { + max_index = (size_t)value; + } + } + + fclose(fp); + + if (result != LANTERN_GENESIS_OK) + { + free(indices); + return result; + } + + if (count == 0) + { + free(indices); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + *out_indices = indices; + *out_count = count; + *out_max_index = max_index; + return LANTERN_GENESIS_OK; +} + + +/** + * Validate that indices are unique and cover [0, max_index]. + * + * @param indices Input indices array. + * @param count Number of indices in the array. + * @param max_index Maximum index observed. + * @param out_record_count Output pointer for the derived record count (max_index + 1). + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/count overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ +static int validate_registry_index_coverage( + const size_t *indices, + size_t count, + size_t max_index, + size_t *out_record_count) +{ + if (!indices || count == 0 || !out_record_count) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + if (max_index == SIZE_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + size_t record_count = max_index + 1; + bool *seen = calloc(record_count, sizeof(*seen)); + if (!seen) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + for (size_t i = 0; i < count; ++i) + { + size_t idx = indices[i]; + if (idx >= record_count || seen[idx]) + { + free(seen); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + seen[idx] = true; + } + + for (size_t i = 0; i < record_count; ++i) + { + if (!seen[i]) + { + free(seen); + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + free(seen); + *out_record_count = record_count; + return LANTERN_GENESIS_OK; +} + + +/** + * Allocate and populate an index-only validator registry. + * + * @param record_count Number of records to allocate. + * @param registry Registry to populate (modified in place). + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to registry. + */ +static int build_index_only_registry( + size_t record_count, + struct lantern_validator_registry *registry) +{ + if (!registry || record_count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); + if (!records) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + for (size_t i = 0; i < record_count; ++i) + { + records[i].index = (uint64_t)i; + } + + registry->records = records; + registry->count = record_count; + return LANTERN_GENESIS_OK; +} + + +/** + * Populate an index-only registry from mapping/scalar list indices. + * + * @param path Filesystem path to validators.yaml. + * @param registry Registry to populate (modified in place). + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO if the file cannot be opened. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/count overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to registry. + */ +static int parse_validator_registry_mapping( + const char *path, + struct lantern_validator_registry *registry) +{ + if (!path || !registry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + size_t *indices = NULL; + size_t count = 0; + size_t max_index = 0; + int result = collect_registry_mapping_indices(path, &indices, &count, &max_index); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + size_t record_count = 0; + result = validate_registry_index_coverage(indices, count, max_index, &record_count); + free(indices); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + return build_index_only_registry(record_count, registry); +} + + +/** + * Scan registry objects to determine format and record count. + * + * Determines whether the registry is annotated with pubkeys and/or explicit indices. + * + * @param objects YAML objects to scan. + * @param object_count Number of objects in `objects`. + * @param out_has_pubkey_field Output flag set if any object has a pubkey field. + * @param out_have_explicit_indices Output flag set if any object has an explicit index. + * @param out_record_count Output pointer for the expected record count. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/count overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * + * @note Thread safety: Thread-safe if callers provide exclusive access to outputs. + */ +static int scan_registry_objects( + const LanternYamlObject *objects, + size_t object_count, + bool *out_has_pubkey_field, + bool *out_have_explicit_indices, + size_t *out_record_count) +{ + if (!objects + || object_count == 0 + || !out_has_pubkey_field + || !out_have_explicit_indices + || !out_record_count) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + bool has_pubkey_field = false; + for (size_t i = 0; i < object_count; ++i) + { + if (yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_PUBKEY)) + { + has_pubkey_field = true; + break; + } + } + + bool have_explicit_indices = false; + size_t max_index = 0; + for (size_t i = 0; i < object_count; ++i) + { + const char *index_val = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_INDEX); + if (!index_val) + { + continue; + } + + int ok = 0; + uint64_t parsed_index = parse_u64(index_val, &ok); + if (!ok || parsed_index > SIZE_MAX) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + have_explicit_indices = true; + if ((size_t)parsed_index > max_index) + { + max_index = (size_t)parsed_index; + } + } + + if (have_explicit_indices && max_index == SIZE_MAX) + { + return LANTERN_GENESIS_ERR_OVERFLOW; + } + + *out_has_pubkey_field = has_pubkey_field; + *out_have_explicit_indices = have_explicit_indices; + *out_record_count = have_explicit_indices ? (max_index + 1) : object_count; + return LANTERN_GENESIS_OK; +} + + +/** + * Populate registry records from YAML objects. + * + * @param objects YAML objects to parse. + * @param object_count Number of objects in `objects`. + * @param has_pubkey_field Whether the registry contains pubkey annotations. + * @param have_explicit_indices Whether objects contain explicit indices. + * @param records Record array to populate. + * @param record_count Number of records in `records`. + * @param assigned Assignment bitmap for explicit-index registries. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to records. + */ +static int populate_registry_records_from_objects( + LanternYamlObject *objects, + size_t object_count, + bool has_pubkey_field, + bool have_explicit_indices, + struct lantern_validator_record *records, + size_t record_count, + bool *assigned) +{ + if (!objects || object_count == 0 || !records || record_count == 0 || !assigned) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + for (size_t i = 0; i < object_count; ++i) + { + size_t slot = i; + if (have_explicit_indices) + { + const char *index_val = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_INDEX); + int ok = 0; + uint64_t parsed_index = parse_u64(index_val, &ok); + if (!index_val || !ok || parsed_index >= record_count) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + slot = (size_t)parsed_index; + } + + if (assigned[slot]) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + records[slot].index = (uint64_t)slot; + + if (has_pubkey_field) + { + const char *pubkey = yaml_object_value(&objects[i], VALIDATOR_REGISTRY_FIELD_PUBKEY); + const char *withdrawal = yaml_object_value( + &objects[i], + VALIDATOR_REGISTRY_FIELD_WITHDRAWAL_CREDENTIALS); + if (!pubkey || !withdrawal) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + + records[slot].pubkey_hex = dup_trimmed(pubkey); + records[slot].withdrawal_credentials_hex = dup_trimmed(withdrawal); + if (!records[slot].pubkey_hex || !records[slot].withdrawal_credentials_hex) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + int rc = set_record_pubkey(&records[slot]); + if (rc != LANTERN_GENESIS_OK) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + assigned[slot] = true; + } + + return LANTERN_GENESIS_OK; +} + + +/** + * Validate full coverage for explicit-index registries. + * + * @param assigned Assignment bitmap. + * @param record_count Expected record count. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_INVALID_DATA if any record is unassigned. + * + * @note Thread safety: Thread-safe. + */ +static int validate_registry_full_coverage(const bool *assigned, size_t record_count) +{ + if (!assigned || record_count == 0) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + for (size_t i = 0; i < record_count; ++i) + { + if (!assigned[i]) + { + return LANTERN_GENESIS_ERR_INVALID_DATA; + } + } + + return LANTERN_GENESIS_OK; +} + + +/** + * Parse a validator registry from YAML objects (annotated or index-only). + * + * @param objects YAML objects to parse. + * @param object_count Number of objects in `objects`. + * @param registry Registry to populate. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/count overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to registry. + */ +static int parse_validator_registry_objects( + LanternYamlObject *objects, + size_t object_count, + struct lantern_validator_registry *registry) +{ + if (!objects || object_count == 0 || !registry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + bool has_pubkey_field = false; + bool have_explicit_indices = false; + size_t record_count = 0; + + int result = scan_registry_objects( + objects, + object_count, + &has_pubkey_field, + &have_explicit_indices, + &record_count); + if (result != LANTERN_GENESIS_OK) + { + return result; + } + + struct lantern_validator_record *records = calloc(record_count, sizeof(*records)); + if (!records) + { + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + bool *assigned = calloc(record_count, sizeof(*assigned)); + if (!assigned) + { + free(records); + return LANTERN_GENESIS_ERR_OUT_OF_MEMORY; + } + + struct lantern_validator_registry tmp = {.records = records, .count = record_count}; + + result = populate_registry_records_from_objects( + objects, + object_count, + has_pubkey_field, + have_explicit_indices, + records, + record_count, + assigned); + if (result == LANTERN_GENESIS_OK && have_explicit_indices) + { + result = validate_registry_full_coverage(assigned, record_count); + } + + free(assigned); + + if (result != LANTERN_GENESIS_OK) + { + genesis_free_validator_registry(&tmp); + return result; + } + + registry->records = records; + registry->count = record_count; + return LANTERN_GENESIS_OK; +} + + +/** + * Parse a validator registry file. + * + * Supports an annotated YAML array under the `validators` key, or a fallback + * scalar list of indices in mapping form. On success, `registry` owns the + * returned record array and must be released with `genesis_free_validator_registry()`. + * + * @spec Lantern devnet genesis artifact files (lean quickstart). + * + * @param path Path to validators.yaml. + * @param registry Registry to populate. + * + * @return LANTERN_GENESIS_OK on success. + * @return LANTERN_GENESIS_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_GENESIS_ERR_IO on file I/O errors. + * @return LANTERN_GENESIS_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_GENESIS_ERR_OVERFLOW on size/count overflow. + * @return LANTERN_GENESIS_ERR_INVALID_DATA on validation failures. + * @return LANTERN_GENESIS_ERR_PARSE on parse failures. + * + * @note Thread safety: Not thread-safe. Caller must ensure exclusive access to registry. + */ +int genesis_parse_validator_registry(const char *path, struct lantern_validator_registry *registry) +{ + if (!path || !registry) + { + return LANTERN_GENESIS_ERR_INVALID_PARAM; + } + + genesis_free_validator_registry(registry); + + size_t object_count = 0; + LanternYamlObject *objects = lantern_yaml_read_array( + path, + VALIDATOR_REGISTRY_ARRAY_KEY, + &object_count); + if (!objects || object_count == 0) + { + lantern_yaml_free_objects(objects, object_count); + return parse_validator_registry_mapping(path, registry); + } + + int result = parse_validator_registry_objects(objects, object_count, registry); + lantern_yaml_free_objects(objects, object_count); + return result; +} From 05ebbc588ad4d5855ffa41743076f2ddc473f02d Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:10:47 +1000 Subject: [PATCH 10/12] Update common.c --- src/http/common.c | 154 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 22 deletions(-) diff --git a/src/http/common.c b/src/http/common.c index 96b29a0..95c16ca 100644 --- a/src/http/common.c +++ b/src/http/common.c @@ -1,39 +1,139 @@ +/** + * @file common.c + * @brief Common helpers for writing HTTP responses. + * + * Provides socket send helpers used by Lantern's HTTP modules. + * + * @spec RFC 9110 (HTTP Semantics) and RFC 9112 (HTTP/1.1). + */ + #include "lantern/http/common.h" #include #include -#include #include #include -int lantern_http_send_all(int fd, const char *data, size_t length) { - if (!data) { - return -1; +static const size_t HTTP_RESPONSE_HEADER_BUFFER_LEN = 256; +static const int HTTP_STATUS_CODE_MIN = 100; +static const int HTTP_STATUS_CODE_MAX = 999; + +/** + * HTTP module-specific error codes. + */ +typedef enum +{ + LANTERN_HTTP_OK = 0, + LANTERN_HTTP_ERR_INVALID_PARAM = -1, + LANTERN_HTTP_ERR_SEND_FAILED = -2, + LANTERN_HTTP_ERR_HEADER_TOO_LARGE = -3, +} lantern_http_error_t; + +/** + * Send the provided buffer to a socket, retrying short writes. + * + * @param fd Socket file descriptor to write to. + * @param data Bytes to send (not modified). + * @param length Number of bytes to send. + * + * @spec POSIX send(2) + * + * @return 0 on success. + * @return LANTERN_HTTP_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_ERR_SEND_FAILED on write failure. + * + * @note Thread safety: Caller must ensure exclusive access to `fd`. + */ +int lantern_http_send_all(int fd, const char *data, size_t length) +{ + if (fd < 0) + { + return LANTERN_HTTP_ERR_INVALID_PARAM; } - while (length > 0) { - ssize_t written = send(fd, data, length, 0); - if (written <= 0) { - if (written < 0 && errno == EINTR) { + if (length == 0) + { + return 0; + } + if (!data) + { + return LANTERN_HTTP_ERR_INVALID_PARAM; + } + + int flags = 0; +#ifdef MSG_NOSIGNAL + flags |= MSG_NOSIGNAL; +#endif + + while (length > 0) + { + ssize_t written = send(fd, data, length, flags); + if (written == 0) + { + return LANTERN_HTTP_ERR_SEND_FAILED; + } + if (written < 0) + { + if (errno == EINTR) + { continue; } - return -1; + return LANTERN_HTTP_ERR_SEND_FAILED; } - data += written; - length -= (size_t)written; + + size_t bytes_written = (size_t)written; + data += bytes_written; + length -= bytes_written; } + return 0; } + +/** + * Send an HTTP/1.1 response and optional body to a socket. + * + * @param fd Socket file descriptor to write to. + * @param status_code HTTP status code (100-999). + * @param status_text Optional HTTP status text (defaults to "OK"). + * @param content_type Optional Content-Type header value (defaults to application/json). + * @param body Optional response body (may be NULL when body_len is 0). + * @param body_len Number of bytes in body. + * + * @spec RFC 9110 (HTTP Semantics) and RFC 9112 (HTTP/1.1) + * + * @return 0 on success. + * @return LANTERN_HTTP_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_ERR_SEND_FAILED on write failure. + * @return LANTERN_HTTP_ERR_HEADER_TOO_LARGE on header formatting/truncation failure. + * + * @note Thread safety: Caller must ensure exclusive access to `fd`. + */ int lantern_http_send_response( int fd, int status_code, const char *status_text, const char *content_type, const char *body, - size_t body_len) { - char header[256]; + size_t body_len) +{ + if (fd < 0) + { + return LANTERN_HTTP_ERR_INVALID_PARAM; + } + if (status_code < HTTP_STATUS_CODE_MIN || status_code > HTTP_STATUS_CODE_MAX) + { + return LANTERN_HTTP_ERR_INVALID_PARAM; + } + if (!body && body_len != 0) + { + return LANTERN_HTTP_ERR_INVALID_PARAM; + } + const char *text = status_text ? status_text : "OK"; const char *type = content_type ? content_type : "application/json"; + size_t content_length = body ? body_len : 0u; + + char header[HTTP_RESPONSE_HEADER_BUFFER_LEN]; int header_len = snprintf( header, sizeof(header), @@ -45,17 +145,27 @@ int lantern_http_send_response( status_code, text, type, - body ? body_len : 0u); - if (header_len <= 0 || (size_t)header_len >= sizeof(header)) { - return -1; + content_length); + if (header_len <= 0 || (size_t)header_len >= sizeof(header)) + { + return LANTERN_HTTP_ERR_HEADER_TOO_LARGE; } - if (lantern_http_send_all(fd, header, (size_t)header_len) != 0) { - return -1; + + int result = lantern_http_send_all(fd, header, (size_t)header_len); + if (result != 0) + { + return result; } - if (body && body_len > 0) { - if (lantern_http_send_all(fd, body, body_len) != 0) { - return -1; - } + if (content_length == 0) + { + return 0; } + + result = lantern_http_send_all(fd, body, content_length); + if (result != 0) + { + return result; + } + return 0; } From 98d78399fdce7c77574633e4556b229064dd245a Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:06:58 +1000 Subject: [PATCH 11/12] Fix busy loop issue --- Dockerfile | 6 +- external/c-libp2p | 2 +- src/http/metrics.c | 1481 +++++++++++++++++++++++++++++++++----------- src/http/server.c | 1389 ++++++++++++++++++++++++++++++++++------- 4 files changed, 2264 insertions(+), 614 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0b87f8..06c4b79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,14 +84,16 @@ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive -# Install runtime dependencies +# Install runtime dependencies and profiling tools RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ gdb \ libssl3 \ libstdc++6 \ zlib1g \ - && rm -rf /var/lib/apt/lists/* + linux-tools-generic \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/lib/linux-tools/*/perf /usr/local/bin/perf || true COPY --from=builder /opt/lantern /opt/lantern COPY docker/entrypoint.sh /usr/local/bin/lantern-entrypoint.sh diff --git a/external/c-libp2p b/external/c-libp2p index f1daed5..8f057d5 160000 --- a/external/c-libp2p +++ b/external/c-libp2p @@ -1 +1 @@ -Subproject commit f1daed58ee86375610727f671b6692901ba2f1e7 +Subproject commit 8f057d52f7d610958542b3cfb8f087a3d1df90a6 diff --git a/src/http/metrics.c b/src/http/metrics.c index 55f4735..6f7caa0 100644 --- a/src/http/metrics.c +++ b/src/http/metrics.c @@ -1,475 +1,1129 @@ -#include "lantern/http/metrics.h" +/** + * @file metrics.c + * @brief Prometheus metrics HTTP endpoint. + * + * Exposes a Prometheus-compatible metrics endpoint: + * - GET /metrics + * + * Metrics are generated from a caller-provided snapshot callback. + * + * @spec Prometheus exposition format 0.0.4 and POSIX sockets/pthreads. + */ -#include "lantern/http/common.h" -#include "lantern/support/log.h" +#include "lantern/http/metrics.h" #include #include #include #include #include -#include #include +#include +#include +#include +#include #include #include #include #include #include -#define LANTERN_METRICS_BUFFER_SIZE 4096 +#include "lantern/http/common.h" +#include "lantern/support/log.h" + +static const size_t LANTERN_METRICS_READ_BUFFER_SIZE = 4096; +static const size_t LANTERN_METRICS_BODY_DEFAULT_CAP = 1024; +static const size_t LANTERN_METRICS_BODY_INITIAL_CAP = 2048; +static const int LANTERN_METRICS_LISTEN_BACKLOG = 16; +static const char LANTERN_METRICS_ENDPOINT_PATH[] = "/metrics"; +static const char LANTERN_METRICS_TEXT_CONTENT_TYPE[] = "text/plain; version=0.0.4"; + +enum +{ + LANTERN_METRICS_METHOD_CAP = 8, + LANTERN_METRICS_PATH_CAP = 128, +}; + +/** + * Metrics server module-specific error codes. + */ +typedef enum +{ + LANTERN_METRICS_SERVER_OK = 0, + LANTERN_METRICS_SERVER_ERR_INVALID_PARAM = -1, + LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY = -2, + LANTERN_METRICS_SERVER_ERR_OVERFLOW = -3, + LANTERN_METRICS_SERVER_ERR_IO = -4, + LANTERN_METRICS_SERVER_ERR_FORMATTING = -5, + LANTERN_METRICS_SERVER_ERR_MALFORMED_REQUEST = -6, + LANTERN_METRICS_SERVER_ERR_UNAVAILABLE = -7, +} lantern_metrics_server_error_t; -struct metrics_buffer { - char *data; - size_t len; - size_t cap; +static const char METRICS_JSON_MALFORMED_REQUEST[] = "{\"error\":\"malformed request\"}"; +static const char METRICS_JSON_UNKNOWN_ENDPOINT[] = "{\"error\":\"unknown endpoint\"}"; +static const char METRICS_JSON_UNAVAILABLE[] = "{\"error\":\"metrics unavailable\"}"; +static const char METRICS_JSON_FORMATTING_FAILED[] = "{\"error\":\"metrics formatting failed\"}"; + +struct lantern_metrics_body_buffer +{ + char *data; /**< Heap buffer (NUL-terminated). */ + size_t len; /**< Bytes written (excluding terminator). */ + size_t cap; /**< Allocated capacity in bytes. */ }; -static int metrics_buffer_init(struct metrics_buffer *buf, size_t initial_cap) { - if (!buf) { - return -1; +/** + * @brief Initialize a dynamic metrics body buffer. + * + * @param buf Buffer to initialize (modified in place). + * @param initial_cap Initial allocation size in bytes (0 uses default). + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int metrics_buffer_init(struct lantern_metrics_body_buffer *buf, size_t initial_cap) +{ + if (!buf) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; } - size_t capacity = initial_cap ? initial_cap : 1024; + + size_t capacity = initial_cap != 0 ? initial_cap : LANTERN_METRICS_BODY_DEFAULT_CAP; buf->data = malloc(capacity); - if (!buf->data) { - return -1; + if (!buf->data) + { + return LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY; } + buf->len = 0; buf->cap = capacity; buf->data[0] = '\0'; return 0; } -static void metrics_buffer_free(struct metrics_buffer *buf) { - if (!buf) { + +/** + * @brief Free resources owned by a metrics body buffer. + * + * @param buf Buffer to free (may be NULL). + * + * @note Thread safety: This function is thread-safe. + */ +static void metrics_buffer_free(struct lantern_metrics_body_buffer *buf) +{ + if (!buf) + { return; } + free(buf->data); buf->data = NULL; buf->len = 0; buf->cap = 0; } -static int metrics_buffer_reserve(struct metrics_buffer *buf, size_t extra) { - if (!buf || extra == 0) { + +/** + * @brief Ensure the buffer can append the requested number of bytes. + * + * @param buf Buffer to grow (modified in place). + * @param extra Additional bytes required (excluding NUL terminator). + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_METRICS_SERVER_ERR_OVERFLOW on size overflow. + * + * @note Thread safety: This function is thread-safe. + */ +static int metrics_buffer_reserve(struct lantern_metrics_body_buffer *buf, size_t extra) +{ + if (!buf) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + if (extra == 0) + { return 0; } + + if (buf->len >= SIZE_MAX - 1) + { + return LANTERN_METRICS_SERVER_ERR_OVERFLOW; + } + if (extra > (SIZE_MAX - buf->len - 1)) + { + return LANTERN_METRICS_SERVER_ERR_OVERFLOW; + } + size_t required = buf->len + extra + 1; - if (required <= buf->cap) { + if (required <= buf->cap) + { return 0; } - size_t new_cap = buf->cap ? buf->cap : 1024; - while (new_cap < required) { - if (new_cap > (SIZE_MAX / 2)) { - return -1; + + size_t new_cap = buf->cap != 0 ? buf->cap : LANTERN_METRICS_BODY_DEFAULT_CAP; + while (new_cap < required) + { + if (new_cap > SIZE_MAX / 2) + { + return LANTERN_METRICS_SERVER_ERR_OVERFLOW; } new_cap *= 2; } - char *data = realloc(buf->data, new_cap); - if (!data) { - return -1; + + char *new_data = realloc(buf->data, new_cap); + if (!new_data) + { + return LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY; } - buf->data = data; + + buf->data = new_data; buf->cap = new_cap; return 0; } -static int metrics_buffer_appendf(struct metrics_buffer *buf, const char *fmt, ...) { - if (!buf || !fmt) { - return -1; + +/** + * @brief Append formatted text to a metrics body buffer. + * + * @param buf Buffer to append to (modified in place). + * @param fmt printf-style format string. + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_METRICS_SERVER_ERR_OVERFLOW on size overflow. + * @return LANTERN_METRICS_SERVER_ERR_FORMATTING on formatting failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int metrics_buffer_appendf(struct lantern_metrics_body_buffer *buf, const char *fmt, ...) +{ + if (!buf || !fmt) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; } + va_list args; va_start(args, fmt); + va_list measure; va_copy(measure, args); int needed = vsnprintf(NULL, 0, fmt, measure); va_end(measure); - if (needed < 0) { + if (needed < 0) + { va_end(args); - return -1; + return LANTERN_METRICS_SERVER_ERR_FORMATTING; } - if (metrics_buffer_reserve(buf, (size_t)needed) != 0) { + + int reserve_rc = metrics_buffer_reserve(buf, (size_t)needed); + if (reserve_rc != 0) + { va_end(args); - return -1; + return reserve_rc; } + int written = vsnprintf(buf->data + buf->len, buf->cap - buf->len, fmt, args); va_end(args); - if (written < 0 || (size_t)written != (size_t)needed) { - return -1; + if (written < 0 || written != needed) + { + return LANTERN_METRICS_SERVER_ERR_FORMATTING; } + buf->len += (size_t)written; return 0; } + +/** + * @brief Append a single Prometheus metric with a uint64 value. + */ +static int append_metric_uint64( + struct lantern_metrics_body_buffer *buf, + const char *name, + const char *help, + const char *type, + uint64_t value) +{ + return metrics_buffer_appendf( + buf, + "# HELP %s %s\n" + "# TYPE %s %s\n" + "%s %" PRIu64 "\n", + name, + help, + name, + type, + name, + value); +} + + +/** + * @brief Append a single Prometheus metric with a size_t value. + */ +static int append_metric_size_t( + struct lantern_metrics_body_buffer *buf, + const char *name, + const char *help, + const char *type, + size_t value) +{ + return metrics_buffer_appendf( + buf, + "# HELP %s %s\n" + "# TYPE %s %s\n" + "%s %zu\n", + name, + help, + name, + type, + name, + value); +} + + +/** + * @brief Append a Prometheus histogram from a lean metrics snapshot. + */ static int append_histogram_metrics( - struct metrics_buffer *buf, + struct lantern_metrics_body_buffer *buf, const char *name, const char *help, - const struct lean_metrics_histogram_snapshot *hist) { - if (!buf || !name || !help || !hist) { - return -1; + const struct lean_metrics_histogram_snapshot *hist) +{ + if (!buf || !name || !help || !hist) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; } - if (metrics_buffer_appendf(buf, "# HELP %s %s\n# TYPE %s histogram\n", name, help, name) != 0) { - return -1; + + int rc = metrics_buffer_appendf( + buf, + "# HELP %s %s\n" + "# TYPE %s histogram\n", + name, + help, + name); + if (rc != 0) + { + return rc; } + size_t bucket_count = hist->bucket_count; - if (bucket_count > LEAN_METRICS_MAX_BUCKETS) { + if (bucket_count > LEAN_METRICS_MAX_BUCKETS) + { bucket_count = LEAN_METRICS_MAX_BUCKETS; } - for (size_t i = 0; i < bucket_count; ++i) { + + for (size_t i = 0; i < bucket_count; ++i) + { double bound = hist->buckets[i]; - if (metrics_buffer_appendf( - buf, - "%s_bucket{le=\"%.9g\"} %" PRIu64 "\n", - name, - bound, - hist->counts[i]) - != 0) { - return -1; - } - } - if (metrics_buffer_appendf( + rc = metrics_buffer_appendf( buf, - "%s_bucket{le=\"+Inf\"} %" PRIu64 "\n", + "%s_bucket{le=\"%.9g\"} %" PRIu64 "\n", name, - hist->counts[bucket_count]) - != 0) { - return -1; + bound, + hist->counts[i]); + if (rc != 0) + { + return rc; + } + } + + rc = metrics_buffer_appendf( + buf, + "%s_bucket{le=\"+Inf\"} %" PRIu64 "\n", + name, + hist->counts[bucket_count]); + if (rc != 0) + { + return rc; } - if (metrics_buffer_appendf(buf, "%s_sum %.9f\n%s_count %" PRIu64 "\n", name, hist->sum, name, hist->total) != 0) { - return -1; + + rc = metrics_buffer_appendf( + buf, + "%s_sum %.9f\n" + "%s_count %" PRIu64 "\n", + name, + hist->sum, + name, + hist->total); + if (rc != 0) + { + return rc; } + return 0; } -static int format_metrics_body( - const struct lantern_metrics_snapshot *snapshot, - char **out_body, - size_t *out_len) { - if (!snapshot || !out_body || !out_len) { - return -1; + +/** + * @brief Append chain and lean subsystem metrics. + */ +static int append_lean_chain_metrics( + struct lantern_metrics_body_buffer *buf, + const struct lantern_metrics_snapshot *snapshot) +{ + if (!buf || !snapshot) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + + int rc = append_metric_uint64( + buf, + "lean_head_slot", + "Latest slot of the lean chain", + "gauge", + snapshot->lean_head_slot); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_latest_justified_slot", + "Latest justified slot observed by state transition", + "gauge", + snapshot->lean_latest_justified_slot); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_latest_finalized_slot", + "Latest finalized slot observed by state transition", + "gauge", + snapshot->lean_latest_finalized_slot); + if (rc != 0) + { + return rc; } - struct metrics_buffer buf; - if (metrics_buffer_init(&buf, 2048) != 0) { - return -1; + rc = append_metric_size_t( + buf, + "lean_validators_count", + "Number of validators connected to this client", + "gauge", + snapshot->lean_validators_count); + if (rc != 0) + { + return rc; } const struct lean_metrics_snapshot *lean = &snapshot->lean_metrics; - if (metrics_buffer_appendf( - &buf, - "# HELP lean_head_slot Latest slot of the lean chain\n" - "# TYPE lean_head_slot gauge\n" - "lean_head_slot %" PRIu64 "\n" - "# HELP lean_latest_justified_slot Latest justified slot observed by state transition\n" - "# TYPE lean_latest_justified_slot gauge\n" - "lean_latest_justified_slot %" PRIu64 "\n" - "# HELP lean_latest_finalized_slot Latest finalized slot observed by state transition\n" - "# TYPE lean_latest_finalized_slot gauge\n" - "lean_latest_finalized_slot %" PRIu64 "\n" - "# HELP lean_validators_count Number of validators connected to this client\n" - "# TYPE lean_validators_count gauge\n" - "lean_validators_count %zu\n" - "# HELP lean_attestations_valid_total Total number of valid attestations\n" - "# TYPE lean_attestations_valid_total counter\n" - "lean_attestations_valid_total %" PRIu64 "\n" - "# HELP lean_attestations_invalid_total Total number of invalid attestations\n" - "# TYPE lean_attestations_invalid_total counter\n" - "lean_attestations_invalid_total %" PRIu64 "\n" - "# HELP lean_state_transition_slots_processed_total Total number of processed slots during state transitions\n" - "# TYPE lean_state_transition_slots_processed_total counter\n" - "lean_state_transition_slots_processed_total %" PRIu64 "\n" - "# HELP lean_state_transition_attestations_processed_total Total number of attestations processed during state transitions\n" - "# TYPE lean_state_transition_attestations_processed_total counter\n" - "lean_state_transition_attestations_processed_total %" PRIu64 "\n", - snapshot->lean_head_slot, - snapshot->lean_latest_justified_slot, - snapshot->lean_latest_finalized_slot, - snapshot->lean_validators_count, - lean->attestations_valid_total, - lean->attestations_invalid_total, - lean->state_transition_slots_processed_total, - lean->state_transition_attestations_processed_total) - != 0) { - metrics_buffer_free(&buf); - return -1; - } - - if (snapshot->peer_vote_metrics_count > 0) { - if (metrics_buffer_appendf( - &buf, - "# HELP lean_gossip_votes_received_total Vote gossip messages received per peer\n" - "# TYPE lean_gossip_votes_received_total counter\n") - != 0) { - metrics_buffer_free(&buf); - return -1; - } - for (size_t i = 0; i < snapshot->peer_vote_metrics_count; ++i) { - const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; - if (metrics_buffer_appendf( - &buf, - "lean_gossip_votes_received_total{peer=\"%s\"} %" PRIu64 "\n", - metric->peer_id, - metric->received_total) - != 0) { - metrics_buffer_free(&buf); - return -1; - } - } - if (metrics_buffer_appendf( - &buf, - "# HELP lean_gossip_votes_accepted_total Vote gossip messages accepted per peer\n" - "# TYPE lean_gossip_votes_accepted_total counter\n") - != 0) { - metrics_buffer_free(&buf); - return -1; - } - for (size_t i = 0; i < snapshot->peer_vote_metrics_count; ++i) { - const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; - if (metrics_buffer_appendf( - &buf, - "lean_gossip_votes_accepted_total{peer=\"%s\"} %" PRIu64 "\n", - metric->peer_id, - metric->accepted_total) - != 0) { - metrics_buffer_free(&buf); - return -1; - } - } - if (metrics_buffer_appendf( - &buf, - "# HELP lean_gossip_votes_rejected_total Vote gossip messages rejected per peer\n" - "# TYPE lean_gossip_votes_rejected_total counter\n") - != 0) { - metrics_buffer_free(&buf); - return -1; - } - for (size_t i = 0; i < snapshot->peer_vote_metrics_count; ++i) { - const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; - if (metrics_buffer_appendf( - &buf, - "lean_gossip_votes_rejected_total{peer=\"%s\"} %" PRIu64 "\n", - metric->peer_id, - metric->rejected_total) - != 0) { - metrics_buffer_free(&buf); - return -1; - } + + rc = append_metric_uint64( + buf, + "lean_attestations_valid_total", + "Total number of valid attestations", + "counter", + lean->attestations_valid_total); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_attestations_invalid_total", + "Total number of invalid attestations", + "counter", + lean->attestations_invalid_total); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_state_transition_slots_processed_total", + "Total number of processed slots during state transitions", + "counter", + lean->state_transition_slots_processed_total); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_state_transition_attestations_processed_total", + "Total number of attestations processed during state " + "transitions", + "counter", + lean->state_transition_attestations_processed_total); + if (rc != 0) + { + return rc; + } + + return 0; +} + + +/** + * @brief Append per-peer vote gossip metrics. + */ +static int append_peer_vote_metrics( + struct lantern_metrics_body_buffer *buf, + const struct lantern_metrics_snapshot *snapshot) +{ + if (!buf || !snapshot) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + if (snapshot->peer_vote_metrics_count == 0) + { + return 0; + } + + size_t count = snapshot->peer_vote_metrics_count; + if (count > LANTERN_METRICS_MAX_PEER_VOTE_STATS) + { + count = LANTERN_METRICS_MAX_PEER_VOTE_STATS; + } + + int rc = metrics_buffer_appendf( + buf, + "# HELP lean_gossip_votes_received_total Vote gossip messages received per peer\n" + "# TYPE lean_gossip_votes_received_total counter\n"); + if (rc != 0) + { + return rc; + } + + for (size_t i = 0; i < count; ++i) + { + const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; + char peer_id[sizeof(metric->peer_id)]; + strncpy(peer_id, metric->peer_id, sizeof(peer_id) - 1); + peer_id[sizeof(peer_id) - 1] = '\0'; + + rc = metrics_buffer_appendf( + buf, + "lean_gossip_votes_received_total{peer=\"%s\"} %" PRIu64 "\n", + peer_id, + metric->received_total); + if (rc != 0) + { + return rc; } - if (metrics_buffer_appendf( - &buf, - "# HELP lean_gossip_votes_last_validator_id Last validator id observed per peer\n" - "# TYPE lean_gossip_votes_last_validator_id gauge\n") - != 0) { - metrics_buffer_free(&buf); - return -1; + } + + rc = metrics_buffer_appendf( + buf, + "# HELP lean_gossip_votes_accepted_total Vote gossip messages accepted per peer\n" + "# TYPE lean_gossip_votes_accepted_total counter\n"); + if (rc != 0) + { + return rc; + } + + for (size_t i = 0; i < count; ++i) + { + const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; + char peer_id[sizeof(metric->peer_id)]; + strncpy(peer_id, metric->peer_id, sizeof(peer_id) - 1); + peer_id[sizeof(peer_id) - 1] = '\0'; + + rc = metrics_buffer_appendf( + buf, + "lean_gossip_votes_accepted_total{peer=\"%s\"} %" PRIu64 "\n", + peer_id, + metric->accepted_total); + if (rc != 0) + { + return rc; } - for (size_t i = 0; i < snapshot->peer_vote_metrics_count; ++i) { - const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; - if (metrics_buffer_appendf( - &buf, - "lean_gossip_votes_last_validator_id{peer=\"%s\"} %" PRIu64 "\n", - metric->peer_id, - metric->last_validator_id) - != 0) { - metrics_buffer_free(&buf); - return -1; - } + } + + rc = metrics_buffer_appendf( + buf, + "# HELP lean_gossip_votes_rejected_total Vote gossip messages rejected per peer\n" + "# TYPE lean_gossip_votes_rejected_total counter\n"); + if (rc != 0) + { + return rc; + } + + for (size_t i = 0; i < count; ++i) + { + const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; + char peer_id[sizeof(metric->peer_id)]; + strncpy(peer_id, metric->peer_id, sizeof(peer_id) - 1); + peer_id[sizeof(peer_id) - 1] = '\0'; + + rc = metrics_buffer_appendf( + buf, + "lean_gossip_votes_rejected_total{peer=\"%s\"} %" PRIu64 "\n", + peer_id, + metric->rejected_total); + if (rc != 0) + { + return rc; } - if (metrics_buffer_appendf( - &buf, - "# HELP lean_gossip_votes_last_slot Last vote slot observed per peer\n" - "# TYPE lean_gossip_votes_last_slot gauge\n") - != 0) { - metrics_buffer_free(&buf); - return -1; + } + + rc = metrics_buffer_appendf( + buf, + "# HELP lean_gossip_votes_last_validator_id Last validator id observed per peer\n" + "# TYPE lean_gossip_votes_last_validator_id gauge\n"); + if (rc != 0) + { + return rc; + } + + for (size_t i = 0; i < count; ++i) + { + const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; + char peer_id[sizeof(metric->peer_id)]; + strncpy(peer_id, metric->peer_id, sizeof(peer_id) - 1); + peer_id[sizeof(peer_id) - 1] = '\0'; + + rc = metrics_buffer_appendf( + buf, + "lean_gossip_votes_last_validator_id{peer=\"%s\"} %" PRIu64 "\n", + peer_id, + metric->last_validator_id); + if (rc != 0) + { + return rc; } - for (size_t i = 0; i < snapshot->peer_vote_metrics_count; ++i) { - const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; - if (metrics_buffer_appendf( - &buf, - "lean_gossip_votes_last_slot{peer=\"%s\"} %" PRIu64 "\n", - metric->peer_id, - metric->last_slot) - != 0) { - metrics_buffer_free(&buf); - return -1; - } + } + + rc = metrics_buffer_appendf( + buf, + "# HELP lean_gossip_votes_last_slot Last vote slot observed per peer\n" + "# TYPE lean_gossip_votes_last_slot gauge\n"); + if (rc != 0) + { + return rc; + } + + for (size_t i = 0; i < count; ++i) + { + const struct lantern_peer_vote_metric *metric = &snapshot->peer_vote_metrics[i]; + char peer_id[sizeof(metric->peer_id)]; + strncpy(peer_id, metric->peer_id, sizeof(peer_id) - 1); + peer_id[sizeof(peer_id) - 1] = '\0'; + + rc = metrics_buffer_appendf( + buf, + "lean_gossip_votes_last_slot{peer=\"%s\"} %" PRIu64 "\n", + peer_id, + metric->last_slot); + if (rc != 0) + { + return rc; } } - if (append_histogram_metrics( - &buf, - "lean_fork_choice_block_processing_time_seconds", - "Time taken to process block in fork choice", - &lean->fork_choice_block_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_attestation_validation_time_seconds", - "Time taken to validate attestation", - &lean->attestation_validation_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_state_transition_time_seconds", - "Time to process state transition", - &lean->state_transition_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_state_transition_slots_processing_time_seconds", - "Time taken to process slots during state transition", - &lean->state_slots_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_state_transition_block_processing_time_seconds", - "Time taken to process block during state transition", - &lean->state_block_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_state_transition_attestations_processing_time_seconds", - "Time taken to process attestations during state transition", - &lean->state_attestations_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_pq_signature_attestation_signing_time_seconds", - "Time taken to sign an attestation", - &lean->pq_signature_signing_time) - != 0 - || append_histogram_metrics( - &buf, - "lean_pq_signature_attestation_verification_time_seconds", - "Time taken to verify an attestation signature", - &lean->pq_signature_verification_time) - != 0) { - metrics_buffer_free(&buf); - return -1; + return 0; +} + + +/** + * @brief Append histogram metrics derived from the lean metrics snapshot. + */ +static int append_lean_histograms( + struct lantern_metrics_body_buffer *buf, + const struct lean_metrics_snapshot *lean) +{ + if (!buf || !lean) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + + int rc = append_histogram_metrics( + buf, + "lean_fork_choice_block_processing_time_seconds", + "Time taken to process block in fork choice", + &lean->fork_choice_block_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_attestation_validation_time_seconds", + "Time taken to validate attestation", + &lean->attestation_validation_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_state_transition_time_seconds", + "Time to process state transition", + &lean->state_transition_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_state_transition_slots_processing_time_seconds", + "Time taken to process slots during state transition", + &lean->state_slots_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_state_transition_block_processing_time_seconds", + "Time taken to process block during state transition", + &lean->state_block_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_state_transition_attestations_processing_time_seconds", + "Time taken to process attestations during state transition", + &lean->state_attestations_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_pq_signature_attestation_signing_time_seconds", + "Time taken to sign an attestation", + &lean->pq_signature_signing_time); + if (rc != 0) + { + return rc; + } + + rc = append_histogram_metrics( + buf, + "lean_pq_signature_attestation_verification_time_seconds", + "Time taken to verify an attestation signature", + &lean->pq_signature_verification_time); + if (rc != 0) + { + return rc; + } + + return 0; +} + + +/** + * Format a metrics snapshot as a Prometheus text body. + * + * @param snapshot Metrics snapshot (not modified). + * @param out_body Output heap buffer (caller owns on success). + * @param out_len Output body length in bytes (excluding terminator). + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_METRICS_SERVER_ERR_OVERFLOW on size overflow. + * @return LANTERN_METRICS_SERVER_ERR_FORMATTING on formatting failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int format_metrics_body( + const struct lantern_metrics_snapshot *snapshot, + char **out_body, + size_t *out_len) +{ + if (!snapshot || !out_body || !out_len) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + + int result = 0; + struct lantern_metrics_body_buffer buf; + memset(&buf, 0, sizeof(buf)); + + result = metrics_buffer_init(&buf, LANTERN_METRICS_BODY_INITIAL_CAP); + if (result != 0) + { + return result; + } + + result = append_lean_chain_metrics(&buf, snapshot); + if (result != 0) + { + goto cleanup; + } + + result = append_peer_vote_metrics(&buf, snapshot); + if (result != 0) + { + goto cleanup; + } + + result = append_lean_histograms(&buf, &snapshot->lean_metrics); + if (result != 0) + { + goto cleanup; } *out_body = buf.data; *out_len = buf.len; + buf.data = NULL; + result = 0; + +cleanup: + metrics_buffer_free(&buf); + return result; +} + + +/** + * Convert a peer address to a printable string. + * + * @param peer_addr Peer address (may be NULL). + * @param out Output buffer (NUL-terminated on return). + * @param out_len Output buffer length. + * + * @note Thread safety: This function is thread-safe. + */ +static void peer_to_text(const struct sockaddr_in *peer_addr, char *out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + const char *fallback = "unknown"; + if (!peer_addr) + { + strncpy(out, fallback, out_len - 1); + out[out_len - 1] = '\0'; + return; + } + + if (!inet_ntop(AF_INET, &peer_addr->sin_addr, out, out_len)) + { + strncpy(out, fallback, out_len - 1); + out[out_len - 1] = '\0'; + } +} + + +/** + * Parse a minimal HTTP request line (method and path). + * + * @param request Request bytes (NUL-terminated). + * @param method Output method buffer (NUL-terminated on success). + * @param method_len Method buffer length. + * @param path Output path buffer (NUL-terminated on success). + * @param path_len Path buffer length. + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_MALFORMED_REQUEST on parse failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int parse_request_line( + const char *request, + char *method, + size_t method_len, + char *path, + size_t path_len) +{ + if (!request || !method || method_len == 0 || !path || path_len == 0) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + + const char *space = strchr(request, ' '); + if (!space) + { + return LANTERN_METRICS_SERVER_ERR_MALFORMED_REQUEST; + } + + size_t method_written = (size_t)(space - request); + if (method_written == 0 || method_written >= method_len) + { + return LANTERN_METRICS_SERVER_ERR_MALFORMED_REQUEST; + } + + memcpy(method, request, method_written); + method[method_written] = '\0'; + + const char *cursor = space; + while (*cursor == ' ') + { + ++cursor; + } + if (*cursor == '\0') + { + return LANTERN_METRICS_SERVER_ERR_MALFORMED_REQUEST; + } + + const char *path_start = cursor; + while (*cursor != '\0' + && *cursor != ' ' + && *cursor != '\r' + && *cursor != '\n' + && *cursor != '\t') + { + ++cursor; + } + + size_t path_written = (size_t)(cursor - path_start); + if (path_written == 0 || path_written >= path_len) + { + return LANTERN_METRICS_SERVER_ERR_MALFORMED_REQUEST; + } + + memcpy(path, path_start, path_written); + path[path_written] = '\0'; return 0; } -static void handle_metrics_request( + +/** + * Send an HTTP response and map common errors to metrics error codes. + * + * @param client_fd Client socket file descriptor. + * @param status_code HTTP status code. + * @param status_text HTTP status text. + * @param content_type Content-Type header value. + * @param body Response body (may be NULL when body_len is 0). + * @param body_len Response body length in bytes. + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_FORMATTING on header formatting failure. + * @return LANTERN_METRICS_SERVER_ERR_IO on send failure. + * + * @note Thread safety: Caller must ensure exclusive access to client_fd. + */ +static int send_http_response( + int client_fd, + int status_code, + const char *status_text, + const char *content_type, + const char *body, + size_t body_len) +{ + int rc = lantern_http_send_response( + client_fd, + status_code, + status_text, + content_type, + body, + body_len); + if (rc == 0) + { + return 0; + } + + if (rc == -1) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + if (rc == -3) + { + return LANTERN_METRICS_SERVER_ERR_FORMATTING; + } + return LANTERN_METRICS_SERVER_ERR_IO; +} + + +/** + * Send a JSON error response. + * + * @param client_fd Client socket file descriptor. + * @param status_code HTTP status code. + * @param status_text HTTP status text. + * @param json_body JSON body (may be NULL). + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_FORMATTING on formatting failure. + * @return LANTERN_METRICS_SERVER_ERR_IO on send failure. + * + * @note Thread safety: Caller must ensure exclusive access to client_fd. + */ +static int send_json_error( + int client_fd, + int status_code, + const char *status_text, + const char *json_body) +{ + if (!json_body) + { + return send_http_response(client_fd, status_code, status_text, "application/json", NULL, 0); + } + + return send_http_response( + client_fd, + status_code, + status_text, + "application/json", + json_body, + strlen(json_body)); +} + + +/** + * Handle a single client connection. + * + * @param server Metrics server instance. + * @param client_fd Client socket file descriptor. + * @param peer_addr Peer address (may be NULL). + * + * @note Thread safety: This function is thread-safe if callbacks are thread-safe. + */ +static void handle_client_connection( struct lantern_metrics_server *server, int client_fd, - const struct sockaddr_in *peer_addr) { - char buffer[LANTERN_METRICS_BUFFER_SIZE]; - ssize_t received = recv(client_fd, buffer, sizeof(buffer) - 1, 0); - if (received <= 0) { + const struct sockaddr_in *peer_addr) +{ + if (!server || client_fd < 0) + { return; } - buffer[received] = '\0'; - char method[8]; - char path[128]; - if (sscanf(buffer, "%7s %127s", method, path) != 2) { - lantern_http_send_response( - client_fd, - 400, - "Bad Request", - "application/json", - "{\"error\":\"malformed request\"}", - strlen("{\"error\":\"malformed request\"}")); + char peer_text[INET_ADDRSTRLEN]; + peer_to_text(peer_addr, peer_text, sizeof(peer_text)); + + char buffer[LANTERN_METRICS_READ_BUFFER_SIZE]; + ssize_t received = -1; + do + { + received = recv(client_fd, buffer, sizeof(buffer) - 1, 0); + } while (received < 0 && errno == EINTR); + + if (received <= 0) + { return; } - char peer_text[INET_ADDRSTRLEN]; - if (peer_addr) { - if (!inet_ntop(AF_INET, &peer_addr->sin_addr, peer_text, sizeof(peer_text))) { - strncpy(peer_text, "unknown", sizeof(peer_text)); - peer_text[sizeof(peer_text) - 1] = '\0'; + buffer[(size_t)received] = '\0'; + + char method[LANTERN_METRICS_METHOD_CAP]; + char path[LANTERN_METRICS_PATH_CAP]; + + int result = parse_request_line(buffer, method, sizeof(method), path, sizeof(path)); + if (result != 0) + { + int rc = send_json_error(client_fd, 400, "Bad Request", METRICS_JSON_MALFORMED_REQUEST); + if (rc == 0) + { + lantern_log_info( + "metrics", + &(const struct lantern_log_metadata){.peer = peer_text}, + "malformed request -> 400"); + } + else + { + lantern_log_error( + "metrics", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send 400 response rc=%d", + rc); } - } else { - strncpy(peer_text, "unknown", sizeof(peer_text)); - peer_text[sizeof(peer_text) - 1] = '\0'; + return; } - if (strcmp(method, "GET") != 0 || strcmp(path, "/metrics") != 0) { - lantern_http_send_response( - client_fd, - 404, - "Not Found", - "application/json", - "{\"error\":\"unknown endpoint\"}", - strlen("{\"error\":\"unknown endpoint\"}")); - lantern_log_info( - "metrics", - &(const struct lantern_log_metadata){.peer = peer_text}, - "%s %s -> 404", - method, - path); + if (strcmp(method, "GET") != 0 || strcmp(path, LANTERN_METRICS_ENDPOINT_PATH) != 0) + { + int rc = send_json_error(client_fd, 404, "Not Found", METRICS_JSON_UNKNOWN_ENDPOINT); + if (rc == 0) + { + lantern_log_info( + "metrics", + &(const struct lantern_log_metadata){.peer = peer_text}, + "%s %s -> 404", + method, + path); + } + else + { + lantern_log_error( + "metrics", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send 404 response rc=%d", + rc); + } return; } - if (!server->callbacks.snapshot) { - lantern_http_send_response( - client_fd, - 503, - "Service Unavailable", - "application/json", - "{\"error\":\"metrics unavailable\"}", - strlen("{\"error\":\"metrics unavailable\"}")); + if (!server->callbacks.snapshot) + { + int rc = send_json_error(client_fd, 503, "Service Unavailable", METRICS_JSON_UNAVAILABLE); lantern_log_error( "metrics", &(const struct lantern_log_metadata){.peer = peer_text}, - "metrics callback missing"); + "metrics callback missing rc=%d", + rc); return; } struct lantern_metrics_snapshot snapshot; memset(&snapshot, 0, sizeof(snapshot)); - if (server->callbacks.snapshot(server->callbacks.context, &snapshot) != 0) { - lantern_http_send_response( - client_fd, - 503, - "Service Unavailable", - "application/json", - "{\"error\":\"metrics unavailable\"}", - strlen("{\"error\":\"metrics unavailable\"}")); + if (server->callbacks.snapshot(server->callbacks.context, &snapshot) != 0) + { + int rc = send_json_error(client_fd, 503, "Service Unavailable", METRICS_JSON_UNAVAILABLE); lantern_log_error( "metrics", &(const struct lantern_log_metadata){.peer = peer_text}, - "snapshot failed"); + "snapshot failed rc=%d", + rc); return; } char *body = NULL; size_t body_len = 0; - if (format_metrics_body(&snapshot, &body, &body_len) != 0) { - lantern_http_send_response( + result = format_metrics_body(&snapshot, &body, &body_len); + if (result != 0) + { + int rc = send_json_error( client_fd, 500, "Internal Server Error", - "application/json", - "{\"error\":\"metrics formatting failed\"}", - strlen("{\"error\":\"metrics formatting failed\"}")); + METRICS_JSON_FORMATTING_FAILED); lantern_log_error( "metrics", &(const struct lantern_log_metadata){.peer = peer_text}, - "formatting failed"); + "formatting failed result=%d send_rc=%d", + result, + rc); return; } - if (lantern_http_send_response( - client_fd, - 200, - "OK", - "text/plain; version=0.0.4", - body, - body_len) - != 0) { + result = send_http_response( + client_fd, + 200, + "OK", + LANTERN_METRICS_TEXT_CONTENT_TYPE, + body, + body_len); + free(body); + if (result != 0) + { lantern_log_error( "metrics", &(const struct lantern_log_metadata){.peer = peer_text}, - "send failed"); - free(body); + "send failed rc=%d", + result); return; } - free(body); + lantern_log_info( "metrics", &(const struct lantern_log_metadata){.peer = peer_text}, @@ -478,36 +1132,66 @@ static void handle_metrics_request( path); } -static void *lantern_metrics_thread(void *arg) { + +/** + * Thread entry point for the metrics server accept loop. + * + * @param arg Pointer to struct lantern_metrics_server. + * @return NULL. + * + * @note Thread safety: This function is not thread-safe; it is intended to run + * as a single server thread created by lantern_metrics_server_start(). + */ +static void *lantern_metrics_thread(void *arg) +{ struct lantern_metrics_server *server = arg; - while (server->running) { + if (!server) + { + return NULL; + } + + int listen_fd = server->listen_fd; + for (;;) + { struct sockaddr_in peer; socklen_t peer_len = sizeof(peer); - int client_fd = accept(server->listen_fd, (struct sockaddr *)&peer, &peer_len); - if (client_fd < 0) { - if (!server->running) { - break; - } - if (errno == EINTR) { + int client_fd = accept(listen_fd, (struct sockaddr *)&peer, &peer_len); + if (client_fd < 0) + { + if (errno == EINTR) + { continue; } - lantern_log_error( - "metrics", - NULL, - "accept failed errno=%d", - errno); + if (errno == EBADF || errno == EINVAL) + { + break; + } + lantern_log_error("metrics", NULL, "accept failed errno=%d", errno); continue; } - handle_metrics_request(server, client_fd, &peer); + + handle_client_connection(server, client_fd, &peer); close(client_fd); } + return NULL; } -void lantern_metrics_server_init(struct lantern_metrics_server *server) { - if (!server) { + +/** + * Initialize a metrics server structure. + * + * @param server Server instance to initialize (modified in place). + * + * @note Thread safety: Caller must not call concurrently with start/stop. + */ +void lantern_metrics_server_init(struct lantern_metrics_server *server) +{ + if (!server) + { return; } + memset(server, 0, sizeof(*server)); server->listen_fd = -1; server->running = 0; @@ -515,30 +1199,64 @@ void lantern_metrics_server_init(struct lantern_metrics_server *server) { server->port = 0; } -void lantern_metrics_server_reset(struct lantern_metrics_server *server) { - if (!server) { + +/** + * Reset a metrics server structure, stopping it if running. + * + * @param server Server instance to reset (modified in place). + * + * @note Thread safety: Caller must not call concurrently with start/stop. + */ +void lantern_metrics_server_reset(struct lantern_metrics_server *server) +{ + if (!server) + { return; } + lantern_metrics_server_stop(server); lantern_metrics_server_init(server); } + +/** + * Start the metrics server. + * + * Creates a listening socket and starts a background thread to accept incoming + * connections and serve GET /metrics. + * + * @param server Server instance to start (modified in place). + * @param port Port to bind (0 binds an ephemeral port). + * @param callbacks Metrics snapshot callback interface. + * + * @spec POSIX sockets and pthreads. + * + * @return 0 on success. + * @return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_METRICS_SERVER_ERR_IO on socket/bind/listen/thread creation failure. + * + * @note Thread safety: Caller must serialize start/stop operations. + */ int lantern_metrics_server_start( struct lantern_metrics_server *server, uint16_t port, - const struct lantern_metrics_callbacks *callbacks) { - if (!server || !callbacks || !callbacks->snapshot) { - return -1; + const struct lantern_metrics_callbacks *callbacks) +{ + if (!server || !callbacks || !callbacks->snapshot) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; } int fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) { + if (fd < 0) + { lantern_log_error("metrics", NULL, "socket creation failed errno=%d", errno); - return -1; + return LANTERN_METRICS_SERVER_ERR_IO; } int opt = 1; - if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) + { lantern_log_warn("metrics", NULL, "setsockopt(SO_REUSEADDR) failed errno=%d", errno); } @@ -548,15 +1266,18 @@ int lantern_metrics_server_start( addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(port); - if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { lantern_log_error("metrics", NULL, "bind failed errno=%d", errno); close(fd); - return -1; + return LANTERN_METRICS_SERVER_ERR_IO; } - if (listen(fd, 16) != 0) { + + if (listen(fd, LANTERN_METRICS_LISTEN_BACKLOG) != 0) + { lantern_log_error("metrics", NULL, "listen failed errno=%d", errno); close(fd); - return -1; + return LANTERN_METRICS_SERVER_ERR_IO; } server->listen_fd = fd; @@ -565,14 +1286,16 @@ int lantern_metrics_server_start( server->running = 1; server->thread_started = 0; - int rc = pthread_create(&server->thread, NULL, lantern_metrics_thread, server); - if (rc != 0) { - lantern_log_error("metrics", NULL, "pthread_create failed rc=%d", rc); + int create_rc = pthread_create(&server->thread, NULL, lantern_metrics_thread, server); + if (create_rc != 0) + { + lantern_log_error("metrics", NULL, "pthread_create failed rc=%d", create_rc); close(fd); server->listen_fd = -1; server->running = 0; - return -1; + return LANTERN_METRICS_SERVER_ERR_IO; } + server->thread_started = 1; lantern_log_info( "metrics", @@ -582,22 +1305,34 @@ int lantern_metrics_server_start( return 0; } -void lantern_metrics_server_stop(struct lantern_metrics_server *server) { - if (!server) { + +/** + * Stop the metrics server if running. + * + * @param server Server instance to stop (modified in place). + * + * @note Thread safety: Caller must serialize start/stop operations. + */ +void lantern_metrics_server_stop(struct lantern_metrics_server *server) +{ + if (!server) + { return; } - if (server->running) { - server->running = 0; - if (server->listen_fd >= 0) { - shutdown(server->listen_fd, SHUT_RDWR); - } + + server->running = 0; + + int listen_fd = server->listen_fd; + server->listen_fd = -1; + if (listen_fd >= 0) + { + (void)shutdown(listen_fd, SHUT_RDWR); + close(listen_fd); } - if (server->thread_started) { + + if (server->thread_started) + { pthread_join(server->thread, NULL); server->thread_started = 0; } - if (server->listen_fd >= 0) { - close(server->listen_fd); - server->listen_fd = -1; - } } diff --git a/src/http/server.c b/src/http/server.c index fac33ff..599ed62 100644 --- a/src/http/server.c +++ b/src/http/server.c @@ -1,69 +1,628 @@ -#include "lantern/http/server.h" +/** + * @file server.c + * @brief Lean API HTTP server. + * + * Exposes a minimal JSON HTTP API: + * - GET /lean/v1/head + * - GET /lean/v1/validators + * - POST /lean/v1/validators/{index}/(activate|deactivate) + * + * @spec RFC 9110/9112 (HTTP) and POSIX sockets/pthreads. + */ -#include "lantern/http/common.h" -#include "lantern/support/log.h" -#include "lantern/support/strings.h" +#include "lantern/http/server.h" #include -#include #include #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include #include -#define LANTERN_HTTP_BUFFER_SIZE 4096 +#include "lantern/http/common.h" +#include "lantern/support/log.h" +#include "lantern/support/strings.h" -static void root_to_hex(const LanternRoot *root, char *out, size_t out_len) { - if (!root || !out || out_len < (2 * LANTERN_ROOT_SIZE) + 3) { - if (out && out_len > 0) { - out[0] = '\0'; - } +static const size_t LANTERN_HTTP_READ_BUFFER_SIZE = 4096; +static const size_t LANTERN_HTTP_BODY_INITIAL_CAP = 512; +static const int LANTERN_HTTP_LISTEN_BACKLOG = 16; +static const size_t LANTERN_HTTP_ROOT_HEX_CAP = (2u * LANTERN_ROOT_SIZE) + 3u; + +enum +{ + LANTERN_HTTP_METHOD_CAP = 8, + LANTERN_HTTP_PATH_CAP = 256, +}; + +/** + * HTTP server module-specific error codes. + */ +typedef enum +{ + LANTERN_HTTP_SERVER_OK = 0, + LANTERN_HTTP_SERVER_ERR_INVALID_PARAM = -1, + LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY = -2, + LANTERN_HTTP_SERVER_ERR_OVERFLOW = -3, + LANTERN_HTTP_SERVER_ERR_IO = -4, + LANTERN_HTTP_SERVER_ERR_FORMATTING = -5, + LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST = -6, +} lantern_http_server_error_t; + +struct lantern_http_body_buffer +{ + char *data; /**< Heap buffer (NUL-terminated). */ + size_t len; /**< Bytes written (excluding terminator). */ + size_t cap; /**< Allocated capacity in bytes. */ +}; + +/** + * Initialize a dynamic HTTP body buffer. + * + * @param buf Buffer to initialize (modified in place). + * @param initial_cap Initial allocation size in bytes (0 uses default). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int http_buffer_init(struct lantern_http_body_buffer *buf, size_t initial_cap) +{ + if (!buf) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + size_t capacity = initial_cap != 0 ? initial_cap : LANTERN_HTTP_BODY_INITIAL_CAP; + buf->data = malloc(capacity); + if (!buf->data) + { + return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY; + } + + buf->len = 0; + buf->cap = capacity; + buf->data[0] = '\0'; + return 0; +} + + +/** + * @brief Free resources owned by an HTTP body buffer. + * + * @param buf Buffer to free (may be NULL). + * + * @note Thread safety: This function is thread-safe. + */ +static void http_buffer_free(struct lantern_http_body_buffer *buf) +{ + if (!buf) + { return; } - if (lantern_bytes_to_hex(root->bytes, LANTERN_ROOT_SIZE, out, out_len, 1) != 0) { - if (out_len > 0) { - out[0] = '\0'; + + free(buf->data); + buf->data = NULL; + buf->len = 0; + buf->cap = 0; +} + + +/** + * Ensure an HTTP body buffer has space for an additional number of bytes. + * + * @param buf Buffer to grow (modified in place). + * @param extra Additional bytes required (excluding terminator). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_HTTP_SERVER_ERR_OVERFLOW on size overflow. + * + * @note Thread safety: This function is thread-safe. + */ +static int http_buffer_reserve(struct lantern_http_body_buffer *buf, size_t extra) +{ + if (!buf) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + if (extra == 0) + { + return 0; + } + + if (buf->len > SIZE_MAX - extra - 1) + { + return LANTERN_HTTP_SERVER_ERR_OVERFLOW; + } + + size_t required = buf->len + extra + 1; + if (required <= buf->cap) + { + return 0; + } + + size_t new_cap = buf->cap != 0 ? buf->cap : LANTERN_HTTP_BODY_INITIAL_CAP; + while (new_cap < required) + { + if (new_cap > SIZE_MAX / 2) + { + return LANTERN_HTTP_SERVER_ERR_OVERFLOW; + } + new_cap *= 2; + } + + char *data = realloc(buf->data, new_cap); + if (!data) + { + return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY; + } + + buf->data = data; + buf->cap = new_cap; + return 0; +} + + +/** + * Append raw bytes to an HTTP body buffer. + * + * @param buf Buffer to append to (modified in place). + * @param data Bytes to append. + * @param len Number of bytes to append. + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_HTTP_SERVER_ERR_OVERFLOW on size overflow. + * + * @note Thread safety: This function is thread-safe. + */ +static int http_buffer_append(struct lantern_http_body_buffer *buf, const char *data, size_t len) +{ + if (!buf) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + if (len == 0) + { + return 0; + } + if (!data) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + int result = http_buffer_reserve(buf, len); + if (result != 0) + { + return result; + } + + memcpy(buf->data + buf->len, data, len); + buf->len += len; + buf->data[buf->len] = '\0'; + return 0; +} + + +/** + * Append a NUL-terminated string to an HTTP body buffer. + * + * @param buf Buffer to append to (modified in place). + * @param str String to append (not modified). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_HTTP_SERVER_ERR_OVERFLOW on size overflow. + * + * @note Thread safety: This function is thread-safe. + */ +static int http_buffer_append_cstr(struct lantern_http_body_buffer *buf, const char *str) +{ + if (!buf || !str) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + return http_buffer_append(buf, str, strlen(str)); +} + + +/** + * Append formatted text to an HTTP body buffer. + * + * @param buf Buffer to append to (modified in place). + * @param fmt printf-style format string. + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_HTTP_SERVER_ERR_OVERFLOW on size overflow. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on formatting failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int http_buffer_appendf(struct lantern_http_body_buffer *buf, const char *fmt, ...) +{ + if (!buf || !fmt) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + va_list args; + va_start(args, fmt); + va_list args_copy; + va_copy(args_copy, args); + + int required = vsnprintf(NULL, 0, fmt, args); + va_end(args); + if (required < 0) + { + va_end(args_copy); + return LANTERN_HTTP_SERVER_ERR_FORMATTING; + } + + size_t extra = (size_t)required; + int result = http_buffer_reserve(buf, extra); + if (result != 0) + { + va_end(args_copy); + return result; + } + + int written = vsnprintf(buf->data + buf->len, buf->cap - buf->len, fmt, args_copy); + va_end(args_copy); + if (written < 0 || (size_t)written != extra) + { + return LANTERN_HTTP_SERVER_ERR_FORMATTING; + } + + buf->len += extra; + return 0; +} + + +/** + * Append a JSON-escaped string value to a buffer (no surrounding quotes). + * + * @param buf Buffer to append to (modified in place). + * @param str String to JSON-escape (may be NULL, treated as empty). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_OUT_OF_MEMORY on allocation failure. + * @return LANTERN_HTTP_SERVER_ERR_OVERFLOW on size overflow. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on formatting failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int http_buffer_append_json_escaped(struct lantern_http_body_buffer *buf, const char *str) +{ + if (!buf) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + const char *text = str ? str : ""; + for (; *text != '\0'; ++text) + { + unsigned char c = (unsigned char)(*text); + switch (c) + { + case '"': + { + int rc = http_buffer_append(buf, "\\\"", 2); + if (rc != 0) + { + return rc; + } + break; + } + case '\\': + { + int rc = http_buffer_append(buf, "\\\\", 2); + if (rc != 0) + { + return rc; + } + break; + } + case '\b': + { + int rc = http_buffer_append(buf, "\\b", 2); + if (rc != 0) + { + return rc; + } + break; + } + case '\f': + { + int rc = http_buffer_append(buf, "\\f", 2); + if (rc != 0) + { + return rc; + } + break; + } + case '\n': + { + int rc = http_buffer_append(buf, "\\n", 2); + if (rc != 0) + { + return rc; + } + break; + } + case '\r': + { + int rc = http_buffer_append(buf, "\\r", 2); + if (rc != 0) + { + return rc; + } + break; + } + case '\t': + { + int rc = http_buffer_append(buf, "\\t", 2); + if (rc != 0) + { + return rc; + } + break; + } + default: + { + if (c < 0x20) + { + char escaped[7]; + int written = snprintf(escaped, sizeof(escaped), "\\u%04x", (unsigned)c); + if (written < 0 || (size_t)written >= sizeof(escaped)) + { + return LANTERN_HTTP_SERVER_ERR_FORMATTING; + } + int rc = http_buffer_append(buf, escaped, (size_t)written); + if (rc != 0) + { + return rc; + } + break; + } + int rc = http_buffer_append(buf, (const char *)&c, 1); + if (rc != 0) + { + return rc; + } + break; + } } } + + return 0; +} + + +/** + * Convert a root to a 0x-prefixed hex string. + * + * @param root Root to encode. + * @param out Output buffer (NUL-terminated on return when out_len > 0). + * @param out_len Output buffer length. + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on encoding failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int root_to_hex(const LanternRoot *root, char *out, size_t out_len) +{ + if (!out || out_len == 0) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + out[0] = '\0'; + if (!root || out_len < LANTERN_HTTP_ROOT_HEX_CAP) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + if (lantern_bytes_to_hex(root->bytes, LANTERN_ROOT_SIZE, out, out_len, 1) != 0) + { + out[0] = '\0'; + return LANTERN_HTTP_SERVER_ERR_FORMATTING; + } + + return 0; +} + + +/** + * Send an HTTP JSON response. + * + * @param client_fd Client socket file descriptor. + * @param status_code HTTP status code. + * @param status_text HTTP status text. + * @param json_body JSON body (may be NULL when json_len is 0). + * @param json_len JSON body length in bytes. + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on header formatting failure. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: Caller must ensure exclusive access to client_fd. + */ +static int send_json_response( + int client_fd, + int status_code, + const char *status_text, + const char *json_body, + size_t json_len) +{ + int rc = lantern_http_send_response( + client_fd, + status_code, + status_text, + "application/json", + json_body, + json_len); + if (rc == 0) + { + return 0; + } + + if (rc == -1) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + if (rc == -3) + { + return LANTERN_HTTP_SERVER_ERR_FORMATTING; + } + return LANTERN_HTTP_SERVER_ERR_IO; } -static int send_simple_json(int fd, int status_code, const char *status_text, const char *json_body) { - size_t len = json_body ? strlen(json_body) : 0; - if (lantern_http_send_response(fd, status_code, status_text, "application/json", json_body, len) != 0) { - return -1; + +/** + * Send a simple static JSON error response. + * + * @param client_fd Client socket file descriptor. + * @param status_code HTTP status code. + * @param status_text HTTP status text. + * @param json_body JSON body (may be NULL). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on header formatting failure. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: Caller must ensure exclusive access to client_fd. + */ +static int send_json_error( + int client_fd, + int status_code, + const char *status_text, + const char *json_body) +{ + if (!json_body) + { + return send_json_response(client_fd, status_code, status_text, NULL, 0); } - return status_code; + + return send_json_response(client_fd, status_code, status_text, json_body, strlen(json_body)); } -static int handle_get_head(struct lantern_http_server *server, int client_fd) { - if (!server->callbacks.snapshot_head) { - return send_simple_json( + +/** + * Send a JSON error response and set the status code output. + * + * @param client_fd Client socket file descriptor. + * @param status_code HTTP status code. + * @param status_text HTTP status text. + * @param json_body JSON body. + * @param out_status_code Output status code (modified in place). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on header formatting failure. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: Caller must ensure exclusive access to client_fd. + */ +static int send_json_error_status( + int client_fd, + int status_code, + const char *status_text, + const char *json_body, + int *out_status_code) +{ + if (!out_status_code) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + *out_status_code = status_code; + return send_json_error(client_fd, status_code, status_text, json_body); +} + + +/** + * Handle the `GET /lean/v1/head` endpoint. + * + * @param server HTTP server instance. + * @param client_fd Client socket file descriptor. + * @param out_status_code Output HTTP status code (modified in place). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: This function is thread-safe if callbacks are thread-safe. + */ +static int handle_get_head( + struct lantern_http_server *server, + int client_fd, + int *out_status_code) +{ + if (!server || client_fd < 0 || !out_status_code) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + if (!server->callbacks.snapshot_head) + { + return send_json_error_status( client_fd, 501, "Not Implemented", - "{\"error\":\"head query unsupported\"}"); + "{\"error\":\"head query unsupported\"}", + out_status_code); } + struct lantern_http_head_snapshot snapshot; - if (server->callbacks.snapshot_head(server->callbacks.context, &snapshot) != 0) { - return send_simple_json( + if (server->callbacks.snapshot_head(server->callbacks.context, &snapshot) != 0) + { + return send_json_error_status( client_fd, 503, "Service Unavailable", - "{\"error\":\"head snapshot unavailable\"}"); + "{\"error\":\"head snapshot unavailable\"}", + out_status_code); + } + + char head_hex[LANTERN_HTTP_ROOT_HEX_CAP]; + char justified_hex[LANTERN_HTTP_ROOT_HEX_CAP]; + char finalized_hex[LANTERN_HTTP_ROOT_HEX_CAP]; + if (root_to_hex(&snapshot.head_root, head_hex, sizeof(head_hex)) != 0 + || root_to_hex(&snapshot.justified.root, justified_hex, sizeof(justified_hex)) != 0 + || root_to_hex(&snapshot.finalized.root, finalized_hex, sizeof(finalized_hex)) != 0) + { + return send_json_error_status( + client_fd, + 500, + "Internal Server Error", + "{\"error\":\"root encoding failed\"}", + out_status_code); } - char head_hex[2 * LANTERN_ROOT_SIZE + 3]; - char justified_hex[2 * LANTERN_ROOT_SIZE + 3]; - char finalized_hex[2 * LANTERN_ROOT_SIZE + 3]; - root_to_hex(&snapshot.head_root, head_hex, sizeof(head_hex)); - root_to_hex(&snapshot.justified.root, justified_hex, sizeof(justified_hex)); - root_to_hex(&snapshot.finalized.root, finalized_hex, sizeof(finalized_hex)); char body[512]; - int len = snprintf( + int written = snprintf( body, sizeof(body), "{" @@ -78,293 +637,597 @@ static int handle_get_head(struct lantern_http_server *server, int client_fd) { justified_hex, snapshot.finalized.slot, finalized_hex); - if (len < 0 || (size_t)len >= sizeof(body)) { - return send_simple_json( + if (written < 0 || (size_t)written >= sizeof(body)) + { + return send_json_error_status( client_fd, 500, "Internal Server Error", - "{\"error\":\"head response too large\"}"); + "{\"error\":\"head response too large\"}", + out_status_code); } - if (lantern_http_send_response(client_fd, 200, "OK", "application/json", body, (size_t)len) != 0) { - return -1; + + int rc = send_json_response(client_fd, 200, "OK", body, (size_t)written); + if (rc != 0) + { + return rc; } - return 200; + + *out_status_code = 200; + return 0; } -static int handle_get_validators(struct lantern_http_server *server, int client_fd) { - if (!server->callbacks.validator_count || !server->callbacks.validator_info) { - return send_simple_json( + +/** + * Handle the `GET /lean/v1/validators` endpoint. + * + * @param server HTTP server instance. + * @param client_fd Client socket file descriptor. + * @param out_status_code Output HTTP status code (modified in place). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: This function is thread-safe if callbacks are thread-safe. + */ +static int handle_get_validators( + struct lantern_http_server *server, + int client_fd, + int *out_status_code) +{ + if (!server || client_fd < 0 || !out_status_code) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + if (!server->callbacks.validator_count || !server->callbacks.validator_info) + { + return send_json_error_status( client_fd, 501, "Not Implemented", - "{\"error\":\"validator listing unsupported\"}"); + "{\"error\":\"validator listing unsupported\"}", + out_status_code); } + size_t count = server->callbacks.validator_count(server->callbacks.context); - char *dynamic_body = NULL; - size_t capacity = 512; - dynamic_body = malloc(capacity); - if (!dynamic_body) { - return send_simple_json( - client_fd, - 500, - "Internal Server Error", - "{\"error\":\"allocator failure\"}"); + + struct lantern_http_body_buffer body; + bool body_initialized = false; + int result = http_buffer_init(&body, LANTERN_HTTP_BODY_INITIAL_CAP); + if (result != 0) + { + goto internal_error; } - size_t offset = 0; - int written = snprintf(dynamic_body, capacity, "{\"validators\":["); - if (written < 0) { - free(dynamic_body); - return send_simple_json( - client_fd, - 500, - "Internal Server Error", - "{\"error\":\"formatting failure\"}"); + body_initialized = true; + + result = http_buffer_append_cstr(&body, "{\"validators\":["); + if (result != 0) + { + goto internal_error; } - offset += (size_t)written; - for (size_t i = 0; i < count; ++i) { + + for (size_t i = 0; i < count; ++i) + { struct lantern_http_validator_info info; - if (server->callbacks.validator_info(server->callbacks.context, i, &info) != 0) { - free(dynamic_body); - return send_simple_json( + if (server->callbacks.validator_info(server->callbacks.context, i, &info) != 0) + { + result = send_json_error_status( client_fd, 503, "Service Unavailable", - "{\"error\":\"validator snapshot unavailable\"}"); + "{\"error\":\"validator snapshot unavailable\"}", + out_status_code); + goto cleanup; + } + + if (i > 0) + { + result = http_buffer_append(&body, ",", 1); + if (result != 0) + { + goto internal_error; + } } - char entry[256]; - int entry_len = snprintf( - entry, - sizeof(entry), - "%s{\"index\":%" PRIu64 ",\"enabled\":%s,\"label\":\"%s\"}", - i > 0 ? "," : "", + + result = http_buffer_appendf( + &body, + "{\"index\":%" PRIu64 ",\"enabled\":%s,\"label\":\"", info.global_index, - info.enabled ? "true" : "false", - info.label); - if (entry_len < 0) { - free(dynamic_body); - return send_simple_json( - client_fd, - 500, - "Internal Server Error", - "{\"error\":\"formatting failure\"}"); + info.enabled ? "true" : "false"); + if (result != 0) + { + goto internal_error; } - size_t needed = offset + (size_t)entry_len + 2; - if (needed > capacity) { - size_t new_capacity = capacity * 2; - while (needed > new_capacity) { - new_capacity *= 2; - } - char *resized = realloc(dynamic_body, new_capacity); - if (!resized) { - free(dynamic_body); - return send_simple_json( - client_fd, - 500, - "Internal Server Error", - "{\"error\":\"allocator failure\"}"); - } - dynamic_body = resized; - capacity = new_capacity; + + result = http_buffer_append_json_escaped(&body, info.label); + if (result != 0) + { + goto internal_error; } - memcpy(dynamic_body + offset, entry, (size_t)entry_len); - offset += (size_t)entry_len; - } - if (offset + 2 > capacity) { - char *resized = realloc(dynamic_body, capacity + 2); - if (!resized) { - free(dynamic_body); - return send_simple_json( - client_fd, - 500, - "Internal Server Error", - "{\"error\":\"allocator failure\"}"); + + result = http_buffer_append_cstr(&body, "\"}"); + if (result != 0) + { + goto internal_error; } - dynamic_body = resized; - capacity += 2; } - dynamic_body[offset++] = ']'; - dynamic_body[offset++] = '}'; - int rc = lantern_http_send_response(client_fd, 200, "OK", "application/json", dynamic_body, offset); - free(dynamic_body); - if (rc != 0) { - return -1; + + result = http_buffer_append_cstr(&body, "]}"); + if (result != 0) + { + goto internal_error; } - return 200; + + result = send_json_response(client_fd, 200, "OK", body.data, body.len); + if (result != 0) + { + goto cleanup; + } + + *out_status_code = 200; + result = 0; + goto cleanup; + +internal_error: + { + const char *error_body = "{\"error\":\"allocator failure\"}"; + if (result == LANTERN_HTTP_SERVER_ERR_FORMATTING) + { + error_body = "{\"error\":\"formatting failure\"}"; + } + result = send_json_error_status( + client_fd, + 500, + "Internal Server Error", + error_body, + out_status_code); + } + +cleanup: + if (body_initialized) + { + http_buffer_free(&body); + } + + return result; } + +/** + * Handle validator activation and deactivation requests. + * + * Supports `POST /lean/v1/validators/{index}/activate` and + * `POST /lean/v1/validators/{index}/deactivate`. + * + * @param server HTTP server instance. + * @param client_fd Client socket file descriptor. + * @param path Request path. + * @param out_status_code Output HTTP status code (modified in place). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: This function is thread-safe if callbacks are thread-safe. + */ static int handle_post_validator_action( struct lantern_http_server *server, int client_fd, - const char *path) { - if (!server->callbacks.set_validator_status) { - return send_simple_json( + const char *path, + int *out_status_code) +{ + if (!server || client_fd < 0 || !path || !out_status_code) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + if (!server->callbacks.set_validator_status) + { + return send_json_error_status( client_fd, 501, "Not Implemented", - "{\"error\":\"validator control unsupported\"}"); + "{\"error\":\"validator control unsupported\"}", + out_status_code); } - const char *prefix = "/lean/v1/validators/"; + + static const char prefix[] = "/lean/v1/validators/"; size_t prefix_len = strlen(prefix); - if (strncmp(path, prefix, prefix_len) != 0) { - return send_simple_json( + if (strncmp(path, prefix, prefix_len) != 0) + { + return send_json_error_status( client_fd, 404, "Not Found", - "{\"error\":\"unknown validator path\"}"); + "{\"error\":\"unknown validator path\"}", + out_status_code); } + const char *rest = path + prefix_len; - if (!*rest) { - return send_simple_json( + if (!*rest) + { + return send_json_error_status( client_fd, 400, "Bad Request", - "{\"error\":\"missing validator index\"}"); + "{\"error\":\"missing validator index\"}", + out_status_code); } - char *endptr = NULL; + errno = 0; + char *endptr = NULL; uint64_t index = strtoull(rest, &endptr, 10); - if (errno != 0 || rest == endptr) { - return send_simple_json( + if (errno != 0 || rest == endptr) + { + return send_json_error_status( client_fd, 400, "Bad Request", - "{\"error\":\"invalid validator index\"}"); + "{\"error\":\"invalid validator index\"}", + out_status_code); } - if (!endptr || *endptr != '/') { - return send_simple_json( + if (!endptr || *endptr != '/') + { + return send_json_error_status( client_fd, 400, "Bad Request", - "{\"error\":\"missing validator action\"}"); + "{\"error\":\"missing validator action\"}", + out_status_code); } + const char *action = endptr + 1; - bool enable = false; - if (strcmp(action, "activate") == 0) { - enable = true; - } else if (strcmp(action, "deactivate") == 0) { - enable = false; - } else { - return send_simple_json( + bool should_enable = false; + if (strcmp(action, "activate") == 0) + { + should_enable = true; + } + else if (strcmp(action, "deactivate") == 0) + { + should_enable = false; + } + else + { + return send_json_error_status( client_fd, 404, "Not Found", - "{\"error\":\"unknown validator action\"}"); + "{\"error\":\"unknown validator action\"}", + out_status_code); } - if (server->callbacks.set_validator_status(server->callbacks.context, index, enable) != 0) { - return send_simple_json( + if (server->callbacks.set_validator_status( + server->callbacks.context, + index, + should_enable) + != 0) + { + return send_json_error_status( client_fd, 404, "Not Found", - "{\"error\":\"validator not found\"}"); + "{\"error\":\"validator not found\"}", + out_status_code); } - if (lantern_http_send_response(client_fd, 204, "No Content", "application/json", NULL, 0) != 0) { - return -1; + + int rc = send_json_response(client_fd, 204, "No Content", NULL, 0); + if (rc != 0) + { + return rc; } - return 204; + + *out_status_code = 204; + return 0; } + +/** + * Dispatch an HTTP request to the appropriate handler. + * + * @param server HTTP server instance. + * @param client_fd Client socket file descriptor. + * @param method Request method. + * @param path Request path. + * @param out_status_code Output HTTP status code (modified in place). + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_IO on send failure. + * + * @note Thread safety: This function is thread-safe if callbacks are thread-safe. + */ static int dispatch_request( struct lantern_http_server *server, int client_fd, const char *method, - const char *path) { - if (strcmp(method, "GET") == 0) { - if (strcmp(path, "/lean/v1/head") == 0) { - return handle_get_head(server, client_fd); + const char *path, + int *out_status_code) +{ + if (!server || client_fd < 0 || !method || !path || !out_status_code) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + if (strcmp(method, "GET") == 0) + { + if (strcmp(path, "/lean/v1/head") == 0) + { + return handle_get_head(server, client_fd, out_status_code); } - if (strcmp(path, "/lean/v1/validators") == 0) { - return handle_get_validators(server, client_fd); + if (strcmp(path, "/lean/v1/validators") == 0) + { + return handle_get_validators(server, client_fd, out_status_code); } - } else if (strcmp(method, "POST") == 0) { - return handle_post_validator_action(server, client_fd, path); } - return send_simple_json( + else if (strcmp(method, "POST") == 0) + { + return handle_post_validator_action(server, client_fd, path, out_status_code); + } + + return send_json_error_status( client_fd, 404, "Not Found", - "{\"error\":\"unknown endpoint\"}"); + "{\"error\":\"unknown endpoint\"}", + out_status_code); +} + + +/** + * Convert a peer address to a printable string. + * + * @param peer_addr Peer address (may be NULL). + * @param out Output buffer (NUL-terminated on return). + * @param out_len Output buffer length. + * + * @note Thread safety: This function is thread-safe. + */ +static void peer_to_text(const struct sockaddr_in *peer_addr, char *out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + const char *fallback = "unknown"; + if (!peer_addr) + { + strncpy(out, fallback, out_len - 1); + out[out_len - 1] = '\0'; + return; + } + + if (!inet_ntop(AF_INET, &peer_addr->sin_addr, out, out_len)) + { + strncpy(out, fallback, out_len - 1); + out[out_len - 1] = '\0'; + } +} + + +/** + * Parse a minimal HTTP request line (method and path). + * + * @param request Request bytes (NUL-terminated). + * @param method Output method buffer (NUL-terminated on success). + * @param method_len Method buffer length. + * @param path Output path buffer (NUL-terminated on success). + * @param path_len Path buffer length. + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST on parse failure. + * + * @note Thread safety: This function is thread-safe. + */ +static int parse_request_line( + const char *request, + char *method, + size_t method_len, + char *path, + size_t path_len) +{ + if (!request || !method || method_len == 0 || !path || path_len == 0) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; + } + + const char *space = strchr(request, ' '); + if (!space) + { + return LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST; + } + + size_t method_written = (size_t)(space - request); + if (method_written == 0 || method_written >= method_len) + { + return LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST; + } + + memcpy(method, request, method_written); + method[method_written] = '\0'; + + const char *cursor = space; + while (*cursor == ' ') + { + ++cursor; + } + if (*cursor == '\0') + { + return LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST; + } + + const char *path_start = cursor; + while (*cursor != '\0' + && *cursor != ' ' + && *cursor != '\r' + && *cursor != '\n' + && *cursor != '\t') + { + ++cursor; + } + + size_t path_written = (size_t)(cursor - path_start); + if (path_written == 0 || path_written >= path_len) + { + return LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST; + } + + memcpy(path, path_start, path_written); + path[path_written] = '\0'; + return 0; } + +/** + * Handle a single client connection. + * + * @param server HTTP server instance. + * @param client_fd Client socket file descriptor. + * @param peer_addr Peer address (may be NULL). + * + * @note Thread safety: This function is thread-safe if callbacks are thread-safe. + */ static void handle_client_connection( struct lantern_http_server *server, int client_fd, - const struct sockaddr_in *peer_addr) { - char buffer[LANTERN_HTTP_BUFFER_SIZE]; - ssize_t received = recv(client_fd, buffer, sizeof(buffer) - 1, 0); - if (received <= 0) { + const struct sockaddr_in *peer_addr) +{ + if (!server || client_fd < 0) + { + return; + } + + char peer_text[INET_ADDRSTRLEN]; + peer_to_text(peer_addr, peer_text, sizeof(peer_text)); + + char buffer[LANTERN_HTTP_READ_BUFFER_SIZE]; + ssize_t received = -1; + do + { + received = recv(client_fd, buffer, sizeof(buffer) - 1, 0); + } while (received < 0 && errno == EINTR); + + if (received <= 0) + { return; } - buffer[received] = '\0'; + buffer[(size_t)received] = '\0'; + + char method[LANTERN_HTTP_METHOD_CAP]; + char path[LANTERN_HTTP_PATH_CAP]; + int http_status = 500; - char method[8]; - char path[256]; - if (sscanf(buffer, "%7s %255s", method, path) != 2) { - send_simple_json( + int result = parse_request_line(buffer, method, sizeof(method), path, sizeof(path)); + if (result != 0) + { + int rc = send_json_error( client_fd, 400, "Bad Request", "{\"error\":\"malformed request\"}"); - return; - } - char peer_text[INET_ADDRSTRLEN]; - if (peer_addr) { - if (!inet_ntop(AF_INET, &peer_addr->sin_addr, peer_text, sizeof(peer_text))) { - strncpy(peer_text, "unknown", sizeof(peer_text)); - peer_text[sizeof(peer_text) - 1] = '\0'; + if (rc == 0) + { + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "malformed request -> 400"); } - } else { - strncpy(peer_text, "unknown", sizeof(peer_text)); - peer_text[sizeof(peer_text) - 1] = '\0'; + else + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send 400 response rc=%d", + rc); + } + return; } - int status = dispatch_request(server, client_fd, method, path); - int http_status = status >= 100 ? status : 500; - if (status < 0) { + + result = dispatch_request(server, client_fd, method, path, &http_status); + if (result != 0) + { lantern_log_error( "http", &(const struct lantern_log_metadata){.peer = peer_text}, - "%s %s failed", method, path); - } else { - lantern_log_info( - "http", - &(const struct lantern_log_metadata){.peer = peer_text}, - "%s %s -> %d", + "%s %s failed rc=%d", method, path, - http_status); + result); + return; } + + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "%s %s -> %d", + method, + path, + http_status); } -static void *lantern_http_server_thread(void *arg) { + +/** + * Thread entry point for the HTTP server accept loop. + * + * @param arg Pointer to struct lantern_http_server. + * @return NULL. + * + * @note Thread safety: This function is not thread-safe; it is intended to run + * as a single server thread created by lantern_http_server_start(). + */ +static void *lantern_http_server_thread(void *arg) +{ struct lantern_http_server *server = arg; - while (server->running) { + if (!server) + { + return NULL; + } + + int listen_fd = server->listen_fd; + for (;;) + { struct sockaddr_in peer; socklen_t peer_len = sizeof(peer); - int client_fd = accept(server->listen_fd, (struct sockaddr *)&peer, &peer_len); - if (client_fd < 0) { - if (!server->running) { - break; - } - if (errno == EINTR) { + int client_fd = accept(listen_fd, (struct sockaddr *)&peer, &peer_len); + if (client_fd < 0) + { + if (errno == EINTR) + { continue; } - lantern_log_error( - "http", - &(const struct lantern_log_metadata){0}, - "accept failed errno=%d", - errno); + if (errno == EBADF || errno == EINVAL) + { + break; + } + lantern_log_error("http", NULL, "accept failed errno=%d", errno); continue; } + handle_client_connection(server, client_fd, &peer); close(client_fd); } + return NULL; } -void lantern_http_server_init(struct lantern_http_server *server) { - if (!server) { + +/** + * Initialize an HTTP server structure. + * + * @param server Server instance to initialize (modified in place). + * + * @note Thread safety: Caller must not call concurrently with start/stop. + */ +void lantern_http_server_init(struct lantern_http_server *server) +{ + if (!server) + { return; } + memset(server, 0, sizeof(*server)); server->listen_fd = -1; server->running = 0; @@ -372,32 +1235,69 @@ void lantern_http_server_init(struct lantern_http_server *server) { server->port = 0; } -void lantern_http_server_reset(struct lantern_http_server *server) { - if (!server) { + +/** + * Reset an HTTP server structure, stopping it if running. + * + * @param server Server instance to reset (modified in place). + * + * @note Thread safety: Caller must not call concurrently with start/stop. + */ +void lantern_http_server_reset(struct lantern_http_server *server) +{ + if (!server) + { return; } + lantern_http_server_stop(server); lantern_http_server_init(server); } -int lantern_http_server_start(struct lantern_http_server *server, const struct lantern_http_server_config *config) { - if (!server || !config) { - return -1; + +/** + * Start the HTTP server. + * + * Creates a listening socket and starts a background thread to accept incoming + * connections and serve the Lean API endpoints. + * + * @param server Server instance to start (modified in place). + * @param config Server configuration (callbacks must be set). + * + * @spec POSIX sockets and pthreads. + * + * @return 0 on success. + * @return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_IO on socket/bind/listen/thread creation failure. + * + * @note Thread safety: Caller must serialize start/stop operations. + */ +int lantern_http_server_start( + struct lantern_http_server *server, + const struct lantern_http_server_config *config) +{ + if (!server || !config) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; } if (!config->callbacks.snapshot_head || !config->callbacks.validator_count || !config->callbacks.validator_info - || !config->callbacks.set_validator_status) { - return -1; + || !config->callbacks.set_validator_status) + { + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; } int fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) { - lantern_log_error("http", NULL, "failed to create socket errno=%d", errno); - return -1; + if (fd < 0) + { + lantern_log_error("http", NULL, "socket creation failed errno=%d", errno); + return LANTERN_HTTP_SERVER_ERR_IO; } + int opt = 1; - if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) + { lantern_log_warn("http", NULL, "setsockopt(SO_REUSEADDR) failed errno=%d", errno); } @@ -407,16 +1307,18 @@ int lantern_http_server_start(struct lantern_http_server *server, const struct l addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(config->port); - if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { lantern_log_error("http", NULL, "bind failed errno=%d", errno); close(fd); - return -1; + return LANTERN_HTTP_SERVER_ERR_IO; } - if (listen(fd, 16) != 0) { + if (listen(fd, LANTERN_HTTP_LISTEN_BACKLOG) != 0) + { lantern_log_error("http", NULL, "listen failed errno=%d", errno); close(fd); - return -1; + return LANTERN_HTTP_SERVER_ERR_IO; } server->listen_fd = fd; @@ -424,39 +1326,50 @@ int lantern_http_server_start(struct lantern_http_server *server, const struct l server->port = config->port; server->running = 1; server->thread_started = 0; + int create_rc = pthread_create(&server->thread, NULL, lantern_http_server_thread, server); - if (create_rc != 0) { + if (create_rc != 0) + { lantern_log_error("http", NULL, "pthread_create failed rc=%d", create_rc); close(fd); server->listen_fd = -1; server->running = 0; - return -1; + return LANTERN_HTTP_SERVER_ERR_IO; } + server->thread_started = 1; - lantern_log_info( - "http", - NULL, - "http server listening port=%" PRIu16, - server->port); + lantern_log_info("http", NULL, "http server listening port=%" PRIu16, server->port); return 0; } -void lantern_http_server_stop(struct lantern_http_server *server) { - if (!server) { + +/** + * Stop the HTTP server if running. + * + * @param server Server instance to stop (modified in place). + * + * @note Thread safety: Caller must serialize start/stop operations. + */ +void lantern_http_server_stop(struct lantern_http_server *server) +{ + if (!server) + { return; } + + server->running = 0; + int listen_fd = server->listen_fd; - if (server->running) { - server->running = 0; - } - if (listen_fd >= 0) { - shutdown(listen_fd, SHUT_RDWR); + server->listen_fd = -1; + if (listen_fd >= 0) + { + (void)shutdown(listen_fd, SHUT_RDWR); close(listen_fd); - server->listen_fd = -1; } - if (server->thread_started) { + + if (server->thread_started) + { pthread_join(server->thread, NULL); server->thread_started = 0; } - server->listen_fd = -1; } From 23226e82e5389cb2604815d5dfd27fcdca0022aa Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:12:23 +1000 Subject: [PATCH 12/12] Update Dockerfile --- Dockerfile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 06c4b79..fd30b56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,16 +84,20 @@ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive -# Install runtime dependencies and profiling tools +# Set to "true" to include gdb and perf for debugging/profiling +ARG INCLUDE_DEBUG_TOOLS=false + +# Install runtime dependencies (and optionally profiling tools) RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ - gdb \ libssl3 \ libstdc++6 \ zlib1g \ - linux-tools-generic \ - && rm -rf /var/lib/apt/lists/* \ - && ln -sf /usr/lib/linux-tools/*/perf /usr/local/bin/perf || true + && if [ "$INCLUDE_DEBUG_TOOLS" = "true" ]; then \ + apt-get install -y --no-install-recommends gdb linux-tools-generic \ + && ln -sf /usr/lib/linux-tools/*/perf /usr/local/bin/perf || true; \ + fi \ + && rm -rf /var/lib/apt/lists/* COPY --from=builder /opt/lantern /opt/lantern COPY docker/entrypoint.sh /usr/local/bin/lantern-entrypoint.sh