From a4de15850c89a91be18f702d13e94523eb6e047a Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:55:23 +1000 Subject: [PATCH 1/4] Refactor attestation collection, prune gossip --- include/lantern/consensus/store.h | 3 + src/consensus/state.c | 203 +++++++----- src/consensus/store.c | 18 + src/core/client.c | 20 ++ src/core/client_validator.c | 77 ++++- tests/integration/consensus_fixture_runner.c | 14 +- tests/unit/test_client_vote.c | 330 ++++++++++++++++++- tests/unit/test_state.c | 71 +++- 8 files changed, 630 insertions(+), 106 deletions(-) diff --git a/include/lantern/consensus/store.h b/include/lantern/consensus/store.h index a4c8370..0ab33b9 100644 --- a/include/lantern/consensus/store.h +++ b/include/lantern/consensus/store.h @@ -110,6 +110,9 @@ int lantern_store_get_gossip_signature( const LanternStore *store, const LanternSignatureKey *key, LanternSignature *out_signature); +int lantern_store_remove_gossip_signature( + LanternStore *store, + const LanternSignatureKey *key); int lantern_store_add_new_aggregated_payload( LanternStore *store, const LanternRoot *data_root, diff --git a/src/consensus/state.c b/src/consensus/state.c index 112571e..23a5cee 100644 --- a/src/consensus/state.c +++ b/src/consensus/state.c @@ -60,9 +60,20 @@ static int lantern_state_set_justified_slot_bit(LanternState *state, uint64_t sl bool lantern_state_slot_in_justified_window(const LanternState *state, uint64_t slot); int lantern_state_get_justified_slot_bit(const LanternState *state, uint64_t slot, bool *out_value); static bool attestation_list_contains_validator(const LanternAttestations *list, uint64_t validator_id); -static bool store_has_known_aggregated_payload_for_vote( - const LanternStore *store, - const LanternVote *vote); +struct lantern_block_attestation_candidate { + LanternVote vote; + LanternRoot data_root; + bool has_vote; +}; +static void lantern_vote_from_attestation_data( + LanternVote *out_vote, + uint64_t validator_id, + const LanternAttestationData *data); +static void lantern_resolve_block_attestation_signature( + const LanternStore *vote_store, + const LanternStore *proof_store, + const struct lantern_block_attestation_candidate *candidate, + LanternSignature *out_signature); static int collect_attestations_for_checkpoint( const LanternState *state, const LanternStore *vote_store, @@ -103,7 +114,6 @@ static int lantern_state_cache_proposer_attestation( const LanternVote *vote = &proposer_attestation->data; if (state->config.num_validators == 0 || vote->validator_id >= state->config.num_validators - || vote->target.slot <= vote->source.slot || signature_is_zero(&proposer_attestation->signature)) { return 0; } @@ -357,38 +367,58 @@ static bool attestation_list_contains_validator(const LanternAttestations *list, return false; } -static bool store_has_known_aggregated_payload_for_vote( - const LanternStore *store, - const LanternVote *vote) { - if (!store || !vote) { - return false; +static void lantern_vote_from_attestation_data( + LanternVote *out_vote, + uint64_t validator_id, + const LanternAttestationData *data) { + if (!out_vote || !data) { + return; } - const struct lantern_aggregated_payload_pool *payloads = &store->known_aggregated_payloads; - if (!payloads->entries || payloads->length == 0) { - return false; + memset(out_vote, 0, sizeof(*out_vote)); + out_vote->validator_id = validator_id; + out_vote->slot = data->slot; + out_vote->head = data->head; + out_vote->target = data->target; + out_vote->source = data->source; +} + +static void lantern_resolve_block_attestation_signature( + const LanternStore *vote_store, + const LanternStore *proof_store, + const struct lantern_block_attestation_candidate *candidate, + LanternSignature *out_signature) { + if (!out_signature) { + return; } - LanternRoot data_root; - if (lantern_hash_tree_root_attestation_data(&vote->data, &data_root) != 0) { - return false; + memset(out_signature, 0, sizeof(*out_signature)); + if (!candidate) { + return; } - for (size_t i = 0; i < payloads->length; ++i) { - const struct lantern_aggregated_payload_entry *entry = &payloads->entries[i]; - if (memcmp(entry->data_root.bytes, data_root.bytes, LANTERN_ROOT_SIZE) != 0) { - continue; - } - if (vote->validator_id >= entry->proof.participants.bit_length - || !entry->proof.participants.bytes) { - continue; - } - if (lantern_bitlist_get(&entry->proof.participants, (size_t)vote->validator_id)) { - return true; + size_t validator_index = (size_t)candidate->vote.validator_id; + if (vote_store + && vote_store->validator_votes + && validator_index < vote_store->validator_votes_len) { + const struct lantern_vote_record *record = &vote_store->validator_votes[validator_index]; + if (record->has_vote + && record->has_signature + && memcmp(&record->vote, &candidate->vote, sizeof(candidate->vote)) == 0) { + *out_signature = record->signature; + return; } } - return false; + if (!proof_store) { + return; + } + + LanternSignatureKey key = { + .validator_index = candidate->vote.validator_id, + .data_root = candidate->data_root, + }; + (void)lantern_store_get_gossip_signature(proof_store, &key, out_signature); } static int collect_attestations_for_checkpoint( @@ -401,45 +431,93 @@ static int collect_attestations_for_checkpoint( if (!state || !vote_store || !proof_store || !checkpoint || !out_attestations || !out_signatures) { return -1; } - if (!vote_store->validator_votes || vote_store->validator_votes_len == 0) { + if (state->config.num_validators == 0 || state->config.num_validators > SIZE_MAX) { return 0; } - for (size_t i = 0; i < vote_store->validator_votes_len; ++i) { - const struct lantern_vote_record *record = &vote_store->validator_votes[i]; - if (!record->has_vote) { + const struct lantern_aggregated_payload_pool *payloads = &proof_store->known_aggregated_payloads; + if (!payloads->entries || payloads->length == 0) { + return 0; + } + + size_t validator_count = (size_t)state->config.num_validators; + struct lantern_block_attestation_candidate *candidates = + calloc(validator_count, sizeof(*candidates)); + if (!candidates) { + return -1; + } + + for (size_t payload_index = 0; payload_index < payloads->length; ++payload_index) { + const struct lantern_aggregated_payload_entry *entry = &payloads->entries[payload_index]; + const struct lantern_bitlist *participants = &entry->proof.participants; + LanternAttestationData data; + memset(&data, 0, sizeof(data)); + + if (participants->bit_length == 0 || !participants->bytes) { continue; } - if (!lantern_checkpoint_equal(&record->vote.source, checkpoint)) { + if (lantern_store_get_attestation_data(proof_store, &entry->data_root, &data) != 0) { + continue; + } + + size_t limit = participants->bit_length; + if (limit > validator_count) { + limit = validator_count; + } + for (size_t validator_index = 0; validator_index < limit; ++validator_index) { + if (!lantern_bitlist_get(participants, validator_index)) { + continue; + } + if (candidates[validator_index].has_vote + && candidates[validator_index].vote.slot >= data.slot) { + continue; + } + lantern_vote_from_attestation_data( + &candidates[validator_index].vote, + (uint64_t)validator_index, + &data); + candidates[validator_index].data_root = entry->data_root; + candidates[validator_index].has_vote = true; + } + } + + for (size_t validator_index = 0; validator_index < validator_count; ++validator_index) { + const struct lantern_block_attestation_candidate *candidate = &candidates[validator_index]; + LanternSignature signature; + + if (!candidate->has_vote) { continue; } - if (attestation_list_contains_validator(out_attestations, record->vote.validator_id)) { + if (!lantern_checkpoint_equal(&candidate->vote.source, checkpoint)) { continue; } - if (!store_has_known_aggregated_payload_for_vote(proof_store, &record->vote)) { + if (attestation_list_contains_validator(out_attestations, candidate->vote.validator_id)) { continue; } if (out_attestations->length >= LANTERN_MAX_ATTESTATIONS) { (void)lantern_attestations_resize(out_attestations, 0); (void)lantern_signature_list_resize(out_signatures, 0); + free(candidates); return -1; } - LanternVote vote = record->vote; - if (lantern_attestations_append(out_attestations, &vote) != 0) { + if (lantern_attestations_append(out_attestations, &candidate->vote) != 0) { (void)lantern_attestations_resize(out_attestations, 0); (void)lantern_signature_list_resize(out_signatures, 0); + free(candidates); return -1; } - LanternSignature signature; - memset(&signature, 0, sizeof(signature)); - if (record->has_signature) { - signature = record->signature; - } + lantern_resolve_block_attestation_signature( + vote_store, + proof_store, + candidate, + &signature); if (lantern_signature_list_append(out_signatures, &signature) != 0) { (void)lantern_attestations_resize(out_attestations, 0); (void)lantern_signature_list_resize(out_signatures, 0); + free(candidates); return -1; } } + free(candidates); return 0; } @@ -2488,10 +2566,6 @@ int lantern_state_compute_vote_checkpoints( } LanternRoot target_root = head_root; uint64_t target_slot = head_slot; - uint64_t source_slot = source_checkpoint.slot; - bool candidate_valid = false; - LanternRoot candidate_root; - uint64_t candidate_slot = 0; if (trace_finalization) { format_root_hex(&head_root, head_hex, sizeof(head_hex)); lantern_log_debug( @@ -2501,11 +2575,6 @@ int lantern_state_compute_vote_checkpoints( head_slot, head_hex[0] ? head_hex : "0x0"); } - if (head_slot > source_slot && lantern_slot_is_justifiable(head_slot, finalized_checkpoint.slot)) { - candidate_valid = true; - candidate_root = head_root; - candidate_slot = head_slot; - } uint64_t safe_slot = head_slot; bool has_safe = false; @@ -2562,11 +2631,6 @@ int lantern_state_compute_vote_checkpoints( } target_root = parent_root; target_slot = parent_slot; - if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { - candidate_valid = true; - candidate_root = target_root; - candidate_slot = target_slot; - } } } @@ -2623,11 +2687,6 @@ int lantern_state_compute_vote_checkpoints( } target_root = parent_root; target_slot = parent_slot; - if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { - candidate_valid = true; - candidate_root = target_root; - candidate_slot = target_slot; - } } if (trace_finalization && !justifiable_slot_found) { lantern_log_debug( @@ -2638,30 +2697,6 @@ int lantern_state_compute_vote_checkpoints( finalized_checkpoint.slot, target_slot); } - if (target_slot > source_slot && lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { - candidate_valid = true; - candidate_root = target_root; - candidate_slot = target_slot; - } - if (target_slot <= source_slot && candidate_valid) { - if (trace_finalization) { - format_root_hex(&target_root, target_hex, sizeof(target_hex)); - char candidate_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - format_root_hex(&candidate_root, candidate_hex, sizeof(candidate_hex)); - lantern_log_debug( - "state", - &trace_meta, - "finalization trace checkpoints promote target slot=%" PRIu64 "->%" PRIu64 - " root=%s promoted=%s source=%" PRIu64, - target_slot, - candidate_slot, - target_hex[0] ? target_hex : "0x0", - candidate_hex[0] ? candidate_hex : "0x0", - source_slot); - } - target_root = candidate_root; - target_slot = candidate_slot; - } out_head->root = head_root; out_head->slot = head_slot; diff --git a/src/consensus/store.c b/src/consensus/store.c index 36b55bf..afe9dfb 100644 --- a/src/consensus/store.c +++ b/src/consensus/store.c @@ -626,6 +626,24 @@ int lantern_store_get_gossip_signature( return gossip_signature_map_get(&store->gossip_signatures, key, out_signature); } +int lantern_store_remove_gossip_signature( + LanternStore *store, + const LanternSignatureKey *key) { + if (!store || !key) { + return -1; + } + + struct lantern_gossip_signature_map *map = &store->gossip_signatures; + for (size_t i = 0; i < map->length; ++i) { + if (!signature_key_equals(&map->entries[i].key, key)) { + continue; + } + gossip_signature_map_remove_index(map, i); + return 0; + } + return -1; +} + static int lantern_store_add_aggregated_payload( LanternStore *store, struct lantern_aggregated_payload_pool *pool, diff --git a/src/core/client.c b/src/core/client.c index cb53e17..ccdbcb6 100644 --- a/src/core/client.c +++ b/src/core/client.c @@ -236,7 +236,27 @@ int lantern_client_skip_fork_choice_intervals_locked( if (target_interval < client->fork_choice.time_intervals) { return -1; } + uint64_t previous_intervals = client->fork_choice.time_intervals; client->fork_choice.time_intervals = target_interval; + uint64_t intervals_per_slot = client->fork_choice.intervals_per_slot; + if (intervals_per_slot == 0u || target_interval == previous_intervals) { + return 0; + } + for (uint64_t step = previous_intervals + 1u;; ++step) { + uint64_t phase = step % intervals_per_slot; + if (phase == 3u) { + if (lantern_fork_choice_update_safe_target(&client->fork_choice) != 0) { + return -1; + } + } else if (phase == 4u) { + if (lantern_fork_choice_accept_new_votes(&client->fork_choice) != 0) { + return -1; + } + } + if (step == target_interval) { + break; + } + } return 0; } diff --git a/src/core/client_validator.c b/src/core/client_validator.c index ff6ce3f..a32830e 100644 --- a/src/core/client_validator.c +++ b/src/core/client_validator.c @@ -2184,9 +2184,8 @@ int validator_publish_attestations(struct lantern_client *client, uint64_t slot) return result; } -static lantern_client_error collect_subnet_votes_for_slot( +static lantern_client_error collect_subnet_votes( struct lantern_client *client, - uint64_t slot, size_t subnet_id, LanternAttestations *out_attestations, LanternSignatureList *out_signatures) { @@ -2218,9 +2217,6 @@ static lantern_client_error collect_subnet_votes_for_slot( if (lantern_store_get_attestation_data(&client->store, &entry->key.data_root, &data) != 0) { continue; } - if (data.slot != slot) { - continue; - } size_t vote_subnet = 0; if (lantern_validator_index_compute_subnet_id( entry->key.validator_index, @@ -2251,11 +2247,67 @@ static lantern_client_error collect_subnet_votes_for_slot( return result; } +static lantern_client_error prune_successfully_aggregated_gossip_signatures( + struct lantern_client *client, + const LanternAggregatedAttestations *aggregated_attestations, + const LanternAttestationSignatures *aggregated_signatures, + size_t count) +{ + if (!client || !aggregated_attestations || !aggregated_signatures) { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + if (count == 0) { + return LANTERN_CLIENT_OK; + } + if (!aggregated_attestations->data || !aggregated_signatures->data) { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) { + return LANTERN_CLIENT_ERR_RUNTIME; + } + + lantern_client_error result = LANTERN_CLIENT_OK; + for (size_t i = 0; i < count; ++i) { + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&aggregated_attestations->data[i].data, &data_root) != 0) { + result = LANTERN_CLIENT_ERR_VALIDATOR; + break; + } + + const struct lantern_bitlist *participants = &aggregated_signatures->data[i].participants; + if (participants->bit_length == 0 || !participants->bytes) { + result = LANTERN_CLIENT_ERR_RUNTIME; + break; + } + + size_t limit = participants->bit_length; + if (limit > LANTERN_VALIDATOR_REGISTRY_LIMIT) { + limit = LANTERN_VALIDATOR_REGISTRY_LIMIT; + } + for (size_t validator = 0; validator < limit; ++validator) { + if (!lantern_bitlist_get(participants, validator)) { + continue; + } + LanternSignatureKey key = { + .validator_index = (LanternValidatorIndex)validator, + .data_root = data_root, + }; + (void)lantern_store_remove_gossip_signature(&client->store, &key); + } + } + + lantern_client_unlock_state(client, state_locked); + return result; +} + static int validator_publish_aggregated_attestations(struct lantern_client *client, uint64_t slot) { if (!client || !client->assigned_validators || !client->assigned_validators->enr.is_aggregator) { return LANTERN_CLIENT_ERR_RUNTIME; } + (void)slot; LanternAttestations attestations; LanternSignatureList signatures; @@ -2267,9 +2319,8 @@ static int validator_publish_aggregated_attestations(struct lantern_client *clie lantern_aggregated_attestations_init(&aggregated_attestations); lantern_attestation_signatures_init(&aggregated_signatures); - lantern_client_error result = collect_subnet_votes_for_slot( + lantern_client_error result = collect_subnet_votes( client, - slot, client->gossip.attestation_subnet_id, &attestations, &signatures); @@ -2314,6 +2365,7 @@ static int validator_publish_aggregated_attestations(struct lantern_client *clie if (aggregated_signatures.length < count) { count = aggregated_signatures.length; } + size_t successful_aggregations = 0u; for (size_t i = 0; i < count; ++i) { LanternSignedAggregatedAttestation signed_attestation; lantern_signed_aggregated_attestation_init(&signed_attestation); @@ -2344,8 +2396,19 @@ static int validator_publish_aggregated_attestations(struct lantern_client *clie } lantern_client_unlock_state(client, locked); } + successful_aggregations += 1u; lantern_signed_aggregated_attestation_reset(&signed_attestation); } + if (successful_aggregations > 0u) { + lantern_client_error prune_rc = prune_successfully_aggregated_gossip_signatures( + client, + &aggregated_attestations, + &aggregated_signatures, + successful_aggregations); + if (prune_rc != LANTERN_CLIENT_OK && result == LANTERN_CLIENT_OK) { + result = prune_rc; + } + } cleanup: lantern_aggregated_attestations_reset(&aggregated_attestations); diff --git a/tests/integration/consensus_fixture_runner.c b/tests/integration/consensus_fixture_runner.c index 713d374..d96ab7e 100644 --- a/tests/integration/consensus_fixture_runner.c +++ b/tests/integration/consensus_fixture_runner.c @@ -1874,17 +1874,21 @@ static int run_fork_choice_fixture(const char *path) { return -1; } if (target_cp.slot != expected_slot) { - /* NOTE: Lantern's attestation target selection uses candidate promotion - * which differs from leanSpec's conservative walk-back approach. - * This difference is intentional for Zeam interop. Log warning only. */ fprintf( stderr, - "note: attestation target differs from leanSpec in %s (step %d): expected %" PRIu64 " got %" PRIu64 "\n", + "attestation target slot mismatch in %s (step %d): expected %" PRIu64 " got %" PRIu64 "\n", path, i, expected_slot, target_cp.slot); - /* Continue instead of failing */ + reset_block(&signed_block.message); + reset_block(&anchor_block); + lantern_fork_choice_reset(&store); + lantern_state_reset(&state); + lantern_fixture_document_reset(&doc); + stored_state_entries_reset(&stored_states, &stored_states_count, &stored_states_cap); + hash_mapping_reset(&hash_mapping, &hash_mapping_count, &hash_mapping_cap); + return -1; } } diff --git a/tests/unit/test_client_vote.c b/tests/unit/test_client_vote.c index 0ee0272..59780ea 100644 --- a/tests/unit/test_client_vote.c +++ b/tests/unit/test_client_vote.c @@ -4,6 +4,7 @@ #include #include +#include "lantern/consensus/duties.h" #include "client_test_helpers.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" @@ -40,6 +41,9 @@ int lantern_client_chain_service_tick_to( bool has_proposal, uint64_t *out_skipped_to_interval, uint64_t *out_ticked_intervals); +int lantern_client_skip_fork_choice_intervals_locked( + struct lantern_client *client, + uint64_t target_interval); int lantern_client_advance_fork_choice_time_locked( struct lantern_client *client, uint64_t now_milliseconds, @@ -1055,6 +1059,88 @@ static int test_chain_service_tick_to_skips_stale_intervals(void) { return rc; } +static int test_skip_fork_choice_intervals_replays_interval_side_effects(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + int rc = 1; + + if (client_test_setup_vote_validation_client(&client, "skip_interval_effects", &pub, &secret, &anchor_root, &child_root) != 0) { + return 1; + } + + if (client.fork_choice.time_intervals != 0u) { + fprintf(stderr, "unexpected initial interval %" PRIu64 " for skip replay test\n", client.fork_choice.time_intervals); + goto cleanup; + } + if (!client.fork_choice.new_votes || !client.fork_choice.known_votes) { + fprintf(stderr, "fork choice vote tables unavailable for skip replay test\n"); + goto cleanup; + } + client.fork_choice.new_aggregated_payloads = NULL; + client.fork_choice.known_aggregated_payloads = NULL; + client.fork_choice.attestation_data_by_root = NULL; + + LanternSignedVote vote; + memset(&vote, 0, sizeof(vote)); + if (make_signed_vote_for_validator(&client, secret, 0u, &anchor_root, &child_root, &vote) != 0) { + fprintf(stderr, "failed to build signed vote for skip replay test\n"); + goto cleanup; + } + if (lantern_fork_choice_add_vote(&client.fork_choice, &vote, false) != 0) { + fprintf(stderr, "failed to stage fork choice vote for skip replay test\n"); + goto cleanup; + } + if (!client.fork_choice.new_votes[0].has_checkpoint || client.fork_choice.known_votes[0].has_checkpoint) { + fprintf(stderr, "skip replay test did not stage vote in new_votes as expected\n"); + goto cleanup; + } + + if (lantern_client_skip_fork_choice_intervals_locked(&client, 4u) != 0) { + fprintf(stderr, "skip replay helper failed to advance intervals\n"); + goto cleanup; + } + + if (client.fork_choice.time_intervals != 4u) { + fprintf(stderr, "skip replay helper ended at wrong interval: %" PRIu64 "\n", client.fork_choice.time_intervals); + goto cleanup; + } + if (!client.fork_choice.has_safe_target + || memcmp(client.fork_choice.safe_target.bytes, child_root.bytes, LANTERN_ROOT_SIZE) != 0) { + fprintf(stderr, "skip replay helper did not update safe target at skipped interval 3\n"); + goto cleanup; + } + if (!client.fork_choice.known_votes[0].has_checkpoint + || client.fork_choice.known_votes[0].slot != vote.data.slot + || memcmp( + client.fork_choice.known_votes[0].checkpoint.root.bytes, + child_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "skip replay helper did not accept votes at skipped interval 4\n"); + goto cleanup; + } + if (client.fork_choice.new_votes[0].has_checkpoint) { + fprintf(stderr, "skip replay helper left pending vote entries after skipped interval 4\n"); + goto cleanup; + } + + LanternRoot head; + if (lantern_fork_choice_current_head(&client.fork_choice, &head) != 0 + || memcmp(head.bytes, child_root.bytes, LANTERN_ROOT_SIZE) != 0) { + fprintf(stderr, "skip replay helper did not recompute head from skipped interval 4\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + static int test_safe_target_uses_attached_aggregated_payloads(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -1733,7 +1819,194 @@ static int test_validator_build_skips_raw_signatures_without_cached_proof(void) return rc; } -static int test_publish_aggregated_attestations_filters_cross_subnet_votes(void) { +static int test_block_build_keeps_known_payload_after_newer_raw_vote(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + LanternRoot parent_root; + LanternRoot proof_root; + LanternRoot raw_root; + LanternSignatureKey proof_key; + LanternSignedVote proof_vote; + LanternSignedVote raw_vote; + LanternAggregatedSignatureProof cached_proof; + LanternAttestations collected; + LanternSignatureList collected_signatures; + LanternAggregatedAttestations aggregated_attestations; + LanternAttestationSignatures aggregated_signatures; + int rc = 1; + + memset(&parent_root, 0, sizeof(parent_root)); + memset(&proof_root, 0, sizeof(proof_root)); + memset(&raw_root, 0, sizeof(raw_root)); + memset(&proof_key, 0, sizeof(proof_key)); + memset(&proof_vote, 0, sizeof(proof_vote)); + memset(&raw_vote, 0, sizeof(raw_vote)); + lantern_aggregated_signature_proof_init(&cached_proof); + lantern_attestations_init(&collected); + lantern_signature_list_init(&collected_signatures); + lantern_aggregated_attestations_init(&aggregated_attestations); + lantern_attestation_signatures_init(&aggregated_signatures); + + if (client_test_setup_vote_validation_client_with_validator_count( + &client, + "vote_shadowed_known_payload", + 2u, + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + return 1; + } + + uint64_t block_slot = client.state.slot + 1u; + uint64_t proposer_index = 0u; + if (lantern_proposer_for_slot(block_slot, client.state.config.num_validators, &proposer_index) != 0) { + fprintf(stderr, "failed to resolve proposer for block-build shadow test\n"); + goto cleanup; + } + + uint64_t validator_id = proposer_index == 0u ? 1u : 0u; + if (make_signed_vote_for_validator( + &client, + secret, + validator_id, + &anchor_root, + &child_root, + &proof_vote) + != 0) { + fprintf(stderr, "failed to build proof-backed vote for block-build shadow test\n"); + goto cleanup; + } + if (lantern_hash_tree_root_attestation_data(&proof_vote.data.data, &proof_root) != 0) { + fprintf(stderr, "failed to hash proof-backed vote for block-build shadow test\n"); + goto cleanup; + } + + proof_key.validator_index = validator_id; + proof_key.data_root = proof_root; + if (lantern_client_set_gossip_signature( + &client, + &proof_key, + &proof_vote.data.data, + &proof_vote.signature, + proof_vote.data.target.slot) + != 0) { + fprintf(stderr, "failed to seed gossip signature for proof-backed vote\n"); + goto cleanup; + } + if (test_make_dummy_proof(&cached_proof, validator_id, 0x5Au) != 0) { + fprintf(stderr, "failed to build cached proof for block-build shadow test\n"); + goto cleanup; + } + if (lantern_client_add_known_aggregated_payload( + &client, + &proof_root, + &proof_vote.data.data, + &cached_proof, + proof_vote.data.target.slot) + != 0) { + fprintf(stderr, "failed to seed known payload for block-build shadow test\n"); + goto cleanup; + } + if (lantern_store_set_signed_validator_vote(&client.store, (size_t)validator_id, &proof_vote) != 0) { + fprintf(stderr, "failed to seed initial validator vote for block-build shadow test\n"); + goto cleanup; + } + + raw_vote = proof_vote; + raw_vote.data.slot = proof_vote.data.slot + 1u; + raw_vote.data.head.slot = raw_vote.data.slot; + raw_vote.data.target.slot = raw_vote.data.slot; + client_test_fill_root(&raw_vote.data.head.root, 0xD1u); + raw_vote.data.target.root = raw_vote.data.head.root; + if (client_test_sign_vote_with_secret(&raw_vote, secret) != 0) { + fprintf(stderr, "failed to sign newer raw vote for block-build shadow test\n"); + goto cleanup; + } + if (lantern_hash_tree_root_attestation_data(&raw_vote.data.data, &raw_root) != 0) { + fprintf(stderr, "failed to hash newer raw vote for block-build shadow test\n"); + goto cleanup; + } + if (memcmp(raw_root.bytes, proof_root.bytes, LANTERN_ROOT_SIZE) == 0) { + fprintf(stderr, "newer raw vote should have a distinct data root in block-build shadow test\n"); + goto cleanup; + } + if (lantern_store_set_signed_validator_vote(&client.store, (size_t)validator_id, &raw_vote) != 0) { + fprintf(stderr, "failed to overwrite validator vote with newer raw vote\n"); + goto cleanup; + } + + if (lantern_state_select_block_parent(&client.state, &client.store, &parent_root) != 0) { + fprintf(stderr, "failed to select block parent for block-build shadow test\n"); + goto cleanup; + } + if (lantern_state_collect_attestations_for_block( + &client.state, + &client.store, + block_slot, + proposer_index, + &parent_root, + NULL, + &collected, + &collected_signatures) + != 0) { + fprintf(stderr, "failed to collect attestations for block-build shadow test\n"); + goto cleanup; + } + if (collected.length != 1u || collected_signatures.length != 1u) { + fprintf( + stderr, + "expected exactly one collected attestation after raw-vote shadow test, got votes=%zu sigs=%zu\n", + collected.length, + collected_signatures.length); + goto cleanup; + } + if (memcmp(&collected.data[0], &proof_vote.data, sizeof(proof_vote.data)) != 0) { + fprintf(stderr, "block collection did not keep the proof-backed attestation\n"); + goto cleanup; + } + if (memcmp(&collected_signatures.data[0], &proof_vote.signature, sizeof(proof_vote.signature)) != 0) { + fprintf(stderr, "block collection did not recover the proof-backed attestation signature\n"); + goto cleanup; + } + + lantern_client_error agg_rc = lantern_client_aggregate_attestations_for_block( + &client, + &collected, + &collected_signatures, + &aggregated_attestations, + &aggregated_signatures); + if (agg_rc != LANTERN_CLIENT_OK) { + fprintf(stderr, "failed to aggregate collected proof-backed attestation rc=%d\n", (int)agg_rc); + goto cleanup; + } + if (aggregated_attestations.length != 1u || aggregated_signatures.length != 1u) { + fprintf(stderr, "expected cached proof reuse for collected proof-backed attestation\n"); + goto cleanup; + } + if (!proof_payload_equals(&aggregated_signatures.data[0], &cached_proof)) { + fprintf(stderr, "aggregated block attestations did not reuse the known cached proof\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_attestation_signatures_reset(&aggregated_signatures); + lantern_aggregated_attestations_reset(&aggregated_attestations); + lantern_signature_list_reset(&collected_signatures); + lantern_attestations_reset(&collected); + lantern_aggregated_signature_proof_reset(&cached_proof); + test_reset_agg_cache(&client); + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + +static int test_publish_aggregated_attestations_collects_any_slot_and_prunes_gossip(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; struct PQSignatureSchemeSecretKey *secret = NULL; @@ -1744,11 +2017,21 @@ static int test_publish_aggregated_attestations_filters_cross_subnet_votes(void) LanternSignedVote vote0; LanternSignedVote vote1; LanternSignedVote vote4; + LanternRoot data_root; + LanternSignatureKey vote0_key; + LanternSignatureKey vote1_key; + LanternSignatureKey vote4_key; + LanternSignature cached_signature; LanternSignedAggregatedAttestation decoded; int rc = 1; memset(&capture, 0, sizeof(capture)); memset(&assigned, 0, sizeof(assigned)); + memset(&data_root, 0, sizeof(data_root)); + memset(&vote0_key, 0, sizeof(vote0_key)); + memset(&vote1_key, 0, sizeof(vote1_key)); + memset(&vote4_key, 0, sizeof(vote4_key)); + memset(&cached_signature, 0, sizeof(cached_signature)); lantern_signed_aggregated_attestation_init(&decoded); if (client_test_setup_vote_validation_client_with_validator_count( @@ -1787,13 +2070,27 @@ static int test_publish_aggregated_attestations_filters_cross_subnet_votes(void) fprintf(stderr, "failed to record votes for subnet filter test\n"); goto cleanup; } + if (client.store.gossip_signatures.length != 3u) { + fprintf(stderr, "expected three gossip signatures before aggregation\n"); + goto cleanup; + } + if (lantern_hash_tree_root_attestation_data(&vote0.data.data, &data_root) != 0) { + fprintf(stderr, "failed to hash attestation data for prune test\n"); + goto cleanup; + } + vote0_key.validator_index = vote0.data.validator_id; + vote0_key.data_root = data_root; + vote1_key.validator_index = vote1.data.validator_id; + vote1_key.data_root = data_root; + vote4_key.validator_index = vote4.data.validator_id; + vote4_key.data_root = data_root; - if (lantern_client_debug_publish_aggregated_attestations(&client, vote0.data.slot) != LANTERN_CLIENT_OK) { - fprintf(stderr, "aggregated attestation publish should succeed for aggregator\n"); + if (lantern_client_debug_publish_aggregated_attestations(&client, vote0.data.slot + 1u) != LANTERN_CLIENT_OK) { + fprintf(stderr, "aggregated attestation publish should succeed for any-slot gossip vote\n"); goto cleanup; } if (capture.calls != 1u || capture.payload_len == 0 || !capture.payload) { - fprintf(stderr, "expected one aggregated attestation publish in subnet filter test\n"); + fprintf(stderr, "expected one aggregated attestation publish in any-slot test\n"); goto cleanup; } @@ -1807,6 +2104,23 @@ static int test_publish_aggregated_attestations_filters_cross_subnet_votes(void) fprintf(stderr, "published aggregated proof participants did not enforce subnet filtering\n"); goto cleanup; } + if (client.store.gossip_signatures.length != 1u) { + fprintf(stderr, "expected aggregated gossip signatures to be pruned after publish\n"); + goto cleanup; + } + if (lantern_store_get_gossip_signature(&client.store, &vote0_key, &cached_signature) == 0 + || lantern_store_get_gossip_signature(&client.store, &vote4_key, &cached_signature) == 0) { + 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 remain cached after publish\n"); + goto cleanup; + } + if (memcmp(&cached_signature, &vote1.signature, sizeof(cached_signature)) != 0) { + fprintf(stderr, "remaining cross-subnet gossip vote signature mismatch after prune\n"); + goto cleanup; + } rc = 0; @@ -2013,6 +2327,9 @@ int main(void) { if (test_record_vote_defers_interval_pipeline() != 0) { return 1; } + if (test_skip_fork_choice_intervals_replays_interval_side_effects() != 0) { + return 1; + } if (test_chain_service_tick_to_skips_stale_intervals() != 0) { return 1; } @@ -2031,13 +2348,16 @@ int main(void) { if (test_validator_build_skips_raw_signatures_without_cached_proof() != 0) { return 1; } + if (test_block_build_keeps_known_payload_after_newer_raw_vote() != 0) { + return 1; + } if (test_validator_build_reuses_cached_group_proof() != 0) { return 1; } if (test_publish_attestations_skips_proposer_pending_vote() != 0) { return 1; } - if (test_publish_aggregated_attestations_filters_cross_subnet_votes() != 0) { + if (test_publish_aggregated_attestations_collects_any_slot_and_prunes_gossip() != 0) { return 1; } if (test_interval_2_aggregation_trigger_respects_aggregator_role() != 0) { diff --git a/tests/unit/test_state.c b/tests/unit/test_state.c index d8634aa..7b53de4 100644 --- a/tests/unit/test_state.c +++ b/tests/unit/test_state.c @@ -2038,13 +2038,14 @@ static int test_process_block_defers_proposer_attestation(void) { /* Use roots from historical_block_hashes so attestations pass validation */ LanternCheckpoint base = state.latest_justified; base.root = get_historical_root_for_tests(&state, base.slot); - LanternCheckpoint next = base; - next.slot = base.slot + 1u; - next.root = get_historical_root_for_tests(&state, next.slot); + LanternCheckpoint head_checkpoint = base; + head_checkpoint.slot = block->slot; + fill_root(&head_checkpoint.root, 0xB3); LanternSignedVote proposer_vote; memset(&proposer_vote, 0, sizeof(proposer_vote)); - build_vote(&proposer_vote.data, NULL, block->proposer_index, block->slot, &base, &next, 0xB2); + build_vote(&proposer_vote.data, NULL, block->proposer_index, block->slot, &base, &base, 0); + proposer_vote.data.head = head_checkpoint; expect_zero(sign_vote_with_secret(&proposer_vote, proposer_secret), "sign proposer attestation"); signed_block.message.proposer_attestation = proposer_vote.data; signed_block.signatures.proposer_signature = proposer_vote.signature; @@ -2104,7 +2105,8 @@ static int test_process_block_defers_proposer_attestation(void) { lantern_store_get_attestation_data(store, &data_root, &cached_data), "retrieve cached proposer attestation data"); assert(checkpoints_equal(&cached_data.source, &base)); - assert(checkpoints_equal(&cached_data.target, &next)); + assert(checkpoints_equal(&cached_data.target, &base)); + assert(checkpoints_equal(&cached_data.head, &head_checkpoint)); assert(fork_choice.new_votes != NULL); assert(fork_choice.known_votes != NULL); @@ -2567,6 +2569,62 @@ static int test_compute_vote_checkpoints_basic(void) { return 0; } +static int test_compute_vote_checkpoints_can_match_source(void) { + LanternState state; + LanternForkChoice fork_choice; + LanternRoot genesis_root; + setup_state_and_fork_choice(&state, &fork_choice, 1550, 4, &genesis_root); + + LanternBlock block1; + LanternRoot block1_root; + make_block(&state, 1, &genesis_root, &block1, &block1_root); + expect_zero( + lantern_fork_choice_add_block(&fork_choice, &block1, NULL, NULL, NULL, &block1_root), + "add block1 source-match test"); + fork_choice.head = block1_root; + fork_choice.has_head = true; + fork_choice.safe_target = genesis_root; + fork_choice.has_safe_target = true; + + LanternCheckpoint head; + LanternCheckpoint target; + LanternCheckpoint source; + int rc = lantern_state_compute_vote_checkpoints(&state, &head, &target, &source); + if (rc != 0) { + fprintf(stderr, "compute vote checkpoints source-match failed (rc=%d)\n", rc); + lantern_block_body_reset(&block1.body); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + if (head.slot != block1.slot || memcmp(head.root.bytes, block1_root.bytes, LANTERN_ROOT_SIZE) != 0) { + fprintf(stderr, "unexpected head checkpoint in source-match test\n"); + lantern_block_body_reset(&block1.body); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + if (!checkpoints_equal(&target, &state.latest_justified)) { + fprintf(stderr, "target should match source/latest_justified when safe target lags at genesis\n"); + lantern_block_body_reset(&block1.body); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + if (!checkpoints_equal(&source, &state.latest_justified)) { + fprintf(stderr, "source checkpoint mismatch in source-match test\n"); + lantern_block_body_reset(&block1.body); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + + lantern_block_body_reset(&block1.body); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 0; +} + static int test_compute_vote_checkpoints_respects_safe_target(void) { LanternState state; LanternForkChoice fork_choice; @@ -3096,6 +3154,9 @@ int main(void) { if (test_compute_vote_checkpoints_basic() != 0) { return 1; } + if (test_compute_vote_checkpoints_can_match_source() != 0) { + return 1; + } if (test_compute_vote_checkpoints_uses_store_source_when_store_ahead() != 0) { return 1; } From fe615087234c101d2ef31dd79f9cb6afac45559c Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:38:31 +1000 Subject: [PATCH 2/4] Use cached fork-choice head state & refine gossip cache --- src/consensus/state.c | 66 ++++++++++++++---- src/consensus/store.c | 11 +-- src/core/client_internal.h | 2 +- src/core/client_sync_votes.c | 39 ++++++++++- tests/unit/test_client_vote.c | 37 ++++++---- tests/unit/test_state.c | 125 ++++++++++++++++++++++++++++++++++ 6 files changed, 249 insertions(+), 31 deletions(-) diff --git a/src/consensus/state.c b/src/consensus/state.c index 23a5cee..18fcf52 100644 --- a/src/consensus/state.c +++ b/src/consensus/state.c @@ -47,6 +47,10 @@ static uint64_t lantern_state_justified_slots_anchor(const LanternState *state) return state->latest_finalized.slot + 1u; } +static const LanternState *lantern_state_cached_fork_choice_state_for_root( + const LanternStore *store, + const LanternRoot *root); + static bool lantern_checkpoint_equal(const LanternCheckpoint *a, const LanternCheckpoint *b); static int lantern_root_list_append(struct lantern_root_list *list, const LanternRoot *root); @@ -55,6 +59,7 @@ static int lantern_bitlist_get_bit(const struct lantern_bitlist *list, size_t in static int lantern_bitlist_ensure_length(struct lantern_bitlist *list, size_t bit_length); static int lantern_bitlist_drop_front(struct lantern_bitlist *list, size_t bits); static void lantern_root_zero(LanternRoot *root); +static bool lantern_root_is_zero(const LanternRoot *root); static int lantern_state_append_historical_root(LanternState *state, const LanternRoot *root); static int lantern_state_set_justified_slot_bit(LanternState *state, uint64_t slot, bool value); bool lantern_state_slot_in_justified_window(const LanternState *state, uint64_t slot); @@ -88,6 +93,15 @@ static int lantern_state_process_attestations_internal( const LanternSignatureList *signatures, bool apply_consensus_effects); +static const LanternState *lantern_state_cached_fork_choice_state_for_root( + const LanternStore *store, + const LanternRoot *root) { + if (!store || !store->fork_choice || !root || lantern_root_is_zero(root)) { + return NULL; + } + return lantern_fork_choice_block_state(store->fork_choice, root); +} + static bool signature_is_zero(const LanternSignature *signature) { if (!signature) { return true; @@ -2303,6 +2317,17 @@ int lantern_state_select_block_parent( return -1; } + if (store->fork_choice) { + LanternRoot head_root; + if (lantern_fork_choice_current_head(store->fork_choice, &head_root) != 0) { + return -1; + } + if (lantern_state_cached_fork_choice_state_for_root(store, &head_root)) { + *out_parent_root = head_root; + return 0; + } + } + if (lantern_state_process_slot(state) != 0) { return -1; } @@ -2321,9 +2346,10 @@ int lantern_state_select_block_parent( return -1; } *out_parent_root = head_root; - } else { - *out_parent_root = header_root; + return 0; } + + *out_parent_root = header_root; return 0; } @@ -2342,7 +2368,11 @@ int lantern_state_collect_attestations_for_block( if (!store->validator_votes || store->validator_votes_len == 0) { return -1; } - if (block_slot <= state->slot) { + const LanternState *base_state = lantern_state_cached_fork_choice_state_for_root(store, parent_root); + if (!base_state) { + base_state = state; + } + if (block_slot <= base_state->slot) { return -1; } if (lantern_attestations_resize(out_attestations, 0) != 0) { @@ -2367,7 +2397,7 @@ int lantern_state_collect_attestations_for_block( LanternAggregatedAttestations aggregated_view; lantern_aggregated_attestations_init(&aggregated_view); - if (lantern_state_clone(state, &slot_snapshot) != 0) { + if (lantern_state_clone(base_state, &slot_snapshot) != 0) { rc = -1; goto cleanup; } @@ -2483,14 +2513,20 @@ int lantern_state_preview_post_state_root( if (!state || !store || !block || !out_state_root) { return -1; } - if (block->message.block.slot <= state->slot) { + const LanternState *base_state = lantern_state_cached_fork_choice_state_for_root( + store, + &block->message.block.parent_root); + if (!base_state) { + base_state = state; + } + if (block->message.block.slot <= base_state->slot) { return -1; } LanternState scratch; lantern_state_init(&scratch); LanternStore scratch_store; lantern_store_init(&scratch_store); - if (lantern_state_clone(state, &scratch) != 0) { + if (lantern_state_clone(base_state, &scratch) != 0) { return -1; } if (lantern_store_clone_validator_votes(store, &scratch_store) != 0) { @@ -2541,23 +2577,27 @@ int lantern_state_compute_vote_checkpoints( const LanternForkChoice *fork_choice = store->fork_choice; bool trace_finalization = finalization_trace_enabled(); - struct lantern_log_metadata trace_meta = {.has_slot = true, .slot = state->slot}; - char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char target_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; - char safe_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; LanternRoot head_root; if (lantern_fork_choice_current_head(fork_choice, &head_root) != 0) { return -1; } + const LanternState *base_state = lantern_state_cached_fork_choice_state_for_root(store, &head_root); + if (!base_state) { + base_state = state; + } + struct lantern_log_metadata trace_meta = {.has_slot = true, .slot = base_state->slot}; + char head_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char target_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char parent_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; + char safe_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; uint64_t head_slot = 0; 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); - LanternCheckpoint source_checkpoint = state->latest_justified; - LanternCheckpoint finalized_checkpoint = state->latest_finalized; + 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; } diff --git a/src/consensus/store.c b/src/consensus/store.c index afe9dfb..e79ba58 100644 --- a/src/consensus/store.c +++ b/src/consensus/store.c @@ -600,10 +600,7 @@ int lantern_store_set_gossip_signature( const LanternAttestationData *data, const LanternSignature *signature, uint64_t target_slot) { - if (!store || !key || !signature) { - return -1; - } - if (gossip_signature_map_set(&store->gossip_signatures, key, signature, target_slot) != 0) { + if (!store || !key) { return -1; } if (data) { @@ -613,6 +610,12 @@ int lantern_store_set_gossip_signature( data, target_slot); } + if (!signature || signature_is_zero(signature)) { + return 0; + } + if (gossip_signature_map_set(&store->gossip_signatures, key, signature, target_slot) != 0) { + return -1; + } return 0; } diff --git a/src/core/client_internal.h b/src/core/client_internal.h index 695e67c..0c12755 100644 --- a/src/core/client_internal.h +++ b/src/core/client_internal.h @@ -191,7 +191,7 @@ const char *connection_reason_text(int reason); * * @param client Client instance (state_lock must be held) * @param key Signature cache key - * @param signature XMSS signature to cache + * @param signature XMSS signature to cache, or NULL to record attestation data only * @return 0 on success, -1 on error * * @note Thread safety: Caller must hold state_lock. diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index 60ddf57..796059c 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -34,6 +34,8 @@ enum VOTE_ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, }; +static const size_t DEFAULT_GOSSIP_ATTESTATION_COMMITTEE_COUNT = 1u; + /* ============================================================================ * External Functions (from client_sync.c) @@ -220,6 +222,39 @@ static bool validate_vote_cache_state( return true; } +static size_t gossip_attestation_committee_count(const struct lantern_client *client) +{ + if (client && client->debug_attestation_committee_count > 0) { + return client->debug_attestation_committee_count; + } + return DEFAULT_GOSSIP_ATTESTATION_COMMITTEE_COUNT; +} + +static bool should_cache_gossip_signature_locked( + const struct lantern_client *client, + const LanternVote *vote) +{ + if (!client || !vote || !client->assigned_validators || !client->assigned_validators->enr.is_aggregator) { + return false; + } + + size_t committee_count = gossip_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; +} + /** * @brief Cache a signed validator vote in state. @@ -403,6 +438,8 @@ static bool process_vote_locked( LanternRoot data_root; if (lantern_hash_tree_root_attestation_data(&vote->data.data, &data_root) == 0) { + const LanternSignature *signature_to_cache = + should_cache_gossip_signature_locked(client, &vote->data) ? &vote->signature : NULL; LanternSignatureKey key = { .validator_index = vote->data.validator_id, .data_root = data_root, @@ -411,7 +448,7 @@ static bool process_vote_locked( client, &key, &vote->data.data, - &vote->signature, + signature_to_cache, vote->data.target.slot) != 0) { diff --git a/tests/unit/test_client_vote.c b/tests/unit/test_client_vote.c index 59780ea..7564087 100644 --- a/tests/unit/test_client_vote.c +++ b/tests/unit/test_client_vote.c @@ -288,14 +288,20 @@ static int test_record_vote_accepts_known_roots(void) { .validator_index = vote.data.validator_id, .data_root = data_root, }; + LanternAttestationData cached_data; LanternSignature cached_signature; + memset(&cached_data, 0, sizeof(cached_data)); memset(&cached_signature, 0, sizeof(cached_signature)); - if (lantern_store_get_gossip_signature(&client.store, &key, &cached_signature) != 0) { - fprintf(stderr, "gossip signature cache missing accepted vote\n"); + if (lantern_store_get_attestation_data(&client.store, &data_root, &cached_data) != 0) { + fprintf(stderr, "attestation data cache missing accepted vote\n"); goto cleanup; } - if (memcmp(&cached_signature, &vote.signature, sizeof(vote.signature)) != 0) { - fprintf(stderr, "cached gossip signature mismatch\n"); + if (memcmp(&cached_data, &vote.data.data, sizeof(vote.data.data)) != 0) { + fprintf(stderr, "cached attestation data mismatch\n"); + goto cleanup; + } + if (lantern_store_get_gossip_signature(&client.store, &key, &cached_signature) == 0) { + fprintf(stderr, "non-aggregator vote should not populate gossip signature cache\n"); goto cleanup; } @@ -2070,8 +2076,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 != 3u) { - fprintf(stderr, "expected three gossip signatures before aggregation\n"); + if (client.store.gossip_signatures.length != 2u) { + fprintf(stderr, "expected only matching-subnet gossip signatures before aggregation\n"); goto cleanup; } if (lantern_hash_tree_root_attestation_data(&vote0.data.data, &data_root) != 0) { @@ -2104,8 +2110,8 @@ static int test_publish_aggregated_attestations_collects_any_slot_and_prunes_gos fprintf(stderr, "published aggregated proof participants did not enforce subnet filtering\n"); goto cleanup; } - if (client.store.gossip_signatures.length != 1u) { - fprintf(stderr, "expected aggregated gossip signatures to be pruned after publish\n"); + if (client.store.gossip_signatures.length != 0u) { + fprintf(stderr, "expected aggregated gossip signatures to be fully pruned after publish\n"); goto cleanup; } if (lantern_store_get_gossip_signature(&client.store, &vote0_key, &cached_signature) == 0 @@ -2113,12 +2119,19 @@ 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 remain cached after publish\n"); + 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"); + goto cleanup; + } + + LanternAttestationData cached_vote1_data; + memset(&cached_vote1_data, 0, sizeof(cached_vote1_data)); + if (lantern_store_get_attestation_data(&client.store, &data_root, &cached_vote1_data) != 0) { + fprintf(stderr, "cross-subnet gossip vote should still retain attestation data\n"); goto cleanup; } - if (memcmp(&cached_signature, &vote1.signature, sizeof(cached_signature)) != 0) { - fprintf(stderr, "remaining cross-subnet gossip vote signature mismatch after prune\n"); + if (memcmp(&cached_vote1_data, &vote1.data.data, sizeof(cached_vote1_data)) != 0) { + fprintf(stderr, "cross-subnet attestation data mismatch after publish\n"); goto cleanup; } diff --git a/tests/unit/test_state.c b/tests/unit/test_state.c index 7b53de4..bab35b9 100644 --- a/tests/unit/test_state.c +++ b/tests/unit/test_state.c @@ -2513,6 +2513,128 @@ static int test_select_block_parent_uses_fork_choice(void) { return 0; } +static int test_validator_helpers_use_cached_fork_choice_head_state(void) { + LanternState state; + LanternState block_one_state; + LanternState expected_state; + LanternForkChoice fork_choice; + LanternRoot genesis_root; + LanternRoot parent_root; + LanternRoot preview_state_root; + LanternRoot expected_state_root; + LanternAttestations collected; + LanternSignatureList collected_signatures; + LanternSignedBlock signed_block; + LanternBlock block_one; + LanternRoot block_one_root; + LanternRoot block_one_state_root; + uint64_t proposer_index = 0; + int result = 1; + + lantern_attestations_init(&collected); + lantern_signature_list_init(&collected_signatures); + lantern_signed_block_with_attestation_init(&signed_block); + lantern_state_init(&block_one_state); + lantern_state_init(&expected_state); + + setup_state_and_fork_choice(&state, &fork_choice, 1525, 4, &genesis_root); + expect_zero( + lantern_state_prepare_validator_votes(&state, state.config.num_validators), + "prepare validator votes for cached head helper test"); + + make_block(&state, 1, &genesis_root, &block_one, &block_one_root); + expect_zero(lantern_state_clone(&state, &block_one_state), "clone block one state"); + expect_zero( + lantern_state_prepare_validator_votes(&block_one_state, block_one_state.config.num_validators), + "prepare block one validator votes"); + expect_zero(lantern_state_process_slots(&block_one_state, block_one.slot), "advance block one state"); + expect_zero( + lantern_state_process_block(&block_one_state, &block_one, NULL, NULL), + "process block one state"); + expect_zero(lantern_hash_tree_root_state(&block_one_state, &block_one_state_root), "hash block one state"); + block_one.state_root = block_one_state_root; + expect_zero(lantern_hash_tree_root_block(&block_one, &block_one_root), "rehash block one with state root"); + expect_zero( + lantern_fork_choice_add_block_with_state( + &fork_choice, + &block_one, + NULL, + &block_one_state.latest_justified, + &block_one_state.latest_finalized, + &block_one_root, + &block_one_state), + "add block one with cached state"); + + if (lantern_state_select_block_parent(&state, &parent_root) != 0) { + fprintf(stderr, "failed to select cached fork-choice parent root\n"); + goto cleanup; + } + if (memcmp(parent_root.bytes, block_one_root.bytes, LANTERN_ROOT_SIZE) != 0) { + fprintf(stderr, "selected parent root did not follow cached fork-choice head\n"); + goto cleanup; + } + + expect_zero( + lantern_proposer_for_slot(2u, state.config.num_validators, &proposer_index), + "compute proposer for preview block"); + if (lantern_state_collect_attestations_for_block( + &state, + 2u, + proposer_index, + &parent_root, + NULL, + &collected, + &collected_signatures) + != 0) { + fprintf(stderr, "failed to collect attestations from cached fork-choice head state\n"); + goto cleanup; + } + if (collected.length != 0u || collected_signatures.length != 0u) { + fprintf(stderr, "expected no collected attestations for empty cached-head test\n"); + goto cleanup; + } + + signed_block.message.block.slot = 2u; + signed_block.message.block.proposer_index = proposer_index; + signed_block.message.block.parent_root = parent_root; + if (lantern_state_preview_post_state_root( + &state, + &signed_block, + &preview_state_root) + != 0) { + fprintf(stderr, "failed to preview post-state root from cached fork-choice head state\n"); + goto cleanup; + } + + expect_zero(lantern_state_clone(&block_one_state, &expected_state), "clone expected state"); + expect_zero( + lantern_state_prepare_validator_votes(&expected_state, expected_state.config.num_validators), + "prepare expected-state validator votes"); + expect_zero(lantern_state_process_slots(&expected_state, signed_block.message.block.slot), "advance expected state"); + expect_zero( + lantern_state_process_block(&expected_state, &signed_block.message.block, NULL, NULL), + "process preview block on cached head state"); + expect_zero(lantern_hash_tree_root_state(&expected_state, &expected_state_root), "hash expected preview state"); + + if (memcmp(preview_state_root.bytes, expected_state_root.bytes, LANTERN_ROOT_SIZE) != 0) { + fprintf(stderr, "preview post-state root did not use cached fork-choice head state\n"); + goto cleanup; + } + + result = 0; + +cleanup: + lantern_state_reset(&expected_state); + lantern_state_reset(&block_one_state); + lantern_signed_block_with_attestation_reset(&signed_block); + lantern_signature_list_reset(&collected_signatures); + lantern_attestations_reset(&collected); + lantern_block_body_reset(&block_one.body); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return result; +} + static int test_compute_vote_checkpoints_basic(void) { LanternState state; LanternForkChoice fork_choice; @@ -3151,6 +3273,9 @@ int main(void) { if (test_select_block_parent_uses_fork_choice() != 0) { return 1; } + if (test_validator_helpers_use_cached_fork_choice_head_state() != 0) { + return 1; + } if (test_compute_vote_checkpoints_basic() != 0) { return 1; } From c0644f34cc855357e9d6a8573ca6ed34d87ed3cb Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:05:47 +1000 Subject: [PATCH 3/4] Cache block proofs and proposer attestations --- src/core/client_sync.c | 7 ++ src/core/client_sync_blocks.c | 45 ++++++++- src/core/client_sync_internal.h | 39 +++++++ src/core/client_sync_votes.c | 14 +-- src/core/client_validator.c | 7 -- tests/unit/test_client_pending.c | 168 +++++++++++++++++++++++++++++++ 6 files changed, 264 insertions(+), 16 deletions(-) diff --git a/src/core/client_sync.c b/src/core/client_sync.c index 11903da..76acfa5 100644 --- a/src/core/client_sync.c +++ b/src/core/client_sync.c @@ -946,6 +946,13 @@ int restore_persisted_blocks(struct lantern_client *client) "failed to restore block at slot %" PRIu64, entry->block.message.block.slot); } + else + { + lantern_client_cache_block_aggregated_proofs_locked(client, &entry->block); + if (proposer_ptr) { + lantern_client_cache_proposer_attestation_locked(client, proposer_ptr); + } + } if (have_cached_post_state) { lantern_state_reset(&cached_post_state); diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index 4c06349..cee585b 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -371,7 +371,7 @@ static bool signed_block_signatures_are_valid( return proposer_ok; } -static void cache_block_aggregated_proofs_locked( +void lantern_client_cache_block_aggregated_proofs_locked( struct lantern_client *client, const LanternSignedBlock *block) { @@ -395,7 +395,7 @@ static void cache_block_aggregated_proofs_locked( if (lantern_hash_tree_root_attestation_data(&attestations->data[i].data, &data_root) != 0) { continue; } - (void)lantern_client_add_new_aggregated_payload( + (void)lantern_client_add_known_aggregated_payload( client, &data_root, &attestations->data[i].data, @@ -404,6 +404,42 @@ static void cache_block_aggregated_proofs_locked( } } +void lantern_client_cache_proposer_attestation_locked( + struct lantern_client *client, + const LanternSignedVote *proposer_attestation) +{ + if (!client || !proposer_attestation) { + return; + } + + const LanternVote *vote = &proposer_attestation->data; + if (client->has_state + && (client->state.config.num_validators == 0 + || vote->validator_id >= client->state.config.num_validators)) { + return; + } + + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote->data, &data_root) != 0) { + return; + } + + const LanternSignature *signature_to_cache = + lantern_client_should_cache_attestation_signature_locked(client, vote) + ? &proposer_attestation->signature + : NULL; + LanternSignatureKey key = { + .validator_index = vote->validator_id, + .data_root = data_root, + }; + (void)lantern_client_set_gossip_signature( + client, + &key, + &vote->data, + signature_to_cache, + vote->target.slot); +} + static void persist_block_after_import( struct lantern_client *client, const LanternSignedBlock *block, @@ -1947,6 +1983,9 @@ static bool add_competing_fork_block_locked( return false; } + lantern_client_cache_block_aggregated_proofs_locked(client, block); + lantern_client_cache_proposer_attestation_locked(client, &proposer_signed); + char block_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(block_root, block_hex, sizeof(block_hex)); lantern_log_info( @@ -2830,7 +2869,7 @@ bool lantern_client_import_block( goto cleanup; } - cache_block_aggregated_proofs_locked(client, block); + lantern_client_cache_block_aggregated_proofs_locked(client, block); persist_finalized_state_if_advanced_locked( client, diff --git a/src/core/client_sync_internal.h b/src/core/client_sync_internal.h index 452f4d1..4fddfcb 100644 --- a/src/core/client_sync_internal.h +++ b/src/core/client_sync_internal.h @@ -179,6 +179,45 @@ const LanternState *lantern_client_state_for_root_locked( LanternState *scratch, bool *out_is_scratch); +/** + * Return the active attestation committee count for sync/validator cache logic. + * + * Respects debug overrides used by tests and falls back to the protocol default. + */ +size_t lantern_client_attestation_committee_count(const struct lantern_client *client); + +/** + * 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. + * + * @note Caller must hold state_lock. + */ +bool lantern_client_should_cache_attestation_signature_locked( + const struct lantern_client *client, + const LanternVote *vote); + +/** + * Cache block-body aggregated proofs as known attestation material. + * + * Mirrors the block-body proof caching step from the spec's Store.on_block(). + * Caller must hold state_lock. + */ +void lantern_client_cache_block_aggregated_proofs_locked( + struct lantern_client *client, + const LanternSignedBlock *block); + +/** + * Cache proposer attestation data and, when eligible, its signature. + * + * Mirrors the proposer-attestation caching step from the spec's Store.on_block(). + * Caller must hold state_lock. + */ +void lantern_client_cache_proposer_attestation_locked( + struct lantern_client *client, + const LanternSignedVote *proposer_attestation); + /* ============================================================================ * Pending Block Functions diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index 796059c..11f3144 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -34,7 +34,7 @@ enum VOTE_ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, }; -static const size_t DEFAULT_GOSSIP_ATTESTATION_COMMITTEE_COUNT = 1u; +static const size_t DEFAULT_SYNC_ATTESTATION_COMMITTEE_COUNT = 1u; /* ============================================================================ @@ -222,15 +222,15 @@ static bool validate_vote_cache_state( return true; } -static size_t gossip_attestation_committee_count(const struct lantern_client *client) +size_t lantern_client_attestation_committee_count(const struct lantern_client *client) { if (client && client->debug_attestation_committee_count > 0) { return client->debug_attestation_committee_count; } - return DEFAULT_GOSSIP_ATTESTATION_COMMITTEE_COUNT; + return DEFAULT_SYNC_ATTESTATION_COMMITTEE_COUNT; } -static bool should_cache_gossip_signature_locked( +bool lantern_client_should_cache_attestation_signature_locked( const struct lantern_client *client, const LanternVote *vote) { @@ -238,7 +238,7 @@ static bool should_cache_gossip_signature_locked( return false; } - size_t committee_count = gossip_attestation_committee_count(client); + size_t committee_count = lantern_client_attestation_committee_count(client); if (committee_count == 0) { return false; } @@ -439,7 +439,9 @@ static bool process_vote_locked( if (lantern_hash_tree_root_attestation_data(&vote->data.data, &data_root) == 0) { const LanternSignature *signature_to_cache = - should_cache_gossip_signature_locked(client, &vote->data) ? &vote->signature : NULL; + lantern_client_should_cache_attestation_signature_locked(client, &vote->data) + ? &vote->signature + : NULL; LanternSignatureKey key = { .validator_index = vote->data.validator_id, .data_root = data_root, diff --git a/src/core/client_validator.c b/src/core/client_validator.c index a32830e..b7d4e76 100644 --- a/src/core/client_validator.c +++ b/src/core/client_validator.c @@ -2684,13 +2684,6 @@ void *validator_thread(void *arg) break; case LANTERN_DUTY_PHASE_SAFE_TARGET: - if (!duty->slot_aggregated && duty->slot_attested) - { - if (validator_publish_aggregated_attestations(client, tp->slot) == LANTERN_CLIENT_OK) - { - duty->slot_aggregated = true; - } - } break; default: diff --git a/tests/unit/test_client_pending.c b/tests/unit/test_client_pending.c index 3ffcede..97e9b0e 100644 --- a/tests/unit/test_client_pending.c +++ b/tests/unit/test_client_pending.c @@ -6,6 +6,7 @@ #include "client_test_helpers.h" #include "../../src/core/client_services_internal.h" +#include "../../src/core/client_sync_internal.h" #include "lantern/consensus/duties.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" @@ -1016,6 +1017,18 @@ static int test_import_block_accepts_complete_signatures(void) fprintf(stderr, "state slot did not advance after importing fully signed block\n"); goto cleanup; } + if (fixture.client.store.new_aggregated_payloads.length != 0u) { + fprintf(stderr, "canonical import should not stage block-body proofs in new payloads\n"); + goto cleanup; + } + if (fixture.client.store.known_aggregated_payloads.length != 1u) { + fprintf(stderr, "canonical import should cache block-body proofs directly in known payloads\n"); + goto cleanup; + } + if (fixture.client.store.attestation_data_by_root.length == 0u) { + fprintf(stderr, "canonical import should retain attestation data for cached block proofs\n"); + goto cleanup; + } rc = 0; @@ -1151,6 +1164,155 @@ static int test_import_block_skips_unknown_attestation_head_root(void) return rc; } +static int test_restore_persisted_blocks_caches_known_attestation_proofs(void) +{ + struct block_signature_fixture fixture; + LanternSignedBlock block; + LanternRoot block_root; + int rc = 1; + + memset(&block, 0, sizeof(block)); + if (setup_block_signature_fixture(&fixture, "test_restore_known_proofs") != 0) { + fprintf(stderr, "failed to set up restore known proofs fixture\n"); + return 1; + } + + if (build_signed_block_for_import(&fixture, true, true, &block, &block_root) != 0) { + fprintf(stderr, "failed to build block fixture for restore known proofs test\n"); + goto cleanup; + } + if (lantern_storage_store_block(fixture.client.data_dir, &block) != 0) { + fprintf(stderr, "failed to persist block fixture for restore known proofs test\n"); + goto cleanup; + } + + if (restore_persisted_blocks(&fixture.client) != LANTERN_CLIENT_OK) { + fprintf(stderr, "restore_persisted_blocks failed for known proofs test\n"); + goto cleanup; + } + if (fixture.client.store.new_aggregated_payloads.length != 0u) { + fprintf(stderr, "restored blocks should not stage block-body proofs in new payloads\n"); + goto cleanup; + } + if (fixture.client.store.known_aggregated_payloads.length != 1u) { + fprintf(stderr, "restored blocks should cache block-body proofs directly in known payloads\n"); + goto cleanup; + } + if (fixture.client.store.attestation_data_by_root.length == 0u) { + fprintf(stderr, "restored blocks should retain attestation data for cached proofs\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_signed_block_with_attestation_reset(&block); + teardown_block_signature_fixture(&fixture); + return rc; +} + +static int test_restore_persisted_blocks_caches_proposer_attestation(void) +{ + struct block_signature_fixture fixture; + struct lantern_validator_config_entry assigned; + LanternSignedBlock block; + LanternRoot block_root; + LanternRoot proposer_data_root; + LanternSignatureKey proposer_key; + LanternSignature cached_signature; + LanternAttestationData cached_data; + int rc = 1; + + memset(&assigned, 0, sizeof(assigned)); + memset(&block, 0, sizeof(block)); + memset(&proposer_data_root, 0, sizeof(proposer_data_root)); + memset(&proposer_key, 0, sizeof(proposer_key)); + memset(&cached_signature, 0, sizeof(cached_signature)); + memset(&cached_data, 0, sizeof(cached_data)); + + if (setup_block_signature_fixture(&fixture, "test_restore_proposer_cache") != 0) { + fprintf(stderr, "failed to set up restore proposer cache fixture\n"); + return 1; + } + + assigned.enr.is_aggregator = true; + fixture.client.assigned_validators = &assigned; + fixture.client.gossip.attestation_subnet_id = 0u; + + if (build_signed_block_for_import(&fixture, true, true, &block, &block_root) != 0) { + fprintf(stderr, "failed to build block fixture for restore proposer cache test\n"); + goto cleanup; + } + if (lantern_aggregated_attestations_resize(&block.message.block.body.attestations, 0u) != 0 + || lantern_attestation_signatures_resize(&block.signatures.attestation_signatures, 0u) != 0) { + fprintf(stderr, "failed to clear block-body attestations for restore proposer cache test\n"); + goto cleanup; + } + if (lantern_state_preview_post_state_root( + &fixture.client.state, + &fixture.client.store, + &block, + &block.message.block.state_root) + != 0) { + fprintf(stderr, "failed to preview state root for proposer-only restore test\n"); + goto cleanup; + } + if (lantern_hash_tree_root_block(&block.message.block, &block_root) != 0) { + fprintf(stderr, "failed to hash proposer-only restore block\n"); + goto cleanup; + } + if (lantern_storage_store_block(fixture.client.data_dir, &block) != 0) { + fprintf(stderr, "failed to persist proposer-only block fixture for restore test\n"); + goto cleanup; + } + if (lantern_hash_tree_root_attestation_data( + &block.message.proposer_attestation.data, + &proposer_data_root) + != 0) { + fprintf(stderr, "failed to hash proposer attestation data for restore test\n"); + goto cleanup; + } + + if (restore_persisted_blocks(&fixture.client) != LANTERN_CLIENT_OK) { + fprintf(stderr, "restore_persisted_blocks failed for proposer cache test\n"); + goto cleanup; + } + if (fixture.client.store.known_aggregated_payloads.length != 0u) { + fprintf(stderr, "proposer-only restored block should not create known block-body proofs\n"); + goto cleanup; + } + if (fixture.client.store.attestation_data_by_root.length != 1u) { + fprintf(stderr, "proposer-only restored block should cache proposer attestation data\n"); + goto cleanup; + } + + proposer_key.validator_index = block.message.proposer_attestation.validator_id; + proposer_key.data_root = proposer_data_root; + if (lantern_store_get_attestation_data(&fixture.client.store, &proposer_data_root, &cached_data) != 0) { + fprintf(stderr, "missing proposer attestation data after restore\n"); + goto cleanup; + } + if (memcmp(&cached_data, &block.message.proposer_attestation.data, sizeof(cached_data)) != 0) { + fprintf(stderr, "restored proposer attestation data mismatch\n"); + goto cleanup; + } + if (lantern_store_get_gossip_signature(&fixture.client.store, &proposer_key, &cached_signature) != 0) { + fprintf(stderr, "missing proposer attestation signature after restore\n"); + goto cleanup; + } + if (memcmp(&cached_signature, &block.signatures.proposer_signature, sizeof(cached_signature)) != 0) { + fprintf(stderr, "restored proposer attestation signature mismatch\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_signed_block_with_attestation_reset(&block); + teardown_block_signature_fixture(&fixture); + return rc; +} + int main(void) { if (test_pending_block_queue() != 0) { return 1; @@ -1176,6 +1338,12 @@ int main(void) { if (test_import_block_skips_unknown_attestation_head_root() != 0) { return 1; } + if (test_restore_persisted_blocks_caches_known_attestation_proofs() != 0) { + return 1; + } + if (test_restore_persisted_blocks_caches_proposer_attestation() != 0) { + return 1; + } puts("lantern_client_pending_test OK"); return 0; } From 6f2574ab9a91311757dae3f650720a557d8d7ab8 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:00:06 +1000 Subject: [PATCH 4/4] Buffer and replay pending gossip votes --- include/lantern/core/client.h | 14 ++ src/core/client.c | 2 + src/core/client_debug.c | 14 ++ src/core/client_pending.c | 144 +++++++++++++++ src/core/client_sync_blocks.c | 2 + src/core/client_sync_internal.h | 51 ++++++ src/core/client_sync_votes.c | 306 ++++++++++++++++++++++--------- tests/unit/client_test_helpers.c | 4 + tests/unit/test_client_vote.c | 237 +++++++++++++++++++++++- 9 files changed, 684 insertions(+), 90 deletions(-) diff --git a/include/lantern/core/client.h b/include/lantern/core/client.h index 0433a87..2113448 100644 --- a/include/lantern/core/client.h +++ b/include/lantern/core/client.h @@ -37,6 +37,7 @@ extern "C" { #define LANTERN_DEFAULT_METRICS_PORT 8080 #define LANTERN_DEFAULT_DEVNET "devnet0" #define LANTERN_PENDING_BLOCK_LIMIT 1024u +#define LANTERN_PENDING_GOSSIP_VOTE_LIMIT 1024u typedef enum { @@ -119,6 +120,17 @@ struct lantern_pending_block_list { struct lantern_pending_parent_index parent_index; }; +struct lantern_pending_vote { + LanternSignedVote vote; + char peer_text[128]; +}; + +struct lantern_pending_vote_list { + struct lantern_pending_vote *items; + size_t length; + size_t capacity; +}; + struct lantern_active_blocks_request { uint64_t request_id; char peer_id[128]; @@ -220,6 +232,7 @@ struct lantern_client { struct lantern_string_list inbound_peer_ids; struct lantern_string_list status_failure_peer_ids; struct lantern_pending_block_list pending_blocks; + struct lantern_pending_vote_list pending_gossip_votes; pthread_mutex_t pending_lock; bool pending_lock_initialized; LanternRoot sync_last_requested_root; @@ -342,6 +355,7 @@ int lantern_client_debug_import_block( const LanternRoot *block_root, const char *peer_id_text); size_t lantern_client_pending_block_count(const struct lantern_client *client); +size_t lantern_client_pending_vote_count(const struct lantern_client *client); #define LANTERN_TEST_BLOCKS_REQUEST_SUCCESS 0 #define LANTERN_TEST_BLOCKS_REQUEST_FAILED 1 diff --git a/src/core/client.c b/src/core/client.c index ccdbcb6..06fffb2 100644 --- a/src/core/client.c +++ b/src/core/client.c @@ -668,6 +668,7 @@ static void client_reset_base(struct lantern_client *client) client->ping_thread_started = false; client->ping_stop_flag = 1; pending_block_list_init(&client->pending_blocks); + pending_vote_list_init(&client->pending_gossip_votes); client->pending_lock_initialized = false; client->sync_state = LANTERN_SYNC_STATE_IDLE; } @@ -3212,6 +3213,7 @@ static void shutdown_genesis_and_network(struct lantern_client *client) */ static void shutdown_state_and_runtime(struct lantern_client *client) { + pending_vote_list_reset(&client->pending_gossip_votes); if (client->has_state) { lantern_state_reset(&client->state); diff --git a/src/core/client_debug.c b/src/core/client_debug.c index aa63cee..a3b507a 100644 --- a/src/core/client_debug.c +++ b/src/core/client_debug.c @@ -149,6 +149,20 @@ size_t lantern_client_pending_block_count(const struct lantern_client *client) } +size_t lantern_client_pending_vote_count(const struct lantern_client *client) +{ + if (!client) + { + return 0; + } + struct lantern_client *mutable_client = (struct lantern_client *)client; + bool locked = lantern_client_lock_state(mutable_client); + size_t count = client->pending_gossip_votes.length; + lantern_client_unlock_state(mutable_client, locked); + return count; +} + + /** * Debug API: Enqueue a pending block for testing. * diff --git a/src/core/client_pending.c b/src/core/client_pending.c index 8a942a1..7873139 100644 --- a/src/core/client_pending.c +++ b/src/core/client_pending.c @@ -142,6 +142,61 @@ static int ensure_pending_block_list_capacity( return LANTERN_CLIENT_PENDING_OK; } + +/** + * @brief Ensure the pending vote list can hold at least `required` entries. + */ +static int ensure_pending_vote_list_capacity( + struct lantern_pending_vote_list *list, + size_t required) +{ + if (!list) + { + return LANTERN_CLIENT_PENDING_ERR_INVALID_PARAM; + } + + if (list->capacity >= required) + { + return LANTERN_CLIENT_PENDING_OK; + } + + size_t new_capacity = BLOCK_LIST_INITIAL_CAPACITY; + if (list->capacity > 0) + { + size_t half = list->capacity / 2u; + if (list->capacity > SIZE_MAX - half) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + new_capacity = list->capacity + half; + if (new_capacity < BLOCK_LIST_INITIAL_CAPACITY) + { + new_capacity = BLOCK_LIST_INITIAL_CAPACITY; + } + } + + if (new_capacity < required) + { + new_capacity = required; + } + + if (new_capacity > SIZE_MAX / sizeof(*list->items)) + { + return LANTERN_CLIENT_PENDING_ERR_OVERFLOW; + } + + struct lantern_pending_vote *expanded = realloc( + list->items, + new_capacity * sizeof(*expanded)); + if (!expanded) + { + return LANTERN_CLIENT_PENDING_ERR_ALLOC; + } + list->items = expanded; + list->capacity = new_capacity; + return LANTERN_CLIENT_PENDING_OK; +} + static int ensure_pending_parent_index_capacity( struct lantern_pending_parent_index *index, size_t required) @@ -433,6 +488,95 @@ static void pending_parent_index_remove_child( } } +/* ============================================================================ + * Pending Vote List + * ============================================================================ */ + +/** + * Initialize a pending vote list. + * + * @param list List to initialize + * + * @note Thread safety: This function is thread-safe + */ +void pending_vote_list_init(struct lantern_pending_vote_list *list) +{ + if (!list) + { + return; + } + list->items = NULL; + list->length = 0; + list->capacity = 0; +} + + +/** + * Reset and free a pending vote list. + * + * @param list List to reset + * + * @note Thread safety: This function is thread-safe + */ +void pending_vote_list_reset(struct lantern_pending_vote_list *list) +{ + if (!list) + { + return; + } + free(list->items); + list->items = NULL; + list->length = 0; + list->capacity = 0; +} + + +/** + * Append a pending gossip vote to the list. + * + * @param list List to append to + * @param vote Vote to append + * @param peer_text Peer ID text (may be NULL) + * @return Pointer to new entry, or NULL on failure + * + * @note Thread safety: Caller must synchronize access + */ +struct lantern_pending_vote *pending_vote_list_append( + struct lantern_pending_vote_list *list, + const LanternSignedVote *vote, + const char *peer_text) +{ + if (!list || !vote) + { + return NULL; + } + + if (list->length >= LANTERN_PENDING_GOSSIP_VOTE_LIMIT + || list->length == SIZE_MAX) + { + return NULL; + } + + int ensure_rc = ensure_pending_vote_list_capacity(list, list->length + 1u); + if (ensure_rc != LANTERN_CLIENT_PENDING_OK) + { + return NULL; + } + + struct lantern_pending_vote *entry = &list->items[list->length]; + memset(entry, 0, sizeof(*entry)); + entry->vote = *vote; + if (peer_text && *peer_text) + { + strncpy(entry->peer_text, peer_text, sizeof(entry->peer_text) - 1u); + entry->peer_text[sizeof(entry->peer_text) - 1u] = '\0'; + } + list->length += 1u; + + return entry; +} + + /* ============================================================================ * Block Cloning * ============================================================================ */ diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index cee585b..586942d 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -2852,6 +2852,7 @@ bool lantern_client_import_block( persist_block_after_import(client, block, meta); lantern_client_process_pending_children(client, &block_root_local); update_sync_progress_after_block(client); + lantern_client_replay_pending_gossip_votes(client); } return false; } @@ -2909,6 +2910,7 @@ bool lantern_client_import_block( lantern_client_process_pending_children(client, &block_root_local); log_imported_block(block, &head_root, head_slot, meta, quiet_log); update_sync_progress_after_block(client); + lantern_client_replay_pending_gossip_votes(client); } return imported; diff --git a/src/core/client_sync_internal.h b/src/core/client_sync_internal.h index 4fddfcb..b754700 100644 --- a/src/core/client_sync_internal.h +++ b/src/core/client_sync_internal.h @@ -90,6 +90,9 @@ struct lantern_vote_rejection_info bool has_unknown_root; /**< True if rejection is due to unknown checkpoint root */ LanternRoot unknown_root; /**< Unknown checkpoint root */ uint64_t unknown_slot; /**< Slot of unknown checkpoint */ + bool should_retry_after_block_import; /**< True if block import may unblock the vote */ + LanternRoot retry_root; /**< Root whose eventual import may unblock validation */ + uint64_t retry_slot; /**< Slot associated with retry_root */ }; @@ -219,6 +222,46 @@ void lantern_client_cache_proposer_attestation_locked( const LanternSignedVote *proposer_attestation); +/* ============================================================================ + * Pending Vote Functions + * ============================================================================ */ + +/** + * Initialize a pending vote list. + * + * @param list List to initialize + * + * @note Thread safety: This function is thread-safe + */ +void pending_vote_list_init(struct lantern_pending_vote_list *list); + + +/** + * Reset and free a pending vote list. + * + * @param list List to reset + * + * @note Thread safety: This function is thread-safe + */ +void pending_vote_list_reset(struct lantern_pending_vote_list *list); + + +/** + * Append a pending gossip vote to the list. + * + * @param list List to append to + * @param vote Vote to append + * @param peer_text Peer ID text (may be NULL) + * @return Pointer to new entry, or NULL on failure + * + * @note Thread safety: Caller must hold state_lock when mutating client-owned lists + */ +struct lantern_pending_vote *pending_vote_list_append( + struct lantern_pending_vote_list *list, + const LanternSignedVote *vote, + const char *peer_text); + + /* ============================================================================ * Pending Block Functions * ============================================================================ */ @@ -469,6 +512,14 @@ void lantern_client_record_vote( const LanternSignedVote *vote, const char *peer_text); +/** + * Replay pending gossip votes after a successful block import. + * + * Drains the pending vote queue, retrying each buffered vote once against the + * updated store. Votes that still fail are discarded. + */ +void lantern_client_replay_pending_gossip_votes(struct lantern_client *client); + /** * Handle a block received via gossip. diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index 11f3144..985b0e9 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -36,6 +36,13 @@ enum static const size_t DEFAULT_SYNC_ATTESTATION_COMMITTEE_COUNT = 1u; +enum lantern_vote_record_status +{ + LANTERN_VOTE_RECORD_REJECTED = 0, + LANTERN_VOTE_RECORD_ACCEPTED = 1, + LANTERN_VOTE_RECORD_BUFFERED = 2, +}; + /* ============================================================================ * External Functions (from client_sync.c) @@ -129,6 +136,9 @@ static bool validate_vote_checkpoint( out_rejection->has_unknown_root = true; out_rejection->unknown_root = checkpoint->root; out_rejection->unknown_slot = checkpoint->slot; + out_rejection->should_retry_after_block_import = true; + out_rejection->retry_root = checkpoint->root; + out_rejection->retry_slot = checkpoint->slot; } return false; } @@ -163,6 +173,46 @@ static bool validate_vote_checkpoint( } +static bool buffer_pending_vote_locked( + struct lantern_client *client, + const LanternSignedVote *vote, + const char *peer_text, + const struct lantern_vote_rejection_info *rejection, + const struct lantern_log_metadata *meta) +{ + if (!client || !vote || !rejection || !meta) + { + return false; + } + + if (pending_vote_list_append(&client->pending_gossip_votes, vote, peer_text) == NULL) + { + lantern_log_warn( + "gossip", + meta, + "failed to buffer vote validator=%" PRIu64 " slot=%" PRIu64 " pending=%zu", + vote->data.validator_id, + vote->data.slot, + client->pending_gossip_votes.length); + return false; + } + + char retry_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + format_root_hex(&rejection->retry_root, retry_hex, sizeof(retry_hex)); + lantern_log_debug( + "gossip", + meta, + "buffered vote validator=%" PRIu64 " slot=%" PRIu64 " waiting_root=%s waiting_slot=%" PRIu64 + " pending=%zu", + vote->data.validator_id, + vote->data.slot, + retry_hex[0] ? retry_hex : "0x0", + rejection->retry_slot, + client->pending_gossip_votes.length); + return true; +} + + /** * @brief Validates vote cache availability. * @@ -401,6 +451,9 @@ static bool process_vote_locked( "missing target state target_slot=%" PRIu64 " root=%s", 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_slot = vote->data.target.slot; lantern_state_reset(&target_state); return false; } @@ -468,6 +521,146 @@ static bool process_vote_locked( } +static enum lantern_vote_record_status lantern_client_record_vote_internal( + struct lantern_client *client, + const LanternSignedVote *vote, + const char *peer_text, + bool allow_buffering, + bool is_replay) +{ + if (!client || !vote || !client->has_state) + { + return LANTERN_VOTE_RECORD_REJECTED; + } + + struct lantern_log_metadata meta = { + .validator = client->node_id, + .peer = (peer_text && *peer_text) ? peer_text : NULL, + }; + + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) + { + return LANTERN_VOTE_RECORD_REJECTED; + } + + struct lantern_vote_rejection_info rejection; + memset(&rejection, 0, sizeof(rejection)); + + LanternSignedVote vote_copy = *vote; + char head_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + char target_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + char source_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + format_root_hex(&vote_copy.data.head.root, head_hex, sizeof(head_hex)); + format_root_hex(&vote_copy.data.target.root, target_hex, sizeof(target_hex)); + format_root_hex(&vote_copy.data.source.root, source_hex, sizeof(source_hex)); + lantern_log_debug( + "gossip", + &meta, + "%s vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64, + is_replay ? "replaying buffered" : "received", + vote_copy.data.validator_id, + vote_copy.data.slot, + head_hex[0] ? head_hex : "0x0", + target_hex[0] ? target_hex : "0x0", + vote_copy.data.target.slot); + + bool vote_processed = process_vote_locked(client, &vote_copy, &meta, &rejection); + bool vote_buffered = false; + if (!vote_processed + && allow_buffering + && rejection.should_retry_after_block_import) + { + vote_buffered = buffer_pending_vote_locked( + client, + &vote_copy, + peer_text, + &rejection, + &meta); + } + + if (vote_processed) + { + lantern_log_info( + "gossip", + &meta, + "%s vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 + " source=%s@%" PRIu64, + is_replay ? "replayed" : "processed", + vote_copy.data.validator_id, + vote_copy.data.slot, + head_hex[0] ? head_hex : "0x0", + target_hex[0] ? target_hex : "0x0", + vote_copy.data.target.slot, + source_hex[0] ? source_hex : "0x0", + vote_copy.data.source.slot); + } + lantern_client_unlock_state(client, state_locked); + + if (vote_processed) + { + lantern_client_note_vote_outcome(client, peer_text, &vote_copy, true); + return LANTERN_VOTE_RECORD_ACCEPTED; + } + + if (vote_buffered) + { + return LANTERN_VOTE_RECORD_BUFFERED; + } + + lantern_client_note_vote_outcome(client, peer_text, &vote_copy, false); + + const char *reason_text = rejection.has_reason ? rejection.message : "unknown"; + if (client->sync_in_progress) + { + lantern_log_debug( + "gossip", + &meta, + "%s vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 + " source=%s@%" PRIu64 " reason=%s", + is_replay ? "dropped replayed" : "rejected", + vote_copy.data.validator_id, + vote_copy.data.slot, + head_hex[0] ? head_hex : "0x0", + target_hex[0] ? target_hex : "0x0", + vote_copy.data.target.slot, + source_hex[0] ? source_hex : "0x0", + vote_copy.data.source.slot, + reason_text); + } + else + { + lantern_log_info( + "gossip", + &meta, + "%s vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 + " source=%s@%" PRIu64 " reason=%s", + is_replay ? "dropped replayed" : "rejected", + vote_copy.data.validator_id, + vote_copy.data.slot, + head_hex[0] ? head_hex : "0x0", + target_hex[0] ? target_hex : "0x0", + vote_copy.data.target.slot, + source_hex[0] ? source_hex : "0x0", + vote_copy.data.source.slot, + reason_text); + } + if (!is_replay && rejection.has_unknown_root && !lantern_root_is_zero(&rejection.unknown_root)) + { + char root_hex[VOTE_ROOT_HEX_BUFFER_LEN]; + format_root_hex(&rejection.unknown_root, root_hex, sizeof(root_hex)); + lantern_log_info( + "reqresp", + &meta, + "dropping vote unknown root=%s slot=%" PRIu64 " (buffer unavailable)", + root_hex[0] ? root_hex : "0x0", + rejection.unknown_slot); + } + + return LANTERN_VOTE_RECORD_REJECTED; +} + + /* ============================================================================ * Vote Signature Verification * ============================================================================ */ @@ -776,105 +969,46 @@ void lantern_client_record_vote( const LanternSignedVote *vote, const char *peer_text) { - if (!client || !vote || !client->has_state) + (void)lantern_client_record_vote_internal(client, vote, peer_text, true, false); +} + + +void lantern_client_replay_pending_gossip_votes(struct lantern_client *client) +{ + if (!client || !client->has_state) { return; } - struct lantern_log_metadata meta = { - .validator = client->node_id, - .peer = (peer_text && *peer_text) ? peer_text : NULL, - }; + struct lantern_pending_vote_list pending; + pending_vote_list_init(&pending); bool state_locked = lantern_client_lock_state(client); if (!state_locked) { return; } - - struct lantern_vote_rejection_info rejection; - memset(&rejection, 0, sizeof(rejection)); - - LanternSignedVote vote_copy = *vote; - char head_hex[VOTE_ROOT_HEX_BUFFER_LEN]; - char target_hex[VOTE_ROOT_HEX_BUFFER_LEN]; - char source_hex[VOTE_ROOT_HEX_BUFFER_LEN]; - format_root_hex(&vote_copy.data.head.root, head_hex, sizeof(head_hex)); - format_root_hex(&vote_copy.data.target.root, target_hex, sizeof(target_hex)); - format_root_hex(&vote_copy.data.source.root, source_hex, sizeof(source_hex)); - lantern_log_debug( - "gossip", - &meta, - "received vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64, - vote_copy.data.validator_id, - vote_copy.data.slot, - head_hex[0] ? head_hex : "0x0", - target_hex[0] ? target_hex : "0x0", - vote_copy.data.target.slot); - - bool vote_processed = process_vote_locked(client, &vote_copy, &meta, &rejection); - if (vote_processed) + if (client->pending_gossip_votes.length == 0) { - lantern_log_info( - "gossip", - &meta, - "processed vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 - " source=%s@%" PRIu64, - vote_copy.data.validator_id, - vote_copy.data.slot, - head_hex[0] ? head_hex : "0x0", - target_hex[0] ? target_hex : "0x0", - vote_copy.data.target.slot, - source_hex[0] ? source_hex : "0x0", - vote_copy.data.source.slot); + lantern_client_unlock_state(client, state_locked); + return; } + + pending = client->pending_gossip_votes; + pending_vote_list_init(&client->pending_gossip_votes); lantern_client_unlock_state(client, state_locked); - lantern_client_note_vote_outcome(client, peer_text, &vote_copy, vote_processed); - if (!vote_processed) + + for (size_t i = 0; i < pending.length; ++i) { - const char *reason_text = rejection.has_reason ? rejection.message : "unknown"; - if (client->sync_in_progress) - { - lantern_log_debug( - "gossip", - &meta, - "rejected vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 - " source=%s@%" PRIu64 " reason=%s", - vote_copy.data.validator_id, - vote_copy.data.slot, - head_hex[0] ? head_hex : "0x0", - target_hex[0] ? target_hex : "0x0", - vote_copy.data.target.slot, - source_hex[0] ? source_hex : "0x0", - vote_copy.data.source.slot, - reason_text); - } - else - { - lantern_log_info( - "gossip", - &meta, - "rejected vote validator=%" PRIu64 " slot=%" PRIu64 " head=%s target=%s@%" PRIu64 - " source=%s@%" PRIu64 " reason=%s", - vote_copy.data.validator_id, - vote_copy.data.slot, - head_hex[0] ? head_hex : "0x0", - target_hex[0] ? target_hex : "0x0", - vote_copy.data.target.slot, - source_hex[0] ? source_hex : "0x0", - vote_copy.data.source.slot, - reason_text); - } - if (rejection.has_unknown_root && !lantern_root_is_zero(&rejection.unknown_root)) - { - char root_hex[VOTE_ROOT_HEX_BUFFER_LEN]; - format_root_hex(&rejection.unknown_root, root_hex, sizeof(root_hex)); - lantern_log_info( - "reqresp", - &meta, - "dropping vote unknown root=%s slot=%" PRIu64 " (no backfill)", - root_hex[0] ? root_hex : "0x0", - rejection.unknown_slot); - } + const char *peer_text = + pending.items[i].peer_text[0] ? pending.items[i].peer_text : NULL; + (void)lantern_client_record_vote_internal( + client, + &pending.items[i].vote, + peer_text, + false, + true); } + + pending_vote_list_reset(&pending); } diff --git a/tests/unit/client_test_helpers.c b/tests/unit/client_test_helpers.c index 884f518..eecb278 100644 --- a/tests/unit/client_test_helpers.c +++ b/tests/unit/client_test_helpers.c @@ -126,6 +126,10 @@ bool client_test_pending_contains_root(const struct lantern_client *client, cons } static void reset_vote_client_on_error(struct lantern_client *client) { + free(client->pending_gossip_votes.items); + client->pending_gossip_votes.items = NULL; + client->pending_gossip_votes.length = 0; + client->pending_gossip_votes.capacity = 0; if (client->has_fork_choice) { lantern_fork_choice_reset(&client->fork_choice); client->has_fork_choice = false; diff --git a/tests/unit/test_client_vote.c b/tests/unit/test_client_vote.c index 7564087..d8aed9e 100644 --- a/tests/unit/test_client_vote.c +++ b/tests/unit/test_client_vote.c @@ -3,12 +3,15 @@ #include #include #include +#include +#include #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/storage/storage.h" #include "lantern/support/time.h" /* Internal core APIs used for targeted cache and block-build regression tests. */ @@ -221,6 +224,81 @@ static int make_signed_vote_for_validator( return client_test_sign_vote_with_secret(out_vote, secret); } +static int build_signed_head_block( + struct lantern_client *client, + struct PQSignatureSchemeSecretKey *secret, + LanternSignedBlock *out_block, + LanternRoot *out_root) { + if (!client || !secret || !out_block || !out_root) { + return -1; + } + + int rc = -1; + LanternCheckpoint head = {0}; + LanternCheckpoint target = {0}; + LanternCheckpoint source = {0}; + LanternSignedVote proposer_vote; + memset(&proposer_vote, 0, sizeof(proposer_vote)); + + lantern_signed_block_with_attestation_init(out_block); + out_block->message.block.slot = client->state.slot + 1u; + if (lantern_proposer_for_slot( + out_block->message.block.slot, + client->state.config.num_validators, + &out_block->message.block.proposer_index) + != 0) { + goto cleanup; + } + if (lantern_state_select_block_parent( + &client->state, + &client->store, + &out_block->message.block.parent_root) + != 0) { + goto cleanup; + } + if (lantern_state_compute_vote_checkpoints( + &client->state, + &client->store, + &head, + &target, + &source) + != 0) { + goto cleanup; + } + + proposer_vote.data.validator_id = out_block->message.block.proposer_index; + proposer_vote.data.slot = out_block->message.block.slot; + proposer_vote.data.head = head; + proposer_vote.data.target = target; + proposer_vote.data.source = source; + if (client_test_sign_vote_with_secret(&proposer_vote, secret) != 0) { + goto cleanup; + } + + out_block->message.proposer_attestation = proposer_vote.data; + out_block->signatures.proposer_signature = proposer_vote.signature; + + if (lantern_state_preview_post_state_root( + &client->state, + &client->store, + out_block, + &out_block->message.block.state_root) + != 0) { + goto cleanup; + } + if (lantern_hash_tree_root_block(&out_block->message.block, out_root) != 0) { + goto cleanup; + } + + rc = 0; + +cleanup: + if (rc != 0) { + lantern_signed_block_with_attestation_reset(out_block); + } + return rc; +} + static int test_record_vote_accepts_known_roots(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -312,7 +390,7 @@ static int test_record_vote_accepts_known_roots(void) { return rc; } -static int test_record_vote_rejects_missing_target_state(void) { +static int test_record_vote_buffers_missing_target_state(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; struct PQSignatureSchemeSecretKey *secret = NULL; @@ -376,6 +454,10 @@ static int test_record_vote_rejects_missing_target_state(void) { fprintf(stderr, "vote with missing target state should not be stored\n"); goto cleanup; } + if (lantern_client_pending_vote_count(&client) != 1u) { + fprintf(stderr, "vote with missing target state should be buffered\n"); + goto cleanup; + } rc = 0; @@ -385,7 +467,7 @@ static int test_record_vote_rejects_missing_target_state(void) { return rc; } -static int test_record_vote_rejects_unknown_head(void) { +static int test_record_vote_buffers_unknown_head(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; struct PQSignatureSchemeSecretKey *secret = NULL; @@ -432,6 +514,10 @@ static int test_record_vote_rejects_unknown_head(void) { fprintf(stderr, "unknown head vote should not be stored\n"); goto cleanup; } + if (lantern_client_pending_vote_count(&client) != 1u) { + fprintf(stderr, "unknown head vote should be buffered\n"); + goto cleanup; + } if (client.fork_choice.new_votes && client.fork_choice.validator_count > 0) { for (size_t i = 0; i < client.fork_choice.validator_count; ++i) { @@ -449,6 +535,146 @@ static int test_record_vote_rejects_unknown_head(void) { return rc; } +static int test_record_vote_replays_buffered_vote_after_block_import(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + LanternSignedBlock grandchild_block; + LanternRoot grandchild_root; + LanternSignedVote vote; + char data_dir_template[256]; + int rc = 1; + + memset(&grandchild_block, 0, sizeof(grandchild_block)); + memset(&grandchild_root, 0, sizeof(grandchild_root)); + memset(&vote, 0, sizeof(vote)); + memset(data_dir_template, 0, sizeof(data_dir_template)); + + if (client_test_setup_vote_validation_client( + &client, + "vote_replay_after_import", + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + return 1; + } + + if (build_signed_head_block(&client, secret, &grandchild_block, &grandchild_root) != 0) { + fprintf(stderr, "failed to build grandchild block for buffered vote replay test\n"); + goto cleanup; + } + if (lantern_fork_choice_set_block_state( + &client.fork_choice, + &grandchild_block.message.block.parent_root, + &client.state) + != 0) { + fprintf(stderr, "failed to cache parent state for buffered vote replay test\n"); + goto cleanup; + } + if (snprintf( + data_dir_template, + sizeof(data_dir_template), + "/tmp/lantern_vote_replay_%02x%02x%02x%02x_%d", + child_root.bytes[0], + child_root.bytes[1], + child_root.bytes[2], + child_root.bytes[3], + (int)getpid()) + <= 0) { + fprintf(stderr, "failed to format temp dir for buffered vote replay test\n"); + goto cleanup; + } + client.data_dir = data_dir_template; + if (mkdir(client.data_dir, 0700) != 0) { + fprintf(stderr, "failed to create temp dir for buffered vote replay test\n"); + goto cleanup; + } + if (lantern_storage_store_state_for_root( + client.data_dir, + &grandchild_block.message.block.parent_root, + &client.state) + != 0) { + fprintf(stderr, "failed to persist parent state for buffered vote replay test\n"); + goto cleanup; + } + + vote.data.validator_id = 0u; + vote.data.slot = grandchild_block.message.block.slot; + vote.data.head.slot = grandchild_block.message.block.slot; + vote.data.head.root = grandchild_root; + vote.data.target.slot = grandchild_block.message.block.slot; + vote.data.target.root = grandchild_root; + vote.data.source.slot = 0u; + vote.data.source.root = anchor_root; + if (client_test_sign_vote_with_secret(&vote, secret) != 0) { + fprintf(stderr, "failed to sign buffered vote replay test fixture\n"); + goto cleanup; + } + + if (lantern_client_debug_record_vote(&client, &vote, "vote_replay_peer") != 0) { + fprintf(stderr, "failed to stage buffered vote for replay test\n"); + goto cleanup; + } + if (lantern_client_pending_vote_count(&client) != 1u) { + fprintf(stderr, "buffered replay test vote should be queued before import\n"); + goto cleanup; + } + if (lantern_store_validator_has_vote(&client.store, 0u)) { + fprintf(stderr, "buffered replay test vote should not be stored before import\n"); + goto cleanup; + } + + if (lantern_client_debug_import_block( + &client, + &grandchild_block, + &grandchild_root, + "vote_replay_peer") + != 1) { + fprintf(stderr, "failed to import grandchild block for buffered vote replay test\n"); + goto cleanup; + } + + if (lantern_client_pending_vote_count(&client) != 0u) { + fprintf(stderr, "buffered vote replay queue should be empty after import\n"); + goto cleanup; + } + if (!lantern_store_validator_has_vote(&client.store, 0u)) { + fprintf(stderr, "buffered vote should replay into store after import\n"); + goto cleanup; + } + + LanternSignedVote stored; + memset(&stored, 0, sizeof(stored)); + if (lantern_store_get_signed_validator_vote(&client.store, 0u, &stored) != 0) { + fprintf(stderr, "failed to fetch replayed buffered vote from store\n"); + goto cleanup; + } + if (memcmp(&stored.data, &vote.data, sizeof(vote.data)) != 0 + || memcmp(&stored.signature, &vote.signature, sizeof(vote.signature)) != 0) { + fprintf(stderr, "replayed buffered vote mismatch\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + if (client.data_dir && client.data_dir[0] != '\0') { + char cleanup_cmd[320]; + int written = snprintf(cleanup_cmd, sizeof(cleanup_cmd), "rm -rf %s", client.data_dir); + if (written > 0 && (size_t)written < sizeof(cleanup_cmd)) { + (void)system(cleanup_cmd); + } + client.data_dir = NULL; + } + lantern_signed_block_with_attestation_reset(&grandchild_block); + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + static int test_record_vote_rejects_slot_mismatch(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -2316,10 +2542,13 @@ int main(void) { if (test_record_vote_accepts_known_roots() != 0) { return 1; } - if (test_record_vote_rejects_missing_target_state() != 0) { + if (test_record_vote_buffers_missing_target_state() != 0) { + return 1; + } + if (test_record_vote_buffers_unknown_head() != 0) { return 1; } - if (test_record_vote_rejects_unknown_head() != 0) { + if (test_record_vote_replays_buffered_vote_after_block_import() != 0) { return 1; } if (test_record_vote_rejects_slot_mismatch() != 0) {