diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9585471..bac6d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,3 +95,5 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ secrets.DOCKERHUB_USERNAME }}/lantern:${{ github.event.inputs.docker_tag }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/CMakeLists.txt b/CMakeLists.txt index c120137..8d0f74a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,10 @@ if(LANTERN_BUILD_TESTS) target_link_libraries(lantern_metrics_test PRIVATE lantern) add_test(NAME lantern_metrics COMMAND lantern_metrics_test) + add_executable(lantern_checkpoint_sync_test tests/unit/test_checkpoint_sync_api.c) + target_link_libraries(lantern_checkpoint_sync_test PRIVATE lantern) + add_test(NAME lantern_checkpoint_sync COMMAND lantern_checkpoint_sync_test) + add_executable(lantern_validator_duties_integration_test tests/integration/test_validator_duties.c) target_link_libraries(lantern_validator_duties_integration_test PRIVATE lantern) add_test(NAME lantern_validator_duties_integration COMMAND lantern_validator_duties_integration_test) diff --git a/external/c-leanvm-xmss b/external/c-leanvm-xmss index 41c9f2c..512e437 160000 --- a/external/c-leanvm-xmss +++ b/external/c-leanvm-xmss @@ -1 +1 @@ -Subproject commit 41c9f2cf8aa94d77daa1e26cfa8dbbc7346f907c +Subproject commit 512e43782c38bc380e5b4389009032196f93e2d9 diff --git a/include/lantern/core/client.h b/include/lantern/core/client.h index 1a4ac02..bd8cbc8 100644 --- a/include/lantern/core/client.h +++ b/include/lantern/core/client.h @@ -35,6 +35,7 @@ extern "C" { #define LANTERN_DEFAULT_HTTP_PORT 5052 #define LANTERN_DEFAULT_METRICS_PORT 8080 #define LANTERN_DEFAULT_DEVNET "devnet0" +#define LANTERN_PENDING_BLOCK_LIMIT 1024u typedef enum { @@ -46,9 +47,17 @@ typedef enum LANTERN_CLIENT_ERR_GENESIS = -5, LANTERN_CLIENT_ERR_VALIDATOR = -6, LANTERN_CLIENT_ERR_RUNTIME = -7, - LANTERN_CLIENT_ERR_NETWORK = -8 + LANTERN_CLIENT_ERR_NETWORK = -8, + LANTERN_CLIENT_ERR_IGNORED = -9 } lantern_client_error; +typedef enum +{ + LANTERN_SYNC_STATE_IDLE = 0, + LANTERN_SYNC_STATE_SYNCING = 1, + LANTERN_SYNC_STATE_SYNCED = 2 +} LanternSyncState; + struct lantern_client_options { const char *data_dir; const char *genesis_config_path; @@ -80,12 +89,28 @@ struct lantern_pending_block { LanternRoot parent_root; char peer_text[128]; bool parent_requested; + uint64_t received_ms; + uint32_t backfill_depth; +}; + +struct lantern_pending_parent_index_entry { + LanternRoot parent_root; + LanternRoot *child_roots; + size_t length; + size_t capacity; +}; + +struct lantern_pending_parent_index { + struct lantern_pending_parent_index_entry *entries; + size_t length; + size_t capacity; }; struct lantern_pending_block_list { struct lantern_pending_block *items; size_t length; size_t capacity; + struct lantern_pending_parent_index parent_index; }; struct lantern_agg_proof_cache_entry { @@ -175,6 +200,7 @@ struct lantern_client { int validator_stop_flag; struct lantern_metrics_server metrics_server; bool metrics_running; + uint64_t start_time_seconds; struct lantern_http_server http_server; bool http_running; bool genesis_fallback_used; @@ -184,6 +210,7 @@ struct lantern_client { struct libp2p_subscription *connection_subscription; struct lantern_string_list dialer_peers; struct lantern_string_list connected_peer_ids; + struct lantern_string_list inbound_peer_ids; struct lantern_string_list status_failure_peer_ids; struct lantern_pending_block_list pending_blocks; pthread_mutex_t pending_lock; @@ -194,6 +221,8 @@ struct lantern_client { uint64_t sync_last_log_ms; uint64_t sync_last_imported_blocks; uint64_t sync_imported_blocks; + uint64_t sync_target_slot; + LanternSyncState sync_state; bool sync_in_progress; size_t status_requests_inflight_total; size_t status_requests_peak; @@ -291,9 +320,9 @@ int lantern_client_debug_import_block( const char *peer_id_text); size_t lantern_client_pending_block_count(const struct lantern_client *client); -#define LANTERN_DEBUG_BLOCKS_REQUEST_SUCCESS 0 -#define LANTERN_DEBUG_BLOCKS_REQUEST_FAILED 1 -#define LANTERN_DEBUG_BLOCKS_REQUEST_ABORTED 2 +#define LANTERN_TEST_BLOCKS_REQUEST_SUCCESS 0 +#define LANTERN_TEST_BLOCKS_REQUEST_FAILED 1 +#define LANTERN_TEST_BLOCKS_REQUEST_ABORTED 2 int lantern_client_debug_enqueue_pending_block( struct lantern_client *client, diff --git a/include/lantern/http/metrics.h b/include/lantern/http/metrics.h index 35b4aff..0d22081 100644 --- a/include/lantern/http/metrics.h +++ b/include/lantern/http/metrics.h @@ -12,6 +12,7 @@ extern "C" { #endif #define LANTERN_METRICS_MAX_PEER_VOTE_STATS 64u +#define LANTERN_METRICS_CONTENT_TYPE "text/plain; version=0.0.4; charset=utf-8" struct lantern_peer_vote_metric { char peer_id[128]; @@ -23,10 +24,14 @@ struct lantern_peer_vote_metric { }; struct lantern_metrics_snapshot { + uint64_t lean_node_start_time_seconds; uint64_t lean_head_slot; + uint64_t lean_current_slot; + uint64_t lean_safe_target_slot; uint64_t lean_latest_justified_slot; uint64_t lean_latest_finalized_slot; size_t lean_validators_count; + size_t lean_connected_peers; struct lean_metrics_snapshot lean_metrics; size_t peer_vote_metrics_count; struct lantern_peer_vote_metric peer_vote_metrics[LANTERN_METRICS_MAX_PEER_VOTE_STATS]; @@ -53,6 +58,10 @@ int lantern_metrics_server_start( uint16_t port, const struct lantern_metrics_callbacks *callbacks); void lantern_metrics_server_stop(struct lantern_metrics_server *server); +int lantern_metrics_format_prometheus( + const struct lantern_metrics_snapshot *snapshot, + char **out_body, + size_t *out_len); #ifdef __cplusplus } diff --git a/include/lantern/http/server.h b/include/lantern/http/server.h index d3e2cc5..dc6407d 100644 --- a/include/lantern/http/server.h +++ b/include/lantern/http/server.h @@ -11,6 +11,20 @@ extern "C" { #endif +struct lantern_metrics_snapshot; + +typedef enum +{ + LANTERN_HTTP_CB_OK = 0, + LANTERN_HTTP_CB_ERR_INVALID_PARAM = -1, + LANTERN_HTTP_CB_ERR_NOT_FOUND = -2, + LANTERN_HTTP_CB_ERR_INVALID_STATE = -3, + LANTERN_HTTP_CB_ERR_LOCK_FAILED = -4, + LANTERN_HTTP_CB_ERR_HASH_FAILED = -5, + LANTERN_HTTP_CB_ERR_IO = -6, + LANTERN_HTTP_CB_ERR_UNAVAILABLE = -7, +} lantern_http_callback_error_t; + struct lantern_http_head_snapshot { uint64_t slot; LanternRoot head_root; @@ -30,6 +44,8 @@ struct lantern_http_server_callbacks { size_t (*validator_count)(void *context); int (*validator_info)(void *context, size_t index, struct lantern_http_validator_info *out_info); int (*set_validator_status)(void *context, uint64_t global_index, bool enabled); + int (*metrics_snapshot)(void *context, struct lantern_metrics_snapshot *out_snapshot); + int (*finalized_state_ssz)(void *context, uint8_t **out_bytes, size_t *out_len); }; struct lantern_http_server_config { diff --git a/include/lantern/metrics/lean_metrics.h b/include/lantern/metrics/lean_metrics.h index e5be3ff..f46de8a 100644 --- a/include/lantern/metrics/lean_metrics.h +++ b/include/lantern/metrics/lean_metrics.h @@ -11,6 +11,27 @@ extern "C" { #define LEAN_METRICS_MAX_BUCKETS 10u +typedef enum { + LEAN_METRICS_DIR_INBOUND = 0, + LEAN_METRICS_DIR_OUTBOUND = 1, + LEAN_METRICS_DIR_COUNT = 2 +} lean_metrics_direction_t; + +typedef enum { + LEAN_METRICS_CONN_RESULT_SUCCESS = 0, + LEAN_METRICS_CONN_RESULT_TIMEOUT = 1, + LEAN_METRICS_CONN_RESULT_ERROR = 2, + LEAN_METRICS_CONN_RESULT_COUNT = 3 +} lean_metrics_connection_result_t; + +typedef enum { + LEAN_METRICS_DISCONNECT_TIMEOUT = 0, + LEAN_METRICS_DISCONNECT_REMOTE_CLOSE = 1, + LEAN_METRICS_DISCONNECT_LOCAL_CLOSE = 2, + LEAN_METRICS_DISCONNECT_ERROR = 3, + LEAN_METRICS_DISCONNECT_REASON_COUNT = 4 +} lean_metrics_disconnection_reason_t; + struct lean_metrics_histogram_snapshot { size_t bucket_count; double buckets[LEAN_METRICS_MAX_BUCKETS]; @@ -22,9 +43,15 @@ struct lean_metrics_histogram_snapshot { struct lean_metrics_snapshot { uint64_t attestations_valid_total; uint64_t attestations_invalid_total; + uint64_t fork_choice_reorgs_total; + uint64_t finalizations_success_total; + uint64_t finalizations_error_total; + uint64_t peer_connection_events_total[LEAN_METRICS_DIR_COUNT][LEAN_METRICS_CONN_RESULT_COUNT]; + uint64_t peer_disconnection_events_total[LEAN_METRICS_DIR_COUNT][LEAN_METRICS_DISCONNECT_REASON_COUNT]; uint64_t state_transition_slots_processed_total; uint64_t state_transition_attestations_processed_total; struct lean_metrics_histogram_snapshot fork_choice_block_time; + struct lean_metrics_histogram_snapshot fork_choice_reorg_depth; struct lean_metrics_histogram_snapshot attestation_validation_time; struct lean_metrics_histogram_snapshot state_transition_time; struct lean_metrics_histogram_snapshot state_slots_time; @@ -36,13 +63,21 @@ struct lean_metrics_snapshot { void lean_metrics_reset(void); void lean_metrics_record_fork_choice_block_time(double seconds); +void lean_metrics_record_fork_choice_reorg(size_t depth); void lean_metrics_record_attestation_validation(double seconds, bool valid); void lean_metrics_record_state_transition(double seconds); void lean_metrics_record_state_transition_slots(uint64_t slots_processed, double seconds); void lean_metrics_record_state_transition_block(double seconds); void lean_metrics_record_state_transition_attestations(uint64_t count, double seconds); +void lean_metrics_record_finalization_attempt(bool success); void lean_metrics_record_pq_signature_signing(double seconds); void lean_metrics_record_pq_signature_verification(double seconds); +void lean_metrics_record_peer_connection( + lean_metrics_direction_t direction, + lean_metrics_connection_result_t result); +void lean_metrics_record_peer_disconnection( + lean_metrics_direction_t direction, + lean_metrics_disconnection_reason_t reason); void lean_metrics_snapshot(struct lean_metrics_snapshot *out); #ifdef __cplusplus diff --git a/include/lantern/networking/reqresp_service.h b/include/lantern/networking/reqresp_service.h index 24ff070..eba44f1 100644 --- a/include/lantern/networking/reqresp_service.h +++ b/include/lantern/networking/reqresp_service.h @@ -18,7 +18,7 @@ #define LANTERN_REQRESP_MAX_CHUNK_BYTES (10u * 1024u * 1024u) #define LANTERN_REQRESP_MAX_CONTEXT_BYTES (1u << 20) #define LANTERN_REQRESP_HEADER_MAX_BYTES 10u -#define LANTERN_REQRESP_TTFB_TIMEOUT_MS 5000u +#define LANTERN_REQRESP_TTFB_TIMEOUT_MS 10000u #define LANTERN_REQRESP_RESP_TIMEOUT_MS 10000u #define LANTERN_REQRESP_STALL_TIMEOUT_MS LANTERN_REQRESP_TTFB_TIMEOUT_MS #define LANTERN_REQRESP_RESPONSE_SUCCESS 0u diff --git a/include/lantern/storage/storage.h b/include/lantern/storage/storage.h index 8559bdf..3a8ab3e 100644 --- a/include/lantern/storage/storage.h +++ b/include/lantern/storage/storage.h @@ -2,6 +2,7 @@ #define LANTERN_STORAGE_STORAGE_H #include +#include #include "lantern/consensus/containers.h" #include "lantern/consensus/state.h" @@ -22,6 +23,27 @@ int lantern_storage_load_state(const char *data_dir, LanternState *state); int lantern_storage_save_votes(const char *data_dir, const LanternState *state); int lantern_storage_load_votes(const char *data_dir, LanternState *state); int lantern_storage_store_block(const char *data_dir, const LanternSignedBlock *block); +int lantern_storage_store_state_for_root( + const char *data_dir, + const LanternRoot *root, + const LanternState *state); +int lantern_storage_load_state_bytes_for_root( + const char *data_dir, + const LanternRoot *root, + uint8_t **out_data, + size_t *out_len); +int lantern_storage_store_slot_root( + const char *data_dir, + uint64_t slot, + const LanternRoot *root); +int lantern_storage_store_head_root( + const char *data_dir, + uint64_t slot, + const LanternRoot *root); +int lantern_storage_store_checkpoints( + const char *data_dir, + const LanternCheckpoint *justified, + const LanternCheckpoint *finalized); int lantern_storage_collect_blocks( const char *data_dir, const LanternRoot *roots, diff --git a/include/lantern/support/version.h b/include/lantern/support/version.h new file mode 100644 index 0000000..9574d48 --- /dev/null +++ b/include/lantern/support/version.h @@ -0,0 +1,7 @@ +#ifndef LANTERN_SUPPORT_VERSION_H +#define LANTERN_SUPPORT_VERSION_H + +#define LANTERN_VERSION "v0.0.2" +#define LANTERN_CLIENT_NAME "lantern" + +#endif /* LANTERN_SUPPORT_VERSION_H */ diff --git a/scripts/run_devnet2.sh b/scripts/run_devnet2.sh index 20f4d9d..b2cf013 100755 --- a/scripts/run_devnet2.sh +++ b/scripts/run_devnet2.sh @@ -123,8 +123,6 @@ DOCKER_NETWORK=${DOCKER_NETWORK:-host} DOCKER_BUILD=${DOCKER_BUILD:-1} DOCKER_BUILD_ARGS=${DOCKER_BUILD_ARGS:-} DOCKER_LISTEN_IP=${DOCKER_LISTEN_IP:-} -LANTERN_DEBUG_FINALIZATION=${LANTERN_DEBUG_FINALIZATION:-} -LANTERN_DEBUG_STATE_HASH=${LANTERN_DEBUG_STATE_HASH:-} ENABLE_COREDUMP=${ENABLE_COREDUMP:-0} if [[ -z "${DOCKER_LISTEN_IP}" ]]; then @@ -543,15 +541,8 @@ for i in $(seq 0 $((NODES-1))); do -e "LANTERN_NODES_FILE=/genesis/nodes.yaml" -e "LANTERN_GENESIS_STATE=/genesis/genesis.ssz" -e "LANTERN_VALIDATOR_CONFIG=/genesis/validator-config.yaml" - -e "LANTERN_XMSS_AGG_TEST_MODE=0" - -e "LANTERN_EXTRA_ARGS=--xmss-public-template /genesis/${HASH_SIG_KEYS_DIR_NAME}/validator_%u_pk.ssz --xmss-secret-template /genesis/${HASH_SIG_KEYS_DIR_NAME}/validator_%u_sk.ssz --log-level ${LOG_LEVEL}" + -e "LANTERN_EXTRA_ARGS=--hash-sig-key-dir /genesis/${HASH_SIG_KEYS_DIR_NAME} --log-level ${LOG_LEVEL}" ) - if [[ -n "${LANTERN_DEBUG_FINALIZATION}" ]]; then - docker_args+=(-e "LANTERN_DEBUG_FINALIZATION=${LANTERN_DEBUG_FINALIZATION}") - fi - if [[ -n "${LANTERN_DEBUG_STATE_HASH}" ]]; then - docker_args+=(-e "LANTERN_DEBUG_STATE_HASH=${LANTERN_DEBUG_STATE_HASH}") - fi if [[ "${ENABLE_COREDUMP}" == "1" ]]; then docker_args+=(--ulimit core=-1) fi @@ -576,12 +567,6 @@ for i in $(seq 0 $((NODES-1))); do docker "${docker_args[@]}" >/dev/null echo "${NODE_ID}" >> "${PIDS_FILE}" else - if [[ -n "${LANTERN_DEBUG_FINALIZATION}" ]]; then - export LANTERN_DEBUG_FINALIZATION - fi - if [[ -n "${LANTERN_DEBUG_STATE_HASH}" ]]; then - export LANTERN_DEBUG_STATE_HASH - fi nohup "${BIN}" \ --data-dir "${DATA}" \ --genesis-config "${GENESIS_DIR}/config.yaml" \ @@ -595,8 +580,7 @@ for i in $(seq 0 $((NODES-1))); do --http-port "${HTTP}" \ --metrics-port "${METRICS}" \ --devnet "${DEVNET}" \ - --xmss-public-template "${HASH_SIG_KEYS_DIR}/validator_%u_pk.ssz" \ - --xmss-secret-template "${HASH_SIG_KEYS_DIR}/validator_%u_sk.ssz" \ + --hash-sig-key-dir "${HASH_SIG_KEYS_DIR}" \ --log-level "${LOG_LEVEL}" \ >"${LOG_DIR}/${NODE_ID}.log" 2>&1 & echo $! >> "${PIDS_FILE}" diff --git a/src/consensus/fork_choice.c b/src/consensus/fork_choice.c index dfd117e..690a17e 100644 --- a/src/consensus/fork_choice.c +++ b/src/consensus/fork_choice.c @@ -52,14 +52,7 @@ static uint64_t root_hash(const LanternRoot *root) { } static bool finalization_trace_enabled(void) { - static bool initialized = false; - static bool enabled = false; - if (!initialized) { - const char *env = getenv("LANTERN_DEBUG_FINALIZATION"); - enabled = env && env[0] != '\0'; - initialized = true; - } - return enabled; + return false; } static void format_root_hex(const LanternRoot *root, char *out, size_t out_len) { @@ -473,10 +466,14 @@ static int update_global_checkpoints( if (!store) { return -1; } - if (post_justified && post_justified->slot >= store->latest_justified.slot) { + if (post_justified + && !root_is_zero(&post_justified->root) + && post_justified->slot >= store->latest_justified.slot) { store->latest_justified = *post_justified; } - if (post_finalized && post_finalized->slot >= store->latest_finalized.slot) { + if (post_finalized + && !root_is_zero(&post_finalized->root) + && post_finalized->slot >= store->latest_finalized.slot) { store->latest_finalized = *post_finalized; } return 0; @@ -756,10 +753,60 @@ static int lmd_ghost_compute( } } +static size_t fork_choice_reorg_depth( + const LanternForkChoice *store, + size_t old_index, + size_t new_index) { + if (!store || old_index >= store->block_len || new_index >= store->block_len) { + return 0; + } + size_t depth = 0; + uint64_t old_slot = store->blocks[old_index].slot; + uint64_t new_slot = store->blocks[new_index].slot; + + while (old_index != new_index) { + if (old_slot > new_slot) { + size_t parent = store->blocks[old_index].parent_index; + if (parent == SIZE_MAX || parent >= store->block_len) { + return 0; + } + old_index = parent; + old_slot = store->blocks[old_index].slot; + depth += 1; + continue; + } + if (new_slot > old_slot) { + size_t parent = store->blocks[new_index].parent_index; + if (parent == SIZE_MAX || parent >= store->block_len) { + return 0; + } + new_index = parent; + new_slot = store->blocks[new_index].slot; + continue; + } + + size_t old_parent = store->blocks[old_index].parent_index; + size_t new_parent = store->blocks[new_index].parent_index; + if (old_parent == SIZE_MAX || new_parent == SIZE_MAX + || old_parent >= store->block_len || new_parent >= store->block_len) { + return 0; + } + old_index = old_parent; + new_index = new_parent; + old_slot = store->blocks[old_index].slot; + new_slot = store->blocks[new_index].slot; + depth += 1; + } + + return depth; +} + int lantern_fork_choice_recompute_head(LanternForkChoice *store) { if (!store || !store->initialized || !store->has_anchor) { return -1; } + LanternRoot previous_head = store->head; + bool had_head = store->has_head; LanternRoot head; if (lmd_ghost_compute( store, @@ -774,6 +821,18 @@ int lantern_fork_choice_recompute_head(LanternForkChoice *store) { store->head = head; store->has_head = true; + if (had_head && root_compare(&previous_head, &head) != 0) { + size_t old_index = 0; + size_t new_index = 0; + if (map_lookup(store, &previous_head, &old_index) + && map_lookup(store, &head, &new_index)) { + size_t depth = fork_choice_reorg_depth(store, old_index, new_index); + if (depth > 0) { + lean_metrics_record_fork_choice_reorg(depth); + } + } + } + size_t head_index = 0; if (map_lookup(store, &head, &head_index)) { const struct lantern_fork_choice_block_entry *entry = &store->blocks[head_index]; diff --git a/src/consensus/hash.c b/src/consensus/hash.c index b24e65b..11b0d96 100644 --- a/src/consensus/hash.c +++ b/src/consensus/hash.c @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -10,8 +9,6 @@ #include "ssz_merkle.h" #include "ssz_utils.h" #include "mincrypt/sha256.h" -#include "lantern/support/log.h" -#include "lantern/support/strings.h" /* XMSS signature layout constants (LeanSpec prod config). */ static const size_t LANTERN_XMSS_FP_BYTES = 4u; @@ -894,118 +891,6 @@ int lantern_hash_tree_root_state(const LanternState *state, LanternRoot *out_roo return -1; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - char debug_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex(config_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu config root: %s", (unsigned long long)state->slot, debug_hex); - } - if (lantern_bytes_to_hex(header_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu header root: %s", (unsigned long long)state->slot, debug_hex); - char header_parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char header_state_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char header_body_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - state->latest_block_header.parent_root.bytes, - LANTERN_ROOT_SIZE, - header_parent_hex, - sizeof(header_parent_hex), - 1) - == 0 - && lantern_bytes_to_hex( - state->latest_block_header.state_root.bytes, - LANTERN_ROOT_SIZE, - header_state_hex, - sizeof(header_state_hex), - 1) - == 0 - && lantern_bytes_to_hex( - state->latest_block_header.body_root.bytes, - LANTERN_ROOT_SIZE, - header_body_hex, - sizeof(header_body_hex), - 1) - == 0) { - lantern_log_debug( - "hash", - NULL, - "header fields parent=%s state=%s body=%s", - header_parent_hex, - header_state_hex, - header_body_hex); - } - } - if (lantern_bytes_to_hex(justified_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu justified root: %s", (unsigned long long)state->slot, debug_hex); - } - if (lantern_bytes_to_hex(finalized_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu finalized root: %s", (unsigned long long)state->slot, debug_hex); - } - if (lantern_bytes_to_hex(historical_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu historical root: %s", (unsigned long long)state->slot, debug_hex); - } - if (lantern_bytes_to_hex(justified_slots_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu justified slots root: %s", (unsigned long long)state->slot, debug_hex); - } - if (lantern_bytes_to_hex(justification_roots_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu justification roots root: %s", (unsigned long long)state->slot, debug_hex); - } - if (lantern_bytes_to_hex(validators_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug("hash", NULL, "hash state slot %llu validators root: %s", (unsigned long long)state->slot, debug_hex); - } - if ( - lantern_bytes_to_hex(justification_validators_root.bytes, LANTERN_ROOT_SIZE, debug_hex, sizeof(debug_hex), 1) == 0) { - lantern_log_debug( - "hash", - NULL, - "hash state slot %llu justification validators root: %s", - (unsigned long long)state->slot, - debug_hex); - } - lantern_log_debug("hash", NULL, "historical_block_hashes len=%zu", state->historical_block_hashes.length); - size_t hist_limit = state->historical_block_hashes.length < 4 ? state->historical_block_hashes.length : 4; - for (size_t i = 0; i < hist_limit; ++i) { - char hist_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - state->historical_block_hashes.items[i].bytes, - LANTERN_ROOT_SIZE, - hist_hex, - sizeof(hist_hex), - 1) - == 0) { - lantern_log_debug("hash", NULL, " historical[%zu]=%s", i, hist_hex); - } - } - lantern_log_debug("hash", NULL, "justification_roots len=%zu", state->justification_roots.length); - size_t just_limit = state->justification_roots.length < 4 ? state->justification_roots.length : 4; - for (size_t i = 0; i < just_limit; ++i) { - char just_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - state->justification_roots.items[i].bytes, - LANTERN_ROOT_SIZE, - just_hex, - sizeof(just_hex), - 1) - == 0) { - lantern_log_debug("hash", NULL, " justification_roots[%zu]=%s", i, just_hex); - } - } - lantern_log_debug("hash", NULL, "justified_slots bits=%zu", state->justified_slots.bit_length); - size_t bit_limit = state->justified_slots.bit_length < 16 ? state->justified_slots.bit_length : 16; - for (size_t bit = 0; bit < bit_limit; ++bit) { - if (bitlist_bit_is_set(&state->justified_slots, bit)) { - lantern_log_debug("hash", NULL, " justified_slots bit %zu = 1", bit); - } - } - lantern_log_debug("hash", NULL, "justification_validators bits=%zu", state->justification_validators.bit_length); - bit_limit = state->justification_validators.bit_length < 16 ? state->justification_validators.bit_length : 16; - for (size_t bit = 0; bit < bit_limit; ++bit) { - if (bitlist_bit_is_set(&state->justification_validators, bit)) { - lantern_log_debug("hash", NULL, " justification_validators bit %zu = 1", bit); - } - } - } - uint8_t chunks[10][SSZ_BYTES_PER_CHUNK]; memcpy(chunks[0], config_root.bytes, SSZ_BYTES_PER_CHUNK); chunk_from_uint64(state->slot, chunks[1]); @@ -1036,7 +921,6 @@ int lantern_hash_tree_root_validators(const uint8_t *pubkeys, size_t count, Lant if (!chunks) { return -1; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); for (size_t i = 0; i < count; ++i) { LanternRoot validator_root; /* Pass index i to match Zeam's Validator { pubkey, index } SSZ structure */ @@ -1044,18 +928,6 @@ int lantern_hash_tree_root_validators(const uint8_t *pubkeys, size_t count, Lant free(chunks); return -1; } - if (debug_hash && debug_hash[0] != '\0' && i == 0) { - char validator_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - validator_root.bytes, - LANTERN_ROOT_SIZE, - validator_hex, - sizeof(validator_hex), - 1) - == 0) { - lantern_log_debug("hash", NULL, "validator[0] root: %s", validator_hex); - } - } memcpy(chunks + (i * SSZ_BYTES_PER_CHUNK), validator_root.bytes, SSZ_BYTES_PER_CHUNK); } } diff --git a/src/consensus/signature.c b/src/consensus/signature.c index cf0d3d0..db4dbf8 100644 --- a/src/consensus/signature.c +++ b/src/consensus/signature.c @@ -30,21 +30,6 @@ static bool bytes_are_zero(const uint8_t *bytes, size_t length) { return true; } -static bool xmss_aggregation_test_mode(void) { - static bool initialized = false; - static bool enabled = true; - if (!initialized) { - const char *env = getenv("LANTERN_XMSS_AGG_TEST_MODE"); - if (env && env[0] != '\0') { - enabled = (env[0] != '0'); - } else { - enabled = true; - } - initialized = true; - } - return enabled; -} - bool lantern_signature_is_zero(const LanternSignature *signature) { if (!signature) { return false; @@ -191,10 +176,9 @@ bool lantern_signature_aggregate( lantern_log_info( "signature", NULL, - "aggregation start count=%zu epoch=%" PRIu64 " test_mode=%d", + "aggregation start count=%zu epoch=%" PRIu64, count, - epoch, - xmss_aggregation_test_mode() ? 1 : 0); + epoch); double start = get_time_seconds(); if (lantern_byte_list_resize(out_proof, LANTERN_AGG_PROOF_MAX_BYTES) != 0) { @@ -251,13 +235,12 @@ bool lantern_signature_aggregate( pq_xmss_aggregation_setup_prover(); uintptr_t written_len = 0; enum PQSigningError err = pq_aggregate_signatures( - pubkey_handles, + (const struct PQSignatureSchemePublicKey *const *)pubkey_handles, (const struct PQSignature *const *)sig_handles, count, message, message_len, epoch, - xmss_aggregation_test_mode(), out_proof->data, out_proof->length, &written_len); @@ -335,11 +318,10 @@ bool lantern_signature_verify_aggregated( lantern_log_info( "signature", NULL, - "aggregation verify start count=%zu epoch=%" PRIu64 " proof_len=%zu test_mode=%d", + "aggregation verify start count=%zu epoch=%" PRIu64 " proof_len=%zu", count, epoch, - proof->length, - xmss_aggregation_test_mode() ? 1 : 0); + proof->length); struct PQSignatureSchemePublicKey **pubkey_handles = calloc(count, sizeof(*pubkey_handles)); if (!pubkey_handles) { @@ -379,8 +361,7 @@ bool lantern_signature_verify_aggregated( message_len, proof->data, proof->length, - epoch, - xmss_aggregation_test_mode()); + epoch); double elapsed = get_time_seconds() - start; lantern_log_debug( "signature", diff --git a/src/consensus/ssz.c b/src/consensus/ssz.c index 5cc3680..198c14e 100644 --- a/src/consensus/ssz.c +++ b/src/consensus/ssz.c @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -11,7 +10,6 @@ #include "ssz_serialize.h" #include "lantern/consensus/state.h" -#include "lantern/support/log.h" static int write_u32(uint8_t *out, size_t remaining, uint32_t value) { if (!out || remaining < SSZ_BYTE_SIZE_OF_UINT32) { @@ -1526,9 +1524,6 @@ int lantern_ssz_encode_state(const LanternState *state, uint8_t *out, size_t out return -1; } const size_t var_field_count = 5; - const char *debug_env = getenv("LANTERN_DEBUG_SSZ"); - bool debug_ssz = debug_env && debug_env[0] != '\0'; - int debug_stage = 0; size_t offset = 0; size_t tmp = 0; @@ -1646,9 +1641,6 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da } const size_t var_field_count = 5; - const char *debug_env = getenv("LANTERN_DEBUG_SSZ"); - bool debug_ssz = debug_env && debug_env[0] != '\0'; - int debug_stage = 0; size_t offset = 0; const size_t offsets_size = var_field_count * SSZ_BYTE_SIZE_OF_UINT32; const size_t min_full_size = LANTERN_CONFIG_SSZ_SIZE + SSZ_BYTE_SIZE_OF_UINT64 + LANTERN_BLOCK_HEADER_SSZ_SIZE @@ -1656,18 +1648,10 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da if (data_len < min_full_size) { return -1; } - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: entry data_len=%zu", data_len); - } - if (lantern_ssz_decode_config(&state->config, data + offset, LANTERN_CONFIG_SSZ_SIZE) != 0) { return -1; } offset += LANTERN_CONFIG_SSZ_SIZE; - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: stage %d config decoded", debug_stage++); - } - if (read_u64(data + offset, data_len - offset, &state->slot) != 0) { return -1; } @@ -1680,11 +1664,6 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da return -1; } offset += LANTERN_BLOCK_HEADER_SSZ_SIZE; - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: stage %d slot=%" PRIu64, debug_stage++, state->slot); - lantern_log_debug("ssz", NULL, "ssz decode state: stage %d header slot=%" PRIu64, debug_stage++, state->latest_block_header.slot); - } - if (data_len - offset < LANTERN_CHECKPOINT_SSZ_SIZE) { return -1; } @@ -1692,15 +1671,6 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da return -1; } offset += LANTERN_CHECKPOINT_SSZ_SIZE; - if (debug_ssz) { - lantern_log_debug( - "ssz", - NULL, - "ssz decode state: stage %d latest_justified slot=%" PRIu64, - debug_stage++, - state->latest_justified.slot); - } - if (data_len - offset < LANTERN_CHECKPOINT_SSZ_SIZE) { return -1; } @@ -1708,15 +1678,6 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da return -1; } offset += LANTERN_CHECKPOINT_SSZ_SIZE; - if (debug_ssz) { - lantern_log_debug( - "ssz", - NULL, - "ssz decode state: stage %d latest_finalized slot=%" PRIu64, - debug_stage++, - state->latest_finalized.slot); - } - if (data_len - offset < offsets_size) { return -1; } @@ -1742,10 +1703,6 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da } } offset = table_end; - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: stage %d offsets parsed (table_end=%zu)", debug_stage++, offset); - } - size_t payload_start = offsets[0]; if (payload_start < offset || payload_start > data_len) { return -1; @@ -1769,53 +1726,19 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da chunk_sizes[i] = chunk_end - offsets[i]; } - if (debug_ssz) { - lantern_log_debug( - "ssz", - NULL, - "ssz decode state: offsets_start=%zu table_end=%zu data_len=%zu", - offsets_start, - offset, - data_len); - lantern_log_debug( - "ssz", - NULL, - "ssz decode state: chunks hist=%zu slots=%zu validators=%zu roots=%zu just_validators=%zu", - chunk_sizes[0], - chunk_sizes[1], - chunk_sizes[2], - chunk_sizes[3], - chunk_sizes[4]); - } - if (decode_root_list(&state->historical_block_hashes, data + offsets[0], chunk_sizes[0]) != 0) { - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: failed historical_block_hashes len=%zu", chunk_sizes[0]); - } return -1; } if (decode_bitlist(&state->justified_slots, data + offsets[1], chunk_sizes[1]) != 0) { - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: failed justified_slots len=%zu", chunk_sizes[1]); - } return -1; } if (decode_validators_list(state, data + offsets[2], chunk_sizes[2]) != 0) { - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: failed validators len=%zu", chunk_sizes[2]); - } return -1; } if (decode_root_list(&state->justification_roots, data + offsets[3], chunk_sizes[3]) != 0) { - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: failed justification_roots len=%zu", chunk_sizes[3]); - } return -1; } if (decode_bitlist(&state->justification_validators, data + offsets[4], chunk_sizes[4]) != 0) { - if (debug_ssz) { - lantern_log_debug("ssz", NULL, "ssz decode state: failed justification_validators len=%zu", chunk_sizes[4]); - } return -1; } diff --git a/src/consensus/state.c b/src/consensus/state.c index 36c962c..60312a2 100644 --- a/src/consensus/state.c +++ b/src/consensus/state.c @@ -74,14 +74,7 @@ static void format_root_hex(const LanternRoot *root, char *out, size_t out_len) } static bool finalization_trace_enabled(void) { - static bool initialized = false; - static bool enabled = false; - if (!initialized) { - const char *env = getenv("LANTERN_DEBUG_FINALIZATION"); - enabled = env && env[0] != '\0'; - initialized = true; - } - return enabled; + return false; } static bool lantern_checkpoint_equal(const LanternCheckpoint *a, const LanternCheckpoint *b); @@ -1024,6 +1017,91 @@ static int lantern_state_remove_justification_root( return 0; } +static int lantern_state_find_latest_slot_for_root( + const LanternState *state, + const LanternRoot *root, + uint64_t start_slot, + uint64_t *out_slot) { + if (!state || !root || !out_slot) { + return -1; + } + size_t length = state->historical_block_hashes.length; + if (length == 0) { + return 1; + } + uint64_t offset = state->historical_roots_offset; + uint64_t end_slot = offset + (uint64_t)length - 1u; + if (start_slot > end_slot) { + return 1; + } + size_t start_index = 0; + if (start_slot > offset) { + uint64_t diff = start_slot - offset; + if (diff > SIZE_MAX) { + return -1; + } + if ((size_t)diff >= length) { + return 1; + } + start_index = (size_t)diff; + } + for (size_t i = length; i-- > start_index;) { + if (memcmp(state->historical_block_hashes.items[i].bytes, root->bytes, LANTERN_ROOT_SIZE) == 0) { + *out_slot = offset + (uint64_t)i; + return 0; + } + } + return 1; +} + +static int lantern_state_prune_justification_roots( + LanternState *state, + uint64_t base_finalized_slot, + uint64_t finalized_slot, + size_t validator_count, + const struct lantern_log_metadata *meta) { + if (!state || validator_count == 0) { + return -1; + } + if (state->justification_roots.length == 0) { + return 0; + } + if (base_finalized_slot == UINT64_MAX) { + return -1; + } + uint64_t start_slot = base_finalized_slot + 1u; + for (size_t i = state->justification_roots.length; i-- > 0;) { + uint64_t latest_slot = 0; + int find_rc = lantern_state_find_latest_slot_for_root( + state, + &state->justification_roots.items[i], + start_slot, + &latest_slot); + if (find_rc != 0) { + if (meta) { + lantern_log_warn( + "state", + meta, + "justification root missing from history during pruning"); + } + return -1; + } + if (latest_slot <= finalized_slot) { + if (lantern_state_remove_justification_root(state, (int)i, validator_count) != 0) { + if (meta) { + lantern_log_warn( + "state", + meta, + "failed to prune justification root at slot %" PRIu64, + latest_slot); + } + return -1; + } + } + } + return 0; +} + int lantern_state_prepare_validator_votes(LanternState *state, uint64_t validator_count) { if (!state || validator_count == 0) { return -1; @@ -1304,17 +1382,6 @@ int lantern_state_process_slot(LanternState *state) { if (lantern_hash_tree_root_state(state, &computed) != 0) { return -1; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - char computed_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex(computed.bytes, LANTERN_ROOT_SIZE, computed_hex, sizeof(computed_hex), 1) == 0) { - lantern_log_debug( - "state", - &(const struct lantern_log_metadata){.has_slot = true, .slot = state->slot}, - "cached header state root=%s", - computed_hex); - } - } state->latest_block_header.state_root = computed; } return 0; @@ -1368,10 +1435,6 @@ int lantern_state_mark_justified_slot(LanternState *state, uint64_t slot) { if (slot > SIZE_MAX) { return -1; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - lantern_log_debug("state", NULL, "mark justified slot %" PRIu64, slot); - } int rc = lantern_state_set_justified_slot_bit(state, slot, true); if (rc == 0 && finalization_trace_enabled()) { lantern_log_debug( @@ -1515,9 +1578,7 @@ static int lantern_state_process_attestations_internal( return -1; } size_t validator_count = (size_t)validator_count_u64; - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - const char *debug_finalization = getenv("LANTERN_DEBUG_FINALIZATION"); - bool trace_finalization = debug_finalization && debug_finalization[0] != '\0'; + bool trace_finalization = finalization_trace_enabled(); const struct lantern_log_metadata meta = { .has_slot = true, .slot = state->slot, @@ -1528,11 +1589,22 @@ static int lantern_state_process_attestations_internal( if (attestations->length > LANTERN_MAX_ATTESTATIONS) { return -1; } + for (size_t i = 0; i < state->justification_roots.length; ++i) { + if (lantern_root_is_zero(&state->justification_roots.items[i])) { + lantern_log_warn( + "state", + &meta, + "zero hash is not allowed in justification roots"); + return -1; + } + } LanternCheckpoint latest_justified = state->latest_justified; LanternCheckpoint latest_finalized = state->latest_finalized; + uint64_t base_finalized_slot = latest_finalized.slot; double att_batch_start = lantern_time_now_seconds(); size_t att_attempted = 0; + bool finalization_attempted = false; for (size_t i = 0; i < attestations->length; ++i) { const LanternVote *vote = &attestations->data[i]; @@ -1604,6 +1676,18 @@ static int lantern_state_process_attestations_internal( continue; } + if (lantern_root_is_zero(&vote->source.root) || lantern_root_is_zero(&vote->target.root)) { + if (trace_finalization) { + lantern_log_debug( + "state", + &meta, + "finalization trace skip zero_hash_vote source_slot=%" PRIu64 " target_slot=%" PRIu64, + vote->source.slot, + vote->target.slot); + } + continue; + } + /* LeanSpec: skip if either source or target root mismatches history (state.py:398-402). */ bool source_matches = false; size_t source_slot_idx = (size_t)vote->source.slot; @@ -1660,16 +1744,6 @@ static int lantern_state_process_attestations_internal( continue; } } - if (debug_hash && debug_hash[0] != '\0') { - lantern_log_debug( - "state", - NULL, - "process attestation validator=%" PRIu64 " source=%" PRIu64 " target=%" PRIu64, - vote->validator_id, - vote->source.slot, - vote->target.slot); - } - LanternSignedVote stored_vote; memset(&stored_vote, 0, sizeof(stored_vote)); stored_vote.data = *vote; @@ -1780,15 +1854,6 @@ static int lantern_state_process_attestations_internal( latest_justified = vote->target; - if (debug_hash && debug_hash[0] != '\0') { - lantern_log_debug( - "state", - NULL, - "marked slot %" PRIu64 " justified (votes=%zu, quorum=%zu)", - vote->target.slot, - vote_count, - quorum); - } if (trace_finalization) { lantern_log_debug( "state", @@ -1820,19 +1885,6 @@ static int lantern_state_process_attestations_internal( vote->source.slot, vote->target.slot, latest_finalized.slot); bool vote_has_consecutive_source = !has_justifiable_between; - if (debug_hash && debug_hash[0] != '\0') { - lantern_log_debug( - "state", - &meta, - "finalization check: source=%" PRIu64 " target=%" PRIu64 " cur_finalized=%" PRIu64 - " has_justifiable_between=%s vote_consecutive=%s", - vote->source.slot, - vote->target.slot, - latest_finalized.slot, - has_justifiable_between ? "true" : "false", - vote_has_consecutive_source ? "true" : "false"); - } - if (trace_finalization) { lantern_log_debug( "state", @@ -1848,23 +1900,23 @@ static int lantern_state_process_attestations_internal( if (vote_has_consecutive_source) { /* Finalize the source checkpoint */ + uint64_t old_finalized_slot = latest_finalized.slot; latest_finalized = vote->source; - if (debug_hash && debug_hash[0] != '\0') { - char source_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if ( - lantern_bytes_to_hex( - vote->source.root.bytes, - LANTERN_ROOT_SIZE, - source_hex, - sizeof(source_hex), - 1) - == 0) { - lantern_log_debug( - "state", - &meta, - "finalized slot=%" PRIu64 " root=%s", - vote->source.slot, - source_hex); + finalization_attempted = true; + lean_metrics_record_finalization_attempt(true); + if (latest_finalized.slot > old_finalized_slot) { + if (lantern_state_prune_justification_roots( + state, + base_finalized_slot, + latest_finalized.slot, + validator_count, + &meta) + != 0) { + if (finalization_attempted) { + lean_metrics_record_finalization_attempt(false); + } + record_attestation_validation_metric(att_validation_start, false); + return -1; } } if (trace_finalization) { @@ -1883,9 +1935,15 @@ static int lantern_state_process_attestations_internal( if (apply_consensus_effects) { if (lantern_state_mark_justified_slot(state, latest_justified.slot) != 0) { + if (finalization_attempted) { + lean_metrics_record_finalization_attempt(false); + } return -1; } if (lantern_state_mark_justified_slot(state, latest_finalized.slot) != 0) { + if (finalization_attempted) { + lean_metrics_record_finalization_attempt(false); + } return -1; } @@ -1905,6 +1963,9 @@ static int lantern_state_process_attestations_internal( &state->latest_justified, &state->latest_finalized) != 0) { + if (finalization_attempted) { + lean_metrics_record_finalization_attempt(false); + } return -1; } } @@ -2048,37 +2109,6 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign if (profiling) { state_profile_record(&g_profile_state_root, state_profile_now() - hash_start); } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (hashed_state && debug_hash && debug_hash[0] != '\0') { - char expected_hex_dbg[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char computed_hex_dbg[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - block->state_root.bytes, - LANTERN_ROOT_SIZE, - expected_hex_dbg, - sizeof(expected_hex_dbg), - 1) - != 0) { - expected_hex_dbg[0] = '\0'; - } - if (lantern_bytes_to_hex( - computed_state_root.bytes, - LANTERN_ROOT_SIZE, - computed_hex_dbg, - sizeof(computed_hex_dbg), - 1) - != 0) { - computed_hex_dbg[0] = '\0'; - } - lantern_log_debug( - "state", - NULL, - "state slot %" PRIu64 " expected=%s computed=%s", - block->slot, - expected_hex_dbg[0] ? expected_hex_dbg : "0x0", - computed_hex_dbg[0] ? computed_hex_dbg : "0x0"); - } - if (hashed_state) { if (memcmp(block->state_root.bytes, computed_state_root.bytes, LANTERN_ROOT_SIZE) != 0) { char expected_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; @@ -2107,6 +2137,53 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign "state root mismatch: expected=%s computed=%s", expected_hex[0] ? expected_hex : "0x0", computed_hex[0] ? computed_hex : "0x0"); + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + finalized_hex[0] = '\0'; + justified_hex[0] = '\0'; + if (lantern_bytes_to_hex( + state->latest_finalized.root.bytes, + LANTERN_ROOT_SIZE, + finalized_hex, + sizeof(finalized_hex), + 1) + != 0) { + finalized_hex[0] = '\0'; + } + if (lantern_bytes_to_hex( + state->latest_justified.root.bytes, + LANTERN_ROOT_SIZE, + justified_hex, + sizeof(justified_hex), + 1) + != 0) { + justified_hex[0] = '\0'; + } + lantern_log_warn( + "state", + &(const struct lantern_log_metadata){.has_slot = true, .slot = block->slot}, + "state root context state_slot=%" PRIu64 " header_slot=%" PRIu64 + " finalized_slot=%" PRIu64 " justified_offset=%" PRIu64 + " justified_bits=%zu hist_offset=%" PRIu64 " hist_len=%zu" + " just_roots=%zu just_votes=%zu", + state->slot, + state->latest_block_header.slot, + state->latest_finalized.slot, + state->justified_slots_offset, + state->justified_slots.bit_length, + state->historical_roots_offset, + state->historical_block_hashes.length, + state->justification_roots.length, + state->justification_validators.bit_length); + lantern_log_warn( + "state", + &(const struct lantern_log_metadata){.has_slot = true, .slot = block->slot}, + "state root checkpoints finalized_slot=%" PRIu64 " finalized_root=%s" + " justified_slot=%" PRIu64 " justified_root=%s", + state->latest_finalized.slot, + finalized_hex[0] ? finalized_hex : "0x0", + state->latest_justified.slot, + justified_hex[0] ? justified_hex : "0x0"); STATE_FAIL("state root mismatch for slot %" PRIu64, block->slot); } } else { @@ -2368,10 +2445,19 @@ int lantern_state_compute_vote_checkpoints( if (lantern_fork_choice_block_info(store, &head_root, &head_slot, NULL, NULL) != 0) { return -1; } - + const LanternCheckpoint *store_justified = lantern_fork_choice_latest_justified(store); + const LanternCheckpoint *store_finalized = lantern_fork_choice_latest_finalized(store); + LanternCheckpoint source_checkpoint = state->latest_justified; + LanternCheckpoint finalized_checkpoint = state->latest_finalized; + if (store_justified && !lantern_root_is_zero(&store_justified->root)) { + source_checkpoint = *store_justified; + } + if (store_finalized && !lantern_root_is_zero(&store_finalized->root)) { + finalized_checkpoint = *store_finalized; + } LanternRoot target_root = head_root; uint64_t target_slot = head_slot; - uint64_t source_slot = state->latest_justified.slot; + uint64_t source_slot = source_checkpoint.slot; bool candidate_valid = false; LanternRoot candidate_root; uint64_t candidate_slot = 0; @@ -2384,7 +2470,7 @@ int lantern_state_compute_vote_checkpoints( head_slot, head_hex[0] ? head_hex : "0x0"); } - if (head_slot > source_slot && lantern_slot_is_justifiable(head_slot, state->latest_finalized.slot)) { + if (head_slot > source_slot && lantern_slot_is_justifiable(head_slot, finalized_checkpoint.slot)) { candidate_valid = true; candidate_root = head_root; candidate_slot = head_slot; @@ -2439,7 +2525,7 @@ int lantern_state_compute_vote_checkpoints( } target_root = parent_root; target_slot = parent_slot; - if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, state->latest_finalized.slot)) { + if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { candidate_valid = true; candidate_root = target_root; candidate_slot = target_slot; @@ -2448,7 +2534,7 @@ int lantern_state_compute_vote_checkpoints( } bool justifiable_slot_found = true; - while (!lantern_slot_is_justifiable(target_slot, state->latest_finalized.slot)) { + while (!lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { LanternRoot parent_root; bool has_parent = false; if (lantern_fork_choice_block_info(store, &target_root, &target_slot, &parent_root, &has_parent) != 0) { @@ -2462,7 +2548,7 @@ int lantern_state_compute_vote_checkpoints( if (lantern_fork_choice_block_info(store, &parent_root, &parent_slot, NULL, NULL) != 0) { return -1; } - if (parent_slot < state->latest_finalized.slot) { + if (parent_slot < finalized_checkpoint.slot) { justifiable_slot_found = false; if (trace_finalization) { format_root_hex(&target_root, target_hex, sizeof(target_hex)); @@ -2494,7 +2580,7 @@ int lantern_state_compute_vote_checkpoints( } target_root = parent_root; target_slot = parent_slot; - if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, state->latest_finalized.slot)) { + if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { candidate_valid = true; candidate_root = target_root; candidate_slot = target_slot; @@ -2506,10 +2592,10 @@ int lantern_state_compute_vote_checkpoints( &trace_meta, "finalization trace checkpoints justifiable_slot_unreachable finalized=%" PRIu64 " current=%" PRIu64, - state->latest_finalized.slot, + finalized_checkpoint.slot, target_slot); } - if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, state->latest_finalized.slot)) { + if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { candidate_valid = true; candidate_root = target_root; candidate_slot = target_slot; @@ -2538,7 +2624,7 @@ int lantern_state_compute_vote_checkpoints( out_head->slot = head_slot; out_target->root = target_root; out_target->slot = target_slot; - *out_source = state->latest_justified; + *out_source = source_checkpoint; if (trace_finalization) { lantern_log_debug( "state", diff --git a/src/core/client.c b/src/core/client.c index dcbba93..0904736 100644 --- a/src/core/client.c +++ b/src/core/client.c @@ -527,7 +527,10 @@ static void client_reset_base(struct lantern_client *client) lantern_string_list_init(&client->bootnodes); lantern_string_list_init(&client->dialer_peers); lantern_string_list_init(&client->connected_peer_ids); + lantern_string_list_init(&client->inbound_peer_ids); lantern_string_list_init(&client->status_failure_peer_ids); + double now_seconds = lantern_time_now_seconds(); + client->start_time_seconds = now_seconds > 0.0 ? (uint64_t)now_seconds : 0u; lantern_genesis_artifacts_init(&client->genesis); lantern_enr_record_init(&client->local_enr); lantern_libp2p_host_init(&client->network); @@ -556,14 +559,14 @@ static void client_reset_base(struct lantern_client *client) client->ping_stop_flag = 1; pending_block_list_init(&client->pending_blocks); client->pending_lock_initialized = false; + client->sync_state = LANTERN_SYNC_STATE_IDLE; } /** * @brief Apply user-provided options to the client instance. * - * Copies configurable strings and ports into the client, and respects the - * optional environment override for disabling the status guard. + * Copies configurable strings and ports into the client. * * @param client Client being configured * @param options Source options (must not be NULL) @@ -595,19 +598,6 @@ static lantern_client_error client_apply_options( return LANTERN_CLIENT_ERR_ALLOC; } - const char *disable_guard_env = getenv("LANTERN_DEBUG_DISABLE_STATUS_GUARD"); - if (disable_guard_env - && disable_guard_env[0] != '\0' - && !(disable_guard_env[0] == '0' && disable_guard_env[1] == '\0')) - { - client->status_guard_disabled = true; - lantern_log_warn( - "reqresp", - &(const struct lantern_log_metadata){.validator = client->node_id}, - "status guard disabled via LANTERN_DEBUG_DISABLE_STATUS_GUARD=\"%s\"", - disable_guard_env); - } - client->http_port = options->http_port; client->metrics_port = options->metrics_port; return LANTERN_CLIENT_OK; @@ -1735,6 +1725,7 @@ static void shutdown_network_services(struct lantern_client *client) client->connected_peers = 0; } lantern_string_list_reset(&client->connected_peer_ids); + lantern_string_list_reset(&client->inbound_peer_ids); } @@ -1995,14 +1986,6 @@ static void shutdown_state_and_runtime(struct lantern_client *client) */ static lantern_client_error client_start_apis(struct lantern_client *client) { - if (client->http_port != 0) - { - lantern_log_warn( - "client", - &(const struct lantern_log_metadata){.validator = client->node_id}, - "HTTP API disabled; ignoring --http-port %" PRIu16, - client->http_port); - } client->http_running = false; struct lantern_metrics_callbacks metrics_callbacks; @@ -2027,6 +2010,30 @@ static lantern_client_error client_start_apis(struct lantern_client *client) client->metrics_running = true; } + struct lantern_http_server_config http_config; + memset(&http_config, 0, sizeof(http_config)); + http_config.port = client->http_port; + http_config.callbacks.context = client; + http_config.callbacks.snapshot_head = http_snapshot_head; + http_config.callbacks.validator_count = http_validator_count_cb; + http_config.callbacks.validator_info = http_validator_info_cb; + http_config.callbacks.set_validator_status = http_set_validator_status_cb; + http_config.callbacks.metrics_snapshot = metrics_snapshot_cb; + http_config.callbacks.finalized_state_ssz = http_finalized_state_ssz_cb; + if (client->http_port != 0) + { + if (lantern_http_server_start(&client->http_server, &http_config) != 0) + { + lantern_log_error( + "client", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "failed to start HTTP server on port %" PRIu16, + client->http_port); + return LANTERN_CLIENT_ERR_NETWORK; + } + client->http_running = true; + } + return LANTERN_CLIENT_OK; } diff --git a/src/core/client_debug.c b/src/core/client_debug.c index 736528f..7392ef4 100644 --- a/src/core/client_debug.c +++ b/src/core/client_debug.c @@ -81,6 +81,7 @@ int lantern_client_debug_import_block( &(const struct lantern_log_metadata){ .validator = client->node_id, .peer = peer_id_text}, + 0, true) ? 1 : 0; @@ -143,6 +144,7 @@ int lantern_client_debug_enqueue_pending_block( block_root, parent_root, peer_id_text, + 0, false); return LANTERN_CLIENT_OK; } @@ -309,7 +311,7 @@ void lantern_client_debug_disable_block_requests(struct lantern_client *client, * @param client Client instance * @param peer_id Peer ID text * @param request_root Root that was requested - * @param outcome_code Outcome code (LANTERN_DEBUG_BLOCKS_REQUEST_*) + * @param outcome_code Outcome code (LANTERN_TEST_BLOCKS_REQUEST_*) * @return 0 on success * @return LANTERN_CLIENT_ERR_INVALID_PARAM on invalid inputs * @@ -328,15 +330,15 @@ int lantern_client_debug_on_blocks_request_complete( enum lantern_blocks_request_outcome outcome; switch (outcome_code) { - case LANTERN_DEBUG_BLOCKS_REQUEST_SUCCESS: + case LANTERN_TEST_BLOCKS_REQUEST_SUCCESS: outcome = LANTERN_BLOCKS_REQUEST_SUCCESS; break; - case LANTERN_DEBUG_BLOCKS_REQUEST_FAILED: + case LANTERN_TEST_BLOCKS_REQUEST_FAILED: outcome = LANTERN_BLOCKS_REQUEST_FAILED; break; - case LANTERN_DEBUG_BLOCKS_REQUEST_ABORTED: + case LANTERN_TEST_BLOCKS_REQUEST_ABORTED: outcome = LANTERN_BLOCKS_REQUEST_ABORTED; break; diff --git a/src/core/client_http.c b/src/core/client_http.c index 9182fbe..942df81 100644 --- a/src/core/client_http.c +++ b/src/core/client_http.c @@ -25,20 +25,10 @@ #include "lantern/consensus/hash.h" #include "lantern/http/server.h" #include "lantern/metrics/lean_metrics.h" +#include "lantern/storage/storage.h" #include "lantern/support/log.h" -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. */ @@ -77,8 +67,8 @@ static void unlock_mutex_with_log( * @param out_index Output for local index * * @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 + * @return LANTERN_HTTP_CB_ERR_INVALID_PARAM if client or out_index is NULL + * @return LANTERN_HTTP_CB_ERR_NOT_FOUND if validator is not found * * @note Thread safety: This function is thread-safe */ @@ -89,7 +79,7 @@ int find_local_validator_index( { if (!client || !out_index) { - return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; } for (size_t i = 0; i < client->local_validator_count; ++i) { @@ -97,10 +87,10 @@ int find_local_validator_index( && client->local_validators[i].global_index == global_index) { *out_index = i; - return LANTERN_CLIENT_HTTP_OK; + return LANTERN_HTTP_CB_OK; } } - return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND; + return LANTERN_HTTP_CB_ERR_NOT_FOUND; } @@ -115,10 +105,10 @@ int find_local_validator_index( * @param out_snapshot Output snapshot structure * * @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 + * @return LANTERN_HTTP_CB_ERR_INVALID_PARAM if context or out_snapshot is NULL + * @return LANTERN_HTTP_CB_ERR_INVALID_STATE if client has no state + * @return LANTERN_HTTP_CB_ERR_LOCK_FAILED if state_lock is initialized but cannot be acquired + * @return LANTERN_HTTP_CB_ERR_HASH_FAILED if head root cannot be computed * * @note Thread safety: This function may acquire state_lock */ @@ -126,7 +116,7 @@ int http_snapshot_head(void *context, struct lantern_http_head_snapshot *out_sna { if (!context || !out_snapshot) { - return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; } struct lantern_client *client = context; memset(out_snapshot, 0, sizeof(*out_snapshot)); @@ -135,18 +125,27 @@ int http_snapshot_head(void *context, struct lantern_http_head_snapshot *out_sna bool state_locked = lantern_client_lock_state(client); if (expect_state_lock && !state_locked) { - return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + return LANTERN_HTTP_CB_ERR_LOCK_FAILED; } if (!client->has_state) { lantern_client_unlock_state(client, state_locked); - return LANTERN_CLIENT_HTTP_ERR_INVALID_STATE; + return LANTERN_HTTP_CB_ERR_INVALID_STATE; } LanternBlockHeader head_header = client->state.latest_block_header; LanternCheckpoint justified = client->state.latest_justified; LanternCheckpoint finalized = client->state.latest_finalized; + if (client->has_fork_choice) + { + const LanternCheckpoint *fork_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_finalized && !lantern_root_is_zero(&fork_finalized->root)) + { + finalized = *fork_finalized; + } + } lantern_client_unlock_state(client, state_locked); out_snapshot->slot = head_header.slot; @@ -154,10 +153,10 @@ int http_snapshot_head(void *context, struct lantern_http_head_snapshot *out_sna out_snapshot->finalized = finalized; if (lantern_hash_tree_root_block_header(&head_header, &out_snapshot->head_root) != 0) { - return LANTERN_CLIENT_HTTP_ERR_HASH_FAILED; + return LANTERN_HTTP_CB_ERR_HASH_FAILED; } - return LANTERN_CLIENT_HTTP_OK; + return LANTERN_HTTP_CB_OK; } @@ -192,10 +191,10 @@ size_t http_validator_count_cb(void *context) * @param out_info Output info structure * * @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 + * @return LANTERN_HTTP_CB_ERR_INVALID_PARAM if context or out_info is NULL + * @return LANTERN_HTTP_CB_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 + * @return LANTERN_HTTP_CB_ERR_LOCK_FAILED if validator_lock is initialized but cannot be * acquired * * @note Thread safety: This function may acquire validator_lock @@ -207,12 +206,12 @@ int http_validator_info_cb( { if (!context || !out_info) { - return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; } struct lantern_client *client = context; if (index >= client->local_validator_count || !client->local_validators) { - return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND; + return LANTERN_HTTP_CB_ERR_NOT_FOUND; } memset(out_info, 0, sizeof(*out_info)); out_info->global_index = client->local_validators[index].global_index; @@ -222,7 +221,7 @@ int http_validator_info_cb( { if (pthread_mutex_lock(&client->validator_lock) != 0) { - return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + return LANTERN_HTTP_CB_ERR_LOCK_FAILED; } if (client->validator_enabled && index < client->local_validator_count) { @@ -248,7 +247,7 @@ int http_validator_info_cb( strncpy(out_info->label, base, sizeof(out_info->label)); out_info->label[sizeof(out_info->label) - 1] = '\0'; } - return LANTERN_CLIENT_HTTP_OK; + return LANTERN_HTTP_CB_OK; } @@ -260,10 +259,10 @@ int http_validator_info_cb( * @param enabled New enabled status * * @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 + * @return LANTERN_HTTP_CB_ERR_INVALID_PARAM if context is NULL + * @return LANTERN_HTTP_CB_ERR_INVALID_STATE if validator tracking is not initialized + * @return LANTERN_HTTP_CB_ERR_LOCK_FAILED if validator_lock cannot be acquired + * @return LANTERN_HTTP_CB_ERR_NOT_FOUND if global_index is not a local validator * * @note Thread safety: This function acquires validator_lock */ @@ -271,23 +270,23 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab { if (!context) { - return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; } struct lantern_client *client = context; if (!client->validator_lock_initialized || !client->validator_enabled) { - return LANTERN_CLIENT_HTTP_ERR_INVALID_STATE; + return LANTERN_HTTP_CB_ERR_INVALID_STATE; } if (pthread_mutex_lock(&client->validator_lock) != 0) { - return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + return LANTERN_HTTP_CB_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) { unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); - return LANTERN_CLIENT_HTTP_ERR_NOT_FOUND; + return LANTERN_HTTP_CB_ERR_NOT_FOUND; } client->validator_enabled[local_index] = enabled; @@ -316,7 +315,7 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab enabled_count, disabled_count); - return LANTERN_CLIENT_HTTP_OK; + return LANTERN_HTTP_CB_OK; } @@ -331,8 +330,8 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab * @param out_snapshot Output snapshot structure * * @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 + * @return LANTERN_HTTP_CB_ERR_INVALID_PARAM if context or out_snapshot is NULL + * @return LANTERN_HTTP_CB_ERR_LOCK_FAILED if required locks cannot be acquired * * @note Thread safety: This function may acquire state_lock and peer_vote_lock */ @@ -340,7 +339,7 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap { if (!context || !out_snapshot) { - return LANTERN_CLIENT_HTTP_ERR_INVALID_PARAM; + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; } struct lantern_client *client = context; memset(out_snapshot, 0, sizeof(*out_snapshot)); @@ -349,13 +348,14 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap bool state_locked = lantern_client_lock_state(client); if (expect_state_lock && !state_locked) { - return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + return LANTERN_HTTP_CB_ERR_LOCK_FAILED; } bool have_fork_head = false; LanternRoot fork_head_root; memset(&fork_head_root, 0, sizeof(fork_head_root)); uint64_t fork_head_slot = 0; + uint64_t safe_target_slot = 0; if (client->has_fork_choice) { if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head_root) == 0) @@ -373,9 +373,26 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap have_fork_head = true; } } + + const LanternRoot *safe_target = lantern_fork_choice_safe_target(&client->fork_choice); + if (safe_target) + { + uint64_t slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + safe_target, + &slot, + NULL, + NULL) + == 0) + { + safe_target_slot = slot; + } + } } uint64_t state_head_slot = 0; + uint64_t current_slot = 0; LanternCheckpoint state_justified; LanternCheckpoint state_finalized; memset(&state_justified, 0, sizeof(state_justified)); @@ -388,18 +405,35 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap state_justified = client->state.latest_justified; state_finalized = client->state.latest_finalized; } + if (!lantern_client_current_slot(client, ¤t_slot)) + { + current_slot = state_head_slot; + } lantern_client_unlock_state(client, state_locked); + out_snapshot->lean_node_start_time_seconds = client->start_time_seconds; out_snapshot->lean_head_slot = have_fork_head ? fork_head_slot : state_head_slot; + out_snapshot->lean_current_slot = current_slot; + out_snapshot->lean_safe_target_slot = safe_target_slot; out_snapshot->lean_latest_justified_slot = state_justified.slot; out_snapshot->lean_latest_finalized_slot = state_finalized.slot; out_snapshot->lean_validators_count = client->local_validator_count; + out_snapshot->lean_connected_peers = 0; + if (client->connection_lock_initialized) + { + if (pthread_mutex_lock(&client->connection_lock) != 0) + { + return LANTERN_HTTP_CB_ERR_LOCK_FAILED; + } + out_snapshot->lean_connected_peers = client->connected_peers; + unlock_mutex_with_log(&client->connection_lock, client->node_id, "connection_lock"); + } out_snapshot->peer_vote_metrics_count = 0; if (client->peer_vote_lock_initialized) { if (pthread_mutex_lock(&client->peer_vote_lock) != 0) { - return LANTERN_CLIENT_HTTP_ERR_LOCK_FAILED; + return LANTERN_HTTP_CB_ERR_LOCK_FAILED; } { @@ -418,5 +452,79 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap } } lean_metrics_snapshot(&out_snapshot->lean_metrics); - return LANTERN_CLIENT_HTTP_OK; + return LANTERN_HTTP_CB_OK; +} + + +/* ============================================================================ + * Checkpoint Sync Callbacks + * ============================================================================ */ + +/** + * Get finalized state SSZ bytes for checkpoint sync. + * + * @param context Client instance + * @param out_bytes Output buffer pointer (caller owns and must free) + * @param out_len Output byte length + * + * @return 0 on success + * @return LANTERN_HTTP_CB_ERR_INVALID_PARAM if inputs are NULL + * @return LANTERN_HTTP_CB_ERR_INVALID_STATE if client has no state or data dir + * @return LANTERN_HTTP_CB_ERR_NOT_FOUND if finalized state is unavailable + * @return LANTERN_HTTP_CB_ERR_LOCK_FAILED if state_lock cannot be acquired + * @return LANTERN_HTTP_CB_ERR_IO on storage read failure + * + * @note Thread safety: This function may acquire state_lock + */ +int http_finalized_state_ssz_cb(void *context, uint8_t **out_bytes, size_t *out_len) +{ + if (!context || !out_bytes || !out_len) + { + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; + } + + *out_bytes = NULL; + *out_len = 0; + + struct lantern_client *client = context; + if (!client->data_dir) + { + return LANTERN_HTTP_CB_ERR_INVALID_STATE; + } + + 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_HTTP_CB_ERR_LOCK_FAILED; + } + + if (!client->has_state) + { + lantern_client_unlock_state(client, state_locked); + return LANTERN_HTTP_CB_ERR_INVALID_STATE; + } + + LanternCheckpoint finalized = client->state.latest_finalized; + lantern_client_unlock_state(client, state_locked); + + if (lantern_root_is_zero(&finalized.root)) + { + return LANTERN_HTTP_CB_ERR_NOT_FOUND; + } + + int load_rc = lantern_storage_load_state_bytes_for_root( + client->data_dir, + &finalized.root, + out_bytes, + out_len); + if (load_rc == 0) + { + return LANTERN_HTTP_CB_OK; + } + if (load_rc > 0) + { + return LANTERN_HTTP_CB_ERR_NOT_FOUND; + } + return LANTERN_HTTP_CB_ERR_IO; } diff --git a/src/core/client_internal.h b/src/core/client_internal.h index ac47fc1..29af224 100644 --- a/src/core/client_internal.h +++ b/src/core/client_internal.h @@ -69,17 +69,6 @@ uint64_t validator_wall_time_now_seconds(void); void validator_sleep_ms(uint32_t ms); -/** - * Calculate backoff time for blocks request based on failure count. - * - * @param failures Number of consecutive failures - * @return Backoff time in milliseconds - * - * @note Thread safety: This function is thread-safe - */ -uint64_t blocks_request_backoff_ms(uint32_t failures); - - /** * Format a root hash as hex string. * diff --git a/src/core/client_network.c b/src/core/client_network.c index ec84064..32f8224 100644 --- a/src/core/client_network.c +++ b/src/core/client_network.c @@ -29,6 +29,7 @@ #include #include "lantern/networking/libp2p.h" +#include "lantern/metrics/lean_metrics.h" #include "lantern/support/log.h" #include "lantern/support/string_list.h" @@ -73,6 +74,32 @@ static void format_peer_id_text(const peer_id_t *peer, char *out, size_t out_len } } +static lean_metrics_direction_t metrics_direction_from_inbound(bool inbound) +{ + return inbound ? LEAN_METRICS_DIR_INBOUND : LEAN_METRICS_DIR_OUTBOUND; +} + +static lean_metrics_connection_result_t metrics_connection_result_from_code(int code) +{ + return (code == LIBP2P_ERR_TIMEOUT) ? LEAN_METRICS_CONN_RESULT_TIMEOUT : LEAN_METRICS_CONN_RESULT_ERROR; +} + +static lean_metrics_disconnection_reason_t metrics_disconnection_reason_from_code(int reason) +{ + switch (reason) + { + case LIBP2P_ERR_TIMEOUT: + return LEAN_METRICS_DISCONNECT_TIMEOUT; + case LIBP2P_ERR_EOF: + case LIBP2P_ERR_RESET: + return LEAN_METRICS_DISCONNECT_REMOTE_CLOSE; + case 0: + case LIBP2P_ERR_CLOSED: + return LEAN_METRICS_DISCONNECT_LOCAL_CLOSE; + default: + return LEAN_METRICS_DISCONNECT_ERROR; + } +} /* ============================================================================ * Connection Counter Functions @@ -96,6 +123,8 @@ void connection_counter_reset(struct lantern_client *client) client->connected_peers = 0; lantern_string_list_reset(&client->connected_peer_ids); lantern_string_list_init(&client->connected_peer_ids); + lantern_string_list_reset(&client->inbound_peer_ids); + lantern_string_list_init(&client->inbound_peer_ids); return; } if (pthread_mutex_lock(&client->connection_lock) == 0) @@ -103,6 +132,8 @@ void connection_counter_reset(struct lantern_client *client) client->connected_peers = 0; lantern_string_list_reset(&client->connected_peer_ids); lantern_string_list_init(&client->connected_peer_ids); + lantern_string_list_reset(&client->inbound_peer_ids); + lantern_string_list_init(&client->inbound_peer_ids); pthread_mutex_unlock(&client->connection_lock); } else @@ -110,6 +141,8 @@ void connection_counter_reset(struct lantern_client *client) client->connected_peers = 0; lantern_string_list_reset(&client->connected_peer_ids); lantern_string_list_init(&client->connected_peer_ids); + lantern_string_list_reset(&client->inbound_peer_ids); + lantern_string_list_init(&client->inbound_peer_ids); } } @@ -140,6 +173,7 @@ void connection_counter_update( char peer_text[128]; format_peer_id_text(peer, peer_text, sizeof(peer_text)); size_t total = 0; + bool was_inbound = inbound; if (pthread_mutex_lock(&client->connection_lock) == 0) { if (peer_text[0]) @@ -150,10 +184,23 @@ void connection_counter_update( { (void)lantern_string_list_append(&client->connected_peer_ids, peer_text); } + if (inbound) + { + if (!string_list_contains(&client->inbound_peer_ids, peer_text)) + { + (void)lantern_string_list_append(&client->inbound_peer_ids, peer_text); + } + } + else + { + string_list_remove(&client->inbound_peer_ids, peer_text); + } } else if (delta < 0) { + was_inbound = string_list_contains(&client->inbound_peer_ids, peer_text); string_list_remove(&client->connected_peer_ids, peer_text); + string_list_remove(&client->inbound_peer_ids, peer_text); } client->connected_peers = client->connected_peer_ids.len; } @@ -183,6 +230,13 @@ void connection_counter_update( { return; } + + if (total == 0 && client->status_lock_initialized + && pthread_mutex_lock(&client->status_lock) == 0) + { + client->sync_state = LANTERN_SYNC_STATE_IDLE; + pthread_mutex_unlock(&client->status_lock); + } lantern_log_trace( "network", &(const struct lantern_log_metadata){ @@ -195,6 +249,19 @@ void connection_counter_update( total, reason, connection_reason_text(reason)); + + if (delta > 0) + { + lean_metrics_record_peer_connection( + metrics_direction_from_inbound(inbound), + LEAN_METRICS_CONN_RESULT_SUCCESS); + } + else if (delta < 0) + { + lean_metrics_record_peer_disconnection( + metrics_direction_from_inbound(was_inbound), + metrics_disconnection_reason_from_code(reason)); + } } @@ -1014,6 +1081,41 @@ static void ping_on_stream_open(libp2p_stream_t *s, void *user_data, int err) free(ctx); } +static bool peer_status_needs_refresh( + struct lantern_client *client, + const char *peer_id, + uint64_t now_ms) +{ + if (!client || !peer_id || peer_id[0] == '\0') + { + return false; + } + if (!client->status_lock_initialized) + { + return true; + } + if (pthread_mutex_lock(&client->status_lock) != 0) + { + return true; + } + struct lantern_peer_status_entry *entry = + lantern_client_find_status_entry_locked(client, peer_id); + bool needs_refresh = true; + if (entry) + { + if (entry->status_request_inflight) + { + needs_refresh = false; + } + else if (entry->has_status && entry->last_status_ms != 0 && now_ms >= entry->last_status_ms) + { + needs_refresh = (now_ms - entry->last_status_ms) > LANTERN_PEER_STATUS_STALE_MS; + } + } + pthread_mutex_unlock(&client->status_lock); + return needs_refresh; +} + /** * Ping all connected peers. @@ -1044,6 +1146,7 @@ static void ping_all_peers(struct lantern_client *client) } pthread_mutex_unlock(&client->connection_lock); + uint64_t now_ms = monotonic_millis(); for (size_t i = 0; i < peers.len; i++) { const char *peer_str = peers.items[i]; @@ -1056,6 +1159,10 @@ static void ping_all_peers(struct lantern_client *client) { continue; } + if (peer_status_needs_refresh(client, peer_str, now_ms)) + { + request_status_now(client, &peer, peer_str); + } struct ping_dial_ctx *ctx = (struct ping_dial_ctx *)calloc(1, sizeof(*ctx)); if (!ctx) { @@ -1335,6 +1442,10 @@ static void handle_outgoing_connection_error_event( code, connection_reason_text(code), msg ? msg : "-"); + + lean_metrics_record_peer_connection( + LEAN_METRICS_DIR_OUTBOUND, + metrics_connection_result_from_code(code)); } @@ -1372,6 +1483,10 @@ static void handle_incoming_connection_error_event( code, connection_reason_text(code), msg ? msg : "-"); + + lean_metrics_record_peer_connection( + LEAN_METRICS_DIR_INBOUND, + metrics_connection_result_from_code(code)); } diff --git a/src/core/client_network_internal.h b/src/core/client_network_internal.h index e840854..835b0fe 100644 --- a/src/core/client_network_internal.h +++ b/src/core/client_network_internal.h @@ -45,17 +45,8 @@ extern "C" { /** Peer dial interval in seconds */ #define LANTERN_PEER_DIAL_INTERVAL_SECONDS 5u -/** Base backoff for blocks request in milliseconds */ -#define LANTERN_BLOCKS_REQUEST_BACKOFF_BASE_MS 5000u - -/** Maximum backoff for blocks request in milliseconds */ -#define LANTERN_BLOCKS_REQUEST_BACKOFF_MAX_MS 300000u - -/** Maximum consecutive failures before max backoff */ -#define LANTERN_BLOCKS_REQUEST_BACKOFF_MAX_FAILURES 8u - -/** Minimum poll interval for blocks requests in milliseconds */ -#define LANTERN_BLOCKS_REQUEST_MIN_POLL_MS 2000u +/** Maximum concurrent blocks requests per peer */ +#define LANTERN_MAX_BLOCKS_REQUESTS_PER_PEER 2u /** Peer dial timeout in milliseconds */ #define LANTERN_PEER_DIAL_TIMEOUT_MS 4000 @@ -71,10 +62,14 @@ extern "C" { enum lantern_blocks_request_outcome { LANTERN_BLOCKS_REQUEST_SUCCESS = 0, - LANTERN_BLOCKS_REQUEST_FAILED, - LANTERN_BLOCKS_REQUEST_ABORTED + LANTERN_BLOCKS_REQUEST_FAILED = 1, + LANTERN_BLOCKS_REQUEST_ABORTED = 2, + LANTERN_BLOCKS_REQUEST_EMPTY = 3 }; +/** Peer status considered stale after this many milliseconds. */ +#define LANTERN_PEER_STATUS_STALE_MS (30000u) + /** * Peer status tracking entry. @@ -89,7 +84,8 @@ struct lantern_peer_status_entry char peer_id[128]; /**< Peer ID string */ LanternStatusMessage status; /**< Latest status message from peer */ bool has_status; /**< True if status has been received */ - bool requested_head; /**< True if head block was requested */ + uint64_t last_status_ms; /**< Timestamp of last status message */ + uint32_t blocks_requests_inflight; /**< Count of in-flight block requests */ bool status_request_inflight; /**< True if status request is pending */ uint64_t last_blocks_request_ms; /**< Timestamp of last blocks request */ uint32_t consecutive_blocks_failures; /**< Count of consecutive request failures */ @@ -106,7 +102,9 @@ struct block_request_ctx struct lantern_client *client; /**< Client instance */ peer_id_t peer_id; /**< Peer ID structure */ char peer_text[128]; /**< Peer ID as text */ - LanternRoot root; /**< Block root being requested */ + LanternRoot *roots; /**< Roots being requested */ + uint32_t *depths; /**< Backfill depth per root */ + size_t root_count; /**< Number of roots requested */ const char *protocol_id; /**< Protocol ID string */ }; diff --git a/src/core/client_pending.c b/src/core/client_pending.c index a3f51b5..58c7634 100644 --- a/src/core/client_pending.c +++ b/src/core/client_pending.c @@ -26,6 +26,7 @@ enum }; static const size_t BLOCK_LIST_INITIAL_CAPACITY = 4u; +static const size_t PARENT_INDEX_INITIAL_CAPACITY = 4u; /* ============================================================================ @@ -141,6 +142,318 @@ static int ensure_pending_block_list_capacity( return LANTERN_CLIENT_PENDING_OK; } +static int ensure_pending_parent_index_capacity( + struct lantern_pending_parent_index *index, + size_t required) +{ + if (!index) + { + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; + } + + if (index->capacity >= required) + { + return LANTERN_CLIENT_PENDING_OK; + } + + size_t new_capacity = PARENT_INDEX_INITIAL_CAPACITY; + if (index->capacity > 0) + { + size_t half = index->capacity / 2u; + if (index->capacity > SIZE_MAX - half) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + new_capacity = index->capacity + half; + if (new_capacity < PARENT_INDEX_INITIAL_CAPACITY) + { + new_capacity = PARENT_INDEX_INITIAL_CAPACITY; + } + } + + if (new_capacity < required) + { + new_capacity = required; + } + + if (new_capacity > SIZE_MAX / sizeof(*index->entries)) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + + struct lantern_pending_parent_index_entry *expanded = realloc( + index->entries, + new_capacity * sizeof(*expanded)); + if (!expanded) + { + return LANTERN_CLIENT_PENDING_ERR_ALLOC; + } + + index->entries = expanded; + index->capacity = new_capacity; + return LANTERN_CLIENT_PENDING_OK; +} + +static int ensure_pending_parent_entry_capacity( + struct lantern_pending_parent_index_entry *entry, + size_t required) +{ + if (!entry) + { + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; + } + + if (entry->capacity >= required) + { + return LANTERN_CLIENT_PENDING_OK; + } + + size_t new_capacity = PARENT_INDEX_INITIAL_CAPACITY; + if (entry->capacity > 0) + { + size_t half = entry->capacity / 2u; + if (entry->capacity > SIZE_MAX - half) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + new_capacity = entry->capacity + half; + if (new_capacity < PARENT_INDEX_INITIAL_CAPACITY) + { + new_capacity = PARENT_INDEX_INITIAL_CAPACITY; + } + } + + if (new_capacity < required) + { + new_capacity = required; + } + + if (new_capacity > SIZE_MAX / sizeof(*entry->child_roots)) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + + LanternRoot *expanded = realloc(entry->child_roots, new_capacity * sizeof(*expanded)); + if (!expanded) + { + return LANTERN_CLIENT_PENDING_ERR_ALLOC; + } + + entry->child_roots = expanded; + entry->capacity = new_capacity; + return LANTERN_CLIENT_PENDING_OK; +} + +static void pending_parent_index_init(struct lantern_pending_parent_index *index) +{ + if (!index) + { + return; + } + index->entries = NULL; + index->length = 0; + index->capacity = 0; +} + +static void pending_parent_index_reset(struct lantern_pending_parent_index *index) +{ + if (!index) + { + return; + } + + if (index->entries) + { + for (size_t i = 0; i < index->length; ++i) + { + free(index->entries[i].child_roots); + index->entries[i].child_roots = NULL; + index->entries[i].length = 0; + index->entries[i].capacity = 0; + } + free(index->entries); + } + + index->entries = NULL; + index->length = 0; + index->capacity = 0; +} + +static struct lantern_pending_parent_index_entry *pending_parent_index_find( + struct lantern_pending_parent_index *index, + const LanternRoot *parent_root) +{ + if (!index || !parent_root || !index->entries) + { + return NULL; + } + + for (size_t i = 0; i < index->length; ++i) + { + if (memcmp(index->entries[i].parent_root.bytes, parent_root->bytes, LANTERN_ROOT_SIZE) == 0) + { + return &index->entries[i]; + } + } + + return NULL; +} + +static struct lantern_pending_parent_index_entry *pending_parent_index_ensure( + struct lantern_pending_parent_index *index, + const LanternRoot *parent_root) +{ + if (!index || !parent_root) + { + return NULL; + } + + struct lantern_pending_parent_index_entry *entry = + pending_parent_index_find(index, parent_root); + if (entry) + { + return entry; + } + + if (index->length == SIZE_MAX) + { + return NULL; + } + + int ensure_rc = ensure_pending_parent_index_capacity(index, index->length + 1u); + if (ensure_rc != LANTERN_CLIENT_PENDING_OK) + { + return NULL; + } + + entry = &index->entries[index->length]; + memset(entry, 0, sizeof(*entry)); + entry->parent_root = *parent_root; + index->length += 1u; + return entry; +} + +static void pending_parent_index_add_child( + struct lantern_pending_parent_index *index, + const LanternRoot *parent_root, + const LanternRoot *child_root) +{ + if (!index || !parent_root || !child_root) + { + return; + } + if (lantern_root_is_zero(parent_root)) + { + return; + } + + struct lantern_pending_parent_index_entry *entry = + pending_parent_index_ensure(index, parent_root); + if (!entry) + { + return; + } + + for (size_t i = 0; i < entry->length; ++i) + { + if (memcmp(entry->child_roots[i].bytes, child_root->bytes, LANTERN_ROOT_SIZE) == 0) + { + return; + } + } + + if (entry->length == SIZE_MAX) + { + return; + } + + int ensure_rc = ensure_pending_parent_entry_capacity(entry, entry->length + 1u); + if (ensure_rc != LANTERN_CLIENT_PENDING_OK) + { + return; + } + + entry->child_roots[entry->length] = *child_root; + entry->length += 1u; +} + +static void pending_parent_index_remove_child( + struct lantern_pending_parent_index *index, + const LanternRoot *parent_root, + const LanternRoot *child_root) +{ + if (!index || !parent_root || !child_root || !index->entries) + { + return; + } + + for (size_t i = 0; i < index->length; ++i) + { + struct lantern_pending_parent_index_entry *entry = &index->entries[i]; + if (memcmp(entry->parent_root.bytes, parent_root->bytes, LANTERN_ROOT_SIZE) != 0) + { + continue; + } + + for (size_t j = 0; j < entry->length; ++j) + { + if (memcmp(entry->child_roots[j].bytes, child_root->bytes, LANTERN_ROOT_SIZE) != 0) + { + continue; + } + + if (j + 1u < entry->length) + { + memmove( + &entry->child_roots[j], + &entry->child_roots[j + 1u], + (entry->length - (j + 1u)) * sizeof(*entry->child_roots)); + } + entry->length -= 1u; + + if (entry->length == 0) + { + free(entry->child_roots); + if (i + 1u < index->length) + { + memmove( + &index->entries[i], + &index->entries[i + 1u], + (index->length - (i + 1u)) * sizeof(*index->entries)); + } + index->length -= 1u; + if (index->length < index->capacity) + { + memset(&index->entries[index->length], 0, sizeof(*index->entries)); + } + } + return; + } + return; + } +} + +static bool pending_parent_index_peek_child( + struct lantern_pending_parent_index *index, + const LanternRoot *parent_root, + LanternRoot *out_child_root) +{ + if (!index || !parent_root || !out_child_root || !index->entries) + { + return false; + } + + struct lantern_pending_parent_index_entry *entry = + pending_parent_index_find(index, parent_root); + if (!entry || entry->length == 0) + { + return false; + } + + *out_child_root = entry->child_roots[entry->length - 1u]; + return true; +} + /* ============================================================================ * Block Cloning @@ -308,6 +621,7 @@ void pending_block_list_init(struct lantern_pending_block_list *list) list->items = NULL; list->length = 0; list->capacity = 0; + pending_parent_index_init(&list->parent_index); } @@ -324,6 +638,7 @@ void pending_block_list_reset(struct lantern_pending_block_list *list) { return; } + pending_parent_index_reset(&list->parent_index); if (list->items) { for (size_t i = 0; i < list->length; ++i) @@ -384,6 +699,10 @@ void pending_block_list_remove(struct lantern_pending_block_list *list, size_t i } struct lantern_pending_block *entry = &list->items[index]; + pending_parent_index_remove_child( + &list->parent_index, + &entry->parent_root, + &entry->root); lantern_signed_block_with_attestation_reset(&entry->block); if (index + 1u < list->length) @@ -423,7 +742,8 @@ struct lantern_pending_block *pending_block_list_append( const LanternSignedBlock *block, const LanternRoot *block_root, const LanternRoot *parent_root, - const char *peer_text) + const char *peer_text, + uint32_t backfill_depth) { if (!list || !block || !block_root || !parent_root) { @@ -452,6 +772,8 @@ struct lantern_pending_block *pending_block_list_append( entry->parent_root = *parent_root; entry->peer_text[0] = '\0'; entry->parent_requested = false; + entry->received_ms = monotonic_millis(); + entry->backfill_depth = backfill_depth; if (peer_text && *peer_text) { @@ -459,7 +781,32 @@ struct lantern_pending_block *pending_block_list_append( entry->peer_text[sizeof(entry->peer_text) - 1u] = '\0'; } + pending_parent_index_add_child(&list->parent_index, parent_root, block_root); list->length += 1u; return entry; } + + +/** + * Peek a pending child root for a given parent root. + * + * @param list Pending block list + * @param parent_root Parent root to match + * @param out_child_root Output child root + * @return true if a child is available, false otherwise + * + * @note Thread safety: Caller must hold pending_lock + */ +bool pending_block_list_peek_child_root( + struct lantern_pending_block_list *list, + const LanternRoot *parent_root, + LanternRoot *out_child_root) +{ + if (!list || !parent_root || !out_child_root) + { + return false; + } + + return pending_parent_index_peek_child(&list->parent_index, parent_root, out_child_root); +} diff --git a/src/core/client_reqresp.c b/src/core/client_reqresp.c index 47b9dc6..c8886e0 100644 --- a/src/core/client_reqresp.c +++ b/src/core/client_reqresp.c @@ -44,9 +44,6 @@ * ============================================================================ */ static const uint64_t SYNC_PROGRESS_LOG_INTERVAL_MS = 5000u; -static const uint64_t SYNC_PROGRESS_SLOT_LAG = 2u; -static const size_t SYNC_PROGRESS_PENDING_THRESHOLD = 8u; -static const uint64_t SYNC_DUPLICATE_REQUEST_MS = 500u; /* ============================================================================ @@ -79,19 +76,11 @@ static struct lantern_peer_status_entry *lantern_client_update_peer_status_entry 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( +static void lantern_client_peer_status_update( 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); + uint64_t local_slot); static bool lantern_client_update_blocks_request_tracking( struct lantern_client *client, const char *peer_id, @@ -301,14 +290,47 @@ static void peer_status_local_view( bool state_locked = lantern_client_lock_state(client); if (state_locked) { - local_slot = client->state.slot; + local_slot = client->state.latest_block_header.slot; + if (client->has_fork_choice) + { + LanternRoot fork_head = {0}; + if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head) == 0) + { + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + &fork_head, + &fork_slot, + NULL, + NULL) + == 0) + { + local_slot = fork_slot; + } + } + } head_known = lantern_client_block_known_locked(client, head_root, NULL); } else if (client->has_state) { - local_slot = client->state.slot; + local_slot = client->state.latest_block_header.slot; if (client->has_fork_choice) { + LanternRoot fork_head = {0}; + if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head) == 0) + { + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + &fork_head, + &fork_slot, + NULL, + NULL) + == 0) + { + local_slot = fork_slot; + } + } uint64_t fork_slot = 0; if (lantern_fork_choice_block_info( &client->fork_choice, @@ -326,38 +348,80 @@ static void peer_status_local_view( *out_local_slot = local_slot; *out_head_known = head_known; + } -static uint64_t max_peer_head_slot_locked( - const struct lantern_client *client, - bool *out_have_status) +static bool peer_status_is_eligible( + struct lantern_client *client, + const struct lantern_peer_status_entry *entry, + uint64_t now_ms) { - uint64_t max_slot = 0; - bool have_status = false; + if (!client || !entry || !entry->has_status) + { + return false; + } + if (entry->last_status_ms == 0 || now_ms < entry->last_status_ms) + { + return false; + } + if (!client->connection_lock_initialized) + { + return true; + } + return lantern_client_is_peer_connected(client, entry->peer_id); +} + +static bool network_finalized_slot_locked( + struct lantern_client *client, + uint64_t now_ms, + uint64_t *out_slot) +{ + if (!client || !out_slot) + { + return false; + } - if (client) + bool found = false; + uint64_t mode_slot = 0; + size_t mode_count = 0; + + for (size_t i = 0; i < client->peer_status_count; ++i) { - for (size_t i = 0; i < client->peer_status_count; ++i) + const struct lantern_peer_status_entry *entry = &client->peer_status_entries[i]; + if (!peer_status_is_eligible(client, entry, now_ms)) + { + continue; + } + + uint64_t slot = entry->status.finalized.slot; + size_t count = 0; + for (size_t j = 0; j < client->peer_status_count; ++j) { - const struct lantern_peer_status_entry *entry = &client->peer_status_entries[i]; - if (!entry->has_status) + const struct lantern_peer_status_entry *other = &client->peer_status_entries[j]; + if (!peer_status_is_eligible(client, other, now_ms)) { continue; } - have_status = true; - if (entry->status.head.slot > max_slot) + if (other->status.finalized.slot == slot) { - max_slot = entry->status.head.slot; + count += 1u; } } + + if (!found || count > mode_count) + { + mode_slot = slot; + mode_count = count; + found = true; + } } - if (out_have_status) + if (found) { - *out_have_status = have_status; + *out_slot = mode_slot; } - return max_slot; + return found; } static void format_duration_seconds(uint64_t seconds, char *out, size_t out_len) @@ -389,34 +453,109 @@ static void format_duration_seconds(uint64_t seconds, char *out, size_t out_len) snprintf(out, out_len, "%" PRIu64 "s", secs); } -static void maybe_log_sync_progress_locked( +static void maybe_log_sync_progress( struct lantern_client *client, - uint64_t local_slot) + uint64_t local_slot, + uint64_t network_finalized, + bool has_network_finalized, + bool allow_sync_complete) { if (!client) { return; } - bool have_status = false; - uint64_t max_peer_slot = max_peer_head_slot_locked(client, &have_status); - if (!have_status) + if (!has_network_finalized) { return; } - size_t pending = lantern_client_pending_block_count(client); - bool sync_gap = max_peer_slot > local_slot + SYNC_PROGRESS_SLOT_LAG; - bool sync_pending = pending >= SYNC_PROGRESS_PENDING_THRESHOLD; - bool syncing = sync_gap || sync_pending; - uint64_t now_ms = monotonic_millis(); + bool was_idle = client->sync_state == LANTERN_SYNC_STATE_IDLE; + if (was_idle) + { + client->sync_state = LANTERN_SYNC_STATE_SYNCING; + } + size_t pending = 0; + size_t orphan_count = 0; + char pending_peer[sizeof(((struct lantern_pending_block *)0)->peer_text)]; + pending_peer[0] = '\0'; + char orphan_peer[sizeof(((struct lantern_pending_block *)0)->peer_text)]; + orphan_peer[0] = '\0'; + { + bool state_locked = lantern_client_lock_state(client); + bool pending_locked = lantern_client_lock_pending(client); + if (pending_locked) + { + pending = client->pending_blocks.length; + for (size_t i = 0; i < client->pending_blocks.length; ++i) + { + const struct lantern_pending_block *entry = &client->pending_blocks.items[i]; + if (pending_peer[0] == '\0' && entry->peer_text[0] != '\0') + { + strncpy(pending_peer, entry->peer_text, sizeof(pending_peer) - 1u); + pending_peer[sizeof(pending_peer) - 1u] = '\0'; + } + if (lantern_root_is_zero(&entry->parent_root)) + { + continue; + } + bool parent_cached = pending_block_list_find( + &client->pending_blocks, + &entry->parent_root) + != NULL; + bool parent_known = false; + if (!parent_cached && state_locked) + { + parent_known = lantern_client_block_known_locked( + client, + &entry->parent_root, + NULL); + } + if (!parent_cached && !parent_known) + { + orphan_count += 1u; + if (orphan_peer[0] == '\0' && entry->peer_text[0] != '\0') + { + strncpy(orphan_peer, entry->peer_text, sizeof(orphan_peer) - 1u); + orphan_peer[sizeof(orphan_peer) - 1u] = '\0'; + } + } + } + } + lantern_client_unlock_pending(client, pending_locked); + lantern_client_unlock_state(client, state_locked); + } + bool has_orphans = orphan_count > 0; + bool behind_finalized = local_slot < network_finalized; + bool syncing = behind_finalized || has_orphans; + + uint64_t now_ms = monotonic_millis(); struct lantern_log_metadata meta = {.validator = client->node_id}; + bool should_request_parent = false; + const char *request_peer = orphan_peer[0] ? orphan_peer : pending_peer; + if (has_orphans && request_peer && request_peer[0] != '\0') + { + should_request_parent = true; + } + bool allow_complete = allow_sync_complete && client->sync_state == LANTERN_SYNC_STATE_SYNCING; if (!syncing) { + if (was_idle) + { + /* Hold SYNCING for one status update to honor IDLE -> SYNCING transition. */ + return; + } + if (!allow_complete) + { + return; + } + client->sync_state = LANTERN_SYNC_STATE_SYNCED; if (client->sync_in_progress) { + uint64_t target_slot = + client->sync_target_slot != 0 ? client->sync_target_slot : network_finalized; uint64_t elapsed_s = 0; if (client->sync_started_ms != 0 && now_ms > client->sync_started_ms) { @@ -429,17 +568,19 @@ static void maybe_log_sync_progress_locked( &meta, "sync complete local_slot=%" PRIu64 " target_slot=%" PRIu64 " duration=%s imported=%" PRIu64, local_slot, - max_peer_slot, + target_slot, duration, client->sync_imported_blocks); client->sync_in_progress = false; client->sync_started_ms = 0; client->sync_last_log_ms = 0; client->sync_last_imported_blocks = 0; + client->sync_target_slot = 0; } return; } + client->sync_state = LANTERN_SYNC_STATE_SYNCING; if (!client->sync_in_progress) { client->sync_in_progress = true; @@ -447,12 +588,13 @@ static void maybe_log_sync_progress_locked( client->sync_last_log_ms = 0; client->sync_last_imported_blocks = 0; client->sync_imported_blocks = 0; + client->sync_target_slot = network_finalized; lantern_log_info( "sync", &meta, "sync starting local_slot=%" PRIu64 " target_slot=%" PRIu64 " pending=%zu", local_slot, - max_peer_slot, + network_finalized, pending); } @@ -462,57 +604,79 @@ static void maybe_log_sync_progress_locked( return; } - uint64_t remaining = (max_peer_slot > local_slot) ? (max_peer_slot - local_slot) : 0; - uint64_t base_ms = - (client->sync_last_log_ms != 0) ? client->sync_last_log_ms : client->sync_started_ms; - uint64_t base_blocks = - (client->sync_last_log_ms != 0) ? client->sync_last_imported_blocks : 0; - double rate = 0.0; - if (base_ms != 0 && now_ms > base_ms) + uint64_t target_slot = + client->sync_target_slot != 0 ? client->sync_target_slot : network_finalized; + uint64_t remaining = (target_slot > local_slot) ? (target_slot - local_slot) : 0; + + if (pending > 0) { - uint64_t delta_blocks = 0; - if (client->sync_imported_blocks >= base_blocks) - { - delta_blocks = client->sync_imported_blocks - base_blocks; - } - double delta_sec = (double)(now_ms - base_ms) / 1000.0; - if (delta_sec > 0.0) - { - rate = (double)delta_blocks / delta_sec; - } + lantern_log_info( + "sync", + &meta, + "sync progress local_slot=%" PRIu64 " target_slot=%" PRIu64 + " remaining=%" PRIu64 " pending=%zu", + local_slot, + target_slot, + remaining, + pending); } + else + { + lantern_log_info( + "sync", + &meta, + "sync progress local_slot=%" PRIu64 " target_slot=%" PRIu64 " remaining=%" PRIu64, + local_slot, + target_slot, + remaining); + } + + client->sync_last_log_ms = now_ms; + client->sync_last_imported_blocks = client->sync_imported_blocks; + + if (should_request_parent) + { + lantern_log_info( + "sync", + &meta, + "sync requesting orphan parent pending=%zu orphans=%zu", + pending, + orphan_count); + lantern_client_request_pending_parent_after_blocks(client, request_peer, NULL); + } +} - double percent = 0.0; - if (max_peer_slot > 0) +/** + * @brief Update sync progress using latest peer status and local slot snapshot. + * + * @param client Client instance + * @param local_slot Local slot snapshot + * + * @note Thread safety: This function acquires status_lock. + */ +void lantern_client_update_sync_progress( + struct lantern_client *client, + uint64_t local_slot) +{ + if (!client || !client->status_lock_initialized) { - uint64_t clamped_local = local_slot > max_peer_slot ? max_peer_slot : local_slot; - percent = ((double)clamped_local * 100.0) / (double)max_peer_slot; + return; } - uint64_t eta_seconds = 0; - if (rate > 0.0 && remaining > 0) + if (pthread_mutex_lock(&client->status_lock) != 0) { - eta_seconds = (uint64_t)((double)remaining / rate); + return; } - char eta_text[32]; - format_duration_seconds(eta_seconds, eta_text, sizeof(eta_text)); - lantern_log_info( - "sync", - &meta, - "sync progress local_slot=%" PRIu64 " target_slot=%" PRIu64 " remaining=%" PRIu64 - " (%.1f%%) pending=%zu imported=%" PRIu64 " rate=%.2f/s eta=%s", - local_slot, - max_peer_slot, - remaining, - percent, - pending, - client->sync_imported_blocks, - rate, - eta_text); + uint64_t network_finalized = 0; + bool has_network_finalized = network_finalized_slot_locked( + client, + monotonic_millis(), + &network_finalized); - client->sync_last_log_ms = now_ms; - client->sync_last_imported_blocks = client->sync_imported_blocks; + pthread_mutex_unlock(&client->status_lock); + + maybe_log_sync_progress(client, local_slot, network_finalized, has_network_finalized, true); } @@ -565,6 +729,7 @@ static struct lantern_peer_status_entry *lantern_client_update_peer_status_entry entry->status = *peer_status; entry->has_status = true; + entry->last_status_ms = monotonic_millis(); if (out_head_changed) { @@ -576,193 +741,50 @@ static struct lantern_peer_status_entry *lantern_client_update_peer_status_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. + * @brief Process peer status under status_lock and update sync progress. * - * @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 + * @param client Client instance + * @param peer_status Peer status message + * @param peer_id_text Peer ID string for tracking/logging + * @param local_slot Local slot snapshot * * @note Thread safety: This function acquires status_lock */ -static bool lantern_client_peer_status_maybe_request_blocks( +static void lantern_client_peer_status_update( 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) + uint64_t local_slot) { - if (!client || !peer_status || !peer_id_text || !head_root_text) + if (!client || !peer_status || !peer_id_text) { - return false; + return; } if (pthread_mutex_lock(&client->status_lock) != 0) { - return false; + return; } - 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( + if (!lantern_client_update_peer_status_entry_locked( client, peer_status, peer_id_text, - &head_changed); - if (!entry) + NULL)) { 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) - { - uint64_t now_ms = monotonic_millis(); - if (client->sync_last_requested_root_ms != 0 - && memcmp( - client->sync_last_requested_root.bytes, - request_root.bytes, - LANTERN_ROOT_SIZE) - == 0 - && now_ms < client->sync_last_requested_root_ms + SYNC_DUPLICATE_REQUEST_MS) - { - lantern_log_debug( - "reqresp", - &status_meta, - "status needs block head_slot=%" PRIu64 " local_slot=%" PRIu64 " " - "head_root=%s reason=%s (duplicate suppressed)", - peer_status->head.slot, - local_slot, - head_root_text, - needs_block_reason ? needs_block_reason : "unspecified"); - } - else - { - should_request = lantern_client_apply_blocks_request_backoff_locked( - client, - entry, - peer_id_text, - head_root_text); - if (should_request) - { - client->sync_last_requested_root = request_root; - client->sync_last_requested_root_ms = now_ms; - lantern_log_debug( - "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"); - } - } - } - 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); + return; } - maybe_log_sync_progress_locked(client, local_slot); + uint64_t network_finalized = 0; + bool has_network_finalized = network_finalized_slot_locked( + client, + monotonic_millis(), + &network_finalized); pthread_mutex_unlock(&client->status_lock); - if (out_request_root) - { - *out_request_root = request_root; - } - - return should_request; + maybe_log_sync_progress(client, local_slot, network_finalized, has_network_finalized, false); } @@ -804,7 +826,10 @@ static bool lantern_client_update_blocks_request_tracking( 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; + if (entry->blocks_requests_inflight > 0) + { + entry->blocks_requests_inflight -= 1u; + } switch (outcome) { case LANTERN_BLOCKS_REQUEST_SUCCESS: @@ -816,8 +841,17 @@ static bool lantern_client_update_blocks_request_tracking( entry->consecutive_blocks_failures += 1; } break; + case LANTERN_BLOCKS_REQUEST_EMPTY: + if (entry->consecutive_blocks_failures < UINT32_MAX) + { + entry->consecutive_blocks_failures += 1; + } + break; case LANTERN_BLOCKS_REQUEST_ABORTED: - entry->last_blocks_request_ms = 0; + if (entry->blocks_requests_inflight == 0) + { + entry->last_blocks_request_ms = 0; + } break; default: break; @@ -851,6 +885,8 @@ static const char *lantern_blocks_request_outcome_text(enum lantern_blocks_reque return "success"; case LANTERN_BLOCKS_REQUEST_FAILED: return "failed"; + case LANTERN_BLOCKS_REQUEST_EMPTY: + return "empty"; case LANTERN_BLOCKS_REQUEST_ABORTED: return "aborted"; default: @@ -860,18 +896,20 @@ static const char *lantern_blocks_request_outcome_text(enum lantern_blocks_reque /** - * @brief Clear parent_requested flags for pending blocks matching request_root. + * @brief Clear parent_requested flags for pending blocks matching requested roots. * - * @param client Client instance - * @param request_root Requested parent root + * @param client Client instance + * @param request_roots Requested parent roots + * @param root_count Number of requested roots * * @note Thread safety: This function acquires pending_lock */ -static void lantern_client_clear_pending_parent_requested( +static void lantern_client_clear_pending_parent_requested_roots( struct lantern_client *client, - const LanternRoot *request_root) + const LanternRoot *request_roots, + size_t root_count) { - if (!client || !request_root || lantern_root_is_zero(request_root)) + if (!client || !request_roots || root_count == 0) { return; } @@ -885,15 +923,42 @@ static void lantern_client_clear_pending_parent_requested( 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) + for (size_t j = 0; j < root_count; ++j) { - entry->parent_requested = false; + if (lantern_root_is_zero(&request_roots[j])) + { + continue; + } + if (memcmp(entry->parent_root.bytes, request_roots[j].bytes, LANTERN_ROOT_SIZE) == 0) + { + entry->parent_requested = false; + break; + } } } lantern_client_unlock_pending(client, locked); } +/** + * @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 (!request_root) + { + return; + } + lantern_client_clear_pending_parent_requested_roots(client, request_root, 1u); +} + /** * @brief Issue a follow-up status request after successful blocks fetch. @@ -994,8 +1059,8 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) * * @spec subspecs/networking/reqresp/message.py - Status protocol * - * Processes a peer's status message and determines if synchronization - * is needed. Delegates to internal peer status processor. + * Processes a peer's status message, updates peer tracking, and records + * sync progress. Peer status does not proactively trigger block requests. * * @param context Client instance * @param peer_status Status message from peer @@ -1137,11 +1202,9 @@ int reqresp_collect_blocks( * * @spec subspecs/forkchoice/store.py - Sync decision logic * - * Determines if block synchronization is needed based on peer status: - * 1. Checks if peer's head is known locally - * 2. Compares peer's head slot with local slot - * 3. Schedules blocks_by_root request if needed - * 4. Implements exponential backoff for failed requests + * Updates peer status tracking and sync progress based on the received + * status message. Block requests are initiated only by reactive sync + * paths (gossip backfill or missing parents), not by status alone. * * @param client Client instance * @param peer_status Status message from peer @@ -1167,10 +1230,6 @@ static void lantern_client_on_peer_status( return; } - 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"; - char peer_copy[sizeof(((struct lantern_peer_status_entry *)0)->peer_id)]; copy_peer_id_text(peer_id, peer_copy, sizeof(peer_copy)); @@ -1185,29 +1244,13 @@ static void lantern_client_on_peer_status( && peer_status->head.slot == 0 && local_slot == 0 && !head_known) { lantern_client_adopt_peer_genesis(client, peer_status, peer_copy); - head_known = true; } - LanternRoot request_root = peer_status->head.root; - bool should_request = lantern_client_peer_status_maybe_request_blocks( + lantern_client_peer_status_update( 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) != 0) - { - lantern_client_on_blocks_request_complete( - client, - peer_copy, - &request_root, - LANTERN_BLOCKS_REQUEST_ABORTED); - } - } + local_slot); } @@ -1280,27 +1323,29 @@ static void lantern_client_adopt_peer_genesis( /** - * Handle completion of a blocks request. + * Handle completion of a blocks request batch. * * @spec subspecs/networking/reqresp - Request lifecycle * * Updates peer tracking state after a blocks_by_root request completes: - * - Resets request-in-flight flag + * - Decrements request-in-flight counter * - Updates consecutive failure counter * - Clears parent_requested flag on pending blocks * - Triggers follow-up status request on success * * @param client Client instance * @param peer_id Peer ID string - * @param request_root Root that was requested + * @param request_roots Roots that were requested + * @param root_count Number of requested roots * @param outcome Request outcome * * @note Thread safety: This function acquires status_lock and pending_lock */ -void lantern_client_on_blocks_request_complete( +void lantern_client_on_blocks_request_complete_batch( struct lantern_client *client, const char *peer_id, - const LanternRoot *request_root, + const LanternRoot *request_roots, + size_t root_count, enum lantern_blocks_request_outcome outcome) { if (!client || !peer_id || !client->status_lock_initialized) @@ -1319,11 +1364,17 @@ void lantern_client_on_blocks_request_complete( return; } + const LanternRoot *first_root = NULL; + if (request_roots && root_count > 0) + { + first_root = &request_roots[0]; + } + char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; root_hex[0] = '\0'; - if (request_root) + if (first_root) { - format_root_hex(request_root, root_hex, sizeof(root_hex)); + format_root_hex(first_root, root_hex, sizeof(root_hex)); } const char *outcome_text = lantern_blocks_request_outcome_text(outcome); if (outcome == LANTERN_BLOCKS_REQUEST_SUCCESS && client->sync_in_progress) @@ -1333,8 +1384,10 @@ void lantern_client_on_blocks_request_complete( &(const struct lantern_log_metadata){ .validator = client->node_id, .peer = peer_id}, - "blocks_by_root complete outcome=%s root=%s entry_found=%s consecutive_failures=%" PRIu32, + "blocks_by_root complete outcome=%s roots=%zu first_root=%s entry_found=%s " + "consecutive_failures=%" PRIu32, outcome_text, + root_count, root_hex[0] ? root_hex : "0x0", entry_found ? "true" : "false", failure_count); @@ -1346,18 +1399,59 @@ void lantern_client_on_blocks_request_complete( &(const struct lantern_log_metadata){ .validator = client->node_id, .peer = peer_id}, - "blocks_by_root complete outcome=%s root=%s entry_found=%s consecutive_failures=%" PRIu32, + "blocks_by_root complete outcome=%s roots=%zu first_root=%s entry_found=%s " + "consecutive_failures=%" PRIu32, outcome_text, + root_count, root_hex[0] ? root_hex : "0x0", entry_found ? "true" : "false", failure_count); } - lantern_client_clear_pending_parent_requested(client, request_root); + lantern_client_clear_pending_parent_requested_roots(client, request_roots, root_count); if (outcome == LANTERN_BLOCKS_REQUEST_SUCCESS && peer_id && peer_id[0] != '\0') { - lantern_client_request_pending_parent_after_blocks(client, peer_id, request_root); + lantern_client_request_pending_parent_after_blocks( + client, + peer_id, + first_root); lantern_client_request_status_after_blocks_success(client, peer_id); } } + +/** + * Handle completion of a blocks request (single root). + * + * @spec subspecs/networking/reqresp - Request lifecycle + * + * @param client Client instance + * @param peer_id Peer ID string + * @param request_root Root that was requested + * @param outcome Request outcome + * + * @note Thread safety: This function acquires status_lock and pending_lock + */ +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) +{ + if (!request_root) + { + lantern_client_on_blocks_request_complete_batch( + client, + peer_id, + NULL, + 0u, + outcome); + return; + } + lantern_client_on_blocks_request_complete_batch( + client, + peer_id, + request_root, + 1u, + outcome); +} diff --git a/src/core/client_reqresp_blocks.c b/src/core/client_reqresp_blocks.c index 7fb2ea3..9cdf6be 100644 --- a/src/core/client_reqresp_blocks.c +++ b/src/core/client_reqresp_blocks.c @@ -55,10 +55,12 @@ static bool lantern_client_process_stream_block_chunk( bool *saw_block); static void *block_request_worker(void *arg); static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int err); -static int schedule_blocks_request( +static int schedule_blocks_request_batch( struct lantern_client *client, const char *peer_id_text, - const LanternRoot *root); + const LanternRoot *roots, + const uint32_t *depths, + size_t root_count); /* ============================================================================ @@ -79,6 +81,8 @@ static void block_request_ctx_free(struct block_request_ctx *ctx) return; } peer_id_destroy(&ctx->peer_id); + free(ctx->roots); + free(ctx->depths); free(ctx); } @@ -191,18 +195,36 @@ static bool lantern_client_process_stream_block_chunk( char computed_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; format_root_hex(&computed, computed_hex, sizeof(computed_hex)); - bool matches = memcmp(computed.bytes, ctx->root.bytes, LANTERN_ROOT_SIZE) == 0; + bool matches = false; + uint32_t backfill_depth = 0; + if (ctx->roots && ctx->root_count > 0) + { + for (size_t i = 0; i < ctx->root_count; ++i) + { + if (memcmp(computed.bytes, ctx->roots[i].bytes, LANTERN_ROOT_SIZE) == 0) + { + matches = true; + if (ctx->depths) + { + backfill_depth = ctx->depths[i]; + } + break; + } + } + } bool quiet_log = ctx->client && ctx->client->sync_in_progress; if (quiet_log) { lantern_log_debug( "reqresp", meta, - "streamed block slot=%" PRIu64 " proposer=%" PRIu64 " root=%s match=%s attestations=%zu", + "streamed block slot=%" PRIu64 " proposer=%" PRIu64 " root=%s match=%s depth=%" PRIu32 + " attestations=%zu", streamed_block.message.block.slot, streamed_block.message.block.proposer_index, computed_hex[0] ? computed_hex : "0x0", matches ? "true" : "false", + backfill_depth, streamed_block.message.block.body.attestations.length); } else @@ -210,11 +232,13 @@ static bool lantern_client_process_stream_block_chunk( lantern_log_info( "reqresp", meta, - "streamed block slot=%" PRIu64 " proposer=%" PRIu64 " root=%s match=%s attestations=%zu", + "streamed block slot=%" PRIu64 " proposer=%" PRIu64 " root=%s match=%s depth=%" PRIu32 + " attestations=%zu", streamed_block.message.block.slot, streamed_block.message.block.proposer_index, computed_hex[0] ? computed_hex : "0x0", matches ? "true" : "false", + backfill_depth, streamed_block.message.block.body.attestations.length); } @@ -224,6 +248,7 @@ static bool lantern_client_process_stream_block_chunk( &computed, ctx->peer_text[0] ? ctx->peer_text : NULL, "reqresp", + backfill_depth, true); lantern_signed_block_with_attestation_reset(&streamed_block); if (saw_block) @@ -280,16 +305,25 @@ static void *block_request_worker(void *arg) }; char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(&ctx->root, root_hex, sizeof(root_hex)); + if (!ctx->roots || ctx->root_count == 0) + { + lantern_log_error( + "reqresp", + &meta, + "blocks_by_root request missing roots"); + goto cleanup; + } + format_root_hex(&ctx->roots[0], root_hex, sizeof(root_hex)); LanternBlocksByRootRequest request; lantern_blocks_by_root_request_init(&request); uint8_t *raw_request = NULL; uint8_t *payload = NULL; - bool request_success = false; + enum lantern_blocks_request_outcome outcome = LANTERN_BLOCKS_REQUEST_FAILED; + bool completed = false; - if (lantern_root_list_resize(&request.roots, 1) != 0) + if (lantern_root_list_resize(&request.roots, ctx->root_count) != 0) { lantern_log_error( "reqresp", @@ -297,7 +331,10 @@ static void *block_request_worker(void *arg) "failed to size blocks_by_root request"); goto cleanup; } - request.roots.items[0] = ctx->root; + for (size_t i = 0; i < ctx->root_count; ++i) + { + request.roots.items[i] = ctx->roots[i]; + } size_t raw_size = request.roots.length * LANTERN_ROOT_SIZE; raw_request = (uint8_t *)malloc(raw_size > 0 ? raw_size : 1u); @@ -415,8 +452,9 @@ static void *block_request_worker(void *arg) lantern_log_debug( "reqresp", &meta, - "sending %s request root=%s bytes=%zu", + "sending %s request roots=%zu first_root=%s bytes=%zu", ctx->protocol_id, + ctx->root_count, root_hex[0] ? root_hex : "0x0", payload_len); @@ -487,7 +525,8 @@ static void *block_request_worker(void *arg) goto cleanup; } } - request_success = saw_block; + completed = true; + outcome = saw_block ? LANTERN_BLOCKS_REQUEST_SUCCESS : LANTERN_BLOCKS_REQUEST_EMPTY; cleanup: free(payload); @@ -496,11 +535,12 @@ static void *block_request_worker(void *arg) libp2p_stream_free(stream); if (ctx->client) { - lantern_client_on_blocks_request_complete( + lantern_client_on_blocks_request_complete_batch( ctx->client, ctx->peer_text, - &ctx->root, - request_success ? LANTERN_BLOCKS_REQUEST_SUCCESS : LANTERN_BLOCKS_REQUEST_FAILED); + ctx->roots, + ctx->root_count, + completed ? outcome : LANTERN_BLOCKS_REQUEST_FAILED); } block_request_ctx_free(ctx); @@ -559,10 +599,11 @@ static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int err); if (ctx->client) { - lantern_client_on_blocks_request_complete( + lantern_client_on_blocks_request_complete_batch( ctx->client, ctx->peer_text, - &ctx->root, + ctx->roots, + ctx->root_count, LANTERN_BLOCKS_REQUEST_FAILED); } if (stream) @@ -584,10 +625,11 @@ static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int libp2p_stream_free(stream); if (ctx->client) { - lantern_client_on_blocks_request_complete( + lantern_client_on_blocks_request_complete_batch( ctx->client, ctx->peer_text, - &ctx->root, + ctx->roots, + ctx->root_count, LANTERN_BLOCKS_REQUEST_FAILED); } block_request_ctx_free(ctx); @@ -607,10 +649,11 @@ static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int libp2p_stream_free(stream); if (ctx->client) { - lantern_client_on_blocks_request_complete( + lantern_client_on_blocks_request_complete_batch( ctx->client, ctx->peer_text, - &ctx->root, + ctx->roots, + ctx->root_count, LANTERN_BLOCKS_REQUEST_FAILED); } block_request_ctx_free(ctx); @@ -629,12 +672,18 @@ static void block_request_on_open(libp2p_stream_t *stream, void *user_data, int * Public Block Request API * ============================================================================ */ -static int schedule_blocks_request( +static int schedule_blocks_request_batch( struct lantern_client *client, const char *peer_id_text, - const LanternRoot *root) + const LanternRoot *roots, + const uint32_t *depths, + size_t root_count) { - if (!client || !peer_id_text || !root) + if (!client || !peer_id_text || !roots || root_count == 0) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + if (root_count > LANTERN_MAX_BLOCKS_PER_REQUEST) { return LANTERN_CLIENT_ERR_INVALID_PARAM; } @@ -642,9 +691,12 @@ static int schedule_blocks_request( { return LANTERN_CLIENT_ERR_NETWORK; } - if (lantern_root_is_zero(root)) + for (size_t i = 0; i < root_count; ++i) { - return LANTERN_CLIENT_ERR_INVALID_PARAM; + if (lantern_root_is_zero(&roots[i])) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } } if (client->debug_disable_block_requests) @@ -655,10 +707,11 @@ static int schedule_blocks_request( .validator = client->node_id, .peer = peer_id_text}, "skipping blocks_by_root dial for test run"); - lantern_client_on_blocks_request_complete( + lantern_client_on_blocks_request_complete_batch( client, peer_id_text, - root, + roots, + root_count, LANTERN_BLOCKS_REQUEST_ABORTED); return 0; } @@ -668,12 +721,34 @@ static int schedule_blocks_request( { return LANTERN_CLIENT_ERR_ALLOC; } + ctx->roots = (LanternRoot *)calloc(root_count, sizeof(*ctx->roots)); + if (!ctx->roots) + { + block_request_ctx_free(ctx); + return LANTERN_CLIENT_ERR_ALLOC; + } + ctx->depths = (uint32_t *)calloc(root_count, sizeof(*ctx->depths)); + if (!ctx->depths) + { + block_request_ctx_free(ctx); + return LANTERN_CLIENT_ERR_ALLOC; + } + ctx->client = client; - ctx->root = *root; + ctx->root_count = root_count; ctx->protocol_id = LANTERN_BLOCKS_BY_ROOT_PROTOCOL_ID; strncpy(ctx->peer_text, peer_id_text, sizeof(ctx->peer_text) - 1); ctx->peer_text[sizeof(ctx->peer_text) - 1] = '\0'; + for (size_t i = 0; i < root_count; ++i) + { + ctx->roots[i] = roots[i]; + if (depths) + { + ctx->depths[i] = depths[i]; + } + } + if (peer_id_create_from_string(peer_id_text, &ctx->peer_id) != PEER_ID_SUCCESS) { lantern_log_warn( @@ -687,14 +762,15 @@ static int schedule_blocks_request( } char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(root, root_hex, sizeof(root_hex)); + format_root_hex(&roots[0], root_hex, sizeof(root_hex)); lantern_log_debug( "reqresp", &(const struct lantern_log_metadata){ .validator = client->node_id, .peer = ctx->peer_text[0] ? ctx->peer_text : NULL}, - "dialing peer for %s root=%s", + "dialing peer for %s roots=%zu first_root=%s", ctx->protocol_id, + root_count, root_hex[0] ? root_hex : "0x0"); int rc = libp2p_host_open_stream_async( @@ -729,9 +805,37 @@ static int schedule_blocks_request( * * @param client Client instance * @param peer_id_text Peer ID string - * @param root Block root to request + * @param roots Block roots to request + * @param depths Backfill depth per root (may be NULL for zeros) + * @param root_count Number of roots + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if parameters are invalid, the peer ID is invalid, or any 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_batch( + struct lantern_client *client, + const char *peer_id_text, + const LanternRoot *roots, + const uint32_t *depths, + size_t root_count) +{ + return schedule_blocks_request_batch(client, peer_id_text, roots, depths, root_count); +} + +/** + * Schedule a single-root 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 backfill_depth Backfill depth for the requested root * @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_INVALID_PARAM if parameters are invalid, 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 * @@ -740,7 +844,13 @@ static int schedule_blocks_request( int lantern_client_schedule_blocks_request( struct lantern_client *client, const char *peer_id_text, - const LanternRoot *root) + const LanternRoot *root, + uint32_t backfill_depth) { - return schedule_blocks_request(client, peer_id_text, root); + if (!root) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + const uint32_t depth = backfill_depth; + return schedule_blocks_request_batch(client, peer_id_text, root, &depth, 1u); } diff --git a/src/core/client_services_internal.h b/src/core/client_services_internal.h index 4d25b36..c14a037 100644 --- a/src/core/client_services_internal.h +++ b/src/core/client_services_internal.h @@ -348,6 +348,18 @@ int http_set_validator_status_cb(void *context, uint64_t global_index, bool enab */ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snapshot); +/** + * Get finalized state SSZ bytes for checkpoint sync. + * + * @param context Client instance + * @param out_bytes Output buffer pointer (caller owns and must free) + * @param out_len Output byte length + * @return 0 on success, negative on failure + * + * @note Thread safety: This function may acquire state_lock + */ +int http_finalized_state_ssz_cb(void *context, uint8_t **out_bytes, size_t *out_len); + /* ============================================================================ * Reqresp Callback Functions @@ -424,6 +436,26 @@ int reqresp_collect_blocks( * * @param client Client instance * @param peer_id Peer ID string + * @param request_roots Roots that were requested + * @param root_count Number of requested roots + * @param outcome Request outcome + * + * @note Thread safety: This function acquires status_lock and pending_lock + */ +void lantern_client_on_blocks_request_complete_batch( + struct lantern_client *client, + const char *peer_id, + const LanternRoot *request_roots, + size_t root_count, + enum lantern_blocks_request_outcome outcome); + +/** + * Handle completion of a blocks request (single root). + * + * @spec subspecs/networking/reqresp.py - blocks by root + * + * @param client Client instance + * @param peer_id Peer ID string * @param request_root Root that was requested * @param outcome Request outcome * @@ -475,11 +507,36 @@ int lantern_reqresp_read_response_chunk( * * @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 client Client instance + * @param peer_id_text Peer ID string + * @param roots Block roots to request + * @param depths Backfill depth per root (may be NULL for zeros) + * @param root_count Number of roots + * @return 0 on success + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if parameters are invalid, the peer ID is invalid, or any 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_batch( + struct lantern_client *client, + const char *peer_id_text, + const LanternRoot *roots, + const uint32_t *depths, + size_t root_count); + +/** + * Schedule a single-root 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 backfill_depth Backfill depth for the requested root * @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_INVALID_PARAM if parameters are invalid, 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 * @@ -488,7 +545,8 @@ int lantern_reqresp_read_response_chunk( int lantern_client_schedule_blocks_request( struct lantern_client *client, const char *peer_id_text, - const LanternRoot *root); + const LanternRoot *root, + uint32_t backfill_depth); /** diff --git a/src/core/client_sync.c b/src/core/client_sync.c index b567cdd..2f98244 100644 --- a/src/core/client_sync.c +++ b/src/core/client_sync.c @@ -18,7 +18,6 @@ #include "client_internal.h" #include -#include #include #include @@ -171,6 +170,16 @@ static const char *peer_id_to_text(const peer_id_t *from, char *out, size_t out_ } +static bool client_accepts_gossip(const struct lantern_client *client) +{ + if (!client) + { + return false; + } + return client->sync_state != LANTERN_SYNC_STATE_IDLE; +} + + /** * Handle a block received via gossip. * @@ -185,6 +194,7 @@ static const char *peer_id_to_text(const peer_id_t *from, char *out, size_t out_ * @param context Client instance * @return 0 on success * @return LANTERN_CLIENT_ERR_INVALID_PARAM if block or context is NULL + * @return LANTERN_CLIENT_ERR_IGNORED if gossip is ignored due to IDLE sync state * * @note Thread safety: This function is thread-safe */ @@ -202,7 +212,19 @@ int gossip_block_handler( 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_id_text, "gossip", false); + if (!client_accepts_gossip(client)) + { + lantern_log_debug( + "gossip", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text && *peer_id_text ? peer_id_text : NULL, + }, + "ignored block gossip in IDLE sync state"); + return LANTERN_CLIENT_ERR_IGNORED; + } + + lantern_client_record_block(client, block, NULL, peer_id_text, "gossip", 0, false); return LANTERN_CLIENT_OK; } @@ -221,6 +243,7 @@ int gossip_block_handler( * @param context Client instance * @return 0 on success * @return LANTERN_CLIENT_ERR_INVALID_PARAM if vote or context is NULL + * @return LANTERN_CLIENT_ERR_IGNORED if gossip is ignored due to IDLE sync state * * @note Thread safety: This function is thread-safe */ @@ -238,6 +261,18 @@ int gossip_vote_handler( char peer_text[PEER_TEXT_BUFFER_LEN]; const char *peer_id_text = peer_id_to_text(from, peer_text, sizeof(peer_text)); + if (!client_accepts_gossip(client)) + { + lantern_log_debug( + "gossip", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text && *peer_id_text ? peer_id_text : NULL, + }, + "ignored vote gossip in IDLE sync state"); + return LANTERN_CLIENT_ERR_IGNORED; + } + lantern_client_note_vote_delivery(client, peer_id_text, vote); lantern_client_record_vote(client, vote, peer_id_text); return LANTERN_CLIENT_OK; @@ -412,9 +447,8 @@ static void log_genesis_anchor_roots( * Initializes the fork choice store from the genesis state: * 1. Configures fork choice with consensus parameters * 2. Computes anchor block header with actual state_root (not zero) - * 3. Sets fork choice anchor with justified/finalized checkpoints - * 4. Updates state checkpoint roots to match anchor - * 5. Persists anchor block to storage + * 3. Sets fork choice anchor with anchor checkpoints + * 4. Persists anchor block to storage * * According to leanSpec's Store.get_forkchoice_store, the anchor block * used for fork choice MUST have state_root = hash_tree_root(state). @@ -500,11 +534,15 @@ int initialize_fork_choice(struct lantern_client *client) anchor.state_root = anchor_state_root; lantern_block_body_init(&anchor.body); + LanternCheckpoint anchor_checkpoint; + anchor_checkpoint.root = anchor_root; + anchor_checkpoint.slot = anchor.slot; + if (lantern_fork_choice_set_anchor( &client->fork_choice, &anchor, - &client->state.latest_justified, - &client->state.latest_finalized, + &anchor_checkpoint, + &anchor_checkpoint, &anchor_root) != 0) { @@ -515,31 +553,21 @@ int initialize_fork_choice(struct lantern_client *client) "failed to set fork choice anchor"); return LANTERN_CLIENT_ERR_RUNTIME; } - 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", - &meta, - "updated justified checkpoint root to anchor"); - } - if (memcmp( - client->state.latest_finalized.root.bytes, - anchor_root.bytes, - LANTERN_ROOT_SIZE) - != 0) + persist_anchor_block(client, &anchor, &anchor_root); + if (client->data_dir) { - client->state.latest_finalized.root = anchor_root; - lantern_log_debug( - "forkchoice", - &meta, - "updated finalized checkpoint root to anchor"); + if (lantern_storage_store_state_for_root( + client->data_dir, + &anchor_root, + &client->state) + != 0) + { + lantern_log_warn( + "storage", + &meta, + "failed to persist anchor state"); + } } - 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; @@ -1003,9 +1031,9 @@ void lantern_client_pending_remove_by_root(struct lantern_client *client, const * @spec subspecs/forkchoice/store.py - Orphan block handling * * Queues a block whose parent is not yet known. The block will be - * imported once its parent arrives via gossip. Does NOT immediately - * request the parent via reqresp - relies on gossip for normal block - * propagation. + * imported once its parent arrives. When the client is syncing or + * synced, it requests the missing parent via reqresp. In IDLE, it + * relies on gossip for normal block propagation. * * @param client Client instance * @param block Block to enqueue @@ -1015,13 +1043,18 @@ void lantern_client_pending_remove_by_root(struct lantern_client *client, const * * @note Thread safety: Acquires pending_lock */ -static bool try_schedule_blocks_request( +static bool try_schedule_blocks_request_batch( struct lantern_client *client, const char *peer_text, - const LanternRoot *root, - bool ignore_backoff) + const LanternRoot *roots, + const uint32_t *depths, + size_t root_count) { - if (!client || !peer_text || !peer_text[0] || !root || lantern_root_is_zero(root)) + if (!client || !roots || root_count == 0) + { + return false; + } + if (root_count > LANTERN_MAX_BLOCKS_PER_REQUEST) { return false; } @@ -1030,16 +1063,40 @@ static bool try_schedule_blocks_request( return false; } - bool state_locked = lantern_client_lock_state(client); - bool known = false; - if (state_locked) + uint32_t min_depth = UINT32_MAX; + uint32_t max_depth = 0; + if (depths) + { + for (size_t i = 0; i < root_count; ++i) + { + if (depths[i] < min_depth) + { + min_depth = depths[i]; + } + if (depths[i] > max_depth) + { + max_depth = depths[i]; + } + } + } + + for (size_t i = 0; i < root_count; ++i) { - known = lantern_client_block_known_locked(client, root, NULL); + if (lantern_root_is_zero(&roots[i])) + { + return false; + } + if (depths && depths[i] > LANTERN_MAX_BACKFILL_DEPTH) + { + return false; + } } - lantern_client_unlock_state(client, state_locked); - if (known) + + char first_root_hex[ROOT_HEX_BUFFER_LEN]; + first_root_hex[0] = '\0'; + if (root_count > 0) { - return false; + format_root_hex(&roots[0], first_root_hex, sizeof(first_root_hex)); } if (pthread_mutex_lock(&client->status_lock) != 0) @@ -1047,48 +1104,185 @@ static bool try_schedule_blocks_request( return false; } - struct lantern_peer_status_entry *entry = - lantern_client_ensure_status_entry_locked(client, peer_text); - if (!entry) + uint64_t now_ms = monotonic_millis(); + struct lantern_peer_status_entry *entry = NULL; + const size_t peer_cap = sizeof(((struct lantern_peer_status_entry *)0)->peer_id); + char selected_peer[PEER_TEXT_BUFFER_LEN]; + selected_peer[0] = '\0'; + + if (peer_text && peer_text[0]) { - pthread_mutex_unlock(&client->status_lock); - return false; + entry = lantern_client_ensure_status_entry_locked(client, peer_text); + if (entry) + { + if (!lantern_client_is_peer_connected(client, peer_text) + || entry->blocks_requests_inflight >= LANTERN_MAX_BLOCKS_REQUESTS_PER_PEER) + { + entry = NULL; + } + } } - if (entry->requested_head) + struct lantern_peer_status_entry *best_entry = entry; + bool best_fresh = false; + uint32_t best_inflight = entry ? entry->blocks_requests_inflight : 0; + uint32_t best_failures = entry ? entry->consecutive_blocks_failures : 0; + uint64_t best_status_ms = entry ? entry->last_status_ms : 0; + if (entry && entry->has_status + && entry->last_status_ms != 0 + && now_ms >= entry->last_status_ms) { - pthread_mutex_unlock(&client->status_lock); - return false; + uint64_t age_ms = now_ms - entry->last_status_ms; + if (age_ms <= LANTERN_PEER_STATUS_STALE_MS) + { + best_fresh = true; + } } - uint64_t now_ms = monotonic_millis(); - if (!ignore_backoff) + for (size_t i = 0; i < client->peer_status_count; ++i) { - 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) + struct lantern_peer_status_entry *candidate = &client->peer_status_entries[i]; + if (!candidate->peer_id[0]) { - backoff_ms = LANTERN_BLOCKS_REQUEST_MIN_POLL_MS; + continue; + } + if (best_entry && candidate == best_entry) + { + continue; + } + if (candidate->blocks_requests_inflight >= LANTERN_MAX_BLOCKS_REQUESTS_PER_PEER) + { + continue; + } + if (!lantern_client_is_peer_connected(client, candidate->peer_id)) + { + continue; } - if (entry->last_blocks_request_ms != 0 - && now_ms < entry->last_blocks_request_ms + backoff_ms) + bool fresh = false; + if (candidate->has_status + && candidate->last_status_ms != 0 + && now_ms >= candidate->last_status_ms) { - pthread_mutex_unlock(&client->status_lock); - return false; + uint64_t age_ms = now_ms - candidate->last_status_ms; + if (age_ms <= LANTERN_PEER_STATUS_STALE_MS) + { + fresh = true; + } + } + + if (!best_entry) + { + best_entry = candidate; + best_fresh = fresh; + best_inflight = candidate->blocks_requests_inflight; + best_failures = candidate->consecutive_blocks_failures; + best_status_ms = candidate->last_status_ms; + continue; } + + if (candidate->consecutive_blocks_failures < best_failures) + { + best_entry = candidate; + best_fresh = fresh; + best_inflight = candidate->blocks_requests_inflight; + best_failures = candidate->consecutive_blocks_failures; + best_status_ms = candidate->last_status_ms; + continue; + } + if (candidate->consecutive_blocks_failures > best_failures) + { + continue; + } + + if (fresh && !best_fresh) + { + best_entry = candidate; + best_fresh = true; + best_inflight = candidate->blocks_requests_inflight; + best_failures = candidate->consecutive_blocks_failures; + best_status_ms = candidate->last_status_ms; + continue; + } + if (fresh == best_fresh) + { + if (candidate->blocks_requests_inflight < best_inflight) + { + best_entry = candidate; + best_inflight = candidate->blocks_requests_inflight; + best_failures = candidate->consecutive_blocks_failures; + best_status_ms = candidate->last_status_ms; + continue; + } + if (candidate->blocks_requests_inflight == best_inflight) + { + if (candidate->last_status_ms > best_status_ms) + { + best_entry = candidate; + best_status_ms = candidate->last_status_ms; + continue; + } + } + } + } + + entry = best_entry; + if (!entry) + { + lantern_log_info( + "reqresp", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "blocks_by_root request skipped: no eligible peers roots=%zu connected=%zu status_entries=%zu", + root_count, + client->connected_peers, + client->peer_status_count); + pthread_mutex_unlock(&client->status_lock); + return false; } - entry->requested_head = true; + size_t copy_cap = sizeof(selected_peer); + if (peer_cap < copy_cap) + { + copy_cap = peer_cap; + } + strncpy(selected_peer, entry->peer_id, copy_cap - 1u); + selected_peer[copy_cap - 1u] = '\0'; + + entry->blocks_requests_inflight += 1u; entry->last_blocks_request_ms = now_ms; pthread_mutex_unlock(&client->status_lock); - if (lantern_client_schedule_blocks_request(client, peer_text, root) != 0) + lantern_log_debug( + "reqresp", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = selected_peer[0] ? selected_peer : NULL}, + "blocks_by_root scheduling roots=%zu first_root=%s depth_min=%" PRIu32 " depth_max=%" PRIu32, + root_count, + first_root_hex[0] ? first_root_hex : "0x0", + min_depth == UINT32_MAX ? 0u : min_depth, + max_depth); + + if (lantern_client_schedule_blocks_request_batch( + client, + selected_peer, + roots, + depths, + root_count) + != 0) { - lantern_client_on_blocks_request_complete( + lantern_log_warn( + "reqresp", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = selected_peer}, + "blocks_by_root request scheduling failed roots=%zu", + root_count); + lantern_client_on_blocks_request_complete_batch( client, - peer_text, - root, + selected_peer, + roots, + root_count, LANTERN_BLOCKS_REQUEST_ABORTED); return false; } @@ -1096,12 +1290,40 @@ static bool try_schedule_blocks_request( return true; } +static bool try_schedule_blocks_request( + struct lantern_client *client, + const char *peer_text, + const LanternRoot *root, + uint32_t backfill_depth) +{ + if (!client || !root || lantern_root_is_zero(root)) + { + return false; + } + if (backfill_depth > LANTERN_MAX_BACKFILL_DEPTH) + { + return false; + } + + const char *preferred_peer = NULL; + if (peer_text && peer_text[0]) + { + preferred_peer = peer_text; + } + return try_schedule_blocks_request_batch( + client, + preferred_peer, + root, + &backfill_depth, + 1u); +} + static void mark_pending_parent_requested( struct lantern_client *client, - const LanternRoot *child_root, + const LanternRoot *parent_root, bool requested) { - if (!client || !child_root) + if (!client || !parent_root) { return; } @@ -1115,10 +1337,9 @@ static void mark_pending_parent_requested( for (size_t i = 0; i < client->pending_blocks.length; ++i) { struct lantern_pending_block *entry = &client->pending_blocks.items[i]; - if (memcmp(entry->root.bytes, child_root->bytes, LANTERN_ROOT_SIZE) == 0) + if (memcmp(entry->parent_root.bytes, parent_root->bytes, LANTERN_ROOT_SIZE) == 0) { entry->parent_requested = requested; - break; } } @@ -1129,14 +1350,40 @@ struct pending_parent_candidate { LanternRoot child_root; LanternRoot parent_root; + uint32_t request_depth; + bool parent_cached; }; +struct pending_child_replay +{ + LanternSignedBlock block; + LanternRoot root; + char peer_text[PEER_TEXT_BUFFER_LEN]; + uint64_t slot; + uint32_t backfill_depth; +}; + +static int pending_child_replay_compare(const void *left, const void *right) +{ + const struct pending_child_replay *left_entry = left; + const struct pending_child_replay *right_entry = right; + if (left_entry->slot < right_entry->slot) + { + return -1; + } + if (left_entry->slot > right_entry->slot) + { + return 1; + } + return memcmp(left_entry->root.bytes, right_entry->root.bytes, LANTERN_ROOT_SIZE); +} + void lantern_client_request_pending_parent_after_blocks( struct lantern_client *client, const char *peer_text, const LanternRoot *request_root) { - if (!client || !peer_text || !peer_text[0]) + if (!client) { return; } @@ -1146,6 +1393,7 @@ void lantern_client_request_pending_parent_after_blocks( LanternRoot requested_root = {0}; bool has_requested_root = false; bool prefer_requested_root = false; + bool requested_parent_requested = false; struct pending_parent_candidate requested_candidate = {0}; if (request_root && !lantern_root_is_zero(request_root)) { @@ -1172,9 +1420,22 @@ void lantern_client_request_pending_parent_after_blocks( { break; } + if (entry->backfill_depth >= LANTERN_MAX_BACKFILL_DEPTH) + { + break; + } requested_candidate.child_root = entry->root; requested_candidate.parent_root = entry->parent_root; - prefer_requested_root = true; + requested_candidate.request_depth = entry->backfill_depth + 1u; + requested_candidate.parent_cached = pending_block_list_find( + &client->pending_blocks, + &entry->parent_root) + != NULL; + requested_parent_requested = entry->parent_requested; + if (!requested_candidate.parent_cached && !requested_parent_requested) + { + prefer_requested_root = true; + } break; } } @@ -1190,8 +1451,15 @@ void lantern_client_request_pending_parent_after_blocks( { continue; } - if (peer_text && *peer_text && entry->peer_text[0] != '\0' - && strcmp(entry->peer_text, peer_text) != 0) + if (entry->backfill_depth >= LANTERN_MAX_BACKFILL_DEPTH) + { + continue; + } + bool parent_cached = pending_block_list_find( + &client->pending_blocks, + &entry->parent_root) + != NULL; + if (parent_cached) { continue; } @@ -1206,11 +1474,28 @@ void lantern_client_request_pending_parent_after_blocks( } candidates[candidate_count].child_root = entry->root; candidates[candidate_count].parent_root = entry->parent_root; + candidates[candidate_count].request_depth = entry->backfill_depth + 1u; + candidates[candidate_count].parent_cached = parent_cached; candidate_count += 1u; } lantern_client_unlock_pending(client, locked); + char requested_hex[ROOT_HEX_BUFFER_LEN]; + requested_hex[0] = '\0'; + if (has_requested_root) + { + format_root_hex(&requested_root, requested_hex, sizeof(requested_hex)); + } + lantern_log_debug( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pending parent scan requested_root=%s candidates=%zu prefer_requested=%s requested_parent=%s", + has_requested_root ? (requested_hex[0] ? requested_hex : "0x0") : "-", + candidate_count, + prefer_requested_root ? "true" : "false", + requested_parent_requested ? "true" : "false"); + if (candidate_count == 0 && peer_text && *peer_text) { locked = lantern_client_lock_pending(client); @@ -1229,6 +1514,18 @@ void lantern_client_request_pending_parent_after_blocks( { continue; } + if (entry->backfill_depth >= LANTERN_MAX_BACKFILL_DEPTH) + { + continue; + } + bool parent_cached = pending_block_list_find( + &client->pending_blocks, + &entry->parent_root) + != NULL; + if (parent_cached) + { + continue; + } if (has_requested_root && memcmp(entry->parent_root.bytes, requested_root.bytes, LANTERN_ROOT_SIZE) == 0) { @@ -1240,43 +1537,87 @@ void lantern_client_request_pending_parent_after_blocks( } candidates[candidate_count].child_root = entry->root; candidates[candidate_count].parent_root = entry->parent_root; + candidates[candidate_count].request_depth = entry->backfill_depth + 1u; + candidates[candidate_count].parent_cached = parent_cached; candidate_count += 1u; } lantern_client_unlock_pending(client, locked); } + LanternRoot request_roots[LANTERN_MAX_BLOCKS_PER_REQUEST]; + uint32_t request_depths[LANTERN_MAX_BLOCKS_PER_REQUEST]; + size_t request_count = 0; + if (prefer_requested_root) { - if (try_schedule_blocks_request( - client, - peer_text, - &requested_candidate.parent_root, - true)) + if (!requested_candidate.parent_cached + && !requested_parent_requested + && !lantern_root_is_zero(&requested_candidate.parent_root)) { - char parent_hex[ROOT_HEX_BUFFER_LEN]; - char child_hex[ROOT_HEX_BUFFER_LEN]; - format_root_hex(&requested_candidate.parent_root, parent_hex, sizeof(parent_hex)); - format_root_hex(&requested_candidate.child_root, child_hex, sizeof(child_hex)); - mark_pending_parent_requested(client, &requested_candidate.child_root, true); + request_roots[request_count] = requested_candidate.parent_root; + request_depths[request_count] = requested_candidate.request_depth; + request_count += 1u; } - return; } for (size_t i = 0; i < candidate_count; ++i) { - if (try_schedule_blocks_request( - client, - peer_text, - &candidates[i].parent_root, - true)) + if (request_count >= LANTERN_MAX_BLOCKS_PER_REQUEST) { - char parent_hex[ROOT_HEX_BUFFER_LEN]; - char child_hex[ROOT_HEX_BUFFER_LEN]; - format_root_hex(&candidates[i].parent_root, parent_hex, sizeof(parent_hex)); - format_root_hex(&candidates[i].child_root, child_hex, sizeof(child_hex)); - mark_pending_parent_requested(client, &candidates[i].child_root, true); break; } + bool duplicate = false; + for (size_t j = 0; j < request_count; ++j) + { + if (memcmp( + request_roots[j].bytes, + candidates[i].parent_root.bytes, + LANTERN_ROOT_SIZE) + == 0) + { + duplicate = true; + break; + } + } + if (duplicate) + { + continue; + } + if (candidates[i].parent_cached) + { + continue; + } + request_roots[request_count] = candidates[i].parent_root; + request_depths[request_count] = candidates[i].request_depth; + request_count += 1u; + } + + if (request_count == 0) + { + lantern_log_debug( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pending parent request skipped candidates=%zu", + candidate_count); + return; + } + + if (try_schedule_blocks_request_batch( + client, + NULL, + request_roots, + request_depths, + request_count)) + { + for (size_t i = 0; i < request_count; ++i) + { + mark_pending_parent_requested(client, &request_roots[i], true); + } + lantern_log_debug( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pending parent request scheduled count=%zu", + request_count); } } @@ -1286,6 +1627,7 @@ void lantern_client_enqueue_pending_block( const LanternRoot *block_root, const LanternRoot *parent_root, const char *peer_text, + uint32_t backfill_depth, bool request_parent) { if (!client || !block || !block_root || !parent_root) @@ -1303,21 +1645,32 @@ void lantern_client_enqueue_pending_block( } struct lantern_pending_block_list *list = &client->pending_blocks; + bool request_parent_now = + request_parent || client->sync_state != LANTERN_SYNC_STATE_IDLE; + bool allow_parent_requests = client->sync_state != LANTERN_SYNC_STATE_IDLE; + if (backfill_depth > LANTERN_MAX_BACKFILL_DEPTH) + { + backfill_depth = LANTERN_MAX_BACKFILL_DEPTH; + } struct lantern_pending_block *existing = pending_block_list_find(list, &block_root_local); if (existing) { - bool should_request = request_parent && !existing->parent_requested - && peer_text && *peer_text; + bool should_request = + allow_parent_requests && request_parent_now && !existing->parent_requested; LanternRoot request_root = existing->parent_root; - LanternRoot child_root = existing->root; + bool parent_cached = pending_block_list_find(list, &request_root) != NULL; char peer_copy[PEER_TEXT_BUFFER_LEN]; peer_copy[0] = '\0'; - if (should_request) + if (peer_text && *peer_text) { strncpy(peer_copy, peer_text, sizeof(peer_copy) - 1u); peer_copy[sizeof(peer_copy) - 1u] = '\0'; } + if (backfill_depth < existing->backfill_depth) + { + existing->backfill_depth = backfill_depth; + } if (peer_text && *peer_text) { if (existing->peer_text[0] == '\0' || strcmp(existing->peer_text, peer_text) != 0) @@ -1326,12 +1679,39 @@ void lantern_client_enqueue_pending_block( existing->peer_text[sizeof(existing->peer_text) - 1u] = '\0'; } } + existing->received_ms = monotonic_millis(); + size_t pending_len = list->length; + bool parent_requested = existing->parent_requested; lantern_client_unlock_pending(client, locked); - if (should_request) + char root_hex[ROOT_HEX_BUFFER_LEN]; + char parent_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&block_root_local, root_hex, sizeof(root_hex)); + format_root_hex(&request_root, parent_hex, sizeof(parent_hex)); + lantern_log_debug( + "sync", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_text && *peer_text ? peer_text : NULL}, + "pending update root=%s parent=%s depth=%" PRIu32 " pending=%zu parent_cached=%s " + "parent_requested=%s should_request=%s", + root_hex[0] ? root_hex : "0x0", + parent_hex[0] ? parent_hex : "0x0", + existing->backfill_depth, + pending_len, + parent_cached ? "true" : "false", + parent_requested ? "true" : "false", + should_request ? "true" : "false"); + if (should_request && !parent_cached + && existing->backfill_depth < LANTERN_MAX_BACKFILL_DEPTH) { - if (try_schedule_blocks_request(client, peer_copy, &request_root, false)) + uint32_t request_depth = existing->backfill_depth + 1u; + if (try_schedule_blocks_request( + client, + peer_copy[0] ? peer_copy : NULL, + &request_root, + request_depth)) { - mark_pending_parent_requested(client, &child_root, true); + mark_pending_parent_requested(client, &request_root, true); } } return; @@ -1349,8 +1729,13 @@ void lantern_client_enqueue_pending_block( pending_block_list_remove(list, 0); } - struct lantern_pending_block *entry = - pending_block_list_append(list, block, &block_root_local, &parent_root_local, peer_text); + struct lantern_pending_block *entry = pending_block_list_append( + list, + block, + &block_root_local, + &parent_root_local, + peer_text, + backfill_depth); if (!entry) { lantern_client_unlock_pending(client, locked); @@ -1373,6 +1758,8 @@ void lantern_client_enqueue_pending_block( }; entry->parent_requested = false; + bool parent_cached = pending_block_list_find(list, &parent_root_local) != NULL; + size_t pending_len = list->length; lantern_client_unlock_pending(client, locked); @@ -1397,16 +1784,38 @@ void lantern_client_enqueue_pending_block( parent_hex[0] ? parent_hex : "0x0"); } - if (request_parent && peer_text && *peer_text) + if (allow_parent_requests && request_parent_now && !parent_cached + && entry->backfill_depth < LANTERN_MAX_BACKFILL_DEPTH) { char peer_copy[PEER_TEXT_BUFFER_LEN]; - strncpy(peer_copy, peer_text, sizeof(peer_copy) - 1u); - peer_copy[sizeof(peer_copy) - 1u] = '\0'; - if (try_schedule_blocks_request(client, peer_copy, &parent_root_local, false)) + peer_copy[0] = '\0'; + if (peer_text && *peer_text) + { + strncpy(peer_copy, peer_text, sizeof(peer_copy) - 1u); + peer_copy[sizeof(peer_copy) - 1u] = '\0'; + } + uint32_t request_depth = entry->backfill_depth + 1u; + if (try_schedule_blocks_request( + client, + peer_copy[0] ? peer_copy : NULL, + &parent_root_local, + request_depth)) { - mark_pending_parent_requested(client, &block_root_local, true); + mark_pending_parent_requested(client, &parent_root_local, true); } } + + lantern_log_debug( + "sync", + &meta, + "pending enqueue root=%s parent=%s depth=%" PRIu32 " pending=%zu parent_cached=%s " + "request_parent=%s", + block_hex[0] ? block_hex : "0x0", + parent_hex[0] ? parent_hex : "0x0", + entry->backfill_depth, + pending_len, + parent_cached ? "true" : "false", + (allow_parent_requests && request_parent_now) ? "true" : "false"); } @@ -1434,25 +1843,45 @@ void lantern_client_process_pending_children( } while (true) { - LanternSignedBlock replay; - LanternRoot child_root; - char peer_copy[PEER_TEXT_BUFFER_LEN]; - bool have_replay = false; - bool locked = lantern_client_lock_pending(client); if (!locked) { return; } + size_t pending_count = 0; 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, parent_root->bytes, LANTERN_ROOT_SIZE) == 0) + { + pending_count += 1u; + } + } + + if (pending_count == 0) + { + lantern_client_unlock_pending(client, locked); + break; + } + + struct pending_child_replay *replays = + calloc(pending_count, sizeof(*replays)); + if (!replays) + { + lantern_client_unlock_pending(client, locked); + return; + } + + size_t replay_count = 0; + for (size_t i = client->pending_blocks.length; i-- > 0;) { struct lantern_pending_block *entry = &client->pending_blocks.items[i]; if (memcmp(entry->parent_root.bytes, parent_root->bytes, LANTERN_ROOT_SIZE) != 0) { continue; } - if (clone_signed_block(&entry->block, &replay) != 0) + if (clone_signed_block(&entry->block, &replays[replay_count].block) != 0) { lantern_log_warn( "state", @@ -1461,31 +1890,69 @@ void lantern_client_process_pending_children( } else { - child_root = entry->root; - peer_copy[0] = '\0'; + replays[replay_count].root = entry->root; + replays[replay_count].slot = entry->block.message.block.slot; + replays[replay_count].backfill_depth = entry->backfill_depth; + replays[replay_count].peer_text[0] = '\0'; if (entry->peer_text[0]) { - strncpy(peer_copy, entry->peer_text, sizeof(peer_copy) - 1u); - peer_copy[sizeof(peer_copy) - 1u] = '\0'; + strncpy( + replays[replay_count].peer_text, + entry->peer_text, + sizeof(replays[replay_count].peer_text) - 1u); + replays[replay_count] + .peer_text[sizeof(replays[replay_count].peer_text) - 1u] = '\0'; } - have_replay = true; + replay_count += 1u; } - pending_block_list_remove(&client->pending_blocks, i); - break; } lantern_client_unlock_pending(client, locked); - if (!have_replay) + if (replay_count == 0) { - break; + free(replays); + continue; } - struct lantern_log_metadata meta = { - .validator = client->node_id, - .peer = peer_copy[0] ? peer_copy : NULL, - }; - (void)lantern_client_import_block(client, &replay, &child_root, &meta, true); - lantern_signed_block_with_attestation_reset(&replay); + qsort(replays, replay_count, sizeof(*replays), pending_child_replay_compare); + + size_t imported_count = 0; + for (size_t i = 0; i < replay_count; ++i) + { + struct lantern_log_metadata meta = { + .validator = client->node_id, + .peer = replays[i].peer_text[0] ? replays[i].peer_text : NULL, + }; + bool imported = lantern_client_import_block( + client, + &replays[i].block, + &replays[i].root, + &meta, + replays[i].backfill_depth, + true); + if (imported) + { + imported_count += 1u; + } + lantern_signed_block_with_attestation_reset(&replays[i].block); + } + free(replays); + + char parent_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(parent_root, parent_hex, sizeof(parent_hex)); + lantern_log_debug( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pending children processed parent=%s pending=%zu replayed=%zu imported=%zu", + parent_hex[0] ? parent_hex : "0x0", + pending_count, + replay_count, + imported_count); + + if (imported_count == 0) + { + break; + } } } diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index b60d06c..66030c7 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -27,6 +27,7 @@ #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" +#include "lantern/consensus/ssz.h" #include "lantern/consensus/state.h" #include "lantern/storage/storage.h" #include "lantern/support/log.h" @@ -42,6 +43,68 @@ enum ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, }; +/* ============================================================================ + * Sync Progress Helpers + * ============================================================================ */ + +static void update_sync_progress_after_block(struct lantern_client *client) +{ + if (!client) + { + return; + } + + uint64_t local_slot = 0; + bool state_locked = lantern_client_lock_state(client); + if (state_locked) + { + local_slot = client->state.latest_block_header.slot; + if (client->has_fork_choice) + { + LanternRoot fork_head = {0}; + if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head) == 0) + { + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + &fork_head, + &fork_slot, + NULL, + NULL) + == 0) + { + local_slot = fork_slot; + } + } + } + } + else if (client->has_state) + { + local_slot = client->state.latest_block_header.slot; + if (client->has_fork_choice) + { + LanternRoot fork_head = {0}; + if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head) == 0) + { + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + &fork_head, + &fork_slot, + NULL, + NULL) + == 0) + { + local_slot = fork_slot; + } + } + } + } + lantern_client_unlock_state(client, state_locked); + + lantern_client_update_sync_progress(client, local_slot); +} + /* ============================================================================ * External Functions (from client_sync_votes.c) @@ -249,6 +312,29 @@ static void cache_block_aggregated_proofs_locked( } } +static void persist_block_after_import( + struct lantern_client *client, + const LanternSignedBlock *block, + const struct lantern_log_metadata *meta) +{ + if (!client || !client->data_dir || !block) + { + return; + } + + struct lantern_log_metadata fallback = {.validator = client->node_id}; + const struct lantern_log_metadata *log_meta = meta ? meta : &fallback; + + if (lantern_storage_store_block(client->data_dir, block) != 0) + { + lantern_log_warn( + "storage", + log_meta, + "failed to persist block slot=%" PRIu64, + block->message.block.slot); + } +} + /* ============================================================================ * Block Import Helpers @@ -260,6 +346,7 @@ static void cache_block_aggregated_proofs_locked( * @param block Signed block to hash * @param provided Optional precomputed root * @param out_root Output root (filled on success) + * @param block_root Root of the block * @param meta Logging metadata * @return true on success, false on failure * @@ -310,24 +397,588 @@ static bool should_process_block( uint64_t local_slot, bool root_known, uint64_t known_slot, - const struct lantern_log_metadata *meta, - bool allow_historical) + 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 && !allow_historical) + return true; +} + +enum block_parent_action +{ + BLOCK_PARENT_ACTION_UNKNOWN = 0, + BLOCK_PARENT_ACTION_MATCHES_HEAD, + BLOCK_PARENT_ACTION_KNOWN_OFF_HEAD, +}; + +struct lantern_root_chain_entry +{ + LanternRoot root; + LanternRoot parent_root; + uint64_t slot; + bool has_parent; +}; + +struct lantern_root_chain +{ + struct lantern_root_chain_entry *items; + size_t length; + size_t capacity; +}; + +static void root_chain_reset(struct lantern_root_chain *chain) +{ + if (!chain) + { + return; + } + free(chain->items); + chain->items = NULL; + chain->length = 0; + chain->capacity = 0; +} + +static bool root_chain_contains(const struct lantern_root_chain *chain, const LanternRoot *root) +{ + if (!chain || !root) + { + return false; + } + for (size_t i = 0; i < chain->length; ++i) { - lantern_log_debug( + if (memcmp(chain->items[i].root.bytes, root->bytes, LANTERN_ROOT_SIZE) == 0) + { + return true; + } + } + return false; +} + +static bool root_chain_append( + struct lantern_root_chain *chain, + const LanternRoot *root, + const LanternRoot *parent_root, + uint64_t slot, + bool has_parent) +{ + if (!chain || !root || !parent_root) + { + return false; + } + if (chain->length == chain->capacity) + { + size_t next_capacity = chain->capacity > 0 ? chain->capacity + (chain->capacity / 2u) : 4u; + if (next_capacity < chain->capacity) + { + return false; + } + if (next_capacity > SIZE_MAX / sizeof(*chain->items)) + { + return false; + } + struct lantern_root_chain_entry *expanded = realloc( + chain->items, + next_capacity * sizeof(*expanded)); + if (!expanded) + { + return false; + } + chain->items = expanded; + chain->capacity = next_capacity; + } + struct lantern_root_chain_entry *entry = &chain->items[chain->length]; + entry->root = *root; + entry->parent_root = *parent_root; + entry->slot = slot; + entry->has_parent = has_parent; + chain->length += 1u; + return true; +} + +static bool build_root_chain_locked( + struct lantern_client *client, + const LanternRoot *target_root, + struct lantern_root_chain *out_chain) +{ + if (!client || !target_root || !out_chain || !client->has_fork_choice) + { + return false; + } + root_chain_reset(out_chain); + + LanternRoot current = *target_root; + size_t steps = 0; + while (!lantern_root_is_zero(¤t)) + { + if (steps > LANTERN_HISTORICAL_ROOTS_LIMIT) + { + return false; + } + if (root_chain_contains(out_chain, ¤t)) + { + return false; + } + + LanternRoot parent = {0}; + uint64_t slot = 0; + bool has_parent = false; + if (lantern_fork_choice_block_info( + &client->fork_choice, + ¤t, + &slot, + &parent, + &has_parent) + != 0) + { + return false; + } + + if (!root_chain_append(out_chain, ¤t, &parent, slot, has_parent)) + { + return false; + } + + if (!has_parent || lantern_root_is_zero(&parent)) + { + break; + } + + current = parent; + steps += 1u; + } + + return out_chain->length > 0; +} + +static bool init_replay_state(const struct lantern_client *client, LanternState *out_state) +{ + if (!client || !out_state) + { + return false; + } + + struct lantern_log_metadata meta = {.validator = client->node_id}; + lantern_state_init(out_state); + + if (client->genesis.state_bytes && client->genesis.state_size > 0) + { + if (lantern_ssz_decode_state( + out_state, + client->genesis.state_bytes, + client->genesis.state_size) + == 0) + { + return true; + } + lantern_log_warn( "state", - meta, - "ignoring block slot=%" PRIu64 " local_slot=%" PRIu64, - slot, - local_slot); + &meta, + "init_replay_state failed to decode genesis state size=%zu", + client->genesis.state_size); + lantern_state_reset(out_state); + lantern_state_init(out_state); + } + + const struct lantern_chain_config *config = &client->genesis.chain_config; + size_t validator_count = config->validator_pubkeys_count; + const uint8_t *pubkeys = config->validator_pubkeys; + bool allocated_pubkeys = false; + + if (!pubkeys || validator_count == 0) + { + size_t registry_count = client->genesis.validator_registry.count; + if (registry_count > 0 && registry_count == config->validator_count) + { + if (registry_count > SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE) + { + lantern_log_warn( + "state", + &meta, + "init_replay_state registry pubkey size overflow count=%zu", + registry_count); + lantern_state_reset(out_state); + return false; + } + + validator_count = registry_count; + size_t pubkeys_len = validator_count * LANTERN_VALIDATOR_PUBKEY_SIZE; + uint8_t *buffer = calloc(pubkeys_len, sizeof(*buffer)); + if (!buffer) + { + lantern_log_warn( + "state", + &meta, + "init_replay_state failed to allocate registry pubkey buffer len=%zu", + pubkeys_len); + lantern_state_reset(out_state); + return false; + } + bool pubkey_ok = true; + for (size_t i = 0; i < validator_count; ++i) + { + const struct lantern_validator_record *rec = + &client->genesis.validator_registry.records[i]; + uint8_t *dest = buffer + (i * LANTERN_VALIDATOR_PUBKEY_SIZE); + if (rec->has_pubkey_bytes) + { + memcpy(dest, rec->pubkey_bytes, LANTERN_VALIDATOR_PUBKEY_SIZE); + } + else if (rec->pubkey_hex + && lantern_hex_decode( + rec->pubkey_hex, + dest, + LANTERN_VALIDATOR_PUBKEY_SIZE) + == 0) + { + /* decoded */ + } + else + { + pubkey_ok = false; + break; + } + } + if (!pubkey_ok) + { + lantern_log_warn( + "state", + &meta, + "init_replay_state failed to populate registry pubkeys"); + free(buffer); + lantern_state_reset(out_state); + return false; + } + pubkeys = buffer; + allocated_pubkeys = true; + } + else if (client->state.validators && client->state.validator_count > 0) + { + size_t state_count = client->state.validator_count; + if (state_count > SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE) + { + lantern_log_warn( + "state", + &meta, + "init_replay_state state pubkey size overflow count=%zu", + state_count); + lantern_state_reset(out_state); + return false; + } + validator_count = state_count; + size_t pubkeys_len = validator_count * LANTERN_VALIDATOR_PUBKEY_SIZE; + uint8_t *buffer = calloc(pubkeys_len, sizeof(*buffer)); + if (!buffer) + { + lantern_log_warn( + "state", + &meta, + "init_replay_state failed to allocate state pubkey buffer len=%zu", + pubkeys_len); + lantern_state_reset(out_state); + return false; + } + for (size_t i = 0; i < validator_count; ++i) + { + const LanternValidator *validator = &client->state.validators[i]; + uint8_t *dest = buffer + (i * LANTERN_VALIDATOR_PUBKEY_SIZE); + memcpy(dest, validator->pubkey, LANTERN_VALIDATOR_PUBKEY_SIZE); + } + pubkeys = buffer; + allocated_pubkeys = true; + } + else + { + lantern_log_warn( + "state", + &meta, + "init_replay_state missing validator pubkeys"); + lantern_state_reset(out_state); + return false; + } + } + + if (validator_count == 0) + { + if (allocated_pubkeys) + { + free((void *)pubkeys); + } + lantern_log_warn( + "state", + &meta, + "init_replay_state invalid validator_count=0"); + lantern_state_reset(out_state); + return false; + } + + if (lantern_state_generate_genesis( + out_state, + config->genesis_time, + (uint64_t)validator_count) + != 0) + { + if (allocated_pubkeys) + { + free((void *)pubkeys); + } + lantern_log_warn( + "state", + &meta, + "init_replay_state failed to generate genesis time=%" PRIu64 " validators=%zu", + config->genesis_time, + validator_count); + lantern_state_reset(out_state); + return false; + } + + if (lantern_state_set_validator_pubkeys(out_state, pubkeys, validator_count) != 0) + { + if (allocated_pubkeys) + { + free((void *)pubkeys); + } + lantern_log_warn( + "state", + &meta, + "init_replay_state failed to set validator pubkeys count=%zu", + validator_count); + lantern_state_reset(out_state); return false; } + + if (allocated_pubkeys) + { + free((void *)pubkeys); + } + + return true; +} + +static bool compute_state_head_root_locked( + struct lantern_client *client, + LanternRoot *out_root) +{ + if (!client || !out_root) + { + return false; + } + if (lantern_state_process_slot(&client->state) != 0) + { + return false; + } + if (lantern_hash_tree_root_block_header(&client->state.latest_block_header, out_root) != 0) + { + return false; + } + return true; +} + +static void adopt_state_locked(struct lantern_client *client, LanternState *state) +{ + if (!client || !state) + { + return; + } + LanternState previous = client->state; + client->state = *state; + lantern_state_attach_fork_choice(&client->state, &client->fork_choice); + lantern_state_reset(&previous); +} + +static bool rebuild_state_for_root_locked( + struct lantern_client *client, + const LanternRoot *target_root, + LanternState *out_state) +{ + if (!client || !target_root || !out_state) + { + return false; + } + + struct lantern_log_metadata meta = {.validator = client->node_id}; + char target_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(target_root, target_hex, sizeof(target_hex)); + + struct lantern_root_chain chain = {0}; + if (!build_root_chain_locked(client, target_root, &chain)) + { + lantern_log_warn( + "state", + &meta, + "rebuild_state failed to build root chain target=%s", + target_hex[0] ? target_hex : "0x0"); + root_chain_reset(&chain); + return false; + } + + size_t root_count = 0; + for (size_t i = 0; i < chain.length; ++i) + { + if (chain.items[i].slot != 0) + { + root_count += 1u; + } + } + + lantern_log_info( + "state", + &meta, + "rebuild_state start target=%s chain_len=%zu root_count=%zu", + target_hex[0] ? target_hex : "0x0", + chain.length, + root_count); + + LanternRoot *roots = NULL; + if (root_count > 0) + { + if (!client->data_dir || !client->data_dir[0]) + { + root_chain_reset(&chain); + return false; + } + roots = calloc(root_count, sizeof(*roots)); + if (!roots) + { + root_chain_reset(&chain); + return false; + } + size_t idx = 0; + for (size_t i = chain.length; i-- > 0;) + { + if (chain.items[i].slot == 0) + { + continue; + } + roots[idx++] = chain.items[i].root; + } + } + + LanternBlocksByRootResponse response; + lantern_blocks_by_root_response_init(&response); + int collect_rc = 0; + if (root_count > 0) + { + collect_rc = lantern_storage_collect_blocks( + client->data_dir, + roots, + root_count, + &response); + } + if (collect_rc != 0 || response.length != root_count) + { + lantern_log_warn( + "state", + &meta, + "rebuild_state block collection mismatch target=%s collect_rc=%d expected=%zu got=%zu", + target_hex[0] ? target_hex : "0x0", + collect_rc, + root_count, + response.length); + if (roots && response.length > 0) + { + LanternRoot *found_roots = calloc(response.length, sizeof(*found_roots)); + if (found_roots) + { + size_t found_count = 0; + for (size_t i = 0; i < response.length; ++i) + { + if (lantern_hash_tree_root_block(&response.blocks[i].message.block, &found_roots[i]) == 0) + { + found_count += 1u; + } + } + size_t missing = 0; + size_t logged = 0; + for (size_t i = 0; i < root_count; ++i) + { + bool present = false; + for (size_t j = 0; j < response.length; ++j) + { + if (memcmp(roots[i].bytes, found_roots[j].bytes, LANTERN_ROOT_SIZE) == 0) + { + present = true; + break; + } + } + if (!present) + { + missing += 1u; + if (logged < 5) + { + char missing_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&roots[i], missing_hex, sizeof(missing_hex)); + lantern_log_warn( + "state", + &meta, + "rebuild_state missing block root=%s", + missing_hex[0] ? missing_hex : "0x0"); + logged += 1u; + } + } + } + lantern_log_warn( + "state", + &meta, + "rebuild_state missing_blocks=%zu found_blocks=%zu", + missing, + found_count); + free(found_roots); + } + } + } + free(roots); + roots = NULL; + if (collect_rc != 0 || response.length != root_count) + { + lantern_blocks_by_root_response_reset(&response); + root_chain_reset(&chain); + return false; + } + + bool success = init_replay_state(client, out_state); + if (!success) + { + lantern_log_warn( + "state", + &meta, + "rebuild_state failed to initialize replay state target=%s", + target_hex[0] ? target_hex : "0x0"); + lantern_blocks_by_root_response_reset(&response); + root_chain_reset(&chain); + return false; + } + + for (size_t i = 0; i < response.length; ++i) + { + int transition_rc = lantern_state_transition(out_state, &response.blocks[i]); + if (transition_rc != 0) + { + LanternRoot block_root = {0}; + char block_hex[ROOT_HEX_BUFFER_LEN] = {0}; + if (lantern_hash_tree_root_block(&response.blocks[i].message.block, &block_root) == 0) + { + format_root_hex(&block_root, block_hex, sizeof(block_hex)); + } + lantern_log_warn( + "state", + &meta, + "rebuild_state transition failed idx=%zu slot=%" PRIu64 " rc=%d root=%s", + i, + response.blocks[i].message.block.slot, + transition_rc, + block_hex[0] ? block_hex : "0x0"); + lantern_blocks_by_root_response_reset(&response); + root_chain_reset(&chain); + lantern_state_reset(out_state); + return false; + } + } + + lantern_blocks_by_root_response_reset(&response); + root_chain_reset(&chain); return true; } @@ -337,30 +988,31 @@ static bool should_process_block( * * @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 + * @param backfill_depth Backfill depth of the block + * @return Parent action describing how to proceed * * @note Thread safety: Caller must hold state_lock */ -static bool handle_block_parent_locked( +static enum block_parent_action handle_block_parent_locked( struct lantern_client *client, const LanternSignedBlock *block, const LanternRoot *block_root, const struct lantern_log_metadata *meta, bool *state_locked, + uint32_t backfill_depth, bool allow_historical) { if (!client || !block || !block_root || !state_locked || !*state_locked) { - return false; + return BLOCK_PARENT_ACTION_UNKNOWN; } LanternRoot parent_root = block->message.block.parent_root; if (lantern_root_is_zero(&parent_root)) { - return true; + return BLOCK_PARENT_ACTION_MATCHES_HEAD; } bool parent_known = lantern_client_block_known_locked(client, &parent_root, NULL); @@ -375,8 +1027,9 @@ static bool handle_block_parent_locked( block_root, &parent_root, peer_text, + backfill_depth, allow_historical); - return false; + return BLOCK_PARENT_ACTION_UNKNOWN; } bool have_head_root = false; @@ -406,11 +1059,9 @@ static bool handle_block_parent_locked( if (parent_matches_head) { - return true; + return BLOCK_PARENT_ACTION_MATCHES_HEAD; } - const char *peer_text = meta && meta->peer ? meta->peer : NULL; - if (have_head_root) { char parent_hex[ROOT_HEX_BUFFER_LEN]; @@ -426,52 +1077,46 @@ static bool handle_block_parent_locked( head_hex[0] ? head_hex : "0x0"); } - /* - * 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) - { - LanternSignedVote proposer_signed = {0}; - proposer_signed.data = block->message.proposer_attestation; - proposer_signed.signature = block->signatures.proposer_signature; - - 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) - { - 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"); - } + return BLOCK_PARENT_ACTION_KNOWN_OFF_HEAD; +} + +static bool add_competing_fork_block_locked( + struct lantern_client *client, + const LanternSignedBlock *block, + const LanternRoot *block_root, + const LanternCheckpoint *post_justified, + const LanternCheckpoint *post_finalized, + const struct lantern_log_metadata *meta) +{ + if (!client || !block || !block_root || !client->has_fork_choice) + { + return false; } - lantern_client_unlock_state(client, *state_locked); - *state_locked = false; - lantern_client_enqueue_pending_block( - client, - block, - block_root, - &parent_root, - peer_text, - false); - return false; + LanternSignedVote proposer_signed = {0}; + proposer_signed.data = block->message.proposer_attestation; + proposer_signed.signature = block->signatures.proposer_signature; + + if (lantern_fork_choice_add_block( + &client->fork_choice, + &block->message.block, + &proposer_signed, + post_justified, + post_finalized, + block_root) != 0) + { + return false; + } + + 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"); + return true; } @@ -686,6 +1331,75 @@ static void persist_state_locked( } } +/** + * @brief Persist per-block post-state and sync indices. + * + * @param client Client instance + * @param post_state Post-state after applying the block + * @param block Imported block + * @param block_root Root of the block + * @param head_root Current head root + * @param head_slot Current head slot + * @param meta Logging metadata + * + * @note Thread safety: Caller must hold state_lock + */ +static void persist_post_state_and_indices_locked( + const struct lantern_client *client, + const LanternState *post_state, + const LanternSignedBlock *block, + const LanternRoot *block_root, + const LanternRoot *head_root, + uint64_t head_slot, + const struct lantern_log_metadata *meta) +{ + if (!client || !client->data_dir || !post_state || !block || !block_root) + { + return; + } + + if (lantern_storage_store_state_for_root(client->data_dir, block_root, post_state) != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist post-state slot=%" PRIu64, + block->message.block.slot); + } + if (lantern_storage_store_slot_root( + client->data_dir, + block->message.block.slot, + block_root) + != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist slot index slot=%" PRIu64, + block->message.block.slot); + } + if (lantern_storage_store_head_root(client->data_dir, head_slot, head_root) != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist head index slot=%" PRIu64, + head_slot); + } + if (lantern_storage_store_checkpoints( + client->data_dir, + &post_state->latest_justified, + &post_state->latest_finalized) + != 0) + { + lantern_log_warn( + "storage", + meta, + "failed to persist checkpoints slot=%" PRIu64, + post_state->slot); + } +} + /** * @brief Logs a successful block import. @@ -749,11 +1463,11 @@ static void log_imported_block( * 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 known but not head: validate, add to fork choice, process cached descendants * - Parent matches head: proceed with full import * 4. Verifies all block signatures * 5. Validates attestation constraints - * 6. Applies state transition + * 6. Applies state transition (head-matching parents only) * 7. Updates fork choice * 8. Persists state and votes * 9. Processes pending children @@ -766,6 +1480,7 @@ static void log_imported_block( * @param block Signed block to import * @param block_root Precomputed block root (may be NULL) * @param meta Logging metadata + * @param backfill_depth Backfill depth of the block * @return true if block was imported successfully * * @note Thread safety: Acquires state_lock and pending_lock @@ -775,6 +1490,7 @@ bool lantern_client_import_block( const LanternSignedBlock *block, const LanternRoot *block_root, const struct lantern_log_metadata *meta, + uint32_t backfill_depth, bool allow_historical) { if (!client || !block || !client->has_state) @@ -811,35 +1527,163 @@ bool lantern_client_import_block( local_slot, root_known, known_slot, - meta, - allow_historical)) + meta)) { goto cleanup; } - if (!handle_block_parent_locked( - client, - block, - &block_root_local, - meta, - &state_locked, - allow_historical)) + enum block_parent_action parent_action = handle_block_parent_locked( + client, + block, + &block_root_local, + meta, + &state_locked, + backfill_depth, + allow_historical); + if (parent_action == BLOCK_PARENT_ACTION_UNKNOWN) { goto cleanup; } + bool parent_off_head = parent_action == BLOCK_PARENT_ACTION_KNOWN_OFF_HEAD; if (!signed_block_signatures_are_valid(client, block, meta)) { + char root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&block_root_local, root_hex, sizeof(root_hex)); + lantern_log_warn( + "state", + meta, + "signature verification failed slot=%" PRIu64 " root=%s depth=%" PRIu32, + block->message.block.slot, + root_hex[0] ? root_hex : "0x0", + backfill_depth); goto cleanup; } if (!validate_block_vote_constraints_locked(client, block, meta)) { + char root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&block_root_local, root_hex, sizeof(root_hex)); + lantern_log_warn( + "state", + meta, + "vote constraints failed slot=%" PRIu64 " root=%s depth=%" PRIu32, + block->message.block.slot, + root_hex[0] ? root_hex : "0x0", + backfill_depth); goto cleanup; } cache_block_aggregated_proofs_locked(client, block); + if (parent_off_head) + { + LanternRoot parent_root = block->message.block.parent_root; + LanternState replay_state; + bool have_replay_state = false; + bool processed = false; + + if (rebuild_state_for_root_locked(client, &parent_root, &replay_state)) + { + have_replay_state = true; + if (lantern_state_transition(&replay_state, block) == 0) + { + processed = add_competing_fork_block_locked( + client, + block, + &block_root_local, + &replay_state.latest_justified, + &replay_state.latest_finalized, + meta); + } + else + { + lantern_log_warn( + "state", + meta, + "off-head state transition failed for slot=%" PRIu64, + block->message.block.slot); + } + } + else + { + lantern_log_warn( + "state", + meta, + "failed to rebuild parent state for off-head slot=%" PRIu64, + block->message.block.slot); + } + + bool adopted_state = false; + if (processed && client->has_fork_choice) + { + LanternRoot fork_head = {0}; + bool have_fork_head = lantern_fork_choice_current_head( + &client->fork_choice, + &fork_head) + == 0; + LanternRoot state_head = {0}; + bool have_state_head = have_fork_head && compute_state_head_root_locked( + client, + &state_head); + + if (have_fork_head && have_state_head + && memcmp(fork_head.bytes, state_head.bytes, LANTERN_ROOT_SIZE) != 0) + { + if (have_replay_state + && memcmp(fork_head.bytes, block_root_local.bytes, LANTERN_ROOT_SIZE) == 0) + { + adopt_state_locked(client, &replay_state); + have_replay_state = false; + adopted_state = true; + } + else + { + LanternState head_state; + if (rebuild_state_for_root_locked(client, &fork_head, &head_state)) + { + adopt_state_locked(client, &head_state); + adopted_state = true; + } + } + } + } + + if (adopted_state) + { + persist_state_locked(client, meta); + } + + if (processed && have_replay_state) + { + get_head_info_locked(client, &head_root, &head_slot); + persist_post_state_and_indices_locked( + client, + &replay_state, + block, + &block_root_local, + &head_root, + head_slot, + meta); + } + + if (have_replay_state) + { + lantern_state_reset(&replay_state); + } + + lantern_client_unlock_state(client, state_locked); + state_locked = false; + lantern_client_pending_remove_by_root(client, &block_root_local); + if (processed) + { + persist_block_after_import(client, block, meta); + lantern_client_process_pending_children(client, &block_root_local); + update_sync_progress_after_block(client); + } + return false; + } + if (!apply_state_transition_locked(client, block, meta)) { goto cleanup; @@ -848,6 +1692,14 @@ bool lantern_client_import_block( advance_fork_choice_time_locked(client, block, meta); get_head_info_locked(client, &head_root, &head_slot); persist_state_locked(client, meta); + persist_post_state_and_indices_locked( + client, + &client->state, + block, + &block_root_local, + &head_root, + head_slot, + meta); imported = true; cleanup: @@ -855,6 +1707,7 @@ bool lantern_client_import_block( if (imported) { + persist_block_after_import(client, block, meta); bool quiet_log = false; if (client->status_lock_initialized && pthread_mutex_lock(&client->status_lock) == 0) @@ -866,6 +1719,7 @@ bool lantern_client_import_block( 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, quiet_log); + update_sync_progress_after_block(client); } return imported; @@ -882,14 +1736,15 @@ bool lantern_client_import_block( * @spec subspecs/forkchoice/store.py - Store.on_block() * * Entry point for recording blocks received from gossip or reqresp. - * Computes the block root if not provided, persists the block to - * storage, then delegates to import_block for processing. + * Computes the block root if not provided, delegates to import_block + * for processing, and persists the block after successful import. * * @param client Client instance * @param block Signed block to record * @param root Precomputed block root (may be NULL) * @param peer_text Peer ID string (may be NULL) * @param context Description of source for logging + * @param backfill_depth Backfill depth of the block * * @note Thread safety: Acquires state_lock via lantern_client_import_block */ @@ -899,6 +1754,7 @@ void lantern_client_record_block( const LanternRoot *root, const char *peer_text, const char *context, + uint32_t backfill_depth, bool allow_historical) { if (!client || !block) @@ -961,17 +1817,11 @@ void lantern_client_record_block( source); } - if (client->data_dir) - { - if (lantern_storage_store_block(client->data_dir, block) != 0) - { - lantern_log_warn( - "storage", - &meta, - "failed to persist block slot=%" PRIu64, - block->message.block.slot); - } - } - - lantern_client_import_block(client, block, selected_root, &meta, allow_historical); + lantern_client_import_block( + client, + block, + selected_root, + &meta, + backfill_depth, + allow_historical); } diff --git a/src/core/client_sync_internal.h b/src/core/client_sync_internal.h index cc50d04..ac67a27 100644 --- a/src/core/client_sync_internal.h +++ b/src/core/client_sync_internal.h @@ -45,8 +45,10 @@ typedef struct peer_id peer_id_t; * Constants * ============================================================================ */ -/** Maximum number of pending blocks to hold */ -#define LANTERN_PENDING_BLOCK_LIMIT 256u +/** Maximum roots per blocks_by_root request */ +#define LANTERN_MAX_BLOCKS_PER_REQUEST 10u +/** Maximum backfill depth when requesting parents */ +#define LANTERN_MAX_BACKFILL_DEPTH LANTERN_PENDING_BLOCK_LIMIT /* ============================================================================ @@ -105,6 +107,7 @@ struct lantern_persisted_block_list void lantern_vote_rejection_set(struct lantern_vote_rejection_info *info, const char *fmt, ...); + /** * Get current slot from fork choice. * @@ -190,6 +193,7 @@ void pending_block_list_remove(struct lantern_pending_block_list *list, size_t i * @param block_root Root of the block * @param parent_root Root of the parent block * @param peer_text Peer ID text (may be NULL) + * @param backfill_depth Backfill depth of the block * @return Pointer to new entry, or NULL on failure * * @note Thread safety: Caller must hold pending_lock @@ -199,7 +203,23 @@ struct lantern_pending_block *pending_block_list_append( const LanternSignedBlock *block, const LanternRoot *block_root, const LanternRoot *parent_root, - const char *peer_text); + const char *peer_text, + uint32_t backfill_depth); + +/** + * Peek a pending child root for a given parent root. + * + * @param list List to search + * @param parent_root Parent root to match + * @param out_child_root Output child root + * @return true if a child is available, false otherwise + * + * @note Thread safety: Caller must hold pending_lock + */ +bool pending_block_list_peek_child_root( + struct lantern_pending_block_list *list, + const LanternRoot *parent_root, + LanternRoot *out_child_root); /** @@ -293,6 +313,18 @@ bool lantern_client_validate_vote_constraints( const char *context, struct lantern_vote_rejection_info *out_rejection); +/** + * Update sync progress using latest peer status and local slot snapshot. + * + * @param client Client instance + * @param local_slot Local slot snapshot + * + * @note Thread safety: This function acquires status_lock. + */ +void lantern_client_update_sync_progress( + struct lantern_client *client, + uint64_t local_slot); + /** * Import a block into the client state and fork choice. @@ -306,6 +338,7 @@ bool lantern_client_validate_vote_constraints( * @param block Signed block to import * @param block_root Precomputed block root (may be NULL) * @param meta Logging metadata + * @param backfill_depth Backfill depth of the block * @param allow_historical True to allow importing blocks older than local slot * @return true if block was imported successfully * @@ -316,6 +349,7 @@ bool lantern_client_import_block( const LanternSignedBlock *block, const LanternRoot *block_root, const struct lantern_log_metadata *meta, + uint32_t backfill_depth, bool allow_historical); @@ -329,6 +363,7 @@ bool lantern_client_import_block( * @param root Precomputed block root (may be NULL) * @param peer_text Peer ID string (may be NULL) * @param context Description of source for logging + * @param backfill_depth Backfill depth of the block * @param allow_historical True to allow importing blocks older than local slot * * @note Thread safety: Acquires state_lock via lantern_client_import_block @@ -339,6 +374,7 @@ void lantern_client_record_block( const LanternRoot *root, const char *peer_text, const char *context, + uint32_t backfill_depth, bool allow_historical); @@ -440,6 +476,7 @@ int lantern_client_refresh_state_validators(struct lantern_client *client); * @param block_root Block root * @param parent_root Parent block root * @param peer_text Peer ID string (may be NULL) + * @param backfill_depth Backfill depth of the block * * @note Thread safety: Acquires pending_lock */ @@ -449,16 +486,17 @@ void lantern_client_enqueue_pending_block( const LanternRoot *block_root, const LanternRoot *parent_root, const char *peer_text, + uint32_t backfill_depth, bool request_parent); /** - * Attempt to request the parent of a pending block after a blocks_by_root success. + * Attempt to request parents for pending blocks after a blocks_by_root success. * - * Uses the peer that just completed a blocks_by_root request to backfill - * missing parents for queued blocks. + * Selects an available peer for backfill requests. A provided peer ID is treated + * as a preference but is not required. * * @param client Client instance - * @param peer_text Peer ID string (must be non-NULL/non-empty) + * @param peer_text Peer ID string (may be NULL/empty) * * @note Thread safety: Thread-safe; acquires pending_lock internally */ diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index e362bef..ab3d536 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -34,136 +34,6 @@ enum VOTE_ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, }; -static bool pending_contains_root(struct lantern_client *client, const LanternRoot *root) -{ - if (!client || !root) - { - return false; - } - - bool locked = lantern_client_lock_pending(client); - if (!locked) - { - return false; - } - - bool found = false; - for (size_t i = 0; i < client->pending_blocks.length; ++i) - { - struct lantern_pending_block *entry = &client->pending_blocks.items[i]; - if (memcmp(entry->root.bytes, root->bytes, LANTERN_ROOT_SIZE) == 0 - || memcmp(entry->parent_root.bytes, root->bytes, LANTERN_ROOT_SIZE) == 0) - { - found = true; - break; - } - } - - lantern_client_unlock_pending(client, locked); - return found; -} - -static bool should_request_missing_root( - struct lantern_client *client, - const char *peer_text, - const LanternRoot *root) -{ - if (!client || !peer_text || !peer_text[0] || !root || lantern_root_is_zero(root)) - { - return false; - } - - bool state_locked = lantern_client_lock_state(client); - bool known = false; - if (state_locked) - { - known = lantern_client_block_known_locked(client, root, NULL); - } - lantern_client_unlock_state(client, state_locked); - - if (known) - { - return false; - } - - if (pthread_mutex_lock(&client->status_lock) != 0) - { - return false; - } - - struct lantern_peer_status_entry *entry = - lantern_client_ensure_status_entry_locked(client, peer_text); - if (!entry) - { - pthread_mutex_unlock(&client->status_lock); - return false; - } - - if (entry->requested_head) - { - pthread_mutex_unlock(&client->status_lock); - 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) - { - pthread_mutex_unlock(&client->status_lock); - return false; - } - - if (pending_contains_root(client, root)) - { - pthread_mutex_unlock(&client->status_lock); - return false; - } - - entry->requested_head = true; - entry->last_blocks_request_ms = now_ms; - pthread_mutex_unlock(&client->status_lock); - return true; -} - -static void request_missing_checkpoint_root( - struct lantern_client *client, - const char *peer_text, - const LanternRoot *root, - uint64_t slot) -{ - if (!should_request_missing_root(client, peer_text, root)) - { - return; - } - - char root_hex[VOTE_ROOT_HEX_BUFFER_LEN]; - format_root_hex(root, root_hex, sizeof(root_hex)); - lantern_log_info( - "reqresp", - &(const struct lantern_log_metadata){ - .validator = client ? client->node_id : NULL, - .peer = (peer_text && *peer_text) ? peer_text : NULL}, - "requesting missing checkpoint root=%s slot=%" PRIu64, - root_hex[0] ? root_hex : "0x0", - slot); - - if (lantern_client_schedule_blocks_request(client, peer_text, root) != 0) - { - lantern_client_on_blocks_request_complete( - client, - peer_text, - root, - LANTERN_BLOCKS_REQUEST_ABORTED); - } -} - /* ============================================================================ * External Functions (from client_sync.c) @@ -901,14 +771,6 @@ void lantern_client_record_vote( lantern_client_note_vote_outcome(client, peer_text, &vote_copy, vote_processed); if (!vote_processed) { - if (rejection.has_unknown_root) - { - request_missing_checkpoint_root( - client, - peer_text, - &rejection.unknown_root, - rejection.unknown_slot); - } const char *reason_text = rejection.has_reason ? rejection.message : "unknown"; if (client->sync_in_progress) { diff --git a/src/core/client_utils.c b/src/core/client_utils.c index 43ec259..85e51c6 100644 --- a/src/core/client_utils.c +++ b/src/core/client_utils.c @@ -207,47 +207,6 @@ void validator_sleep_ms(uint32_t ms) } -/* ============================================================================ - * Backoff Calculation - * ============================================================================ */ - -/** - * Calculate backoff time for blocks request based on failure count. - * - * @param failures Number of consecutive failures - * @return Backoff time in milliseconds - * - * @note Thread safety: This function is thread-safe - */ -uint64_t blocks_request_backoff_ms(uint32_t failures) -{ - if (failures == 0) - { - return 0; - } - if (failures >= LANTERN_BLOCKS_REQUEST_BACKOFF_MAX_FAILURES) - { - return LANTERN_BLOCKS_REQUEST_BACKOFF_MAX_MS; - } - const uint64_t max_backoff = LANTERN_BLOCKS_REQUEST_BACKOFF_MAX_MS; - uint64_t backoff = LANTERN_BLOCKS_REQUEST_BACKOFF_BASE_MS; - for (uint32_t i = 1; i < failures && backoff < max_backoff; ++i) - { - if (backoff > max_backoff / 2 || backoff > UINT64_MAX / 2) - { - backoff = max_backoff; - break; - } - backoff *= 2; - } - if (backoff > max_backoff) - { - backoff = max_backoff; - } - return backoff; -} - - /* ============================================================================ * State Locking * ============================================================================ */ diff --git a/src/core/client_validator.c b/src/core/client_validator.c index c9117e5..54f2ed1 100644 --- a/src/core/client_validator.c +++ b/src/core/client_validator.c @@ -45,6 +45,8 @@ static const uint32_t VALIDATOR_SERVICE_POLL_SLEEP_MS = 50; static const uint64_t VALIDATOR_SYNC_SLOT_LAG = 2; /** Pending queue size that indicates we're still catching up. */ static const size_t VALIDATOR_SYNC_PENDING_THRESHOLD = 8; +/** Wall clock lag (in slots) tolerated before treating peer status as stale. */ +static const uint64_t VALIDATOR_SYNC_WALL_CLOCK_LAG = 16; static bool validator_should_pause_for_sync(const struct lantern_client *client) { @@ -53,8 +55,49 @@ static bool validator_should_pause_for_sync(const struct lantern_client *client) return false; } + uint64_t local_slot = client->has_state ? client->state.latest_block_header.slot : 0; + LanternRoot local_head = {0}; + bool have_local_head = false; + if (client->has_fork_choice + && lantern_fork_choice_current_head(&client->fork_choice, &local_head) == 0) + { + have_local_head = true; + uint64_t head_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + &local_head, + &head_slot, + NULL, + NULL) + == 0) + { + local_slot = head_slot; + } + } + uint64_t max_peer_slot = 0; - bool have_status = false; + bool have_fresh_status = false; + bool have_peer_at_or_ahead = false; + bool head_match_at_or_ahead = false; + bool any_head_match = false; + uint64_t now_ms = monotonic_millis(); + static LanternRoot last_head_root = {{0}}; + static bool last_head_root_set = false; + static uint64_t last_head_match_ms = 0; + if (have_local_head) + { + if (!last_head_root_set + || memcmp( + last_head_root.bytes, + local_head.bytes, + LANTERN_ROOT_SIZE) + != 0) + { + last_head_root = local_head; + last_head_root_set = true; + last_head_match_ms = now_ms; + } + } pthread_mutex_t *status_lock = (pthread_mutex_t *)&client->status_lock; if (pthread_mutex_lock(status_lock) == 0) { @@ -65,21 +108,92 @@ static bool validator_should_pause_for_sync(const struct lantern_client *client) { continue; } - have_status = true; + if (entry->last_status_ms == 0 + || now_ms < entry->last_status_ms + || now_ms - entry->last_status_ms > LANTERN_PEER_STATUS_STALE_MS) + { + continue; + } + have_fresh_status = true; if (entry->status.head.slot > max_peer_slot) { max_peer_slot = entry->status.head.slot; } + if (have_local_head && entry->status.head.slot >= local_slot) + { + have_peer_at_or_ahead = true; + if (memcmp(entry->status.head.root.bytes, local_head.bytes, LANTERN_ROOT_SIZE) == 0) + { + head_match_at_or_ahead = true; + } + } + if (have_local_head + && memcmp(entry->status.head.root.bytes, local_head.bytes, LANTERN_ROOT_SIZE) == 0) + { + any_head_match = true; + } } pthread_mutex_unlock(status_lock); } - if (!have_status) + if (!have_fresh_status) { + bool has_connections = false; + pthread_mutex_t *connection_lock = (pthread_mutex_t *)&client->connection_lock; + if (client->connection_lock_initialized + && pthread_mutex_lock(connection_lock) == 0) + { + has_connections = client->connected_peers > 0; + pthread_mutex_unlock(connection_lock); + } + if (has_connections || client->bootnodes.len > 0) + { + return true; + } return false; } - uint64_t local_slot = client->state.slot; + uint64_t current_slot = 0; + if (lantern_client_current_slot(client, ¤t_slot) + && current_slot > max_peer_slot + VALIDATOR_SYNC_WALL_CLOCK_LAG) + { + return true; + } + + if (have_peer_at_or_ahead && have_local_head && !head_match_at_or_ahead) + { + return true; + } + + if (have_fresh_status && have_local_head) + { + if (any_head_match) + { + last_head_match_ms = now_ms; + } + else + { + uint64_t grace_ms = 0; + if (client->has_fork_choice && client->fork_choice.seconds_per_slot > 0) + { + uint64_t slot_ms = client->fork_choice.seconds_per_slot * 1000u; + if (slot_ms > UINT64_MAX / 2u) + { + slot_ms = UINT64_MAX / 2u; + } + grace_ms = slot_ms * 2u; + } + if (grace_ms == 0) + { + grace_ms = 8000u; + } + if (now_ms > last_head_match_ms + grace_ms) + { + return true; + } + } + } + if (max_peer_slot > local_slot + VALIDATOR_SYNC_SLOT_LAG) { return true; @@ -1653,7 +1767,7 @@ int validator_propose_block(struct lantern_client *client, uint64_t slot, size_t slot, block.message.block.proposer_index); - lantern_client_record_block(client, &block, NULL, NULL, "local", false); + lantern_client_record_block(client, &block, NULL, NULL, "local", 0, false); rc = lantern_client_publish_block(client, &block); if (rc != LANTERN_CLIENT_OK) { diff --git a/src/core/main.c b/src/core/main.c index f35c0af..2e5a3b7 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -10,6 +10,7 @@ #include "lantern/core/client.h" #include "lantern/support/log.h" +#include "lantern/support/version.h" #include #include @@ -22,9 +23,6 @@ #include #include -/** Lantern version string. */ -static const char *const LANTERN_VERSION = "v0.0.1"; - enum { OPT_GENESIS_CONFIG = 1000, OPT_VALIDATOR_REGISTRY, @@ -43,6 +41,7 @@ enum { OPT_DEVNET, OPT_LOG_LEVEL, OPT_XMSS_KEY_DIR, + OPT_HASH_SIG_KEY_DIR, OPT_XMSS_PUBLIC_PATH, OPT_XMSS_SECRET_PATH, OPT_XMSS_PUBLIC_TEMPLATE, @@ -243,6 +242,7 @@ static lantern_client_error apply_option( } return LANTERN_CLIENT_OK; case OPT_XMSS_KEY_DIR: + case OPT_HASH_SIG_KEY_DIR: case OPT_XMSS_PUBLIC_PATH: case OPT_XMSS_SECRET_PATH: case OPT_XMSS_PUBLIC_TEMPLATE: @@ -361,6 +361,7 @@ static lantern_client_error handle_xmss_option( switch (opt) { case OPT_XMSS_KEY_DIR: + case OPT_HASH_SIG_KEY_DIR: options->xmss_key_dir = optarg; return LANTERN_CLIENT_OK; case OPT_XMSS_PUBLIC_PATH: @@ -430,6 +431,7 @@ static lantern_client_error parse_arguments( {"devnet", required_argument, NULL, OPT_DEVNET}, {"log-level", required_argument, NULL, OPT_LOG_LEVEL}, {"xmss-key-dir", required_argument, NULL, OPT_XMSS_KEY_DIR}, + {"hash-sig-key-dir", required_argument, NULL, OPT_HASH_SIG_KEY_DIR}, {"xmss-public", required_argument, NULL, OPT_XMSS_PUBLIC_PATH}, {"xmss-secret", required_argument, NULL, OPT_XMSS_SECRET_PATH}, {"xmss-public-template", required_argument, NULL, OPT_XMSS_PUBLIC_TEMPLATE}, @@ -744,6 +746,10 @@ static void print_usage_xmss(void) "main", NULL, " --xmss-key-dir PATH Directory containing XMSS key files"); + lantern_log_info( + "main", + NULL, + " --hash-sig-key-dir PATH Alias for --xmss-key-dir"); lantern_log_info( "main", NULL, diff --git a/src/http/metrics.c b/src/http/metrics.c index 6f7caa0..5ec715a 100644 --- a/src/http/metrics.c +++ b/src/http/metrics.c @@ -30,13 +30,17 @@ #include "lantern/http/common.h" #include "lantern/support/log.h" +#include "lantern/support/version.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"; +static const char *const kLeanDirectionLabels[LEAN_METRICS_DIR_COUNT] = {"inbound", "outbound"}; +static const char *const kLeanConnectionResultLabels[LEAN_METRICS_CONN_RESULT_COUNT] = {"success", "timeout", "error"}; +static const char *const kLeanDisconnectionReasonLabels[LEAN_METRICS_DISCONNECT_REASON_COUNT] = + {"timeout", "remote_close", "local_close", "error"}; enum { @@ -373,7 +377,30 @@ static int append_lean_chain_metrics( return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; } - int rc = append_metric_uint64( + int rc = metrics_buffer_appendf( + buf, + "# HELP lean_node_info Node information (always 1)\n" + "# TYPE lean_node_info gauge\n" + "lean_node_info{name=\"%s\",version=\"%s\"} 1\n", + LANTERN_CLIENT_NAME, + LANTERN_VERSION); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_node_start_time_seconds", + "Start timestamp", + "gauge", + snapshot->lean_node_start_time_seconds); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( buf, "lean_head_slot", "Latest slot of the lean chain", @@ -384,6 +411,28 @@ static int append_lean_chain_metrics( return rc; } + rc = append_metric_uint64( + buf, + "lean_current_slot", + "Current slot of the lean chain", + "gauge", + snapshot->lean_current_slot); + if (rc != 0) + { + return rc; + } + + rc = append_metric_uint64( + buf, + "lean_safe_target_slot", + "Safe target slot", + "gauge", + snapshot->lean_safe_target_slot); + if (rc != 0) + { + return rc; + } + rc = append_metric_uint64( buf, "lean_latest_justified_slot", @@ -417,8 +466,31 @@ static int append_lean_chain_metrics( return rc; } + rc = metrics_buffer_appendf( + buf, + "# HELP lean_connected_peers Number of connected peers\n" + "# TYPE lean_connected_peers gauge\n" + "lean_connected_peers{client=\"%s\"} %zu\n", + LANTERN_CLIENT_NAME, + snapshot->lean_connected_peers); + if (rc != 0) + { + return rc; + } + const struct lean_metrics_snapshot *lean = &snapshot->lean_metrics; + rc = append_metric_uint64( + buf, + "lean_fork_choice_reorgs_total", + "Total number of fork choice reorgs", + "counter", + lean->fork_choice_reorgs_total); + if (rc != 0) + { + return rc; + } + rc = append_metric_uint64( buf, "lean_attestations_valid_total", @@ -441,6 +513,19 @@ static int append_lean_chain_metrics( return rc; } + rc = metrics_buffer_appendf( + buf, + "# HELP lean_finalizations_total Total number of finalization attempts\n" + "# TYPE lean_finalizations_total counter\n" + "lean_finalizations_total{result=\"success\"} %" PRIu64 "\n" + "lean_finalizations_total{result=\"error\"} %" PRIu64 "\n", + lean->finalizations_success_total, + lean->finalizations_error_total); + if (rc != 0) + { + return rc; + } + rc = append_metric_uint64( buf, "lean_state_transition_slots_processed_total", @@ -628,6 +713,73 @@ static int append_peer_vote_metrics( return 0; } +/** + * @brief Append peer connection metrics derived from the lean metrics snapshot. + */ +static int append_lean_peer_connection_metrics( + struct lantern_metrics_body_buffer *buf, + const struct lean_metrics_snapshot *lean) +{ + if (!buf || !lean) + { + return LANTERN_METRICS_SERVER_ERR_INVALID_PARAM; + } + + int rc = metrics_buffer_appendf( + buf, + "# HELP lean_peer_connection_events_total Total number of peer connection events\n" + "# TYPE lean_peer_connection_events_total counter\n"); + if (rc != 0) + { + return rc; + } + + for (size_t dir = 0; dir < LEAN_METRICS_DIR_COUNT; ++dir) + { + for (size_t res = 0; res < LEAN_METRICS_CONN_RESULT_COUNT; ++res) + { + rc = metrics_buffer_appendf( + buf, + "lean_peer_connection_events_total{direction=\"%s\",result=\"%s\"} %" PRIu64 "\n", + kLeanDirectionLabels[dir], + kLeanConnectionResultLabels[res], + lean->peer_connection_events_total[dir][res]); + if (rc != 0) + { + return rc; + } + } + } + + rc = metrics_buffer_appendf( + buf, + "# HELP lean_peer_disconnection_events_total Total number of peer disconnection events\n" + "# TYPE lean_peer_disconnection_events_total counter\n"); + if (rc != 0) + { + return rc; + } + + for (size_t dir = 0; dir < LEAN_METRICS_DIR_COUNT; ++dir) + { + for (size_t reason = 0; reason < LEAN_METRICS_DISCONNECT_REASON_COUNT; ++reason) + { + rc = metrics_buffer_appendf( + buf, + "lean_peer_disconnection_events_total{direction=\"%s\",reason=\"%s\"} %" PRIu64 "\n", + kLeanDirectionLabels[dir], + kLeanDisconnectionReasonLabels[reason], + lean->peer_disconnection_events_total[dir][reason]); + if (rc != 0) + { + return rc; + } + } + } + + return 0; +} + /** * @brief Append histogram metrics derived from the lean metrics snapshot. @@ -651,6 +803,16 @@ static int append_lean_histograms( return rc; } + rc = append_histogram_metrics( + buf, + "lean_fork_choice_reorg_depth", + "Depth of fork choice reorgs (in blocks)", + &lean->fork_choice_reorg_depth); + if (rc != 0) + { + return rc; + } + rc = append_histogram_metrics( buf, "lean_attestation_validation_time_seconds", @@ -772,6 +934,12 @@ static int format_metrics_body( goto cleanup; } + result = append_lean_peer_connection_metrics(&buf, &snapshot->lean_metrics); + if (result != 0) + { + goto cleanup; + } + result = append_lean_histograms(&buf, &snapshot->lean_metrics); if (result != 0) { @@ -788,6 +956,23 @@ static int format_metrics_body( return result; } +int lantern_metrics_format_prometheus( + 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 = format_metrics_body(snapshot, out_body, out_len); + if (result != 0) + { + return result; + } + return 0; +} + /** * Convert a peer address to a printable string. @@ -1110,7 +1295,7 @@ static void handle_client_connection( client_fd, 200, "OK", - LANTERN_METRICS_TEXT_CONTENT_TYPE, + LANTERN_METRICS_CONTENT_TYPE, body, body_len); free(body); diff --git a/src/http/server.c b/src/http/server.c index 44cb410..2c500f2 100644 --- a/src/http/server.c +++ b/src/http/server.c @@ -1,16 +1,633 @@ /** * @file server.c - * @brief Lean API HTTP server (disabled stub). + * @brief Lean API HTTP server for checkpoint sync and health/metrics endpoints. * - * The HTTP API is intentionally disabled. The CLI flags remain accepted, but - * the server does not start. This file provides no-op stubs to satisfy the - * public API while removing the server implementation. + * Exposes endpoints: + * - GET /lean/states/finalized (SSZ state snapshot) + * - GET /health (JSON health response) + * - GET /metrics (Prometheus metrics) + * + * @spec RFC 9110/9112 (HTTP/1.1) and leanSpec subspecs/api. */ #include "lantern/http/server.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include + +#include "lantern/http/common.h" +#include "lantern/http/metrics.h" +#include "lantern/support/log.h" + +static const size_t LANTERN_HTTP_READ_BUFFER_SIZE = 4096; +static const int LANTERN_HTTP_LISTEN_BACKLOG = 16; +static const char LANTERN_HTTP_PATH_HEALTH[] = "/health"; +static const char LANTERN_HTTP_PATH_METRICS[] = "/metrics"; +static const char LANTERN_HTTP_PATH_FINALIZED[] = "/lean/states/finalized"; +static const char LANTERN_HTTP_JSON_HEALTH[] = "{\"status\":\"healthy\",\"service\":\"lean-spec-api\"}"; +static const char LANTERN_HTTP_JSON_MALFORMED[] = "{\"error\":\"malformed request\"}"; +static const char LANTERN_HTTP_JSON_UNKNOWN_ENDPOINT[] = "{\"error\":\"unknown endpoint\"}"; +static const char LANTERN_HTTP_JSON_UNAVAILABLE[] = "{\"error\":\"service unavailable\"}"; +static const char LANTERN_HTTP_JSON_STATE_MISSING[] = "{\"error\":\"finalized state not available\"}"; +static const char LANTERN_HTTP_JSON_INTERNAL[] = "{\"error\":\"internal error\"}"; + +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_IO = -2, + LANTERN_HTTP_SERVER_ERR_FORMATTING = -3, + LANTERN_HTTP_SERVER_ERR_MALFORMED_REQUEST = -4, +} lantern_http_server_error_t; + + +/** + * 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; +} + + +/** + * Send an HTTP response and map common errors to server 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_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_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_HTTP_SERVER_ERR_INVALID_PARAM; + } + if (rc == -3) + { + return LANTERN_HTTP_SERVER_ERR_FORMATTING; + } + return LANTERN_HTTP_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_HTTP_SERVER_ERR_INVALID_PARAM on invalid parameters. + * @return LANTERN_HTTP_SERVER_ERR_FORMATTING on 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_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 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) +{ + 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[(size_t)received] = '\0'; + + char method[LANTERN_HTTP_METHOD_CAP]; + char path[LANTERN_HTTP_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", LANTERN_HTTP_JSON_MALFORMED); + if (rc == 0) + { + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "malformed request -> 400"); + } + else + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send 400 response rc=%d", + rc); + } + return; + } + + if (strcmp(method, "GET") != 0) + { + int rc = send_json_error(client_fd, 404, "Not Found", LANTERN_HTTP_JSON_UNKNOWN_ENDPOINT); + if (rc == 0) + { + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "%s %s -> 404", + method, + path); + } + else + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send 404 response rc=%d", + rc); + } + return; + } + + if (strcmp(path, LANTERN_HTTP_PATH_HEALTH) == 0) + { + int rc = send_http_response( + client_fd, + 200, + "OK", + "application/json", + LANTERN_HTTP_JSON_HEALTH, + strlen(LANTERN_HTTP_JSON_HEALTH)); + if (rc == 0) + { + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "GET %s -> 200", + path); + } + else + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send health response rc=%d", + rc); + } + return; + } + + if (strcmp(path, LANTERN_HTTP_PATH_METRICS) == 0) + { + if (!server->callbacks.metrics_snapshot) + { + int rc = send_json_error(client_fd, 503, "Service Unavailable", LANTERN_HTTP_JSON_UNAVAILABLE); + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "metrics callback missing rc=%d", + rc); + return; + } + + struct lantern_metrics_snapshot snapshot; + memset(&snapshot, 0, sizeof(snapshot)); + if (server->callbacks.metrics_snapshot(server->callbacks.context, &snapshot) != 0) + { + int rc = send_json_error(client_fd, 503, "Service Unavailable", LANTERN_HTTP_JSON_UNAVAILABLE); + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "metrics snapshot failed rc=%d", + rc); + return; + } + + char *body = NULL; + size_t body_len = 0; + result = lantern_metrics_format_prometheus(&snapshot, &body, &body_len); + if (result != 0) + { + int rc = send_json_error(client_fd, 500, "Internal Server Error", LANTERN_HTTP_JSON_INTERNAL); + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "metrics formatting failed result=%d send_rc=%d", + result, + rc); + return; + } + + result = send_http_response( + client_fd, + 200, + "OK", + LANTERN_METRICS_CONTENT_TYPE, + body, + body_len); + free(body); + if (result != 0) + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "metrics send failed rc=%d", + result); + return; + } + + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "GET %s -> 200", + path); + return; + } + + if (strcmp(path, LANTERN_HTTP_PATH_FINALIZED) == 0) + { + if (!server->callbacks.finalized_state_ssz) + { + int rc = send_json_error(client_fd, 503, "Service Unavailable", LANTERN_HTTP_JSON_UNAVAILABLE); + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "finalized state callback missing rc=%d", + rc); + return; + } + + uint8_t *state_bytes = NULL; + size_t state_len = 0; + int state_rc = server->callbacks.finalized_state_ssz( + server->callbacks.context, + &state_bytes, + &state_len); + if (state_rc != 0) + { + const char *body = LANTERN_HTTP_JSON_INTERNAL; + int status_code = 500; + const char *status_text = "Internal Server Error"; + if (state_rc == LANTERN_HTTP_CB_ERR_NOT_FOUND) + { + body = LANTERN_HTTP_JSON_STATE_MISSING; + status_code = 404; + status_text = "Not Found"; + } + else if (state_rc == LANTERN_HTTP_CB_ERR_INVALID_STATE + || state_rc == LANTERN_HTTP_CB_ERR_LOCK_FAILED + || state_rc == LANTERN_HTTP_CB_ERR_UNAVAILABLE) + { + body = LANTERN_HTTP_JSON_UNAVAILABLE; + status_code = 503; + status_text = "Service Unavailable"; + } + + if (state_bytes) + { + free(state_bytes); + } + + int rc = send_json_error(client_fd, status_code, status_text, body); + if (state_rc == LANTERN_HTTP_CB_ERR_NOT_FOUND) + { + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "finalized state missing -> 404"); + } + else if (state_rc == LANTERN_HTTP_CB_ERR_INVALID_STATE + || state_rc == LANTERN_HTTP_CB_ERR_LOCK_FAILED + || state_rc == LANTERN_HTTP_CB_ERR_UNAVAILABLE) + { + lantern_log_warn( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "finalized state unavailable rc=%d send_rc=%d", + state_rc, + rc); + } + else + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "finalized state failed rc=%d send_rc=%d", + state_rc, + rc); + } + return; + } + + if (!state_bytes || state_len == 0) + { + if (state_bytes) + { + free(state_bytes); + } + int rc = send_json_error(client_fd, 500, "Internal Server Error", LANTERN_HTTP_JSON_INTERNAL); + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "finalized state empty send_rc=%d", + rc); + return; + } + + result = send_http_response( + client_fd, + 200, + "OK", + "application/octet-stream", + (const char *)state_bytes, + state_len); + free(state_bytes); + if (result != 0) + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "finalized state send failed rc=%d", + result); + return; + } + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "GET %s -> 200", + path); + return; + } + + { + int rc = send_json_error(client_fd, 404, "Not Found", LANTERN_HTTP_JSON_UNKNOWN_ENDPOINT); + if (rc == 0) + { + lantern_log_info( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "%s %s -> 404", + method, + path); + } + else + { + lantern_log_error( + "http", + &(const struct lantern_log_metadata){.peer = peer_text}, + "failed to send 404 response rc=%d", + rc); + } + } +} + + +/** + * 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_thread(void *arg) +{ + struct lantern_http_server *server = arg; + 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(listen_fd, (struct sockaddr *)&peer, &peer_len); + if (client_fd < 0) + { + if (errno == EINTR) + { + continue; + } + if (!server->running) + { + break; + } + if (errno == EBADF || errno == EINVAL) + { + break; + } + if (errno == ECONNABORTED) + { + continue; + } + lantern_log_error("http", NULL, "accept failed errno=%d", errno); + continue; + } + + handle_client_connection(server, client_fd, &peer); + close(client_fd); + } + + return NULL; +} + + +/** + * 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) @@ -25,6 +642,14 @@ void lantern_http_server_init(struct lantern_http_server *server) server->port = 0; } + +/** + * 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) @@ -36,22 +661,105 @@ void lantern_http_server_reset(struct lantern_http_server *server) lantern_http_server_init(server); } + +/** + * Start the HTTP server. + * + * Creates a listening socket and starts a background thread to accept incoming + * connections and serve health, metrics, and checkpoint sync endpoints. + * + * @param server Server instance to start (modified in place). + * @param config Server configuration (port and callbacks). + * + * @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 -1; + return LANTERN_HTTP_SERVER_ERR_INVALID_PARAM; } - server->listen_fd = -1; - server->running = 0; + int fd = socket(AF_INET, SOCK_STREAM, 0); + 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) + { + lantern_log_warn("http", NULL, "setsockopt(SO_REUSEADDR) failed errno=%d", errno); + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(config->port); + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { + lantern_log_error("http", NULL, "bind failed errno=%d", errno); + close(fd); + return LANTERN_HTTP_SERVER_ERR_IO; + } + + if (listen(fd, LANTERN_HTTP_LISTEN_BACKLOG) != 0) + { + lantern_log_error("http", NULL, "listen failed errno=%d", errno); + close(fd); + return LANTERN_HTTP_SERVER_ERR_IO; + } + + server->listen_fd = fd; + server->callbacks = config->callbacks; + server->port = config->port; + server->running = 1; server->thread_started = 0; - server->port = 0; - return -1; + + if (server->port == 0) + { + struct sockaddr_in bound_addr; + socklen_t bound_len = sizeof(bound_addr); + if (getsockname(fd, (struct sockaddr *)&bound_addr, &bound_len) == 0) + { + server->port = ntohs(bound_addr.sin_port); + } + } + + int create_rc = pthread_create(&server->thread, NULL, lantern_http_thread, server); + 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 LANTERN_HTTP_SERVER_ERR_IO; + } + + server->thread_started = 1; + lantern_log_info("http", NULL, "http server listening port=%" PRIu16, server->port); + return 0; } + +/** + * 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) @@ -60,7 +768,18 @@ void lantern_http_server_stop(struct lantern_http_server *server) } server->running = 0; + + int listen_fd = server->listen_fd; server->listen_fd = -1; - server->thread_started = 0; - server->port = 0; + if (listen_fd >= 0) + { + (void)shutdown(listen_fd, SHUT_RDWR); + close(listen_fd); + } + + if (server->thread_started) + { + pthread_join(server->thread, NULL); + server->thread_started = 0; + } } diff --git a/src/metrics/lean_metrics.c b/src/metrics/lean_metrics.c index 2593a41..0952a00 100644 --- a/src/metrics/lean_metrics.c +++ b/src/metrics/lean_metrics.c @@ -15,10 +15,16 @@ struct lean_histogram { static const double kDefaultShortBuckets[] = {0.005, 0.01, 0.025, 0.05, 0.1, 1.0}; static const double kStateTransitionBuckets[] = {0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0}; +static const double kReorgDepthBuckets[] = {1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0, 50.0, 100.0}; static pthread_mutex_t g_metrics_lock = PTHREAD_MUTEX_INITIALIZER; static uint64_t g_attestations_valid_total = 0; static uint64_t g_attestations_invalid_total = 0; +static uint64_t g_fork_choice_reorgs_total = 0; +static uint64_t g_finalizations_success_total = 0; +static uint64_t g_finalizations_error_total = 0; +static uint64_t g_peer_connection_events_total[LEAN_METRICS_DIR_COUNT][LEAN_METRICS_CONN_RESULT_COUNT]; +static uint64_t g_peer_disconnection_events_total[LEAN_METRICS_DIR_COUNT][LEAN_METRICS_DISCONNECT_REASON_COUNT]; static uint64_t g_state_slots_processed_total = 0; static uint64_t g_state_attestations_processed_total = 0; @@ -26,6 +32,10 @@ static struct lean_histogram g_hist_fork_choice_block = { .bounds = kDefaultShortBuckets, .bucket_count = ARRAY_LEN(kDefaultShortBuckets), }; +static struct lean_histogram g_hist_fork_choice_reorg_depth = { + .bounds = kReorgDepthBuckets, + .bucket_count = ARRAY_LEN(kReorgDepthBuckets), +}; static struct lean_histogram g_hist_attestation_validation = { .bounds = kDefaultShortBuckets, .bucket_count = ARRAY_LEN(kDefaultShortBuckets), @@ -118,9 +128,15 @@ void lean_metrics_reset(void) { pthread_mutex_lock(&g_metrics_lock); g_attestations_valid_total = 0; g_attestations_invalid_total = 0; + g_fork_choice_reorgs_total = 0; + g_finalizations_success_total = 0; + g_finalizations_error_total = 0; + memset(g_peer_connection_events_total, 0, sizeof(g_peer_connection_events_total)); + memset(g_peer_disconnection_events_total, 0, sizeof(g_peer_disconnection_events_total)); g_state_slots_processed_total = 0; g_state_attestations_processed_total = 0; histogram_reset(&g_hist_fork_choice_block); + histogram_reset(&g_hist_fork_choice_reorg_depth); histogram_reset(&g_hist_attestation_validation); histogram_reset(&g_hist_state_transition); histogram_reset(&g_hist_state_slots); @@ -137,6 +153,16 @@ void lean_metrics_record_fork_choice_block_time(double seconds) { pthread_mutex_unlock(&g_metrics_lock); } +void lean_metrics_record_fork_choice_reorg(size_t depth) { + if (depth == 0) { + return; + } + pthread_mutex_lock(&g_metrics_lock); + g_fork_choice_reorgs_total += 1; + histogram_observe(&g_hist_fork_choice_reorg_depth, (double)depth); + pthread_mutex_unlock(&g_metrics_lock); +} + void lean_metrics_record_attestation_validation(double seconds, bool valid) { pthread_mutex_lock(&g_metrics_lock); if (valid) { @@ -174,6 +200,16 @@ void lean_metrics_record_state_transition_attestations(uint64_t count, double se pthread_mutex_unlock(&g_metrics_lock); } +void lean_metrics_record_finalization_attempt(bool success) { + pthread_mutex_lock(&g_metrics_lock); + if (success) { + g_finalizations_success_total += 1; + } else { + g_finalizations_error_total += 1; + } + pthread_mutex_unlock(&g_metrics_lock); +} + void lean_metrics_record_pq_signature_signing(double seconds) { pthread_mutex_lock(&g_metrics_lock); histogram_observe(&g_hist_pq_signature_signing, seconds); @@ -186,6 +222,28 @@ void lean_metrics_record_pq_signature_verification(double seconds) { pthread_mutex_unlock(&g_metrics_lock); } +void lean_metrics_record_peer_connection( + lean_metrics_direction_t direction, + lean_metrics_connection_result_t result) { + if (direction >= LEAN_METRICS_DIR_COUNT || result >= LEAN_METRICS_CONN_RESULT_COUNT) { + return; + } + pthread_mutex_lock(&g_metrics_lock); + g_peer_connection_events_total[direction][result] += 1; + pthread_mutex_unlock(&g_metrics_lock); +} + +void lean_metrics_record_peer_disconnection( + lean_metrics_direction_t direction, + lean_metrics_disconnection_reason_t reason) { + if (direction >= LEAN_METRICS_DIR_COUNT || reason >= LEAN_METRICS_DISCONNECT_REASON_COUNT) { + return; + } + pthread_mutex_lock(&g_metrics_lock); + g_peer_disconnection_events_total[direction][reason] += 1; + pthread_mutex_unlock(&g_metrics_lock); +} + void lean_metrics_snapshot(struct lean_metrics_snapshot *out) { if (!out) { return; @@ -193,9 +251,23 @@ void lean_metrics_snapshot(struct lean_metrics_snapshot *out) { pthread_mutex_lock(&g_metrics_lock); out->attestations_valid_total = g_attestations_valid_total; out->attestations_invalid_total = g_attestations_invalid_total; + out->fork_choice_reorgs_total = g_fork_choice_reorgs_total; + out->finalizations_success_total = g_finalizations_success_total; + out->finalizations_error_total = g_finalizations_error_total; + for (size_t dir = 0; dir < LEAN_METRICS_DIR_COUNT; ++dir) { + for (size_t res = 0; res < LEAN_METRICS_CONN_RESULT_COUNT; ++res) { + out->peer_connection_events_total[dir][res] = g_peer_connection_events_total[dir][res]; + } + } + for (size_t dir = 0; dir < LEAN_METRICS_DIR_COUNT; ++dir) { + for (size_t reason = 0; reason < LEAN_METRICS_DISCONNECT_REASON_COUNT; ++reason) { + out->peer_disconnection_events_total[dir][reason] = g_peer_disconnection_events_total[dir][reason]; + } + } out->state_transition_slots_processed_total = g_state_slots_processed_total; out->state_transition_attestations_processed_total = g_state_attestations_processed_total; histogram_snapshot(&out->fork_choice_block_time, &g_hist_fork_choice_block); + histogram_snapshot(&out->fork_choice_reorg_depth, &g_hist_fork_choice_reorg_depth); histogram_snapshot(&out->attestation_validation_time, &g_hist_attestation_validation); histogram_snapshot(&out->state_transition_time, &g_hist_state_transition); histogram_snapshot(&out->state_slots_time, &g_hist_state_slots); diff --git a/src/networking/gossipsub_service.c b/src/networking/gossipsub_service.c index 45c75ca..46cb0a4 100644 --- a/src/networking/gossipsub_service.c +++ b/src/networking/gossipsub_service.c @@ -457,7 +457,8 @@ static libp2p_gossipsub_validation_result_t gossipsub_block_validator( goto cleanup; } - if (service->block_handler(&block, msg->from, service->block_handler_user_data) != 0) { + if (service->block_handler(&block, msg->from, service->block_handler_user_data) != 0) + { result = LIBP2P_GOSSIPSUB_VALIDATION_IGNORE; } char block_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; @@ -470,13 +471,26 @@ static libp2p_gossipsub_validation_result_t gossipsub_block_validator( != 0) { block_root_hex[0] = '\0'; } - lantern_log_debug( - "gossip", - &meta, - "accepted block gossip slot=%" PRIu64 " proposer=%" PRIu64 " parent=%s", - block.message.block.slot, - block.message.block.proposer_index, - block_root_hex[0] ? block_root_hex : "0x0"); + if (result == LIBP2P_GOSSIPSUB_VALIDATION_ACCEPT) + { + lantern_log_debug( + "gossip", + &meta, + "accepted block gossip slot=%" PRIu64 " proposer=%" PRIu64 " parent=%s", + block.message.block.slot, + block.message.block.proposer_index, + block_root_hex[0] ? block_root_hex : "0x0"); + } + else if (result == LIBP2P_GOSSIPSUB_VALIDATION_IGNORE) + { + lantern_log_debug( + "gossip", + &meta, + "ignored block gossip slot=%" PRIu64 " proposer=%" PRIu64 " parent=%s", + block.message.block.slot, + block.message.block.proposer_index, + block_root_hex[0] ? block_root_hex : "0x0"); + } cleanup: lantern_signed_block_with_attestation_reset(&block); @@ -524,7 +538,8 @@ static libp2p_gossipsub_validation_result_t gossipsub_vote_validator( goto cleanup; } - if (service->vote_handler(&vote, msg->from, service->vote_handler_user_data) != 0) { + if (service->vote_handler(&vote, msg->from, service->vote_handler_user_data) != 0) + { result = LIBP2P_GOSSIPSUB_VALIDATION_IGNORE; } char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; @@ -537,13 +552,26 @@ static libp2p_gossipsub_validation_result_t gossipsub_vote_validator( != 0) { head_hex[0] = '\0'; } - lantern_log_debug( - "gossip", - &meta, - "accepted vote gossip validator=%" PRIu64 " slot=%" PRIu64 " head=%s", - vote.data.validator_id, - vote.data.slot, - head_hex[0] ? head_hex : "0x0"); + if (result == LIBP2P_GOSSIPSUB_VALIDATION_ACCEPT) + { + lantern_log_debug( + "gossip", + &meta, + "accepted vote gossip validator=%" PRIu64 " slot=%" PRIu64 " head=%s", + vote.data.validator_id, + vote.data.slot, + head_hex[0] ? head_hex : "0x0"); + } + else if (result == LIBP2P_GOSSIPSUB_VALIDATION_IGNORE) + { + lantern_log_debug( + "gossip", + &meta, + "ignored vote gossip validator=%" PRIu64 " slot=%" PRIu64 " head=%s", + vote.data.validator_id, + vote.data.slot, + head_hex[0] ? head_hex : "0x0"); + } cleanup: return result; diff --git a/src/networking/reqresp_service.c b/src/networking/reqresp_service.c index 0d3b740..bc99204 100644 --- a/src/networking/reqresp_service.c +++ b/src/networking/reqresp_service.c @@ -55,14 +55,7 @@ uint32_t lantern_reqresp_stall_timeout_ms(void) { } bool lantern_reqresp_debug_bytes_enabled(void) { - static bool initialized = false; - static bool enabled = false; - if (!initialized) { - const char *env = getenv("LANTERN_DEBUG_REQRESP_BYTES"); - enabled = env && env[0] != '\0' && !(env[0] == '0' && env[1] == '\0'); - initialized = true; - } - return enabled; + return false; } uint64_t lantern_reqresp_debug_sequence_next(void) { diff --git a/src/storage/storage.c b/src/storage/storage.c index ad99308..639b2ad 100644 --- a/src/storage/storage.c +++ b/src/storage/storage.c @@ -29,9 +29,14 @@ #define LANTERN_STORAGE_VOTES_MAGIC "LNVOTES\0" #define LANTERN_STORAGE_VOTES_VERSION 2u #define LANTERN_STORAGE_BLOCKS_DIR "blocks" +#define LANTERN_STORAGE_STATES_DIR "states" +#define LANTERN_STORAGE_INDICES_DIR "indices" +#define LANTERN_STORAGE_SLOT_INDEX_DIR "slots" #define LANTERN_STORAGE_STATE_FILE "state.ssz" #define LANTERN_STORAGE_STATE_META_FILE "state.meta" #define LANTERN_STORAGE_VOTES_FILE "votes.bin" +#define LANTERN_STORAGE_HEAD_FILE "head.bin" +#define LANTERN_STORAGE_CHECKPOINTS_FILE "checkpoints.bin" #define LANTERN_STORAGE_STATE_META_VERSION 1u #if defined(_WIN32) @@ -55,6 +60,16 @@ struct lantern_storage_state_meta { uint64_t justified_slots_offset; }; +struct lantern_storage_head_record { + uint64_t slot; + LanternRoot root; +}; + +struct lantern_storage_checkpoint_record { + LanternCheckpoint justified; + LanternCheckpoint finalized; +}; + static int ensure_directory(const char *path) { if (!path) { return -1; @@ -432,6 +447,19 @@ static int write_state_meta(const char *data_dir, const LanternState *state) { return rc; } +static int write_state_meta_path(const char *path, const LanternState *state) { + if (!path || !state) { + return -1; + } + struct lantern_storage_state_meta meta = { + .version = LANTERN_STORAGE_STATE_META_VERSION, + .reserved = 0, + .historical_roots_offset = state->historical_roots_offset, + .justified_slots_offset = state->justified_slots_offset, + }; + return write_atomic_file(path, (const uint8_t *)&meta, sizeof(meta)); +} + static int read_state_meta(const char *data_dir, struct lantern_storage_state_meta *meta) { if (!data_dir || !meta) { return -1; @@ -464,6 +492,24 @@ static int build_blocks_dir(const char *data_dir, char **out_path) { return join_path(data_dir, LANTERN_STORAGE_BLOCKS_DIR, out_path); } +static int build_states_dir(const char *data_dir, char **out_path) { + return join_path(data_dir, LANTERN_STORAGE_STATES_DIR, out_path); +} + +static int build_indices_dir(const char *data_dir, char **out_path) { + return join_path(data_dir, LANTERN_STORAGE_INDICES_DIR, out_path); +} + +static int build_slot_index_dir(const char *data_dir, char **out_path) { + char *indices_dir = NULL; + if (build_indices_dir(data_dir, &indices_dir) != 0) { + return -1; + } + int rc = join_path(indices_dir, LANTERN_STORAGE_SLOT_INDEX_DIR, out_path); + free_path(indices_dir); + return rc; +} + int lantern_storage_prepare(const char *data_dir) { if (!data_dir) { return -1; @@ -477,6 +523,33 @@ int lantern_storage_prepare(const char *data_dir) { } int rc = ensure_directory(blocks_dir); free_path(blocks_dir); + if (rc != 0) { + return rc; + } + char *states_dir = NULL; + if (build_states_dir(data_dir, &states_dir) != 0) { + return -1; + } + rc = ensure_directory(states_dir); + free_path(states_dir); + if (rc != 0) { + return rc; + } + char *indices_dir = NULL; + if (build_indices_dir(data_dir, &indices_dir) != 0) { + return -1; + } + rc = ensure_directory(indices_dir); + free_path(indices_dir); + if (rc != 0) { + return rc; + } + char *slot_dir = NULL; + if (build_slot_index_dir(data_dir, &slot_dir) != 0) { + return -1; + } + rc = ensure_directory(slot_dir); + free_path(slot_dir); return rc; } @@ -799,6 +872,226 @@ int lantern_storage_store_block(const char *data_dir, const LanternSignedBlock * return rc; } +int lantern_storage_store_state_for_root( + const char *data_dir, + const LanternRoot *root, + const LanternState *state) { + if (!data_dir || !root || !state || state->config.num_validators == 0) { + return -1; + } + size_t encoded_size = state_encoded_size(state); + if (encoded_size == 0) { + return -1; + } + uint8_t *buffer = malloc(encoded_size); + if (!buffer) { + return -1; + } + size_t written = 0; + if (lantern_ssz_encode_state(state, buffer, encoded_size, &written) != 0 || written != encoded_size) { + free(buffer); + return -1; + } + char *states_dir = NULL; + if (build_states_dir(data_dir, &states_dir) != 0) { + free(buffer); + return -1; + } + if (ensure_directory(states_dir) != 0) { + free_path(states_dir); + free(buffer); + return -1; + } + char root_hex[2u * LANTERN_ROOT_SIZE + 1u]; + if (lantern_bytes_to_hex(root->bytes, LANTERN_ROOT_SIZE, root_hex, sizeof(root_hex), 0) != 0) { + free_path(states_dir); + free(buffer); + return -1; + } + char filename[sizeof(root_hex) + 4]; + int name_written = snprintf(filename, sizeof(filename), "%s.ssz", root_hex); + if (name_written < 0 || (size_t)name_written >= sizeof(filename)) { + free_path(states_dir); + free(buffer); + return -1; + } + char *state_path = NULL; + if (join_path(states_dir, filename, &state_path) != 0) { + free_path(states_dir); + free(buffer); + return -1; + } + int rc = write_atomic_file(state_path, buffer, written); + free(buffer); + if (rc != 0) { + free_path(state_path); + free_path(states_dir); + return rc; + } + char meta_name[sizeof(root_hex) + 6]; + int meta_written = snprintf(meta_name, sizeof(meta_name), "%s.meta", root_hex); + if (meta_written < 0 || (size_t)meta_written >= sizeof(meta_name)) { + free_path(state_path); + free_path(states_dir); + return -1; + } + char *meta_path = NULL; + if (join_path(states_dir, meta_name, &meta_path) != 0) { + free_path(state_path); + free_path(states_dir); + return -1; + } + int meta_rc = write_state_meta_path(meta_path, state); + free_path(meta_path); + free_path(state_path); + free_path(states_dir); + return meta_rc; +} + +int lantern_storage_load_state_bytes_for_root( + const char *data_dir, + const LanternRoot *root, + uint8_t **out_data, + size_t *out_len) { + if (!data_dir || !root || !out_data || !out_len) { + return -1; + } + *out_data = NULL; + *out_len = 0; + + char *states_dir = NULL; + if (build_states_dir(data_dir, &states_dir) != 0) { + return -1; + } + + char root_hex[2u * LANTERN_ROOT_SIZE + 1u]; + if (lantern_bytes_to_hex(root->bytes, LANTERN_ROOT_SIZE, root_hex, sizeof(root_hex), 0) != 0) { + free_path(states_dir); + return -1; + } + char filename[sizeof(root_hex) + 4]; + int name_written = snprintf(filename, sizeof(filename), "%s.ssz", root_hex); + if (name_written < 0 || (size_t)name_written >= sizeof(filename)) { + free_path(states_dir); + return -1; + } + char *state_path = NULL; + if (join_path(states_dir, filename, &state_path) != 0) { + free_path(states_dir); + return -1; + } + free_path(states_dir); + + uint8_t *data = NULL; + size_t len = 0; + int read_rc = read_file_buffer(state_path, &data, &len); + free_path(state_path); + if (read_rc != 0) { + if (data) { + free(data); + } + return read_rc > 0 ? 1 : -1; + } + *out_data = data; + *out_len = len; + return 0; +} + +int lantern_storage_store_slot_root( + const char *data_dir, + uint64_t slot, + const LanternRoot *root) { + if (!data_dir || !root) { + return -1; + } + char *slot_dir = NULL; + if (build_slot_index_dir(data_dir, &slot_dir) != 0) { + return -1; + } + if (ensure_directory(slot_dir) != 0) { + free_path(slot_dir); + return -1; + } + char filename[64]; + int written = snprintf(filename, sizeof(filename), "%" PRIu64 ".root", slot); + if (written < 0 || (size_t)written >= sizeof(filename)) { + free_path(slot_dir); + return -1; + } + char *slot_path = NULL; + if (join_path(slot_dir, filename, &slot_path) != 0) { + free_path(slot_dir); + return -1; + } + free_path(slot_dir); + int rc = write_atomic_file(slot_path, root->bytes, LANTERN_ROOT_SIZE); + free_path(slot_path); + return rc; +} + +int lantern_storage_store_head_root( + const char *data_dir, + uint64_t slot, + const LanternRoot *root) { + if (!data_dir || !root) { + return -1; + } + char *indices_dir = NULL; + if (build_indices_dir(data_dir, &indices_dir) != 0) { + return -1; + } + if (ensure_directory(indices_dir) != 0) { + free_path(indices_dir); + return -1; + } + char *head_path = NULL; + if (join_path(indices_dir, LANTERN_STORAGE_HEAD_FILE, &head_path) != 0) { + free_path(indices_dir); + return -1; + } + free_path(indices_dir); + struct lantern_storage_head_record record = { + .slot = slot, + .root = *root, + }; + int rc = write_atomic_file(head_path, (const uint8_t *)&record, sizeof(record)); + free_path(head_path); + return rc; +} + +int lantern_storage_store_checkpoints( + const char *data_dir, + const LanternCheckpoint *justified, + const LanternCheckpoint *finalized) { + if (!data_dir || !justified || !finalized) { + return -1; + } + char *indices_dir = NULL; + if (build_indices_dir(data_dir, &indices_dir) != 0) { + return -1; + } + if (ensure_directory(indices_dir) != 0) { + free_path(indices_dir); + return -1; + } + char *checkpoint_path = NULL; + if (join_path(indices_dir, LANTERN_STORAGE_CHECKPOINTS_FILE, &checkpoint_path) != 0) { + free_path(indices_dir); + return -1; + } + free_path(indices_dir); + struct lantern_storage_checkpoint_record record = { + .justified = *justified, + .finalized = *finalized, + }; + int rc = write_atomic_file( + checkpoint_path, + (const uint8_t *)&record, + sizeof(record)); + free_path(checkpoint_path); + return rc; +} + int lantern_storage_collect_blocks( const char *data_dir, const LanternRoot *roots, diff --git a/src/support/log.c b/src/support/log.c index 81a30cc..e55624e 100644 --- a/src/support/log.c +++ b/src/support/log.c @@ -313,12 +313,14 @@ void lantern_log_log( * - Context: dim */ + static const char kUnknownTimestamp[] = "????" "-??" "-?? ??:??:??.???"; + if (colorize) { /* Colored output with selective coloring */ fprintf( target, "%s%s%s %s%s%s %s[%s]%s %s", - ANSI_DIM, timestamp[0] ? timestamp : "????-??-?? ??:??:??.???", ANSI_RESET, + ANSI_DIM, timestamp[0] ? timestamp : kUnknownTimestamp, ANSI_RESET, level_to_color(level), level_to_string(level), ANSI_RESET, ANSI_CYAN, component ? component : "?", ANSI_RESET, formatted); @@ -342,7 +344,7 @@ void lantern_log_log( fprintf( target, "%s %s [%s] %s", - timestamp[0] ? timestamp : "????-??-?? ??:??:??.???", + timestamp[0] ? timestamp : kUnknownTimestamp, level_to_string(level), component ? component : "?", formatted); diff --git a/tests/integration/consensus_fixture_runner.c b/tests/integration/consensus_fixture_runner.c index 6261e53..1332b18 100644 --- a/tests/integration/consensus_fixture_runner.c +++ b/tests/integration/consensus_fixture_runner.c @@ -268,52 +268,6 @@ static int stored_state_save( } } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - LanternRoot original_root; - if (lantern_hash_tree_root_state(state, &original_root) == 0) { - LanternState decoded; - lantern_state_init(&decoded); - if (lantern_ssz_decode_state(&decoded, encoded, encoded_len) == 0) { - LanternRoot decoded_root; - if (lantern_hash_tree_root_state(&decoded, &decoded_root) == 0) { - char original_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char decoded_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char key_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - original_root.bytes, - LANTERN_ROOT_SIZE, - original_hex, - sizeof(original_hex), - 1) - == 0 - && lantern_bytes_to_hex( - decoded_root.bytes, - LANTERN_ROOT_SIZE, - decoded_hex, - sizeof(decoded_hex), - 1) - == 0 - && lantern_bytes_to_hex( - root->bytes, - LANTERN_ROOT_SIZE, - key_hex, - sizeof(key_hex), - 1) - == 0) { - fprintf( - stderr, - "stored state key=%s original=%s decoded=%s\n", - key_hex, - original_hex, - decoded_hex); - } - } - } - lantern_state_reset(&decoded); - } - } - int add_status = stored_state_add(entries_ptr, count_ptr, cap_ptr, root, encoded, encoded_len, votes, vote_capacity); if (add_status != 0) { free(votes); @@ -372,26 +326,6 @@ static int stored_state_restore( if (profiling) { profile_record(&g_profile_restore_state, profile_now() - start); } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (rc == 0 && debug_hash && debug_hash[0] != '\0') { - LanternRoot restored_root; - if (lantern_hash_tree_root_state(state, &restored_root) == 0) { - char restored_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - restored_root.bytes, - LANTERN_ROOT_SIZE, - restored_hex, - sizeof(restored_hex), - 1) - == 0) { - fprintf( - stderr, - "restored state slot %" PRIu64 " root: %s\n", - (unsigned long long)state->slot, - restored_hex); - } - } - } return rc; } @@ -829,11 +763,6 @@ static int run_state_transition_fixture(const char *path) { return 0; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - fprintf(stderr, "fixture: %s\n", path); - } - int pre_idx = lantern_fixture_object_get_field(&doc, case_idx, "pre"); int blocks_idx = lantern_fixture_object_get_field(&doc, case_idx, "blocks"); int post_idx = lantern_fixture_object_get_field(&doc, case_idx, "post"); @@ -989,11 +918,6 @@ static int run_fork_choice_fixture(const char *path) { return 0; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - fprintf(stderr, "fork fixture: %s\n", path); - } - int anchor_state_idx = lantern_fixture_object_get_field(&doc, case_idx, "anchorState"); int anchor_block_idx = lantern_fixture_object_get_field(&doc, case_idx, "anchorBlock"); int steps_idx = lantern_fixture_object_get_field(&doc, case_idx, "steps"); @@ -1184,21 +1108,6 @@ static int run_fork_choice_fixture(const char *path) { transition_performed = true; block_justified = state.latest_justified; block_finalized = state.latest_finalized; - if (debug_hash && debug_hash[0] != '\0') { - LanternRoot post_transition_root; - if (lantern_hash_tree_root_state(&state, &post_transition_root) == 0) { - char post_transition_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - post_transition_root.bytes, - LANTERN_ROOT_SIZE, - post_transition_hex, - sizeof(post_transition_hex), - 1) - == 0) { - fprintf(stderr, "state after transition root=%s\n", post_transition_hex); - } - } - } } else { active_state = &state; block_justified = state.latest_justified; @@ -1278,37 +1187,7 @@ static int run_fork_choice_fixture(const char *path) { proposer_vote.source.root = signed_block.message.parent_root; proposer_vote.source.slot = has_parent_info ? parent_slot : 0; - if (debug_hash && debug_hash[0] != '\0') { - char block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex(block_root.bytes, LANTERN_ROOT_SIZE, block_hex, sizeof(block_hex), 1) != 0) { - block_hex[0] = '\0'; - } - fprintf( - stderr, - "fork step %d slot %" PRIu64 " extends=%d transition=%d block=%s\n", - i, - signed_block.message.slot, - extends_canonical ? 1 : 0, - transition_performed ? 1 : 0, - block_hex[0] ? block_hex : "0x0"); - } - if (transition_performed) { - if (debug_hash && debug_hash[0] != '\0') { - LanternRoot pre_vote_root; - if (lantern_hash_tree_root_state(active_state, &pre_vote_root) == 0) { - char pre_vote_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - pre_vote_root.bytes, - LANTERN_ROOT_SIZE, - pre_vote_hex, - sizeof(pre_vote_hex), - 1) - == 0) { - fprintf(stderr, "state before vote root=%s\n", pre_vote_hex); - } - } - } if (lantern_state_set_validator_vote( active_state, (size_t)signed_block.message.proposer_index, @@ -1325,21 +1204,6 @@ static int run_fork_choice_fixture(const char *path) { stored_state_entries_reset(&stored_states, &stored_states_count, &stored_states_cap); return -1; } - if (debug_hash && debug_hash[0] != '\0') { - LanternRoot post_vote_root; - if (lantern_hash_tree_root_state(active_state, &post_vote_root) == 0) { - char post_vote_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - post_vote_root.bytes, - LANTERN_ROOT_SIZE, - post_vote_hex, - sizeof(post_vote_hex), - 1) - == 0) { - fprintf(stderr, "state after vote root=%s\n", post_vote_hex); - } - } - } if (stored_state_save(&stored_states, &stored_states_count, &stored_states_cap, &block_root, active_state) != 0) { if (branch_state_initialized) { lantern_state_reset(&branch_state); diff --git a/tests/integration/test_verify_signatures_vectors.c b/tests/integration/test_verify_signatures_vectors.c index 8dcb2ab..58448a5 100644 --- a/tests/integration/test_verify_signatures_vectors.c +++ b/tests/integration/test_verify_signatures_vectors.c @@ -43,6 +43,14 @@ static bool pubkey_is_zero(const uint8_t *pubkey) { return true; } +// leanSpec fixtures currently emit 0x00 for aggregated proofs when test_mode is enabled. +static bool is_placeholder_agg_proof(const LanternByteList *proof) { + if (!proof || !proof->data) { + return false; + } + return proof->length == 1 && proof->data[0] == 0u; +} + static bool verify_aggregated_attestations( const LanternState *state, const LanternSignedBlock *block, @@ -132,6 +140,14 @@ static bool verify_aggregated_attestations( fprintf(stderr, "%s: failed to hash attestation data\n", path ? path : "(unknown)"); return false; } + if (is_placeholder_agg_proof(&proof->proof_data)) { + fprintf( + stderr, + "%s: skipping aggregated signature verification (placeholder proof data)\n", + path ? path : "(unknown)"); + free(pubkeys); + continue; + } bool sig_ok = lantern_signature_verify_aggregated( pubkeys, participant_count, diff --git a/tests/support/fixture_loader.c b/tests/support/fixture_loader.c index 9a13c71..d78b930 100644 --- a/tests/support/fixture_loader.c +++ b/tests/support/fixture_loader.c @@ -31,10 +31,6 @@ int lantern_fixture_read_text_file(const char *path, char **out_buf) { } FILE *file = fopen(path, "rb"); if (!file) { - const char *debug = getenv("LANTERN_DEBUG_FIXTURES"); - if (debug && debug[0] != '\0') { - fprintf(stderr, "fixture fopen failed: %s\n", path); - } perror("fopen"); return -1; } @@ -1644,19 +1640,6 @@ int lantern_fixture_parse_anchor_state( free(validator_pubkeys); return -1; } - const char *debug_hash = getenv("LANTERN_DEBUG_STATE_HASH"); - if (debug_hash && debug_hash[0] != '\0') { - char validators_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - if (lantern_bytes_to_hex( - state->validator_registry_root.bytes, - LANTERN_ROOT_SIZE, - validators_hex, - sizeof(validators_hex), - 1) - == 0) { - fprintf(stderr, "fixture validators root: %s\n", validators_hex); - } - } free(validator_pubkeys); int slot_idx = lantern_fixture_object_get_field(doc, anchor_state_index, "slot"); diff --git a/tests/unit/test_checkpoint_sync_api.c b/tests/unit/test_checkpoint_sync_api.c new file mode 100644 index 0000000..acd5a08 --- /dev/null +++ b/tests/unit/test_checkpoint_sync_api.c @@ -0,0 +1,486 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "lantern/consensus/state.h" +#include "lantern/consensus/ssz.h" +#include "lantern/http/server.h" +#include "lantern/storage/storage.h" +#include "lantern/support/strings.h" + +struct checkpoint_fixture +{ + char *data_dir; + LanternState state; + LanternRoot root; + uint8_t *ssz_bytes; + size_t ssz_len; +}; + +struct checkpoint_callback_ctx +{ + const uint8_t *data; + size_t len; + int rc; +}; + +static void expect_zero(int rc, const char *label) +{ + if (rc != 0) + { + fprintf(stderr, "%s failed rc=%d (errno=%d)\n", label, rc, errno); + exit(EXIT_FAILURE); + } +} + +static void expect_true(bool value, const char *label) +{ + if (!value) + { + fprintf(stderr, "%s expected true\n", label); + exit(EXIT_FAILURE); + } +} + +static void cleanup_path(const char *path) +{ + if (!path) + { + return; + } + if (unlink(path) != 0 && errno != ENOENT) + { + fprintf(stderr, "failed to remove %s: %s\n", path, strerror(errno)); + exit(EXIT_FAILURE); + } +} + +static void cleanup_dir(const char *path) +{ + if (!path) + { + return; + } + if (rmdir(path) != 0 && errno != ENOENT) + { + fprintf(stderr, "failed to remove dir %s: %s\n", path, strerror(errno)); + exit(EXIT_FAILURE); + } +} + +static int build_checkpoint_fixture(struct checkpoint_fixture *fixture) +{ + if (!fixture) + { + return 1; + } + memset(fixture, 0, sizeof(*fixture)); + + char dir_template[] = "/tmp/lantern_checkpoint_syncXXXXXX"; + char *temp_dir = mkdtemp(dir_template); + if (!temp_dir) + { + perror("mkdtemp"); + return 1; + } + fixture->data_dir = strdup(temp_dir); + if (!fixture->data_dir) + { + perror("strdup"); + return 1; + } + + lantern_state_init(&fixture->state); + if (lantern_state_generate_genesis(&fixture->state, 1234u, 4u) != 0) + { + fprintf(stderr, "failed to generate genesis state\n"); + return 1; + } + + uint8_t pubkeys[4u * LANTERN_VALIDATOR_PUBKEY_SIZE]; + for (size_t i = 0; i < sizeof(pubkeys); ++i) + { + pubkeys[i] = (uint8_t)(0x20 + (i & 0x3Fu)); + } + if (lantern_state_set_validator_pubkeys(&fixture->state, pubkeys, 4u) != 0) + { + fprintf(stderr, "failed to set validator pubkeys\n"); + return 1; + } + + memset(&fixture->root, 0x42, sizeof(fixture->root)); + fixture->state.latest_finalized.root = fixture->root; + fixture->state.latest_finalized.slot = 0; + + if (lantern_storage_store_state_for_root(fixture->data_dir, &fixture->root, &fixture->state) != 0) + { + fprintf(stderr, "failed to store state for root\n"); + return 1; + } + + if (lantern_storage_load_state_bytes_for_root( + fixture->data_dir, + &fixture->root, + &fixture->ssz_bytes, + &fixture->ssz_len) + != 0) + { + fprintf(stderr, "failed to load state bytes for root\n"); + return 1; + } + + return 0; +} + +static void cleanup_checkpoint_fixture(struct checkpoint_fixture *fixture) +{ + if (!fixture) + { + return; + } + + if (fixture->ssz_bytes) + { + free(fixture->ssz_bytes); + fixture->ssz_bytes = NULL; + fixture->ssz_len = 0; + } + lantern_state_reset(&fixture->state); + + if (fixture->data_dir) + { + char root_hex[2u * LANTERN_ROOT_SIZE + 1u]; + if (lantern_bytes_to_hex( + fixture->root.bytes, + LANTERN_ROOT_SIZE, + root_hex, + sizeof(root_hex), + 0) + == 0) + { + char states_dir[PATH_MAX]; + int dir_written = snprintf(states_dir, sizeof(states_dir), "%s/states", fixture->data_dir); + if (dir_written > 0 && (size_t)dir_written < sizeof(states_dir)) + { + char state_path[PATH_MAX]; + char meta_path[PATH_MAX]; + int state_written = snprintf(state_path, sizeof(state_path), "%s/%s.ssz", states_dir, root_hex); + int meta_written = snprintf(meta_path, sizeof(meta_path), "%s/%s.meta", states_dir, root_hex); + if (state_written > 0 && (size_t)state_written < sizeof(state_path)) + { + cleanup_path(state_path); + } + if (meta_written > 0 && (size_t)meta_written < sizeof(meta_path)) + { + cleanup_path(meta_path); + } + cleanup_dir(states_dir); + } + } + cleanup_dir(fixture->data_dir); + free(fixture->data_dir); + fixture->data_dir = NULL; + } +} + +static int finalized_state_cb(void *context, uint8_t **out_bytes, size_t *out_len) +{ + if (!context || !out_bytes || !out_len) + { + return LANTERN_HTTP_CB_ERR_INVALID_PARAM; + } + struct checkpoint_callback_ctx *ctx = context; + if (ctx->rc != 0) + { + return ctx->rc; + } + uint8_t *buffer = malloc(ctx->len); + if (!buffer) + { + return LANTERN_HTTP_CB_ERR_IO; + } + memcpy(buffer, ctx->data, ctx->len); + *out_bytes = buffer; + *out_len = ctx->len; + return LANTERN_HTTP_CB_OK; +} + +static int send_all(int fd, const uint8_t *data, size_t length) +{ + size_t remaining = length; + const uint8_t *cursor = data; + while (remaining > 0) + { + ssize_t written = send(fd, cursor, remaining, 0); + if (written < 0) + { + if (errno == EINTR) + { + continue; + } + return -1; + } + if (written == 0) + { + return -1; + } + cursor += (size_t)written; + remaining -= (size_t)written; + } + return 0; +} + +static int read_response(uint16_t port, const char *request, uint8_t **out_data, size_t *out_len) +{ + if (!request || !out_data || !out_len) + { + return -1; + } + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + return -1; + } + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) <= 0) + { + close(fd); + return -1; + } + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) + { + close(fd); + return -1; + } + size_t request_len = strlen(request); + if (send_all(fd, (const uint8_t *)request, request_len) != 0) + { + close(fd); + return -1; + } + + uint8_t *buffer = NULL; + size_t buffer_len = 0; + size_t buffer_cap = 0; + for (;;) + { + uint8_t chunk[1024]; + ssize_t read_bytes = recv(fd, chunk, sizeof(chunk), 0); + if (read_bytes < 0) + { + if (errno == EINTR) + { + continue; + } + free(buffer); + close(fd); + return -1; + } + if (read_bytes == 0) + { + break; + } + size_t needed = buffer_len + (size_t)read_bytes; + if (needed > buffer_cap) + { + size_t new_cap = buffer_cap == 0 ? 2048u : buffer_cap * 2u; + while (new_cap < needed) + { + new_cap *= 2u; + } + uint8_t *resized = realloc(buffer, new_cap); + if (!resized) + { + free(buffer); + close(fd); + return -1; + } + buffer = resized; + buffer_cap = new_cap; + } + memcpy(buffer + buffer_len, chunk, (size_t)read_bytes); + buffer_len += (size_t)read_bytes; + } + close(fd); + + if (!buffer || buffer_len == 0) + { + free(buffer); + return -1; + } + + *out_data = buffer; + *out_len = buffer_len; + return 0; +} + +static int find_header_end(const uint8_t *data, size_t len, size_t *out_index) +{ + if (!data || !out_index || len < 4) + { + return -1; + } + for (size_t i = 0; i + 3 < len; ++i) + { + if (data[i] == '\r' + && data[i + 1] == '\n' + && data[i + 2] == '\r' + && data[i + 3] == '\n') + { + *out_index = i + 4; + return 0; + } + } + return -1; +} + +static int test_checkpoint_state_endpoint(void) +{ + struct checkpoint_fixture fixture; + if (build_checkpoint_fixture(&fixture) != 0) + { + cleanup_checkpoint_fixture(&fixture); + return 1; + } + + struct checkpoint_callback_ctx ctx = { + .data = fixture.ssz_bytes, + .len = fixture.ssz_len, + .rc = LANTERN_HTTP_CB_OK, + }; + + struct lantern_http_server server; + lantern_http_server_init(&server); + struct lantern_http_server_config config; + memset(&config, 0, sizeof(config)); + config.port = 0; + config.callbacks.context = &ctx; + config.callbacks.finalized_state_ssz = finalized_state_cb; + + if (lantern_http_server_start(&server, &config) != 0) + { + cleanup_checkpoint_fixture(&fixture); + return 1; + } + + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + if (getsockname(server.listen_fd, (struct sockaddr *)&addr, &addr_len) != 0) + { + lantern_http_server_stop(&server); + cleanup_checkpoint_fixture(&fixture); + return 1; + } + uint16_t port = ntohs(addr.sin_port); + expect_true(port != 0, "ephemeral port assigned"); + + const char *request = + "GET /lean/states/finalized HTTP/1.1\r\n" + "Host: localhost\r\n" + "Accept: application/octet-stream\r\n" + "Connection: close\r\n" + "\r\n"; + + uint8_t *response = NULL; + size_t response_len = 0; + if (read_response(port, request, &response, &response_len) != 0) + { + lantern_http_server_stop(&server); + cleanup_checkpoint_fixture(&fixture); + return 1; + } + + size_t header_end = 0; + expect_zero(find_header_end(response, response_len, &header_end), "find header end"); + expect_true(header_end < response_len, "header size"); + + char *header = malloc(header_end + 1); + expect_true(header != NULL, "header alloc"); + memcpy(header, response, header_end); + header[header_end] = '\0'; + + expect_true(strstr(header, "HTTP/1.1 200") != NULL, "status 200"); + expect_true(strstr(header, "Content-Type: application/octet-stream") != NULL, "content-type"); + + size_t body_len = response_len - header_end; + expect_true(body_len == fixture.ssz_len, "body length"); + expect_true(memcmp(response + header_end, fixture.ssz_bytes, fixture.ssz_len) == 0, "body bytes"); + + free(header); + free(response); + + ctx.rc = LANTERN_HTTP_CB_ERR_NOT_FOUND; + response = NULL; + response_len = 0; + if (read_response(port, request, &response, &response_len) != 0) + { + lantern_http_server_stop(&server); + cleanup_checkpoint_fixture(&fixture); + return 1; + } + + header_end = 0; + expect_zero(find_header_end(response, response_len, &header_end), "find header end 404"); + header = malloc(header_end + 1); + expect_true(header != NULL, "header alloc 404"); + memcpy(header, response, header_end); + header[header_end] = '\0'; + expect_true(strstr(header, "HTTP/1.1 404") != NULL, "status 404"); + + free(header); + free(response); + + lantern_http_server_stop(&server); + cleanup_checkpoint_fixture(&fixture); + return 0; +} + +static int test_storage_state_bytes(void) +{ + struct checkpoint_fixture fixture; + if (build_checkpoint_fixture(&fixture) != 0) + { + cleanup_checkpoint_fixture(&fixture); + return 1; + } + + LanternState decoded; + lantern_state_init(&decoded); + int decode_rc = lantern_ssz_decode_state(&decoded, fixture.ssz_bytes, fixture.ssz_len); + expect_zero(decode_rc, "decode state bytes"); + expect_true(decoded.validator_count == 4u, "validator count"); + expect_true( + memcmp(decoded.latest_finalized.root.bytes, fixture.root.bytes, LANTERN_ROOT_SIZE) == 0, + "finalized root"); + + lantern_state_reset(&decoded); + cleanup_checkpoint_fixture(&fixture); + return 0; +} + +int main(void) +{ + if (test_storage_state_bytes() != 0) + { + return 1; + } + if (test_checkpoint_state_endpoint() != 0) + { + return 1; + } + return 0; +} diff --git a/tests/unit/test_client_pending.c b/tests/unit/test_client_pending.c index 3bcb266..d90c019 100644 --- a/tests/unit/test_client_pending.c +++ b/tests/unit/test_client_pending.c @@ -45,7 +45,13 @@ static int test_pending_block_queue(void) { client_test_fill_root_with_index(&last_root, 0); int rc = 0; - if (lantern_client_debug_enqueue_pending_block(&client, &child, &child_root, &parent_root, peer_a) != 0) { + if (lantern_client_debug_enqueue_pending_block( + &client, + &child, + &child_root, + &parent_root, + peer_a) + != 0) { fprintf(stderr, "failed to enqueue initial pending block\n"); rc = 1; goto cleanup; @@ -58,7 +64,13 @@ static int test_pending_block_queue(void) { } if (lantern_client_debug_pending_entry( - &client, 0, &fetched_root, &fetched_parent, &parent_requested, peer_text, sizeof(peer_text)) + &client, + 0, + &fetched_root, + &fetched_parent, + &parent_requested, + peer_text, + sizeof(peer_text)) != 0) { fprintf(stderr, "failed to fetch pending entry\n"); rc = 1; @@ -85,7 +97,13 @@ static int test_pending_block_queue(void) { goto cleanup; } - if (lantern_client_debug_enqueue_pending_block(&client, &child, &child_root, &parent_root, peer_b) != 0) { + if (lantern_client_debug_enqueue_pending_block( + &client, + &child, + &child_root, + &parent_root, + peer_b) + != 0) { fprintf(stderr, "failed to enqueue duplicate pending block\n"); rc = 1; goto cleanup; @@ -99,7 +117,13 @@ static int test_pending_block_queue(void) { parent_requested = true; if (lantern_client_debug_pending_entry( - &client, 0, &fetched_root, &fetched_parent, &parent_requested, peer_text, sizeof(peer_text)) + &client, + 0, + &fetched_root, + &fetched_parent, + &parent_requested, + peer_text, + sizeof(peer_text)) != 0) { fprintf(stderr, "failed to fetch pending entry after duplicate enqueue\n"); rc = 1; @@ -118,7 +142,16 @@ static int test_pending_block_queue(void) { } parent_requested = false; - if (lantern_client_debug_pending_entry(&client, 0, NULL, NULL, &parent_requested, NULL, 0) != 0 || !parent_requested) { + if (lantern_client_debug_pending_entry( + &client, + 0, + NULL, + NULL, + &parent_requested, + NULL, + 0) + != 0 + || !parent_requested) { fprintf(stderr, "parent_requested flag did not persist after manual set\n"); rc = 1; goto cleanup; @@ -128,7 +161,7 @@ static int test_pending_block_queue(void) { &client, peer_b, &parent_root, - LANTERN_DEBUG_BLOCKS_REQUEST_SUCCESS) + LANTERN_TEST_BLOCKS_REQUEST_SUCCESS) != 0) { fprintf(stderr, "blocks_request_complete debug wrapper failed\n"); rc = 1; @@ -136,7 +169,15 @@ static int test_pending_block_queue(void) { } parent_requested = true; - if (lantern_client_debug_pending_entry(&client, 0, NULL, NULL, &parent_requested, NULL, 0) != 0) { + if (lantern_client_debug_pending_entry( + &client, + 0, + NULL, + NULL, + &parent_requested, + NULL, + 0) + != 0) { fprintf(stderr, "failed to inspect parent_requested after completion\n"); rc = 1; goto cleanup; @@ -147,7 +188,8 @@ static int test_pending_block_queue(void) { goto cleanup; } - for (size_t i = 0; i < 300; ++i) { + size_t extra_count = LANTERN_PENDING_BLOCK_LIMIT + 50u; + for (size_t i = 0; i < extra_count; ++i) { LanternSignedBlock extra; memset(&extra, 0, sizeof(extra)); lantern_block_body_init(&extra.message.body); @@ -159,7 +201,13 @@ static int test_pending_block_queue(void) { if (i == 299) { last_root = extra_root; } - if (lantern_client_debug_enqueue_pending_block(&client, &extra, &extra_root, &extra_parent, NULL) != 0) { + if (lantern_client_debug_enqueue_pending_block( + &client, + &extra, + &extra_root, + &extra_parent, + NULL) + != 0) { fprintf(stderr, "failed to enqueue additional pending block %zu\n", i); lantern_block_body_reset(&extra.message.body); rc = 1; @@ -169,7 +217,7 @@ static int test_pending_block_queue(void) { } size_t count = lantern_client_pending_block_count(&client); - if (count > 256) { + if (count > LANTERN_PENDING_BLOCK_LIMIT) { fprintf(stderr, "pending queue exceeded expected limit: %zu\n", count); rc = 1; goto cleanup; @@ -211,6 +259,7 @@ static int test_import_block_parent_mismatch(void) { LanternRoot block_root; LanternRoot parent_root; LanternRoot head_root; + LanternRoot parent_block_root; LanternRoot pending_root; LanternRoot pending_parent; bool parent_requested = true; @@ -298,14 +347,24 @@ static int test_import_block_parent_mismatch(void) { parent_block.parent_root = head_root; client_test_fill_root(&parent_block.state_root, 0x44); + if (lantern_hash_tree_root_block(&parent_block, &parent_block_root) != 0) + { + fprintf(stderr, "failed to hash parent block\n"); + lantern_block_body_reset(&parent_block.body); + lantern_block_body_reset(&anchor_block.body); + rc = 1; + goto cleanup; + } + if (lantern_fork_choice_add_block( &client.fork_choice, &parent_block, NULL, &client.state.latest_justified, &client.state.latest_finalized, - &parent_root) - != 0) { + &parent_block_root) + != 0) + { fprintf(stderr, "failed to add parent block to fork choice\n"); lantern_block_body_reset(&parent_block.body); lantern_block_body_reset(&anchor_block.body); diff --git a/tests/unit/test_signature.c b/tests/unit/test_signature.c index 4ad0cde..45db825 100644 --- a/tests/unit/test_signature.c +++ b/tests/unit/test_signature.c @@ -187,6 +187,117 @@ static int test_proposer_vote_signature_rejects_tampering(void) { return 0; } +static int test_aggregated_signature_roundtrip(void) { + enum { kSignerCount = 2 }; + struct PQSignatureSchemePublicKey *pubkeys[kSignerCount]; + struct PQSignatureSchemeSecretKey *secrets[kSignerCount]; + uint8_t serialized_pubkeys[kSignerCount][LANTERN_VALIDATOR_PUBKEY_SIZE]; + const uint8_t *pubkey_ptrs[kSignerCount]; + LanternSignature signatures[kSignerCount]; + LanternByteList proof; + LanternByteList tampered; + LanternRoot message; + uint64_t epoch = 1; + + memset(pubkeys, 0, sizeof(pubkeys)); + memset(secrets, 0, sizeof(secrets)); + memset(serialized_pubkeys, 0, sizeof(serialized_pubkeys)); + memset(signatures, 0, sizeof(signatures)); + lantern_byte_list_init(&proof); + lantern_byte_list_init(&tampered); + fill_root(&message, 0xA1); + + for (size_t i = 0; i < kSignerCount; ++i) { + if (generate_test_keypair(&pubkeys[i], &secrets[i]) != 0) { + fprintf(stderr, "aggregate: keygen failed index=%zu\n", i); + goto fail; + } + uintptr_t written = 0; + enum PQSigningError serr = pq_public_key_serialize( + pubkeys[i], + serialized_pubkeys[i], + sizeof(serialized_pubkeys[i]), + &written); + if (serr != Success || written != sizeof(serialized_pubkeys[i])) { + fprintf(stderr, "aggregate: pubkey serialize failed index=%zu err=%d written=%zu\n", i, (int)serr, (size_t)written); + goto fail; + } + pubkey_ptrs[i] = serialized_pubkeys[i]; + if (!lantern_signature_sign( + secrets[i], + epoch, + message.bytes, + sizeof(message.bytes), + &signatures[i])) { + fprintf(stderr, "aggregate: sign failed index=%zu\n", i); + goto fail; + } + } + + if (!lantern_signature_aggregate( + pubkey_ptrs, + signatures, + kSignerCount, + message.bytes, + sizeof(message.bytes), + epoch, + &proof)) { + fprintf(stderr, "aggregate: lantern_signature_aggregate failed\n"); + goto fail; + } + if (proof.length == 0 || !proof.data) { + fprintf(stderr, "aggregate: empty proof\n"); + goto fail; + } + if (!lantern_signature_verify_aggregated( + pubkey_ptrs, + kSignerCount, + message.bytes, + sizeof(message.bytes), + &proof, + epoch)) { + fprintf(stderr, "aggregate: verify_aggregated failed\n"); + goto fail; + } + + if (lantern_byte_list_copy(&tampered, &proof) != 0 || tampered.length == 0 || !tampered.data) { + fprintf(stderr, "aggregate: failed to copy proof\n"); + goto fail; + } + tampered.data[0] ^= 0xFF; + if (lantern_signature_verify_aggregated( + pubkey_ptrs, + kSignerCount, + message.bytes, + sizeof(message.bytes), + &tampered, + epoch)) { + fprintf(stderr, "aggregate: tampered proof unexpectedly verified\n"); + goto fail; + } + + for (size_t i = 0; i < kSignerCount; ++i) { + pq_secret_key_free(secrets[i]); + pq_public_key_free(pubkeys[i]); + } + lantern_byte_list_reset(&proof); + lantern_byte_list_reset(&tampered); + return 0; + +fail: + for (size_t i = 0; i < kSignerCount; ++i) { + if (secrets[i]) { + pq_secret_key_free(secrets[i]); + } + if (pubkeys[i]) { + pq_public_key_free(pubkeys[i]); + } + } + lantern_byte_list_reset(&proof); + lantern_byte_list_reset(&tampered); + return 1; +} + int main(void) { if (test_proposer_vote_signature_roundtrip() != 0) { return 1; @@ -194,6 +305,9 @@ int main(void) { if (test_proposer_vote_signature_rejects_tampering() != 0) { return 1; } + if (test_aggregated_signature_roundtrip() != 0) { + return 1; + } puts("lantern_signature_test OK"); return 0; } diff --git a/tests/unit/test_state.c b/tests/unit/test_state.c index 418f6a5..4691cb4 100644 --- a/tests/unit/test_state.c +++ b/tests/unit/test_state.c @@ -575,6 +575,7 @@ static int test_attestations_single_vote_justifies(void) { lantern_state_generate_genesis(&state, genesis_time, validator_count), "genesis for single-vote justification test"); mark_slot_justified_for_tests(&state, state.latest_justified.slot); + populate_historical_hashes_for_tests(&state, 1); LanternAttestations attestations; lantern_attestations_init(&attestations); @@ -582,9 +583,10 @@ static int test_attestations_single_vote_justifies(void) { lantern_signature_list_init(&signatures); LanternCheckpoint source_checkpoint = state.latest_justified; + source_checkpoint.root = get_historical_root_for_tests(&state, source_checkpoint.slot); LanternCheckpoint target_checkpoint = source_checkpoint; target_checkpoint.slot = source_checkpoint.slot + 1u; - fill_root(&target_checkpoint.root, 0xAB); + target_checkpoint.root = get_historical_root_for_tests(&state, target_checkpoint.slot); expect_zero(lantern_attestations_resize(&attestations, 1), "resize single attestation"); expect_zero(lantern_signature_list_resize(&signatures, 1), "resize single signature"); @@ -622,10 +624,11 @@ static int test_attestations_require_justified_source(void) { LanternCheckpoint source_checkpoint = state.latest_justified; source_checkpoint.slot = state.latest_justified.slot + 2; - fill_root(&source_checkpoint.root, 0xD0); LanternCheckpoint target_checkpoint = source_checkpoint; target_checkpoint.slot = source_checkpoint.slot + 1; - fill_root(&target_checkpoint.root, 0xDC); + populate_historical_hashes_for_tests(&state, target_checkpoint.slot); + source_checkpoint.root = get_historical_root_for_tests(&state, source_checkpoint.slot); + target_checkpoint.root = get_historical_root_for_tests(&state, target_checkpoint.slot); size_t quorum = (size_t)lantern_consensus_quorum_threshold(state.config.num_validators); expect_zero( @@ -670,12 +673,15 @@ static int test_attestations_accept_duplicate_votes(void) { expect_zero(lantern_attestations_resize(&attestations, 2), "double vote resize"); expect_zero(lantern_signature_list_resize(&signatures, 2), "double vote signature resize"); - LanternCheckpoint target_checkpoint = state.latest_justified; + populate_historical_hashes_for_tests(&state, 1); + LanternCheckpoint source_checkpoint = state.latest_justified; + source_checkpoint.root = get_historical_root_for_tests(&state, source_checkpoint.slot); + LanternCheckpoint target_checkpoint = source_checkpoint; target_checkpoint.slot = 1; - fill_root(&target_checkpoint.root, 0xCD); + target_checkpoint.root = get_historical_root_for_tests(&state, target_checkpoint.slot); - build_vote(&attestations.data[0], &signatures.data[0], 0, 1, &state.latest_justified, &target_checkpoint, 0x11); - build_vote(&attestations.data[1], &signatures.data[1], 0, 1, &state.latest_justified, &target_checkpoint, 0x22); + build_vote(&attestations.data[0], &signatures.data[0], 0, 1, &source_checkpoint, &target_checkpoint, 0x11); + build_vote(&attestations.data[1], &signatures.data[1], 0, 1, &source_checkpoint, &target_checkpoint, 0x22); expect_zero( lantern_state_process_attestations(&state, &attestations, &signatures), @@ -708,9 +714,17 @@ static void setup_prejustified_consecutive_source( state->latest_justified.slot = slot_one; fill_root(&state->latest_justified.root, source_marker); + uint64_t target_slot = slot_one + 1u; + expect_zero( + lantern_root_list_resize(&state->historical_block_hashes, (size_t)(target_slot + 1u)), + "resize historical hashes for consecutive attestation test"); + fill_root(&state->historical_block_hashes.items[slot_one - 1u], alt_marker); + fill_root(&state->historical_block_hashes.items[slot_one], source_marker); + fill_root(&state->historical_block_hashes.items[target_slot], target_marker); + *out_source = state->latest_justified; *out_target = *out_source; - out_target->slot = out_source->slot + 1u; + out_target->slot = target_slot; fill_root(&out_target->root, target_marker); *out_alt_source = *out_source; @@ -846,12 +860,14 @@ static int test_attestations_finalize_across_gap(void) { uint64_t source_slot = state.latest_justified.slot + 1u; mark_slot_justified_for_tests(&state, source_slot); state.latest_justified.slot = source_slot; - fill_root(&state.latest_justified.root, 0xC1); LanternCheckpoint source = state.latest_justified; LanternCheckpoint target = source; target.slot = source.slot + 3u; - fill_root(&target.root, 0xC2); + populate_historical_hashes_for_tests(&state, target.slot); + state.latest_justified.root = get_historical_root_for_tests(&state, source.slot); + source.root = state.latest_justified.root; + target.root = get_historical_root_for_tests(&state, target.slot); LanternAttestations first_vote; lantern_attestations_init(&first_vote); @@ -884,6 +900,149 @@ static int test_attestations_finalize_across_gap(void) { return 0; } +static int test_pruning_keeps_pending_justifications(void) { + LanternState state; + lantern_state_init(&state); + const uint64_t validator_count = 3; + expect_zero(lantern_state_generate_genesis(&state, 760, validator_count), "genesis for pruning test"); + mark_slot_justified_for_tests(&state, state.latest_justified.slot); + populate_historical_hashes_for_tests(&state, 5); + + size_t quorum = (size_t)lantern_consensus_quorum_threshold(state.config.num_validators); + LanternAttestations attestations; + lantern_attestations_init(&attestations); + LanternSignatureList signatures; + lantern_signature_list_init(&signatures); + expect_zero(lantern_attestations_resize(&attestations, quorum), "resize pruning attestations"); + expect_zero(lantern_signature_list_resize(&signatures, quorum), "resize pruning signatures"); + + LanternCheckpoint source_0 = state.latest_justified; + source_0.root = get_historical_root_for_tests(&state, source_0.slot); + LanternCheckpoint target_1 = source_0; + target_1.slot = source_0.slot + 1u; + target_1.root = get_historical_root_for_tests(&state, target_1.slot); + + for (size_t i = 0; i < quorum; ++i) { + build_vote( + &attestations.data[i], + &signatures.data[i], + (uint64_t)i, + target_1.slot, + &source_0, + &target_1, + (uint8_t)(0x81u + i)); + } + + expect_zero( + lantern_state_process_attestations(&state, &attestations, &signatures), + "justify slot 1 for pruning test"); + assert(state.latest_justified.slot == target_1.slot); + assert(state.latest_finalized.slot == source_0.slot); + + expect_zero(lantern_root_list_resize(&state.justification_roots, 1), "resize pending roots"); + state.justification_roots.items[0] = get_historical_root_for_tests(&state, 3); + expect_zero( + lantern_bitlist_resize(&state.justification_validators, validator_count), + "resize pending validators"); + expect_zero( + lantern_bitlist_set(&state.justification_validators, 0, true), + "mark pending validator vote"); + + LanternCheckpoint source_1 = target_1; + LanternCheckpoint target_2 = source_1; + target_2.slot = source_1.slot + 1u; + target_2.root = get_historical_root_for_tests(&state, target_2.slot); + + expect_zero(lantern_attestations_resize(&attestations, quorum), "resize finalization attestations"); + expect_zero(lantern_signature_list_resize(&signatures, quorum), "resize finalization signatures"); + for (size_t i = 0; i < quorum; ++i) { + build_vote( + &attestations.data[i], + &signatures.data[i], + (uint64_t)i, + target_2.slot, + &source_1, + &target_2, + (uint8_t)(0x91u + i)); + } + + expect_zero( + lantern_state_process_attestations(&state, &attestations, &signatures), + "finalize slot 1 for pruning test"); + assert(state.latest_finalized.slot == source_1.slot); + assert(state.latest_justified.slot == target_2.slot); + assert(state.justification_roots.length == 1); + assert(memcmp( + state.justification_roots.items[0].bytes, + get_historical_root_for_tests(&state, 3).bytes, + LANTERN_ROOT_SIZE) + == 0); + + lantern_attestations_reset(&attestations); + lantern_signature_list_reset(&signatures); + lantern_state_reset(&state); + return 0; +} + +static int test_attestations_ignore_zero_hash_votes(void) { + LanternState state; + lantern_state_init(&state); + const uint64_t validator_count = 3; + expect_zero( + lantern_state_generate_genesis(&state, 765, validator_count), + "genesis for zero-hash vote test"); + mark_slot_justified_for_tests(&state, state.latest_justified.slot); + populate_historical_hashes_for_tests(&state, 1); + + LanternAttestations attestations; + lantern_attestations_init(&attestations); + LanternSignatureList signatures; + lantern_signature_list_init(&signatures); + expect_zero(lantern_attestations_resize(&attestations, 2), "resize zero-hash attestations"); + expect_zero(lantern_signature_list_resize(&signatures, 2), "resize zero-hash signatures"); + + LanternCheckpoint source_zero = state.latest_justified; + zero_root(&source_zero.root); + LanternCheckpoint target_nonzero = source_zero; + target_nonzero.slot = source_zero.slot + 1u; + target_nonzero.root = get_historical_root_for_tests(&state, target_nonzero.slot); + + LanternCheckpoint source_nonzero = state.latest_justified; + source_nonzero.root = get_historical_root_for_tests(&state, source_nonzero.slot); + LanternCheckpoint target_zero = source_nonzero; + target_zero.slot = source_nonzero.slot + 1u; + zero_root(&target_zero.root); + + build_vote( + &attestations.data[0], + &signatures.data[0], + 0, + target_nonzero.slot, + &source_zero, + &target_nonzero, + 0xA1); + build_vote( + &attestations.data[1], + &signatures.data[1], + 1, + target_zero.slot, + &source_nonzero, + &target_zero, + 0xA2); + + expect_zero( + lantern_state_process_attestations(&state, &attestations, &signatures), + "process zero-hash votes"); + assert(state.latest_justified.slot == 0); + assert(state.latest_finalized.slot == 0); + assert(state.justification_roots.length == 0); + + lantern_attestations_reset(&attestations); + lantern_signature_list_reset(&signatures); + lantern_state_reset(&state); + return 0; +} + static int test_attestations_ignore_out_of_range_validator(void) { LanternState state; lantern_state_init(&state); @@ -904,9 +1063,12 @@ static int test_attestations_ignore_out_of_range_validator(void) { lantern_signature_list_resize(&signatures, att_count), "resize mixed attestation signatures"); - LanternCheckpoint target_checkpoint = state.latest_justified; - target_checkpoint.slot = state.latest_justified.slot + 1u; - fill_root(&target_checkpoint.root, 0x75); + LanternCheckpoint source_checkpoint = state.latest_justified; + LanternCheckpoint target_checkpoint = source_checkpoint; + target_checkpoint.slot = source_checkpoint.slot + 1u; + populate_historical_hashes_for_tests(&state, target_checkpoint.slot); + source_checkpoint.root = get_historical_root_for_tests(&state, source_checkpoint.slot); + target_checkpoint.root = get_historical_root_for_tests(&state, target_checkpoint.slot); uint64_t invalid_validator = state.config.num_validators; build_vote( @@ -914,7 +1076,7 @@ static int test_attestations_ignore_out_of_range_validator(void) { &signatures.data[0], invalid_validator, target_checkpoint.slot, - &state.latest_justified, + &source_checkpoint, &target_checkpoint, 0x31); for (size_t i = 1; i <= quorum; ++i) { @@ -924,7 +1086,7 @@ static int test_attestations_ignore_out_of_range_validator(void) { &signatures.data[i], validator_id, target_checkpoint.slot, - &state.latest_justified, + &source_checkpoint, &target_checkpoint, (uint8_t)(0x32u + i)); } @@ -967,9 +1129,12 @@ static int test_process_block_accepts_mixed_attestations(void) { lantern_attestations_resize(&votes, att_count), "resize mixed block attestations"); - LanternCheckpoint target_checkpoint = state.latest_justified; - target_checkpoint.slot = state.latest_justified.slot + 1u; - fill_root(&target_checkpoint.root, 0x76); + LanternCheckpoint source_checkpoint = state.latest_justified; + LanternCheckpoint target_checkpoint = source_checkpoint; + target_checkpoint.slot = source_checkpoint.slot + 1u; + populate_historical_hashes_for_tests(&state, target_checkpoint.slot); + source_checkpoint.root = get_historical_root_for_tests(&state, source_checkpoint.slot); + target_checkpoint.root = get_historical_root_for_tests(&state, target_checkpoint.slot); uint64_t invalid_validator = state.config.num_validators; build_vote( @@ -977,7 +1142,7 @@ static int test_process_block_accepts_mixed_attestations(void) { NULL, invalid_validator, target_checkpoint.slot, - &state.latest_justified, + &source_checkpoint, &target_checkpoint, 0x41); for (size_t i = 1; i <= quorum; ++i) { @@ -987,7 +1152,7 @@ static int test_process_block_accepts_mixed_attestations(void) { NULL, validator_id, target_checkpoint.slot, - &state.latest_justified, + &source_checkpoint, &target_checkpoint, (uint8_t)(0x41u + i)); } @@ -1790,6 +1955,9 @@ static int test_compute_vote_checkpoints_respects_safe_target(void) { state.latest_finalized.slot = 0; state.latest_finalized.root = genesis_root; state.latest_justified = state.latest_finalized; + expect_zero( + lantern_fork_choice_update_checkpoints(&fork_choice, &state.latest_justified, &state.latest_finalized), + "sync fork choice checkpoints for safe target test"); LanternCheckpoint head; LanternCheckpoint target; @@ -1864,6 +2032,9 @@ static int test_compute_vote_checkpoints_justifiable(void) { state.latest_finalized.slot = 0; state.latest_finalized.root = genesis_root; state.latest_justified = state.latest_finalized; + expect_zero( + lantern_fork_choice_update_checkpoints(&fork_choice, &state.latest_justified, &state.latest_finalized), + "sync fork choice checkpoints for justifiable test"); LanternCheckpoint head; LanternCheckpoint target; @@ -1937,6 +2108,9 @@ static int test_compute_vote_checkpoints_consecutive_target(void) { state.latest_finalized.root = block_roots[3]; state.latest_justified.slot = 4; state.latest_justified.root = block_roots[4]; + expect_zero( + lantern_fork_choice_update_checkpoints(&fork_choice, &state.latest_justified, &state.latest_finalized), + "sync fork choice checkpoints for consecutive target test"); LanternCheckpoint head; LanternCheckpoint target; @@ -2004,6 +2178,9 @@ static int test_compute_vote_checkpoints_advances_beyond_source(void) { state.latest_finalized.root = block_roots[0]; state.latest_justified.slot = 6; state.latest_justified.root = block_roots[6]; + expect_zero( + lantern_fork_choice_update_checkpoints(&fork_choice, &state.latest_justified, &state.latest_finalized), + "sync fork choice checkpoints for advance target test"); LanternCheckpoint head; LanternCheckpoint target; @@ -2170,6 +2347,12 @@ int main(void) { if (test_attestations_finalize_across_gap() != 0) { return 1; } + if (test_pruning_keeps_pending_justifications() != 0) { + return 1; + } + if (test_attestations_ignore_zero_hash_votes() != 0) { + return 1; + } if (test_attestations_ignore_out_of_range_validator() != 0) { return 1; } diff --git a/tests/unit/test_storage.c b/tests/unit/test_storage.c index ce220f7..ef53f42 100644 --- a/tests/unit/test_storage.c +++ b/tests/unit/test_storage.c @@ -281,6 +281,9 @@ int main(void) { char votes_path[PATH_MAX]; char meta_path[PATH_MAX]; char blocks_dir[PATH_MAX]; + char states_dir[PATH_MAX]; + char indices_dir[PATH_MAX]; + char slot_index_dir[PATH_MAX]; int written = snprintf(state_path, sizeof(state_path), "%s/%s", base_dir, "state.ssz"); assert(written > 0 && (size_t)written < sizeof(state_path)); written = snprintf(votes_path, sizeof(votes_path), "%s/%s", base_dir, "votes.bin"); @@ -289,6 +292,12 @@ int main(void) { assert(written > 0 && (size_t)written < sizeof(meta_path)); written = snprintf(blocks_dir, sizeof(blocks_dir), "%s/%s", base_dir, "blocks"); assert(written > 0 && (size_t)written < sizeof(blocks_dir)); + written = snprintf(states_dir, sizeof(states_dir), "%s/%s", base_dir, "states"); + assert(written > 0 && (size_t)written < sizeof(states_dir)); + written = snprintf(indices_dir, sizeof(indices_dir), "%s/%s", base_dir, "indices"); + assert(written > 0 && (size_t)written < sizeof(indices_dir)); + written = snprintf(slot_index_dir, sizeof(slot_index_dir), "%s/%s", indices_dir, "slots"); + assert(written > 0 && (size_t)written < sizeof(slot_index_dir)); char block_path[PATH_MAX]; char root_hex[2u * LANTERN_ROOT_SIZE + 1u]; @@ -298,6 +307,9 @@ int main(void) { cleanup_path(block_path); cleanup_dir(blocks_dir); + cleanup_dir(slot_index_dir); + cleanup_dir(indices_dir); + cleanup_dir(states_dir); cleanup_path(votes_path); cleanup_path(meta_path); cleanup_path(state_path); diff --git a/tools/leanMetrics b/tools/leanMetrics index a5d0c50..7fac4cc 160000 --- a/tools/leanMetrics +++ b/tools/leanMetrics @@ -1 +1 @@ -Subproject commit a5d0c5032a12ec89dae0a563a476b5f563c29aa5 +Subproject commit 7fac4cc3e3d0bcc5e498f6abd74c80c8245f5591