diff --git a/external/c-libp2p b/external/c-libp2p index cf5116a..94f4a76 160000 --- a/external/c-libp2p +++ b/external/c-libp2p @@ -1 +1 @@ -Subproject commit cf5116a23423f0601f88d7ddcdb6dceca75acd48 +Subproject commit 94f4a766eddeff935944c8fc83fdbe3226de4cf3 diff --git a/include/lantern/consensus/fork_choice.h b/include/lantern/consensus/fork_choice.h index 6baa5a1..14fe3bf 100644 --- a/include/lantern/consensus/fork_choice.h +++ b/include/lantern/consensus/fork_choice.h @@ -29,10 +29,6 @@ struct lantern_fork_choice_block_entry { LanternValidatorIndex proposer_index; bool has_validator_count; uint64_t validator_count; - bool has_justified; - bool has_finalized; - LanternCheckpoint latest_justified; - LanternCheckpoint latest_finalized; }; struct lantern_fork_choice_state_entry { @@ -160,7 +156,8 @@ int lantern_fork_choice_update_checkpoints( * startup restoration and may move checkpoints backwards when the persisted * state is behind the temporary anchor checkpoints used during init. * - * Any provided checkpoint root must already exist in the fork-choice store. + * Restored checkpoints must refer to blocks already materialized in the local + * fork-choice tree. */ int lantern_fork_choice_restore_checkpoints( LanternForkChoice *store, diff --git a/include/lantern/networking/gossipsub_service.h b/include/lantern/networking/gossipsub_service.h index 421a8e7..0265249 100644 --- a/include/lantern/networking/gossipsub_service.h +++ b/include/lantern/networking/gossipsub_service.h @@ -45,6 +45,7 @@ struct lantern_gossipsub_service { char vote_subnet_topic[128]; char aggregated_attestation_topic[128]; const char *data_dir; + const char *devnet; size_t attestation_subnet_id; int subscribe_attestation_subnet; int (*publish_hook)(const char *topic, const uint8_t *payload, size_t payload_len, void *user_data); @@ -60,6 +61,9 @@ struct lantern_gossipsub_service { libp2p_gossipsub_validator_handle_t *vote_validator_handle; libp2p_gossipsub_validator_handle_t *vote_subnet_validator_handle; libp2p_gossipsub_validator_handle_t *aggregated_attestation_validator_handle; + char (*extra_vote_subnet_topics)[128]; + libp2p_gossipsub_validator_handle_t **extra_vote_subnet_validator_handles; + size_t extra_vote_subnet_topic_count; }; void lantern_gossipsub_service_init(struct lantern_gossipsub_service *service); @@ -76,10 +80,14 @@ int lantern_gossipsub_service_publish_vote( const LanternSignedVote *vote); int lantern_gossipsub_service_publish_vote_subnet( struct lantern_gossipsub_service *service, - const LanternSignedVote *vote); + const LanternSignedVote *vote, + size_t subnet_id); int lantern_gossipsub_service_publish_aggregated_attestation( struct lantern_gossipsub_service *service, const LanternSignedAggregatedAttestation *attestation); +int lantern_gossipsub_service_subscribe_attestation_subnet( + struct lantern_gossipsub_service *service, + size_t subnet_id); void lantern_gossipsub_service_set_publish_hook( struct lantern_gossipsub_service *service, int (*hook)(const char *topic, const uint8_t *payload, size_t payload_len, void *user_data), diff --git a/src/consensus/fork_choice.c b/src/consensus/fork_choice.c index b3f46db..c47ba0e 100644 --- a/src/consensus/fork_choice.c +++ b/src/consensus/fork_choice.c @@ -508,9 +508,7 @@ static int register_block( const LanternRoot *root, const LanternRoot *parent_root, uint64_t slot, - LanternValidatorIndex proposer_index, - const LanternCheckpoint *latest_justified, - const LanternCheckpoint *latest_finalized) { + LanternValidatorIndex proposer_index) { if (!store || !root) { return -1; } @@ -519,14 +517,6 @@ static int register_block( struct lantern_fork_choice_block_entry *entry = &store->blocks[existing_index]; entry->slot = slot; entry->proposer_index = proposer_index; - if (latest_justified) { - entry->latest_justified = *latest_justified; - entry->has_justified = true; - } - if (latest_finalized) { - entry->latest_finalized = *latest_finalized; - entry->has_finalized = true; - } if (parent_root) { entry->parent_root = *parent_root; entry->parent_index = parent_index_for_block(store, parent_root); @@ -548,18 +538,6 @@ static int register_block( entry->proposer_index = proposer_index; entry->has_validator_count = false; entry->validator_count = 0; - entry->has_justified = latest_justified != NULL; - if (latest_justified) { - entry->latest_justified = *latest_justified; - } else { - memset(&entry->latest_justified, 0, sizeof(entry->latest_justified)); - } - entry->has_finalized = latest_finalized != NULL; - if (latest_finalized) { - entry->latest_finalized = *latest_finalized; - } else { - memset(&entry->latest_finalized, 0, sizeof(entry->latest_finalized)); - } size_t new_index = store->block_len; store->block_len += 1; if (map_insert(store, root, new_index) != 0) { @@ -678,6 +656,7 @@ int lantern_fork_choice_set_anchor_with_state( if (!store || !store->initialized || !anchor_block) { return -1; } + struct lantern_log_metadata meta = {.has_slot = true, .slot = anchor_block->slot}; LanternRoot root; if (block_root_hint) { root = *block_root_hint; @@ -697,8 +676,8 @@ int lantern_fork_choice_set_anchor_with_state( previous_entry = store->blocks[existing_index]; } size_t previous_block_len = store->block_len; - LanternCheckpoint previous_justified = store->latest_justified; - LanternCheckpoint previous_finalized = store->latest_finalized; + LanternCheckpoint previous_latest_justified = store->latest_justified; + LanternCheckpoint previous_latest_finalized = store->latest_finalized; LanternRoot previous_anchor_root = store->anchor_root; uint64_t previous_anchor_slot = store->anchor_slot; LanternRoot previous_head = store->head; @@ -713,12 +692,40 @@ int lantern_fork_choice_set_anchor_with_state( &root, &anchor_block->parent_root, anchor_block->slot, - anchor_block->proposer_index, - latest_justified, - latest_finalized) + anchor_block->proposer_index) != 0) { return -1; } + char root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&root, root_hex, sizeof(root_hex)); + format_root_hex(&anchor_block->parent_root, parent_hex, sizeof(parent_hex)); + format_root_hex( + latest_justified ? &latest_justified->root : NULL, + justified_hex, + sizeof(justified_hex)); + format_root_hex( + latest_finalized ? &latest_finalized->root : NULL, + finalized_hex, + sizeof(finalized_hex)); + lantern_log_info( + "forkchoice", + &meta, + "set anchor slot=%" PRIu64 " root=%s parent=%s justified_slot=%" PRIu64 + " justified_root=%s finalized_slot=%" PRIu64 " finalized_root=%s" + " block_root_hint=%s anchor_state=%s existed=%s", + anchor_block->slot, + root_hex[0] ? root_hex : "0x0", + parent_hex[0] ? parent_hex : "0x0", + latest_justified ? latest_justified->slot : 0u, + justified_hex[0] ? justified_hex : "0x0", + latest_finalized ? latest_finalized->slot : 0u, + finalized_hex[0] ? finalized_hex : "0x0", + block_root_hint ? "true" : "false", + anchor_state ? "true" : "false", + existed ? "true" : "false"); if (latest_justified) { store->latest_justified = *latest_justified; } else { @@ -739,8 +746,8 @@ int lantern_fork_choice_set_anchor_with_state( uint64_t anchor_intervals = anchor_block->slot * store->intervals_per_slot; store->time_intervals = anchor_intervals; if (anchor_state && lantern_fork_choice_set_block_state(store, &root, anchor_state) != 0) { - store->latest_justified = previous_justified; - store->latest_finalized = previous_finalized; + store->latest_justified = previous_latest_justified; + store->latest_finalized = previous_latest_finalized; store->anchor_root = previous_anchor_root; store->anchor_slot = previous_anchor_slot; store->head = previous_head; @@ -780,28 +787,135 @@ static bool checkpoint_known_in_store( if (!store->blocks || checkpoint_index >= store->block_len) { return false; } + if (store->has_anchor && root_compare(&checkpoint->root, &store->anchor_root) == 0) { + return true; + } return store->blocks[checkpoint_index].slot == checkpoint->slot; } -static int update_global_checkpoints( +static bool checkpoint_root_present_in_store( + const LanternForkChoice *store, + const LanternCheckpoint *checkpoint) { + if (!store || !checkpoint || root_is_zero(&checkpoint->root)) { + return false; + } + size_t checkpoint_index = 0; + if (!map_lookup(store, &checkpoint->root, &checkpoint_index)) { + return false; + } + return store->blocks && checkpoint_index < store->block_len; +} + +static void normalize_checkpoint_for_anchor_alias( + const LanternForkChoice *store, + const LanternCheckpoint *checkpoint, + LanternCheckpoint *out_checkpoint, + const char *label) { + if (!out_checkpoint) { + return; + } + memset(out_checkpoint, 0, sizeof(*out_checkpoint)); + if (!checkpoint) { + return; + } + *out_checkpoint = *checkpoint; + if (!store || !store->has_anchor || root_is_zero(&checkpoint->root)) { + return; + } + if (checkpoint_root_present_in_store(store, checkpoint)) { + return; + } + if (checkpoint->slot > store->anchor_slot) { + return; + } + + char original_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char anchor_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&checkpoint->root, original_hex, sizeof(original_hex)); + format_root_hex(&store->anchor_root, anchor_hex, sizeof(anchor_hex)); + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){0}, + "aliasing %s checkpoint slot=%" PRIu64 " original_root=%s" + " anchor_slot=%" PRIu64 " anchor_root=%s", + label ? label : "post-state", + checkpoint->slot, + original_hex[0] ? original_hex : "0x0", + store->anchor_slot, + anchor_hex[0] ? anchor_hex : "0x0"); + out_checkpoint->root = store->anchor_root; +} + +static bool should_replace_checkpoint( + const LanternCheckpoint *current, + const LanternCheckpoint *candidate) { + if (!current || !candidate || root_is_zero(&candidate->root)) { + return false; + } + if (candidate->slot > current->slot) { + return true; + } + if (candidate->slot == current->slot + && root_compare(&candidate->root, ¤t->root) != 0) { + return true; + } + return false; +} + +static int update_latest_checkpoints( LanternForkChoice *store, const LanternCheckpoint *post_justified, const LanternCheckpoint *post_finalized) { if (!store) { return -1; } - if (post_justified - && !root_is_zero(&post_justified->root) - && post_justified->slot > store->latest_justified.slot - && checkpoint_known_in_store(store, post_justified)) { - store->latest_justified = *post_justified; + LanternCheckpoint latest_justified = store->latest_justified; + LanternCheckpoint latest_finalized = store->latest_finalized; + LanternCheckpoint normalized_post_justified; + LanternCheckpoint normalized_post_finalized; + const LanternCheckpoint *effective_post_justified = post_justified; + const LanternCheckpoint *effective_post_finalized = post_finalized; + + if (post_justified) { + normalize_checkpoint_for_anchor_alias( + store, + post_justified, + &normalized_post_justified, + "justified"); + effective_post_justified = &normalized_post_justified; + } + if (post_finalized) { + normalize_checkpoint_for_anchor_alias( + store, + post_finalized, + &normalized_post_finalized, + "finalized"); + effective_post_finalized = &normalized_post_finalized; + } + + if (effective_post_justified && !root_is_zero(&effective_post_justified->root)) { + if (!checkpoint_known_in_store(store, effective_post_justified)) { + return -1; + } + if (should_replace_checkpoint(&latest_justified, effective_post_justified)) { + latest_justified = *effective_post_justified; + } + } + if (effective_post_finalized && !root_is_zero(&effective_post_finalized->root)) { + if (!checkpoint_known_in_store(store, effective_post_finalized)) { + return -1; + } + if (should_replace_checkpoint(&latest_finalized, effective_post_finalized)) { + latest_finalized = *effective_post_finalized; + } } - if (post_finalized - && !root_is_zero(&post_finalized->root) - && post_finalized->slot > store->latest_finalized.slot - && checkpoint_known_in_store(store, post_finalized)) { - store->latest_finalized = *post_finalized; + + if (latest_finalized.slot > latest_justified.slot) { + return -1; } + + store->latest_justified = latest_justified; + store->latest_finalized = latest_finalized; return 0; } @@ -837,6 +951,7 @@ int lantern_fork_choice_add_block_with_state( (void)proposer_attestation; bool trace_finalization = finalization_trace_enabled(); struct lantern_log_metadata trace_meta = {.has_slot = true, .slot = block->slot}; + struct lantern_log_metadata diag_meta = {.has_slot = true, .slot = block->slot}; char block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; char parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; uint64_t parent_slot = 0; @@ -850,9 +965,26 @@ int lantern_fork_choice_add_block_with_state( return -1; } } + LanternRoot hashed_block_root = {0}; + bool have_hashed_block_root = lantern_hash_tree_root_block(block, &hashed_block_root) == 0; + char hinted_block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char hashed_block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + if (block_root_hint && have_hashed_block_root + && root_compare(&hashed_block_root, &block_root) != 0) { + format_root_hex(&block_root, hinted_block_hex, sizeof(hinted_block_hex)); + format_root_hex(&hashed_block_root, hashed_block_hex, sizeof(hashed_block_hex)); + lantern_log_info( + "forkchoice", + &diag_meta, + "block root hint differs from block hash slot=%" PRIu64 + " hinted_root=%s hashed_root=%s", + block->slot, + hinted_block_hex[0] ? hinted_block_hex : "0x0", + hashed_block_hex[0] ? hashed_block_hex : "0x0"); + } - LanternCheckpoint previous_justified = store->latest_justified; - LanternCheckpoint previous_finalized = store->latest_finalized; + LanternCheckpoint previous_latest_justified = store->latest_justified; + LanternCheckpoint previous_latest_finalized = store->latest_finalized; LanternRoot previous_head = store->head; bool had_head = store->has_head; @@ -894,15 +1026,62 @@ int lantern_fork_choice_add_block_with_state( &block_root, &block->parent_root, block->slot, - block->proposer_index, - post_justified, - post_finalized) + block->proposer_index) != 0) { + format_root_hex(&block_root, block_hex, sizeof(block_hex)); + format_root_hex(&block->parent_root, parent_hex, sizeof(parent_hex)); + lantern_log_warn( + "forkchoice", + &diag_meta, + "add_block register failed slot=%" PRIu64 " root=%s parent=%s", + block->slot, + block_hex[0] ? block_hex : "0x0", + parent_hex[0] ? parent_hex : "0x0"); free(touched); vote_undo_reset(&undo); return -1; } - if (update_global_checkpoints(store, post_justified, post_finalized) != 0) { + if (update_latest_checkpoints(store, post_justified, post_finalized) != 0) { + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char anchor_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + bool post_justified_known = + post_justified && checkpoint_known_in_store(store, post_justified); + bool post_finalized_known = + post_finalized && checkpoint_known_in_store(store, post_finalized); + format_root_hex(&block_root, block_hex, sizeof(block_hex)); + format_root_hex(&block->parent_root, parent_hex, sizeof(parent_hex)); + format_root_hex( + post_justified ? &post_justified->root : NULL, + justified_hex, + sizeof(justified_hex)); + format_root_hex( + post_finalized ? &post_finalized->root : NULL, + finalized_hex, + sizeof(finalized_hex)); + format_root_hex( + store->has_anchor ? &store->anchor_root : NULL, + anchor_hex, + sizeof(anchor_hex)); + lantern_log_warn( + "forkchoice", + &diag_meta, + "add_block rejected checkpoints slot=%" PRIu64 " root=%s parent=%s" + " anchor_slot=%" PRIu64 " anchor_root=%s post_justified_slot=%" PRIu64 + " post_justified_root=%s post_justified_known=%s" + " post_finalized_slot=%" PRIu64 " post_finalized_root=%s" + " post_finalized_known=%s", + block->slot, + block_hex[0] ? block_hex : "0x0", + parent_hex[0] ? parent_hex : "0x0", + store->anchor_slot, + anchor_hex[0] ? anchor_hex : "0x0", + post_justified ? post_justified->slot : 0u, + justified_hex[0] ? justified_hex : "0x0", + post_justified_known ? "true" : "false", + post_finalized ? post_finalized->slot : 0u, + finalized_hex[0] ? finalized_hex : "0x0", + post_finalized_known ? "true" : "false"); goto rollback; } @@ -913,6 +1092,14 @@ int lantern_fork_choice_add_block_with_state( store->validator_count, &expanded) != 0) { + format_root_hex(&block_root, block_hex, sizeof(block_hex)); + lantern_log_warn( + "forkchoice", + &diag_meta, + "add_block attestation expansion failed slot=%" PRIu64 " root=%s attestations=%zu", + block->slot, + block_hex[0] ? block_hex : "0x0", + block->body.attestations.length); goto rollback_attestations; } for (size_t i = 0; i < expanded.length; ++i) { @@ -930,9 +1117,33 @@ int lantern_fork_choice_add_block_with_state( } lantern_attestations_reset(&expanded); if (lantern_fork_choice_recompute_head(store) != 0) { + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&block_root, block_hex, sizeof(block_hex)); + format_root_hex(&store->latest_justified.root, justified_hex, sizeof(justified_hex)); + format_root_hex(&store->latest_finalized.root, finalized_hex, sizeof(finalized_hex)); + lantern_log_warn( + "forkchoice", + &diag_meta, + "add_block head recompute failed slot=%" PRIu64 " root=%s" + " latest_justified_slot=%" PRIu64 " latest_justified_root=%s" + " latest_finalized_slot=%" PRIu64 " latest_finalized_root=%s", + block->slot, + block_hex[0] ? block_hex : "0x0", + store->latest_justified.slot, + justified_hex[0] ? justified_hex : "0x0", + store->latest_finalized.slot, + finalized_hex[0] ? finalized_hex : "0x0"); goto rollback; } if (post_state && lantern_fork_choice_set_block_state(store, &block_root, post_state) != 0) { + format_root_hex(&block_root, block_hex, sizeof(block_hex)); + lantern_log_warn( + "forkchoice", + &diag_meta, + "add_block failed to attach post-state slot=%" PRIu64 " root=%s", + block->slot, + block_hex[0] ? block_hex : "0x0"); goto rollback; } lean_metrics_record_fork_choice_block_time(lantern_time_now_seconds() - metrics_start); @@ -960,8 +1171,8 @@ int lantern_fork_choice_add_block_with_state( lantern_attestations_reset(&expanded); rollback: - store->latest_justified = previous_justified; - store->latest_finalized = previous_finalized; + store->latest_justified = previous_latest_justified; + store->latest_finalized = previous_latest_finalized; store->head = previous_head; store->has_head = had_head; @@ -1040,7 +1251,7 @@ int lantern_fork_choice_update_checkpoints( if (!store || !store->initialized) { return -1; } - return update_global_checkpoints(store, latest_justified, latest_finalized); + return update_latest_checkpoints(store, latest_justified, latest_finalized); } int lantern_fork_choice_restore_checkpoints( @@ -1050,46 +1261,95 @@ int lantern_fork_choice_restore_checkpoints( if (!store || !store->initialized || !store->has_anchor) { return -1; } + struct lantern_log_metadata meta = {0}; + char anchor_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&store->anchor_root, anchor_hex, sizeof(anchor_hex)); - LanternCheckpoint restored_justified = store->latest_justified; - LanternCheckpoint restored_finalized = store->latest_finalized; - bool justified_changed = false; + LanternCheckpoint restored_latest_justified = store->latest_justified; + LanternCheckpoint restored_latest_finalized = store->latest_finalized; if (latest_justified && !root_is_zero(&latest_justified->root)) { - size_t justified_index = 0; - if (!map_lookup(store, &latest_justified->root, &justified_index)) { + if (!checkpoint_known_in_store(store, latest_justified)) { + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&latest_justified->root, justified_hex, sizeof(justified_hex)); + lantern_log_warn( + "forkchoice", + &meta, + "restore checkpoints missing justified checkpoint slot=%" PRIu64 + " root=%s anchor_slot=%" PRIu64 " anchor_root=%s block_len=%zu", + latest_justified->slot, + justified_hex[0] ? justified_hex : "0x0", + store->anchor_slot, + anchor_hex[0] ? anchor_hex : "0x0", + store->block_len); return -1; } - restored_justified = *latest_justified; - justified_changed = true; + restored_latest_justified = *latest_justified; } if (latest_finalized && !root_is_zero(&latest_finalized->root)) { - size_t finalized_index = 0; - if (!map_lookup(store, &latest_finalized->root, &finalized_index)) { + if (!checkpoint_known_in_store(store, latest_finalized)) { + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&latest_finalized->root, finalized_hex, sizeof(finalized_hex)); + lantern_log_warn( + "forkchoice", + &meta, + "restore checkpoints missing finalized checkpoint slot=%" PRIu64 + " root=%s anchor_slot=%" PRIu64 " anchor_root=%s block_len=%zu", + latest_finalized->slot, + finalized_hex[0] ? finalized_hex : "0x0", + store->anchor_slot, + anchor_hex[0] ? anchor_hex : "0x0", + store->block_len); return -1; } - restored_finalized = *latest_finalized; + restored_latest_finalized = *latest_finalized; } - if (restored_finalized.slot > restored_justified.slot) { + if (restored_latest_finalized.slot > restored_latest_justified.slot) { + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&restored_latest_justified.root, justified_hex, sizeof(justified_hex)); + format_root_hex(&restored_latest_finalized.root, finalized_hex, sizeof(finalized_hex)); + lantern_log_warn( + "forkchoice", + &meta, + "restore checkpoints rejected ordering justified_slot=%" PRIu64 + " justified_root=%s finalized_slot=%" PRIu64 " finalized_root=%s", + restored_latest_justified.slot, + justified_hex[0] ? justified_hex : "0x0", + restored_latest_finalized.slot, + finalized_hex[0] ? finalized_hex : "0x0"); return -1; } - LanternCheckpoint previous_justified = store->latest_justified; - LanternCheckpoint previous_finalized = store->latest_finalized; + LanternCheckpoint previous_latest_justified = store->latest_justified; + LanternCheckpoint previous_latest_finalized = store->latest_finalized; LanternRoot previous_head = store->head; bool previous_has_head = store->has_head; - store->latest_justified = restored_justified; - if (justified_changed && lantern_fork_choice_recompute_head(store) != 0) { - store->latest_justified = previous_justified; - store->latest_finalized = previous_finalized; + store->latest_justified = restored_latest_justified; + store->latest_finalized = restored_latest_finalized; + if (!root_is_zero(&restored_latest_justified.root) + && lantern_fork_choice_recompute_head(store) != 0) { + char justified_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char finalized_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&restored_latest_justified.root, justified_hex, sizeof(justified_hex)); + format_root_hex(&restored_latest_finalized.root, finalized_hex, sizeof(finalized_hex)); + lantern_log_warn( + "forkchoice", + &meta, + "restore checkpoints head recompute failed justified_slot=%" PRIu64 + " justified_root=%s finalized_slot=%" PRIu64 " finalized_root=%s", + restored_latest_justified.slot, + justified_hex[0] ? justified_hex : "0x0", + restored_latest_finalized.slot, + finalized_hex[0] ? finalized_hex : "0x0"); + store->latest_justified = previous_latest_justified; + store->latest_finalized = previous_latest_finalized; store->head = previous_head; store->has_head = previous_has_head; return -1; } - store->latest_justified = restored_justified; - store->latest_finalized = restored_finalized; return 0; } diff --git a/src/consensus/state.c b/src/consensus/state.c index a8e4c86..ed813cd 100644 --- a/src/consensus/state.c +++ b/src/consensus/state.c @@ -2592,15 +2592,17 @@ int lantern_state_compute_vote_checkpoints( if (lantern_fork_choice_block_info(fork_choice, &head_root, &head_slot, NULL, NULL) != 0) { return -1; } - const LanternCheckpoint *store_justified = lantern_fork_choice_latest_justified(fork_choice); - const LanternCheckpoint *store_finalized = lantern_fork_choice_latest_finalized(fork_choice); + const LanternCheckpoint *store_latest_justified = + lantern_fork_choice_latest_justified(fork_choice); + const LanternCheckpoint *store_latest_finalized = + lantern_fork_choice_latest_finalized(fork_choice); LanternCheckpoint source_checkpoint = base_state->latest_justified; LanternCheckpoint finalized_checkpoint = base_state->latest_finalized; - if (store_justified && !lantern_root_is_zero(&store_justified->root)) { - source_checkpoint = *store_justified; + if (store_latest_justified && !lantern_root_is_zero(&store_latest_justified->root)) { + source_checkpoint = *store_latest_justified; } - if (store_finalized && !lantern_root_is_zero(&store_finalized->root)) { - finalized_checkpoint = *store_finalized; + if (store_latest_finalized && !lantern_root_is_zero(&store_latest_finalized->root)) { + finalized_checkpoint = *store_latest_finalized; } LanternRoot target_root = head_root; uint64_t target_slot = head_slot; diff --git a/src/core/client.c b/src/core/client.c index c1704dc..fd462d2 100644 --- a/src/core/client.c +++ b/src/core/client.c @@ -1298,7 +1298,7 @@ static int checkpoint_sync_parse_port( return 0; } -static int checkpoint_sync_parse_url( +int lantern_client_checkpoint_sync_parse_url( const char *url, char **out_host, uint16_t *out_port, @@ -1322,7 +1322,7 @@ static int checkpoint_sync_parse_url( } else if (strncasecmp(url, https_prefix, strlen(https_prefix)) == 0) { - /* TLS not supported; downgrade to plain HTTP */ + /* TLS transport is not implemented yet; downgrade to plain HTTP parsing */ prefix_len = strlen(https_prefix); } else @@ -2064,7 +2064,12 @@ static int checkpoint_sync_fetch_state_bytes( char *host = NULL; char *base_path = NULL; uint16_t port = 0; - if (checkpoint_sync_parse_url(checkpoint_sync_url, &host, &port, &base_path) != 0) + if (lantern_client_checkpoint_sync_parse_url( + checkpoint_sync_url, + &host, + &port, + &base_path) + != 0) { return -1; } @@ -2331,6 +2336,8 @@ static lantern_client_error client_load_state_from_checkpoint( goto cleanup; } + LanternCheckpoint original_latest_justified = decoded.latest_justified; + LanternCheckpoint original_latest_finalized = decoded.latest_finalized; LanternBlockHeader anchor_header = decoded.latest_block_header; anchor_header.state_root = state_root; LanternRoot anchor_root; @@ -2344,20 +2351,142 @@ static lantern_client_error client_load_state_from_checkpoint( goto cleanup; } + char header_parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char header_state_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char original_justified_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char original_finalized_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex( + &decoded.latest_block_header.parent_root, + header_parent_hex, + sizeof(header_parent_hex)); + format_root_hex( + &decoded.latest_block_header.state_root, + header_state_root_hex, + sizeof(header_state_root_hex)); + format_root_hex( + &original_latest_justified.root, + original_justified_root_hex, + sizeof(original_justified_root_hex)); + format_root_hex( + &original_latest_finalized.root, + original_finalized_root_hex, + sizeof(original_finalized_root_hex)); + + lantern_log_info( + "checkpoint_sync", + &meta, + "decoded checkpoint state slot=%" PRIu64 + " header_slot=%" PRIu64 " proposer=%" PRIu64 " header_parent=%s" + " header_state_root=%s justified_slot=%" PRIu64 " justified_root=%s" + " finalized_slot=%" PRIu64 " finalized_root=%s", + decoded.slot, + decoded.latest_block_header.slot, + decoded.latest_block_header.proposer_index, + header_parent_hex[0] ? header_parent_hex : "0x0", + header_state_root_hex[0] ? header_state_root_hex : "0x0", + original_latest_justified.slot, + original_justified_root_hex[0] ? original_justified_root_hex : "0x0", + original_latest_finalized.slot, + original_finalized_root_hex[0] ? original_finalized_root_hex : "0x0"); + /* - * Preserve checkpoint roots exactly as provided by the remote state. + * Keep the fetched checkpoint state canonical. Fork choice rewrites the + * checkpoint roots to the synthetic anchor locally during bootstrap, but + * mutating the decoded state here changes its hash and causes a different + * anchor to be recomputed later from the modified snapshot. * - * Rewriting justified/finalized roots here mutates state content before - * fork-choice anchor initialization, which can skew state/anchor hashing - * relative to peers and force unnecessary slot-0 backfill requests. - * Any root remapping needed for local fork-choice restoration is handled - * later during restore_persisted_blocks(). + * We still materialize the anchor-alias view below for diagnostics so the + * logs show how the state/root pair would drift if those checkpoint roots + * were rewritten before fork-choice initialization. */ + LanternState anchor_checkpoint_alias = decoded; + anchor_checkpoint_alias.latest_justified.root = anchor_root; + anchor_checkpoint_alias.latest_finalized.root = anchor_root; char state_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; char anchor_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + LanternRoot adjusted_state_root = {0}; + LanternRoot adjusted_anchor_root = {0}; + bool have_adjusted_state_root = false; + bool have_adjusted_anchor_root = false; + if (lantern_hash_tree_root_state(&anchor_checkpoint_alias, &adjusted_state_root) == 0) + { + have_adjusted_state_root = true; + LanternBlockHeader adjusted_anchor_header = anchor_checkpoint_alias.latest_block_header; + adjusted_anchor_header.state_root = adjusted_state_root; + if (lantern_hash_tree_root_block_header( + &adjusted_anchor_header, + &adjusted_anchor_root) + == 0) + { + have_adjusted_anchor_root = true; + } + else + { + lantern_log_warn( + "checkpoint_sync", + &meta, + "failed to compute adjusted checkpoint anchor root after checkpoint root override"); + } + } + else + { + lantern_log_warn( + "checkpoint_sync", + &meta, + "failed to compute adjusted checkpoint state root after checkpoint root override"); + } + + char adjusted_state_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char adjusted_anchor_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char adjusted_justified_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char adjusted_finalized_root_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; format_root_hex(&state_root, state_root_hex, sizeof(state_root_hex)); format_root_hex(&anchor_root, anchor_root_hex, sizeof(anchor_root_hex)); + format_root_hex( + &adjusted_state_root, + adjusted_state_root_hex, + sizeof(adjusted_state_root_hex)); + format_root_hex( + &adjusted_anchor_root, + adjusted_anchor_root_hex, + sizeof(adjusted_anchor_root_hex)); + format_root_hex( + &anchor_checkpoint_alias.latest_justified.root, + adjusted_justified_root_hex, + sizeof(adjusted_justified_root_hex)); + format_root_hex( + &anchor_checkpoint_alias.latest_finalized.root, + adjusted_finalized_root_hex, + sizeof(adjusted_finalized_root_hex)); + + lantern_log_info( + "checkpoint_sync", + &meta, + "checkpoint root override justified_before=%s justified_after=%s" + " finalized_before=%s finalized_after=%s original_state_root=%s" + " adjusted_state_root=%s original_anchor_root=%s adjusted_anchor_root=%s" + " adjusted_anchor_matches_original=%s", + original_justified_root_hex[0] ? original_justified_root_hex : "0x0", + adjusted_justified_root_hex[0] ? adjusted_justified_root_hex : "0x0", + original_finalized_root_hex[0] ? original_finalized_root_hex : "0x0", + adjusted_finalized_root_hex[0] ? adjusted_finalized_root_hex : "0x0", + state_root_hex[0] ? state_root_hex : "0x0", + have_adjusted_state_root + ? (adjusted_state_root_hex[0] ? adjusted_state_root_hex : "0x0") + : "", + anchor_root_hex[0] ? anchor_root_hex : "0x0", + have_adjusted_anchor_root + ? (adjusted_anchor_root_hex[0] ? adjusted_anchor_root_hex : "0x0") + : "", + (have_adjusted_anchor_root + && memcmp( + adjusted_anchor_root.bytes, + anchor_root.bytes, + LANTERN_ROOT_SIZE) + == 0) + ? "true" + : "false"); lantern_state_reset(&client->state); client->state = decoded; @@ -2831,12 +2960,24 @@ static lantern_client_error client_start_protocols( struct lantern_client *client, uint8_t node_key[NODE_PRIVATE_KEY_SIZE]) { + size_t attestation_committee_count = + client->debug_attestation_committee_count > 0 + ? client->debug_attestation_committee_count + : LANTERN_ATTESTATION_COMMITTEE_COUNT; size_t subnet_id = 0; if (client->local_validators && client->local_validator_count > 0) { - (void)lantern_validator_index_compute_subnet_id( - client->local_validators[0].global_index, - LANTERN_ATTESTATION_COMMITTEE_COUNT, - &subnet_id); + if (lantern_validator_index_compute_subnet_id( + client->local_validators[0].global_index, + attestation_committee_count, + &subnet_id) + != 0) { + lantern_log_error( + "client", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "failed to compute startup attestation subnet validator=%" PRIu64, + client->local_validators[0].global_index); + return LANTERN_CLIENT_ERR_NETWORK; + } } struct lantern_gossipsub_config gossip_cfg = { .host = client->network.host, @@ -2860,6 +3001,39 @@ static lantern_client_error client_start_protocols( return LANTERN_CLIENT_ERR_NETWORK; } client->gossip_running = true; + if (client->local_validators && client->local_validator_count > 0) { + for (size_t i = 0; i < client->local_validator_count; ++i) { + size_t validator_subnet_id = 0; + if (lantern_validator_index_compute_subnet_id( + client->local_validators[i].global_index, + attestation_committee_count, + &validator_subnet_id) + != 0) { + lantern_log_error( + "client", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "failed to compute attestation subnet validator=%" PRIu64, + client->local_validators[i].global_index); + lantern_gossipsub_service_reset(&client->gossip); + client->gossip_running = false; + return LANTERN_CLIENT_ERR_NETWORK; + } + if (lantern_gossipsub_service_subscribe_attestation_subnet( + &client->gossip, + validator_subnet_id) + != 0) { + lantern_log_error( + "client", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "failed to subscribe attestation subnet validator=%" PRIu64 " subnet=%zu", + client->local_validators[i].global_index, + validator_subnet_id); + lantern_gossipsub_service_reset(&client->gossip); + client->gossip_running = false; + return LANTERN_CLIENT_ERR_NETWORK; + } + } + } struct lantern_reqresp_service_callbacks req_callbacks; memset(&req_callbacks, 0, sizeof(req_callbacks)); diff --git a/src/core/client_http.c b/src/core/client_http.c index d754237..df5711d 100644 --- a/src/core/client_http.c +++ b/src/core/client_http.c @@ -140,8 +140,14 @@ int http_snapshot_head(void *context, struct lantern_http_head_snapshot *out_sna LanternCheckpoint finalized = client->state.latest_finalized; if (client->has_fork_choice) { + const LanternCheckpoint *fork_justified = + lantern_fork_choice_latest_justified(&client->fork_choice); const LanternCheckpoint *fork_finalized = lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_justified && !lantern_root_is_zero(&fork_justified->root)) + { + justified = *fork_justified; + } if (fork_finalized && !lantern_root_is_zero(&fork_finalized->root)) { finalized = *fork_finalized; diff --git a/src/core/client_internal.h b/src/core/client_internal.h index 80b1a8c..c984e4c 100644 --- a/src/core/client_internal.h +++ b/src/core/client_internal.h @@ -211,6 +211,21 @@ bool lantern_client_persisted_state_is_stale_for_checkpoint_sync( uint64_t *out_expected_current_slot, uint64_t *out_gap); +/** + * Parse a checkpoint sync base URL. + * + * `http://` URLs are parsed directly. `https://` URLs are currently accepted + * but downgraded to the existing plaintext HTTP transport. Callers own the + * returned host/base-path buffers and must free them with `free()`. + * + * @return 0 on success, -1 on parse/validation failure. + */ +int lantern_client_checkpoint_sync_parse_url( + const char *url, + char **out_host, + uint16_t *out_port, + char **out_base_path); + /** * Cache an individual gossip signature keyed by validator and attestation root. * diff --git a/src/core/client_network.c b/src/core/client_network.c index 02674f2..ff3151a 100644 --- a/src/core/client_network.c +++ b/src/core/client_network.c @@ -1495,7 +1495,6 @@ static void handle_connection_closed_event( char peer_text[128]; format_peer_id_text(peer, peer_text, sizeof(peer_text)); - lantern_log_info( "network", &(const struct lantern_log_metadata){ diff --git a/src/core/client_reqresp.c b/src/core/client_reqresp.c index ad61907..eeb62fe 100644 --- a/src/core/client_reqresp.c +++ b/src/core/client_reqresp.c @@ -106,6 +106,7 @@ static void peer_status_local_view( struct lantern_client *client, const LanternRoot *head_root, uint64_t *out_local_slot, + uint64_t *out_local_finalized_slot, bool *out_head_known); static struct lantern_peer_status_entry *lantern_client_update_peer_status_entry_locked( struct lantern_client *client, @@ -306,28 +307,35 @@ static void peer_status_local_view( struct lantern_client *client, const LanternRoot *head_root, uint64_t *out_local_slot, + uint64_t *out_local_finalized_slot, bool *out_head_known) { if (out_local_slot) { *out_local_slot = 0; } + if (out_local_finalized_slot) + { + *out_local_finalized_slot = 0; + } if (out_head_known) { *out_head_known = false; } - if (!client || !head_root || !out_local_slot || !out_head_known) + if (!client || !head_root || !out_local_slot || !out_local_finalized_slot || !out_head_known) { return; } uint64_t local_slot = 0; + uint64_t local_finalized_slot = 0; bool head_known = false; bool state_locked = lantern_client_lock_state(client); if (state_locked) { local_slot = client->state.latest_block_header.slot; + local_finalized_slot = client->state.latest_finalized.slot; if (client->has_fork_choice) { LanternRoot fork_head = {0}; @@ -345,12 +353,19 @@ static void peer_status_local_view( local_slot = fork_slot; } } + const LanternCheckpoint *fork_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_finalized && !lantern_root_is_zero(&fork_finalized->root)) + { + local_finalized_slot = fork_finalized->slot; + } } head_known = lantern_client_block_known_locked(client, head_root, NULL); } else if (client->has_state) { local_slot = client->state.latest_block_header.slot; + local_finalized_slot = client->state.latest_finalized.slot; if (client->has_fork_choice) { LanternRoot fork_head = {0}; @@ -368,6 +383,12 @@ static void peer_status_local_view( local_slot = fork_slot; } } + const LanternCheckpoint *fork_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_finalized && !lantern_root_is_zero(&fork_finalized->root)) + { + local_finalized_slot = fork_finalized->slot; + } uint64_t fork_slot = 0; if (lantern_fork_choice_block_info( &client->fork_choice, @@ -384,10 +405,105 @@ static void peer_status_local_view( lantern_client_unlock_state(client, state_locked); *out_local_slot = local_slot; + *out_local_finalized_slot = local_finalized_slot; *out_head_known = head_known; } +static void maybe_seed_head_request_from_status( + struct lantern_client *client, + const LanternStatusMessage *peer_status, + const char *peer_id_text, + uint64_t local_head_slot, + uint64_t local_finalized_slot, + bool head_known) +{ + if (!client || !peer_status || !peer_id_text || peer_id_text[0] == '\0') + { + return; + } + if (head_known || lantern_root_is_zero(&peer_status->head.root)) + { + return; + } + + const bool peer_finalized_ahead = peer_status->finalized.slot > local_finalized_slot; + const bool peer_head_ahead = peer_status->head.slot > local_head_slot; + if (!peer_finalized_ahead && !peer_head_ahead) + { + return; + } + + bool has_pending_blocks = false; + bool pending_locked = lantern_client_lock_pending(client); + if (pending_locked) + { + has_pending_blocks = client->pending_blocks.length > 0; + } + lantern_client_unlock_pending(client, pending_locked); + if (has_pending_blocks) + { + return; + } + + uint64_t now_ms = monotonic_millis(); + bool recently_requested = false; + if (client->status_lock_initialized && pthread_mutex_lock(&client->status_lock) == 0) + { + if (!lantern_root_is_zero(&client->sync_last_requested_root) + && memcmp( + client->sync_last_requested_root.bytes, + peer_status->head.root.bytes, + LANTERN_ROOT_SIZE) + == 0 + && client->sync_last_requested_root_ms != 0 + && now_ms >= client->sync_last_requested_root_ms + && (now_ms - client->sync_last_requested_root_ms) < LANTERN_BLOCKS_REQUEST_TIMEOUT_MS) + { + recently_requested = true; + } + pthread_mutex_unlock(&client->status_lock); + } + if (recently_requested) + { + return; + } + + const LanternRoot roots[1] = {peer_status->head.root}; + const uint32_t depths[1] = {0u}; + if (!lantern_client_try_schedule_blocks_request_batch( + client, + peer_id_text, + roots, + depths, + 1u)) + { + return; + } + + if (client->status_lock_initialized && pthread_mutex_lock(&client->status_lock) == 0) + { + client->sync_last_requested_root = peer_status->head.root; + client->sync_last_requested_root_ms = now_ms; + pthread_mutex_unlock(&client->status_lock); + } + + char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + format_root_hex(&peer_status->head.root, head_hex, sizeof(head_hex)); + lantern_log_info( + "sync", + &(const struct lantern_log_metadata){ + .validator = client->node_id, + .peer = peer_id_text}, + "peer is ahead; requesting head block root=%s peer_head_slot=%" PRIu64 + " peer_finalized_slot=%" PRIu64 " local_head_slot=%" PRIu64 " local_finalized_slot=%" PRIu64, + head_hex[0] ? head_hex : "0x0", + peer_status->head.slot, + peer_status->finalized.slot, + local_head_slot, + local_finalized_slot); +} + static bool peer_status_is_eligible( struct lantern_client *client, const struct lantern_peer_status_entry *entry, @@ -1133,8 +1249,8 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) * * @spec subspecs/networking/reqresp/message.py - Status protocol * - * Processes a peer's status message, updates peer tracking, and records - * sync progress. Peer status does not proactively trigger block requests. + * Processes a peer's status message, updates peer tracking, records + * sync progress, and may proactively seed backfill for an ahead peer's head. * * @param context Client instance * @param peer_status Status message from peer @@ -1457,8 +1573,8 @@ int reqresp_collect_blocks( * @spec subspecs/forkchoice/store.py - Sync decision logic * * 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. + * status message. When a materially ahead peer advertises an unknown head, + * this path may also seed backfill by requesting that head block. * * @param client Client instance * @param peer_status Status message from peer @@ -1488,8 +1604,14 @@ static void lantern_client_on_peer_status( copy_peer_id_text(peer_id, peer_copy, sizeof(peer_copy)); uint64_t local_slot = 0; + uint64_t local_finalized_slot = 0; bool head_known = false; - peer_status_local_view(client, &peer_status->head.root, &local_slot, &head_known); + peer_status_local_view( + client, + &peer_status->head.root, + &local_slot, + &local_finalized_slot, + &head_known); struct lantern_log_metadata meta = { .validator = client->node_id, @@ -1518,6 +1640,13 @@ static void lantern_client_on_peer_status( peer_status, peer_copy, local_slot); + maybe_seed_head_request_from_status( + client, + peer_status, + peer_copy, + local_slot, + local_finalized_slot, + head_known); } @@ -1553,12 +1682,14 @@ static void lantern_client_adopt_peer_genesis( /* Use the peer's advertised head root as both state_root and hint so our fork-choice anchor matches the peer even if we cannot reproduce their SSZ state. */ anchor.state_root = peer_status->head.root; + LanternCheckpoint anchor_checkpoint = peer_status->finalized; + anchor_checkpoint.root = peer_status->head.root; if (lantern_fork_choice_set_anchor( &client->fork_choice, &anchor, - &peer_status->finalized, - &peer_status->finalized, + &anchor_checkpoint, + &anchor_checkpoint, &peer_status->head.root) != 0) { diff --git a/src/core/client_sync.c b/src/core/client_sync.c index 57c2827..0e2fd99 100644 --- a/src/core/client_sync.c +++ b/src/core/client_sync.c @@ -301,10 +301,14 @@ int gossip_vote_handler( static bool verify_and_cache_aggregated_attestation_locked( struct lantern_client *client, const LanternSignedAggregatedAttestation *attestation, - const struct lantern_log_metadata *meta) { + const struct lantern_log_metadata *meta, + LanternRoot *out_missing_root) { if (!client || !attestation || !meta || !client->has_state) { return false; } + if (out_missing_root) { + memset(out_missing_root, 0, sizeof(*out_missing_root)); + } if (attestation->proof.participants.bit_length == 0 || !attestation->proof.participants.bytes || attestation->proof.proof_data.length == 0 @@ -314,12 +318,28 @@ static bool verify_and_cache_aggregated_attestation_locked( LanternState target_state; lantern_state_init(&target_state); - const LanternState *sig_state = lantern_client_state_for_root_locked( + LanternRoot missing_root = {0}; + const LanternState *sig_state = lantern_client_state_for_root_local_locked( client, &attestation->data.target.root, &target_state, NULL); + if (!sig_state + && !lantern_client_find_missing_state_root_locked( + client, + &attestation->data.target.root, + &missing_root)) { + sig_state = lantern_client_state_for_root_locked( + client, + &attestation->data.target.root, + &target_state, + NULL); + } if (!sig_state) { + if (out_missing_root) { + *out_missing_root = + lantern_root_is_zero(&missing_root) ? attestation->data.target.root : missing_root; + } lantern_state_reset(&target_state); return false; } @@ -416,14 +436,22 @@ int gossip_aggregated_attestation_handler( if (!locked) { return LANTERN_CLIENT_ERR_RUNTIME; } - bool verified = verify_and_cache_aggregated_attestation_locked(client, attestation, &meta); + LanternRoot missing_root = {0}; + bool verified = verify_and_cache_aggregated_attestation_locked( + client, + attestation, + &meta, + &missing_root); lantern_client_unlock_state(client, locked); if (!verified) { + char missing_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&missing_root, missing_hex, sizeof(missing_hex)); lantern_log_debug( "gossip", &meta, - "ignoring aggregated attestation slot=%" PRIu64, - attestation->data.slot); + "ignoring aggregated attestation slot=%" PRIu64 " missing_root=%s", + attestation->data.slot, + missing_hex[0] ? missing_hex : "0x0"); return LANTERN_CLIENT_ERR_IGNORED; } lantern_log_debug( @@ -634,6 +662,55 @@ static void log_genesis_anchor_roots( slot); } +static void log_fork_choice_state_snapshot( + const struct lantern_log_metadata *meta, + const LanternState *state, + const char *label) +{ + if (!meta || !state || !label) + { + return; + } + + char header_parent_hex[ROOT_HEX_BUFFER_LEN]; + char header_state_root_hex[ROOT_HEX_BUFFER_LEN]; + char justified_root_hex[ROOT_HEX_BUFFER_LEN]; + char finalized_root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + &state->latest_block_header.parent_root, + header_parent_hex, + sizeof(header_parent_hex)); + format_root_hex( + &state->latest_block_header.state_root, + header_state_root_hex, + sizeof(header_state_root_hex)); + format_root_hex( + &state->latest_justified.root, + justified_root_hex, + sizeof(justified_root_hex)); + format_root_hex( + &state->latest_finalized.root, + finalized_root_hex, + sizeof(finalized_root_hex)); + + lantern_log_info( + "forkchoice", + meta, + "%s slot=%" PRIu64 " header_slot=%" PRIu64 " proposer=%" PRIu64 + " header_parent=%s header_state_root=%s justified_slot=%" PRIu64 + " justified_root=%s finalized_slot=%" PRIu64 " finalized_root=%s", + label, + state->slot, + state->latest_block_header.slot, + state->latest_block_header.proposer_index, + header_parent_hex[0] ? header_parent_hex : "0x0", + header_state_root_hex[0] ? header_state_root_hex : "0x0", + state->latest_justified.slot, + justified_root_hex[0] ? justified_root_hex : "0x0", + state->latest_finalized.slot, + finalized_root_hex[0] ? finalized_root_hex : "0x0"); +} + /** * Initialize fork choice from genesis state. @@ -699,6 +776,11 @@ int initialize_fork_choice(struct lantern_client *client) return LANTERN_CLIENT_ERR_RUNTIME; } + log_fork_choice_state_snapshot( + &meta, + &client->state, + "fork choice init state snapshot"); + LanternRoot anchor_state_root; LanternBlockHeader anchor_header; LanternRoot anchor_root; @@ -713,6 +795,54 @@ int initialize_fork_choice(struct lantern_client *client) return root_rc; } + char justified_root_hex[ROOT_HEX_BUFFER_LEN]; + char finalized_root_hex[ROOT_HEX_BUFFER_LEN]; + char anchor_state_root_hex[ROOT_HEX_BUFFER_LEN]; + char anchor_root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + &client->state.latest_justified.root, + justified_root_hex, + sizeof(justified_root_hex)); + format_root_hex( + &client->state.latest_finalized.root, + finalized_root_hex, + sizeof(finalized_root_hex)); + format_root_hex( + &anchor_state_root, + anchor_state_root_hex, + sizeof(anchor_state_root_hex)); + format_root_hex( + &anchor_root, + anchor_root_hex, + sizeof(anchor_root_hex)); + + lantern_log_info( + "forkchoice", + &meta, + "fork choice computed anchor state_root=%s anchor_root=%s" + " justified_slot=%" PRIu64 " justified_root=%s finalized_slot=%" PRIu64 + " finalized_root=%s justified_matches_anchor=%s finalized_matches_anchor=%s", + anchor_state_root_hex[0] ? anchor_state_root_hex : "0x0", + anchor_root_hex[0] ? anchor_root_hex : "0x0", + client->state.latest_justified.slot, + justified_root_hex[0] ? justified_root_hex : "0x0", + client->state.latest_finalized.slot, + finalized_root_hex[0] ? finalized_root_hex : "0x0", + memcmp( + client->state.latest_justified.root.bytes, + anchor_root.bytes, + LANTERN_ROOT_SIZE) + == 0 + ? "true" + : "false", + memcmp( + client->state.latest_finalized.root.bytes, + anchor_root.bytes, + LANTERN_ROOT_SIZE) + == 0 + ? "true" + : "false"); + log_genesis_anchor_roots( &meta, &anchor_root, @@ -728,23 +858,50 @@ 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; + LanternRoot synthetic_anchor_block_root = {0}; + bool have_synthetic_anchor_block_root = + lantern_hash_tree_root_block(&anchor, &synthetic_anchor_block_root) == 0; + char synthetic_anchor_block_root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + &synthetic_anchor_block_root, + synthetic_anchor_block_root_hex, + sizeof(synthetic_anchor_block_root_hex)); + lantern_log_info( + "forkchoice", + &meta, + "materialized synthetic anchor block slot=%" PRIu64 " hinted_root=%s" + " hashed_block_root=%s hinted_root_matches_block=%s", + anchor.slot, + anchor_root_hex[0] ? anchor_root_hex : "0x0", + have_synthetic_anchor_block_root + ? (synthetic_anchor_block_root_hex[0] + ? synthetic_anchor_block_root_hex + : "0x0") + : "", + (have_synthetic_anchor_block_root + && memcmp( + synthetic_anchor_block_root.bytes, + anchor_root.bytes, + LANTERN_ROOT_SIZE) + == 0) + ? "true" + : "false"); + + LanternCheckpoint anchor_justified = client->state.latest_justified; + LanternCheckpoint anchor_finalized = client->state.latest_finalized; + anchor_justified.root = anchor_root; + anchor_finalized.root = anchor_root; /* - * Seed fork-choice with anchor_checkpoint (whose root matches the anchor - * block that set_anchor registers in the tree). This guarantees - * lmd_ghost_compute can always find its start_root during block restore. - * - * Real persisted checkpoints are synced to the store AFTER - * restore_persisted_blocks() via lantern_fork_choice_restore_checkpoints(). + * Seed fork-choice with the anchor block/root while preserving the state's + * justified/finalized slots. This matches LeanSpec checkpoint bootstrap: + * both checkpoints refer to the materialized anchor root from the start. */ if (lantern_fork_choice_set_anchor_with_state( &client->fork_choice, &anchor, - &anchor_checkpoint, - &anchor_checkpoint, + &anchor_justified, + &anchor_finalized, &anchor_root, &client->state) != 0) @@ -759,16 +916,42 @@ int initialize_fork_choice(struct lantern_client *client) persist_anchor_block(client, &anchor, &anchor_root); if (client->data_dir) { + LanternState anchor_state_alias = client->state; + anchor_state_alias.latest_justified.root = anchor_root; + anchor_state_alias.latest_finalized.root = anchor_root; if (lantern_storage_store_state_for_root( client->data_dir, &anchor_root, - &client->state) + &anchor_state_alias) != 0) { lantern_log_warn( "storage", &meta, - "failed to persist anchor state"); + "failed to persist anchor state alias"); + } + else + { + char alias_justified_hex[ROOT_HEX_BUFFER_LEN]; + char alias_finalized_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + &anchor_state_alias.latest_justified.root, + alias_justified_hex, + sizeof(alias_justified_hex)); + format_root_hex( + &anchor_state_alias.latest_finalized.root, + alias_finalized_hex, + sizeof(alias_finalized_hex)); + lantern_log_info( + "forkchoice", + &meta, + "persisted anchor state alias root=%s justified_slot=%" PRIu64 + " justified_root=%s finalized_slot=%" PRIu64 " finalized_root=%s", + anchor_root_hex[0] ? anchor_root_hex : "0x0", + anchor_state_alias.latest_justified.slot, + alias_justified_hex[0] ? alias_justified_hex : "0x0", + anchor_state_alias.latest_finalized.slot, + alias_finalized_hex[0] ? alias_finalized_hex : "0x0"); } } lantern_block_body_reset(&anchor.body); @@ -907,6 +1090,63 @@ int restore_persisted_blocks(struct lantern_client *client) } qsort(list.items, list.length, sizeof(list.items[0]), compare_blocks_by_slot); + const LanternRoot *store_anchor_root = + lantern_fork_choice_anchor_root(&client->fork_choice); + uint64_t store_anchor_slot = 0; + bool have_store_anchor_slot = + lantern_fork_choice_anchor_slot(&client->fork_choice, &store_anchor_slot) == 0; + LanternRoot store_head = {0}; + bool have_store_head = + lantern_fork_choice_current_head(&client->fork_choice, &store_head) == 0; + uint64_t store_head_slot = 0; + if (have_store_head + && lantern_fork_choice_block_info( + &client->fork_choice, + &store_head, + &store_head_slot, + NULL, + NULL) + != 0) + { + store_head_slot = 0; + } + const LanternCheckpoint *store_latest_justified = + lantern_fork_choice_latest_justified(&client->fork_choice); + const LanternCheckpoint *store_latest_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + char store_anchor_root_hex[ROOT_HEX_BUFFER_LEN]; + char store_head_hex[ROOT_HEX_BUFFER_LEN]; + char store_justified_root_hex[ROOT_HEX_BUFFER_LEN]; + char store_finalized_root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + store_anchor_root, + store_anchor_root_hex, + sizeof(store_anchor_root_hex)); + format_root_hex(&store_head, store_head_hex, sizeof(store_head_hex)); + format_root_hex( + store_latest_justified ? &store_latest_justified->root : NULL, + store_justified_root_hex, + sizeof(store_justified_root_hex)); + format_root_hex( + store_latest_finalized ? &store_latest_finalized->root : NULL, + store_finalized_root_hex, + sizeof(store_finalized_root_hex)); + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "restoring persisted blocks count=%zu anchor_slot=%" PRIu64 " anchor_root=%s" + " head_slot=%" PRIu64 " head_root=%s justified_slot=%" PRIu64 + " justified_root=%s finalized_slot=%" PRIu64 " finalized_root=%s", + list.length, + have_store_anchor_slot ? store_anchor_slot : 0u, + store_anchor_root_hex[0] ? store_anchor_root_hex : "0x0", + have_store_head ? store_head_slot : 0u, + have_store_head ? (store_head_hex[0] ? store_head_hex : "0x0") : "", + store_latest_justified ? store_latest_justified->slot : 0u, + store_justified_root_hex[0] ? store_justified_root_hex : "0x0", + store_latest_finalized ? store_latest_finalized->slot : 0u, + store_finalized_root_hex[0] ? store_finalized_root_hex : "0x0"); + for (size_t i = 0; i < list.length; ++i) { const struct lantern_persisted_block *entry = &list.items[i]; @@ -930,6 +1170,89 @@ int restore_persisted_blocks(struct lantern_client *client) post_justified = &cached_post_state.latest_justified; post_finalized = &cached_post_state.latest_finalized; } + const LanternState *post_state_for_restore = + have_cached_post_state ? &cached_post_state : NULL; + bool using_canonical_anchor_state = false; + if (store_anchor_root + && memcmp( + entry->root.bytes, + store_anchor_root->bytes, + LANTERN_ROOT_SIZE) + == 0) + { + post_state_for_restore = &client->state; + using_canonical_anchor_state = true; + } + + char block_root_hex[ROOT_HEX_BUFFER_LEN]; + char parent_root_hex[ROOT_HEX_BUFFER_LEN]; + char post_justified_root_hex[ROOT_HEX_BUFFER_LEN]; + char post_finalized_root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&entry->root, block_root_hex, sizeof(block_root_hex)); + format_root_hex(&block->parent_root, parent_root_hex, sizeof(parent_root_hex)); + format_root_hex( + &post_justified->root, + post_justified_root_hex, + sizeof(post_justified_root_hex)); + format_root_hex( + &post_finalized->root, + post_finalized_root_hex, + sizeof(post_finalized_root_hex)); + + bool justified_known = + !lantern_root_is_zero(&post_justified->root) + && lantern_fork_choice_block_info( + &client->fork_choice, + &post_justified->root, + NULL, + NULL, + NULL) + == 0; + bool finalized_known = + !lantern_root_is_zero(&post_finalized->root) + && lantern_fork_choice_block_info( + &client->fork_choice, + &post_finalized->root, + NULL, + NULL, + NULL) + == 0; + bool trace_restore_entry = + block->slot <= client->state.slot + 4u + || (store_anchor_root + && (memcmp( + post_justified->root.bytes, + store_anchor_root->bytes, + LANTERN_ROOT_SIZE) + != 0 + || memcmp( + post_finalized->root.bytes, + store_anchor_root->bytes, + LANTERN_ROOT_SIZE) + != 0)); + if (trace_restore_entry) + { + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "restore candidate slot=%" PRIu64 " root=%s parent=%s" + " cached_post_state=%s post_justified_slot=%" PRIu64 + " post_justified_root=%s post_justified_known=%s" + " post_finalized_slot=%" PRIu64 " post_finalized_root=%s" + " post_finalized_known=%s canonical_anchor_state=%s", + block->slot, + block_root_hex[0] ? block_root_hex : "0x0", + parent_root_hex[0] ? parent_root_hex : "0x0", + have_cached_post_state ? "true" : "false", + post_justified->slot, + post_justified_root_hex[0] ? post_justified_root_hex : "0x0", + justified_known ? "true" : "false", + post_finalized->slot, + post_finalized_root_hex[0] ? post_finalized_root_hex : "0x0", + finalized_known ? "true" : "false", + using_canonical_anchor_state ? "true" : "false"); + } + if (lantern_fork_choice_add_block_with_state( &client->fork_choice, block, @@ -937,14 +1260,28 @@ int restore_persisted_blocks(struct lantern_client *client) post_justified, post_finalized, &entry->root, - have_cached_post_state ? &cached_post_state : NULL) + post_state_for_restore) != 0) { lantern_log_warn( "forkchoice", &(const struct lantern_log_metadata){.validator = client->node_id}, - "failed to restore block at slot %" PRIu64, - entry->block.message.block.slot); + "failed to restore block at slot %" PRIu64 + " root=%s parent=%s cached_post_state=%s post_justified_slot=%" PRIu64 + " post_justified_root=%s post_justified_known=%s" + " post_finalized_slot=%" PRIu64 " post_finalized_root=%s" + " post_finalized_known=%s canonical_anchor_state=%s", + entry->block.message.block.slot, + block_root_hex[0] ? block_root_hex : "0x0", + parent_root_hex[0] ? parent_root_hex : "0x0", + have_cached_post_state ? "true" : "false", + post_justified->slot, + post_justified_root_hex[0] ? post_justified_root_hex : "0x0", + justified_known ? "true" : "false", + post_finalized->slot, + post_finalized_root_hex[0] ? post_finalized_root_hex : "0x0", + finalized_known ? "true" : "false", + using_canonical_anchor_state ? "true" : "false"); } else { @@ -970,39 +1307,76 @@ int restore_persisted_blocks(struct lantern_client *client) LanternCheckpoint restored_justified = client->state.latest_justified; LanternCheckpoint restored_finalized = client->state.latest_finalized; - const LanternCheckpoint *anchor_checkpoint = - lantern_fork_choice_latest_justified(&client->fork_choice); - if (anchor_checkpoint && !lantern_root_is_zero(&anchor_checkpoint->root)) - { - /* - * Keep checkpoint slots from state, but if the persisted checkpoint roots - * are not present in this local fork-choice tree (common after checkpoint - * sync), remap them to the local anchor root before restore. - * - * This mirrors leanSpec store bootstrap behavior where justified/finalized - * roots are anchored to the local store anchor. - */ - if (!lantern_root_is_zero(&restored_justified.root) - && lantern_fork_choice_block_info( - &client->fork_choice, - &restored_justified.root, - NULL, - NULL, - NULL) - != 0) - { - restored_justified.root = anchor_checkpoint->root; - } - if (!lantern_root_is_zero(&restored_finalized.root) - && lantern_fork_choice_block_info( - &client->fork_choice, - &restored_finalized.root, - NULL, - NULL, - NULL) - != 0) - { - restored_finalized.root = anchor_checkpoint->root; + const LanternRoot *restore_anchor_root = + lantern_fork_choice_anchor_root(&client->fork_choice); + uint64_t restore_anchor_slot = 0; + bool have_restore_anchor_slot = + lantern_fork_choice_anchor_slot(&client->fork_choice, &restore_anchor_slot) == 0; + bool restored_justified_known = + !lantern_root_is_zero(&restored_justified.root) + && lantern_fork_choice_block_info( + &client->fork_choice, + &restored_justified.root, + NULL, + NULL, + NULL) + == 0; + bool restored_finalized_known = + !lantern_root_is_zero(&restored_finalized.root) + && lantern_fork_choice_block_info( + &client->fork_choice, + &restored_finalized.root, + NULL, + NULL, + NULL) + == 0; + if (restore_anchor_root && have_restore_anchor_slot) + { + if (!restored_justified_known && restored_justified.slot <= restore_anchor_slot) + { + char original_justified_hex[ROOT_HEX_BUFFER_LEN]; + char anchor_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + &restored_justified.root, + original_justified_hex, + sizeof(original_justified_hex)); + format_root_hex( + restore_anchor_root, + anchor_hex, + sizeof(anchor_hex)); + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "aliasing restored justified checkpoint slot=%" PRIu64 + " original_root=%s anchor_slot=%" PRIu64 " anchor_root=%s", + restored_justified.slot, + original_justified_hex[0] ? original_justified_hex : "0x0", + restore_anchor_slot, + anchor_hex[0] ? anchor_hex : "0x0"); + restored_justified.root = *restore_anchor_root; + } + if (!restored_finalized_known && restored_finalized.slot <= restore_anchor_slot) + { + char original_finalized_hex[ROOT_HEX_BUFFER_LEN]; + char anchor_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex( + &restored_finalized.root, + original_finalized_hex, + sizeof(original_finalized_hex)); + format_root_hex( + restore_anchor_root, + anchor_hex, + sizeof(anchor_hex)); + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "aliasing restored finalized checkpoint slot=%" PRIu64 + " original_root=%s anchor_slot=%" PRIu64 " anchor_root=%s", + restored_finalized.slot, + original_finalized_hex[0] ? original_finalized_hex : "0x0", + restore_anchor_slot, + anchor_hex[0] ? anchor_hex : "0x0"); + restored_finalized.root = *restore_anchor_root; } } @@ -2068,6 +2442,73 @@ static bool pending_child_root_queue_next( return true; } +void lantern_client_pending_remove_branch_by_root( + struct lantern_client *client, + const LanternRoot *root) +{ + if (!client || !root || lantern_root_is_zero(root)) + { + return; + } + + bool locked = lantern_client_lock_pending(client); + if (!locked) + { + return; + } + + struct pending_child_root_queue queue; + pending_child_root_queue_init(&queue); + if (!pending_child_root_queue_append(&queue, root)) + { + pending_child_root_queue_reset(&queue); + lantern_client_unlock_pending(client, locked); + return; + } + + size_t removed = 0; + LanternRoot current = {0}; + while (pending_child_root_queue_next(&queue, ¤t)) + { + for (size_t i = client->pending_blocks.length; i-- > 0;) + { + struct lantern_pending_block *entry = &client->pending_blocks.items[i]; + bool root_matches = + memcmp(entry->root.bytes, current.bytes, LANTERN_ROOT_SIZE) == 0; + bool child_matches = + memcmp(entry->parent_root.bytes, current.bytes, LANTERN_ROOT_SIZE) == 0; + if (!root_matches && !child_matches) + { + continue; + } + + LanternRoot child_root = entry->root; + pending_block_list_remove(&client->pending_blocks, i); + removed += 1u; + + if (child_matches) + { + (void)pending_child_root_queue_append(&queue, &child_root); + } + } + } + + pending_child_root_queue_reset(&queue); + lantern_client_unlock_pending(client, locked); + + if (removed > 0) + { + char root_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(root, root_hex, sizeof(root_hex)); + lantern_log_debug( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pruned pending branch root=%s removed=%zu", + root_hex[0] ? root_hex : "0x0", + removed); + } +} + void lantern_client_request_pending_parent_after_blocks( struct lantern_client *client, const char *peer_text, @@ -2630,6 +3071,16 @@ void lantern_client_process_pending_children( continue; } + char parent_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(¤t_parent, parent_hex, sizeof(parent_hex)); + lantern_log_info( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pending child replay starting parent=%s pending=%zu queue_len=%zu", + parent_hex[0] ? parent_hex : "0x0", + pending_count, + client->pending_blocks.length); + struct pending_child_replay *replays = calloc(pending_count, sizeof(*replays)); if (!replays) @@ -2726,8 +3177,15 @@ void lantern_client_process_pending_children( } free(replays); - char parent_hex[ROOT_HEX_BUFFER_LEN]; - format_root_hex(¤t_parent, parent_hex, sizeof(parent_hex)); + lantern_log_info( + "sync", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pending child replay finished parent=%s pending=%zu replayed=%zu imported=%zu queued=%zu", + parent_hex[0] ? parent_hex : "0x0", + pending_count, + replay_count, + imported_count, + queued_children); lantern_log_debug( "sync", &(const struct lantern_log_metadata){.validator = client->node_id}, diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index f32c5dd..217be14 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -22,7 +22,6 @@ #include #include #include -#include #include #include @@ -163,7 +162,8 @@ static bool signed_block_signatures_are_valid( const struct lantern_client *client, const LanternSignedBlock *block, const struct lantern_log_metadata *meta, - bool *out_missing_parent_state) + bool *out_missing_parent_state, + LanternRoot *out_missing_parent_root) { if (!client || !block) { @@ -173,6 +173,10 @@ static bool signed_block_signatures_are_valid( { *out_missing_parent_state = false; } + if (out_missing_parent_root) + { + memset(out_missing_parent_root, 0, sizeof(*out_missing_parent_root)); + } LanternState parent_state; lantern_state_init(&parent_state); const LanternState *state_for_sig = NULL; @@ -187,11 +191,35 @@ static bool signed_block_signatures_are_valid( } else { - state_for_sig = lantern_client_state_for_root_locked( + LanternRoot missing_root = {0}; + state_for_sig = lantern_client_state_for_root_local_locked( (struct lantern_client *)client, &parent_root, &parent_state, NULL); + if (!state_for_sig + && lantern_client_find_missing_state_root_locked( + (struct lantern_client *)client, + &parent_root, + &missing_root)) + { + if (out_missing_parent_state) + { + *out_missing_parent_state = true; + } + if (out_missing_parent_root && !lantern_root_is_zero(&missing_root)) + { + *out_missing_parent_root = missing_root; + } + } + else if (!state_for_sig) + { + state_for_sig = lantern_client_state_for_root_locked( + (struct lantern_client *)client, + &parent_root, + &parent_state, + NULL); + } } if (!state_for_sig) { @@ -199,13 +227,22 @@ static bool signed_block_signatures_are_valid( { *out_missing_parent_state = true; } + if (out_missing_parent_root + && lantern_root_is_zero(out_missing_parent_root) + && !lantern_root_is_zero(&parent_root)) + { + *out_missing_parent_root = parent_root; + } char parent_hex[ROOT_HEX_BUFFER_LEN]; + char missing_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(&parent_root, parent_hex, sizeof(parent_hex)); + format_root_hex(out_missing_parent_root, missing_hex, sizeof(missing_hex)); lantern_log_warn( "state", meta, - "missing parent state for signature verification parent_root=%s", - parent_hex[0] ? parent_hex : "0x0"); + "missing parent state for signature verification parent_root=%s missing_root=%s", + parent_hex[0] ? parent_hex : "0x0", + missing_hex[0] ? missing_hex : "0x0"); lantern_state_reset(&parent_state); return false; } @@ -1321,7 +1358,7 @@ static void cache_rebuilt_state_for_root_locked( } } -const LanternState *lantern_client_state_for_root_locked( +const LanternState *lantern_client_state_for_root_local_locked( struct lantern_client *client, const LanternRoot *root, LanternState *scratch, @@ -1394,6 +1431,45 @@ const LanternState *lantern_client_state_for_root_locked( } } + if (client->data_dir && client->data_dir[0] && scratch) + { + lantern_state_reset(scratch); + lantern_state_init(scratch); + if (load_replay_base_from_finalized_locked( + client, + root, + scratch, + NULL, + NULL)) + { + if (out_is_scratch) + { + *out_is_scratch = true; + } + return scratch; + } + lantern_state_reset(scratch); + } + + return NULL; +} + +const LanternState *lantern_client_state_for_root_locked( + struct lantern_client *client, + const LanternRoot *root, + LanternState *scratch, + bool *out_is_scratch) +{ + const LanternState *state = lantern_client_state_for_root_local_locked( + client, + root, + scratch, + out_is_scratch); + if (state) + { + return state; + } + if (client->data_dir && client->data_dir[0]) { lantern_state_reset(scratch); @@ -1411,6 +1487,112 @@ const LanternState *lantern_client_state_for_root_locked( return NULL; } +bool lantern_client_find_missing_state_root_locked( + struct lantern_client *client, + const LanternRoot *root, + LanternRoot *out_missing_root) +{ + if (!client || !root || !out_missing_root || !client->has_fork_choice) + { + return false; + } + + memset(out_missing_root, 0, sizeof(*out_missing_root)); + + LanternRoot replay_stop_root = {0}; + uint64_t replay_stop_slot = 0; + uint64_t anchor_slot = 0; + bool allow_anchor_slot_stop = false; + + const LanternCheckpoint *fork_latest_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_latest_finalized) + { + replay_stop_root = fork_latest_finalized->root; + replay_stop_slot = fork_latest_finalized->slot; + } + else if (client->has_state) + { + replay_stop_root = client->state.latest_finalized.root; + replay_stop_slot = client->state.latest_finalized.slot; + } + + if (!lantern_root_is_zero(&replay_stop_root)) + { + const LanternRoot *anchor_root = + lantern_fork_choice_anchor_root(&client->fork_choice); + if (anchor_root + && memcmp( + anchor_root->bytes, + replay_stop_root.bytes, + LANTERN_ROOT_SIZE) + == 0 + && lantern_fork_choice_anchor_slot( + &client->fork_choice, + &anchor_slot) + == 0 + && replay_stop_slot < anchor_slot) + { + allow_anchor_slot_stop = true; + } + } + + LanternRoot current = *root; + size_t steps = 0; + while (!lantern_root_is_zero(¤t)) + { + if (steps > LANTERN_HISTORICAL_ROOTS_LIMIT) + { + return false; + } + + uint64_t slot = 0; + LanternRoot parent = {0}; + bool has_parent = false; + if (lantern_fork_choice_block_info( + &client->fork_choice, + ¤t, + &slot, + &parent, + &has_parent) + != 0) + { + *out_missing_root = current; + return true; + } + + if (!lantern_root_is_zero(&replay_stop_root) + && memcmp( + current.bytes, + replay_stop_root.bytes, + LANTERN_ROOT_SIZE) + == 0) + { + return false; + } + + if (allow_anchor_slot_stop && slot <= anchor_slot) + { + return false; + } + + if (!has_parent || lantern_root_is_zero(&parent)) + { + if (!lantern_root_is_zero(&replay_stop_root) || allow_anchor_slot_stop) + { + *out_missing_root = current; + return true; + } + return false; + } + + current = parent; + steps += 1u; + } + + return false; +} + static void adopt_state_locked(struct lantern_client *client, LanternState *state) { if (!client || !state) @@ -1558,12 +1740,12 @@ static bool rebuild_state_for_root_locked( if (client->has_fork_choice) { - const LanternCheckpoint *fork_finalized = + const LanternCheckpoint *fork_latest_finalized = lantern_fork_choice_latest_finalized(&client->fork_choice); - if (fork_finalized) + if (fork_latest_finalized) { - replay_stop_root = fork_finalized->root; - replay_stop_slot = fork_finalized->slot; + replay_stop_root = fork_latest_finalized->root; + replay_stop_slot = fork_latest_finalized->slot; } } else if (client->has_state) @@ -1941,6 +2123,84 @@ static enum block_parent_action handle_block_parent_locked( bool parent_known = lantern_client_block_known_locked(client, &parent_root, NULL); if (!parent_known) { + struct lantern_log_metadata parent_meta = {0}; + if (meta) + { + parent_meta = *meta; + } + parent_meta.has_slot = true; + parent_meta.slot = block->message.block.slot; + + LanternRoot head_root = {0}; + uint64_t head_slot = client->state.slot; + if (client->has_fork_choice + && lantern_fork_choice_current_head(&client->fork_choice, &head_root) == 0) + { + uint64_t fork_slot = 0; + if (lantern_fork_choice_block_info( + &client->fork_choice, + &head_root, + &fork_slot, + NULL, + NULL) + == 0) + { + head_slot = fork_slot; + } + } + const LanternRoot *anchor_root = + client->has_fork_choice + ? lantern_fork_choice_anchor_root(&client->fork_choice) + : NULL; + uint64_t anchor_slot = 0; + bool have_anchor_slot = + client->has_fork_choice + && lantern_fork_choice_anchor_slot(&client->fork_choice, &anchor_slot) == 0; + const LanternCheckpoint *store_latest_justified = + client->has_fork_choice + ? lantern_fork_choice_latest_justified(&client->fork_choice) + : NULL; + const LanternCheckpoint *store_latest_finalized = + client->has_fork_choice + ? lantern_fork_choice_latest_finalized(&client->fork_choice) + : NULL; + char block_hex[ROOT_HEX_BUFFER_LEN]; + char parent_hex[ROOT_HEX_BUFFER_LEN]; + char head_hex[ROOT_HEX_BUFFER_LEN]; + char anchor_hex[ROOT_HEX_BUFFER_LEN]; + char justified_hex[ROOT_HEX_BUFFER_LEN]; + char finalized_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(block_root, block_hex, sizeof(block_hex)); + format_root_hex(&parent_root, parent_hex, sizeof(parent_hex)); + format_root_hex(&head_root, head_hex, sizeof(head_hex)); + format_root_hex(anchor_root, anchor_hex, sizeof(anchor_hex)); + format_root_hex( + store_latest_justified ? &store_latest_justified->root : NULL, + justified_hex, + sizeof(justified_hex)); + format_root_hex( + store_latest_finalized ? &store_latest_finalized->root : NULL, + finalized_hex, + sizeof(finalized_hex)); + lantern_log_info( + "state", + &parent_meta, + "parent missing for block slot=%" PRIu64 " root=%s parent=%s" + " head_slot=%" PRIu64 " head_root=%s anchor_slot=%" PRIu64 + " anchor_root=%s store_justified_slot=%" PRIu64 + " store_justified_root=%s store_finalized_slot=%" PRIu64 + " store_finalized_root=%s", + block->message.block.slot, + block_hex[0] ? block_hex : "0x0", + parent_hex[0] ? parent_hex : "0x0", + head_slot, + head_hex[0] ? head_hex : "0x0", + have_anchor_slot ? anchor_slot : 0u, + anchor_hex[0] ? anchor_hex : "0x0", + store_latest_justified ? store_latest_justified->slot : 0u, + justified_hex[0] ? justified_hex : "0x0", + store_latest_finalized ? store_latest_finalized->slot : 0u, + finalized_hex[0] ? finalized_hex : "0x0"); const char *peer_text = meta && meta->peer ? meta->peer : NULL; lantern_client_unlock_state(client, *state_locked); *state_locked = false; @@ -2269,6 +2529,30 @@ static void get_head_info_locked( } } +static bool historical_import_floor_slot_locked( + struct lantern_client *client, + uint64_t *out_anchor_slot) +{ + if (out_anchor_slot) + { + *out_anchor_slot = 0; + } + if (!client || !out_anchor_slot || !client->has_fork_choice) + { + return false; + } + + uint64_t anchor_slot = 0; + if (lantern_fork_choice_anchor_slot(&client->fork_choice, &anchor_slot) != 0 + || anchor_slot == 0) + { + return false; + } + + *out_anchor_slot = anchor_slot; + return true; +} + static bool finalized_checkpoint_advanced( const LanternCheckpoint *previous_finalized, const LanternCheckpoint *current_finalized) @@ -2636,6 +2920,26 @@ static bool lantern_client_import_block_internal( uint64_t known_slot = 0; bool root_known = lantern_client_block_known_locked(client, &block_root_local, &known_slot); + uint64_t historical_floor_slot = 0; + bool below_historical_floor = + !root_known + && historical_import_floor_slot_locked(client, &historical_floor_slot) + && block->message.block.slot <= historical_floor_slot; + if (below_historical_floor) + { + char block_hex[ROOT_HEX_BUFFER_LEN]; + format_root_hex(&block_root_local, block_hex, sizeof(block_hex)); + lantern_log_debug( + "state", + meta, + "dropping pre-anchor historical block slot=%" PRIu64 " root=%s anchor_slot=%" PRIu64, + block->message.block.slot, + block_hex[0] ? block_hex : "0x0", + historical_floor_slot); + lantern_client_unlock_state(client, state_locked); + lantern_client_pending_remove_branch_by_root(client, &block_root_local); + return false; + } if (root_known && allow_historical && block->message.block.slot <= known_slot) { lantern_client_unlock_state(client, state_locked); @@ -2680,28 +2984,17 @@ static bool lantern_client_import_block_internal( bool parent_off_head = parent_action == BLOCK_PARENT_ACTION_KNOWN_OFF_HEAD; bool missing_parent_state_for_signature = false; + LanternRoot missing_parent_root = {0}; if (!signed_block_signatures_are_valid( client, block, meta, - &missing_parent_state_for_signature)) + &missing_parent_state_for_signature, + &missing_parent_root)) { if (missing_parent_state_for_signature) { LanternRoot parent_root = block->message.block.parent_root; - LanternRoot missing_roots[LANTERN_MAX_REQUEST_BLOCKS]; - size_t missing_count = 0; - LanternState rebuild_scratch; - lantern_state_init(&rebuild_scratch); - (void)rebuild_state_for_root_locked( - client, - &parent_root, - &rebuild_scratch, - missing_roots, - LANTERN_MAX_REQUEST_BLOCKS, - &missing_count); - lantern_state_reset(&rebuild_scratch); - const char *peer_text = meta && meta->peer ? meta->peer : NULL; lantern_client_enqueue_pending_block( client, @@ -2717,29 +3010,17 @@ static bool lantern_client_import_block_internal( { request_depth = LANTERN_MAX_BACKFILL_DEPTH; } - if (missing_count > 0) - { - uint32_t request_depths[LANTERN_MAX_REQUEST_BLOCKS]; - for (size_t i = 0; i < missing_count; ++i) - { - request_depths[i] = request_depth; - } - (void)lantern_client_try_schedule_blocks_request_batch( - client, - peer_text, - missing_roots, - request_depths, - missing_count); - } - else + LanternRoot request_root = parent_root; + if (!lantern_root_is_zero(&missing_parent_root)) { - (void)lantern_client_try_schedule_blocks_request_batch( - client, - peer_text, - &parent_root, - &request_depth, - 1u); + request_root = missing_parent_root; } + (void)lantern_client_try_schedule_blocks_request_batch( + client, + peer_text, + &request_root, + &request_depth, + 1u); } char root_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(&block_root_local, root_hex, sizeof(root_hex)); diff --git a/src/core/client_sync_internal.h b/src/core/client_sync_internal.h index bde1c6d..1d762d2 100644 --- a/src/core/client_sync_internal.h +++ b/src/core/client_sync_internal.h @@ -161,6 +161,24 @@ bool lantern_client_block_known_locked( const LanternRoot *root, uint64_t *out_slot); +/** + * Get a state snapshot for a specific block root without attempting replay. + * + * This probes only exact local sources: + * - the current state + * - fork-choice cached states + * - exact persisted state snapshots / finalized replay base snapshots + * + * It does not reconstruct missing states from a chain of blocks. + * + * @note Thread safety: Caller must hold state_lock + */ +const LanternState *lantern_client_state_for_root_local_locked( + struct lantern_client *client, + const LanternRoot *root, + LanternState *scratch, + bool *out_is_scratch); + /** * Get a state snapshot for a specific block root. * @@ -182,6 +200,20 @@ const LanternState *lantern_client_state_for_root_locked( LanternState *scratch, bool *out_is_scratch); +/** + * Find the first missing block/root on the replay path back toward the finalized anchor. + * + * Returns true and populates `out_missing_root` when the ancestry required to + * reconstruct `root` is not locally connected. Returns false when the ancestry + * looks locally complete or the root cannot be analyzed. + * + * @note Thread safety: Caller must hold state_lock + */ +bool lantern_client_find_missing_state_root_locked( + struct lantern_client *client, + const LanternRoot *root, + LanternRoot *out_missing_root); + /** * Return the active attestation committee count for sync/validator cache logic. * @@ -192,8 +224,8 @@ size_t lantern_client_attestation_committee_count(const struct lantern_client *c /** * Determine whether this node should retain an attestation signature locally. * - * The signature is retained only when the node is configured as an aggregator - * and the attester is on the local attestation subnet/committee. + * The signature is retained when the node is configured as an aggregator. + * Attestation subnet selection happens at the gossip subscription layer. * * @note Caller must hold state_lock. */ @@ -705,6 +737,18 @@ bool lantern_client_try_schedule_blocks_request_batch( */ void lantern_client_pending_remove_by_root(struct lantern_client *client, const LanternRoot *root); +/** + * Remove a pending block and any pending descendants rooted under it. + * + * @param client Client instance + * @param root Root of the dropped block + * + * @note Thread safety: Acquires pending_lock + */ +void lantern_client_pending_remove_branch_by_root( + struct lantern_client *client, + const LanternRoot *root); + /** * Process pending children of a newly imported block. diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index 985b0e9..c6c4d48 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -114,7 +114,8 @@ static bool validate_vote_checkpoint( } uint64_t block_slot = 0; - if (!lantern_client_block_known_locked(client, &checkpoint->root, &block_slot)) + bool known = lantern_client_block_known_locked(client, &checkpoint->root, &block_slot); + if (!known) { lantern_log_debug( log_facility, @@ -212,7 +213,6 @@ static bool buffer_pending_vote_locked( return true; } - /** * @brief Validates vote cache availability. * @@ -287,22 +287,7 @@ bool lantern_client_should_cache_attestation_signature_locked( if (!client || !vote || !client->assigned_validators || !client->assigned_validators->enr.is_aggregator) { return false; } - - size_t committee_count = lantern_client_attestation_committee_count(client); - if (committee_count == 0) { - return false; - } - - size_t vote_subnet = 0; - if (lantern_validator_index_compute_subnet_id( - vote->validator_id, - committee_count, - &vote_subnet) - != 0) { - return false; - } - - return vote_subnet == client->gossip.attestation_subnet_id; + return true; } @@ -430,20 +415,39 @@ static bool process_vote_locked( LanternState target_state; lantern_state_init(&target_state); - const LanternState *sig_state = lantern_client_state_for_root_locked( + LanternRoot missing_target_root = {0}; + const LanternState *sig_state = lantern_client_state_for_root_local_locked( client, &vote->data.target.root, &target_state, NULL); + if (!sig_state + && !lantern_client_find_missing_state_root_locked( + client, + &vote->data.target.root, + &missing_target_root)) + { + sig_state = lantern_client_state_for_root_locked( + client, + &vote->data.target.root, + &target_state, + NULL); + } if (!sig_state) { char target_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + char retry_hex[VOTE_ROOT_HEX_BUFFER_LEN]; format_root_hex(&vote->data.target.root, target_hex, sizeof(target_hex)); + format_root_hex( + lantern_root_is_zero(&missing_target_root) ? &vote->data.target.root : &missing_target_root, + retry_hex, + sizeof(retry_hex)); lantern_log_debug( "gossip", meta, - "missing target state root=%s for validator=%" PRIu64 " slot=%" PRIu64, + "missing target state root=%s retry_root=%s for validator=%" PRIu64 " slot=%" PRIu64, target_hex[0] ? target_hex : "0x0", + retry_hex[0] ? retry_hex : "0x0", vote->data.validator_id, vote->data.slot); lantern_vote_rejection_set( @@ -452,7 +456,8 @@ static bool process_vote_locked( vote->data.target.slot, target_hex[0] ? target_hex : "0x0"); rejection->should_retry_after_block_import = true; - rejection->retry_root = vote->data.target.root; + rejection->retry_root = + lantern_root_is_zero(&missing_target_root) ? vote->data.target.root : missing_target_root; rejection->retry_slot = vote->data.target.slot; lantern_state_reset(&target_state); return false; @@ -1006,7 +1011,7 @@ void lantern_client_replay_pending_gossip_votes(struct lantern_client *client) client, &pending.items[i].vote, peer_text, - false, + true, true); } diff --git a/src/core/client_validator.c b/src/core/client_validator.c index b7d4e76..fe632f9 100644 --- a/src/core/client_validator.c +++ b/src/core/client_validator.c @@ -1781,35 +1781,30 @@ int validator_publish_vote(struct lantern_client *client, const LanternSignedVot } lantern_client_unlock_state(client, state_locked); - int rc = lantern_gossipsub_service_publish_vote(&client->gossip, vote); - if (rc != 0) - { - lantern_log_warn( - "gossip", - &meta, - "failed to publish attestation validator=%" PRIu64 " slot=%" PRIu64, - vote->data.validator_id, - vote->data.slot); - return LANTERN_CLIENT_ERR_NETWORK; - } size_t subnet_id = 0; if (lantern_validator_index_compute_subnet_id( vote->data.validator_id, validator_attestation_committee_count(client), &subnet_id) == 0) { - if (subnet_id == client->gossip.attestation_subnet_id) { - if (lantern_gossipsub_service_publish_vote_subnet(&client->gossip, vote) != 0) { - lantern_log_warn( - "gossip", - &meta, - "failed to publish subnet attestation validator=%" PRIu64 " slot=%" PRIu64 " subnet=%zu", - vote->data.validator_id, - vote->data.slot, - subnet_id); - return LANTERN_CLIENT_ERR_NETWORK; - } + if (lantern_gossipsub_service_publish_vote_subnet(&client->gossip, vote, subnet_id) != 0) { + lantern_log_warn( + "gossip", + &meta, + "failed to publish subnet attestation validator=%" PRIu64 " slot=%" PRIu64 " subnet=%zu", + vote->data.validator_id, + vote->data.slot, + subnet_id); + return LANTERN_CLIENT_ERR_NETWORK; } + } else { + lantern_log_warn( + "gossip", + &meta, + "failed to compute attestation subnet validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + return LANTERN_CLIENT_ERR_NETWORK; } lantern_log_info( "gossip", diff --git a/src/http/server.c b/src/http/server.c index 6302e83..b7c69f6 100644 --- a/src/http/server.c +++ b/src/http/server.c @@ -42,7 +42,7 @@ static const char LANTERN_HTTP_PATH_METRICS[] = "/metrics"; static const char LANTERN_HTTP_PATH_FINALIZED[] = "/lean/v0/states/finalized"; static const char LANTERN_HTTP_PATH_JUSTIFIED[] = "/lean/v0/checkpoints/justified"; static const char LANTERN_HTTP_PATH_FORK_CHOICE[] = "/lean/v0/fork_choice"; -static const char LANTERN_HTTP_JSON_HEALTH[] = "{\"status\":\"healthy\",\"service\":\"lean-spec-api\"}"; +static const char LANTERN_HTTP_JSON_HEALTH[] = "{\"status\":\"healthy\",\"service\":\"lean-rpc-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\"}"; diff --git a/src/networking/gossipsub_service.c b/src/networking/gossipsub_service.c index 2d1c815..20d114f 100644 --- a/src/networking/gossipsub_service.c +++ b/src/networking/gossipsub_service.c @@ -364,8 +364,15 @@ static size_t gossipsub_snappy_max_uncompressed( if (service->vote_topic[0] != '\0' && strcmp(topic, service->vote_topic) == 0) { return vote_max; } - if (service->vote_subnet_topic[0] != '\0' && strcmp(topic, service->vote_subnet_topic) == 0) { - return vote_max; + if (service) { + if (service->vote_subnet_topic[0] != '\0' && strcmp(topic, service->vote_subnet_topic) == 0) { + return vote_max; + } + for (size_t i = 0; i < service->extra_vote_subnet_topic_count; ++i) { + if (strcmp(topic, service->extra_vote_subnet_topics[i]) == 0) { + return vote_max; + } + } } if (service->aggregated_attestation_topic[0] != '\0' && strcmp(topic, service->aggregated_attestation_topic) == 0) { @@ -468,6 +475,41 @@ static int subscribe_topic( return err == LIBP2P_ERR_OK ? 0 : -1; } +static libp2p_gossipsub_validation_result_t gossipsub_vote_validator( + const libp2p_gossipsub_message_t *msg, + void *user_data); + +static int register_vote_validator_for_topic( + struct lantern_gossipsub_service *service, + const char *topic, + const char *error_message, + libp2p_gossipsub_validator_handle_t **out_handle) { + if (!service || !service->gossipsub || !topic || !out_handle) { + return -1; + } + *out_handle = NULL; + if (!service->vote_handler) { + return 0; + } + + libp2p_gossipsub_validator_def_t def = { + .struct_size = sizeof(def), + .type = LIBP2P_GOSSIPSUB_VALIDATOR_SYNC, + .sync_fn = gossipsub_vote_validator, + .async_fn = NULL, + .user_data = service + }; + if (libp2p_gossipsub_add_validator(service->gossipsub, topic, &def, out_handle) != LIBP2P_ERR_OK) { + lantern_log_error( + "gossip", + NULL, + "%s", + error_message ? error_message : "failed to register vote gossip validator"); + return -1; + } + return 0; +} + static libp2p_gossipsub_validation_result_t gossipsub_block_validator( const libp2p_gossipsub_message_t *msg, void *user_data) { @@ -733,11 +775,25 @@ static void lantern_gossipsub_service_remove_validators(struct lantern_gossipsub service->gossipsub, service->aggregated_attestation_validator_handle); } + for (size_t i = 0; i < service->extra_vote_subnet_topic_count; ++i) { + if (service->extra_vote_subnet_validator_handles + && service->extra_vote_subnet_validator_handles[i]) { + (void)libp2p_gossipsub_remove_validator( + service->gossipsub, + service->extra_vote_subnet_validator_handles[i]); + } + } } service->block_validator_handle = NULL; service->vote_validator_handle = NULL; service->vote_subnet_validator_handle = NULL; service->aggregated_attestation_validator_handle = NULL; + if (service->extra_vote_subnet_validator_handles) { + memset( + service->extra_vote_subnet_validator_handles, + 0, + service->extra_vote_subnet_topic_count * sizeof(*service->extra_vote_subnet_validator_handles)); + } } void lantern_gossipsub_service_stop(struct lantern_gossipsub_service *service) { @@ -764,11 +820,17 @@ void lantern_gossipsub_service_reset(struct lantern_gossipsub_service *service) memset(service->vote_subnet_topic, 0, sizeof(service->vote_subnet_topic)); memset(service->aggregated_attestation_topic, 0, sizeof(service->aggregated_attestation_topic)); service->data_dir = NULL; + service->devnet = NULL; service->attestation_subnet_id = 0; service->subscribe_attestation_subnet = 0; service->publish_hook = NULL; service->publish_hook_user_data = NULL; service->loopback_only = 0; + free(service->extra_vote_subnet_topics); + service->extra_vote_subnet_topics = NULL; + free(service->extra_vote_subnet_validator_handles); + service->extra_vote_subnet_validator_handles = NULL; + service->extra_vote_subnet_topic_count = 0; } int lantern_gossipsub_service_start( @@ -799,6 +861,7 @@ int lantern_gossipsub_service_start( service->aggregated_attestation_handler = previous_aggregated_handler; service->aggregated_attestation_handler_user_data = previous_aggregated_user_data; service->data_dir = config->data_dir; + service->devnet = config->devnet; service->attestation_subnet_id = config->attestation_subnet_id; service->subscribe_attestation_subnet = config->subscribe_attestation_subnet ? 1 : 0; @@ -874,15 +937,17 @@ int lantern_gossipsub_service_start( &(const struct lantern_log_metadata){.peer = config->devnet}, "subscribed gossipsub topic=%s", service->block_topic); - if (subscribe_topic(service, service->vote_topic) != 0) { - lantern_gossipsub_service_reset(service); - return -1; + if (!service->subscribe_attestation_subnet) { + if (subscribe_topic(service, service->vote_topic) != 0) { + lantern_gossipsub_service_reset(service); + return -1; + } + lantern_log_info( + "gossip", + &(const struct lantern_log_metadata){.peer = config->devnet}, + "subscribed gossipsub topic=%s", + service->vote_topic); } - lantern_log_info( - "gossip", - &(const struct lantern_log_metadata){.peer = config->devnet}, - "subscribed gossipsub topic=%s", - service->vote_topic); if (service->subscribe_attestation_subnet) { if (subscribe_topic(service, service->vote_subnet_topic) != 0) { lantern_gossipsub_service_reset(service); @@ -923,37 +988,26 @@ int lantern_gossipsub_service_start( } } - if (service->vote_handler) { - libp2p_gossipsub_validator_def_t def = { - .struct_size = sizeof(def), - .type = LIBP2P_GOSSIPSUB_VALIDATOR_SYNC, - .sync_fn = gossipsub_vote_validator, - .async_fn = NULL, - .user_data = service - }; - if (libp2p_gossipsub_add_validator(service->gossipsub, service->vote_topic, &def, &service->vote_validator_handle) - != LIBP2P_ERR_OK) { - lantern_log_error( - "gossip", - NULL, - "failed to register vote gossip validator"); + if (!service->subscribe_attestation_subnet) { + if (register_vote_validator_for_topic( + service, + service->vote_topic, + "failed to register vote gossip validator", + &service->vote_validator_handle) + != 0) { lantern_gossipsub_service_reset(service); return -1; } - if (service->subscribe_attestation_subnet) { - if (libp2p_gossipsub_add_validator( - service->gossipsub, - service->vote_subnet_topic, - &def, - &service->vote_subnet_validator_handle) - != LIBP2P_ERR_OK) { - lantern_log_error( - "gossip", - NULL, - "failed to register subnet vote gossip validator"); - lantern_gossipsub_service_reset(service); - return -1; - } + } + if (service->subscribe_attestation_subnet) { + if (register_vote_validator_for_topic( + service, + service->vote_subnet_topic, + "failed to register subnet vote gossip validator", + &service->vote_subnet_validator_handle) + != 0) { + lantern_gossipsub_service_reset(service); + return -1; } } @@ -1083,8 +1137,26 @@ int lantern_gossipsub_service_publish_vote( int lantern_gossipsub_service_publish_vote_subnet( struct lantern_gossipsub_service *service, - const LanternSignedVote *vote) { - if (!service || !vote || service->vote_subnet_topic[0] == '\0') { + const LanternSignedVote *vote, + size_t subnet_id) { + if (!service || !vote) { + return -1; + } + char topic[LANTERN_GOSSIPSUB_TOPIC_CAP]; + const char *publish_topic = NULL; + if (service->devnet + && lantern_gossip_topic_format_subnet( + LANTERN_GOSSIP_TOPIC_VOTE_SUBNET, + service->devnet, + subnet_id, + topic, + sizeof(topic)) + == 0) { + publish_topic = topic; + } else if (service->vote_subnet_topic[0] != '\0' + && service->attestation_subnet_id == subnet_id) { + publish_topic = service->vote_subnet_topic; + } else { return -1; } size_t max_compressed = 0; @@ -1100,11 +1172,93 @@ int lantern_gossipsub_service_publish_vote_subnet( free(compressed); return -1; } - int publish_rc = publish_payload(service, service->vote_subnet_topic, compressed, written); + int publish_rc = publish_payload(service, publish_topic, compressed, written); free(compressed); return publish_rc; } +int lantern_gossipsub_service_subscribe_attestation_subnet( + struct lantern_gossipsub_service *service, + size_t subnet_id) { + if (!service || !service->gossipsub || !service->devnet) { + return -1; + } + + char topic[LANTERN_GOSSIPSUB_TOPIC_CAP]; + if (lantern_gossip_topic_format_subnet( + LANTERN_GOSSIP_TOPIC_VOTE_SUBNET, + service->devnet, + subnet_id, + topic, + sizeof(topic)) + != 0) { + return -1; + } + + if (service->vote_subnet_topic[0] != '\0' && strcmp(topic, service->vote_subnet_topic) == 0) { + return 0; + } + for (size_t i = 0; i < service->extra_vote_subnet_topic_count; ++i) { + if (strcmp(topic, service->extra_vote_subnet_topics[i]) == 0) { + return 0; + } + } + + if (subscribe_topic(service, topic) != 0) { + return -1; + } + lantern_log_info( + "gossip", + &(const struct lantern_log_metadata){.peer = service->devnet}, + "subscribed gossipsub topic=%s", + topic); + + libp2p_gossipsub_validator_handle_t *handle = NULL; + if (register_vote_validator_for_topic( + service, + topic, + "failed to register subnet vote gossip validator", + &handle) + != 0) { + (void)libp2p_gossipsub_unsubscribe(service->gossipsub, topic); + return -1; + } + + size_t new_count = service->extra_vote_subnet_topic_count + 1u; + char (*new_topics)[LANTERN_GOSSIPSUB_TOPIC_CAP] = + calloc(new_count, sizeof(*new_topics)); + libp2p_gossipsub_validator_handle_t **new_handles = + calloc(new_count, sizeof(*new_handles)); + if (!new_topics || !new_handles) { + free(new_topics); + free(new_handles); + if (handle) { + (void)libp2p_gossipsub_remove_validator(service->gossipsub, handle); + } + (void)libp2p_gossipsub_unsubscribe(service->gossipsub, topic); + return -1; + } + if (service->extra_vote_subnet_topic_count > 0) { + memcpy( + new_topics, + service->extra_vote_subnet_topics, + service->extra_vote_subnet_topic_count * sizeof(*new_topics)); + memcpy( + new_handles, + service->extra_vote_subnet_validator_handles, + service->extra_vote_subnet_topic_count * sizeof(*new_handles)); + } + memcpy(new_topics[service->extra_vote_subnet_topic_count], topic, sizeof(topic)); + new_handles[service->extra_vote_subnet_topic_count] = handle; + + free(service->extra_vote_subnet_topics); + free(service->extra_vote_subnet_validator_handles); + service->extra_vote_subnet_topics = new_topics; + service->extra_vote_subnet_validator_handles = new_handles; + service->extra_vote_subnet_topic_count = new_count; + return 0; +} + int lantern_gossipsub_service_publish_aggregated_attestation( struct lantern_gossipsub_service *service, const LanternSignedAggregatedAttestation *attestation) { diff --git a/tests/unit/test_checkpoint_sync_api.c b/tests/unit/test_checkpoint_sync_api.c index a48786e..191274b 100644 --- a/tests/unit/test_checkpoint_sync_api.c +++ b/tests/unit/test_checkpoint_sync_api.c @@ -907,7 +907,7 @@ static int test_health_endpoint(void) expect_true(strstr(header, "HTTP/1.1 200") != NULL, "status 200 health"); expect_true(strstr(header, "Content-Type: application/json") != NULL, "content-type health"); - static const char expected[] = "{\"status\":\"healthy\",\"service\":\"lean-spec-api\"}"; + static const char expected[] = "{\"status\":\"healthy\",\"service\":\"lean-rpc-api\"}"; size_t body_len = response_len - header_end; expect_true(body_len == sizeof(expected) - 1u, "health body length"); expect_true(memcmp(response + header_end, expected, sizeof(expected) - 1u) == 0, "health body"); diff --git a/tests/unit/test_client_gossip.c b/tests/unit/test_client_gossip.c index 11e08a3..f0f275f 100644 --- a/tests/unit/test_client_gossip.c +++ b/tests/unit/test_client_gossip.c @@ -1,11 +1,14 @@ +#include #include #include #include +#include "../../src/core/client_services_internal.h" #include "client_test_helpers.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" #include "lantern/core/client.h" +#include "lantern/support/string_list.h" static void reset_agg_cache(struct lantern_client *client) { @@ -15,6 +18,91 @@ static void reset_agg_cache(struct lantern_client *client) lantern_store_reset(&client->store); } +static int test_enable_blocks_request_peer( + struct lantern_client *client, + const char *peer_id) +{ + if (!client || !peer_id || peer_id[0] == '\0') { + return -1; + } + + lantern_string_list_init(&client->connected_peer_ids); + if (pthread_mutex_init(&client->connection_lock, NULL) != 0) { + return -1; + } + client->connection_lock_initialized = true; + + if (pthread_mutex_init(&client->status_lock, NULL) != 0) { + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + lantern_string_list_reset(&client->connected_peer_ids); + return -1; + } + client->status_lock_initialized = true; + + if (lantern_string_list_append(&client->connected_peer_ids, peer_id) != 0) { + pthread_mutex_destroy(&client->status_lock); + client->status_lock_initialized = false; + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + lantern_string_list_reset(&client->connected_peer_ids); + return -1; + } + client->connected_peers = 1u; + + client->peer_status_entries = calloc(1u, sizeof(*client->peer_status_entries)); + if (!client->peer_status_entries) { + client->connected_peers = 0u; + lantern_string_list_reset(&client->connected_peer_ids); + pthread_mutex_destroy(&client->status_lock); + client->status_lock_initialized = false; + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + return -1; + } + client->peer_status_count = 1u; + client->peer_status_capacity = 1u; + strncpy( + client->peer_status_entries[0].peer_id, + peer_id, + sizeof(client->peer_status_entries[0].peer_id) - 1u); + client->peer_status_entries[0].peer_id[sizeof(client->peer_status_entries[0].peer_id) - 1u] = + '\0'; + + return 0; +} + +static void test_disable_blocks_request_peer(struct lantern_client *client) +{ + if (!client) { + return; + } + + free(client->peer_status_entries); + client->peer_status_entries = NULL; + client->peer_status_count = 0u; + client->peer_status_capacity = 0u; + + free(client->active_blocks_requests); + client->active_blocks_requests = NULL; + client->active_blocks_request_count = 0u; + client->active_blocks_request_capacity = 0u; + client->next_blocks_request_id = 0u; + + if (client->status_lock_initialized) { + pthread_mutex_destroy(&client->status_lock); + client->status_lock_initialized = false; + } + + if (client->connection_lock_initialized) { + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + } + + lantern_string_list_reset(&client->connected_peer_ids); + client->connected_peers = 0u; +} + static int build_single_participant_aggregated_attestation( struct lantern_client *client, struct PQSignatureSchemeSecretKey *secret, @@ -330,6 +418,13 @@ static int test_gossip_aggregated_attestation_rejects_unknown_target(void) != 0) { return 1; } + if (test_enable_blocks_request_peer( + &client, + "12D3KooWQH2VQK1kF2L8a7T4AtestAggUnknownTarget111111111111") + != 0) { + fprintf(stderr, "failed to set up schedulable peer for aggregated gossip test\n"); + goto cleanup; + } if (build_single_participant_aggregated_attestation( &client, @@ -352,12 +447,17 @@ static int test_gossip_aggregated_attestation_rejects_unknown_target(void) fprintf(stderr, "unknown-target aggregated attestation should not be cached\n"); goto cleanup_attestation; } + if (client.next_blocks_request_id != 0u) { + fprintf(stderr, "unknown-target aggregated attestation should not schedule block requests\n"); + goto cleanup_attestation; + } rc = 0; cleanup_attestation: lantern_signed_aggregated_attestation_reset(&attestation); cleanup: + test_disable_blocks_request_peer(&client); reset_agg_cache(&client); client_test_teardown_vote_validation_client(&client, pub, secret); return rc; diff --git a/tests/unit/test_client_vote.c b/tests/unit/test_client_vote.c index d8aed9e..4151abc 100644 --- a/tests/unit/test_client_vote.c +++ b/tests/unit/test_client_vote.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -6,11 +7,13 @@ #include #include +#include "../../src/core/client_services_internal.h" #include "lantern/consensus/duties.h" #include "client_test_helpers.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" #include "lantern/networking/gossip_payloads.h" +#include "lantern/support/string_list.h" #include "lantern/storage/storage.h" #include "lantern/support/time.h" @@ -66,6 +69,91 @@ static void test_reset_agg_cache(struct lantern_client *client) { lantern_store_reset(&client->store); } +static int test_enable_blocks_request_peer( + struct lantern_client *client, + const char *peer_id) +{ + if (!client || !peer_id || peer_id[0] == '\0') { + return -1; + } + + lantern_string_list_init(&client->connected_peer_ids); + if (pthread_mutex_init(&client->connection_lock, NULL) != 0) { + return -1; + } + client->connection_lock_initialized = true; + + if (pthread_mutex_init(&client->status_lock, NULL) != 0) { + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + lantern_string_list_reset(&client->connected_peer_ids); + return -1; + } + client->status_lock_initialized = true; + + if (lantern_string_list_append(&client->connected_peer_ids, peer_id) != 0) { + pthread_mutex_destroy(&client->status_lock); + client->status_lock_initialized = false; + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + lantern_string_list_reset(&client->connected_peer_ids); + return -1; + } + client->connected_peers = 1u; + + client->peer_status_entries = calloc(1u, sizeof(*client->peer_status_entries)); + if (!client->peer_status_entries) { + client->connected_peers = 0u; + lantern_string_list_reset(&client->connected_peer_ids); + pthread_mutex_destroy(&client->status_lock); + client->status_lock_initialized = false; + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + return -1; + } + client->peer_status_count = 1u; + client->peer_status_capacity = 1u; + strncpy( + client->peer_status_entries[0].peer_id, + peer_id, + sizeof(client->peer_status_entries[0].peer_id) - 1u); + client->peer_status_entries[0].peer_id[sizeof(client->peer_status_entries[0].peer_id) - 1u] = + '\0'; + + return 0; +} + +static void test_disable_blocks_request_peer(struct lantern_client *client) +{ + if (!client) { + return; + } + + free(client->peer_status_entries); + client->peer_status_entries = NULL; + client->peer_status_count = 0u; + client->peer_status_capacity = 0u; + + free(client->active_blocks_requests); + client->active_blocks_requests = NULL; + client->active_blocks_request_count = 0u; + client->active_blocks_request_capacity = 0u; + client->next_blocks_request_id = 0u; + + if (client->status_lock_initialized) { + pthread_mutex_destroy(&client->status_lock); + client->status_lock_initialized = false; + } + + if (client->connection_lock_initialized) { + pthread_mutex_destroy(&client->connection_lock); + client->connection_lock_initialized = false; + } + + lantern_string_list_reset(&client->connected_peer_ids); + client->connected_peers = 0u; +} + static int test_make_dummy_proof( LanternAggregatedSignatureProof *out_proof, uint64_t validator_id, @@ -413,6 +501,13 @@ static int test_record_vote_buffers_missing_target_state(void) { != 0) { return 1; } + if (test_enable_blocks_request_peer( + &client, + "12D3KooWLU9zbm4c9KTL3f6bAtestVoteMissingTarget111111111111") + != 0) { + fprintf(stderr, "failed to set up schedulable peer for missing target state test\n"); + goto cleanup; + } lantern_block_body_init(&grandchild.body); uint64_t child_slot = 0; @@ -458,15 +553,104 @@ static int test_record_vote_buffers_missing_target_state(void) { fprintf(stderr, "vote with missing target state should be buffered\n"); goto cleanup; } + if (client.next_blocks_request_id != 0u) { + fprintf(stderr, "missing target state vote should not schedule block requests\n"); + goto cleanup; + } rc = 0; cleanup: + test_disable_blocks_request_peer(&client); lantern_block_body_reset(&grandchild.body); client_test_teardown_vote_validation_client(&client, pub, secret); return rc; } +static int test_record_vote_buffers_source_root_known_only_via_historical_hashes(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + LanternRoot historical_source_root; + int rc = 1; + + memset(&historical_source_root, 0, sizeof(historical_source_root)); + + if (client_test_setup_vote_validation_client( + &client, + "vote_historical_source_only", + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + return 1; + } + + if (lantern_fork_choice_set_block_state(&client.fork_choice, &child_root, &client.state) != 0) { + fprintf(stderr, "failed to cache child state for historical source test\n"); + goto cleanup; + } + + if (lantern_root_list_resize(&client.state.historical_block_hashes, 1u) != 0) { + fprintf(stderr, "failed to resize historical block hashes for source fallback test\n"); + goto cleanup; + } + client_test_fill_root(&historical_source_root, 0xA5u); + client.state.historical_block_hashes.items[0] = historical_source_root; + + { + uint64_t unexpected_slot = 0; + if (client_test_slot_for_root(&client, &historical_source_root, &unexpected_slot) == 0) { + fprintf(stderr, "historical-only source root unexpectedly exists in fork choice\n"); + goto cleanup; + } + } + + LanternSignedVote vote; + memset(&vote, 0, sizeof(vote)); + uint64_t child_slot = 0; + if (client_test_slot_for_root(&client, &child_root, &child_slot) != 0) { + fprintf(stderr, "failed to resolve child slot for historical source test\n"); + goto cleanup; + } + vote.data.validator_id = 0u; + vote.data.slot = 2u; + vote.data.head.slot = child_slot; + vote.data.head.root = child_root; + vote.data.target.slot = child_slot; + vote.data.target.root = child_root; + vote.data.source.slot = 0u; + vote.data.source.root = historical_source_root; + + if (client_test_sign_vote_with_secret(&vote, secret) != 0) { + fprintf(stderr, "failed to sign vote with historical-only source root\n"); + goto cleanup; + } + + if (lantern_client_debug_record_vote(&client, &vote, "vote_historical_source_peer") != 0) { + fprintf(stderr, "lantern_client_debug_record_vote failed for historical source test\n"); + goto cleanup; + } + + if (lantern_store_validator_has_vote(&client.store, 0u)) { + fprintf(stderr, "historical-only source root vote should not be stored\n"); + goto cleanup; + } + if (lantern_client_pending_vote_count(&client) != 1u) { + fprintf(stderr, "historical-only source root vote should be buffered\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + static int test_record_vote_buffers_unknown_head(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -2302,8 +2486,8 @@ static int test_publish_aggregated_attestations_collects_any_slot_and_prunes_gos fprintf(stderr, "failed to record votes for subnet filter test\n"); goto cleanup; } - if (client.store.gossip_signatures.length != 2u) { - fprintf(stderr, "expected only matching-subnet gossip signatures before aggregation\n"); + if (client.store.gossip_signatures.length != 3u) { + fprintf(stderr, "expected every gossip signature to be cached before aggregation\n"); goto cleanup; } if (lantern_hash_tree_root_attestation_data(&vote0.data.data, &data_root) != 0) { @@ -2333,11 +2517,11 @@ static int test_publish_aggregated_attestations_collects_any_slot_and_prunes_gos if (!lantern_bitlist_get(&decoded.proof.participants, 0u) || !lantern_bitlist_get(&decoded.proof.participants, 4u) || lantern_bitlist_get(&decoded.proof.participants, 1u)) { - fprintf(stderr, "published aggregated proof participants did not enforce subnet filtering\n"); + fprintf(stderr, "published aggregated proof participants should still match the local subnet\n"); goto cleanup; } - if (client.store.gossip_signatures.length != 0u) { - fprintf(stderr, "expected aggregated gossip signatures to be fully pruned after publish\n"); + if (client.store.gossip_signatures.length != 1u) { + fprintf(stderr, "expected only the aggregated subnet signatures to be pruned after publish\n"); goto cleanup; } if (lantern_store_get_gossip_signature(&client.store, &vote0_key, &cached_signature) == 0 @@ -2345,8 +2529,8 @@ static int test_publish_aggregated_attestations_collects_any_slot_and_prunes_gos fprintf(stderr, "aggregated subnet votes should have been removed from gossip cache\n"); goto cleanup; } - if (lantern_store_get_gossip_signature(&client.store, &vote1_key, &cached_signature) == 0) { - fprintf(stderr, "cross-subnet gossip vote should never enter the gossip signature cache\n"); + if (lantern_store_get_gossip_signature(&client.store, &vote1_key, &cached_signature) != 0) { + fprintf(stderr, "cross-subnet gossip vote should remain cached after local subnet aggregation\n"); goto cleanup; } @@ -2545,6 +2729,9 @@ int main(void) { if (test_record_vote_buffers_missing_target_state() != 0) { return 1; } + if (test_record_vote_buffers_source_root_known_only_via_historical_hashes() != 0) { + return 1; + } if (test_record_vote_buffers_unknown_head() != 0) { return 1; } diff --git a/tests/unit/test_fork_choice.c b/tests/unit/test_fork_choice.c index 2a6f324..fcf3b93 100644 --- a/tests/unit/test_fork_choice.c +++ b/tests/unit/test_fork_choice.c @@ -947,7 +947,7 @@ static int test_fork_choice_checkpoint_progression(void) { LanternRoot unknown_root; memset(&unknown_root, 0xEE, sizeof(unknown_root)); LanternCheckpoint unknown_cp = make_checkpoint(&unknown_root, block_one.slot + 1u); - assert(lantern_fork_choice_update_checkpoints(&store, &unknown_cp, &unknown_cp) == 0); + assert(lantern_fork_choice_update_checkpoints(&store, &unknown_cp, &unknown_cp) != 0); latest_justified = lantern_fork_choice_latest_justified(&store); latest_finalized = lantern_fork_choice_latest_finalized(&store); assert(latest_justified); @@ -1044,14 +1044,16 @@ static int test_fork_choice_restore_checkpoints(void) { fill_root(&unknown_cp.root, 0xEE); assert(lantern_fork_choice_restore_checkpoints(&store, &unknown_cp, &genesis_cp) != 0); - const LanternCheckpoint *justified_after_failure = lantern_fork_choice_latest_justified(&store); - const LanternCheckpoint *finalized_after_failure = lantern_fork_choice_latest_finalized(&store); - assert(justified_after_failure && checkpoints_equal(justified_after_failure, &block_one_cp)); - assert(finalized_after_failure && checkpoints_equal(finalized_after_failure, &genesis_cp)); + const LanternCheckpoint *justified_after_restore = + lantern_fork_choice_latest_justified(&store); + const LanternCheckpoint *finalized_after_restore = + lantern_fork_choice_latest_finalized(&store); + assert(justified_after_restore && checkpoints_equal(justified_after_restore, &block_one_cp)); + assert(finalized_after_restore && checkpoints_equal(finalized_after_restore, &genesis_cp)); - LanternRoot head_after_failure; - assert(lantern_fork_choice_current_head(&store, &head_after_failure) == 0); - assert(roots_equal(&head_after_failure, &head_before_failure)); + LanternRoot head_after_restore; + assert(lantern_fork_choice_current_head(&store, &head_after_restore) == 0); + assert(roots_equal(&head_after_restore, &head_before_failure)); lantern_store_reset(&backing_store); lantern_fork_choice_reset(&store); @@ -1082,6 +1084,25 @@ static int test_fork_choice_anchor_metadata_survives_checkpoint_restore(void) { assert(lantern_fork_choice_anchor_slot(&store, &stored_anchor_slot) == 0); assert(stored_anchor_slot == anchor.slot); + LanternCheckpoint anchor_bootstrap_justified = anchor_cp; + LanternCheckpoint anchor_bootstrap_finalized = anchor_cp; + anchor_bootstrap_justified.slot = anchor.slot - 1u; + anchor_bootstrap_finalized.slot = anchor.slot - 2u; + assert( + lantern_fork_choice_restore_checkpoints( + &store, + &anchor_bootstrap_justified, + &anchor_bootstrap_finalized) + == 0); + const LanternCheckpoint *latest_justified = lantern_fork_choice_latest_justified(&store); + const LanternCheckpoint *latest_finalized = lantern_fork_choice_latest_finalized(&store); + assert(latest_justified); + assert(latest_finalized); + assert(latest_justified->slot == anchor_bootstrap_justified.slot); + assert(latest_finalized->slot == anchor_bootstrap_finalized.slot); + assert(roots_equal(&latest_justified->root, &anchor_root)); + assert(roots_equal(&latest_finalized->root, &anchor_root)); + LanternBlock block_one; init_block(&block_one, anchor.slot + 1u, 1, &anchor_root, 0x52); LanternRoot block_one_root; @@ -1116,6 +1137,62 @@ static int test_fork_choice_anchor_metadata_survives_checkpoint_restore(void) { return 0; } +static int test_fork_choice_restore_checkpoints_rejects_unknown_roots(void) { + LanternForkChoice store; + LanternStore backing_store; + + LanternConfig config = {.num_validators = 4, .genesis_time = 1}; + configure_fork_choice_with_backing_store(&store, &backing_store, &config); + + LanternBlock anchor; + init_block(&anchor, 8, 0, NULL, 0x61); + LanternRoot anchor_root; + assert(lantern_hash_tree_root_block(&anchor, &anchor_root) == 0); + LanternCheckpoint anchor_cp = make_checkpoint(&anchor_root, anchor.slot); + assert(lantern_fork_choice_set_anchor(&store, &anchor, &anchor_cp, &anchor_cp, &anchor_root) == 0); + + LanternCheckpoint latest_justified = anchor_cp; + LanternCheckpoint latest_finalized = anchor_cp; + latest_justified.slot = 5u; + fill_root(&latest_justified.root, 0xA5); + latest_finalized.slot = 4u; + fill_root(&latest_finalized.root, 0xB4); + + assert(lantern_fork_choice_restore_checkpoints( + &store, + &latest_justified, + &latest_finalized) + != 0); + + const LanternCheckpoint *restored_latest_justified = + lantern_fork_choice_latest_justified(&store); + const LanternCheckpoint *restored_latest_finalized = + lantern_fork_choice_latest_finalized(&store); + + assert(restored_latest_justified); + assert(restored_latest_finalized); + + assert(checkpoints_equal(restored_latest_justified, &anchor_cp)); + assert(checkpoints_equal(restored_latest_finalized, &anchor_cp)); + + LanternRoot head_root; + assert(lantern_fork_choice_current_head(&store, &head_root) == 0); + assert(roots_equal(&head_root, &anchor_root)); + + struct lantern_fork_choice_tree_snapshot snapshot; + memset(&snapshot, 0, sizeof(snapshot)); + assert(lantern_fork_choice_snapshot_tree(&store, &snapshot) == 0); + assert(snapshot.node_count == 1u); + assert(checkpoints_equal(&snapshot.justified, &anchor_cp)); + assert(checkpoints_equal(&snapshot.finalized, &anchor_cp)); + lantern_fork_choice_tree_snapshot_reset(&snapshot); + + lantern_store_reset(&backing_store); + lantern_fork_choice_reset(&store); + reset_block(&anchor); + return 0; +} + static int test_fork_choice_advance_time_schedules_votes(void) { LanternForkChoice store; LanternStore backing_store; @@ -1540,6 +1617,9 @@ int main(void) { if (test_fork_choice_anchor_metadata_survives_checkpoint_restore() != 0) { return 1; } + if (test_fork_choice_restore_checkpoints_rejects_unknown_roots() != 0) { + return 1; + } if (test_fork_choice_advance_time_schedules_votes() != 0) { return 1; } diff --git a/tests/unit/test_genesis_anchor.c b/tests/unit/test_genesis_anchor.c index c110d48..2454360 100644 --- a/tests/unit/test_genesis_anchor.c +++ b/tests/unit/test_genesis_anchor.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -401,6 +402,431 @@ static int test_checkpoint_consumers_use_fork_choice_store(void) return rc; } +static int test_checkpoint_sync_parse_url_scheme_handling(void) +{ + char *host = NULL; + char *base_path = NULL; + uint16_t port = 0; + + if (lantern_client_checkpoint_sync_parse_url( + "http://checkpoint.example:5052/lean/v0/states/finalized", + &host, + &port, + &base_path) + != 0) + { + fprintf(stderr, "failed to parse http checkpoint sync url\n"); + goto fail; + } + if (!host || strcmp(host, "checkpoint.example") != 0) + { + fprintf(stderr, "unexpected checkpoint sync host\n"); + goto fail; + } + if (port != 5052u) + { + fprintf(stderr, "unexpected checkpoint sync port\n"); + goto fail; + } + if (!base_path || strcmp(base_path, "/lean/v0/states/finalized") != 0) + { + fprintf(stderr, "unexpected checkpoint sync base path\n"); + goto fail; + } + + free(host); + host = NULL; + free(base_path); + base_path = NULL; + port = 0; + + if (lantern_client_checkpoint_sync_parse_url( + "https://checkpoint.example/lean/v0/states/finalized", + &host, + &port, + &base_path) + != 0) + { + fprintf(stderr, "failed to parse https checkpoint sync url for downgrade\n"); + goto fail; + } + if (!host || strcmp(host, "checkpoint.example") != 0) + { + fprintf(stderr, "unexpected downgraded checkpoint sync host\n"); + goto fail; + } + if (port != 80u) + { + fprintf(stderr, "unexpected downgraded checkpoint sync port\n"); + goto fail; + } + if (!base_path || strcmp(base_path, "/lean/v0/states/finalized") != 0) + { + fprintf(stderr, "unexpected downgraded checkpoint sync base path\n"); + goto fail; + } + + return 0; + +fail: + free(host); + free(base_path); + return 1; +} + +static int test_pre_anchor_historical_block_is_dropped(void) +{ + struct lantern_client client; + memset(&client, 0, sizeof(client)); + client.node_id = "pre_anchor_floor_regression"; + client.has_state = true; + lantern_state_init(&client.state); + lantern_store_init(&client.store); + + if (pthread_mutex_init(&client.state_lock, NULL) != 0) + { + fprintf(stderr, "failed to initialize state lock for pre-anchor regression\n"); + lantern_store_reset(&client.store); + lantern_state_reset(&client.state); + return 1; + } + client.state_lock_initialized = true; + + if (pthread_mutex_init(&client.pending_lock, NULL) != 0) + { + fprintf(stderr, "failed to initialize pending lock for pre-anchor regression\n"); + pthread_mutex_destroy(&client.state_lock); + client.state_lock_initialized = false; + lantern_store_reset(&client.store); + lantern_state_reset(&client.state); + return 1; + } + client.pending_lock_initialized = true; + + if (lantern_state_generate_genesis(&client.state, UINT64_C(1761717362), 4u) != 0) + { + fprintf(stderr, "failed to generate state for pre-anchor regression\n"); + goto cleanup; + } + + uint8_t pubkeys[4u * LANTERN_VALIDATOR_PUBKEY_SIZE]; + fill_pubkeys(pubkeys, 4u); + if (lantern_state_set_validator_pubkeys(&client.state, pubkeys, 4u) != 0) + { + fprintf(stderr, "failed to set validator pubkeys for pre-anchor regression\n"); + goto cleanup; + } + + client.state.slot = 447u; + client.state.latest_block_header.slot = 443u; + client.state.latest_block_header.proposer_index = 1u; + fill_root(&client.state.latest_block_header.parent_root, 0x55u); + fill_root(&client.state.latest_justified.root, 0x39u); + client.state.latest_justified.slot = 439u; + fill_root(&client.state.latest_finalized.root, 0x34u); + client.state.latest_finalized.slot = 434u; + + if (initialize_fork_choice(&client) != LANTERN_CLIENT_OK) + { + fprintf(stderr, "initialize_fork_choice failed for pre-anchor regression\n"); + goto cleanup; + } + + LanternSignedBlock historical; + lantern_signed_block_with_attestation_init(&historical); + historical.message.block.slot = 440u; + historical.message.block.proposer_index = 0u; + fill_root(&historical.message.block.parent_root, 0x91u); + fill_root(&historical.message.block.state_root, 0x92u); + + LanternRoot historical_root; + if (lantern_hash_tree_root_block(&historical.message.block, &historical_root) != 0) + { + fprintf(stderr, "failed to hash historical block for pre-anchor regression\n"); + lantern_signed_block_with_attestation_reset(&historical); + goto cleanup; + } + + bool imported = lantern_client_import_block( + &client, + &historical, + &historical_root, + &(const struct lantern_log_metadata){.validator = client.node_id}, + 0, + true, + NULL, + 0); + lantern_signed_block_with_attestation_reset(&historical); + + if (imported) + { + fprintf(stderr, "pre-anchor historical block should not import\n"); + goto cleanup; + } + if (client.pending_blocks.length != 0) + { + fprintf(stderr, "pre-anchor historical block should not be queued pending\n"); + goto cleanup; + } + + lantern_fork_choice_reset(&client.fork_choice); + lantern_store_reset(&client.store); + lantern_state_reset(&client.state); + pthread_mutex_destroy(&client.pending_lock); + pthread_mutex_destroy(&client.state_lock); + return 0; + +cleanup: + lantern_fork_choice_reset(&client.fork_choice); + lantern_store_reset(&client.store); + lantern_state_reset(&client.state); + if (client.pending_lock_initialized) + { + pthread_mutex_destroy(&client.pending_lock); + client.pending_lock_initialized = false; + } + if (client.state_lock_initialized) + { + pthread_mutex_destroy(&client.state_lock); + client.state_lock_initialized = false; + } + return 1; +} + +static int test_checkpoint_sync_anchor_alias_restores(void) +{ + struct lantern_client client; + char dir_template[] = "/tmp/lantern_checkpoint_anchor_restoreXXXXXX"; + char *data_dir = NULL; + LanternCheckpoint remote_justified = {0}; + LanternCheckpoint remote_finalized = {0}; + LanternRoot canonical_state_root = {0}; + LanternRoot expected_anchor_root = {0}; + LanternRoot actual_head = {0}; + int rc = 1; + + memset(&client, 0, sizeof(client)); + client.node_id = "checkpoint_anchor_restore"; + client.has_state = true; + lantern_state_init(&client.state); + lantern_store_init(&client.store); + + if (lantern_state_generate_genesis(&client.state, UINT64_C(1761717362), 5u) != 0) + { + fprintf(stderr, "failed to generate state for checkpoint anchor restore regression\n"); + goto cleanup; + } + + uint8_t pubkeys[5u * LANTERN_VALIDATOR_PUBKEY_SIZE]; + fill_pubkeys(pubkeys, 5u); + if (lantern_state_set_validator_pubkeys(&client.state, pubkeys, 5u) != 0) + { + fprintf(stderr, "failed to set validator pubkeys for checkpoint anchor restore regression\n"); + goto cleanup; + } + + client.state.slot = 4612u; + client.state.latest_block_header.slot = 4612u; + client.state.latest_block_header.proposer_index = 2u; + fill_root(&client.state.latest_block_header.parent_root, 0xF1u); + + fill_root(&remote_justified.root, 0xE5u); + remote_justified.slot = 4597u; + fill_root(&remote_finalized.root, 0x28u); + remote_finalized.slot = 4592u; + if (roots_equal(&remote_justified.root, &remote_finalized.root)) + { + remote_finalized.root.bytes[0] ^= 0x01u; + } + client.state.latest_justified = remote_justified; + client.state.latest_finalized = remote_finalized; + + char *temp_dir = mkdtemp(dir_template); + if (!temp_dir) + { + fprintf(stderr, "failed to create temp data dir for checkpoint anchor restore regression\n"); + goto cleanup; + } + data_dir = strdup(temp_dir); + if (!data_dir) + { + fprintf(stderr, "failed to duplicate temp data dir for checkpoint anchor restore regression\n"); + goto cleanup; + } + client.data_dir = data_dir; + + if (lantern_hash_tree_root_state(&client.state, &canonical_state_root) != 0) + { + fprintf(stderr, "failed to hash checkpoint anchor restore regression state\n"); + goto cleanup; + } + + LanternBlockHeader expected_anchor_header = client.state.latest_block_header; + expected_anchor_header.state_root = canonical_state_root; + if (lantern_hash_tree_root_block_header(&expected_anchor_header, &expected_anchor_root) != 0) + { + fprintf(stderr, "failed to hash checkpoint anchor restore regression anchor header\n"); + goto cleanup; + } + + if (initialize_fork_choice(&client) != LANTERN_CLIENT_OK) + { + fprintf(stderr, "initialize_fork_choice failed for checkpoint anchor restore regression\n"); + goto cleanup; + } + + if (!roots_equal(&client.state.latest_justified.root, &remote_justified.root) + || !roots_equal(&client.state.latest_finalized.root, &remote_finalized.root)) + { + fprintf(stderr, "checkpoint anchor restore regression unexpectedly rewrote client state checkpoints\n"); + goto cleanup; + } + + if (restore_persisted_blocks(&client) != LANTERN_CLIENT_OK) + { + fprintf(stderr, "restore_persisted_blocks failed for checkpoint anchor restore regression\n"); + goto cleanup; + } + + if (lantern_fork_choice_current_head(&client.fork_choice, &actual_head) != 0) + { + fprintf(stderr, "failed to read fork choice head for checkpoint anchor restore regression\n"); + goto cleanup; + } + if (!roots_equal(&actual_head, &expected_anchor_root)) + { + fprintf(stderr, "checkpoint anchor restore regression head mismatch\n"); + goto cleanup; + } + + const LanternCheckpoint *store_justified = + lantern_fork_choice_latest_justified(&client.fork_choice); + const LanternCheckpoint *store_finalized = + lantern_fork_choice_latest_finalized(&client.fork_choice); + if (!store_justified || !store_finalized) + { + fprintf(stderr, "missing fork-choice checkpoints for checkpoint anchor restore regression\n"); + goto cleanup; + } + if (store_justified->slot != remote_justified.slot + || !roots_equal(&store_justified->root, &expected_anchor_root)) + { + fprintf(stderr, "checkpoint anchor restore regression justified checkpoint mismatch\n"); + goto cleanup; + } + if (store_finalized->slot != remote_finalized.slot + || !roots_equal(&store_finalized->root, &expected_anchor_root)) + { + fprintf(stderr, "checkpoint anchor restore regression finalized checkpoint mismatch\n"); + goto cleanup; + } + + LanternBlock child_block; + memset(&child_block, 0, sizeof(child_block)); + child_block.slot = client.state.slot + 1u; + if (lantern_proposer_for_slot( + child_block.slot, + client.state.config.num_validators, + &child_block.proposer_index) + != 0) + { + fprintf(stderr, "failed to compute child proposer for checkpoint anchor restore regression\n"); + goto cleanup; + } + child_block.parent_root = expected_anchor_root; + lantern_block_body_init(&child_block.body); + LanternRoot child_root = {0}; + if (lantern_hash_tree_root_block(&child_block, &child_root) != 0) + { + fprintf(stderr, "failed to hash child block for checkpoint anchor restore regression\n"); + lantern_block_body_reset(&child_block.body); + goto cleanup; + } + if (lantern_fork_choice_add_block_with_state( + &client.fork_choice, + &child_block, + NULL, + &remote_justified, + &remote_finalized, + &child_root, + NULL) + != 0) + { + fprintf(stderr, "fork choice rejected first post-anchor block for checkpoint anchor restore regression\n"); + lantern_block_body_reset(&child_block.body); + goto cleanup; + } + lantern_block_body_reset(&child_block.body); + + const LanternState *cached_anchor_state = + lantern_fork_choice_block_state(&client.fork_choice, &expected_anchor_root); + if (!cached_anchor_state) + { + fprintf(stderr, "missing cached anchor state for checkpoint anchor restore regression\n"); + goto cleanup; + } + + LanternState cached_anchor_clone; + lantern_state_init(&cached_anchor_clone); + if (lantern_state_clone(cached_anchor_state, &cached_anchor_clone) != 0) + { + fprintf(stderr, "failed to clone cached anchor state for checkpoint anchor restore regression\n"); + lantern_state_reset(&cached_anchor_clone); + goto cleanup; + } + if (lantern_state_process_slot(&cached_anchor_clone) != 0) + { + fprintf(stderr, "failed to process cached anchor state slot for checkpoint anchor restore regression\n"); + lantern_state_reset(&cached_anchor_clone); + goto cleanup; + } + LanternRoot cached_anchor_header_root = {0}; + if (lantern_hash_tree_root_block_header( + &cached_anchor_clone.latest_block_header, + &cached_anchor_header_root) + != 0) + { + fprintf(stderr, "failed to hash cached anchor header for checkpoint anchor restore regression\n"); + lantern_state_reset(&cached_anchor_clone); + goto cleanup; + } + lantern_state_reset(&cached_anchor_clone); + if (!roots_equal(&cached_anchor_header_root, &expected_anchor_root)) + { + fprintf(stderr, "checkpoint anchor restore regression cached anchor state drifted\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + if (data_dir) + { + cleanup_storage_root_file(data_dir, "states", &expected_anchor_root, "ssz"); + cleanup_storage_root_file(data_dir, "states", &expected_anchor_root, "meta"); + cleanup_storage_root_file(data_dir, "blocks", &expected_anchor_root, "ssz"); + char states_dir[PATH_MAX]; + char blocks_dir[PATH_MAX]; + int states_written = snprintf(states_dir, sizeof(states_dir), "%s/states", data_dir); + int blocks_written = snprintf(blocks_dir, sizeof(blocks_dir), "%s/blocks", data_dir); + if (states_written > 0 && (size_t)states_written < sizeof(states_dir)) + { + cleanup_dir(states_dir); + } + if (blocks_written > 0 && (size_t)blocks_written < sizeof(blocks_dir)) + { + cleanup_dir(blocks_dir); + } + client.data_dir = NULL; + cleanup_dir(data_dir); + free(data_dir); + } + lantern_fork_choice_reset(&client.fork_choice); + lantern_store_reset(&client.store); + lantern_state_reset(&client.state); + return rc; +} + int main(void) { struct lantern_client client; @@ -565,9 +991,9 @@ int main(void) } /* - * After initialize_fork_choice the store checkpoints match the anchor - * (not the persisted state checkpoints). Real persisted checkpoints are - * synced later by restore_persisted_blocks → restore_checkpoints. + * After initialize_fork_choice the store checkpoints keep the state's + * justified/finalized slots but point both roots at the materialized + * anchor block. */ const LanternCheckpoint *store_justified = lantern_fork_choice_latest_justified(&client.fork_choice); @@ -580,24 +1006,24 @@ int main(void) lantern_fork_choice_reset(&client.fork_choice); return 1; } - if (store_justified->slot != restart_anchor_header.slot + if (store_justified->slot != client.state.latest_justified.slot || !roots_equal(&store_justified->root, &expected_restart_anchor_root)) { fprintf(stderr, - "justified checkpoint should match anchor after init " + "justified checkpoint should keep its slot and use the anchor root " "(got slot=%" PRIu64 " expected slot=%" PRIu64 ")\n", - store_justified->slot, (uint64_t)restart_anchor_header.slot); + store_justified->slot, client.state.latest_justified.slot); lantern_state_reset(&client.state); lantern_fork_choice_reset(&client.fork_choice); return 1; } - if (store_finalized->slot != restart_anchor_header.slot + if (store_finalized->slot != client.state.latest_finalized.slot || !roots_equal(&store_finalized->root, &expected_restart_anchor_root)) { fprintf(stderr, - "finalized checkpoint should match anchor after init " + "finalized checkpoint should keep its slot and use the anchor root " "(got slot=%" PRIu64 " expected slot=%" PRIu64 ")\n", - store_finalized->slot, (uint64_t)restart_anchor_header.slot); + store_finalized->slot, client.state.latest_finalized.slot); lantern_state_reset(&client.state); lantern_fork_choice_reset(&client.fork_choice); return 1; @@ -609,6 +1035,24 @@ int main(void) lantern_fork_choice_reset(&client.fork_choice); return 1; } + if (test_checkpoint_sync_parse_url_scheme_handling() != 0) + { + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + if (test_pre_anchor_historical_block_is_dropped() != 0) + { + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + if (test_checkpoint_sync_anchor_alias_restores() != 0) + { + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } lantern_state_reset(&client.state); lantern_fork_choice_reset(&client.fork_choice);